6 min readRishi

Claude Thinking Blocks: Echo the Signature or the Tool Loop Breaks

Claude Thinking Blocks: Echo the Signature or the Tool Loop Breaks

The model called get_weather. Your loop kept the tool_use block, dropped everything else, and sent the result back. The next request returned a 400: thinking blocks cannot be modified. The tool-use loop did not change. Thinking added a block you are not allowed to edit, and a second type you will miss if you filter on type == "thinking" alone.

Thinking is scratch work that arrives as content blocks, billed as output, and encrypted in a signature you pass back. The text you see is a summary. The raw chain of thought is not a display option.

Two knobs, one hard cap

On Claude Sonnet 4.6, thinking is off until you set it. Adaptive mode is the one to use:

from anthropic import Anthropic

client = Anthropic()
response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=16000,
    thinking={"type": "adaptive", "display": "summarized"},
    output_config={"effort": "high"},
    messages=[{"role": "user", "content": "What's the weather in Paris?"}],
)

thinking.type is whether Claude may emit thinking blocks. output_config.effort is how willing it is to think, and how deeply. Do not put adaptive in effort. That is a thinking mode, not a level. The documented levels are low, medium, high, xhigh, and max. Availability is per model; high is the default on Sonnet 4.6.

Effort is soft. max_tokens is the ceiling for thinking plus response text on that request. In a tool loop, each HTTP call has its own max_tokens. It does not bound the whole turn.

thinking: {type: "enabled", budget_tokens: N} is the older manual mode. On Sonnet 4.6 it still runs and is deprecated. On Claude 4.7 and later it is a 400 whose message starts with "thinking.type.enabled" is not supported. Manual mode also rejects tool_choice of any or a named tool. Adaptive mode accepts those, except on Claude Opus 5.5, Claude Sonnet 5.5, Claude Fable 5.1, and Claude Mythos 5.1, which reject forced tool use on every request. Where forced tool use is accepted, the response starts with the tool call and has no thinking block.

What the block actually is

A thinking-enabled response looks like this:

{
  "content": [
    {
      "type": "thinking",
      "thinking": "The user named a city. I should call get_weather before answering.",
      "signature": "WaUjzkypQ2mUEVM36O2Txu...."
    },
    {
      "type": "tool_use",
      "id": "toolu_01D7FLrfh4GYq7yT1ULFeyMV",
      "name": "get_weather",
      "input": { "city": "Paris" }
    }
  ]
}

display controls the thinking string, not whether the block exists:

displayWhat you get
"summarized"A summary of the reasoning
"omitted"An empty thinking field. The signature is still there

On many current models, including Opus 5.5 and Haiku 5.5, omitted is the default, so thinking is on and the summary is hidden until you opt in. Billing does not change. usage.output_tokens is the billed total. usage.output_tokens_details.thinking_tokens is how much of that was internal reasoning — the raw tokens, not the summary. On a stream, that breakdown arrives on the final message_delta.

Adaptive mode can skip thinking on a simple turn. Do not assume every assistant message starts with a thinking block. Manual mode still requires that the final assistant turn of a thinking-enabled request begins with one.

The API may also return redacted_thinking: { "type": "redacted_thinking", "data": "..." }. There is no readable text. omitted is a regular thinking block with an empty string. Redaction is a different type.

Echo the whole assistant turn

A tool-use loop is one assistant turn. From the model's side it is not done until the final text (or the next tool call) lands. When you return tool_result, you are continuing that turn. The thinking that chose the tool has to still be there.

messages.append({"role": "assistant", "content": response.content})
messages.append({
    "role": "user",
    "content": [
        {
            "type": "tool_result",
            "tool_use_id": block.id,
            "content": "20°C, sunny",
        }
    ],
})

response.content is the right payload: every thinking block, every redacted_thinking block, every tool_use, in the original order, signatures untouched. Dropping thinking, rewriting the summary, or keeping only type == "thinking" is how you get a 400. The one documented exception: text you stuff into the empty thinking field of an omitted block is ignored.

Across turns, pass everything back and let the API drop what the current model will not keep. Older Sonnet and Haiku models through 4.5 keep the last turn's thinking. Opus 4.5, Sonnet 4.6, and later keep prior turns and bill them as input. You do not have to prune. If you want to, the context-editing strategy is clear_thinking_20251015.

Do not toggle thinking in the middle of a tool loop. The API does not error; it silently disables thinking for that request. Change the configuration on the next user turn, after the assistant turn has finished.

Changing effort or switching enabled ↔ adaptive invalidates prompt cache breakpoints. The resolved effort is rendered into the prompt. Pick a configuration per conversation. If one turn needs more or less thinking, append guidance to that user message rather than flipping the request fields.

Streaming is the same rule with more events

Thinking streams as thinking_delta events, then one signature_delta just before content_block_stop. Text comes after. With display: "omitted", the thinking delta is empty and the signature still arrives; first visible text is sooner because the server is not streaming a summary.

Do not concatenate deltas into the next request. Use stream.get_final_message() (or the SDK equivalent) so the signature is attached. The SSE loop already says not to parse incomplete tool JSON. Thinking is the same shape: the block is not a value until it stops.

What thinking does not replace

Thinking is how the model checks its work before it answers or calls a tool. It is not a permission system. A write still belongs behind a function the model cannot skip, the same split as putting the workflow in code. Untrusted tool output is still data. The signature proves the block came from Claude. It does not prove the tool arguments are safe to execute.

Steer depth with effort. Cap the request with max_tokens. Round-trip response.content. If you treat thinking as debug prose you can strip, the tool loop is the first place that will tell you otherwise.

Keep reading

Newsletter

New posts, straight to your inbox

One email per post. No spam, no tracking pixels, unsubscribe anytime.

Comments

  • No comments yet. Be the first.