"""OpenAI-compatible chat-completions backend. Covers OpenAI / Azure OpenAI / DeepSeek / Moonshot / Qwen-cloud / Ollama / vLLM / LM Studio — anything that exposes `POST {endpoint}/chat/completions`. We avoid the openai SDK to keep the plugin's dependency surface to stdlib + pyyaml. Uses urllib so even environments without `requests` work. """ from __future__ import annotations import json import urllib.error import urllib.request from dataclasses import dataclass @dataclass class OpenAICompatibleBackend: cfg: dict @property def endpoint(self) -> str: return self.cfg["endpoint"].rstrip("/") @property def model(self) -> str: return self.cfg["model"] @property def api_key(self) -> str: return self.cfg.get("api_key") or "missing-key" @property def max_output_tokens(self) -> int: return int(self.cfg.get("max_output_tokens", 4000)) @property def timeout(self) -> int: return int(self.cfg.get("timeout_seconds", 60)) def summarise(self, system_prompt: str, conversation_text: str) -> str: url = f"{self.endpoint}/chat/completions" body = { "model": self.model, "max_tokens": self.max_output_tokens, "temperature": 0.2, "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": conversation_text}, ], } data = json.dumps(body).encode("utf-8") req = urllib.request.Request( url, data=data, headers={ "Content-Type": "application/json", "Authorization": f"Bearer {self.api_key}", }, method="POST", ) try: with urllib.request.urlopen(req, timeout=self.timeout) as resp: payload = json.loads(resp.read().decode("utf-8")) except urllib.error.HTTPError as e: detail = e.read().decode("utf-8", errors="replace")[:500] raise RuntimeError(f"LLM HTTP {e.code}: {detail}") from e except urllib.error.URLError as e: raise RuntimeError(f"LLM connection failed: {e.reason}") from e choices = payload.get("choices") or [] if not choices: raise RuntimeError(f"LLM returned no choices: {payload}") msg = choices[0].get("message") or {} content = msg.get("content") if not isinstance(content, str) or not content.strip(): raise RuntimeError(f"LLM returned empty content: {payload}") return content.strip()