●BILLING — The Jun 15 change is now live: Agent SDK, headless runs, GitHub Actions, and third-party agents leave subscription limits for separate monthly credits ($20/$100/$200) metered at full API rates, no rollover●RETIRED — As of today, Sonnet 4 and Opus 4 are retired from the API; scripts referencing older models should switch to the latest generation such as Opus 4.8●EXPORT — Claude Fable 5 and Mythos 5 are suspended for all foreign nationals under a US export-control directive (Jun 12); Anthropic calls it a misunderstanding and is working to restore access●SAFE — Only the two new Mythos-class models are affected; every other model including Opus 4.8 keeps running normally●SUBAGENTS — Claude Code sub-agents can now spawn their own sub-agents (up to 5 levels), and Dynamic workflows arrived in research preview●INCIDENT — A Jun 5 outage raised error rates across claude.ai, the API, Claude Code, and Cowork, a reminder to design retries and fallbacks into automated runs●BILLING — The Jun 15 change is now live: Agent SDK, headless runs, GitHub Actions, and third-party agents leave subscription limits for separate monthly credits ($20/$100/$200) metered at full API rates, no rollover●RETIRED — As of today, Sonnet 4 and Opus 4 are retired from the API; scripts referencing older models should switch to the latest generation such as Opus 4.8●EXPORT — Claude Fable 5 and Mythos 5 are suspended for all foreign nationals under a US export-control directive (Jun 12); Anthropic calls it a misunderstanding and is working to restore access●SAFE — Only the two new Mythos-class models are affected; every other model including Opus 4.8 keeps running normally●SUBAGENTS — Claude Code sub-agents can now spawn their own sub-agents (up to 5 levels), and Dynamic workflows arrived in research preview●INCIDENT — A Jun 5 outage raised error rates across claude.ai, the API, Claude Code, and Cowork, a reminder to design retries and fallbacks into automated runs
Centralizing the anthropic-beta Header So a Retired Beta Won't Kill Your Batch
Scattered anthropic-beta headers turn a beta retirement or GA graduation into a 400 that takes down an entire batch. A small capability registry, a startup preflight, and tiered fallback keep your pipeline running across feature generations.
On June 15, 2026, Sonnet 4 and Opus 4 retired from the API, and in the same week Fable 5 and Mythos 5 were temporarily pulled. Swapping a model ID is easy to notice. What is easy to miss is the anthropic-beta header. In the backend that drives automated publishing across my four sites, I came in one morning to find that the overnight batch had not completed a single article. The cause was a call still sending context-1m-2025-08-07, a beta identifier I had written half a year earlier. The moment the beta window closed, that call started returning 400 Bad Request.
The painful part was that this one line did not break a single request. It took down every call that happened to share the same header string. Beta features are useful, but they turn over quickly: retirement, GA graduation, and renames all happen on a scale of months. The point of this article is to absorb that churn in exactly one place, by treating anthropic-beta as a capability registry and wrapping it with a startup preflight and tiered fallback.
How one beta header took down the whole overnight batch
The first implementation looked like what most people write. Calls that wanted long context and prompt caching set the header inline, every time.
The problem with this is that the same string was copied into the article-generation, summarization, tagging, and translation modules until it lived in 14 places. When context-1m-2025-08-07 stopped being accepted, I could not say which of those 14 was responsible without grepping. While I fixed them one by one, the batch stayed down. A single missed overnight run can be recovered by hand the next morning, but once it happens once or twice a week, the reliability of the whole operation starts to feel shaky.
Why a "feature retirement" breaks the pipeline through the header
The anthropic-beta header is an explicit switch that turns on a feature that is not yet generally available. The trouble is that this switch can stop working for three distinct reasons.
The first is retirement. A time-boxed beta such as the 1M context window becomes an unknown identifier once its window closes, and depending on the combination it returns 400. The second is GA graduation. When something like prompt caching moves to general availability, the header is no longer needed. It is often still accepted for a while, but leaving it in place forever only carries a future rejection risk. The third is a name or date change: a dated identifier like code-execution-2025-05-22 can be superseded by a newer dated version when the spec is revised.
What all three share is a structure: things break the instant the world your code knows about drifts away from the world the API will accept. That is exactly why it pays to stop scattering the header string and express the generational change in one place.
✦
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
✦Fold 14 scattered anthropic-beta strings into one Capability registry that the rest of the code never has to know about
✦A startup preflight that detects an unsupported beta and disables only that capability while the rest of the run proceeds
✦Three tiers of fallback for retirement, GA graduation, and runtime rejection, plus a structured log of every disablement
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.
Collapsing scattered headers into a capability registry
The first move is to let the call site declare what capability it wants, not which beta string to send. Define a Capability enum and keep the mapping to identifiers in a single registry.
from enum import Enumclass Capability(Enum): PROMPT_CACHE = "prompt_cache" LONG_CONTEXT = "long_context" CONTEXT_EDIT = "context_edit" CODE_EXECUTION = "code_execution"# Currently valid beta identifiers. None means GA (no header needed).# Identifiers change over time; confirm the latest in the official release notes.BETA_IDS: dict[Capability, str | None] = { Capability.PROMPT_CACHE: None, # GA -> no header Capability.LONG_CONTEXT: "context-1m-2025-08-07", Capability.CONTEXT_EDIT: "context-management-2025-06-27", Capability.CODE_EXECUTION: "code-execution-2025-05-22",}def beta_header(caps: set[Capability], enabled: set[Capability]) -> dict[str, str]: """Join only the requested capabilities that are enabled and need a header.""" ids = sorted( BETA_IDS[c] for c in caps & enabled if BETA_IDS.get(c) is not None ) return {"anthropic-beta": ",".join(ids)} if ids else {}
That alone frees the call site from identifier strings.
# After: declare the capability you want; the registry knows the identifier.headers = beta_header( caps={Capability.LONG_CONTEXT, Capability.PROMPT_CACHE}, enabled=ENABLED, # the enabled set fixed by preflight (below))resp = client.messages.create( model="claude-opus-4-8", max_tokens=4096, messages=messages, extra_headers=headers,)
When context-1m-2025-08-07 retires, you change exactly one line in BETA_IDS. The grep-and-fix tour across 14 sites is gone. On GA graduation you just set the value to None, and the header drops out automatically.
A startup preflight that disables only the unusable beta
Even with a registry, you cannot know whether an identifier is still accepted until you actually call it. So before the batch does any real work, verify each capability once and keep only the ones that pass as the enabled set. I prefer using the cheap count_tokens endpoint for this check.
import anthropic, loggingclient = anthropic.Anthropic()log = logging.getLogger("beta_preflight")def preflight(candidates: set[Capability]) -> set[Capability]: """Verify each capability individually; return only those the API accepts.""" enabled: set[Capability] = set() for c in candidates: if BETA_IDS.get(c) is None: # GA capabilities need no check enabled.add(c) continue try: client.messages.count_tokens( model="claude-opus-4-8", messages=[{"role": "user", "content": "preflight"}], extra_headers={"anthropic-beta": BETA_IDS[c]}, ) enabled.add(c) except anthropic.BadRequestError as e: # Unknown/retired betas return 400. Drop only that capability; keep going. if "beta" in str(e).lower(): log.warning("capability disabled: %s (%s)", c.value, e) else: raise # a 400 unrelated to beta must not be swallowed return enabledENABLED = preflight({ Capability.LONG_CONTEXT, Capability.CONTEXT_EDIT, Capability.PROMPT_CACHE,})
The key is to verify capabilities one at a time, not in a single combined request. If you send them together, you cannot tell whether a retired one dragged the still-healthy ones down with it. Independent checks give you an accurate state right at startup: "LONG_CONTEXT is off, everything else is healthy." In production, fix this ENABLED set once at process start and have every later request read from it.
Three tiers of fallback for retirement, GA, and errors
Preflight catches retirements that are knowable at startup. In real operation a beta can also be disabled mid-run, or long context can become unavailable while the process is live. So decide, per capability, what the alternative is when it cannot be used.
The first tier covers cases where simply dropping the header and resending works. Prompt caching, if its header is rejected, just stops caching; the generation itself still succeeds. Here it is enough to silently drop the header and retry.
The second tier covers cases that need a substitute for the feature itself. If long context is unavailable, split the input and switch to hierarchical summarization.
def generate(messages, caps: set[Capability]): headers = beta_header(caps, ENABLED) try: return client.messages.create( model="claude-opus-4-8", max_tokens=4096, messages=messages, extra_headers=headers, ) except anthropic.BadRequestError as e: if "beta" not in str(e).lower(): raise # Runtime disablement detected. Degrade and reconstruct. log.warning("runtime beta rejection, degrading: %s", e) if Capability.LONG_CONTEXT in caps: messages = chunk_and_summarize(messages) # tier 2: feature substitute return client.messages.create( # tier 1: resend without header model="claude-opus-4-8", max_tokens=4096, messages=messages, extra_headers=beta_header( caps - {Capability.LONG_CONTEXT}, ENABLED), )
The third tier, when no substitute holds, is to skip that one article and move on. If one of the four sites loses an article, finishing the other three and the rest of the queue is healthier operationally than halting everything. The skipped piece goes to the log and gets picked up by the next day's backfill slot.
SDK betas argument versus a raw header
The official SDK offers a dedicated entry point instead of writing the raw header: client.beta.messages.create(..., betas=[...]). It gives you type completion and makes a mistyped identifier easier to catch. The registry we built feeds either form, the raw header (extra_headers) or the betas argument, equally well.
# The registry does not care about the destination. A betas-argument version.def beta_list(caps: set[Capability], enabled: set[Capability]) -> list[str]: return sorted(BETA_IDS[c] for c in caps & enabled if BETA_IDS.get(c))resp = client.beta.messages.create( model="claude-opus-4-8", max_tokens=4096, messages=messages, betas=beta_list({Capability.CONTEXT_EDIT}, ENABLED),)
My own rule is to use the betas argument in new code, and the raw-header version in existing code or in non-official-language HTTP clients where I want fine control over the header. What matters is not which entry point you pick, but that the source of truth for identifiers lives in one registry. Either way, handling a retirement is the same single line.
Keeping a trail of every disablement
Finally, record when, which capability, and why something was disabled. Without this, you cannot back up a vague feeling like "summary quality seems worse since last week." Emit both preflight and runtime disablements as one line of structured log.
import json, datetimedef log_capability_state(enabled: set[Capability], reason: str = "preflight"): record = { "ts": datetime.datetime.now(datetime.UTC).isoformat(), "reason": reason, "enabled": sorted(c.value for c in enabled), "disabled": sorted(c.value for c in set(Capability) - enabled), } log.info("capability_state %s", json.dumps(record, ensure_ascii=False))
Aggregate this daily and you can overlay beta generational changes onto your pipeline's behavior over time. As an indie developer at Dolice, I route this line into the same dashboard as my daily AdMob revenue check, so "context editing has been off since this date" is visible at a glance. Reducing an unexplained quality swing to the plain fact of a feature being enabled or disabled is a real source of calm.
What changed after adopting this
Since introducing this abstraction, reacting to a beta retirement became "fix one line in BETA_IDS." What used to take nearly an hour of grepping and patching 14 sites is now close to zero. More importantly, the startup preflight ended the class of incident where a retired beta keeps getting sent and drops the entire overnight batch.
A good next step is to let BETA_IDS be overridden from an environment variable or config file, so you can respond to a retirement without waiting on a code change and deploy. Beta features will keep turning over. Closing that churn into one place, on the assumption that it will break, is what quietly keeps an automated operation on its feet.
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.