夜間の無人タスクが、朝には全部終わっていました。失敗ログはありません。ただ、いちばん重い工程だけ、いつもより明らかに雑な仕上がりになっています。
原因を辿ると、xhigh を指定していたはずの工程が high で走っていました。エラーは一度も出ていません。落ちてくれた方がまだ楽だった、というのが正直なところです。
この取り違えは、thinking と effort という二つのつまみが互いに制約し合っていることを知らないと見つかりません。順に整理していきます。
is not supported when thinking is disabled が返るのは、いつか
まず、エラーとして表に出ていた頃の姿から確認します。返ってくるのは 400 で、本文はこの形です。
output_config.effort 'xhigh' is not supported when thinking is disabled
Claude Opus 5 は思考が既定でオンになっているモデルです。thinking: {"type": "disabled"} を明示して切ること自体はできますが、それが許されるのは effort が high 以下のときに限られます。xhigh と max については、思考を切った状態での実行を受け付けません。この判定はリクエストごとに行われるため、「セッションの最初だけ通っていた」という抜け道もありません。
最小の再現はこれだけです。
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "claude-opus-5",
"max_tokens": 1024,
"thinking": {"type": "disabled"},
"output_config": {"effort": "xhigh"},
"messages": [{"role": "user", "content": "ping"}]
}'effort を high に落とすか、thinking の行を丸ごと消すか、どちらかで通ります。エラーメッセージ自体も「effort を high 以下にするか、thinking を有効にしてください」と両方の出口を示してくれます。
ここまでは、素直な仕様です。問題はこの先にあります。
モデルごとに、拒否される指定が違います
thinking は世代によって受け付ける型が入れ替わってきました。設定ファイルを使い回したままモデル名だけ差し替えると、ここで足を取られます。
| モデル | 使える thinking の型 | 既定 | 400 になる指定 |
|---|---|---|---|
| Claude Fable 5 | adaptive のみ | 常時オン | enabled / disabled |
| Claude Opus 5 | adaptive のみ | オン | enabled、および disabled(effort が xhigh・max のとき) |
| Claude Opus 4.8 / 4.7 | adaptive のみ | オフ | enabled |
| Claude Sonnet 5 | adaptive のみ | オン | enabled |
| Claude Opus 4.6 / Sonnet 4.6 | adaptive と extended(非推奨) | オフ | なし |
| Claude Opus 4.5 / Haiku 4.5 / Sonnet 4.5 | extended のみ | オフ | adaptive |
読み方の勘所は二つあります。
ひとつは、thinking: {"type": "enabled", "budget_tokens": N} という旧来の書き方が 4.7 以降で拒否されることです。4.6 系では非推奨のまま通ってしまうので、「4.6 では動いていたのに」という形で表面化します。
もうひとつは逆向きで、4.5 世代は adaptive を受け付けません。新しい書き方に揃えたつもりでモデルを一段古いものへ落とすと、今度はそちらで 400 になります。
effort にも同じ種類の段差があります。xhigh は max より後から入った水準なので、max は使えるのに xhigh は使えないモデルがあります。Haiku 4.5 と Sonnet 4.5 に至っては、effort パラメータ自体に対応していません。
エラーが消えたあとに残るもの
Claude Code の v2.1.251(2026年8月28日)で、この組み合わせの扱いが変わりました。思考を無効にしたまま effort を xhigh または max にしていた場合、リクエストが失敗する代わりに high として送られます。
利用者としては、たしかに親切な変更です。作業の途中で 400 に叩き落とされることはなくなりました。
ただ、私はここに引っかかりました。失敗が消えたのではなく、失敗が見えなくなっただけだからです。個人開発で無人のパイプラインを毎晩回している立場からすると、落ちてくれるエラーはむしろ扱いやすい部類に入ります。落ちた工程は再実行すれば済みます。一方で「指定と違う水準で完走した」という結果は、成果物の質を眺めて初めて疑うことになります。
しかも v2.1.251 では、/effort の既定値がモデルごとに保存されるようになりました。設定が細かく効くようになったぶん、モデルを切り替えたときに以前そのモデルで選んでいた値が戻ってくる、という経路も増えています。
私は当面、重い工程の前で /effort と /status を突き合わせて、いま何がセッションに載っているのかを目で確認するようにしました。原始的ですが、静かに別物へ落ちる相手には有効です。似た性質の落とし穴として、settings.json のキー名を1文字間違えても、Claude Code は何も言わずに無視します も併せて点検の対象にしています。
送る前に、組み合わせだけ判定してしまう
API を直接叩く側は、送信前に弾いてしまうのが確実です。判定に必要な情報は上の表がすべてなので、表を定数として持ち、リクエストを組み立てる直前に照合します。
#!/usr/bin/env python3
"""thinking と effort の組み合わせを、リクエストを送る前に判定する。"""
# thinking: 受理する type と、そのモデルの既定
THINKING = {
"claude-fable-5": {"accepts": {"adaptive"}, "default": "always-on"},
"claude-mythos-5": {"accepts": {"adaptive"}, "default": "always-on"},
"claude-opus-5": {"accepts": {"adaptive", "disabled"}, "default": "on"},
"claude-opus-4-8": {"accepts": {"adaptive", "disabled"}, "default": "off"},
"claude-opus-4-7": {"accepts": {"adaptive", "disabled"}, "default": "off"},
"claude-sonnet-5": {"accepts": {"adaptive", "disabled"}, "default": "on"},
"claude-opus-4-6": {"accepts": {"adaptive", "enabled", "disabled"}, "default": "off"},
"claude-sonnet-4-6": {"accepts": {"adaptive", "enabled", "disabled"}, "default": "off"},
"claude-opus-4-5": {"accepts": {"enabled", "disabled"}, "default": "off"},
"claude-haiku-4-5": {"accepts": {"enabled", "disabled"}, "default": "off"},
"claude-sonnet-4-5": {"accepts": {"enabled", "disabled"}, "default": "off"},
}
# effort: 受理する水準(表にないモデルは effort 自体が非対応)
EFFORT = {
"claude-fable-5": {"low", "medium", "high", "xhigh", "max"},
"claude-mythos-5": {"low", "medium", "high", "xhigh", "max"},
"claude-opus-5": {"low", "medium", "high", "xhigh", "max"},
"claude-opus-4-8": {"low", "medium", "high", "xhigh", "max"},
"claude-opus-4-7": {"low", "medium", "high", "xhigh", "max"},
"claude-sonnet-5": {"low", "medium", "high", "xhigh", "max"},
"claude-opus-4-6": {"low", "medium", "high", "max"},
"claude-sonnet-4-6": {"low", "medium", "high", "max"},
"claude-opus-4-5": {"low", "medium", "high", "max"},
}
NO_DISABLE_AT = {"xhigh", "max"} # ここで thinking を切れない
STRICT_MODELS = {"claude-opus-5"} # 上の制約が効くモデル
def normalize(model: str) -> str:
"""claude-haiku-4-5-20251001 のような日付サフィックスを落とす。"""
parts = model.split("-")
while parts and parts[-1].isdigit() and len(parts[-1]) == 8:
parts.pop()
return "-".join(parts)
def preflight(model: str, thinking_type=None, effort=None):
key = normalize(model)
if key not in THINKING:
return [("unknown", f"{model} は判定表にありません。表を更新してください")]
findings = []
spec = THINKING[key]
if thinking_type and thinking_type not in spec["accepts"]:
findings.append(("400", f'thinking.type "{thinking_type}" は {key} が拒否します'))
if effort:
allowed = EFFORT.get(key)
if allowed is None:
findings.append(("400", f"{key} は effort パラメータ自体に非対応です"))
elif effort not in allowed:
findings.append(("400", f'effort "{effort}" は {key} では使えません'))
elif (key in STRICT_MODELS and effort in NO_DISABLE_AT
and thinking_type == "disabled"):
findings.append(("400", f'effort "{effort}" と thinking disabled は同時に指定できません'))
if effort in NO_DISABLE_AT and thinking_type is None and spec["default"] == "off":
findings.append(("warn", f"{key} は thinking 既定オフです。"
f'effort "{effort}" を活かすなら adaptive を明示してください'))
return findings or [("ok", "この組み合わせは受理されます")]手元で9通りを流したときの出力がこれです。
[ 400] claude-opus-5 / thinking=disabled / effort=xhigh
-> effort "xhigh" と thinking disabled は同時に指定できません
[ ok] claude-opus-5 / thinking=disabled / effort=high
-> この組み合わせは受理されます
[ ok] claude-opus-5 / thinking=(未指定) / effort=xhigh
-> この組み合わせは受理されます
[ warn] claude-opus-4-7 / thinking=(未指定) / effort=xhigh
-> claude-opus-4-7 は thinking 既定オフです。effort "xhigh" を活かすなら adaptive を明示してください
[ 400] claude-sonnet-5 / thinking=enabled / effort=high
-> thinking.type "enabled" は claude-sonnet-5 が拒否します
[ ok] claude-sonnet-4-6 / thinking=enabled / effort=medium
-> この組み合わせは受理されます
[ 400] claude-haiku-4-5-20251001 / thinking=adaptive / effort=(未指定)
-> thinking.type "adaptive" は claude-haiku-4-5 が拒否します
[ 400] claude-haiku-4-5-20251001 / thinking=enabled / effort=low
-> claude-haiku-4-5 は effort パラメータ自体に非対応です
[ 400] claude-fable-5 / thinking=disabled / effort=(未指定)
-> thinking.type "disabled" は claude-fable-5 が拒否します
9通りのうち 400 相当が5件、警告が1件、通過が3件でした。注目していただきたいのは 4行目の warn です。これは API が拒否する組み合わせではありません。Opus 4.7 は思考が既定オフなので、xhigh だけ指定しても期待した深さにはならない、という設計上の取りこぼしです。
400 になる組み合わせと、通るけれど意図と違う組み合わせを、同じ関数で並べて出すようにしています。前者は放っておいてもいずれ気づきますが、後者は誰も教えてくれません。
normalize() を挟んでいるのは、claude-haiku-4-5-20251001 のような日付付きの ID を表のキーに寄せるためです。8桁の数字だけを末尾から落とすので、claude-opus-4-5 のようなバージョン番号は残ります。
effort を変えると、キャッシュの前置が別物になります
もうひとつ、この話には続きがあります。
thinking の設定と effort の値は、キャッシュされるプロンプトの前置に含まれます。つまり会話の途中でどちらかを変えると、それまで積み上げたプレフィックスが一致しなくなり、cache_read_input_tokens が 0 に落ちます。
400 を避けようとして「じゃあ次のリクエストから high に下げよう」と判断したときに、静かに二重のコストが発生する経路がここにあります。エラーは消え、キャッシュも消えます。
対処は単純で、会話やセッションの単位では effort を固定し、変えたいときはワークロードごと分けます。なお、既定値を明示的に書くことは省略と等価に扱われるため、"effort": "high" と書き足すだけでキャッシュが落ちることはありません。
この「どこで区切るか」の判断そのものについては、キャッシュのTTLを1時間に延ばすかは、離席の長さではなく戻る場所で決めています で、私自身の運用の切り分け方をまとめています。
今日できる確認をひとつだけ
起動スクリプト、エージェント定義、CI のジョブ定義から xhigh と max を検索してください。
grep -rn 'xhigh\|"max"' --include='*.json' --include='*.md' --include='*.sh' .拾った箇所それぞれについて、同じ場所で指定しているモデルが xhigh に対応しているか、そして思考を切っていないかを確認します。この二点が噛み合っていない設定は、いま失敗ではなく別の水準で完走しています。
私自身、無人実行の質が落ちた原因をしばらく取り違えていました。同じ回り道を短くできれば嬉しいです。お読みいただきありがとうございました。