2026 年 6 月 15 日、API から Sonnet 4 と Opus 4 が引退し、同じ週に Fable 5・Mythos 5 がいったん利用停止になりました。モデル ID の差し替えはすぐ気づけますが、見落としやすいのが anthropic-beta ヘッダの方です。個人開発で続けている Dolice Labs の 4 サイトでは、記事の自動投稿をひとつのバックエンドが支えています。私自身、ある朝の夜間バッチが 1 件も完走していないことに気づきました。原因は、半年前に書いた context-1m-2025-08-07 というベータ識別子をまだ送り続けていた呼び出しで、ベータの提供窓が閉じた瞬間に 400 Bad Request を返すようになっていた、というものでした。
厄介なのは、この 1 行が特定の 1 リクエストではなく、同じヘッダを共有していた複数の呼び出しを巻き込んで落とした点です。ベータ機能は便利な反面、世代交代が速く、引退・GA 昇格・名称変更が数か月単位で起こります。ここではその変化を 1 か所で吸収するために、anthropic-beta を「ケイパビリティ登録簿」として抽象化し、起動時プリフライトと段階的フォールバックまで含めて実装します。
ベータヘッダ1行が、夜間バッチを丸ごと止めた
最初の実装は、よくある形でした。長文コンテキストとプロンプトキャッシュを使いたい呼び出しに、その都度ヘッダを直書きしていたのです。
# Before: 呼び出しごとにヘッダを直書き(各所にコピーされ、散らばっていく)
resp = client.messages.create(
model = "claude-opus-4-8" ,
max_tokens = 4096 ,
messages = messages,
extra_headers = {
"anthropic-beta" : "context-1m-2025-08-07,prompt-caching-2024-07-31"
},
)
この書き方の問題は、同じ文字列が記事生成・要約・タグ付け・翻訳の各モジュールにコピーされ、最終的に 14 箇所へ増えていたことでした。context-1m-2025-08-07 が無効になったとき、私はそのうちのどこが該当するのかをすぐに言えませんでした。grep で探して 1 箇所ずつ直す間、バッチは止まったままです。1 回の夜間バッチの取りこぼしは翌朝の手動再実行で取り戻せますが、これが週 1〜2 回起きると、運用の信頼性そのものが揺らぎます。
なぜ「機能の引退」がヘッダ経由でパイプラインを壊すのか
anthropic-beta ヘッダは、まだ正式版になっていない機能を明示的に有効化するスイッチです。問題は、このスイッチが 3 つの異なる理由で「効かなくなる」ことにあります。
ひとつ目は引退です。1M コンテキストのように期間限定で提供されたベータは、窓が閉じると未知の識別子として扱われ、組み合わせによっては 400 を返します。ふたつ目は GA 昇格です。プロンプトキャッシュのように正式版へ移ると、ヘッダは不要になります。多くの場合は当面そのまま受理されますが、いつまでも残しておくのは将来の拒否リスクを抱え込むだけです。みっつ目は名称・日付の変更で、code-execution-2025-05-22 のように日付入りの識別子は、仕様改定で新しい日付版へ差し替わることがあります。
共通するのは、いずれも「コード側が知っている世界」と「API が受け付ける世界」がずれた瞬間に壊れる、という構造です。だからこそ、ヘッダ文字列を散らさず、世代交代を 1 か所で表現できる形にしておく価値があります。
散らばったヘッダ指定を「ケイパビリティ登録簿」に集約する
最初の一歩は、呼び出し側に「どのベータ識別子を送るか」ではなく「どの能力が欲しいか」を語らせることです。Capability という列挙を定義し、識別子への対応はレジストリ 1 か所に閉じ込めます。
from enum import Enum
class Capability ( Enum ):
PROMPT_CACHE = "prompt_cache"
LONG_CONTEXT = "long_context"
CONTEXT_EDIT = "context_edit"
CODE_EXECUTION = "code_execution"
# 現在有効なベータ識別子。GA 済みは None(=ヘッダ不要)。
# 識別子は時期により変わるため、最新は公式の API リリースノートで確認します。
BETA_IDS : dict[Capability, str | None ] = {
Capability. PROMPT_CACHE : None , # GA 済み → ヘッダ不要
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 ]:
"""要求された能力のうち、有効かつヘッダが必要なものだけを連結する。"""
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 {}
これだけで、呼び出し側は識別子の文字列から解放されます。
# After: 欲しい能力を宣言するだけ。識別子はレジストリが知っている。
headers = beta_header(
caps = {Capability. LONG_CONTEXT , Capability. PROMPT_CACHE },
enabled = ENABLED , # プリフライトで確定した有効集合(後述)
)
resp = client.messages.create(
model = "claude-opus-4-8" ,
max_tokens = 4096 ,
messages = messages,
extra_headers = headers,
)
context-1m-2025-08-07 が引退したら、変更するのは BETA_IDS の 1 行だけです。14 箇所を grep して回る作業は消えました。GA 昇格時も値を None にするだけで、ヘッダは自動的に外れます。
起動時プリフライトで、使えないベータを先に切り離す
レジストリ化しても、識別子が今も受理されるかどうかは実際に叩いてみないと分かりません。そこで、バッチの本処理に入る前に各能力を 1 回ずつ検証し、通ったものだけを「有効集合」として確定させます。検証には課金の軽い count_tokens を使うのが私のお勧めです。
import anthropic, logging
client = anthropic.Anthropic()
log = logging.getLogger( "beta_preflight" )
def preflight (candidates: set[Capability]) -> set[Capability]:
"""各能力を個別に検証し、API が受理したものだけを返す。"""
enabled: set[Capability] = set ()
for c in candidates:
if BETA_IDS .get(c) is None : # GA 済みは検証不要
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:
# 未知・引退済みベータは 400。その能力だけ落とし、処理は続行。
if "beta" in str (e).lower():
log.warning( "capability disabled: %s ( %s )" , c.value, e)
else :
raise # ベータと無関係な 400 は握りつぶさない
return enabled
ENABLED = preflight({
Capability. LONG_CONTEXT ,
Capability. CONTEXT_EDIT ,
Capability. PROMPT_CACHE ,
})
ポイントは、能力をまとめて 1 回で検証せず、1 つずつ独立して試すことです。まとめて送ると、引退した 1 つのせいで生きている残りも巻き添えで落ちたかどうかを切り分けられません。独立検証なら、「LONG_CONTEXT だけ無効、残りは健全」という正確な状態が起動直後に手に入ります。本番運用では、この ENABLED をプロセス起動時に一度だけ確定し、以降のリクエストはすべてここを参照させます。
引退・GA 昇格・エラーに対する3段階フォールバック
プリフライトで弾けるのは「起動時点で分かる引退」です。実運用では、稼働中にベータが無効化されたり、長文機能が使えなくなったりもします。そこで能力ごとに「使えなかったときの代替」を決めておきます。
第 1 段階は、ヘッダを外して再送するだけで成立するケースです。プロンプトキャッシュは、ヘッダが拒否されてもキャッシュが効かなくなるだけで、本文の生成自体は成功します。この場合は黙ってヘッダを落として再送すれば十分です。
第 2 段階は、機能そのものの代替が必要なケースです。長文コンテキストが使えないなら、入力を分割して階層要約に切り替えます。
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
# 稼働中の無効化を検知。能力を落として再構成する。
log.warning( "runtime beta rejection, degrading: %s " , e)
if Capability. LONG_CONTEXT in caps:
messages = chunk_and_summarize(messages) # 第2段階: 機能代替
return client.messages.create( # 第1段階: ヘッダを外して再送
model = "claude-opus-4-8" , max_tokens = 4096 ,
messages = messages, extra_headers = beta_header(
caps - {Capability. LONG_CONTEXT }, ENABLED ),
)
第 3 段階は、代替が成立しないときに「その記事だけ skip して次へ進む」ことです。4 サイトのうち 1 本が落ちても、残り 3 本と他の記事を完走させる方が、全体を止めるより運用上は健全だと私は考えています。落ちた 1 本はログに残し、翌日の補完枠で拾えば取り戻せます。
SDK の betas 引数と生ヘッダのどちらを使うか
Anthropic の公式 SDK には、生ヘッダを書く代わりに client.beta.messages.create(..., betas=[...]) という専用の入口があります。型補完が効き、識別子の指定ミスに気づきやすい利点があります。一方で、ここまで作ったレジストリは生ヘッダ(extra_headers)でも betas 引数でも、どちらにも値を供給できます。
# レジストリは出力先を選ばない。betas 引数へ流す版。
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 ),
)
私の判断としては、新規コードでは betas 引数を、ヘッダを細かく制御したい既存コードや非公式言語の HTTP クライアントでは生ヘッダ版を選んでいます。重要なのは入口の種類ではなく、識別子の正本がレジストリ 1 か所に集約されていることです。どちらの入口を使っても、引退対応は同じ 1 行で済みます。
無効化の履歴を、あとから追えるように残す
最後に、いつ・どの能力が・なぜ無効化されたのかを残します。これがないと、「先週から長文要約の品質が落ちた気がする」という曖昧な体感を裏取りできません。プリフライトとランタイム両方の無効化を、構造化ログとして 1 行で吐いておきます。
import json, datetime
def 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 ))
このログを日次で集計すると、ベータの世代交代と自分のパイプラインの挙動を時系列で重ねられます。私は AdMob 収益の日次チェックと同じダッシュボードにこの行を流し込み、「この日からコンテキスト編集が外れている」と一目で分かるようにしています。原因不明の品質変動を、機能の有効・無効という事実に還元できるのは大きな安心材料です。
導入してから変わったこと
この抽象化を入れてからは、ベータの引退に対する作業が「BETA_IDS を 1 行直す」だけになりました。14 箇所の grep と個別修正で 1 時間近くかけていたものが、ほぼゼロになっています。何より、起動時プリフライトのおかげで「引退済みベータを送り続けて夜間バッチを丸ごと落とす」事故が起きなくなりました。
次の一歩としては、BETA_IDS のレジストリを環境変数や設定ファイルから上書きできるようにしておくと、コード変更とデプロイを待たずに引退へ即応できます。ベータ機能は今後も世代交代を続けます。その変化を「壊れる前提で 1 か所に閉じ込めておく」ことが、自動運用を静かに支えてくれます。お読みいただきありがとうございました。