個人開発のバックエンドで API を呼んでいる小さなスクリプトを、日曜の夜に棚卸ししていたときのことです。temperature=0.2 と書かれた行が、思っていたよりずっと多く出てきました。
書いたのは自分です。それなのに、どのスクリプトが何のために 0.2 にしたのか、半分は思い出せませんでした。いま思えば、値を決めた理由をどこにも書き残さなかった私の側に原因がありました。
きっかけは、公式の移行ページ「API version 2026-10-21」を読んだことです。サンプリング引数の扱いが変わり、temperature・top_p・top_k を送ると、既定値と同じ数字でも 400 が返ると書かれていました。
最初にお伝えしたいのは、これは「モデルを変えたとき」ではなく「SDK を上げたとき」に踏む話だということです。モデル ID を一文字も変えていなくても、依存を更新した次のデプロイで止まりえます。
何が変わるのか(移行ページから確認できた範囲)
ページには、切り替えの日付も廃止の期日も書かれていませんでした。書かれているのは、新しいバージョン文字列が 2026-10-21 で、これまでの版が 2023-06-01 だということです。期日の記載がない以上、「いつ壊れるか」ではなく「いつ自分が切り替えるか」を決める話になります。
切り替わり方は二通りです。
SDK を使っている場合、バージョンは SDK のメジャーバージョンに結びついています。Python なら anthropic を 2.0.0 に上げたときに 2026-10-21 で送るようになります。SDK を使わず直接叩いている場合は、anthropic-version ヘッダを自分で書き換えたときです。
Messages API のリクエストで 400 になる記述は三つです。
| 記述 | 2026-10-21 での扱い | 直し方 |
|---|---|---|
temperature / top_p / top_k | 既定値でも 400(トークン数のカウントとバッチも同じ) | 引数ごと削除する |
assistant メッセージ直下の document ブロック | 400 | user メッセージに移す |
user メッセージの text ブロックにある空でない citations | 400 | citations を外す。assistant 側と、document の citations.enabled は引き続き使える |
サンプリングについては、もう一点あります。Claude 4.7 以降のモデルは、もともと既定値しか受け付けません。ですので、そちらの呼び出しは挙動が変わらず、「引数を消すだけ」で済みます。注意が要るのは、非既定値を受け付けていたモデル——ページが例に挙げているのは Sonnet 4.6——で temperature を下げていた呼び出しです。引数を消すと既定の 1.0 で動きますので、エラーは出ないのに、出力のばらつきが変わります。
洗い出す前に決めておく線
最初のうち、私は「全部消せば終わり」だと考えておりました。結果は芳しくありませんでした。消す作業よりも、消してよい行と、旧バージョンに残す行の仕分けのほうが時間を食うのです。
いまは次の線引きにしています。
- 4.7 以降のモデルへの呼び出し:引数を消す(挙動は変わらない)
- 4.6 以前で、出力のばらつきに依存していない呼び出し:引数を消して、出力を何本か見比べる
- 4.6 以前で、
temperature=0付近に依存している呼び出し:そのリクエストだけ2023-06-01に残すか、プロンプト側で揃え方を固定する
三つ目は、ページに「非既定値に依存するなら、引数を送らないか、そのリクエストを 2023-06-01 に残す」と書かれている部分に沿っています。
拾うためのスキャナ
洗い出しは、目で grep するより、候補を機械的に出して人が判断するほうが確実でした。移行ページに載っている項目のうち、コードの上で見つけやすいものを正規表現にしています。構文解析ではないので、コメントや変数名の誤検出は出ます。それは承知のうえで、見落としを減らすことを優先しました。私自身、コメントの中の単語に何度か反応して、少し煩わしく感じたかもしれません。それでも、拾い漏らすよりはましだと考えております。
#!/usr/bin/env python3
"""API バージョン 2026-10-21 で 400 や動作変更になりうる記述を拾う簡易スキャナ。
使い方: python3 scan_20261021.py <dir> [<dir> ...] 候補があれば exit 1"""
import re, sys, pathlib
EXTS = {".py", ".ts", ".tsx", ".js", ".mjs", ".rb", ".go", ".java", ".json", ".yaml", ".yml", ".sh"}
SKIP = {"node_modules", ".git", ".venv", "dist", "build", ".next"}
RULES = [
# (id, 正規表現, 次にやること)
("sampling", r"""["']?\b(temperature|top_p|top_k)\b["']?\s*[:=]""", "削除する。非既定値に依存するならそのリクエストだけ旧バージョンに残す"),
("version", r"anthropic-version.{0,20}2023-06-01", "ヘッダを 2026-10-21 にするのは修正と同時に"),
("auth-hdr", r"""x-api-key""", "SDK 新版は Authorization: Bearer。プロキシ側の読み取りを確認"),
("msgparam", r"\bMessageParam\(", "辞書リテラルか型注釈付きの代入へ"),
("compaction", r"\bcompactionControl\b", "context_management の compact に置き換える"),
("completions",r"\bcompletions\.create\b", "Text Completions は削除済み。Messages API へ"),
("assist-doc", r"""["']role["']\s*:\s*["']assistant["'].{0,200}["']type["']\s*:\s*["']document["']""", "document ブロックは user メッセージへ"),
]
def scan(root: pathlib.Path):
hits = []
for p in root.rglob("*"):
if not p.is_file() or p.suffix not in EXTS or SKIP & set(p.parts):
continue
try:
lines = p.read_text(encoding="utf-8", errors="ignore").splitlines()
except OSError:
continue
for no, line in enumerate(lines, 1):
for rid, rx, todo in RULES:
if re.search(rx, line, re.I if rid == "auth-hdr" else 0):
hits.append((str(p), no, rid, line.strip()[:90], todo))
return hits
if __name__ == "__main__":
roots = [pathlib.Path(a) for a in sys.argv[1:]] or [pathlib.Path(".")]
found = [h for r in roots for h in scan(r)]
for path, no, rid, text, todo in found:
print(f"{path}:{no} [{rid}] {text}\n -> {todo}")
print(f"\n候補 {len(found)} 件")
sys.exit(1 if found else 0)assist-doc の規則は、一行に収まった辞書リテラルしか拾えません。複数行に折り返したメッセージは見逃しますので、document を使っている箇所は別に目で確認してください。
小さなサンプルで動かした結果
temperature=0.2 を渡す Python ファイルと、top_p を持つ TypeScript、そして単語として temperature を含むだけのファイルを、一つずつ置いて走らせました。
t/app.py:6 [sampling] temperature=0.2,
-> 削除する。非既定値に依存するならそのリクエストだけ旧バージョンに残す
t/app.py:9 [msgparam] m = anthropic.types.MessageParam(role="user", content="x")
-> 辞書リテラルか型注釈付きの代入へ
t/proxy.ts:1 [auth-hdr] const key = req.headers["x-api-key"];
-> SDK 新版は Authorization: Bearer。プロキシ側の読み取りを確認
t/proxy.ts:2 [sampling] const body = { model: "m", top_p: 0.9, messages: [] };
-> 削除する。非既定値に依存するならそのリクエストだけ旧バージョンに残す
候補 4 件単語だけを含むファイルは拾われず、終了コードは 1 でした。検出は 4 件で、期待どおりです。
三つ目の auth-hdr は、見落としやすい項目です。新しい SDK は API キーを X-Api-Key ではなく Authorization: Bearer で送ります。自前のプロキシやゲートウェイが X-Api-Key を読んでキーを検証している構成だと、サンプリングの 400 より先に、認証の段階で止まる可能性があります。個人開発のバックエンドに薄い中継を挟んでいる方は、SDK を上げる前にここを確認しておくと安心です。
直す順番
一度に全部を直すと、何が効いたのか分からなくなります。次の順に進めています。
- スキャナを走らせ、件数を数える
- 上の線引きで、残す行と消す行を仕分ける
- 消す行は、更新前の SDK のままで消してコミットする(新旧どちらの版でも通る修正になります)
- 最後に SDK を上げる。移行ページにも、修正はアップグレードと同時にリリースするよう書かれていますので、手順 3 と 4 は同じ日に出しています
手順 3 を先に入れておくと、SDK を上げた日に 400 が出ても、原因をサンプリング引数以外に絞れます。
SDK 側の変更で、サンプリングのほかに手が入りうるものも書き留めておきます。MessageParam(...) をクラスとして呼んでいる箇所は TypeError になります。1 つのクライアントに api_key と auth_token のように認証を二つ渡していると ValueError です。CLI の --api-key と --auth-token は削除されています。
temperature の使い分けそのものについては、別の記事で扱っております。モデル ID の入れ替えに伴う 400 を拾う話は、Sonnet 4.5 の退役に向けたスキャナの記事に書きました。今回のスキャナと見る項目が重ならないので、両方走らせても二重にはなりません。
次のアクション
まずはご自身のリポジトリで、このスキャナを一度だけ走らせてみてください。件数が 0 なら、SDK を上げる日を決めるだけです。件数が出たなら、その数が、切り替えまでの作業量そのものになります。
切り替えの日を決める前に、自分のコードのどこが引数に頼っているかを数えておく——この線引きだけは、次のバージョンが出たときも変えないつもりです。
Sources: API version 2026-10-21 migration