Quickstart
Run this in a repo that calls a hosted model. It needs no keys and no config, and it spends nothing.
npx @allthingsai/swapcheck # or: uvx swapcheck
It finds model IDs in your code and config, shows which are changing with dates and source links, and writes models.lock. Commit that file. Then:
# Fail CI when something changes within 30 days
npx @allthingsai/swapcheck watch --fail-within-days 30
# Shortlist replacements for a model
npx @allthingsai/swapcheck find gemini-2.5-pro
# Write contract checks and drafts from your code
npx @allthingsai/swapcheck cases init
npx @allthingsai/swapcheck cases accept --all
# Measure your current model, then compare candidates
export GEMINI_API_KEY=...
npx @allthingsai/swapcheck baseline
npx @allthingsai/swapcheck test --to candidate-a,candidate-b
# Edit the IDs and open a pull request
npx @allthingsai/swapcheck bump gemini-2.5-pro --to candidate-b --open-pr
Commands that spend show the maximum cost and the vendors first, and ask. Off a terminal, they refuse to spend without --yes. Keys come from OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY or GOOGLE_API_KEY, and OPENROUTER_API_KEY.
Commands
| Command | What it does | Spends |
|---|---|---|
swapcheck, scan | Find models and show what is changing. Writes models.lock unless --no-lock. | No |
watch | Compare models.lock to the registry. --fail-within-days N exits 1 inside the window. | No |
find <model> | Shortlist replacements. --limit N (default 5). --test runs the shortlist. | Only with --test |
cases init | Write contract checks and drafts from your code. | No |
cases import <kind> <path> | Import from modelbump, modelpin, otel, litellm or langfuse. Traces are redacted unless --allow-unredacted. | No |
cases accept <id>... | --all | Accept generated drafts. Drafts do not run until accepted. | No |
baseline | Measure the current model and its noise. --model picks one. | Yes |
test --to a,b | Compare candidates. --from sets the current model. | Yes |
bump <model> --to <model> | Edit IDs, remove parameters the target rejects, update the lock. --dry-run, --open-pr, --evidence <file>. | No |
report <file> | Render a saved report. Exits with the code its verdict implies. | No |
mcp | Run an MCP server on stdio. | As above |
Options on every command
--format text|json|markdown|sarif, --cwd <dir>, --config <file>, --registry-url <url>, --offline, --yes, --no-color, --debug.
Options on commands that spend
--runs <n> runs per case, --max-usd <amount> hard spending limit, --judge <model> model that grades meaning checks, --route <route> where the model is served, such as aws-bedrock.
Config
swapcheck.yaml is optional. Everything has a default.
models:
- id: gemini-2.5-pro
route: google-ai-studio
paths: ["app/**"]
cases: swapcheck/cases
runs: 5
budget:
max_usd_per_run: 5
policy:
providers: [google, anthropic]
regions: [us, eu]
licenses: [proprietary]
max_usd_per_million_output: 15
watch:
fail_within_days: 30
include_text_in_reports: false
exclude: ["vendor/**"]
| Field | Meaning |
|---|---|
models | Declare models the scanner cannot see, such as IDs built at runtime. |
cases | Folder of case files. |
runs | Runs per case and model. Default 5. |
budget.max_usd_per_run | Hard limit for one paid command, retries included. Default 5. |
policy | What find may suggest: providers, regions, licences, price cap. |
watch.fail_within_days | Default window for watch. Default 30. |
include_text_in_reports | Keep model output in reports. Off by default. |
exclude | Paths the scanner skips. |
models.lock
Hosted model IDs are not pinned by anything in your package manager, so Swapcheck writes a lockfile for them. It records each model, where it is served, its status and when it was last verified.
{
"version": 1,
"models": [
{
"id": "gemini-2.5-pro",
"route": "google-ai-studio",
"status": "reroute_scheduled",
"reroute_to": "gemini-3.8-flash",
"effective": "2026-12-01",
"verified_at": null,
"baseline": null
}
]
}
Commit it. watch compares it to the registry, so a change shows up in CI even though your code did not move. It cannot stop a provider changing a model. It makes the change visible.
Files in your repo
swapcheck.yaml (optional), models.lock, swapcheck/cases/*.json, swapcheck/reports/<id>.json, swapcheck/baselines/<hash>.json.
Cases
A case is a request plus checks on the response. Available checks: json_valid, json_schema, label_in, contains, not_contains, regex, tool_called, tool_not_called, tool_args_valid, no_refusal, refusal, max_tokens_output and meaning, which a judge model grades.
Generated cases are excluded until you accept them. Multi-turn cases replay only up to each decision point. Later logged user turns are never replayed against a new model's replies.
GitHub Action
name: Model watch
on:
pull_request:
schedule:
- cron: "0 6 * * 1"
permissions:
contents: read
jobs:
watch:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: allthingsN/swapcheck@v1
with:
fail-within-days: 30
A scheduled run matters, because the point is to notice a change when your code has not moved.
| Input | Meaning |
|---|---|
mode | watch (default), check, find or bump. |
models | Models to act on. Defaults to what the scan finds. |
candidates | Models to compare in check mode, or targets in bump mode. |
cases, runs | Case folder and runs per case. Default to the config. |
max-usd | The most this job may spend. check mode spends nothing without it. |
fail-within-days | Fail when a change takes effect within this many days. |
open-prs | In bump mode, open one pull request per model. Needs GITHUB_TOKEN. |
registry-url, working-directory | Registry snapshot URL and project folder. |
Outputs: verdict, changes-due, report-path, spent-usd and recommended-model. In check mode, recommended-model is the cheapest candidate with no change detected. Pull requests from forks get the free scan only.
MCP server
{ "mcpServers": { "swapcheck": {
"command": "npx", "args": ["-y", "@allthingsai/swapcheck", "mcp"]
} } }
Tools: scan, watch, find and explain_event. Tools that spend money, such as test, appear only when SWAPCHECK_MCP_ALLOW_SPEND=1 is set. The budget for a test call is the lowest of the requested max_usd, budget.max_usd_per_run in your config, and SWAPCHECK_MCP_MAX_USD if set, so the agent can never raise it.
Verdicts
| Verdict | Meaning |
|---|---|
NO_CHANGE_DETECTED | Nothing differed beyond noise on your cases. Not a guarantee. |
CHANGED | Check results, tool calls or token use differed beyond noise, with no confirmed drop. |
REGRESSED | At least one case dropped and the drop repeated on a fresh sample. |
NOT_ENOUGH_EVIDENCE | Error rate above 20%, the budget ran out, or fewer than six usable cases. |
How a verdict is reached
- Baseline: the current model, N runs per case (default 5).
- Noise: compare two independent halves or a second baseline, and report the false-change rate.
- Candidate: N runs per case on each candidate.
- Per case: two-sided Fisher exact test on pass counts, Benjamini-Hochberg across the suite.
- Suite: paired permutation test across cases on pass-rate difference, with a bootstrap 95% interval.
- Any case with q < 0.1 and a lower candidate pass rate is re-run on a fresh sample of N, and must reproduce to count as confirmed.
Exit codes
| Code | Meaning |
|---|---|
0 | Fine. |
1 | A regression, or a change inside the failure window. |
2 | Config or usage error. |
3 | Could not measure: budget hit, provider errors, or not enough evidence. |
Limits
- It only knows what your cases cover. A behaviour you do not test can change without any verdict noticing.
- Five runs detect large drops, not small ones. Raise
--runsto find smaller differences, at more cost. - The registry is what providers announce. An unannounced change will not appear. Dates can move.
- The scanner reads source statically. IDs built at runtime are reported as unresolved. Declare them in
swapcheck.yaml. - LiteLLM route names are not model IDs. In a proxy config or Router, Swapcheck checks
litellm_params.modeland skipsmodel_name, also where other code calls the proxy by that name. - Bedrock changes are per region. A change is reported when it covers the region in
aws_region_nameorregion_name, or the regions an inference profile such asus.reaches. - An Azure deployment hides its version. For a deployment such as
gpt-4o, the soonest retirement among Azure's dated versions is shown. - A result describes one day. A hosted model can change after you test it.
bumpedits IDs and parameters. It does not rewrite your prompts.- Accuracy has not been published yet. The test suite targets under 5% false alarms and 80% of seeded regressions caught, in simulation.