Files
AgapHost/ai/provision_litellm_keys.py
alvis 9094d71e2f ai: migrate LLM backbone from Kimi CLI to Codex CLI
Retires the Moonshot/Kimi subscription in favour of the already-paid ChatGPT
plan. Both CLI wrappers now run `codex exec`; the kimi-agent container is gone.

adolf-llm + hindsight-llm:
- runKimi -> runCodex (`codex exec --json --skip-git-repo-check`), resume via
  `codex exec resume <thread_id>`.
- MCP moves from a per-session .mcp.json (a workaround for Kimi having no
  --mcp-config-file flag) to a $CODEX_HOME/config.toml generated once at
  startup from shared-mcp.json. Field translation is load-bearing:
  bearerTokenEnvVar -> bearer_token_env_var, enabledTools -> enabled_tools.
- approval_policy="never" + sandbox_mode required, or unattended turns block
  on an approval prompt nobody can answer.

kimi-agent removed. It was the ONLY large-tier deployment behind LiteLLM, so
deleting it outright would have silently degraded every large-tier request to
the local 4B model via the existing fallbacks. tier-large, the auto_router
complex-reasoning route and their fallbacks now point at the codex-backed
adolf-llm wrapper (model_name: codex-agent).

Three environment blockers fixed along the way:
- OpenAI geo-blocks this host (403 unsupported_country_region_territory).
  Both containers now egress via the host xray proxy, with NO_PROXY keeping
  MCP and *.alogins.net traffic off the tunnel.
- node:22-slim ships no system CA store; the Rust codex binary validates TLS
  against it, so every HTTPS call failed with a generic transport error while
  Node's own fetch worked. ca-certificates added to both images.
- `codex exec resume` rejects -C/--cd (plain `codex exec` accepts it), which
  broke follow-up turns while first turns succeeded.

Known regression: Kimi's managed-usage API has no Codex equivalent, so the
/usage route returns 501 and there is no quota probe for the codex model.
The two quota plugins degrade quietly to no output.

Also: stop tracking cognee.env (live LLM + JWT secrets) and gitignore it.
The secrets remain in earlier history and should be rotated.

Verified live: plain turn, SSE streaming, session resume, MCP tool call,
bearer-token MCP call, and completions through both LiteLLM routes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Y5QPagv4iun1ghpwM96Ff
2026-08-01 06:13:27 +00:00

109 lines
4.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""provision_litellm_keys — kb#147 (A2A-15): turn agent-registry.yaml grants
into real LiteLLM virtual keys.
This is the ONE place a capability grant (model allow-list + budget) crosses
from data (agent-registry.yaml, version-controlled) into a live LiteLLM
key (via the proxy's /key/generate or /key/update admin API, master-key
authenticated). It deliberately does nothing destructive: --dry-run (the
default) only computes and prints the payload each agent WOULD get, making
zero network calls. --apply is required to actually create/update a key,
and needs LITELLM_MASTER_KEY in the environment (never hardcoded here, never
committed) — this is a privileged write against a live production service,
so it is not something this task runs unattended; --apply is the kb#147
handover step for a human/approved run.
Usage:
# Safe, run-anytime: print what each agent's key WOULD look like.
./provision_litellm_keys.py --dry-run
./provision_litellm_keys.py --dry-run --id torgash
# Privileged, requires explicit opt-in + master key (kb#147 handover):
LITELLM_MASTER_KEY=sk-... ./provision_litellm_keys.py --apply --id adolf
"""
import argparse
import json
import os
import sys
import urllib.error
import urllib.request
import agent_registry as ar
import model_registry as mr
LITELLM_BASE_URL = os.environ.get("LITELLM_BASE_URL", "http://localhost:4000")
def agent_ids_with_grants(registry):
return [a["id"] for a in registry["agents"] if a.get("capability_grant")]
def _http_post(path, payload, master_key):
req = urllib.request.Request(
f"{LITELLM_BASE_URL}{path}",
data=json.dumps(payload).encode(),
headers={
"Authorization": f"Bearer {master_key}",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(req, timeout=10) as resp:
return json.loads(resp.read())
def apply_key(spec, master_key):
"""Create (or update, if key_alias already exists) a LiteLLM virtual key
matching `spec` (the dict returned by agent_registry.litellm_key_spec).
Raises on any HTTP error rather than swallowing it — a failed grant
should never look like a successful one."""
payload = {
"key_alias": spec["key_alias"],
"models": spec["models"],
"max_budget": spec["max_budget"],
"budget_duration": spec["budget_duration"],
"metadata": {"agent_id": spec["agent_id"], "trust_class": spec["trust_class"], "source": "kb#147 agent-registry.yaml"},
}
try:
return _http_post("/key/generate", payload, master_key)
except urllib.error.HTTPError as e:
body = e.read().decode(errors="replace")
raise SystemExit(f"LiteLLM /key/generate failed for {spec['key_alias']}: {e.code} {body}")
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--registry", default=None)
ap.add_argument("--model-registry", default=None)
ap.add_argument("--id", default=None, help="only this agent id (default: every agent with a capability_grant)")
mode = ap.add_mutually_exclusive_group()
mode.add_argument("--dry-run", action="store_true", default=True, help="default: compute + print only, no network call")
mode.add_argument("--apply", action="store_true", help="actually call LiteLLM /key/generate (needs LITELLM_MASTER_KEY) -- privileged, kb#147 handover step")
args = ap.parse_args()
reg = ar.load_registry(args.registry)
model_reg = mr.load_registry(args.model_registry)
ids = [args.id] if args.id else agent_ids_with_grants(reg)
if not ids:
print("no agents with a capability_grant in the registry", file=sys.stderr)
sys.exit(1)
master_key = os.environ.get("LITELLM_MASTER_KEY")
if args.apply and not master_key:
print("error: --apply requires LITELLM_MASTER_KEY in the environment", file=sys.stderr)
sys.exit(2)
for agent_id in ids:
spec = ar.litellm_key_spec(reg, agent_id, model_reg)
if args.apply:
result = apply_key(spec, master_key)
print(json.dumps({"agent_id": agent_id, "key_alias": spec["key_alias"], "applied": True, "litellm_response_keys": list(result.keys())}))
else:
print(json.dumps({"mode": "dry-run", **spec}, indent=2))
if __name__ == "__main__":
main()