"""Minimal, independent OpenRouter chat-completions client. Deliberately does not depend on llm-connect's `OpenRouterAdapter`: its `execute_prompt(prompt: str, config)` takes a single prompt string, builds a fixed [system, user] message pair itself, and never surfaces `message.tool_calls` in its response — it cannot drive a multi-turn tool-calling loop without a breaking change to llm-connect's frozen Core `LLMAdapter` ABC (`execute_prompt` signature). Rather than force that change onto every llm-connect consumer, rein-openweights owns this thin transport directly. See glas-harness ADR-002. """ from __future__ import annotations from typing import Any import httpx _DEFAULT_API_BASE = "https://openrouter.ai/api/v1" class OpenRouterError(RuntimeError): pass class OpenRouterClient: def __init__( self, api_key: str, model: str, api_base: str = _DEFAULT_API_BASE, timeout: float = 120.0, ) -> None: self.api_key = api_key self.model = model self.api_base = api_base.rstrip("/") self.timeout = timeout def chat( self, messages: list[dict[str, Any]], tools: list[dict[str, Any]] | None = None, temperature: float = 0.2, max_tokens: int = 4096, ) -> dict[str, Any]: payload: dict[str, Any] = { "model": self.model, "messages": messages, "temperature": temperature, "max_tokens": max_tokens, } if tools: payload["tools"] = tools payload["tool_choice"] = "auto" response = httpx.post( f"{self.api_base}/chat/completions", json=payload, headers={"Authorization": f"Bearer {self.api_key}"}, timeout=self.timeout, ) if response.status_code >= 400: raise OpenRouterError(f"OpenRouter {response.status_code}: {response.text[:300]}") return response.json()