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>
This commit is contained in:
@@ -153,6 +153,76 @@ def effective_card(registry, agent_id, model_registry=None):
|
||||
return card
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# litellm_key_spec — kb#147 (A2A-15): turn an agent's static registry fields
|
||||
# into the LiteLLM virtual-key grant provision_litellm_keys.py provisions.
|
||||
# "Grants live in the agent registry, not scattered configs" (kb#147 accept-
|
||||
# ance bar) means the model allow-list and budget are COMPUTED here from
|
||||
# preferred_tier + trust_class, never hand-typed per agent.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Ascending order matching model-registry.yaml's routing.tiers keys. An
|
||||
# agent may use its preferred tier and anything below it (a "large"-
|
||||
# preferring agent degrades to "small" gracefully; a "small"-only agent
|
||||
# never gets "large" — that asymmetry IS the sandboxed/trusted split this
|
||||
# key spec exists to enforce).
|
||||
_TIER_ORDER = ["small", "large"]
|
||||
|
||||
|
||||
def _reachable_tiers(preferred_tier):
|
||||
if preferred_tier not in _TIER_ORDER:
|
||||
return []
|
||||
return _TIER_ORDER[: _TIER_ORDER.index(preferred_tier) + 1]
|
||||
|
||||
|
||||
def litellm_key_spec(registry, agent_id, model_registry=None):
|
||||
"""Return the LiteLLM virtual-key grant for `agent_id`: which
|
||||
litellm_model_name values it may use and its default budget, derived
|
||||
from THIS registry's data (preferred_tier, trust_class) plus
|
||||
model-registry.yaml's routing.tiers/metered_opt_in — never hand-entered
|
||||
per agent. Models with no litellm_model_name (e.g. `kimi`, called
|
||||
directly via the adolf-llm wrapper, never through LiteLLM) are outside
|
||||
LiteLLM's enforcement surface by construction and are excluded, not
|
||||
silently allowed.
|
||||
|
||||
provision_litellm_keys.py consumes this dict's `models`/`max_budget`/
|
||||
`budget_duration`/`key_alias` as the body of a LiteLLM /key/generate (or
|
||||
/key/update) call. This function makes no network call itself.
|
||||
"""
|
||||
a = get_agent(registry, agent_id)
|
||||
model_registry = model_registry if model_registry is not None else mr.load_registry()
|
||||
grant = a.get("capability_grant") or {}
|
||||
key_alias = grant.get("litellm_key_alias", agent_id)
|
||||
|
||||
opted_in = set(model_registry.get("routing", {}).get("metered_opt_in", []) or [])
|
||||
opted_in_key = f"agent:{agent_id}"
|
||||
pools = model_registry.get("routing", {}).get("tiers", {})
|
||||
|
||||
models = []
|
||||
for tier in _reachable_tiers(a.get("preferred_tier")):
|
||||
for model_id in pools.get(tier, []):
|
||||
m = mr.get_model(model_registry, model_id)
|
||||
name = m.get("litellm_model_name")
|
||||
if not name:
|
||||
continue # not LiteLLM-routed (e.g. kimi's adolf-llm wrapper) -- nothing to grant/deny here
|
||||
if m.get("metered") and opted_in_key not in opted_in:
|
||||
continue # §3a: no metered API by default, per-key opt-in only
|
||||
if name not in models:
|
||||
models.append(name)
|
||||
|
||||
classes = registry.get("trust_classes", {})
|
||||
cls = classes.get(a["trust_class"], {})
|
||||
return {
|
||||
"agent_id": agent_id,
|
||||
"key_alias": key_alias,
|
||||
"trust_class": a["trust_class"],
|
||||
"models": models,
|
||||
"max_budget": cls.get("default_budget_usd"),
|
||||
"budget_duration": cls.get("budget_duration"),
|
||||
"mcp_auth_token_env": grant.get("mcp_auth_token_env"),
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI — manual verification only, not part of the library contract.
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -175,6 +245,9 @@ def main():
|
||||
p = sub.add_parser("can-reach-vault")
|
||||
p.add_argument("--id", required=True)
|
||||
|
||||
p = sub.add_parser("litellm-key-spec")
|
||||
p.add_argument("--id", required=True)
|
||||
|
||||
sub.add_parser("list")
|
||||
|
||||
args = ap.parse_args()
|
||||
@@ -192,6 +265,8 @@ def main():
|
||||
ok = can_reach_vault(reg, args.id)
|
||||
print(json.dumps({"id": args.id, "can_reach_vault": ok}))
|
||||
sys.exit(0 if ok else 1)
|
||||
elif args.cmd == "litellm-key-spec":
|
||||
print(json.dumps(litellm_key_spec(reg, args.id, model_reg), indent=2))
|
||||
elif args.cmd == "list":
|
||||
for a in reg["agents"]:
|
||||
backbone = a.get("backbone") or "-"
|
||||
|
||||
Reference in New Issue
Block a user