generated from coulomb/repo-seed
Add activity-core LLM endpoint support
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user