"""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; agent-harness 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", } ) 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, ) resp.raise_for_status() except httpx.HTTPError as exc: raise LLMConnectError(f"llm-connect request failed: {exc}") from exc 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 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