skeleton: package, schema, config, CLI, alembic, CI, deploy, docs
ci / lint-test-leaks (pull_request) Canceled after 0s
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)
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
"""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}")
|
||||
Reference in New Issue
Block a user