ci / lint-test-leaks (pull_request) Canceled after 0s
- cayceops core: env-only config (CAYCE_OPS_*), engine/session, ORM models with state machines (proposal/mail/content) and audited writes - provider-agnostic LLM interface (null provider default, stdlib openai-compat) - Typer CLI with lazy component loading (scanner/executor/maild/publisher/monitor) - alembic 0001 initial schema; sqlite-portable JSON columns - tests: 7 passing on throwaway sqlite; fixtures invented, never real data - ci: ruff + alembic smoke + pytest + gitleaks(full history) in .gitea/workflows + .github/workflows; local gate = make check - deploy: docker-compose (postgres 16) + hardened systemd units/timers - docs: DECISIONS D1-D9; README project (grant text retired)
81 lines
2.7 KiB
Python
81 lines
2.7 KiB
Python
"""LLM provider interface — provider-agnostic by design.
|
|
|
|
The default provider is ``null`` (no network calls, no LLM). Components must
|
|
degrade gracefully when the provider is null: rule-based fallbacks kick in.
|
|
|
|
Adding a provider = implementing ``LLMProvider.complete`` and registering it in
|
|
``get_provider``. Provider and model are selected exclusively via env vars
|
|
(``CAYCE_OPS_LLM_PROVIDER``, ``CAYCE_OPS_LLM_MODEL``).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import urllib.request
|
|
from typing import Protocol
|
|
|
|
|
|
class LLMProvider(Protocol):
|
|
name: str
|
|
|
|
def complete(self, prompt: str, *, system: str = "", timeout: float = 30.0) -> str:
|
|
"""Return the model's completion for the prompt. Raises on failure."""
|
|
... # pragma: no cover
|
|
|
|
|
|
class NullProvider:
|
|
"""No-op provider: never touches the network. Signals 'no LLM available'."""
|
|
|
|
name = "null"
|
|
|
|
def complete(self, prompt: str, *, system: str = "", timeout: float = 30.0) -> str:
|
|
raise RuntimeError("LLM provider is 'null': no LLM configured")
|
|
|
|
|
|
class OpenAICompatProvider:
|
|
"""Minimal OpenAI-compatible /chat/completions client (stdlib urllib only).
|
|
|
|
Works with OpenAI, OpenRouter, local llama.cpp/vLLM servers, etc.
|
|
"""
|
|
|
|
name = "openai-compat"
|
|
|
|
def __init__(self, base_url: str, api_key: str, model: str) -> None:
|
|
self.base_url = base_url.rstrip("/")
|
|
self.api_key = api_key
|
|
self.model = model
|
|
|
|
def complete(self, prompt: str, *, system: str = "", timeout: float = 30.0) -> str:
|
|
payload = {
|
|
"model": self.model,
|
|
"messages": (
|
|
([{"role": "system", "content": system}] if system else [])
|
|
+ [{"role": "user", "content": prompt}]
|
|
),
|
|
"temperature": 0.0,
|
|
}
|
|
req = urllib.request.Request(
|
|
f"{self.base_url}/chat/completions",
|
|
data=json.dumps(payload).encode(),
|
|
headers={
|
|
"Content-Type": "application/json",
|
|
"Authorization": f"Bearer {self.api_key}",
|
|
},
|
|
method="POST",
|
|
)
|
|
with urllib.request.urlopen(req, timeout=timeout) as resp: # noqa: S310 (base_url is operator-configured)
|
|
body = json.loads(resp.read().decode())
|
|
return body["choices"][0]["message"]["content"]
|
|
|
|
|
|
def get_provider(cfg=None) -> LLMProvider:
|
|
"""Build the provider named by CAYCE_OPS_LLM_PROVIDER (default: null)."""
|
|
from cayceops.config import load
|
|
|
|
cfg = cfg or load()
|
|
if cfg.llm_provider == "null":
|
|
return NullProvider()
|
|
if cfg.llm_provider == "openai-compat":
|
|
return OpenAICompatProvider(cfg.llm_base_url, cfg.llm_api_key, cfg.llm_model)
|
|
raise ValueError(f"unknown LLM provider: {cfg.llm_provider!r}")
|