Inside the discovery
This page is for people working on the runtime itself. What the discovery is for, how to run it and what it keeps of a deployment's file are in the guide; this page holds the walk and the merge rule by rule. Nothing here runs in the serving path - this is the one corner of the models family that depends on the llm package's clients, because listing and pinging require them.
| Class | Role |
|---|---|
| CatalogDiscovery | The action (main: [--out models.json] [--report-only] [--include-older]): walk, list, ping, merge, validate, write |
| ModelDiscovery | Implemented by a ClientProvider that can list its account's reachable models (each provider knows its own listing API) |
| DiscoveredModel | One listed model as the provider reported it: wire id, modalities in the provider's words, whether the id is invokable as listed, lifecycle flag, retention posture, quota facts |
| CatalogPing | The probe recipe without the job: one call per entry, harvesting limits from response headers and classifying failures; also the recipe a deployment's availability probe reuses |
| ModelClassifier | The strongest callable model proposing the entry for a listed model with no entry and no ancestor, one call per listing, held behind the deterministic gates below |
The walk
CatalogDiscovery runs from a classpath carrying the provider artifacts and the credentials. It walks every provider on the classpath in key order: an unconfigured one has its seed entries left out and the credential named; a configured one that implements ModelDiscovery lists what the account can reach (Anthropic's and OpenAI's model lists, Bedrock's control plane plus Service Quotas plus the Mantle catalog for allowed_modes); one that cannot list, or whose listing failed, has its seed entries pinged blind, except one whose listing failed on an unknown host, the endpoint not being served where this deployment points: no ping is spent on it, its seed entries are left out and its entries from the previous file are closed as UNREACHABLE, reopened by a later run whose listing answers. Every open entry is pinged through CatalogPing - a one-word prompt at Depth.IMMEDIATE with 2048 tokens of headroom, a one-word embed for embeddings entries, two minutes cap, a transient failure retried a few times, a failed call being a result with a classification - and a reachable entry is written with the limits the response headers carried, else the quota the listing carried, else its seed value. A DISABLED or DEPRECATED entry is kept and never pinged; a listed model the vendor marks retired has its status set to DEPRECATED. Two ping outcomes are verdicts of their own, never "unreachable": an entry this process cannot route (NOT_ROUTABLE_HERE: its compliance envelope refuses the model, or the model needs the Mantle LAX project and none is configured) is kept and not pinged, because that is this deployment's own state and says nothing about the account - a discovery run inside an application inherits that application's seal, which is why run reads the dispatcher's envelope and main seals a permitting one; and a ping the provider refuses because the model does not offer zero data retention (ZDR), the mode the account runs at (NON_ZDR, matched on the provider's own wording, the only signal a surface without allowed_modes gives) leaves a new entry out under its own heading, since a surface that routes the model no other way has no entry to write, and keeps a previous one flagged. A new entry's requires_lax is the listing's word alone (retentionFromListing): set when Mantle's allowed_modes exclude none, removed whatever the inherited ancestor or the classifier's proposal said, before the ping, because the posture decides how the ping is routed. A ping's failure is recorded with its whole cause chain (CatalogPing.describe), so the provider's body - the part that tells a person what to do - reaches the report. A seed entry naming a model through another geography's profile, or through a bare id where the region has a profile, is rewritten to the region's own profile. The seed is the shipped fragments; the deployment's previous file, when present, contributes its entries, pins and hand-set fields, is never shrunk (history prices against it), and is never re-evaluated: an entry the file already carries gets no ping and no rewrite - its facts stay exactly as the person left them - and is flagged NOT_LISTED only when the provider's own listing no longer names it, which costs no call; flagged so, it is kept for the runs it served and closed as UNLISTED, so no picker, pool or benchmark calls a model the account no longer serves, and a later run whose listing names it again reopens it and pings it like a new entry. A run spends only on the delta: new listings, their classification, and their pings. run(includeOlder, scope) narrows a run to the named provider keys: an out-of-scope provider is neither listed nor pinged and its previous entries ride through exactly as they were, reported OUT_OF_SCOPE - the incremental form a host runs as credentials arrive, paying only for what changed. A provider the previous file names that no artifact on this classpath declares (a native image run against a catalog a JVM build wrote) is reported NOT_ON_CLASSPATH, and its entries ride through the same way, kept exactly as written with any pin naming them.
What a listed model the catalog does not name becomes is judged per family and per platform (all Bedrock keys are one platform, Azure another; a family disabled on one platform is untouched on another). A family the catalog names in an older version gains every listed newer version, shape inherited from the ancestor; a family never named gains only its newest listed version; a family whose newest entry is DISABLED gains its new versions disabled too, unpinged - a new version does not undo a deliberate choice. Where the deterministic lineage can say all that, it does (addListed); where it cannot - a family with no ancestor, names that do not sort themselves into families - ModelClassifier asks the strongest model the run can call to write the entry under the same rules, one call per listing. Its judgment stays on the record and behind deterministic gates: a proposal may only name a listed model of its own platform, on a key whose client speaks the model's family (ClientProvider.serves), never an existing id, and in its kind's shape - a grade and both prices for an LLM, embedding_dimensions for an embeddings model, no grade and exactly one bound (tpm or max_concurrent) for a decision model, whose provider key ends in -decision (ModelClassifier.rejection); a listing the vendor marks retired is not sent to it at all; the catalog loader's own validation prices it in a currency, the disabled-family overlay is applied deterministically whatever the proposal said, every accepted entry is pinged live like any other and dropped when unreachable, and both the entry's note and the report name the classifier so a person knows which facts to verify. A model the classifier is not confident about stays on the person's list, reported as before; a run with no callable model says so and leaves every unknown untouched. A person waits behind the run, so it pays in wall time, not in sum: every unclassified listing classifies on a parallel call of its own (its platform's other listings ride along for family context, and a failed call costs only its own listing, named in the report), the accepted entries ping concurrently, and the Bedrock quota table is read once per region per run, shared by its surfaces.
The merge
The listing is merged into the catalog, not just reported. A listed model no entry names is added when the same key holds an ancestor - an entry of the same family (ModelLineage: the identity's family and version, opus-5 after opus-4.8, nova-2-lite after nova-lite) - with the ancestor's whole shape inherited, the ping correcting what it measures, a note saying so, and the ancestor's disposition: a newer model of a DISABLED one, or of a family disabled anywhere on the platform (ClientProvider.platform(): all of Bedrock is one, OpenAI and Azure are not), arrives disabled and unpinged; a model the deployment disabled on another key of the same platform arrives disabled on this one. A newer model of a pinned entry is added and announced at the top of the report, and the pin is never moved. A first pull keeps one entry per family and key, the newest, and a listed older version is left out with a line saying so, unless --include-older; a previous file's older entries are kept whatever happens. Before any of that, a listing is placed on the key that fits it: one whose client speaks the model's family (ClientProvider.serves) and whose kind matches the model's stated output - an embeddings model rides an embeddings key, an LLM an LLM key - since the keys of one platform list each other's vendors and the listing does not sort that out by itself. A model no key of its platform both lists and fits is reported once as listed with no client, and never classified (every in-scope provider lists before any key decides, so a key can look its siblings up). A model whose stated modalities fit neither an LLM seat nor an embeddings one - image generation, speech, video - is written as a seatless entry (addSeatless): its id, identity, wire id, modalities and a note, no grade, no ceilings, no prices, never pinged, so the catalog states what the account offers and no picker lands on it. A listed model with no entry and no ancestor is reported for a person, or the skill, to write. The result records the providers it was written for and each entry's platform (ProviderLinks.stamp, carrying the previous file's record for a provider this classpath lacks), and is validated by loading it against those providers before it is written.
The report's Pins: block lists every pinned id with its entry's verdict, one line per id, a grade's order numbered from its default (SMALL 1, SMALL 2); a catalog with no pins says each grade is served by its cheapest reachable entry until an order is written.
Related packages
- The catalog, grades and the picker - the catalog this tool authors: Models, ModelSpec, pricing, status, the fragments
- Model clients and rate limits - the providers and clients the walk lists and pings through