The model_retired error: when a model gets retired

A model that worked yesterday answers with 400 and code model_retired today. Here is what that means and how to handle it in code instead of by hand.

4 min · · Updated · errors

The model catalogue is not static: providers ship new versions and retire old ones. When that happens to a model you referenced in a request, the response is not model_not_found — it is a dedicated model_retired error, and it names the replacement right there.

What the response looks like

bash
{
  "error": {
    "message": "Model 'gpt-5.4' has been retired. Use 'gpt-5.6-sol'",
    "code": "model_retired"
  }
}

The HTTP status is 400. Worth distinguishing from codes it's often confused with:

  • 404 model_not_found — a typo, or a model id that never existed.
  • 503 provider_unavailable — the model is in the catalogue, but the provider is temporarily unavailable (an upstream incident, maintenance).
  • 503 model_disabled — the model is disabled in the Zukko catalogue (that's what claude-opus-4.6 returns, for example). There's no replacement in the response — pick one yourself from GET /v1/models.
  • 400 model_retired — the model is permanently removed from the catalogue. It will not come back, and the message already names its replacement.

This is not a bug on your end

If a request with model: "gpt-5.4" worked yesterday and fails today with model_retired, nobody touched your config — the server-side catalogue changed. This is a normal part of the model lifecycle: older GPT, Claude, or GLM versions get superseded and eventually retired.

The current list is always available without a key:

bash
curl https://api.zukko.pro/v1/models

An extended version with limits and pricing is GET /v1/models/info, also keyless.

Don't guess the replacement — it's already in the response

A common mistake is seeing model_retired and manually trying similarly-named models. No need — the error message literally contains the correct id. The simplest approach is to pull it out with a regex directly from the message text.

Python:

bash
import re
import requests

def call_model(model_id, messages):
    resp = requests.post(
        "https://api.zukko.pro/v1/chat/completions",
        headers={"Authorization": "Bearer fetch_your_key"},
        json={"model": model_id, "messages": messages},
    )
    if resp.status_code == 400:
        data = resp.json()
        if data.get("error", {}).get("code") == "model_retired":
            match = re.search(r"Use '([^']+)'", data["error"]["message"])
            if match:
                new_model = match.group(1)
                return call_model(new_model, messages)
    resp.raise_for_status()
    return resp.json()

The same idea in JS, if you're on the Anthropic-compatible /v1/messages path:

bash
async function callModel(modelId, body) {
  const resp = await fetch("https://api.zukko.pro/v1/messages", {
    method: "POST",
    headers: {
      "Authorization": "Bearer fetch_your_key",
      "anthropic-version": "2023-06-01",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ ...body, model: modelId }),
  });

  if (resp.status === 400) {
    const data = await resp.json();
    if (data.error?.code === "model_retired") {
      const match = data.error.message.match(/Use '([^']+)'/);
      if (match) return callModel(match[1], body);
    }
  }
  return resp.json();
}

Automatic switching is handy for background services with nobody watching logs in real time. For interactive tools (Claude Code, Cursor) it's cleaner to just update the model id in the config once, rather than switch on the fly on every run.

Where a stale id usually hides

  • A hardcoded model value in a script or CI pipeline nobody has touched in months.
  • An .env file with something like LLM_MODEL=gpt-5.4, copied over from an older project.
  • A Claude Code / Cursor config, where the model was picked once during setup and forgotten.

If you have several places with a hardcoded id, model_retired in the logs is a signal to walk through all of them and check against GET /v1/models.

What not to do

Don't try to guess the replacement by name similarity ("gpt-5.4 got retired, I'll grab gpt-5.6-luna, it's the same line"). The GPT-5.6 models (sol and luna) are not interchangeable synonyms — they're priced differently ($0.30 vs $0.10 per 1M tokens). The model_retired message names the replacement that actually corresponds to the retired model — use that one, not the closest-sounding name.

If behavior noticeably changes after switching (different response length, different style), that's expected — you're calling a different model now. Check your prompt and max_tokens in that case, rather than looking for a bug in the integration.

What's actually in the catalogue right now

If the replacement named in an error message looks unfamiliar, it helps to keep the current text-model lineup in mind, not just the one you used to call:

  • Claude: claude-opus-5-5 (Claude Opus 5.5, 1M context), claude-sonnet-5 (1M context).
  • GPT-5.6: gpt-5.6-sol and gpt-5.6-luna (1M context).
  • GLM: glm-5.3 and glm-5.3-flash (128K context, no vision).

The current list is always at GET https://api.zukko.pro/v1/models. Note that the Claude and GPT models have a 1,000,000-token context and accept images, while GLM has 128K and no vision — so if you swap onto GLM, recheck any logic around images and long context. Image generation through Zukko is temporarily unavailable.

If your project still references names like claude-3.5, gpt-4o, or claude-3-opus, that's a different situation from an in-catalogue retirement: those ids were never in this catalogue to begin with, so you won't see model_retired for them — you'll get a plain model_not_found instead. Migrating off names like that has to happen manually, checking against the list above, rather than waiting for a hint in an error message.

Bottom line

model_retired isn't a configuration mistake — it's a signal that the catalogue moved on. The replacement is already in message; the job is making your code pick it up automatically, instead of a person manually patching the config every time a model gets sunset.

zukko

Pay 25×
less

for Claude Code & ChatGPT

One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.

Get API keyDocumentation

api.zukko.pro · fetch_*

model_retired error explained: what to do next · Zukko