Make your CI fail before your model does.
Your provider emails the account owner when a model is deprecated. Your build system never finds out. check-models closes that gap: point it at your repo, and it fails the build while you still have months, not while you have an incident.
Thirty seconds
curl -sLO https://aimodelwatch.dev/ci/check-models.mjs
node check-models.mjs --scan .No install, no dependencies, no key, no signup — one file and Node 18+. The scan reads your source for anything that looks like a model id we track, so there is nothing to configure on the first run.
What it looks like when it fires
$ node check-models.mjs --scan .
check-models 1.1.0 — feed updated 2026-08-05, 204 models
scanned . — found 3 model ids in your source
claude-mythos-preview src/llm.ts:14
gpt-5.5-pro src/llm.ts:15
gemini-3.5-flash config.yaml:7
checked 3 models · retirement threshold 90 days
✖ claude-mythos-preview: Claude Mythos Preview is DEPRECATED. Retires 2026-06-30.
Provider's stated replacement: claude-mythos-5.
1 problem, 0 warnings.
$ echo $?
1In GitHub Actions
There is an action, so this is the whole setup — no install step, no setup-node, no inputs:
# .github/workflows/model-lifecycle.yml
name: model lifecycle
on:
pull_request:
schedule: [{ cron: '0 7 * * 1' }] # and once a week, so it catches drift with no PR
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Khavel/check-models-action@v1With no inputs it walks the repo, finds every model id in your source and checks all of them. Findings become inline ::error annotations and a job-summary table. The action vendors the script, so @v1 pins the code while the catalog stays live — source and inputs. The weekly cron is the half that matters: a deprecation lands on a day nobody opened a PR.
Reading the results when it fails. GitHub does not evaluate a composite action's outputs when the action fails the job, so steps.*.outputs.* come back empty on exactly the path you care about. The JSON report is always written — point report-path somewhere and read it with if: always(), or set warn-only: true and branch on the outputs yourself.
Prefer no third-party action in your pipeline? The script is still one file and still the honest MVP — curl -sLO https://aimodelwatch.dev/ci/check-models.mjs then node check-models.mjs --scan ., on any CI.
What it checks
| Signal | Meaning | Result |
|---|---|---|
| Retired | The provider has switched the endpoint off. Your call is already failing, or will on the next deploy. | fails |
| Deprecated | The provider has declared it end-of-life. Still answering, on a clock. | fails |
| Retiring soon | retires_on is within --within days (default 90). | fails |
| Re-priced | Input, output or cached price differs from your pinned snapshot. Needs --pin. | fails |
| Lifecycle moved | The retirement date or the stated replacement changed since your pin. | fails |
| Unknown id | We don't track that id — a typo, or a model we haven't added yet. | warns |
Every one of these is what the provider states, read from its own documentation — never inferred by us. Where a provider publishes nothing, the field is null and the check stays quiet rather than guessing.
Catching a price change
Nobody emails you when a model gets cheaper or dearer. Pin the numbers you costed your product on, commit the file, and the diff shows up in a pull request like any other change:
node check-models.mjs --scan . --pin .amw-pin.json --update-pin # once, then commit
node check-models.mjs --scan . --pin .amw-pin.json # every build
✖ claude-opus-4-5: Claude Opus 4.5 input price ($/1M) changed since your pin: 5 → 4.Options
| Flag | What it does |
|---|---|
| --scan <dir> | Walk a directory and find every model id or API string we know about. No config file. |
| --file <path> | Read ids from a newline-delimited file, a JSON array, or {"models":[…]}. |
| --models a,b | Ids inline. Bare positional arguments work too. |
| --within <days> | Retirement horizon that fails the build. Default 90. |
| --pin <file> | Compare against a pinned snapshot — this is what catches a price change. |
| --update-pin | Write the current values to --pin and exit 0. Commit the file. |
| --json | Machine-readable report on stdout. |
| --json-out <path> | The same report, written to a file. Use this one inside GitHub Actions — there the ::error annotations share stdout with --json. |
| -w, --warn-only | Report, never fail. Good for the first week. |
| --strict-unknown | Treat an unrecognised id as a failure instead of a warning. |
| --cache <dir> | Store the feed + ETag; an unchanged feed then costs a 304, not a download. |
Exit codes: 0 clean · 1 findings · 2 usage or network error. A network failure never fails your build as if a model were dead — it exits 2, distinctly.
What it sends us
Two GETs to the public feed — the same URL anyone can curl. No repo contents, no ids, no telemetry: the scan happens entirely on your machine, and only the models you ask about are ever resolved locally against the downloaded catalog. The script identifies itself as amw-ci-check/1.1.0 in its User-Agent, which is how we count how many projects depend on this data. Strip it if you'd rather not be counted; nothing breaks.
MIT — use it in commercial products, fork it, vendor it into your repo. It is a plain file on purpose: read it before you run it. The data behind it is the same free JSON API and open dataset, refreshed daily from official provider documentation. Something wrong, or a check you want?Tell us.