Files
cayce 34a5f331ac
ci / lint-test-leaks (pull_request) Canceled after 0s
skeleton: package, schema, config, CLI, alembic, CI, deploy, docs
- 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)
2026-10-19 21:35:50 +00:00

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}")