thinking blocks cannot be modifiedエラー(400)の原因と直し方
拡張思考を使うマルチターン会話でtool_resultを送り返すと出る400エラー。原因は前ターンのアシスタントメッセージを自前で組み立て直していることで、直し方はverbatimでのechoです。
拡張思考を有効にしたままツール呼び出しのラウンドトリップを実装すると、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はこのアシスタントメッセージが、自分が返したものと一字一句変わっていないかを検証します。送り返した内容が少しでも違えば、このエラーが返ります。
なぜアシスタントメッセージが変わってしまうのか
原因はコード側の実装パターンにほぼ限られます。よくあるのは次の2つです。
- content blockをtypeでフィルタしている:
tool_useとtextだけを抜き出して送り返し、thinkingやredacted_thinkingのブロックを取りこぼす - アシスタントメッセージを再構築している: APIから返ったレスポンスをそのまま使わず、自前のロジックで新しいメッセージオブジェクトを組み立て直す
redacted_thinkingは本文が読めない不透明なブロックです。中身が見えないため「使っていないなら消してよい」と判断してフィルタから漏らすコードが典型的に出ます。しかしAPI側はこのブロックの存在自体を検証対象にしているため、削るだけで検証に失敗します。
直し方 — アシスタントターンをverbatimでechoする
対処は一つだけです。APIレスポンスのcontent配列を、含まれるブロックのtypeを問わず丸ごとそのまま次のリクエストのmessagesに積み戻します。公式ドキュメントのPython例では、ツール呼び出しを含む1回目のレスポンスをそのまま2回目のリクエストのassistantメッセージに渡し、その後ろにtool_resultを積んだuserメッセージを続けています。
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ブロックと必ずセットで戻す必要があり、片方だけを送り返すことはできません。
よくあるつまずき
- SDKのヘルパー関数を経由すると起きやすい:
tool_useブロックだけを抽出してツール実行結果を組み立てるユーティリティ関数を書くと、その戻り値をそのままアシスタントメッセージ扱いで送り返してしまい、thinking系ブロックが欠落する block.type == "thinking"だけでフィルタする: 公式ドキュメントも名指しで注意しているパターンです。redacted_thinkingは安全上の理由で内容が編集済みになったブロックで、thinkingとは別のtypeを持ちます。thinkingだけを条件にフィルタするとredacted_thinkingが黙って落ち、ラウンドトリップの規約が壊れます- JSONのシリアライズ/デシリアライズで型情報を落とす:
signatureやdataのような不透明なフィールドを持たない独自の型定義でレスポンスを受け、必要なフィールドだけをマッピングして保存すると、次に送り返すときに元の中身を復元できない display: "omitted"の空文字を埋めようとする:thinkingフィールドが空文字で返ってきたときに、可読性のためにその場で説明文を書き込んで送り返すコードを書くことがあります。この場合に限っては例外で、omittedブロックの空のthinkingフィールドに置いた文字列はAPI側で無視されるだけで、エラーにはなりません。エラーになるのはsignatureやdata、ブロックの並び順を変えたときです
これらはいずれも「一部のブロックだけ使えばよい」という判断がコードに入り込むことで起きます。会話履歴に含めるアシスタントメッセージはAPIから受け取った状態を単位として扱い、加工しないのが原則です。過去のthinkingブロックを意図的に間引きたい場合も、自前でフィルタするのではなくcontext-editingのclear_thinking_20251015戦略のような公式の仕組みを使います。
前ターンの改変が原因の別エラーとの違い
似た文言のエラーに、会話の前提(systemプロンプトやツール定義、それより前のメッセージ)を書き換えたときに出るInvalid signature in thinking blockがあります。こちらは前提そのものが変わったことが原因で、アシスタントメッセージの中身自体は改変していなくても発生する点が今回のエラーと異なります。
Claude Codeを利用していて400 due to tool use concurrency issuesのように文言が変わって表示される場合は、Claude Code自身のセッション管理層で会話履歴が壊れているケースです。この場合はコード側の実装ではなくthinking block mismatchエラーの原因とrewindでの直し方の/rewind手順で復旧します。API・Agent SDKでmessages配列を自前で組み立てている実装は今回のエラーが対象、Claude CodeのCLIをそのまま使っている場合は/rewindが対象、と切り分けます。
この制約は拡張思考特有のものか
thinking・redacted_thinkingブロックの改変チェックは拡張思考(adaptive thinking / extended thinking)を有効にしている会話にのみ関係します。思考ブロックを含まない通常のツール呼び出しラウンドトリップでは、アシスタントメッセージの内容が多少前後しても、このエラー自体は発生しません。ただし会話履歴を編集して送り返す実装はtool_use_idの不整合など別の400エラーを招きやすいため、思考の有無にかかわらずアシスタントメッセージは受け取った形のまま扱うのが安全です。
なぜAPIはここまで厳格に検証するのか
Claudeがツールを呼び出すとき、APIは応答の組み立てを一時停止して外部からの結果を待っています。ツール結果を受け取ってから応答の続きを組み立てる以上、その手前にあった思考の内容がそのまま残っている必要があります。この検証には2つの理由があります。
- 推論の連続性: thinkingブロックはツール呼び出しに至った段階的な思考を記録しています。これを含めることで、Claudeは中断した場所から推論を再開できます
- 文脈の維持: ツール結果はAPIの構造上ユーザーメッセージとして表現されますが、実際には一連の推論の流れの一部です。thinkingブロックを保持することで、複数回のAPI呼び出しをまたいでこの流れを保てます
どこまで送り返す必要があるかは、ツール呼び出しの内側か外側かで変わります。
| 範囲 | thinkingブロックの扱い |
|---|---|
| ツール呼び出しのターンの内側 | thinkingブロックの扱い必須。送り返さないとエラーになる |
| ターンをまたぐ会話全体 | thinkingブロックの扱い推奨。古いターンの分もそのまま送り返すのが基本 |
| ツール呼び出しを伴わない会話 | thinkingブロックの扱い任意。過去のターンのthinkingブロックを省略してもよい |
過去のthinkingを自分でどこまで残すか判断する必要はありません。すべて送り返せば、APIが会話の維持に必要なブロックを自動的に選別し、実際にClaudeへ見せた分だけを入力トークンとして課金します。どのターンまで保持するかはモデルによって異なり、Claude Opus 4.5以降・Sonnet 4.6以降・Fable / Mythos系は全ターンを保持し、それより前のOpus / Sonnetと全Haikuモデルは直前のターンのみを保持します。
モデルを切り替える実装での注意点
フォールバック処理などで会話の途中でモデルを切り替える実装でも、thinkingブロックはそれまで通り改変せずに送り返します。thinkingブロックは、生成したモデルと同じか、それより新しいモデルからしか読めません。読めないモデルに送っても400エラーにはならず、APIが該当ブロックを黙って落として処理を続けます。取り除いてよいのは、読めないと分かっているブロックを入力トークン削減の目的で自分から間引くときだけで、それも必須の対処ではありません。
よくある質問
拡張思考(budget_tokens)の会話でも同じエラーになるか
なります。公式ドキュメントの手順はadaptive thinkingを前提にしていますが、thinking: {type: "enabled", budget_tokens: N}のみをサポートするモデルでも、アシスタントメッセージを改変せずに送り返すというラウンドトリップの規則自体は同じです。サンプルコードのthinking設定を書き換えるだけで、対処の考え方はそのまま当てはまります。
ツール呼び出しを使わない会話でも起きるか
起きにくくなります。thinkingブロックの改変チェックが必須になるのは主にツール呼び出しのターンの内側で、ツールを使わない通常のマルチターン会話では過去のthinkingブロックを省略すること自体は許容されています。ただし省略ではなく「一部だけ改変して送る」実装は、ツール呼び出しの有無にかかわらず問題を起こしやすいため、送るなら改変しないという原則に変わりはありません。省略するか送り返すかのどちらかにし、中途半端な加工を挟まないことが安全です。
まとめ
「thinking blocks cannot be modified」は、APIが返したアシスタントメッセージのcontentブロックを、typeでフィルタしたり再構築したりして送り返したときに出るエラーです。直し方は、返ってきたアシスタントメッセージをverbatimで次のリクエストに積み戻すことだけです。ツール呼び出しのラウンドトリップを自前で実装しているAPI・Agent SDKの利用者はこの記事の対処が該当し、effortによる思考量の調整方法はClaude effortとは — low〜maxの使い分け方に、thinkingフィールドが空文字で返る現象はthinkingのdisplay: summarizedとは — フィールドが空文字になる理由にまとめています。