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.
Set reasoning on chat(). It works the same on every provider:
// 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);
}Put each thinking part in a box that the user can open:
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.
The thinking streams before the answer. Its content grows token by token, the same as a text part.
The usual order of events:
useChat in every framework updates messages with both thinking parts and text parts while they arrive.
To see levels and token budgets, read Reasoning.
If you want the model to think but do not want to show the text, set summary: false:
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.
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:
thinking, tool-call, thinking, tool-callWhat this means for your UI:
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.
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.
Thinking without a signature stays in the UI. Signed thinking is a ThinkingPart with a signature. Anthropic extended thinking uses it.
Claude can send a redacted thinking block. It is encrypted, so it has no text. It arrives as a ThinkingPart with:
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 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.
For all provider options, see the Anthropic adapter and the OpenAI adapter. To see how thinking fits into a tool loop, read Agentic Cycle.