Every provider has its own knob for thinking: reasoning.effort on OpenAI, thinking on Anthropic, thinkingConfig on Gemini, think on Ollama. You do not want to learn all of them. Set reasoning on chat() and the adapter sends the right field for the model.
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
export async function POST(request: Request) {
const { messages, threadId, runId } = await chatParamsFromRequest(request);
const stream = chat({
adapter: openaiText("gpt-6.1-sol"),
messages,
threadId,
runId,
reasoning: "high",
});
return toServerSentEventsResponse(stream);
}The levels go from least to most thinking:
off, minimal, low, medium, high, xhigh, max
Leave reasoning out and the provider default applies.
Each model has only some of the levels. The types take only the levels of the model you picked:
import { chat } from "@tanstack/ai";
import { anthropicText } from "@tanstack/ai-anthropic";
chat({
adapter: anthropicText("claude-opus-5-5"),
messages: [{ role: "user", content: "Hello" }],
reasoning: "max",
});
chat({
adapter: anthropicText("claude-haiku-4-5"),
messages: [{ role: "user", content: "Hello" }],
// @ts-expect-error claude-haiku-4-5 has no max level
reasoning: "max",
});A model that does not reason takes no reasoning at all. Each adapter page lists the levels of its models.
A level can come from a settings menu or from middleware, so the types cannot always catch it. At run time, the adapter moves it to the nearest level that the model has. It looks up first, then down.
For example, minimal on claude-opus-5-5 goes out as low, and max on gemini-3.8-flash goes out as high.
Pass an object for more control:
import { chat } from "@tanstack/ai";
import { geminiText } from "@tanstack/ai-gemini";
const stream = chat({
adapter: geminiText("gemini-2.5-pro"),
messages: [{ role: "user", content: "Check this proof." }],
reasoning: { level: "high", budgetTokens: 8000 },
});No budgetTokens? A budget model gets a budget from the level. The shared default is 1024 tokens for minimal, 2048 for low, 8192 for medium, and 16384 for high and up. Gemini 2.5 uses its own budgets: for example, high is 32768 on gemini-2.5-pro and 24576 on gemini-2.5-flash.
Middleware can set the level for one model call. For example, think less when the run retries:
import type { ChatMiddleware } from "@tanstack/ai";
const lowEffort: ChatMiddleware = {
name: "low-effort",
onConfig: () => ({ reasoning: { level: "low", summary: true } }),
};The thinking text arrives as thinking parts in message.parts, next to the text parts:
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
export function Chat() {
const { messages } = useChat({
connection: fetchServerSentEvents("/api/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>{part.content}</pre>
</details>
);
}
return part.type === "text" ? <p key={index}>{part.content}</p> : null;
})}
</div>
));
}Some models think without sending any text back. Then the part stays empty. See Thinking & Reasoning for streaming order and signed thinking.
| Adapter | Sent as |
|---|---|
| OpenAI, Azure OpenAI, Grok, Bedrock Responses | reasoning: { effort, summary } |
| OpenAI Chat Completions, Groq, LLM Gateway, Lovable, Bedrock chat, Cloudflare | reasoning_effort |
| Anthropic | adaptive thinking with the effort, or a thinking budget |
| Gemini | thinkingConfig.thinkingLevel or thinkingConfig.thinkingBudget |
| OpenRouter, Vercel AI Gateway | the gateway's reasoning object |
| Mistral | reasoning_effort or prompt_mode |
| BytePlus | thinking.type and reasoning_effort |
| Ollama | think |
| Bedrock Converse | Claude thinking fields |
| Claude Code, Codex, OpenCode | the CLI or server effort setting |
Upgrading from the old modelOptions fields? See Moving reasoning into chat().