thinking blocks cannot be modifiedエラー(400)の原因と直し方
thinkingを使うマルチターン会話でtool_resultを送り返すと出る400エラー。原因は直前のアシスタントメッセージを自前で組み立て直していることで、直し方は返ってきた内容をそのまま積み戻すことです。
thinkingを有効にしたままツール呼び出しのラウンドトリップを実装すると、2ターン目のリクエストが400エラーで拒否されることがあります。多くの場合の原因は、前のターンでAPIが返したアシスタントメッセージを、そのまま送り返さずに自前で組み立て直していることです。エラー文の読み方と、正しい送り返し方、似た文言の別エラーとの見分け方を扱います。
エラーの内容
ツール結果を送り返すリクエストが、次のinvalid_request_errorとともに400で失敗します。
`thinking` or `redacted_thinking` blocks in the latest assistant message cannot be modifiedマルチターンやツール利用の会話では、前のアシスタントメッセージをthinkingブロックとredacted_thinkingブロックごと次のmessagesに含めて送ります。APIはそれが、自分が返したものと変わっていないかを検証します。
検証の対象は、最新のアシスタントメッセージに並ぶ連続したthinkingブロックです。モデルが生成した並びと一致している必要があり、並べ替え・編集・一部だけ落とす、のどれもが拒否されます。
改変にあたる3つの操作
コード側の実装は、次の3つのどれかに当てはまることがほとんどです。
アシスタントメッセージを変えてしまう操作
typeでフィルタする
tool_useとtextだけを抜き出し、thinkingやredacted_thinkingを落とします。メッセージを組み立て直す
APIのレスポンスを使わず、自前のロジックで新しいメッセージオブジェクトを作ります。
シリアライザが形を崩す
未知のブロックtypeを捨てる、空のフィールドを捨てる、ブロックの順序を入れ替える、のいずれかです。
特に見落とされやすいのがredacted_thinkingです。安全上の理由で内容が編集済みになったブロックで、dataフィールドに暗号化された中身が入り、読める本文はありません。thinkingとは別のtypeを持つため、block.type == "thinking"だけで絞ると黙って落ちます。公式ドキュメントも、typeでフィルタするならredacted_thinkingを含めるよう注意しています。ブロック自体の仕様はredacted_thinkingブロックとはで扱っています。
直し方 — アシスタントターンをそのまま積み戻す
対処は一つです。レスポンスのcontent配列を、含まれるブロックのtypeを問わず丸ごと、次のリクエストのアシスタントメッセージに渡します。
ツール結果を返すときの流れ
- 1
1回目のレスポンスのcontentを保持する
thinking・redacted_thinking・tool_use・textを含む配列を、加工せずに変数かセッションに残します。 - 2
assistantメッセージとして先頭から積む
直前のuserメッセージの後ろに、保持したcontentをそのままassistantとして置きます。
- 3
最後にtool_resultを続ける
tool_useのidをtool_use_idに入れたtool_resultを、userメッセージとして末尾に追加します。
公式ドキュメントのPython例では、次のように1回目のレスポンスをそのまま2回目のリクエストに渡しています。
tool_use_block = next(block for block in response.content if block.type == "tool_use")
continuation = client.messages.create(
model="claude-opus-4-8",
max_tokens=16000,
thinking={"type": "adaptive"},
tools=[weather_tool],
messages=[
{"role": "user", "content": "What's the weather in Paris?"},
# レスポンスのcontentをそのまま渡す(thinkingブロックを含めて改変しない)
{"role": "assistant", "content": response.content},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_use_block.id,
"content": "Current temperature: 88°F",
}
],
},
],
)ツール呼び出しの途中にある最新のアシスタントメッセージには、末尾へのメッセージ追加以外の手を加えません。thinkingブロックは、同じメッセージのtool_useブロックとセットで戻します。thinking: {type: "enabled", budget_tokens: N}しか使えないモデルでも、この規則は同じです。サンプルのthinking設定を差し替えれば、そのまま当てはまります。
この規律はPreserved thinkingのprefix不変条件と同じ考え方です。
空のthinkingブロックも削らない
displayが"omitted"のとき、thinkingフィールドは空文字で返り、推論の中身はsignatureに入っています。Claude Fable 5.1では、この空の状態が既定です。空だからと捨てても、エラーにはなりません。モデルが以前の推論を失うだけです。
- 空フィールドを捨てるシリアライザ: 全ブロックが消えてもリクエストは失敗せず、毎ターン推論が抜け落ちたままになる
- ストリームを自前で解釈する実装: テキストが届かなくても、ブロックを開き、
signature_deltaで署名を受け取り、閉じるところまで保持する signatureが空のブロック: 送り返すと失敗する
一つだけ例外があります。omittedブロックの空のthinkingフィールドに自分で文字列を書き込んでも、その文字列は無視され、400にはなりません。400になるのは、dataやブロックの並び順を変えたときです。signatureを改変した場合は、次節のInvalid signatureのほうが返ります。空のthinkingは、表示用に埋め直す必要がありません。フィールドが空文字になる理由は別の記事にあります。
文言が似た2つの400エラーの見分け方
「modified」とは別に、Invalid signature in thinking blockで始まる400があります。出るのはClaude Fable 5.1・Claude Opus 5.5・Claude Sonnet 5.5の3モデルです。送り返すブロックの手前にあるsystem・tools・メッセージが、そのブロックを作ったときと違うと発生します。
| 症状 | 原因 | 直し方 |
|---|---|---|
cannot be modified | 原因最新のアシスタントメッセージの中身を改変した | 直し方レスポンスのcontentをそのまま積み戻す |
Invalid signatureの後に変更点を述べる文が続く | 原因手前のsystem・tools・履歴が変わった | 直し方履歴を追記のみにする |
Invalid signatureで文が止まる | 原因署名が切れた・改変された・空で送られた | 直し方署名を含むブロックを加工しない |
同じ本文のリクエストを再送しても、エラーは消えません。文末の変更点を述べる文は人間とログ向けで、文言は変わりうるため、コードで一致判定しない前提で読みます。署名はそのままで思考テキストだけを編集したときは、3行目ではなく「modified」のほうが返ります。
2行目のエラーは、前提が変わっていないかを疑います。
- 日付やモード名を差し込んだ
systemプロンプトの再構築 tools配列の追加・削除・編集- 一度付けたリマインダーを、後のターンで外す、書き換える
- 古いターンを要約して捨てつつ、直近のターンをthinking付きで残すクライアント側の圧縮
直し方の基本は、履歴を追記のみにすることです。途中で指示を足したいなら、systemを書き換えず、会話の途中に挿入するシステムメッセージを使います。間引きはサーバー側のコンテキスト編集や圧縮に任せます。この2つはエラーの引き金になりません。
このチェックは、2026年8月31日以降に作成されたアカウントでは既定で有効です。それより前のアカウントでは、thinking.block_binding.prefix_mismatch_behaviorを指定したリクエストにだけ適用されます。当面は古いアカウントで動いていても、追記のみの実装に揃えておくと、アカウントの違いで挙動が分かれません。
先に進めたいだけなら、ベータヘッダーthinking-binding-controls-2026-08-01を付け、prefix_mismatch_behaviorを"drop_block"にします。無効になったブロックは、推論を捨てたうえで落とされます。そのブロック以降のthinkingブロックもすべて落ち、プロンプトキャッシュは編集位置から作り直されます。ヘッダーを送れないときは、thinkingとredacted_thinkingの全ブロックを履歴から外し、他のブロックは残して1回だけ再試行します。
ヘッダーを付けたリクエストでは、レスポンスのinput_transformationsに、落としたブロックが報告されます。全ターンで空のままなら、履歴は追記のみで保たれています。生成したモデルが読めないブロックは、このエラーの原因になりません。APIがモデルに渡す前に落とし、ベータヘッダーを付ければinput_transformationsに出ます。
Claude Sonnet 5.5には追加の条件が2つあります。block_bindingが使えるのはthinking: {"type": "adaptive"}のときだけです。また、そのモデルのthinkingブロックは、生成したアカウントにも結び付いています。別のアカウントのキーで会話の続きを送る構成では、この点も疑う対象になります。
input_transformationsの項目は2種類です。
thinking_dropped: モデルが読む前に落とされ、課金されないブロック。reasonはprefix_binding_mismatch、model_binding_mismatch、別アカウント由来のorganization_binding_mismatchのいずれかthinking_mismatch_allowed: 手前が変わっているのに、そのリクエストでは検査が強制されず、ブロックがそのままモデルに届いて課金された状態。検査が既定で有効でない古いアカウントのリクエストにだけ現れる
トークン数を数えるエンドポイントも同じ検査を行い、同じ400を返します。同じ検査が走るので、本番に送る前の確認にも使えます。
ターンをまたぐときの扱い
どこまで送り返すかは、ツール呼び出しの内側か外側かで変わります。
| 範囲 | thinkingブロックの扱い |
|---|---|
| ツール呼び出しのターンの内側 | thinkingブロックの扱い必須。送り返さないとエラーになる |
| ターンをまたぐ会話全体 | thinkingブロックの扱い推奨。古いターンの分もそのまま送り返すのが基本 |
| ツール呼び出しを伴わない会話 | thinkingブロックの扱い任意。過去のターンのthinkingブロックを省略してもよい |
過去のthinkingを自分で選別する必要はありません。すべて送り返せば、APIが必要なブロックだけを選び、実際にClaudeへ見せた分だけを入力トークンとして課金します。残るターンはモデルで異なります。
- 全ターンを保持: Claude Opus 4.5以降のOpus、Claude Sonnet 4.6以降のSonnet、Fable系、Mythos系
- 直前のターンのみ: それより前のOpus・Sonnetと、Haiku 4.5までのHaiku全モデル
古い思考を意図して減らしたいときは、自前のフィルタではなく、context-editingのclear_thinking_20251015戦略を使います。
フォールバックなどで会話の途中にモデルを切り替えるときも、ブロックは変えずに送ります。読めないモデルに渡しても400にならず、APIがそのブロックを落として続行します。自分で取り除いてよいのは、無視されるだけのモデルへ送るときの入力トークン削減くらいです。フォールバッククレジットを使うリクエストは本文を変えられないため、このときの除去は避けます。
Claude Codeでの見え方
API・Agent SDKでmessages配列を自前で組み立てている実装は、ここまでの内容が対象です。Claude Codeをそのまま使っていて、400 due to tool use concurrency issuesのような文言が出る場合は、会話履歴のtool_use・tool_result・thinkingの並びが噛み合わなくなっています。手順はthinking block mismatchエラーの原因とrewindでの直し方の/rewindです。文言の違いはTool Use Concurrencyエラーの原因と対処法にまとめています。
まとめ
「thinking blocks cannot be modified」は、返ってきたcontentをそのまま積み戻せば消えます。Invalid signatureが出るなら、改変ではなく手前のsystem・tools・履歴の変更を疑います。effortによる思考量の調整はClaude effortとは — low〜maxの使い分け方に載せています。