退役の通知を受け取ってから、モデル ID の文字列を claude-sonnet-5-5 に置き換える作業を、私はずっと「一行の変更」だと思っておりました。
公式の移行ガイドを上から順に読んだのは、そのあとです。thinking の指定、sampling の引数、prefill、tool_choice、beta ヘッダ——モデル ID の外側に、同じリクエストを 400 にする記述がいくつも並んでいました。一行どころか、リクエストの組み立て方そのものに手が入ります。
最初にお伝えしたいのは、文字列を置換する前に、400 になる記述を機械的に拾っておくということです。そのためのスキャナを書き、手元の小さなサンプルで動かした結果を残しておきます。個人開発で小さなスクリプトから API を呼ぶ身としては、置換ひとつで済むと思い込んでいた自分が、少し恥ずかしくなりました。
退役の日付と、置き換え先
公式の廃止予定ページ(2026 年 10 月 8 日に確認)では、claude-sonnet-4-5-20250929 は 2026 年 9 月 30 日に Deprecated となり、退役予定日は 2026 年 11 月 30 日、推奨の置き換え先は claude-sonnet-5-5 です。退役後はリクエストが失敗します。
同じページに、この日付が当てはまる範囲も書かれています。Claude API、Claude Platform on AWS、Microsoft Foundry が対象で、Amazon Bedrock と Google Cloud は独自のスケジュールです。経由先によって日付が違う可能性がありますので、ご自身の呼び出し経路は別に確かめてください。
単価やトークナイザの差は、別の記事で扱っております。ここでは費用ではなく、「そもそもリクエストが通るか」だけに絞ります。
公式の移行ガイドから、400 や黙った誤動作になる項目を抜き出す
Sonnet 5.5 の移行ガイドのうち、Sonnet 4.5 から移るときに手を入れる必要があるものを、コードの上で見つけやすい順に並べました。
| 項目 | 4.5 での書き方 | 5.5 での扱い |
|---|---|---|
| sampling | temperature / top_p / top_k | 非既定値は 400。削除する |
| thinking の予算 | {"type": "enabled", "budget_tokens": N} | エラー。adaptive と output_config.effort を使う |
| thinking を切る | {"type": "disabled"} | 400。between_tools を使う |
| prefill | 末尾に assistant メッセージ | エラー。会話は user メッセージで終える |
| tool_choice | "tool" / "any" | エラー。auto と strict: true に置き換える |
| 構造化出力 | output_format | 非推奨。output_config.format へ |
| beta ヘッダ | interleaved-thinking-2025-05-14 など | 外す、または eager_input_streaming に置き換える |
| 応答の読み方 | content[0].text | 先頭に thinking ブロックが来うる。type で探す |
見落としやすいのは最後の行です。リクエストは通るのに、content[0] が thinking ブロックになって .text が取れない——エラーメッセージのない壊れ方は、本番で気づくのが遅れます。
また、リクエストに thinking を書いていない場合も、5.5 では adaptive thinking が既定で動きます。4.5 では動いていなかったものですので、max_tokens が thinking の分も含むことは頭に入れておく必要があります。
表をそのまま正規表現にしたスキャナ
上の表の「4.5 での書き方」を、一行ずつ正規表現にしました。完璧な構文解析ではなく、「人の目で確認する候補を出す」ことを割り切った道具です。
#!/usr/bin/env python3
"""Sonnet 4.5 -> 5.5 で 400 になりうる記述を拾う簡易スキャナ。
使い方: python3 sonnet55_scan.py <dir> 該当があれば exit 1"""
import re, sys, pathlib
RULES = [
("model-id", r"claude-sonnet-4-5(?:-\d{8})?", "モデルIDを claude-sonnet-5-5 へ"),
("sampling", r"\b(temperature|top_p|top_k)\s*=", "非既定値は 400。削除"),
("budget", r"budget_tokens", "adaptive + output_config.effort へ"),
("think-off", r"""["']type["']\s*:\s*["']disabled["']""", "disabled は 400。between_tools へ"),
("prefill", r"""["']role["']\s*:\s*["']assistant["']""", "末尾が assistant なら prefill。要確認"),
("tool-choice", r"""["']type["']\s*:\s*["'](?:tool|any)["']""", "tool/any は 400。auto + strict へ"),
("output-fmt", r"\boutput_format\b", "output_config.format へ"),
("beta-hdr", r"interleaved-thinking-2025-05-14|fine-grained-tool-streaming-2025-05-14", "ヘッダを外す/置き換える"),
("content0", r"content\[0\]\.text", "thinking ブロックが先頭に来うる。type で探す"),
]
EXTS = {".py", ".ts", ".js", ".mjs", ".json", ".yaml", ".yml", ".env", ".md"}
def scan(root):
hits = []
for p in sorted(pathlib.Path(root).rglob("*")):
if p.suffix not in EXTS or not p.is_file() or "node_modules" in p.parts:
continue
for n, line in enumerate(p.read_text(errors="ignore").splitlines(), 1):
for tag, pat, hint in RULES:
if re.search(pat, line):
hits.append((str(p), n, tag, hint, line.strip()))
return hits
if __name__ == "__main__":
hits = scan(sys.argv[1] if len(sys.argv) > 1 else ".")
for f, n, tag, hint, src in hits:
print(f"{f}:{n} [{tag}] {hint}\n {src}")
files = len({h[0] for h in hits})
print(f"\n{len(hits)} 件 / {files} ファイル")
sys.exit(1 if hits else 0)prefill の規則は、"role": "assistant" を含む行をすべて拾います。過去の会話履歴を渡しているだけの正当な使い方も混ざりますので、ここは「会話の最後がその行になっていないか」を人が見る前提です。
動かした結果
4.5 向けの書き方を意図的に詰め込んだ 2 ファイルを用意し、実際に走らせました。1 つ目は temperature・budget_tokens・prefill・content[0].text を含む要約関数、2 つ目は beta ヘッダ・tool_choice・output_format を含む抽出処理です。
app/extract.py:4 [model-id] モデルIDを claude-sonnet-5-5 へ
model="claude-sonnet-4-5",
app/extract.py:6 [beta-hdr] ヘッダを外す/置き換える
betas=["interleaved-thinking-2025-05-14"],
app/extract.py:8 [tool-choice] tool/any は 400。auto + strict へ
tool_choice={"type": "tool", "name": "pick"},
app/extract.py:9 [output-fmt] output_config.format へ
output_format={"type": "json_schema", "schema": {}},
app/summarize.py:6 [model-id] モデルIDを claude-sonnet-5-5 へ
model="claude-sonnet-4-5-20250929",
app/summarize.py:8 [sampling] 非既定値は 400。削除
temperature=0.3,
app/summarize.py:9 [budget] adaptive + output_config.effort へ
thinking={"type": "enabled", "budget_tokens": 2000},
app/summarize.py:12 [prefill] 末尾が assistant なら prefill。要確認
{"role": "assistant", "content": "{"},
app/summarize.py:14 [content0] thinking ブロックが先頭に来うる。type で探す
).content[0].text
9 件 / 2 ファイル終了コードは 1 でした。モデル ID の行は 2 件だけで、残りの 7 件は ID を置換しても残る記述です。置換だけで済ませていたら、7 箇所がそのまま 400 か誤動作に化けたことになります。
なお、これはスキャナの動作確認です。置き換えたコードを実際の API に投げて通ることまでは、この記事のために確かめておりません。直し方は公式の移行ガイドの記述に沿ったものですので、ご自身の環境で一度は通してください。
直す順番
ヒットを前にすると、上から順に直したくなります。私自身は次の順番が安全なのかもしれないと考えております。
- まず
temperatureなどの sampling 引数と、budget_tokens・disabledを消します。どれも 400 で即座に気づけるものです。 - 次に prefill を外します。JSON を
{から始めさせていた場合は、構造化出力(output_config.format)へ移すのが公式の案内です。 tool_choiceをautoにし、ツールにstrict: trueを付けます。strict ツールは、各オブジェクトにadditionalProperties: falseが要ります。- 最後に
content[0].textを、type を見て text ブロックを探す書き方に変えます。これだけはエラーが出ませんので、スキャナの出力から漏らさないことが大切です。
直したあとの要約関数は、たとえば次のようになります。
resp = client.messages.create(
model="claude-sonnet-5-5", # 日付サフィックスなし
max_tokens=8000, # thinking の分も含めて見直す
thinking={"type": "adaptive"},
output_config={"effort": "medium"}, # 明示しておく
messages=[{"role": "user", "content": text}],
)
# 先頭が thinking ブロックでも壊れない読み方
answer = next(b.text for b in resp.content if b.type == "text")effort は 4.5 に無かった引数で、5.5 は五段階、API の既定は high です。既定に任せず明示しておくと、あとで挙動が変わったときに見比べられます。
次のアクション
今日は、ご自身のリポジトリでこのスキャナを一度だけ走らせてみてください。件数が 0 なら、退役日までの余裕は十分です。件数が出たなら、その数がそのまま、11 月 30 日までの作業量の見積もりになります。
期限が決まっているときほど、置換の前に「何が残るか」を数える——この原則だけは、次のモデル退役でも守るようにしております。
Sources: Model deprecations、Claude Sonnet 5.5 migration guide