After checking the Sonnet 4.5 retirement date in the official table, the first thing I did was grep my repository for claude-sonnet-4-5.
What came back was tags, URL slugs and old article text. Not one line that actually calls the API. As an indie developer running a few sites and apps on my own, I've learned that the call I don't remember writing is the one that worries me most. That didn't settle my nerves, though, because grep can't see the calls that aren't written in code: environment variables, scripts on another machine, no-code integrations, an old key I'd forgotten was still live.
The retirement date is November 30, 2026, and today is October 18 — 43 days left. That's enough time, but I decided to stop hunting for strings and count from the side of what was actually used. Here's the procedure.
The dates the docs commit to
On the official model deprecations page (checked October 18, 2026), these are the models carrying a date:
| Model ID | Status | Retirement | Recommended replacement |
|---|---|---|---|
claude-sonnet-4-5-20250929 | Deprecated (announced Sep 30) | 2026-11-30 | claude-sonnet-5-5 |
claude-opus-4-5-20251101 | Active | Not sooner than Nov 24 | Not announced |
claude-haiku-4-5-20251001 | Active | Not sooner than Oct 15 | Not announced |
The bottom two rows are not retirement notices. They are the earliest possible dates in the docs' "Not sooner than" column. Still, Sonnet 4.5 went from announcement to retirement in 61 days, and the docs promise at least 60 days' notice for publicly released models. So I treat those two rows as "a notice could land any day" and include them in the inventory.
One more caveat: these dates apply to Anthropic-operated platforms (the Claude API, Claude Platform on AWS, Microsoft Foundry). Amazon Bedrock and Google Cloud keep their own schedules, so if you call through Bedrock, check its table instead.
grep only tells you what is written
I split the inventory into two tools:
- Code side: count model IDs left in config files and scripts (what is written)
- Usage side: export a CSV from the Console's Usage page and look at volume per key and per model (what actually ran)
The official steps are simple: open Usage in the Console, click Export, and read the downloaded CSV for a per-key, per-model breakdown. A line in code that never moved a token can wait. A key that moved tokens but appears nowhere in code is the first thing to find an owner for.
Where I tripped first was the code-side count. Matching anything that starts with claude- across the whole repository pulled in model names inside articles and returned 417 hits. A big number makes it feel like plenty is left. Doubt the kinds of strings before you trust the count — that became the first line of the script's spec.
One script for both counts
The script walks a directory, counts model IDs, and annotates the ones with a date with their deadline and replacement. --code restricts it to code and config extensions. Pass --csv with a Usage export and it groups by the column whose header contains model and sums every numeric column.
I deliberately didn't hard-code column names. I haven't been able to confirm the Console CSV's headers — the docs only say the export breaks usage down by API key and model, without naming columns. So the script looks for a header containing model, and if it finds none it prints the headers and stops.
#!/usr/bin/env python3
"""model_inventory.py — コード内のモデル ID と、Console の Usage CSV を同じ表で突き合わせる。
使い方: python3 model_inventory.py <ディレクトリ> [--code] [--csv usage.csv]
--code 設定・コードの拡張子だけを見る(記事本文やタグの文字列を拾わない)
"""
import csv, re, sys, pathlib, collections
# 公式 model-deprecations の「Model status」表(2026-10-18 取得)から、期限のあるものだけ
RETIRE = {
"claude-sonnet-4-5-20250929": ("2026-11-30", "claude-sonnet-5-5"),
"claude-opus-4-5-20251101": ("2026-11-24以降(最短)", "claude-opus-5-5"),
"claude-haiku-4-5-20251001": ("2026-10-15以降(最短)", "claude-haiku-5-5"),
}
SKIP_DIRS = {".git", "node_modules", ".next", "dist", "build", "__pycache__", "generated"}
CODE_EXT = {".py", ".js", ".mjs", ".ts", ".tsx", ".sh", ".yaml", ".yml", ".toml", ".env", ".cfg", ".ini"}
SKIP_EXT = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".pdf", ".zip", ".lock", ".woff", ".woff2"}
# 数字で終わる ID だけを拾う。slug(claude-opus-4-6-release など)を巻き込まない
ID_RE = re.compile(
r"claude-(?:(?:sonnet|opus|haiku|fable|mythos)-\d+(?:-\d+)?(?:-\d{8})?"
r"|3-(?:5-haiku|haiku|7-sonnet)-\d{8})(?![A-Za-z0-9]|-[A-Za-z0-9])"
)
def scan(root, code_only=False):
hits = collections.defaultdict(list)
for p in pathlib.Path(root).rglob("*"):
if not p.is_file() or p.suffix.lower() in SKIP_EXT:
continue
if any(part in SKIP_DIRS for part in p.parts):
continue
if code_only and p.suffix.lower() not in CODE_EXT and p.name != ".env":
continue
try:
text = p.read_text(encoding="utf-8")
except (UnicodeDecodeError, OSError):
continue
for n, line in enumerate(text.splitlines(), 1):
for m in ID_RE.findall(line):
hits[m].append(f"{p}:{n}")
return hits
def read_usage(path):
"""model を含む見出しの列でグループ化し、数値列を合計する。列名は固定しない。"""
with open(path, newline="", encoding="utf-8-sig") as f:
rows = list(csv.DictReader(f))
if not rows:
return {}
cols = rows[0].keys()
mcol = next((c for c in cols if "model" in c.lower()), None)
if mcol is None:
sys.exit("model を含む列が見つかりません: " + ", ".join(cols))
numeric = [c for c in cols if c != mcol and all(_num(r[c]) is not None for r in rows if r[c] != "")]
out = collections.defaultdict(lambda: collections.Counter())
for r in rows:
for c in numeric:
v = _num(r[c])
if v is not None:
out[r[mcol]][c] += v
return out
def _num(s):
try:
return float(str(s).replace(",", ""))
except ValueError:
return None
def main():
args = sys.argv[1:]
root = args[0] if args else "."
csv_path = args[args.index("--csv") + 1] if "--csv" in args else None
hits = scan(root, code_only="--code" in args)
print(f"== コード内のモデル ID({root})")
for mid in sorted(hits, key=lambda k: (k not in RETIRE, k)):
note = ""
if mid in RETIRE:
d, to = RETIRE[mid]
note = f" ← 期限 {d} / 置き換え先 {to}"
print(f"{len(hits[mid]):4d} {mid}{note}")
if mid in RETIRE:
for loc in hits[mid][:5]:
print(f" {loc}")
if csv_path:
print(f"\n== Usage CSV({csv_path})")
for model, cnt in read_usage(csv_path).items():
flag = " ← 期限あり" if model in RETIRE else ""
print(model + flag)
for c, v in cnt.items():
print(f" {c}: {v:,.0f}")
if __name__ == "__main__":
main()The ID regex has a quirk. To avoid swallowing URL slugs such as claude-sonnet-4-6-vs-..., a lookahead rejects any match followed by an alphanumeric character or a hyphen plus alphanumeric. My first version was looser: it reported claude-sonnet-4 on its own four times, because backtracking let only the prefix of a longer slug match.
What it printed
First I ran it over this site's repository (Next.js), code and config only:
python3 model_inventory.py ~/repos/claudelab.net --codeIt printed the header line and nothing else. There is not a single line in this repository that calls the Claude API. Without --code it reports over 400 matches, every one of them article text, tags or URL slugs. A large count and a needed migration are different things.
To check the Usage side, I wrote a tiny CSV by hand (it is not a real Console export, and the column names are my own placeholders):
date,api_key,model,input_tokens,output_tokens
2026-10-01,key_wallpaper_batch,claude-sonnet-4-5-20250929,1200000,210000
2026-10-01,key_wallpaper_batch,claude-sonnet-5-5,300000,52000
2026-10-01,key_translate,claude-haiku-4-5-20251001,880000,140000
2026-10-02,key_translate,claude-haiku-4-5-20251001,910000,150000
2026-10-02,key_wallpaper_batch,claude-sonnet-4-5-20250929,1100000,190000python3 model_inventory.py ~/repos/claudelab.net --code --csv usage_sample.csv== Usage CSV (usage_sample.csv)
claude-sonnet-4-5-20250929 ← has a deadline
input_tokens: 2,300,000
output_tokens: 400,000
claude-sonnet-5-5
input_tokens: 300,000
output_tokens: 52,000
claude-haiku-4-5-20251001 ← has a deadline
input_tokens: 1,790,000
output_tokens: 290,000Those numbers are made up. What I'd like you to see is the shape: deadline models get flagged, and the migrated volume (claude-sonnet-5-5) sits next to the unmigrated volume on one screen. If your real CSV uses different headers, the only line to touch is the "model" in c.lower() check inside read_usage.
After you find a row: the order I'll follow
I won't stop at swapping strings. The order:
- From the Usage CSV, identify which keys actually send
claude-sonnet-4-5-20250929traffic, and write down who owns each and what it's for. - Find the caller. If the code-side scan doesn't turn it up, open environment variables, CI variables and any no-code integration settings in turn.
- Before replacing the ID, check the migration guide for argument differences (budget_tokens, prefill,
tool_choiceand the like — I listed the ones that return 400 in "Sonnet 4.5 の退役前に、コードに残った「400 になる引数」を一本のスクリプトで拾いました"). - A week after the switch, export the CSV again and confirm the old ID's row hasn't grown.
Step 4 is there because "done" is easy to declare too early. If an old copy of a config lives on another machine, the old ID will start flowing again. Re-reading one export catches that kind of miss, so I'd rather spend the few minutes.
Next step
This week, export one CSV from the Console's Usage page. Sort by the model column and look only for claude-sonnet-4-5-20250929. If it's there, today's job is to write down the owner and purpose of each API key involved — nothing more.
What you're looking for isn't a string; it's the owner of the tokens that flowed. Of the 43 days left, I'm spending the first one on exactly that.