●SCIENCE — Claude Science launches in beta, a workbench that unifies research tools and produces auditable artifacts●MODEL — Fast mode for Claude Opus 4.7 retires on July 24; migrate to Opus 4.8 fast mode●CODE — Claude Code v2.1.195 adds a toggle to disable mouse clicks in fullscreen mode●CODE — Hyphenated hook matchers now match exactly instead of substring-matching●AGENT — Claude Science pairs a coordinating agent with specialists and a reviewer that checks citations and math●CLOUD — Claude is generally available in Microsoft Foundry on Azure with Azure-native access●SCIENCE — Claude Science launches in beta, a workbench that unifies research tools and produces auditable artifacts●MODEL — Fast mode for Claude Opus 4.7 retires on July 24; migrate to Opus 4.8 fast mode●CODE — Claude Code v2.1.195 adds a toggle to disable mouse clicks in fullscreen mode●CODE — Hyphenated hook matchers now match exactly instead of substring-matching●AGENT — Claude Science pairs a coordinating agent with specialists and a reviewer that checks citations and math●CLOUD — Claude is generally available in Microsoft Foundry on Azure with Azure-native access
When the Model Survives but One Parameter Expires: A Dated Deprecation Calendar for Claude API Requests
Your model ID can stay valid while a parameter you pinned quietly reaches its sunset date and takes the batch down with it. Here is a design that breaks a request into parts, gives each part its own expiry date, and catches the problem before the call goes out — with working TypeScript and real operational numbers.
One of the batches I run overnight started returning a quiet 400 one morning.
I had not touched the model name. claude-opus-4-7 was still listed in the docs. Yet the call failed. When I traced it, the cause was not the model at all — it was the single word I had been attaching for months: speed: "fast". The model was staying alive; only a particular parameter combination on top of it had crossed its announced sunset date.
I have written before about watching for model retirement: probe the model once at startup, distinguish retirement, withdrawal, and regional restriction, and rewrite the run config accordingly. But this incident slipped through that net. The probe only confirms that the model responds. It never asks whether the model still accepts the parameters I am attaching to it.
This article reframes a request as a bundle of parts — model, parameters, headers — gives each part its own expiry date, and checks them before the run begins. Running Dolice Labs' several sites unattended as an indie developer, I have found that this kind of single-part expiry is the hardest to notice and the quietest at draining value. This is the small, reliable mechanism that closes that gap.
A model's lifespan and a request part's lifespan are two different things
Most preflights quietly assume that a request equals a model. If the model ID is valid, let it through; if not, swap it. That assumption holds when a model retires whole.
Real deprecations, though, arrive at a finer grain.
Kind of deprecation
Example
Caught by a model probe?
Model ID retirement
claude-opus-4-7 itself is withdrawn
Yes
Parameter sunset
Model stays, but speed: "fast" starts erroring
No
Beta header retirement
A feature in anthropic-beta graduates to GA and disappears
No
Default change
Omitted-value behavior shifts, breaking an implicit dependency
No
The bottom three rows are the problem. The model responds, so the probe reports green — yet the request as a whole is rejected. To watch for this partial death, you have to lower the unit of monitoring from the model to the parts of the request.
The idea is simple. Express a request as a set of parts, each with its own lifespan. Then keep a ledger that binds each part to a date — "when does this stop working" — inside the code rather than outside it.
Modeling parts and the deprecation calendar with types
First, decide how to pull "parts with a lifespan" out of a request. Here a part is a kind and a value.
// A request part that carries its own lifespantype RequestPart = | { kind: "model"; value: string } // e.g. "claude-opus-4-7" | { kind: "param"; value: string } // e.g. "speed=fast" | { kind: "beta"; value: string }; // e.g. "anthropic-beta: code-execution-2026"// One row of the deprecation calendarinterface DeprecationEntry { part: RequestPart; // The instant the deprecation takes effect (ISO 8601, always with a timezone) effectiveAt: string; // e.g. "2026-07-24T00:00:00-07:00" // A migration hint. Not for humans to skim — logged so the run records itself migrateTo: string; // The source, so you can re-verify later source: string;}
Writing effectiveAt with a timezone is the crucial part. Deprecation notices are usually stated in the provider's timezone; if you naively keep only a date, later comparisons interpret it in your environment's timezone and the boundary drifts by half a day. I once handled a log date without its timezone and overwrote the previous day's file; since then, "anything that carries a time carries its zone" has been an iron rule for me.
The calendar is nothing more than an array of these rows. Its purpose is to gather scattered knowledge in one place, so this single file is the source of truth.
Then a careful refinement. speed=fast is not sunset uniformly across every model — it is sunset onclaude-opus-4-7 while it continues on claude-opus-4-8. What we actually want to watch is not a lone part but a co-occurrence of parts. So widen a calendar row to fire when a set of parts appears together.
interface DeprecationEntry { // Fires when all of these parts appear together when: RequestPart[]; effectiveAt: string; migrateTo: string; source: string;}const DEPRECATION_CALENDAR: DeprecationEntry[] = [ { when: [ { kind: "model", value: "claude-opus-4-7" }, { kind: "param", value: "speed=fast" }, ], effectiveAt: "2026-07-24T00:00:00-07:00", migrateTo: "Move onto claude-opus-4-8 (speed=fast continues on 4.8)", source: "anthropic changelog 2026-07-01", },];
Now "the model survives but this combination dies" can be expressed exactly.
✦
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
✦Catches parameter-level sunsets that model-ID retirement checks miss, by decomposing a request into parts and dating each one
✦A warn-ahead / fail-on-date verdict that warns N days early and blocks the run once the date passes, with the timezone handling that keeps the boundary honest
✦A CI test design that makes it impossible to ship a live request that still carries an expired part
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.
Next, extract the set of parts from the request you are about to send. This is a plain mapping; the only thing that matters is that nothing is missed.
interface OutgoingRequest { model: string; params?: Record<string, string | number | boolean>; betaHeaders?: string[];}function decompose(req: OutgoingRequest): RequestPart[] { const parts: RequestPart[] = [{ kind: "model", value: req.model }]; for (const [k, v] of Object.entries(req.params ?? {})) { parts.push({ kind: "param", value: `${k}=${v}` }); } for (const b of req.betaHeaders ?? []) { parts.push({ kind: "beta", value: b }); } return parts;}
Decide part equality on kind and value alone. Normalizing to strings lets you use set operations directly.
const partKey = (p: RequestPart) => `${p.kind}:${p.value}`;// Does req contain every part of entry.when?function matches(reqParts: RequestPart[], entry: DeprecationEntry): boolean { const have = new Set(reqParts.map(partKey)); return entry.when.every((w) => have.has(partKey(w)));}
Separating warning from stopping, before the run
This is the center of the design. A part that has not yet reached its sunset date warns you how many days remain; a part already past its date stops the run itself. Call the former warn-ahead and the latter fail-on-date.
type Verdict = | { level: "ok" } | { level: "warn"; daysLeft: number; entry: DeprecationEntry } | { level: "expired"; entry: DeprecationEntry };const DAY = 24 * 60 * 60 * 1000;function evaluate( req: OutgoingRequest, now: Date, warnWindowDays: number,): Verdict[] { const parts = decompose(req); const verdicts: Verdict[] = []; for (const entry of DEPRECATION_CALENDAR) { if (!matches(parts, entry)) continue; // A timezone-bearing string resolves to a correct absolute (UTC) instant const effective = new Date(entry.effectiveAt).getTime(); const diff = effective - now.getTime(); if (diff <= 0) { verdicts.push({ level: "expired", entry }); } else if (diff <= warnWindowDays * DAY) { verdicts.push({ level: "warn", daysLeft: Math.ceil(diff / DAY), entry }); } } if (verdicts.length === 0) verdicts.push({ level: "ok" }); return verdicts;}
new Date(entry.effectiveAt) resolves a timezone-bearing ISO string to an absolute UTC instant internally — which is exactly why writing the zone into effectiveAt pays off. Keep only a date here and the comparison against now is dragged into the runtime's local time, so the verdict wobbles for half a day around the boundary.
Place the check at the entry point of the run. Under unattended operation, it is safest to log warnings and continue, but stop on expiry immediately so a human notices.
function preflight(req: OutgoingRequest, warnWindowDays = 14): void { const verdicts = evaluate(req, new Date(), warnWindowDays); for (const v of verdicts) { if (v.level === "warn") { console.warn( `[deprecation] expires in ${v.daysLeft}d: ${JSON.stringify(v.entry.when)} → ${v.entry.migrateTo} (source: ${v.entry.source})`, ); } else if (v.level === "expired") { // Stop clearly, before the call throw new Error( `[deprecation] expired request part: ${JSON.stringify(v.entry.when)}. Migrate to: ${v.entry.migrateTo} (source: ${v.entry.source})`, ); } }}
The caller adds a single line.
function runScheduledBatch(req: OutgoingRequest) { preflight(req); // throws here on expiry, without hitting the API // …the actual Messages API call…}
The effect of that one line is understated but reliable. The moment a sunset date passes, it stops with a message that names the migration target, without ever emitting a wasted 400. No time is spent decoding the error afterward.
Choosing the warn-ahead window from operational feel
How many days warnWindowDays should be depends on how often you revisit your logs. Honestly, I do not look at my sites' auto-posting every day. I check in batches every few days, so too short a window produces the "already past it by the time I noticed" failure.
I keep it at 14 days now. Since adopting that width, the reactive scramble of fixing a batch only after it stopped on the sunset date has disappeared. I used to learn about an expired model or parameter for the first time from an error log about once a month. Since the startup warning began flagging these ahead of time, that miss rate has held at zero.
Make the window too long and warnings fire on every run while everything still works, until the warning itself fades into background noise. "Two to three times your review cadence" is a good target: it guarantees room to act when a warning appears, without ringing constantly.
A CI test so the calendar never rots
The weakness of this design is obvious — if a human forgets to append to the deprecation calendar, none of it matters. So guard the calendar's own health in CI. Two things are worth protecting: that you cannot merge a live config carrying an already-expired row, and that every calendar row at least carries a source.
import { test, expect } from "vitest";// The configs your scheduled tasks actually send (make it reflect production truth)import { LIVE_REQUESTS } from "./live-requests";test("no live request carries an expired part", () => { const now = new Date(); for (const req of LIVE_REQUESTS) { const verdicts = evaluate(req, now, 0); const expired = verdicts.filter((v) => v.level === "expired"); expect( expired, `an expired config remains: ${JSON.stringify(req)}`, ).toHaveLength(0); }});test("every calendar row carries a source and a migration target", () => { for (const entry of DEPRECATION_CALENDAR) { expect(entry.source.length).toBeGreaterThan(0); expect(entry.migrateTo.length).toBeGreaterThan(0); }});
The first test targeting the actually-running configs, LIVE_REQUESTS, is the point. Verifying the calendar in the abstract means nothing if production configs do not follow it. If a live config still carries an expired row, CI goes red right then, and a deploy that crosses a sunset date is stopped. The official docs do not cover how to fold a deprecation notice into your own code. These two tests are the minimum way to move that folding from human memory into machinery.
Where to start
If you have processes running unattended, the safe start is to slot only decompose and evaluate in front of an existing call and emit warn-level logs alone. Turning on the stop is worth doing only after you have written one real deprecation notice into the calendar and confirmed the test goes red on that date as expected.
If you already have a mechanism watching for model retirement, layer this part-level watch on top of it. A slip like this one — the model alive while a single part dies — can then be closed at the same entry point, together.
If it removes one quiet miss for anyone else running several automated jobs, I will be glad. Thank you for reading.
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.