Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a02b6f-7db1-7222-918b-e813a6bda38d
127 lines
3.8 KiB
Python
127 lines
3.8 KiB
Python
"""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
|