「6月15日に claude-sonnet-4 と claude-opus-4 が API から引退します」という一文を見たとき、最初に確認すべきは自分のコードのどこにそのモデルIDが直書きされているか、です。困るのは目につく場所ではありません。半年前に書いて以来触っていない夜間バッチや、環境変数のデフォルト値、いつか組み込んだベンダー製ラッパーの奥に残った model: "claude-opus-4" です。引退当日、そこだけが静かに model_not_found を返し始めます。
個人開発の運用では、こうした「忘れた固定値」を私自身も何度か踏んできました。動いているコードほど読み返さないので、引退のアナウンスが来てから慌てて全文検索しても、検索語が一致せず取りこぼします。今回はその取りこぼしを機械的になくし、切り替え後の挙動差まで含めて安全に移行するところまでを順に書いていきます。
grep だけでは漏れる — まず機械的に棚卸しする
grep -r "claude-opus-4" で済むなら苦労はありません。漏れるのは、os.environ.get("MODEL", "claude-opus-4") のようにデフォルト値として埋め込まれたケースや、config.json に文字列で置かれたケース、そして日付サフィックス付き(claude-opus-4-20250514 のような形)で書かれたケースです。
そこで「現役のIDを許可リストに持ち、それ以外の claude-* を全部あぶり出す」方針にします。引退する2つを名指しで拾いつつ、見覚えのないIDも「要確認」として報告させるのが肝心です。
#!/usr/bin/env python3
"""本番コードに残る引退予定・不明なモデルIDを洗い出すスキャナ。"""
import re
import sys
from pathlib import Path
# 2026-06 時点で現役のモデルID(ここに無い claude-* は要確認とみなす)
# 最新の正確なIDは必ず公式ドキュメントで確認してください
ALLOWED = {
"claude-opus-4-8" ,
"claude-sonnet-4-6" ,
"claude-haiku-4-5" ,
}
# 6/15 に API から引退する明示ターゲット
RETIRING = { "claude-sonnet-4" , "claude-opus-4" }
# モデルIDらしき文字列。-20250514 のような日付サフィックスも拾う
MODEL_RE = re.compile( r "claude- [ a-z ] + - [ 0-9 ][ a-z0-9- ] * " )
SCAN_EXT = { ".py" , ".ts" , ".tsx" , ".js" , ".mjs" , ".json" ,
".yaml" , ".yml" , ".env" , ".toml" , ".sh" }
def normalize (model_id: str ) -> str :
# 末尾の日付サフィックスを落として「世代」だけで比較する
return re.sub( r "- \d {8} $ " , "" , model_id)
def scan (root: str ):
hits = []
for path in Path(root).rglob( "*" ):
if path.is_dir() or path.suffix not in SCAN_EXT :
continue
if "node_modules" in path.parts or ".git" in path.parts:
continue
try :
text = path.read_text( encoding = "utf-8" , errors = "ignore" )
except OSError :
continue
for lineno, line in enumerate (text.splitlines(), 1 ):
for raw in MODEL_RE .findall(line):
base = normalize(raw)
if base in RETIRING :
hits.append(( str (path), lineno, raw, "RETIRING 6/15" ))
elif base not in ALLOWED :
hits.append(( str (path), lineno, raw, "unknown - confirm" ))
return hits
if __name__ == "__main__" :
root = sys.argv[ 1 ] if len (sys.argv) > 1 else "."
found = scan(root)
for path, lineno, model, tag in found:
print ( f "[ { tag } ] { path } : { lineno } { model } " )
print ( f " \n{ len (found) } 件の要対応モデルIDが見つかりました" , file = sys.stderr)
sys.exit( 1 if found else 0 )
これをリポジトリのルートで python3 scan_models.py . と走らせ、[RETIRING 6/15] のタグが付いた行が、当日エラーになる箇所です。許可リスト方式にしておくと、将来別のモデルが引退するときも、許可リストを更新するだけで同じスキャナが使い回せます。
ひとつ補足すると、コードを全部直しても安心はできません。すでに送信中のリクエストログやダッシュボードのクエリにIDが残っていることがあります。直近30日のリクエストログを集計して、実際に呼ばれているモデルIDの分布を出しておくと、コードからは消したつもりでも経路が残っていた、という事故を防げます。
なぜ「sed で一括置換」が危険なのか
検出できたら、claude-opus-4 を claude-opus-4-8 に一括置換したくなります。ですが、モデルIDの置換は文字列置換ではなく、挙動の変更です。世代が上がると、同じプロンプトでも出力トークン量が変わり、レイテンシが変わり、整形の癖(箇条書きにするか散文にするか、コードブロックの付け方)も微妙に変わります。
ここで効いてくるのが、IDをコード中に散らさないという設計判断です。client.messages.create(model="...") の呼び出しが数十箇所に散っていると、置換のたびに数十箇所をレビューすることになり、しかも一箇所だけ直し忘れます。私はこの「直し忘れ一箇所」で痛い目を見てから、モデルIDを一枚のレイヤーに閉じ込めるようにしました。具体的な実装は後半で示します。
引退前に出力パリティを測る検証ハーネス
置換が安全かどうかは、引退を待たずに測れます。本番で実際に使っているプロンプトを数件用意し、旧モデルと新モデルの両方に同じ入力を流して、出力・トークン量・レイテンシを並べて比較します。
import json
import time
from anthropic import Anthropic
client = Anthropic() # ANTHROPIC_API_KEY を環境変数から読む
OLD = "claude-opus-4" # 引退予定
NEW = "claude-opus-4-8" # 移行先(最新IDはドキュメントで確認)
# 本番で実際に投げているプロンプトを 5〜10 件入れる
PROMPTS = [
"次の問い合わせを緊急度 high/normal/low で分類し、理由を1文で: ..." ,
"次の差分にバグがあれば指摘し、なければ NONE とだけ返す: ..." ,
]
def run (model: str , prompt: str ) -> dict :
t0 = time.time()
msg = client.messages.create(
model = model,
max_tokens = 512 ,
messages = [{ "role" : "user" , "content" : prompt}],
)
text = "" .join(b.text for b in msg.content if b.type == "text" )
return {
"text" : text,
"out_tokens" : msg.usage.output_tokens,
"latency_ms" : round ((time.time() - t0) * 1000 ),
}
for prompt in PROMPTS :
a = run( OLD , prompt)
b = run( NEW , prompt)
print (json.dumps({
"prompt" : prompt[: 40 ],
"identical" : a[ "text" ].strip() == b[ "text" ].strip(),
"out_tokens" : [a[ "out_tokens" ], b[ "out_tokens" ]],
"latency_ms" : [a[ "latency_ms" ], b[ "latency_ms" ]],
}, ensure_ascii = False ))
ここで identical がほとんど false になっても、慌てる必要はありません。一字一句同じになることのほうがまれです。見るべきは「下流が壊れないか」です。出力をJSONとして json.loads しているなら、新モデルの出力もスキーマ検証を通るかを確認します。整形が変わると、出力を正規表現で切り出している箇所がずれます。分類タスクで high/normal/low のラベルだけを期待しているなら、新モデルが余計な前置きを付けないかを見ます。
私はこの比較で、出力そのものは良くなっているのに、後段の貧弱なパーサが新しい整形を受け止められず壊れる、というパターンに何度か出会いました。直すべきは多くの場合モデルではなくパーサ側です。引退前にこれが分かるだけで、当日の切り替えが「祈りながらデプロイ」から「確認済みのデプロイ」に変わります。
別名レイヤーで「次の引退」を1行にする
検証が済んだら、移行先IDをコード全体にばら撒くのではなく、論理名から具体的なIDへ解決する一枚のレイヤーに集約します。役割で名前を付けるのがコツです。
// models.ts — モデルIDを一箇所に閉じ込める別名レイヤー
export type ModelRole = "reasoning" | "balanced" | "fast" ;
const RESOLVE : Record < ModelRole , string > = {
reasoning: "claude-opus-4-8" ,
balanced: "claude-sonnet-4-6" ,
fast: "claude-haiku-4-5" ,
};
export function modelFor ( role : ModelRole ) : string {
return RESOLVE [role];
}
呼び出し側は具体的なIDを知りません。
import { modelFor } from "./models" ;
const msg = await client.messages. create ({
model: modelFor ( "reasoning" ),
max_tokens: 1024 ,
messages,
});
次にどのモデルが引退しても、変更箇所は RESOLVE の1行だけです。呼び出し側のコードは1行も触りません。これがアンチコラプションレイヤー(外部の都合を自分のコードへ漏らさない境界)の実利です。引退アナウンスのたびに全文検索する運用から、設定ファイルを1行直す運用へ移れます。
引退後の安全網 — model_not_found を握りつぶさない
別名レイヤーがあっても、デプロイの反映が遅れたり、キャッシュ済みの古いコードが残ったりすると、引退直後の数分間だけ model_not_found が出ることがあります。ここを無防備にしておくと、その数分がそのまま障害になります。
import Anthropic from "@anthropic-ai/sdk" ;
const client = new Anthropic ();
// 引退IDが万一呼ばれたときの一時退避先
const FALLBACK : Record < string , string > = {
"claude-opus-4" : "claude-opus-4-8" ,
"claude-sonnet-4" : "claude-sonnet-4-6" ,
};
export async function createWithFallback (
params : Anthropic . MessageCreateParams ,
) {
try {
return await client.messages. create (params);
} catch (err) {
if (err instanceof Anthropic . NotFoundError && FALLBACK [params.model]) {
const next = FALLBACK [params.model];
console. warn ( `model ${ params . model } unavailable -> falling back to ${ next }` );
metrics. increment ( "model_fallback" , { from: params.model, to: next });
return client.messages. create ({ ... params, model: next });
}
throw err;
}
}
ただしフォールバックは握りつぶしと紙一重です。console.warn とメトリクスを必ず出してください。「フォールバックが発火し続けている」=「移行がまだ終わっていない経路が残っている」という強いシグナルです。ここを黙って吸収してしまうと、引退IDを参照したままのコードが半永久的に居座ります。安全網は、移行の完了を検知して自分を不要にするためにある、と考えるのがちょうどよい温度感です。
一気に切り替えない — カナリアで様子を見る
最後に、切り替えそのものを段階的にします。引退期日までに全量を新モデルへ寄せる必要はありますが、いきなり100%を倒すと、想定外の挙動差があったときに被害が全ユーザーに及びます。
実務では次の順でいつも進めています。まず社内・自分用のトラフィックだけを新モデルに向けて1日置き、出力の質と下流のエラー率を見ます。問題なければ全体の10%程度に広げ、output_tokens の平均と、後段パーサのエラー率、p95レイテンシの3点をダッシュボードで並べます。劣化が無いことを数値で確認してから、残りを寄せます。
// ユーザーIDのハッシュで安定的に 10% だけ新モデルへ振る
function useNewModel ( userId : string ) : boolean {
return hash (userId) % 100 < 10 ;
}
カナリアの良いところは、ロールバックが「設定の数字を10から0に戻す」だけで済むことです。引退前に余裕を持ってこの仕組みを通しておけば、当日は割合を100にするだけで移行が完了します。
同じ移行に追われている方の段取りの参考になれば幸いです。まずは上のスキャナを本番リポジトリのルートで一度走らせてみてください。[RETIRING 6/15] が1件でも出たら、そこが当日 model_not_found になる場所です。