Skip to content
Chat & Streaming

Thinking & Reasoning

Some models expose their internal reasoning as "thinking" content -- Claude with extended thinking, OpenAI o-series models with reasoning, and others. TanStack AI captures this as ThinkingPart in messages, streamed to your UI in real-time alongside text and tool calls.

Unsigned thinking stays in the UI. Signed thinking is a ThinkingPart with a signature. Anthropic extended thinking uses this. The next request sends signed thinking back in the same order as the original response, including around provider-executed tools. The next-turn body puts that signature on spec encryptedValue on the role: "reasoning" fan-out. Stream events use REASONING_ENCRYPTED_VALUE.

How It Works

Read the ThinkingPart in message.parts. Thinking content comes from REASONING_* events and from REASONING_ENCRYPTED_VALUE. STEP_STARTED and STEP_FINISHED only carry stepName.

ts
interface ThinkingPart {
  type: "thinking";
  content: string;
  stepId?: string;
  signature?: string;
  redacted?: boolean;
}

The ThinkingPart appears in UIMessage.parts alongside TextPart and ToolCallPart entries. As reasoning tokens arrive, its content accumulates token by token.

Claude can also send a redacted thinking block. It is encrypted, so it has no text. It arrives as a ThinkingPart with redacted: true, an empty content, and the encrypted data in signature. Keep the part in your stored messages: the next request sends it back to Claude unchanged. In a UI, show a short placeholder such as "Thinking hidden" instead of the empty text.

Turn thinking on

Set reasoning on chat(). It works the same on every provider:

ts
import {
  chat,
  chatParamsFromRequest,
  toServerSentEventsResponse,
} from "@tanstack/ai";
import { anthropicText } from "@tanstack/ai-anthropic";

export async function POST(request: Request) {
  const { messages, threadId, runId } = await chatParamsFromRequest(request);
  const stream = chat({
    adapter: anthropicText("claude-sonnet-5-5"),
    messages,
    threadId,
    runId,
    reasoning: "high",
  });
  return toServerSentEventsResponse(stream);
}

The thinking text streams back as ThinkingParts. Some models think without sending text back. With those, the part stays empty.

Want the model to think, but keep the text out of your UI? Set summary: false:

ts
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";

const stream = chat({
  adapter: openaiText("gpt-6.1-sol"),
  messages: [{ role: "user", content: "Plan a database migration." }],
  reasoning: { level: "high", summary: false },
});

Then no thinking text streams. See Reasoning for the levels and token budgets.

Rendering in React

Thinking parts appear in message.parts just like text and tool calls. A common pattern is to render them in a collapsible element so they don't dominate the UI:

tsx
import type { UIMessage } from "@tanstack/ai-react";

function MessageContent({ message }: { message: UIMessage }) {
  return (
    <div>
      {message.parts.map((part, idx) => {
        if (part.type === "thinking") {
          return (
            <details key={idx}>
              <summary>Thinking...</summary>
              <pre style={{ whiteSpace: "pre-wrap" }}>{part.content}</pre>
            </details>
          );
        }
        if (part.type === "text") {
          return <p key={idx}>{part.content}</p>;
        }
        return null;
      })}
    </div>
  );
}

Streaming Behavior

Thinking content streams before the final text response. As reasoning tokens arrive, ThinkingPart.content accumulates token by token, the same way TextPart.content does for the response text.

The typical streaming order is:

  1. Reasoning starts (REASONING_START / REASONING_MESSAGE_START). Encrypted blobs use REASONING_ENCRYPTED_VALUE.
  2. Reasoning tokens stream in (REASONING_MESSAGE_CONTENT) and accumulate into ThinkingPart.content.
  3. TEXT_MESSAGE_START starts the visible response.
  4. TEXT_MESSAGE_CONTENT streams the response text.

STEP_STARTED and STEP_FINISHED only carry stepName. They do not carry thinking text.

If you use useChat from @tanstack/ai-react (or the Solid/Vue/Svelte equivalents), your messages array updates with both thinking and text parts as they arrive.

Thinking and tool calls in order

Claude can think, call a tool, think again, then call another tool. You want your UI to show that order, not all the thinking in one lump at the top.

You get it by default. message.parts keeps the order the model used. For that answer, the part types are:

plaintext
thinking, tool-call, thinking, tool-call

What this means for your UI:

  • Each thinking block is its own ThinkingPart. Each one gets its own REASONING_MESSAGE_END and REASONING_END.
  • Text after a second thinking block starts a new text part. You get "A" and "B" as two parts, not "AB".
  • The render loop from Rendering in React already walks the parts in order. You do not need extra code.

The order also goes back to the model. The stored message keeps a blockOrder map, and the next request sends the blocks in the same order. Claude needs this, because it signs each thinking block against the blocks before it.

Gemini, Mistral, and Bedrock Converse also send thinking back on the next request, for history from the same model. History from another model goes in as plain text. See Keep saved history when you switch.

Next Steps