"""HTTP client for in-cluster / remote llm-connect (server mode). Mirrors activity-core's llm_client pattern: provider keys and model routing stay behind llm-connect; rein-aharness only sends prompts over HTTP. """ from __future__ import annotations import os from typing import Any import httpx _SAFE_RESPONSE_METADATA_KEYS = frozenset( { "finish_reason", "usage", "model", "model_name", "provider", "request_id", "response_id", "trace_id", "latency_ms", "duration_ms", "elapsed_ms", "created", "created_at", } ) _SAFE_ERROR_KEYS = ("error", "provider_status", "provider", "model") _MAX_ERROR_MESSAGE_CHARS = 400 class LLMConnectError(RuntimeError): pass class LLMConnectClient: """Synchronous client for llm-connect ``POST /execute``.""" def __init__(self, base_url: str, timeout_seconds: float = 300.0) -> None: self.base_url = base_url.rstrip("/") self.timeout_seconds = timeout_seconds self.last_response_metadata: dict[str, Any] | None = None def complete( self, prompt: str, *, model: str = "", config: dict[str, Any] | None = None, ) -> str: run_config = dict(config or {}) if model and "model_name" not in run_config: run_config["model_name"] = model run_config.setdefault("timeout_seconds", int(self.timeout_seconds)) payload: dict[str, Any] = {"prompt": prompt, "config": run_config} try: resp = httpx.post( f"{self.base_url}/execute", json=payload, timeout=self.timeout_seconds, ) except httpx.HTTPError as exc: raise LLMConnectError(f"llm-connect request failed: {exc}") from exc if resp.status_code >= 400: raise LLMConnectError(_llm_connect_error_text(resp)) try: data = resp.json() except ValueError as exc: raise LLMConnectError("llm-connect returned non-JSON body") from exc self.last_response_metadata = _extract_response_metadata(data) content = data.get("content") if not isinstance(content, str) or not content.strip(): raise LLMConnectError("llm-connect response missing string content") return content def _llm_connect_error_text(resp: httpx.Response) -> str: """Keep llm-connect's safe cause without copying an upstream response blob.""" base = f"llm-connect returned HTTP {resp.status_code}" try: body = resp.json() except ValueError: return base if not isinstance(body, dict): return base parts = [ f"{key}={body[key]}" for key in _SAFE_ERROR_KEYS if body.get(key) not in (None, "") ] message = body.get("message") if isinstance(message, str) and message.strip(): parts.append(f"message={message.strip()[:_MAX_ERROR_MESSAGE_CHARS]}") return f"{base}: " + "; ".join(parts) if parts else base def get_llm_connect_client() -> LLMConnectClient: base_url = os.environ.get("LLM_CONNECT_URL", "").strip() if not base_url: raise LLMConnectError( "LLM_CONNECT_URL is not set " "(e.g. http://llm-connect.activity-core.svc.cluster.local:8080)" ) timeout = float(os.environ.get("LLM_CONNECT_TIMEOUT_SECONDS", "300")) return LLMConnectClient(base_url, timeout) def _extract_response_metadata(data: dict[str, Any]) -> dict[str, Any]: out: dict[str, Any] = {} for key, value in data.items(): if key in _SAFE_RESPONSE_METADATA_KEYS and _json_safe(value): out[key] = value return out def _json_safe(value: Any) -> bool: try: import json json.dumps(value) except (TypeError, ValueError): return False return True