●2.1.310 — Monitor tool now works on Bedrock, Vertex and Foundry, and gateway failures are fixed (10/23)●2.1.307 — Fixes MCP failing to reconnect after sleep or a keychain lock (10/21). 2.1.306 also fixed the false claude -p login message●SONNET 4.5 — Retires on the Claude API on 11/30, 34 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●API — In API version 2026-10-21, passing temperature, top_p or top_k returns a 400 error●NEW — I Put a Ten-Line Promise File at the To…●2.1.310 — Monitor tool now works on Bedrock, Vertex and Foundry, and gateway failures are fixed (10/23)●2.1.307 — Fixes MCP failing to reconnect after sleep or a keychain lock (10/21). 2.1.306 also fixed the false claude -p login message●SONNET 4.5 — Retires on the Claude API on 11/30, 34 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●API — In API version 2026-10-21, passing temperature, top_p or top_k returns a 400 error●NEW — I Put a Ten-Line Promise File at the To…
Haiku 4.5's October 15 Floor Has Passed, but No Stop Date Has Been Set
An Active model's date in the deprecations table is a floor, not a schedule. Using the 60-day notice rule and past retirement gaps, here is how I audit pinned model IDs.
I was auditing the Claude code samples on my own site against the deprecations table when one line in the script's output looked wrong.
claude-haiku-4-5-20251001, 76 files. The official table says "Not sooner than October 15, 2026", and today is October 27. The date passed twelve days ago.
My first reaction was to wonder whether I should turn everything red and fix it tonight. It turned out the answer was no. Below is how I read the date, and a script you can run on your own repository to do the same audit.
"Not sooner than" is a floor, not a schedule
The deprecations page lists a status and a date for every model, and the date means something different depending on the status.
Status
What the date means
Example (as of Oct 27)
Retired
The day the model stopped working
claude-opus-4-1-20250805 (Aug 5)
Deprecated
The confirmed day it will stop working
claude-sonnet-4-5-20250929 (Nov 30)
Active
"Not sooner than": the earliest it can stop
claude-haiku-4-5-20251001 (Oct 15)
The same page promises at least 60 days' notice before a publicly released model is retired, and says affected customers are told by email and in the documentation. That lets you work backwards. An Active model cannot stop earlier than 60 days after a notice goes out. Even if a notice appeared today, the earliest date for Haiku 4.5 would be December 26, 2026.
The page does not define "Not sooner than", so reading it as a floor is my interpretation. The past retirements in the table at least do not contradict it.
Model
Deprecated
Retired
Gap
claude-3-7-sonnet-20250219
2025-10-28
2026-02-19
114 days
claude-3-5-haiku-20241022
2025-12-19
2026-02-19
62 days
claude-3-haiku-20240307
2026-02-19
2026-04-20
60 days
claude-sonnet-4-20250514
2026-04-14
2026-06-15
62 days
claude-opus-4-1-20250805
2026-06-05
2026-08-05
61 days
claude-sonnet-4-5-20250929
2026-09-30
2026-11-30
61 days
Five of the six gaps sit between 60 and 62 days; one is 114. None is shorter than 60. For the Sonnet 4.5 deadline itself, I wrote up counting callers from the Usage CSV.
What happens if you call every past date "expired"
My first version of the script flagged anything with a date in the past. The output looked like this:
Haiku 4.5 landed with the same weight as models that really are gone, across 76 files, even though nothing has stopped and no notice exists. Left alone, a nightly check like that shows the same red every night, and a real Deprecated entry would disappear into it.
Two things went against my intuition. A past date on an Active model does not mean it has been retired. And a future date on a Deprecated model is more urgent than an Active one, because the deadline is confirmed. The classes had to be built from status plus the meaning of the date, not from before and after today.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦You can now tell whether a passed date in the deprecations table means a confirmed stop, before you start a panicked bulk replace
✦You can now put a nightly check in your own repo that does not raise false alarms, because it knows what the date means for Active, Deprecated and Retired
✦You will be able to decide today what to finish first inside the 60-day window, so the morning a notice arrives is calm
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
The deprecations table goes into a JSON file. I copy it by hand from the official page and keep the fetch date next to it. I'll explain why I don't scrape it near the end.
Here is the script. Save it as retirement_audit.py in the same folder as the test below. All the logic lives in one function, classify.
#!/usr/bin/env python3"""retirement_audit.py — 書かれているモデル ID を、廃止表の「状態」と「日付の意味」で仕分ける。使い方: python3 retirement_audit.py <dir> --table retirement_table.json [--today YYYY-MM-DD]終了コード: 0=要対応なし / 1=退役済み・退役予定あり / 2=表が古い(判定を信用しない)"""import re, sys, json, pathlib, datetime as dt, collectionsID_RE = re.compile(r"claude-(?:sonnet|opus|haiku|fable)(?:-\d+){1,3}(?:-\d{8})?(?![\w.])" r"|claude-3(?:-[57])?-(?:sonnet|haiku)(?:-\d{8})?(?![\w.])")FENCE = re.compile(r"```.*?```", re.S)SKIP = {"node_modules", ".git", ".next", "dist"}def classify(entry, today, notice_days): """返り値: (区分, 日数)。active の日付は『これより早くは終わらない下限』であって予定日ではない。""" d = dt.date.fromisoformat(entry["date"]) left = (d - today).days if entry["status"] == "retired": return "RETIRED", left if entry["status"] == "deprecated": return ("RETIRED" if left <= 0 else "DEPRECATED"), left # active: 告知が無い限り、今日から数えて notice_days 日より前には止まらない earliest = max(d, today + dt.timedelta(days=notice_days)) if left < 0: return "FLOOR_PASSED", (earliest - today).days # 下限は過ぎた。止まるのは早くても告知の60日後 return ("FLOOR_NEAR" if left <= notice_days else "OK"), leftdef scan(root, table, today): models, notice = table["models"], table["notice_days"] seen = collections.defaultdict(lambda: {"any": set(), "code": set()}) for p in pathlib.Path(root).rglob("*"): if not p.is_file() or p.suffix not in {".mdx", ".md", ".py", ".ts", ".js", ".mjs", ".json", ".yaml", ".yml", ".sh"}: continue if SKIP & set(p.parts): continue t = p.read_text(encoding="utf-8", errors="ignore") code = "\n".join(FENCE.findall(t)) if p.suffix in {".mdx", ".md"} else t for m in ID_RE.finditer(t): seen[m.group(0)]["any"].add(str(p)) for m in ID_RE.finditer(code): seen[m.group(0)]["code"].add(str(p)) rows = [] for mid, f in seen.items(): if mid in models: cls, n = classify(models[mid], today, notice) else: cls, n = "NOT_IN_TABLE", None rows.append({"id": mid, "class": cls, "days": n, "files": len(f["any"]), "code_files": len(f["code"])}) return rowsdef main(): root = sys.argv[1] table = json.load(open(sys.argv[sys.argv.index("--table") + 1])) today = dt.date.fromisoformat(sys.argv[sys.argv.index("--today") + 1]) if "--today" in sys.argv else dt.date.today() age = (today - dt.date.fromisoformat(table["fetched"])).days if age > 14: print(f"STALE: 廃止表の取得から {age} 日。公式ページを開き直してください"); sys.exit(2) rows = scan(root, table, today) order = ["RETIRED", "DEPRECATED", "FLOOR_PASSED", "FLOOR_NEAR", "NOT_IN_TABLE", "OK"] rows.sort(key=lambda r: (order.index(r["class"]), -r["code_files"])) for r in rows: if r["class"] == "NOT_IN_TABLE": continue d = "" if r["days"] is None else f'{r["days"]:+d}d' print(f'{r["class"]:<13}{r["id"]:<30}any={r["files"]:<4}code={r["code_files"]:<4}{d}') bad = sum(r["code_files"] for r in rows if r["class"] in ("RETIRED", "DEPRECATED")) sys.exit(1 if bad else 0)if __name__ == "__main__": main()
For FLOOR_PASSED, the number is not "floor minus today". It is the days until the earliest possible stop if a notice went out today. It reads 60 because that cannot shrink while no notice exists.
The counting has one more detail. In MDX files, IDs mentioned in prose are counted separately from IDs inside code fences, which are the lines a reader would actually run. The gap between any and code is the number of IDs that appear only as explanation.
The result
This is the output over this site's Japanese articles on October 27, 2026.
Rows are ordered by priority. The four retired IDs and the one Deprecated ID are candidates for rewriting. Some of those mentions exist precisely to say "this ID is retired", which is what the any and code gap shows, so I read each hit in context before replacing anything. I wrote about that in checking the catalog before a bulk replace.
What to do with the 76 Haiku 4.5 files
Looking inside, 72 lines pinned the dated ID in a model: field and 28 used the undated alias claude-haiku-4-5. The alias is moved by the provider; the dated ID is fixed. What matters is not the rewrite priority but who has to fix it on the day the stop date arrives.
Situation
What to do today
Why
A dated ID is pinned in production
Pick a replacement candidate and run one test call against it
Notice comes at least 60 days ahead. No rush, but two months is short if you only start looking after the notice
You use the alias
Nothing, but keep watching the table
The provider manages where the alias points
It appears as an example in docs or articles
Fix it the next time you touch the file
It does not run, but readers copy and paste it
As an indie developer running a site, I treat the code samples in my articles as production code I wrote myself, because readers paste them and run them. That is why I handle them in this order. Rewriting an ID before any notice exists leaves you unable to explain later why you changed it. Once the reason is an official notice, the bulk rewrite can begin.
A self-test and a nightly entry point
The class boundaries are the part most likely to break when someone edits the code later, so I pin the edge cases first.
Exit code 2 (table too old) closes the last hole. I copy the table by hand, so once the fetch date is more than 14 days old the script refuses to judge. A stale table that says "nothing to do" is more dangerous than a red line.
Pitfalls
The regex makes the -YYYYMMDD suffix optional so that it catches both dated IDs and aliases. An ID that is not in the table is only counted as NOT_IN_TABLE and left out of the output. Whether a dated ID is real at all needs a separate look
The date field means different things for Active and Deprecated. When you copy the table, copy the status column with it
Confirming that there is no notice is a human job. Whether the page still says Active and whether a retirement email has arrived are outside the script
Next step
Today you can create the JSON table and run the script once on your own repository. If something turns red, look at which kind of red it is: retired, Deprecated, or an Active model past its floor. A date in the past is not the same as a model that has stopped. Building that difference into the classes means that on the morning a real notice arrives, you will not need to hurry.
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.