Skip to content
Chat & Streaming

Thinking & Reasoning

A thinking part holds the text a reasoning model writes before the answer. Render it in order with the tool calls.

TanStack AI streams the thinking as ThinkingPart entries in message.parts, next to the text and the tool calls.

Quick setup

1. Turn thinking on in the server route

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

ts
// api/chat.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);
}

2. Render the thinking parts on the client

Put each thinking part in a box that the user can open:

tsx
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";

export function Chat() {
  const { messages } = useChat({
    connection: fetchServerSentEvents("/api/chat"),
    threadId: "support-chat",
  });

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

Ask a hard question. The thinking box fills first, then the answer streams below it.

Some models think without sending text back. For those models, the thinking part stays empty.

How it works

The thinking streams before the answer. Its content grows token by token, the same as a text part.

The usual order of events:

  1. REASONING_START and REASONING_MESSAGE_START start the thinking.
  2. REASONING_MESSAGE_CONTENT events add tokens to ThinkingPart.content.
  3. TEXT_MESSAGE_START starts the visible answer.
  4. TEXT_MESSAGE_CONTENT events stream the answer text.

useChat in every framework updates messages with both thinking parts and text parts while they arrive.

To see levels and token budgets, read Reasoning.

Keep the thinking out of the UI

If you want the model to think but do not want to show the text, 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." }],
  threadId: "thread-1",
  reasoning: { level: "high", summary: false },
});

Then no thinking text streams.

Thinking and tool calls in order

Claude can think, call a tool, think again, then call another tool. Your UI must show that order, not all the thinking in one block at the top.

You get this order by default. message.parts keeps the order that 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 in the Quick setup already walks the parts in order. You do not need extra code.

Advanced

The ThinkingPart type

This is the type in @tanstack/ai, which the server and stored messages use. The thinking part that useChat returns has only type and content.

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

The thinking content comes from the REASONING_* events and from REASONING_ENCRYPTED_VALUE. STEP_STARTED and STEP_FINISHED carry only stepName. They do not carry thinking text.

Signed thinking

Thinking without a signature stays in the UI. Signed thinking is a ThinkingPart with a signature. Anthropic extended thinking uses it.

  • The next request sends signed thinking back in the same order as the original response. This includes the order around provider-executed tools.
  • In the next request body, the signature goes on the spec field encryptedValue of the role: "reasoning" message.
  • In the stream, the signature arrives as a REASONING_ENCRYPTED_VALUE event.

Redacted thinking

Claude can send a redacted thinking block. It is encrypted, so it has no text. It arrives as a ThinkingPart with:

  • redacted: true
  • an empty content
  • the encrypted data in signature

Keep the part in your stored messages. The next request sends it back to Claude unchanged. In a UI, the part from useChat has an empty content. Show a short label such as "Thinking hidden" in place of the empty text.

The block order goes back to the model

The stored message keeps a blockOrder map. 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.

Provider options

For all provider options, see the Anthropic adapter and the OpenAI adapter. To see how thinking fits into a tool loop, read Agentic Cycle.