Source code for nlp_shap.backends.api.backend

"""OpenAI-compatible HTTP generative backend."""

import asyncio
import os
from typing import Any

from ...backends.mock.generation import generation_record_from_snapshot
from ...backends.transformers.chat import snapshot_has_audio, snapshot_to_chat_messages
from ...domain.conversation import ConversationSnapshot
from ...domain.generation import GenerationRecord
from ...errors import BackendUnavailableError
from ...pipeline.config import BackendConfig
from .payload import build_chat_payload, extract_message_content, payload_cache_key


[docs] class ApiBackend: """Generate text through an OpenAI-compatible ``/chat/completions`` endpoint.""" def __init__( self, config: BackendConfig, *, transport: Any | None = None, ) -> None: self._config = config self._transport = transport self._client: Any | None = None self._response_cache: dict[str, str] = {} self._init_lock = asyncio.Lock() @property def model_id(self) -> str: """Return the configured remote model identifier.""" return self._config.model_id
[docs] async def generate( self, snapshot: ConversationSnapshot, max_new_tokens: int, temperature: float, top_k: int, ) -> GenerationRecord: """Generate assistant text for ``snapshot`` via the HTTP API.""" if snapshot_has_audio(snapshot): msg = "audio snapshots are not supported by the API backend" raise ValueError(msg) messages = snapshot_to_chat_messages(snapshot) payload = build_chat_payload( self._config.model_id, messages, max_new_tokens, temperature, top_k, ) cache_key: str | None = None if temperature == 0.0: cache_key = payload_cache_key(payload) cached = self._response_cache.get(cache_key) if cached is not None: return generation_record_from_snapshot(cached, snapshot) text = await self._post_chat_completions(payload) if cache_key is not None: self._response_cache[cache_key] = text return generation_record_from_snapshot(text, snapshot)
[docs] async def aclose(self) -> None: """Close the underlying HTTP client.""" if self._client is not None: await self._client.aclose() self._client = None
async def _post_chat_completions(self, payload: dict[str, Any]) -> str: client = await self._ensure_client() url = f"{self._base_url()}/chat/completions" headers = self._headers() httpx = _import_httpx() try: response = await client.post(url, headers=headers, json=payload) response.raise_for_status() except httpx.HTTPError as exc: msg = "HTTP API generation request failed" raise BackendUnavailableError(msg) from exc data = response.json() if not isinstance(data, dict): msg = "HTTP API response was not a JSON object" raise BackendUnavailableError(msg) return extract_message_content(data) def _base_url(self) -> str: raw = ( self._config.api_host or os.environ.get("OPENAI_BASE_URL") or "http://127.0.0.1:1234/v1" ) return raw.rstrip("/") def _headers(self) -> dict[str, str]: headers = {"Content-Type": "application/json"} api_key = os.environ.get("OPENAI_API_KEY") or os.environ.get("NLP_SHAP_API_KEY") if api_key: headers["Authorization"] = f"Bearer {api_key}" return headers async def _ensure_client(self) -> Any: async with self._init_lock: if self._client is not None: return self._client httpx = _import_httpx() client_kwargs: dict[str, Any] = {"timeout": 600.0} if self._transport is not None: client_kwargs["transport"] = self._transport self._client = httpx.AsyncClient(**client_kwargs) return self._client
def _import_httpx() -> Any: try: import httpx except ImportError as exc: msg = "httpx package is required for the API backend extra" raise BackendUnavailableError(msg) from exc return httpx