# retoor from __future__ import annotations import logging from typing import Any, Callable import httpx from devplacepy import stealth from .config import Settings from .cost import record_cost, record_usage from .errors import LLMError logger = logging.getLogger("devii.llm") class LLMClient: def __init__( self, settings: Settings, key_resolver: Callable[[], str] | None = None, ) -> None: self._settings = settings self._key_resolver = key_resolver self._client = stealth.stealth_async_client( 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 _post(self, payload: dict[str, Any]) -> httpx.Response: response = await self._client.post(self._settings.ai_url, json=payload) if response.status_code == 401 and self._refresh_key(): response = await self._client.post(self._settings.ai_url, json=payload) return response def _refresh_key(self) -> bool: if self._key_resolver is None: return False try: key = self._key_resolver() except Exception: # noqa: BLE001 - refresh is best-effort; the original 401 still surfaces logger.exception("Credential refresh failed after 401") return False if not key or self._client.headers.get("Authorization") == f"Bearer {key}": return False self._client.headers["Authorization"] = f"Bearer {key}" logger.info("Refreshed model endpoint credentials after 401") return True 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._post(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")) self._record_native_cost(response) logger.debug( "LLM response received (tool_calls=%s)", bool(message.get("tool_calls")) ) return message async def complete_text( self, messages: list[dict[str, Any]], temperature: float = 0.0 ) -> str: payload = { "model": self._settings.ai_model, "messages": messages, "temperature": temperature, } try: response = await self._post(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 endpoint returned an unexpected response.") from exc record_usage(data.get("usage")) self._record_native_cost(response) return content 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._post(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")) self._record_native_cost(response) return content @staticmethod def _record_native_cost(response: httpx.Response) -> None: raw = response.headers.get("X-Gateway-Cost-USD") if raw is None: return try: record_cost(float(raw)) except (TypeError, ValueError): return @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