Skip to main content

modelrot

PyPI version license PyPI downloads

claude-3-5-haiku-20241022 stopped working on 19 February 2026. gpt-3.5-turbo switches off on 23 October 2026. gpt-4-turbo the same day.

Your code names a model in six places — a call, a constant, a deploy file, a notebook, a test fixture, a README — and none of them announce the day the provider turns it off. You find out when the calls start failing.

pip install modelrot
modelrot

No API key. No network. No cost. It reads files, by default — see --fix below for the one command that can write, and only when you ask it to twice.

What it tells you

high   "claude-3-5-haiku-20241022" was retired on 2026-02-19
       app/summarise.py:4, tests/fixtures.py:12
       Calls naming this model do not work any more. The provider recommends
       claude-haiku-4-5-20251001.
       "anthropic lists claude-3-5-haiku-20241022 as retired."
       https://platform.claude.com/docs/en/about-claude/model-deprecations

high   "gpt-3.5-turbo" is switched off in 32 days, on 2026-10-23
       config/deploy.yaml:1
       It still works today. On that date it stops, with no further notice.
       The provider recommends gpt-5.6-terra.

high   temperature passed to claude-opus-4-7, which rejects it
       app/summarise.py:9
       The Python SDK (v1.0 and later) removes these parameters from request
       types, so passing them raises a TypeError. On the wire it is a 400.
       Omit them and use prompting to guide model behaviour.

That last one is code that is already broken and does not know it: on Claude 4.7 and later, a non-default temperature, top_p or top_k returns a 400 — and the Python SDK raises a TypeError before the request leaves the process. The same rule exists on the other side: GPT-6 Astra does not accept temperature, top_p or top_logprobs either.

Severity is days, not labels

Most tooling calls a retired model and a deprecated one the same thing and gives them the same yellow warning. One is a note for next quarter; the other is production, already down.

  • retired → high
  • deprecated, switching off within 90 days → high, with the countdown in the title
  • deprecated, further out → medium

A shutdown crossing the 90-day line escalates on its own. You do not need a new version of modelrot for that to happen.

Every finding quotes the provider

modelrot tells you your code is broken because of a decision another company made. If it is wrong about that, it sends you to change working code — so every finding carries the provider's own page and a sentence from it. Anything without a published sentence behind it is printed as (inferred, not a published rule) and never dressed up as one.

Where it looks

Python files are parsed with ast, which is what lets temperature=0.2 be read together with the model= beside it. Everything else is searched as text: .yaml, .yml, .toml, .json, .ini, .cfg, .env, .sh, .js, .mjs, .cjs, .ts, .tsx, .jsx, and .md with --prose.

The model name that survives a migration is almost always the one in a deploy file nobody greps.

A model in the right family, but not on the list

low    "claude-opus-9" is not in the modelrot catalog
       app/fallback.py:14
       It looks like an anthropic model — the family matches — but this
       catalog, captured 2026-09-21, does not list it. Either a typo, a
       custom deployment alias, or a model anthropic shipped after that
       date. (inferred, not a published rule)

Silence used to be the answer for a model id the catalog does not recognise, which makes claude-opus-9 look exactly as healthy as claude-opus-4-7. This only fires on a model= call, because judging a bare string requires knowing it is a model at all — and only when the id's family (claude, gpt, ...) is one the catalog covers, so it never fires on Gemini, Mistral or anything else this tool does not track. It cannot tell a typo from a model shipped after the snapshot or a deployment alias your own infrastructure named, so it says all three and stays low.

What it cannot see

  • Anything that is not a literal. A model name built at runtime from an env var or a settings object is invisible. Always will be.
  • Models it does not know. The catalog is a dated snapshot of what Anthropic, OpenAI and Azure OpenAI publish. A model retired after that date is not in it, so its absence from the report means nothing — and past 45 days the report says so in yellow rather than staying quiet. Google Vertex is not in there: it publishes no machine-readable version of its deprecation page, only the rendered HTML, which this project has already decided is not a source it parses (see Where the data comes from).
  • Unknown model names outside a call. A model id assigned to a variable or sitting in a config value, rather than passed as model=, is not checked against the catalog's families — only a call site carries enough context to judge.
  • Whether any of it is a good idea. modelrot does not know if the model is right for the job, what it costs, or whether your prompts still work after a swap.

There is no green tick. A clean run prints what was checked and what was not.

--fix

modelrot --fix          # prints what would change. Writes nothing.
modelrot --fix --write  # applies it.
app.py:1
  - MODEL = "claude-3-5-haiku-20241022"
  + MODEL = "claude-haiku-4-5-20251001"

--fix alone is still reading — it computes the same replacement the report already names in detail, for every hit the catalog has a replacement for, and prints it as a before/after. Nothing on disk changes. --write is the one exception to "it reads, it never writes," and it needs its own flag rather than riding along on --fix, so it can never be reached by a single flag skimmed past in a changelog. Scope is the same as the report's: only a model the catalog already calls deprecated or retired, only an exact literal, matched with the same bounded regex the report uses — a fix can no more eat into gpt-4-turbo-2024-04-09 while rewriting gpt-4-turbo than a finding can confuse the two.

In CI

- run: pipx run modelrot --fail-on medium

Exit code is 1 at high by default. --fail-on medium catches a shutdown before it is urgent; --fail-on none never fails the build.

Options

modelrot [path]              scan a directory (default: the current one)
  --json                     machine-readable, same fields as the report
  --inventory                list every model found, healthy ones included
  --no-sources               omit the quoted provider documentation
  --fail-on high|medium|none exit code threshold (default: high)
  --fix                      preview a rewrite to each model's replacement (writes nothing)
  --write                    with --fix, actually write the changes

Where the data comes from

Anthropic and OpenAI each serve a machine-readable version of their deprecation page: append .md to the documentation URL. OpenAI documents it; Anthropic links its own from llms.txt. Azure OpenAI's docs are docs-as-code on GitHub, so its table is read from the same raw markdown Microsoft's own docs build from. All three are parsed, so the catalog is read from the source of truth rather than transcribed from it. Google Vertex is not: its deprecation page returns rendered HTML regardless of what you append to the URL, and no public source carries the content as markdown — the thing every row here is checked to avoid.

Every row records the URL it came from, the sha256 of the document at the time, and when it was fetched — so any claim can be diffed back to bytes.

python3 scripts/refresh_catalog.py --check      # how old is the snapshot?
python3 scripts/refresh_catalog.py --refresh    # fetch, parse, write

The repository re-reads all three pages weekly and opens a pull request when anything moved, with a row-level summary rather than a hash. It never merges on its own: every row is a claim about someone else's product.

A refresh merges. A model that disappears from a provider's page keeps its row, because a model vanishing is the event this tool exists to report — and it is why the aggregated catalogs are not used as a source. LiteLLM, models.dev and OpenRouter are live routing tables: they delete their dead.

Azure's own table names the same model under several dated model versions sometimes — gpt-4o shipped three times, each with its own retirement date — and nothing in a deployment's bare name says which version it runs. The catalog keeps the earliest of those dates, and the finding says so rather than pretending to a precision the name doesn't carry. Where an Azure model name is identical to one OpenAI already publishes directly (o3-mini, gpt-realtime, ...), the direct OpenAI row wins; the Azure version of that same name is not merged into it.

As a GitHub Action

- uses: sheldor26/modelrot@v0.2.0
  with:
    fail-on: medium

As a pre-commit hook

- repo: https://github.com/sheldor26/modelrot
  rev: v0.2.0
  hooks:
    - id: modelrot

Requirements

Python 3.9 or newer. No dependencies.

License

MIT

Metadata

Release files for modelrot 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for modelrot 0.2.0
File Size Uploaded
modelrot-0.2.0.tar.gz 349.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for modelrot 0.2.0
File Interpreter ABI Platform
modelrot-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 375.9 kB

Release files / modelrot-0.2.0.tar.gz

Download URL modelrot-0.2.0.tar.gz
Size 349.4 kB
Tags Source
SHA-256 checksum
How to use checksums
88427ad418a91dedc59eb243829d50ef6f13ab0ed64bce2bef06de17e5804926
BLAKE2b-256 checksum
How to use checksums
a318efa090810182d37c0005f82564c31c9a92ee9b86c14c86627dd810f31afd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.6

Release files / modelrot-0.2.0-py3-none-any.whl

Download URL modelrot-0.2.0-py3-none-any.whl
Size 26.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
064b66725b28f4e20d82d3bf3638895cd4ba6bc3859bd1ad3cea5d365e4f2376
BLAKE2b-256 checksum
How to use checksums
771e0fc6e8fe6123aebea4d14c0a3014aa8c81653fa035f1c278f2fa850e36b2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.6

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page