There is a category of production incident that has no bug, no bad deploy, and no root cause inside your codebase. Your code is byte-identical to the version that worked yesterday. Your tests pass. Your dependencies are pinned. And a feature is returning 404s.
The model string in your config refers to something that no longer exists.
This is the strangest failure mode in the whole LLM stack, because it is the only one that arrives entirely on somebody else's schedule — and the only one you were told about, in writing, months in advance, in an email nobody on the on-call rotation read.
I teach AI engineering at Cursuri-AI.ro, and model retirement is the incident I see teams handle worst, not because it's hard, but because it doesn't feel like engineering work until the day it does. Here's the whole thing: what actually breaks, what the provider tells you and when, and the runbook that turns a surprise outage into a boring Tuesday ticket.
The vocabulary you're missing two-thirds of
Most teams have two mental states for a model: "it works" and "it's old." Anthropic's model deprecations page defines four, and the gap between them is where the incident lives:
- Active — fully supported, recommended.
- Legacy — no longer receiving updates, may be deprecated in future.
- Deprecated — still functional, no longer recommended, has an assigned retirement date.
- Retired — gone. Requests fail.
The important one is deprecated. A deprecated model works perfectly. Latency is normal, quality is normal, your dashboards are green. Nothing in the response payload tells you a clock is running. The only signal is a documentation page and an email to the account owner — who is usually in finance.
And "retired" means what it says. The model ID stops resolving, and the API answers the way it answers any unknown model ID: HTTP 404, not_found_error, whose documented cause is "invalid endpoint or model ID." Your retry logic will not help you, because 404 is not retryable. If your error handling catches one broad exception class and retries everything, you'll spend the outage generating three times the traffic and no useful log lines.
import anthropic
client = anthropic.Anthropic()
try:
response = client.messages.create(
model=settings.CHAT_MODEL,
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
except anthropic.NotFoundError:
# This is not a blip. This is a retired or misspelled model ID.
# Page someone. Do not retry.
raise
except anthropic.RateLimitError:
...
except anthropic.APIConnectionError:
...
That first except block is maybe ninety seconds of work, and it's the difference between an alert that says "the model is gone" and an alert that says "elevated error rate, service degraded."
The dates are published before the deprecation, and almost nobody reads them
Here is the part that genuinely surprises people, and it's the single most useful fact in this article.
Anthropic publishes tentative retirement dates for models that are currently active. Not deprecated — active. As of the end of August 2026, the model status table reads, in part:
| Model | State | Earliest retirement |
|---|---|---|
claude-sonnet-4-5-20250929 |
Active | Not sooner than September 29, 2026 |
claude-haiku-4-5-20251001 |
Active | Not sooner than October 15, 2026 |
claude-opus-4-5-20251101 |
Active | Not sooner than November 24, 2026 |
claude-opus-4-6 |
Active | Not sooner than February 5, 2027 |
claude-opus-5 |
Active | Not sooner than July 24, 2027 |
claude-fable-5 |
Active | Not sooner than June 9, 2027 |
Read the first row again. If you are running claude-sonnet-4-5-20250929 in production today, its published floor is roughly four weeks out. That is not a retirement announcement — "not sooner than" is a floor, not a date, and no deprecation has been announced for it. But it is a planning input you already have, for free, and it costs you nothing to put in a calendar.
The counterpart is the models that already went: claude-opus-4-1-20250805 was deprecated on June 5, 2026 and retired on August 5, 2026 — three weeks before this article. claude-opus-4-20250514 and claude-sonnet-4-20250514 were deprecated April 14, 2026 and retired June 15, 2026.
Do the arithmetic on those pairs: 61 days, and 62 days. Anthropic commits to "at least 60 days' notice before model retirement for publicly released models," and in practice the notice window is exactly that. Sixty days is enough time to run a migration. It is not enough time to build an eval suite, get budget approval, and negotiate with a team that owns the prompt. That work has to already exist when the email lands.
Aliases are a strategy, not a shield
Anthropic exposes two shapes of model ID: aliases like claude-opus-5 or claude-sonnet-4-6, and dated snapshots like claude-sonnet-4-5-20250929. Note that the current generation are complete as aliases — claude-opus-5 is the whole ID, and appending a date suffix to it produces a 404 as surely as a retired model does.
Neither shape saves you:
- A dated snapshot is reproducible and will never change behavior under you — and it is precisely what appears in the retirement table, by name, with a date.
- An alias may keep resolving across point releases, but the alias is scoped to a model family. When the family goes, the alias goes. And an alias that silently starts pointing somewhere new is its own kind of incident: your outputs change, your evals shift, and nothing in your diff explains it.
The real answer isn't picking one. It's picking one per route, deliberately, and writing down why. A billing-adjacent classifier that must produce identical output for audit reasons wants a snapshot and a calendar entry. A chat surface where quality improvements are welcome wants an alias and an eval suite that runs on a schedule. What you must not have is thirty model strings scattered across a codebase, each chosen by whoever wrote that file, with no owner.
It is never just the model string
This is the trap that turns a one-line change into a two-week migration, and it's why "we'll swap it when we have to" fails.
Request parameters retire too. On Claude Opus 4.7 and later, temperature, top_p and top_k return a 400 error when set to a non-default value — the recommended replacement is to omit them and steer behavior through prompting. And it's not only the API: the Python SDK v1.0 and later removes those parameters from the request types, so passing them raises a TypeError before a request is ever made.
The same applies across the current generation: the fixed thinking budget (thinking: {type: "enabled", budget_tokens: N}) is gone in favor of adaptive thinking plus an effort level, and assistant-message prefill — the classic trick for forcing a response format — returns a 400 on the whole 4.6-and-later family. If your "quick model swap" touches code written against a 2025-era API, you are not changing a string. You are rewriting the request.
# Written for an older model. Every line here is now a 400 or a TypeError.
response = client.messages.create(
model="claude-3-5-sonnet-20241022", # retired October 28, 2025
max_tokens=4096,
temperature=0.2, # 400 on Opus 4.7+ (non-default)
thinking={"type": "enabled", "budget_tokens": 8000}, # removed
messages=[
{"role": "user", "content": prompt},
{"role": "assistant", "content": "{"}, # prefill: 400
],
)
# The current shape.
response = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
thinking={"type": "adaptive"},
output_config={
"effort": "high",
"format": {"type": "json_schema", "schema": INVOICE_SCHEMA},
},
messages=[{"role": "user", "content": prompt}],
)
And prompts are part of the migration. This is the least mechanical and most expensive piece. Prompts tuned against an older model are frequently over-prescriptive for a newer one — step-by-step scaffolding that raised quality two generations ago can now suppress it. A migration that swaps the ID, passes your smoke test, and ships is a migration that quietly degraded output quality in a way you'll attribute to something else three weeks later. This is exactly the failure mode that makes an evaluation suite the load-bearing part of the runbook rather than the optional part.
The runbook
Five steps. The first three are cheap and you should do them this week, whether or not anything of yours is deprecated.
1. Inventory: find every model string you own
You cannot plan a migration you can't enumerate. Two passes:
# Every model ID that lives in code, config, notebooks, IaC, or a CI secret file.
rg -n --hidden -g '!node_modules' -g '!.git' \
'claude-(fable|mythos|opus|sonnet|haiku)-[0-9a-z.\-]*|claude-[0-9]' .
Then the pass that matters more, because it catches the jobs nobody remembers: in the Claude Console, open Usage → Export, and read the CSV. It breaks usage down by API key and by model, which means it shows you the nightly batch job in a repo you don't own, running under a key that was issued in 2025. That report is the actual inventory. The grep is just the part you control.
The output of this step is a single table — route, model ID, owner, alias-or-snapshot, why. Keep it in the repo, not in a wiki nobody opens.
2. Centralize the strings
Every model ID resolves through one module. Not because indirection is beautiful, but because a migration should be one reviewable diff, and because it gives you a place to hang the metadata:
# models.py — the only file in the repo that contains a model ID.
from dataclasses import dataclass
@dataclass(frozen=True)
class ModelChoice:
id: str
owner: str # who approves a change
pinned_reason: str # why this shape, not the other one
review_by: str # ISO date, from the published retirement floor
CHAT = ModelChoice("claude-opus-5", "platform", "quality-led surface, alias by choice", "2027-07-24")
CLASSIFIER = ModelChoice("claude-haiku-4-5", "platform", "high volume, latency-sensitive", "2026-10-15")
review_by is not a guess. It is the published "not sooner than" date, copied in. Now your calendar and your code agree.
3. A canary that fails loudly and cheaply
A retirement should be caught by a scheduled job, not by a user. This is about eight lines and costs a fraction of a cent per run:
# canary.py — run daily in CI. Alerts on the one error that is never transient.
import anthropic, sys
from models import CHAT, CLASSIFIER
client = anthropic.Anthropic()
dead = []
for choice in (CHAT, CLASSIFIER):
try:
client.messages.create(
model=choice.id,
max_tokens=1,
messages=[{"role": "user", "content": "ping"}],
)
except anthropic.NotFoundError:
dead.append(choice)
if dead:
for choice in dead:
print(f"MODEL GONE: {choice.id} (owner: {choice.owner})", file=sys.stderr)
sys.exit(1)
For the inventory half, client.models.list() gives you what your key can currently reach, with id, display_name, max_input_tokens, max_tokens and a capabilities tree — useful for catching the day a model stops appearing, and for asserting that a replacement actually supports the features your route depends on before you switch to it.
4. The eval gate, built before you need it
Sixty days is plenty of time to change a string and not nearly enough to answer "is the new one better on our task?" if you're starting from zero. The gate needs three things and they take an afternoon each:
- A frozen set of real inputs. Fifty to two hundred requests sampled from production, with the messy ones deliberately over-represented. Not synthetic examples.
- A grader that isn't a vibe. Exact match where you can get it, schema validation where the output is structured, an LLM judge with a written rubric where you can't. The judge is fine — an unwritten rubric is not.
- A cost and latency baseline. Because the honest outcome of a migration is sometimes "quality held, p95 doubled," and you want to know that before your users do.
Run it against the old model and the new one on the same inputs, same day. The comparison is the deliverable, and it's the artifact that lets you say "yes, ship it" in an afternoon instead of a fortnight. If you're standing this up for the first time, this is the same machinery you need for every other change you'll ever make to an LLM feature — building it properly once pays for itself on the second migration.
5. Migrate the request, then the prompt, then the model
Order matters, because it isolates variables:
- Modernize the request shape on the model you're already running — remove sampling parameters, replace fixed thinking budgets with adaptive thinking plus an effort level, replace prefill with structured outputs. Ship it. Nothing about behavior should change, and if something does, you've learned it in isolation.
- Swap the model ID behind a flag, run the eval gate, compare.
- Re-tune the prompt for the new model — usually by deleting scaffolding rather than adding it — and re-run the gate.
- Roll out by percentage, keeping the old model reachable until its retirement date, not until your rollout finishes.
Doing all four at once produces a result you can't attribute. Doing them in order takes the same total effort and tells you which change did what.
What the provider actually owes you, and what it doesn't
Worth being clear-eyed here, because the answer is better than the ecosystem average and still not a guarantee.
You get: at least 60 days' notice before retirement of a publicly released model, email to affected accounts with active deployments, published tentative floors for active models, a recommended replacement for every deprecated model, and a self-service usage audit. Anthropic has also published commitments on model deprecation and preservation, including long-term preservation of model weights, and states openly that retirement exists to free capacity — with real downsides for people who depended on a specific model's behavior.
You don't get: a promise that the replacement behaves like the old one on your task. That has never been promised by anyone, and it's the only part that requires actual work from you.
One more thing worth knowing if you're multi-cloud: those dates apply to Anthropic-operated platforms — the Claude API, Claude Platform on AWS, and Microsoft Foundry. Amazon Bedrock and Google Cloud set their own retirement schedules, so a model's status and dates can differ there. If your failover path routes to a different platform, that path has its own calendar, and it is not this one.
The checklist
Print this. It's the whole article.
- [ ] Every model ID in the codebase resolves through one module.
- [ ] That module records, per route: owner, alias-or-snapshot, and the reason.
- [ ] The published "not sooner than" date for each model is in the code and in a shared calendar.
- [ ]
NotFoundError/ HTTP 404 is caught separately and pages a human. It is never retried. - [ ] A daily canary calls every configured model with
max_tokens=1and fails the build when one is gone. - [ ] The Console usage export has been read at least once this quarter, by a human, looking for keys and models nobody claims.
- [ ] An eval suite with real inputs and a written rubric exists today, not on announcement day.
- [ ] The request shape is modernized independently of the model swap.
- [ ] Prompt re-tuning is scheduled as part of every migration, not treated as optional.
- [ ] Anyone routing through Bedrock or Vertex has checked those platforms' separate schedules.
None of this is clever. All of it is the difference between a migration you schedule and an outage you explain.
I build and teach this stack at Cursuri-AI.ro — including taking LLM features to production and shipping a full AI product end to end. If your team has a model string you can't account for, the inventory step is genuinely the highest-value hour you'll spend this month.
Top comments (0)