from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from typing import Any import requests from models.base import BackendStatus from models.model_catalog import ModelInfo from models.response_parsing import extract_chat_response from models.server_helpers import request_openai_chat_text @dataclass(frozen=True) class OpenAICompatibleConfig: base_url: str = "http://127.0.0.1:1234" api_key: str = "lm-studio" model_name: str = "" temperature: float = 0.7 max_tokens: int = 512 timeout_seconds: float = 120 @property def api_base(self) -> str: return self.base_url.rstrip("/") def request_model(self, fallback: str) -> str: return self.model_name.strip() or fallback class OpenAICompatibleService: """Local OpenAI-compatible chat client for LM Studio, vLLM, or similar servers.""" def __init__( self, model: ModelInfo, config: OpenAICompatibleConfig | None = None, get_func: Callable[..., requests.Response] = requests.get, post_func: Callable[..., requests.Response] = requests.post, ) -> None: self.model = model self.config = config or OpenAICompatibleConfig() self.get_func = get_func self.post_func = post_func @staticmethod def status( base_url: str = "http://127.0.0.1:1234", get_func: Callable[..., requests.Response] = requests.get, ) -> BackendStatus: url = base_url.rstrip("/") try: response = get_func(f"{url}/v1/models", timeout=2) response.raise_for_status() except requests.RequestException as exc: return BackendStatus( "openai-compatible", False, f"OpenAI-compatible local server is not reachable at {url}: {exc}", ) return BackendStatus( "openai-compatible", True, f"OpenAI-compatible local server is reachable at {url}.", ) def chat(self, system_prompt: str, user_prompt: str) -> str: status = self.status(self.config.api_base, self.get_func) if not status.available: return ( "[OpenAI-compatible backend unavailable]\n\n" f"{status.detail}\n\n" "Start LM Studio or another local OpenAI-compatible server before retrying." ) return request_openai_chat_text( post_func=self.post_func, base_url=self.config.api_base, model=self.config.request_model(self.model.hf_id), system_prompt=system_prompt, user_prompt=user_prompt, temperature=self.config.temperature, max_tokens=self.config.max_tokens, timeout_seconds=self.config.timeout_seconds, failure_label="OpenAI-compatible", headers={"Authorization": f"Bearer {self.config.api_key}"}, ) @staticmethod def _extract_response(data: dict[str, Any]) -> str: return extract_chat_response(data)