""" Minimal OpenRouter REST client (sync). Reads OPENROUTER_API_KEY / OPENROUTER_MODEL / OPENROUTER_BASE_URL from env. """ from __future__ import annotations import json import os from typing import Any, Dict, List, Optional import httpx class OpenRouterError(Exception): """Upstream or transport failure.""" def openrouter_chat_completion( *, api_key: str, model: str, user_content: str, system_content: Optional[str] = None, timeout_sec: float = 120.0, temperature: float = 0.25, site_url: Optional[str] = None, app_title: Optional[str] = None, ) -> str: """ Returns assistant message content (plain string). Caller validates empty responses. """ base = (os.getenv("OPENROUTER_BASE_URL") or "").strip().rstrip("/") or "https://openrouter.ai/api/v1" url = f"{base}/chat/completions" headers: Dict[str, str] = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", } referer = (site_url or os.getenv("APP_URL") or "").strip() if referer: headers["HTTP-Referer"] = referer title = (app_title or os.getenv("OPENROUTER_APP_TITLE") or "Shinkan Jinkendo").strip() if title: headers["X-Title"] = title messages: List[Dict[str, str]] = [] if system_content and str(system_content).strip(): messages.append({"role": "system", "content": str(system_content).strip()}) messages.append({"role": "user", "content": user_content}) payload: Dict[str, Any] = { "model": model, "messages": messages, "temperature": temperature, } try: with httpx.Client(timeout=timeout_sec) as client: resp = client.post(url, headers=headers, json=payload) except httpx.RequestError as e: raise OpenRouterError(str(e)) from e if resp.status_code >= 400: detail = "" try: j = resp.json() detail = ( str(j.get("error", {}).get("message")) if isinstance(j.get("error"), dict) else str(j.get("message") or j) ) except Exception: detail = (resp.text or "")[:600] raise OpenRouterError(f"HTTP {resp.status_code}: {detail}".strip()) try: data = resp.json() except json.JSONDecodeError as e: raise OpenRouterError("Ungueltige JSON-Antwort von OpenRouter") from e choices = data.get("choices") if isinstance(data, dict) else None if not choices or not isinstance(choices, list): raise OpenRouterError("OpenRouter: keine choices in Antwort") msg0 = choices[0] if choices else {} inner = msg0.get("message") if isinstance(msg0, dict) else None content = "" if isinstance(inner, dict): content = str(inner.get("content") or "") elif isinstance(inner, str): content = inner elif isinstance(msg0.get("content"), str): content = msg0.get("content") or "" return content.strip() def normalize_openrouter_env() -> tuple[str, str]: key = (os.getenv("OPENROUTER_API_KEY") or "").strip() model = (os.getenv("OPENROUTER_MODEL") or "anthropic/claude-sonnet-4").strip() return key, model