Building a Model Deprecation Calendar for Your Stack
10 min read · updated August 11, 2026
A deprecation calendar is a join between two tables that live in different places: the model identifiers your code actually sends, and the retirement dates your providers publish on a web page. Neither half is available from an API, which is why this has to be built.
Two halves: what you call, and when it dies
Almost every failed deprecation is a failure of the first half, not the second. The dates are published, dated and easy to find. What nobody has is a reliable answer to “which of our services sends claude-3-haiku-20240307”, because the answer is spread across a repository, a Helm chart, a database column of per-tenant overrides and a notebook someone scheduled. The provider’s email tells you the model is being retired; it cannot tell you where you use it.
So build the inventory first and the date table second, and keep them as separate inputs that are joined by the model string. That way an inventory refresh does not require re-reading every vendor page, and a date correction does not require redeploying anything.
Discovering what you actually call
Four sources, in increasing order of trustworthiness.
- Grep the repositories. Cheap, immediate, and incomplete by construction: it misses identifiers in environment variables, in a config service, in a database row, and in anything a customer configured. Use it as a starting list, never as the answer.
- Your provider’s usage export. Anthropic documents an audit path for exactly this on its deprecations page: the Usage page in the Console has an Export that produces a CSV broken down by API key and model. That is authoritative about what was called, and if your API keys are issued per service it is also authoritative about who called it — which is a good argument for issuing keys per service.
- Your own request logs. The best source, if you log the model field on every call. It is real-time, it is joined to your own service and tenant identifiers, and it survives a provider changing their console. What to log around a model call covers the wider set; for this purpose the minimum is the identifier you sent, the identifier the response reports, the service name and the timestamp.
- The response, not just the request. Record both. Where a provider resolves an alias to a dated snapshot, the response body is where you can see which snapshot you actually got, and that resolved identifier is the one with a retirement date attached. An inventory built only from what you send will contain aliases and no dates.
Why the API will not give you the dates
The instinct is to automate this against the models endpoint, and it is worth being precise about why that does not work: the endpoint does not carry the field. Anthropic’s List Models reference documents each entry as an id, a display_name, a created_at timestamp, a type of "model", max_input_tokens, max_tokens and a capabilities object — a description of what the model is and can do today. There is no retirement date in it. OpenAI’s equivalent returns id, object, created and owned_by, which is less again. The lifecycle information lives on a human-readable deprecations page, in prose and tables, and is not exposed as data.
One thing the endpoint does give you, and it is worth wiring up as a backstop rather than as a plan: disappearance detection. Poll the model list on a schedule, store the set of identifiers, diff it, and alert when an identifier you are recorded as using stops being returned. That catches a retirement reliably — on the day it happens, which is far too late to be your primary mechanism but exactly right as the thing that catches what your process missed.
The file
Keep it in version control next to the code, not in a wiki, so that changing it is a reviewed diff and so that CI can read it. One entry per model identifier in use:
# models.yaml — reviewed like code - id: claude-sonnet-4-5-20250929 provider: anthropic platform: anthropic-api # partner platforms have their own dates used_by: [search-api, ingest-worker] status: active retires: 2026-09-29 retires_is_floor: true # "not sooner than" — a guarantee, not a deadline replacement_candidate: claude-sonnet-5 source: https://platform.claude.com/docs/en/about-claude/model-deprecations checked: 2026-08-11 owner: search-team
Four of those fields carry more weight than the rest. retires_is_floor encodes the distinction covered in reading a deprecation notice: a “not sooner than” date is a guarantee you may rely on, while an assigned retirement date is a deadline you must beat, and a calendar that conflates them either panics early or sleeps late. platform exists because the same model reached through a partner cloud can have a different schedule. checked is the field that makes staleness visible — without it, an entry that was right in March looks identical to one confirmed yesterday. And owner is a team, because an alert with no owner is a notification.
Making it fire, and when
Alerting on the retirement date is the obvious design and the wrong one. What you want to know is the last responsible start date, which is the retirement date minus everything that has to happen first:
start_by = retires
− evaluation_cycle assumption: 14 days
− dual_run_window assumption: 14 days
− rollout_ramp assumption: 7 days
− one_slipped_sprint assumption: 14 days
= retires − 49 days
With a 60-day notice period, that leaves 11 days of genuine slack
between the notice arriving and the work having to begin.Substitute your own numbers — the point is that the slack is measured in days rather than months, and that a notice period which sounds generous mostly is not. Where the dual-run window comes from is the subject of defining an exit condition rather than a duration, and it is the term most likely to be larger than you assumed.
Two jobs then keep the file honest. A CI check that fails the build when any entry’s start_by is in the past, when checked is older than ninety days, or when a model identifier appears in production logs but not in the file. And a scheduled job that opens a ticket at start_by, assigned to owner, with the replacement candidate in the description.
# fails the build on a stale or overdue entry
for m in models:
if m.status != "active" or m.retires is None:
continue
if date.today() > m.retires - timedelta(days=49):
fail(f"{m.id}: past the last responsible start date")
if date.today() - m.checked > timedelta(days=90):
fail(f"{m.id}: not verified against {m.source} since {m.checked}")
# and the third check, from the other direction
for seen in models_seen_in_production_logs(days=7):
if seen not in {m.id for m in models}:
fail(f"{seen}: called in production but absent from models.yaml")That third check is the one that keeps the whole thing from rotting. Everything else in this page is a discipline somebody has to sustain; the log-derived check is the only part that notices when the discipline lapses, because it fails the moment a service starts calling a model nobody added to the file. Pair it with pinning the identifier in one place and the set of strings it has to know about stays small enough to maintain by hand.