Keep the model catalog current
Providers release, reprice, and retire models faster than any deployment upgrades. Tetrate publishes the provider and model data in a public repository, and tetrate agentrouter api catalog sync reconciles a deployment against it, reporting by default and writing only under an explicit opt-in. From 0.8.0 the Admin Console and the admin API run the same sync behind a preview, and show which models retire soon and who still calls them.
Persona: Platform operator. The one-shot sync runs anywhere the tetrate CLI reaches the management plane; the scheduled CronJob applies where a self-hosted data plane is deployed, and reconciles the same organization catalog.
Estimated time: 10 minutes for a first dry run; one review cycle before allowing writes.
Where the catalog data comes from
The published catalog lives at github.com/tetrateio/agent-router-models: providers.json lists the providers, and each provider has one JSON file carrying its models with their prices, limits, capabilities, and lifecycle (deprecation and retirement markers). Since 0.4.0 the states new, preview, deprecated, and retired are derived from this metadata by one shared policy, so the Console, the sync report, and the gateway read the same verdict. The record formats are defined in the repository's schemas/, and CHANGELOG.md records every update as a dated block, so when a price or a limit changed is answerable from the history. The repository is public: reading it needs no credential.
What a sync does, and deliberately does not do
A sync fetches the published catalog, compares every entry with the organization catalog on the management plane, and classifies each difference:
| Action | Meaning |
|---|---|
create | The published entry does not exist in the deployment yet |
update | The entry exists and a field differs (price, limits, capabilities) |
disable | The published catalog marks the model deprecated or past its retirement date, so it is turned off, with the reason in the report |
unchanged | Already matching; counted in the report, never listed |
The disable action is the one that prevents the worst surprise: a model the provider has retired does not stay selectable until it fails at request time. Since 0.4.0 the sync also warns when a model past its retirement date is still enabled on the management plane, and names the remedy in the report: --apply disables the ones the published catalog still lists, --disable-absent the ones it no longer lists at all.
Three safety properties are deliberate:
- A sync never deletes anything, and it reports without writing unless
--applyis passed. - Models the published catalog does not mention are left alone. A deployment's catalog is not required to be a subset of Tetrate's: self-hosted models and operator-added entries never appear in the published catalog, and disabling them automatically would break the deployment's own routing. They are listed in the report, and turned off only under the explicit
--disable-absentopt-in. - A sync never enables a model an operator disabled. An entry the published catalog would enable is surfaced in the report as staged, and stays off until an operator acts.
Whether a synced model is callable is still governed by enablement and project grants, exactly as for a hand-provisioned one; see Add providers and models to projects.
How the catalog is listed
Since 0.7.0 the Admin Console lists the catalog with first-party providers first, then the newest models by release date, and a model's details (description, backend URL, pricing) are shown only for models available in the selected project.
The API behind it, ListCatalogModels, pages the same way from 0.7.0: send page and follow page.next_page_token until it is empty. Search, mode, modality, context-window and price filters, sorting, and snapshot grouping are additive on that paged read, and a paged list leaves out models whose provider is disabled unless include_disabled is set. A call that sets only provider_id or include_disabled, or nothing at all, still returns the whole catalog in one response, unpaged and ordered by provider then model, so an older tare or a script written before 0.7.0 keeps reading every model. That unpaged response is deprecated and returns one page in a later release, so new callers send page from the start. See ListCatalogModels.
Find models that retire soon
From 0.8.0 the Admin Console's Models catalog badges a model on its way out, in the catalog list and on the model's page: Deprecated, retires ⟨date⟩. for a deprecated model, and Retired ⟨date⟩. once its retirement date has passed. The badge reads the same lifecycle verdict the sync acts on, so the Console and the sync report agree. A model with no lifecycle signal, or one that is only new, carries no badge, and a model that is merely disabled is not badged as retired. Project model lists do not show the badge.
Three admin API reads answer the questions the badge raises:
| Call | Answers |
|---|---|
ListRetiringModels (GET /admin/v1/retiring-models) | Which models the organization called in the last 30 days retire inside the window (window_days, 60 by default, 1 to 365), soonest first. Each entry carries the retirement date, the days left, the request count, the projects that called the model, and how many API keys did. A model whose retirement date has passed is included, flagged retired, while the organization still calls it. |
GetModelUsage (GET /admin/v1/models/{model_id}/usage) | Which projects and API keys called one catalog model inside a look-back window (window_days, 30 by default, 1 to 365), with request counts and when each was last seen, most recent first. |
GetModelLifecycles (POST /admin/v1/catalog:model-lifecycles) | Whether each model in a batch is retired, deprecated, new, or in preview, with the reason and any retirement date. A model ID the catalog does not contain is left out of the result. |
The two usage reads cover only the calling organization, and a caller with no organization binding is refused rather than shown every tenant's usage.
The usual sequence is ListRetiringModels to find what is at risk, then GetModelUsage on each model to name the API keys still calling it, so their owners move before the date. Two mechanisms keep traffic flowing through a retirement:
- A fallback policy on the retiring model. On a data plane on 0.8.0 or later, a request for a retired model whose key has an applicable fallback policy is served by the policy's first live fallback model instead of returning
model_retired. See A retired model. - A staged move with attribute-based routing, where traffic nobody is waiting on moves first. See A model is being retired.
Run a one-shot sync
Sources are passed explicitly, one --models per provider file, and the run is a dry run unless --apply is added:
tetrate agentrouter api catalog sync \
--providers https://raw.githubusercontent.com/tetrateio/agent-router-models/main/providers.json \
--models https://raw.githubusercontent.com/tetrateio/agent-router-models/main/anthropic.json \
--models https://raw.githubusercontent.com/tetrateio/agent-router-models/main/openai.json
The report leads with the shape of the run, then lists only what would change:
providers: 0 create, 1 update, 11 unchanged
models: 4 create, 7 update, 2 disable, 668 unchanged
update provider deepinfra (endpoint)
create model openai/gpt-6-astra
disable model gemini/gemini-1.5-pro -- retired 2026-06-30
dry run: nothing written. 14 entr(ies) would change; pass --apply to write.
Re-run with --apply to write. A by-hand run authenticates as the signed-in session or a management key against the public catalog API; failures on individual entries are collected and reported rather than aborting the run.
To review updates before they are ever fetchable, pin the URLs to a tag or commit SHA of the published repository instead of main.
Sync from the Admin Console
From 0.8.0 the same sync runs from the Admin Console, without the CLI or the CronJob. The management plane fetches the published catalog itself, from the published set it is configured with; the sources are not chosen per sync.
-
On the Admin Console's Models page, click Sync from published catalog. A preview opens with what a sync would change, on New, Modified, and Retired tabs with the per-field changes, and collapsible Warnings, Unknown providers, Skipped, Absent from the catalog, and Staged sections. When nothing would change, the dialog reads Catalog is up to date. and offers no apply.

-
Click Apply all changes, then Confirm N writes. The apply is pinned to the exact documents the preview read: the management plane fetches them again and refuses the run if any has changed since, and the dialog then asks for a new preview.
-
The dialog follows the run with a progress bar. Closing it does not stop the run, which continues in the background.
The toolbar on the Models page shows Last synced with the applied catalog version and time, marked update available when a preview or a nightly dry run has read a newer catalog than the one applied. The button needs permission to create and edit models.
The Console sync keeps the safety properties above, with two differences from the CLI. It is all or nothing: there is no per-entry selection, and no equivalent of --disable-absent, so models absent from the published catalog are listed in the preview and never disabled. And one sync runs at a time: a second apply while one is running is refused, and the dialog switches to following the run already in progress. Each catalog write is attributed in the audit log to the person who applied the sync.
The same flow is three admin API calls:
| Call | What it does |
|---|---|
PlanCatalogSync (POST /admin/v1/catalog/sync:plan) | Returns what a sync would change, with the catalog version and the documents read, each with its sha256 digest. Writes nothing to the catalog, but counts as a run in tetrate agentrouter api catalog status, exactly as a dry run does |
ApplyCatalogSync (POST /admin/v1/catalog/sync:apply) | Takes the preview's version as expected_catalog_version and its documents as expected_sources, starts one background run, and returns it running. Answers ALREADY_EXISTS while another run is in flight, ABORTED when the preview has gone stale, and FAILED_PRECONDITION when the management plane cannot sync until an operator fixes something |
GetCatalogSyncRun (GET /admin/v1/catalog/sync/run) | Returns the latest run: its phase (running, succeeded, or failed), entries written out of the total, and any failures. A run that stops reporting is shown as failed |
Where the management plane has no published sources configured, the Console and API sync are refused, and the CLI with local files remains the path, as for an air-gapped cluster.
Schedule it
Where a self-hosted data plane is deployed, its chart ships a catalog-sync CronJob that runs the same sync nightly. Since 0.4.0 the job authenticates with a management key over the admin API surface (the CLI's --admin flag); on earlier releases the scheduled job could not authenticate at all, because the public catalog API accepts only a session token or a management key issued to a user, neither of which a scheduled job has. It is off by default, behind two separate opt-ins so it can be watched before it is trusted:
| Helm value | What it does |
|---|---|
catalog-sync.enabled | Renders the CronJob. Alone, this buys a nightly dry-run report in the job's logs, and writes nothing |
catalog-sync.apply | Lets the job write |
catalog-sync.adminSurface | On by default: the job runs tetrate agentrouter api catalog sync --admin, driving the admin API surface. Switched off, the job uses the public catalog API, and the secret below must hold a management key valid for that surface instead. Flip the surface and the secret together, or every run fails before reading anything |
catalog-sync.apiKey.secretName | Names the secret holding the management key the job authenticates with |
catalog-sync.proxy | Outbound proxy for a cluster that egresses through one |
catalog-sync.source | Where to read the catalog: the published repository's main by default, a pinned tag or SHA for review-first change control, or mounted files for a cluster with no egress |
The rollout pattern the defaults are built for: enable the job, read a cycle of nightly reports in kubectl logs, and only then allow apply.
Watch catalog freshness
Since 0.5.0 the management plane keeps a record of its catalog syncs: every run updates it, report-only runs included, and tetrate agentrouter api catalog status reads it back:
last run 2026-09-17T03:00:04Z (9 hour(s) ago), dry run, by admin-key-7
catalogue 2026-09-16T00:00:00Z
last applied 2026-08-05T03:00:11Z (43 day(s) ago), by admin-key-7
catalogue 2026-08-05T00:00:00Z (43 day(s) old)
wrote 4 provider(s), 700 model(s)
DRIFT: the last run read catalogue 2026-09-16T00:00:00Z, which this plane has not applied.
It is on 2026-08-05T00:00:00Z. Run the sync with --apply to converge.
The two halves answer different questions. Last run says when this deployment last looked at a published catalog, and moves on every run, so a nightly job left in its default report-only mode still reads as checked. Last applied says which catalog the deployment is actually on, and moves only when a run wrote the whole difference: a run that failed part way, lost its publish step, or skipped entries does not claim the new version. The DRIFT block appears when the two disagree, which is the state that needs an operator.
For a monitor, two thresholds turn the same record into exit codes, and each points at a different fault:
| Flag | Fails when | The fault it points at |
|---|---|---|
--max-since-run 48h | No sync has run inside the window, report-only runs included | The sync job itself stopped: CronJob suspended, credential expired, cluster egress broken |
--max-age 168h | The applied catalog's own publication date is older than the window | The deployment is on an old catalog: either syncs stopped converging, or the published catalog itself has gone quiet |
Set both. A stopped job also makes the applied catalog age, so --max-since-run is checked first and a plane that stopped syncing reports the job rather than the consequence. Like the sync itself, catalog status takes --admin when authenticating with a management key.
One row in the audit log records each run (RECORD_CATALOG_SYNC, with the catalog version and whether it applied), and since 0.5.0 the sync's individual catalog writes carry batched: true in their audit metadata, so the audit view can be filtered to just the changes a person made.
Air-gapped clusters
Both sources accept a local file path in place of a URL. Download the published catalog out of band, then apply it from files:
tetrate agentrouter api catalog sync --providers ./providers.json --models ./anthropic.json --apply
--models-sha256 pins a digest per models document, in the same order as the --models flags, for pipelines that verify what they fetched.
Where to go next