49. The model catalogue is declarative data, and routing is an explainable projection of it
Context
The AI workers of this repository run on models, and the repository knew nothing about
them. Which models exist, what each can do, how large its context is, which name is an
alias of which — those facts lived in the workers' own heads and in scattered prose,
which is precisely the shape (a hand-maintained truth per consumer) every other part
of this codebase refuses. The capture schema declares optional model and tokens
fields nobody fills; docs/ECONOMICS.md names the missing provider-side telemetry;
sessions record a free-text worker.
Two standing decisions constrain any fix. ADR 0024/0032 spent the word "provider" on the AI client tools (Claude Code, Codex, Gemini CLI) and defend it: a model vendor must be a different noun. And ADR 0032 keeps HTTP clients, TLS, async runtimes and SDKs out of the crate — so a "registry" that live-probes vendors, discovers models over their APIs, or measures health by calling them is out of the question here.
Decision
A declarative model catalogue, exactly in the grain of share/providers.yaml:
- One declaration.
share/models.yamldeclares vendors (the parties that serve models — the word "provider" stays spent) and models: canonical id, the vendor's native id, aliases, typed capability words, context window, lifecycle status, one optional note. Declaration order is meaningful: it is routing's preference order. - Facts carry provenance or stay out. Entries state what a named reference could
verify (the shipped entries: the Anthropic model reference as cached on a stated
date). Pricing is deliberately absent — a price the tool cannot verify live is a
fact it must not state — and vendors nobody verified are absent rather than
guessed.
credential_envnames an environment variable; only its presence is ever reported, no code path reads the value, and the catalogue's schema has no field a secret could hide in (a test proves the property). - Two capabilities, every surface.
models.list(the catalogue, narrowable, with the catalogue's own diagnostics — duplicate aliases, undeclared vendors) andmodels.route(which model a stated need selects). Onecapability!declaration each, so the CLI (majordomus models ...),/api/v1/models*, OpenAPI, the MCP tools and the Cockpit's Models page are projections that cannot disagree, and no consumer keeps a model name of its own. - Routing is a pure function with its reasons attached. Input: required capability words, minimum context, vendor, local-only, or a model named outright (checked against the requirements all the same). Output: the selected model and why, the qualifying rest as the fallback chain, and every excluded model with the first check it failed. No state, no health guesses, no opaque dispatcher: the same question gets the same answer, and "why did this pick X / exclude Z" is in the answer itself.
- No live layer, by design. Nothing calls a vendor, lists models over an API, or probes health. Where a live fact is someday wanted, ADR 0032's precedent stands (an outside layer writes; the crate reads files) or a new ADR revisits the dependency posture explicitly. Recording which model actually executed a session's work belongs to the capture/lifecycle adapters (their schema already declares the fields) — provider-hook work, named here as the follow-up, not smuggled into this crate.
Alternatives rejected
- A live provider/model registry with health and usage. Forbidden twice over: the crate has no HTTP client and must not gain one for this (ADR 0032), and a health answer computed from probes this tool cannot make would be fabricated. What can be true here is declared data plus presence-of-credential; what cannot is marked absent, not invented.
- Reusing the word "provider". ADR 0032 rejects it by name; a second registry
under the same noun as
share/providers.yamlis the ambiguity it warned against. - Routing policy as scattered call-site logic. The if/else-per-caller shape is the defect; a pure function over declared data, projected as a capability, is the repair.
- A per-repository catalogue under
.ai/. Model facts are facts about the world the distribution ships into, not about one repository; the share directory is where such declarations live (providers.yaml,distribution.yaml). A repository needing its own additions is a later, explicit extension of the share resolution, not a second file format now.
Consequences
- Adding a model or vendor is editing
share/models.yaml; every surface follows, andmodels.list's diagnostics catch a duplicate alias or an undeclared vendor at the first read. Nothing else is edited — the zero-per-consumer-registration property the tests hold for the mesh holds here by the same mechanism (one declaration, one registry build, derived projections). - The catalogue can go stale, and honestly: entries carry their provenance date in the file's own header, staleness is visible where it can be judged, and no auto-refresh exists to silently rewrite facts.
- The mesh's capability exchange can carry sanitized model-capability awareness
later (a node advertising
reasoningorlocalinference) by reading this same catalogue — one truth for what a fleet's nodes can do, no credentials anywhere near it.