GOPURA

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:

FieldUse 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_adjustablevector width, and whether you can truncate it
pricing.prompt / .completion / .image / .page / .character / .secondcost, to sort or cap by budget
context_lengthfits 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.
  1. On startup and hourly: GET /v1/models → cache in memory.
  2. Pick models by capability, not id.
  3. Daily (optional): read GET /v1/models/changelog to surface adds/removes/reprices.
  4. On model_not_found: evict, refetch, fall back.
  5. Don't want any of that? Point at gopura/auto and read routing for 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.