Files
AgapHost/openai/provision_litellm_keys.py
alvis b27d31b3ca openai: compose healthchecks + dependency ordering, registries, LiteLLM routing
docker-compose.yml gains healthchecks and depends_on/condition chains for the
litellm/langfuse/postgres tier so dependants wait for a genuinely ready
service instead of a started container. Also plumbs AGAP_MCP_TOKEN into the
adolf and adolf-llm containers, sourced from openai/.env (gitignored), for the
kb#180 bearer auth on the agap MCP server; shared-mcp.json consumes it via
bearerTokenEnvVar so the Kimi backbone authenticates too.

agent-registry.yaml / agent_registry.py: the version-controlled source of
truth for agent identities and trust classes -- the same ids the agap-mcp
token map resolves to (`adolf`, `claude-coder`; note `claude-code-cli` is the
runtime entry, not an agent identity).

model-registry.yaml, litellm-config.yaml, auto-router-routes.json and
provision_litellm_keys.py: model tiering, virtual-key provisioning and
auto-router routes. tei-reranker/ is the local reranker service backing
Hindsight recall.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 04:41:31 +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()