feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from .config import Settings
|
||||
from .cost import record_usage
|
||||
from .errors import LLMError
|
||||
|
||||
logger = logging.getLogger("devii.llm")
|
||||
|
||||
|
||||
class LLMClient:
|
||||
def __init__(self, settings: Settings) -> None:
|
||||
self._settings = settings
|
||||
self._client = httpx.AsyncClient(
|
||||
timeout=settings.timeout_seconds,
|
||||
headers={
|
||||
"Authorization": f"Bearer {settings.ai_key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
)
|
||||
|
||||
async def aclose(self) -> None:
|
||||
await self._client.aclose()
|
||||
|
||||
async def complete(
|
||||
self,
|
||||
messages: list[dict[str, Any]],
|
||||
tools: list[dict[str, Any]],
|
||||
) -> dict[str, Any]:
|
||||
payload = {
|
||||
"model": self._settings.ai_model,
|
||||
"messages": messages,
|
||||
"tools": tools,
|
||||
"tool_choice": "auto",
|
||||
}
|
||||
try:
|
||||
logger.debug("LLM request with %d messages", len(messages))
|
||||
response = await self._client.post(self._settings.ai_url, json=payload)
|
||||
except httpx.HTTPError as exc:
|
||||
raise LLMError(f"Could not reach the model endpoint: {exc}") from exc
|
||||
|
||||
if response.status_code >= 400:
|
||||
raise LLMError(
|
||||
f"Model endpoint returned {response.status_code}: {self._reason(response)}",
|
||||
status=response.status_code,
|
||||
body=response.text[:500],
|
||||
)
|
||||
|
||||
try:
|
||||
data = response.json()
|
||||
except ValueError as exc:
|
||||
raise LLMError("Model endpoint returned invalid JSON.") from exc
|
||||
|
||||
choices = data.get("choices")
|
||||
if not choices:
|
||||
raise LLMError("Model response contained no choices.", body=str(data)[:500])
|
||||
|
||||
message = choices[0].get("message")
|
||||
if message is None:
|
||||
raise LLMError("Model response contained no message.", body=str(data)[:500])
|
||||
|
||||
record_usage(data.get("usage"))
|
||||
logger.debug("LLM response received (tool_calls=%s)", bool(message.get("tool_calls")))
|
||||
return message
|
||||
|
||||
async def summarize(self, text: str) -> str:
|
||||
payload = {
|
||||
"model": self._settings.ai_model,
|
||||
"messages": [
|
||||
{"role": "system", "content": "You are a precise technical summarizer."},
|
||||
{"role": "user", "content": text},
|
||||
],
|
||||
"temperature": 0.0,
|
||||
}
|
||||
try:
|
||||
response = await self._client.post(self._settings.ai_url, json=payload)
|
||||
except httpx.HTTPError as exc:
|
||||
raise LLMError(f"Could not reach the model endpoint: {exc}") from exc
|
||||
if response.status_code >= 400:
|
||||
raise LLMError(f"Model endpoint returned {response.status_code}: {self._reason(response)}")
|
||||
try:
|
||||
data = response.json()
|
||||
content = data["choices"][0]["message"]["content"] or ""
|
||||
except (ValueError, KeyError, IndexError) as exc:
|
||||
raise LLMError("Model summarization returned an unexpected response.") from exc
|
||||
record_usage(data.get("usage"))
|
||||
return content
|
||||
|
||||
@staticmethod
|
||||
def _reason(response: httpx.Response) -> str:
|
||||
try:
|
||||
error = response.json().get("error")
|
||||
except ValueError:
|
||||
return response.text[:200] or response.reason_phrase
|
||||
if isinstance(error, dict):
|
||||
return str(error.get("message", error))
|
||||
if error:
|
||||
return str(error)
|
||||
return response.reason_phrase
|
||||
Reference in New Issue
Block a user