Keeping models in sync
Best practice for downstream apps — never hardcode model lists; fetch, cache, and adapt from /v1/models and the changelog.
The catalog changes constantly — models are added, occasionally removed, and repriced (see the changelog). An app that hardcodes a list of model ids goes stale, and worse, keeps offering a model that no longer exists. The rule is simple: never hardcode the model list — fetch it, cache it, and select by capability. Everything below is how.
1. Fetch the catalog, cache with a TTL
GET /v1/models is public (no auth) and is the source of truth. Fetch it at
startup and refresh in the background on a timer — an hour is plenty; the
catalog only moves on our ~6-hour sync.
curl -s https://api.gopura.net/v1/models
// Refresh at most hourly; serve from memory in between.
let cache: { at: number; models: WireModel[] } | null = null;
async function getModels(): Promise<WireModel[]> {
if (cache && Date.now() - cache.at < 3_600_000) return cache.models;
const res = await fetch('https://api.gopura.net/v1/models');
const { data } = await res.json();
cache = { at: Date.now(), models: data };
return data;
}
Do not ship a static JSON of models with your app. Do not cache for
days. Respect the Cache-Control header if your HTTP layer honours it.
2. Select by capability, never by hardcoded id
Every row carries the fields you need to pick a model programmatically, so your picker adapts as the catalog evolves instead of breaking:
| Field | Use it to find |
|---|---|
modality | "chat", "embedding", "image", "ocr", "speech", "transcription" — the endpoint the model serves |
architecture.input_modalities | "image" present ⇒ vision-capable; "audio" ⇒ transcription |
supported_parameters | "tools", "reasoning", "response_format", "dimensions" |
embedding.dimensions / dimensions_adjustable | vector width, and whether you can truncate it |
pricing.prompt / .completion / .image / .page / .character / .second | cost, to sort or cap by budget |
context_length | fits your longest input |
// "The cheapest chat model that does tools + vision" — no id hardcoded.
const pick = (await getModels())
.filter((m) => m.modality === 'chat')
.filter((m) => m.supported_parameters.includes('tools'))
.filter((m) => m.architecture.input_modalities.includes('image'))
.sort((a, b) => parseFloat(a.pricing.prompt) - parseFloat(b.pricing.prompt))[0];
3. Watch the changelog to know what changed
GET /v1/models/changelog reports additions, removals, and price changes by
date (newest first, ?days= to widen the window). Poll it daily, surface it
in your admin UI, or use it to tell your users about new models.
curl -s "https://api.gopura.net/v1/models/changelog?days=7"
{
"data": [
{
"date": "2026-09-07",
"added": [{ "id": "anthropic/claude-fable-5.1", "modality": "chat", "pricing": { "input": "0.000003" } }],
"removed": [{ "id": "deepseek/deepseek-v3" }],
"repriced": [{ "id": "zai/glm-5.3", "old": { "input": "0.0000014" }, "new": { "input": "0.0000007" } }]
}
]
}
Also available as the human-readable page at gopura.net/changelog.
4. Handle a removed model gracefully
When a model is retired it stops resolving, and a request naming it returns a
clean 400 — not an upstream failure:
{ "error": { "code": "model_not_found", "message": "Model '…' does not exist or is not available", "type": "invalid_request_error" } }
On model_not_found: drop the id from your cache, refetch /v1/models, and
fall back to another model of the same capability (or to gopura/auto). Never
let a retired id become a hard error your users see.
5. Or don't manage a list at all — pin gopura/auto
If you don't need a specific model, use the auto-router.
gopura/auto (and :economy / :quality / :ultra) classifies each request
and routes to a current, appropriate model — it never returns
model_not_found, so it's the strongest "set and forget" option. The concrete
model that answered is reported back in the routing object.
const res = await client.chat.completions.create({
model: 'gopura/auto', // never goes stale
messages: [{ role: 'user', content: 'Summarise this contract…' }],
});
// res.routing.model — the model actually used, for your logs.
The recommended loop, in one place
- On startup and hourly:
GET /v1/models→ cache in memory. - Pick models by capability, not id.
- Daily (optional): read
GET /v1/models/changelogto surface adds/removes/reprices. - On
model_not_found: evict, refetch, fall back. - Don't want any of that? Point at
gopura/autoand readroutingfor the concrete model.
Follow this and your app tracks the catalog automatically — new models become available the day they ship, and retired ones never turn into an error your users hit.