Add activity-core LLM endpoint support
Some checks failed
CI / test (3.10) (push) Has been cancelled
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled

This commit is contained in:
2026-06-07 19:24:45 +02:00
parent 1d9fc107ed
commit 14ba47c129
25 changed files with 2082 additions and 18 deletions

View File

@@ -35,7 +35,16 @@ from urllib.parse import parse_qs, urlsplit
from llm_connect._diagnostics import capture_diagnostics
from llm_connect.adapter import LLMAdapter
from llm_connect.exceptions import (
LLMBudgetExceededError,
LLMAPIError,
LLMConfigurationError,
LLMError,
LLMRateLimitError,
LLMTimeoutError,
)
from llm_connect.models import LLMResponse, RunConfig
from llm_connect.profiles import ProfiledLLMAdapter, default_runtime_profiles
class _Handler(BaseHTTPRequestHandler):
@@ -86,7 +95,13 @@ class _Handler(BaseHTTPRequestHandler):
diagnostics_enabled = debug_enabled or bool(audit_dir)
try:
with capture_diagnostics(diagnostics_enabled) as diagnostics:
response = self.server.adapter.execute_prompt(prompt, config) # type: ignore[attr-defined]
adapter = self.server.adapter # type: ignore[attr-defined]
if not adapter.validate_config(config):
raise LLMConfigurationError(
"Adapter rejected RunConfig",
context={"model_name": config.model_name},
)
response = adapter.execute_prompt(prompt, config)
latency = time.time() - start
body = response.to_dict()
debug = diagnostics.to_dict() if diagnostics is not None else None
@@ -96,7 +111,8 @@ class _Handler(BaseHTTPRequestHandler):
_write_audit_record(audit_dir, prompt, config, response, debug, latency)
self._respond(200, body)
except Exception as exc:
self._respond(500, {"error": str(exc)})
status, body = _error_response(exc)
self._respond(status, body)
# ── helpers ────────────────────────────────────────────────────
@@ -155,9 +171,23 @@ class LLMServer:
# ── CLI entry point ────────────────────────────────────────────────────────────
def _build_adapter(provider: str, model: Optional[str]) -> LLMAdapter:
def _build_adapter(
provider: str,
model: Optional[str],
*,
enable_profiles: bool = True,
strict_profiles: bool = False,
) -> LLMAdapter:
from llm_connect.factory import create_adapter
return create_adapter(provider, model=model)
adapter = create_adapter(provider, model=model)
if not enable_profiles:
return adapter
return ProfiledLLMAdapter(
adapter,
default_runtime_profiles(provider=provider, model=model),
strict_profiles=strict_profiles,
)
def _debug_requested(query: str) -> bool:
@@ -172,6 +202,76 @@ def _truthy(value: str) -> bool:
return value.strip().lower() in {"1", "true", "yes", "on"}
def _error_response(exc: Exception) -> tuple[int, dict]:
"""Map exceptions to operator-useful, secret-safe server responses."""
if isinstance(exc, LLMRateLimitError):
body = _error_body("provider_rate_limited", exc)
body["provider_status"] = exc.status_code
return 429, body
if isinstance(exc, LLMTimeoutError):
return 504, _error_body("provider_timeout", exc)
if isinstance(exc, LLMAPIError):
body = _error_body("provider_api_error", exc)
if exc.status_code:
body["provider_status"] = exc.status_code
return 502, body
if isinstance(exc, LLMBudgetExceededError):
return 400, _error_body("budget_exceeded", exc)
if isinstance(exc, LLMConfigurationError):
if _message(exc).startswith("Unknown LLM runtime profile"):
return 400, _error_body("unknown_profile", exc)
return 500, _error_body("configuration_error", exc)
if isinstance(exc, LLMError):
return 500, _error_body("llm_error", exc)
return 500, _error_body("internal_error", exc)
def _error_body(code: str, exc: Exception) -> dict:
body = {
"error": code,
"message": _sanitize_text(_message(exc)),
"type": exc.__class__.__name__,
}
context = getattr(exc, "context", None)
if isinstance(context, dict):
safe_context = _safe_context(context)
if safe_context:
body["context"] = safe_context
return body
def _message(exc: Exception) -> str:
if exc.args:
return str(exc.args[0])
return str(exc)
def _safe_context(context: dict) -> dict:
safe = {}
for key, value in context.items():
lowered = str(key).lower()
if any(secret_word in lowered for secret_word in ("key", "secret", "token", "password")):
safe[key] = "<redacted>"
elif isinstance(value, (str, int, float, bool)) or value is None:
safe[key] = _sanitize_text(str(value)) if isinstance(value, str) else value
else:
safe[key] = _sanitize_text(str(value))
return safe
def _sanitize_text(value: str) -> str:
value = re.sub(r"Bearer\s+[A-Za-z0-9._~+/=-]+", "Bearer <redacted>", value)
value = re.sub(r"([?&]key=)[^&\s]+", r"\1<redacted>", value)
value = re.sub(r"\bsk-[A-Za-z0-9_-]{8,}", "sk-<redacted>", value)
value = re.sub(
r"(?i)(api[_-]?key|token|secret|password)=([^,\s\]]+)",
r"\1=<redacted>",
value,
)
return value
def _write_audit_record(
audit_dir: str,
prompt: str,
@@ -214,13 +314,46 @@ def main(argv=None) -> None:
prog="python -m llm_connect.server",
description="Start llm_connect HTTP serve mode.",
)
parser.add_argument("--port", type=int, default=8080, help="TCP port (default: 8080)")
parser.add_argument("--host", default="127.0.0.1", help="Bind address (default: 127.0.0.1)")
parser.add_argument("--provider", default="mock", help="Provider name passed to create_adapter")
parser.add_argument("--model", default=None, help="Model name (optional)")
parser.add_argument(
"--port",
type=int,
default=int(os.environ.get("LLM_CONNECT_PORT", "8080")),
help="TCP port (default: env LLM_CONNECT_PORT or 8080)",
)
parser.add_argument(
"--host",
default=os.environ.get("LLM_CONNECT_HOST", "127.0.0.1"),
help="Bind address (default: env LLM_CONNECT_HOST or 127.0.0.1)",
)
parser.add_argument(
"--provider",
default=os.environ.get("LLM_CONNECT_PROVIDER", "mock"),
help="Provider name passed to create_adapter (default: env LLM_CONNECT_PROVIDER or mock)",
)
parser.add_argument(
"--model",
default=os.environ.get("LLM_CONNECT_MODEL") or None,
help="Model name (default: env LLM_CONNECT_MODEL, optional)",
)
parser.add_argument(
"--disable-profiles",
action="store_true",
help="Disable server runtime profile dispatch.",
)
parser.add_argument(
"--strict-profiles",
action="store_true",
default=_truthy(os.environ.get("LLM_CONNECT_STRICT_PROFILES", "")),
help="Reject non-profile model_name values instead of passing them through.",
)
args = parser.parse_args(argv)
adapter = _build_adapter(args.provider, args.model)
adapter = _build_adapter(
args.provider,
args.model,
enable_profiles=not args.disable_profiles,
strict_profiles=args.strict_profiles,
)
server = LLMServer(adapter=adapter, host=args.host, port=args.port)
print(f"llm_connect server listening on http://{args.host}:{args.port}")
try: