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
{
"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 whatclaude-opus-4.6returns, for example). There's no replacement in the response — pick one yourself fromGET /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:
curl https://api.zukko.pro/v1/modelsAn 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:
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:
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
modelvalue in a script or CI pipeline nobody has touched in months. - An
.envfile with something likeLLM_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-solandgpt-5.6-luna(1M context). - GLM:
glm-5.3andglm-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.
Related articles
- OpenAI API Keys in the Open: Why You Should Not Use ThemWhere free keys on GitHub come from, why using them is dangerous for you and for the owner, and how to protect your own key from the same leak.
- GLM API Errors: Why the Request Fails and How to Fix ItA wrong model id, an overflowing 128K context, the key, the balance, limits: the typical causes of GLM API errors and a quick way to find yours.
- This organization has been disabled in the Claude API: What to DoAn Anthropic key suddenly answers "organization has been disabled". What the error is, what to check, and how to move the SDK and Claude Code to another provider.
- JetBrains AI Assistant not working: causes and workaroundsAI Assistant will not activate, stays silent or is off for some files. We go through the causes in the JetBrains docs and connect your own models via a gateway.