How to Stream Thinking Blocks to User Interfaces
Stream Claude extended thinking blocks with Server-Sent Events to render real-time collapsible accordions and isolate thinking deltas from text.
Frontier reasoning models execute internal inference steps to evaluate hypotheses, verify logic, and synthesize complex multi-step solutions. In interactive conversational applications, streaming these internal reasoning tokens directly into the visible response window degrades user experience. Raw thinking tokens flood the chat viewport, obscuring final answers and creating visual friction for end users.
To maintain responsive, transparent user interfaces, modern frontend architectures decouple the thinking stream from visible text generation. The Anthropic Messages API exposes extended thinking as separate content blocks within its Server-Sent Events (SSE) protocol. By tracking content block indices and event types, frontend message handlers route thinking deltas into collapsible accordion components while reserving the main viewport for final answers.
This guide details the SSE event lifecycle for thinking blocks, UI state machines for collapsible accordions, handling thinking_delta and signature_delta events, full client implementations across Python and TypeScript, raw cURL streaming probes, and production error-handling strategies.
Companion code repository: ZeroLabs Claude Recipes Monorepo.
Contents
- How Does Extended Thinking Stream Over SSE?
- What Are the Hard Rules for Thinking Streaming?
- How Do Thinking and Text Streams Compare Across UI States?
- How to Design a Thinking Accordion State Machine?
- How to Implement Async Thinking Streaming in Python?
- How to Implement UI Thinking Streams in TypeScript?
- How to Inspect Raw Thinking Events with cURL?
- How to Handle Signatures and Multi-Turn History?
- What Are Common Production Failure Modes?
- FAQ
How Does Extended Thinking Stream Over SSE?
When extended thinking is enabled on Claude 3.7 Sonnet, the Anthropic Messages API structures the streaming response into sequential content blocks. Rather than interleaving thinking and output text within a single string buffer, the inference cluster emits thinking tokens inside a dedicated thinking content block (typically at block index 0) before opening a text content block (typically at block index 1).
flowchart LR
A["Client Request\n(thinking.type: 'enabled'\nbudget_tokens: 2048)"] --> B["Anthropic Gateway\n(SSE Connection Opened)"]
B --> C["Block 0: content_block_start\n(type: 'thinking')\nUI: Expand Accordion"]
C --> D["Block 0: content_block_delta\n(type: 'thinking_delta')\nUI: Append to Accordion"]
D --> E["Block 0: content_block_delta\n(type: 'signature_delta')\nUI: Store Signature"]
E --> F["Block 0: content_block_stop\nUI: Collapse Accordion"]
F --> G["Block 1: content_block_start\n(type: 'text')\nUI: Initialize Answer View"]
G --> H["Block 1: content_block_delta\n(type: 'text_delta')\nUI: Stream Answer Text"]
H --> I["Block 1: content_block_stop\n+ message_delta (Usage)"]The streaming sequence proceeds through distinct event types:
message_start: Emits the initial message container, specifying model identifier and input token counts.content_block_start(index 0): Declarescontent_block: {"type": "thinking", "thinking": ""}. The client UI recognizes that reasoning has commenced and initializes the accordion component in an expanded or pulsing state.content_block_delta(index 0,thinking_delta): Emits incremental reasoning tokens insidedelta: {"type": "thinking_delta", "thinking": "..."}. The client appends these tokens exclusively to the thinking buffer.content_block_delta(index 0,signature_delta): Emitsdelta: {"type": "signature_delta", "signature": "..."}containing an encrypted cryptographic token that validates model integrity.content_block_stop(index 0): Signals completion of the reasoning phase. The client computes elapsed reasoning duration and collapses the thinking accordion.content_block_start(index 1): Declarescontent_block: {"type": "text", "text": ""}. The client transitions to the answer rendering phase.content_block_delta(index 1,text_delta): Emits standard text tokens insidedelta: {"type": "text_delta", "text": "..."}for primary viewport rendering.content_block_stop(index 1): Concludes the primary answer block.message_delta&message_stop: Emits final execution metadata, includingstop_reasonand cumulative output tokens billed across both thinking and text.
For background on standard SSE streaming payloads and connection error handling, consult our foundation guide on How to Implement Server-Sent Event Streaming with Claude.
What Are the Hard Rules for Thinking Streaming?
Building robust user interfaces around extended thinking requires adherence to key operational constraints:
Beyond token validation, client applications must observe four engineering imperatives:
- Strict Buffer Separation: Client event loops must route
thinking_deltaandtext_deltapayloads to independent state containers. Never concatenate thinking deltas onto the visible chat message string. - Signature Preservation: The cryptographic signature emitted via
signature_deltamust be preserved in state if your application passes assistant messages back into subsequent conversational turns. Removing or altering signatures invalidates multi-turn context. - Token Billing Accounting: All tokens generated during thinking (
thinking_delta) count againstusage.output_tokensand are billed at standard output token pricing ($15.00 per million tokens on Claude 3.7 Sonnet). A request with a 2,048 token thinking budget and 500 tokens of text incurs billing for up to 2,548 output tokens. - Forbidden Sampling Modifiers: When thinking is enabled, you cannot set
temperature,top_p, ortop_kvalues. Attempting to override temperature produces an immediate API validation rejection.
For turn alternation and request structure specifics, review How to Structure Messages API Requests and Roles.
How Do Thinking and Text Streams Compare Across UI States?
Managing user perception during long reasoning phases requires distinct visual states. The table below contrasts how client frontends handle each streaming phase:
| Stream Phase | API Event Signature | Client UI State | Visual Behavior & Interaction |
|---|---|---|---|
| Reasoning Phase | content_block_delta (thinking_delta) | uiState = "thinking" | Collapsible accordion expanded; displays streaming monospace reasoning text and animated stopwatch counter. |
| Signature Phase | content_block_delta (signature_delta) | uiState = "validating" | Accordion displays "Reasoning verified"; captures encrypted token silently into component memory. |
| Transition Phase | content_block_stop & content_block_start | uiState = "transition" | Accordion collapses smoothly; status indicator displays "Thought for 3.4 seconds"; cursor focuses on answer view. |
| Answering Phase | content_block_delta (text_delta) | uiState = "answering" | Main chat bubble renders markdown text via streaming typewriter effect; accordion remains collapsed but clickable. |
How to Design a Thinking Accordion State Machine?
A resilient web client represents stream progress through a finite state machine. This design prevents UI flickering, race conditions, and out-of-order text rendering.
[IDLE] | | (content_block_start: type="thinking") v [THINKING] None: self.thinking_buffer: str = "" self.signature_buffer: str = "" self.text_buffer: str = "" self.current_block: Optional[str] = None self.thinking_start: Optional[float] = None self.thinking_duration_ms: float = 0.0 def on_block_start(self, block_type: str) -> None: self.current_block = block_type if block_type == "thinking": self.thinking_start = time.perf_counter() print("\n[UI: ACCORDION OPENED] Reasoning stream initiated...") elif block_type == "text": print("\n[UI: ACCORDION COLLAPSED] Answer streaming initiated...\n") def on_thinking_delta(self, delta_text: str) -> None: self.thinking_buffer += delta_text sys.stdout.write(f"\r[Thinking: {len(self.thinking_buffer)} chars streamed]") sys.stdout.flush() def on_signature_delta(self, signature: str) -> None: self.signature_buffer += signature def on_text_delta(self, delta_text: str) -> None: self.text_buffer += delta_text sys.stdout.write(delta_text) sys.stdout.flush() def on_block_stop(self) -> None: if self.current_block == "thinking" and self.thinking_start: self.thinking_duration_ms = (time.perf_counter() - self.thinking_start) * 1000 print(f"\n[UI: REASONING FINALIZED] Elapsed: {self.thinking_duration_ms:.1f}ms") self.current_block = Noneasync def stream_thinking_pipeline(prompt: str, budget: int = 2048) -> None: client = anthropic.AsyncAnthropic(api_key=os.environ["ANTHROPIC_API_KEY"]) manager = ThinkingStreamManager() async with client.messages.stream( model="claude-3-7-sonnet-20250219", max_tokens=4096, thinking={"type": "enabled", "budget_tokens": budget}, messages=[{"role": "user", "content": prompt}], ) as stream: async for event in stream: if event.type == "content_block_start": manager.on_block_start(event.content_block.type) elif event.type == "content_block_delta": dtype = event.delta.type if dtype == "thinking_delta": manager.on_thinking_delta(event.delta.thinking) elif dtype == "signature_delta": manager.on_signature_delta(event.delta.signature) elif dtype == "text_delta": manager.on_text_delta(event.delta.text) elif event.type == "content_block_stop": manager.on_block_stop() print(f"\nTotal thinking tokens buffered: {len(manager.thinking_buffer)} characters") print(f"Total answer tokens buffered: {len(manager.text_buffer)} characters")if __name__ == "__main__": asyncio.run(stream_thinking_pipeline("Derive the optimal cache eviction policy for 100k key items."))How to Implement UI Thinking Streams in TypeScript?
In frontend web clients (built with React, Next.js, or Vue), state updates must trigger component re-renders. The TypeScript implementation below structures state updates for a reactive accordion:
import Anthropic from '@anthropic-ai/sdk';export interface UIThinkingState { status: 'idle' | 'thinking' | 'answering' | 'completed'; thinkingText: string; signature: string; answerText: string; thinkingDurationMs: number; isExpanded: boolean;}export async function streamThinkingToClient( prompt: string, onUpdate: (state: UIThinkingState) => void): Promise { const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY, }); const state: UIThinkingState = { status: 'idle', thinkingText: '', signature: '', answerText: '', thinkingDurationMs: 0, isExpanded: true, }; let thinkingStart = 0; const stream = await anthropic.messages.create({ model: 'claude-3-7-sonnet-20250219', max_tokens: 4096, thinking: { type: 'enabled', budget_tokens: 2048, }, stream: true, messages: [{ role: 'user', content: prompt }], }); for await (const event of stream) { switch (event.type) { case 'content_block_start': if (event.content_block.type === 'thinking') { state.status = 'thinking'; state.isExpanded = true; thinkingStart = Date.now(); } else if (event.content_block.type === 'text') { state.status = 'answering'; state.isExpanded = false; // Auto-collapse accordion when answer begins } onUpdate({ ...state }); break; case 'content_block_delta': if (event.delta.type === 'thinking_delta') { state.thinkingText += event.delta.thinking; } else if (event.delta.type === 'signature_delta') { state.signature += event.delta.signature; } else if (event.delta.type === 'text_delta') { state.answerText += event.delta.text; } onUpdate({ ...state }); break; case 'content_block_stop': if (state.status === 'thinking') { state.thinkingDurationMs = Date.now() - thinkingStart; } onUpdate({ ...state }); break; case 'message_stop': state.status = 'completed'; onUpdate({ ...state }); break; } } return state;}How to Inspect Raw Thinking Events with cURL?
You can observe raw SSE thinking events using curl with the unbuffered flag (-N):
curl -s -N 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-3-7-sonnet-20250219", "max_tokens": 4096, "thinking": { "type": "enabled", "budget_tokens": 2048 }, "stream": true, "messages": [ { "role": "user", "content": "Verify if 32,767 is a prime number." } ] }'The raw event output begins with the initialization of block 0 as a thinking block:
event: message_startdata: {"type":"message_start","message":{"id":"msg_01AB...","type":"message","role":"assistant","content":[],"model":"claude-3-7-sonnet-20250219","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":25,"output_tokens":1}}}event: content_block_startdata: {"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}event: content_block_deltadata: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"Let's evaluate whether 32,767 is a prime number."}}event: content_block_deltadata: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" Recall that 32,767 = 2^15 - 1. We know that 2^ab - 1 is divisible by 2^a - 1."}}event: content_block_deltadata: {"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":"Ev8BAgMA...=="}}event: content_block_stopdata: {"type":"content_block_stop","index":0}event: content_block_startdata: {"type":"content_block_start","index":1,"content_block":{"type":"text","text":""}}event: content_block_deltadata: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"32,767 is not a prime number. It is a composite number factored as 7 × 31 × 151."}}event: content_block_stopdata: {"type":"content_block_stop","index":1}event: message_deltadata: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":682}}event: message_stopdata: {"type":"message_stop"}Notice how signature_delta arrives at the end of block 0 right before content_block_stop.
How to Handle Signatures and Multi-Turn History?
When persisting conversation history in multi-turn dialogues, thinking blocks require special handling:
- Retaining Signatures: When sending previous assistant turns back to Claude in multi-turn chats, you must include the complete thinking block with its valid
signature. Modifying the thinking text or omitting the signature causes an API validation error. - Stripping Thinking for Storage Economy: If your application does not need Claude to recall its exact chain of reasoning in subsequent turns, you can strip thinking blocks entirely from your conversation database, storing only the visible
textcontent block as the assistant turn. - Redacted Thinking Blocks: In enterprise environments where safety filters redact intermediate thinking, the API emits
type: "redacted_thinking"content blocks with encrypted payloads. Your UI should render these as "Thinking processed securely" without attempting string parsing.
To understand output token limits and cutoff reasons during extended generation, see How to Handle Stop Reasons and Max Token Truncation.
What Are Common Production Failure Modes?
When engineering thinking stream pipelines, protect your application against these four architectural pitfalls:
- Max Tokens Budget Inversion (
max_tokens <= budget_tokens): Developers often configuremax_tokens: 2048andbudget_tokens: 2048. The API requires headroom for final answer generation;max_tokensmust always be larger thanbudget_tokens. Always allocate at least 1,024 tokens of output headroom above your thinking budget. - Buffer Bleed: Appending all
content_block_deltapayloads to a single message string renders raw chain-of-thought analysis directly into the user interface. Always switch ondelta.typeorevent.index. - Missing Minimum Budget: Setting
budget_tokensbelow 1,024 tokens causes an immediate HTTP 400 validation failure. Claude requires a minimum 1,024 token thinking allocation. - Unsupported Temperature Parameters: Specifying
temperature: 0.7alongsidethinking.type: "enabled"triggers an API error. Thinking models enforce deterministic sampling internally. Remove all sampling parameters when enabling extended thinking.
To configure API keys and deployment environments securely, consult How to Manage Anthropic API Keys & Env Variables.
FAQ
- What is the minimum thinking budget allowed by Claude Messages API?
The minimum thinking budget is 1,024 tokens. Setting
budget_tokensto any value lower than 1,024 returns an HTTP 400 Bad Request error.
- Why must max_tokens be greater than thinking budget_tokens?
The
budget_tokensparameter specifies the maximum number of tokens allocated for internal reasoning, whilemax_tokenscaps the total response (thinking tokens plus text output tokens). Ifmax_tokensis equal to or less thanbudget_tokens, there would be no token headroom remaining to generate the actual user-facing answer.
- Are thinking tokens billed at the same rate as standard output tokens?
Yes. All tokens generated within the thinking block are billed at the standard output token rate for the selected model. For Claude 3.7 Sonnet, both thinking tokens and final text tokens are billed at $15.00 per million output tokens.
- Can users interact with the thinking accordion while text is still streaming?
Yes. By decoupling UI accordion state from the main text stream, users can click to expand, collapse, or scroll through the thinking logs without blocking or disrupting the real-time typewriter rendering of the final answer.