"""One OpenAI-compatible provider call, and the model listing beside it. Chat completions with function tools is the dialect every hosted provider and every local server speaks — OpenAI, Anthropic's compatibility endpoint, OpenRouter, Ollama, vLLM — so one client covers them without a provider SDK. The admin types the URL; the model never chooses where a request goes. """ from __future__ import annotations from typing import Any from urllib.parse import urlparse import httpx class ProviderError(Exception): """The provider answered, and the answer was a refusal.""" def check_base(api_base: str) -> None: parsed = urlparse(api_base) if parsed.scheme not in ("http", "https") or not parsed.hostname: raise ValueError("The provider URL must be an http(s) URL") def _headers(api_key: str) -> dict[str, str]: return {"Authorization": f"Bearer {api_key}"} def _detail(response: httpx.Response) -> str: try: error = response.json().get("error") except ValueError: error = None if isinstance(error, dict): error = error.get("message") text = str(error or response.text or response.reason_phrase) return f"{response.status_code}: {text[:300]}" async def chat_completion( *, api_base: str, api_key: str, model: str, messages: list[dict[str, Any]], tools: list[dict[str, Any]], timeout: float = 120.0, ) -> dict[str, Any]: """``POST {api_base}/chat/completions``, the whole answer at once.""" body: dict[str, Any] = {"model": model, "messages": messages} if tools: body["tools"] = tools async with httpx.AsyncClient(timeout=timeout) as http: response = await http.post( api_base.rstrip("/") + "/chat/completions", json=body, headers=_headers(api_key), ) if response.is_error: raise ProviderError(_detail(response)) result: dict[str, Any] = response.json() return result async def list_models(api_base: str, api_key: str, timeout: float = 15.0) -> list[str]: """The ids ``GET {api_base}/models`` offers, for the settings screen.""" async with httpx.AsyncClient(timeout=timeout) as http: response = await http.get( api_base.rstrip("/") + "/models", headers=_headers(api_key) ) if response.is_error: raise ProviderError(_detail(response)) data = response.json().get("data") or [] return sorted(str(m["id"]) for m in data if isinstance(m, dict) and "id" in m)