◉CLAUDE LABJP
●2.1.302 — Fork subagents are now on by default in claude -p, the Agent SDK and the desktop app. Turn them off with CLAUDE_CODE_FORK_SUBAGENT=0 (10/16)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 43 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…●2.1.302 — Fork subagents are now on by default in claude -p, the Agent SDK and the desktop app. Turn them off with CLAUDE_CODE_FORK_SUBAGENT=0 (10/16)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 43 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…
Articles/API & SDK
⬡ API & SDK/2026-10-18Intermediate

43 Days Until Sonnet 4.5 Retires: I Counted Callers From the Usage CSV, Not From grep

claude-sonnet-4-5 retires on November 30. grep only shows what is written in code. Here is a single script that cross-checks model IDs in your code against the Console's Usage CSV, with the results from running it.

Claude API129Sonnet 4.5model deprecationUsageinventory

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 IDStatusRetirementRecommended replacement
claude-sonnet-4-5-20250929Deprecated (announced Sep 30)2026-11-30claude-sonnet-5-5
claude-opus-4-5-20251101ActiveNot sooner than Nov 24Not announced
claude-haiku-4-5-20251001ActiveNot sooner than Oct 15Not 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 --code

It 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,190000
python3 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,000

Those 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:

  1. From the Usage CSV, identify which keys actually send claude-sonnet-4-5-20250929 traffic, and write down who owns each and what it's for.
  2. 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.
  3. Before replacing the ID, check the migration guide for argument differences (budget_tokens, prefill, tool_choice and the like — I listed the ones that return 400 in "Sonnet 4.5 の退役前に、コードに残った「400 になる引数」を一本のスクリプトで拾いました").
  4. 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.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⬡ API & SDK2026-10-16
Sending 40 Rows From Make to the Claude API Stops at Row 14 — Avoiding the 429 With One Subtraction
Why a no-code automation that sends spreadsheet rows to the Claude API one by one hits a 429 partway through, and how to fix it. Seven spacing strategies tested on a mock bucket, a formula for the Sleep delay, and settings that stop failed rows from vanishing.
⬡ API & SDK2026-10-13
Haiku 5.5 is cheaper per token, but your bill jumps fivefold past about 77k Haiku 4.5 tokens
On the price sheet, Haiku 5.5 costs roughly a tenth of Haiku 4.5. But the same text becomes about 30% more tokens, and a prompt over 100k tokens is billed at five times the rate. Here is the boundary in Haiku 4.5 terms, a script I actually ran, and the changes that return a 400.
⬡ API & SDK2026-10-08
Before Sonnet 4.5 retires, one script to find the arguments that will return a 400
claude-sonnet-4-5 retires on November 30. Swapping the model ID is not enough: budget_tokens, prefill, and tool_choice will fail. Here is a scanner built from the official migration guide, its real output, and the order I would fix things in.