본문으로 건너뛰기

사고 및 추론

일부 모델은 내부 추론을 "thinking" 콘텐츠로 노출합니다. 여기에는 extended thinking을 사용하는 Claude, 추론 기능을 사용하는 OpenAI o-series 모델 등이 포함됩니다. TanStack AI는 이를 메시지의 ThinkingPart로 캡처하고, 텍스트 및 도구 호출과 함께 UI로 실시간 스트리밍합니다.

서명되지 않은 thinking은 UI에 유지됩니다. 서명된 thinking은 signature가 있는 ThinkingPart입니다. Anthropic extended thinking이 이를 사용합니다. 다음 요청에서는 프로바이더가 실행한 도구 주변의 항목을 포함해, 서명된 thinking을 원래 응답과 동일한 순서로 다시 전송합니다. 다음 턴의 본문은 role: "reasoning" fan-out에서 해당 서명을 사양의 encryptedValue에 배치합니다. 스트림 이벤트는 REASONING_ENCRYPTED_VALUE를 사용합니다.

작동 방식

message.parts에서 ThinkingPart를 읽습니다. Thinking 콘텐츠는 REASONING_* 이벤트와 REASONING_ENCRYPTED_VALUE에서 제공됩니다. STEP_STARTEDSTEP_FINISHEDstepName만 전달합니다.

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

ThinkingPartUIMessage.parts에서 TextPartToolCallPart 항목과 함께 나타납니다. 추론 토큰이 도착하면 content에 토큰 단위로 누적됩니다.

Thinking 활성화

Thinking을 활성화하는 방법은 프로바이더에 따라 다릅니다.

앤트로픽 (확장된 사고)

modelOptions에서 type: "enabled"budget_tokens(최소 1024)를 지정해 thinking 옵션을 전달합니다. thinking 예산 외에 표시되는 응답을 위한 공간이 있도록 budget_tokensmodelOptions.max_tokens보다 작게 유지합니다:

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

export async function POST(request: Request) {
const { messages } = await request.json();
const stream = chat({
adapter: anthropicText("claude-sonnet-4-6"),
messages,
modelOptions: {
max_tokens: 32000,
// budget_tokens must be at least 1024 and below max_tokens
thinking: { type: "enabled", budget_tokens: 10000 },
},
});
return toServerSentEventsResponse(stream);
}

오픈 AI (추론 모델)

OpenAI o-series 모델(o1, o3, o3-mini, o3-pro)은 자동으로 추론을 수행합니다. reasoning 옵션으로 깊이를 제어할 수 있습니다:

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

export async function POST(request: Request) {
const { messages } = await request.json();
const stream = chat({
adapter: openaiText("o3-mini"),
messages,
modelOptions: {
reasoning: {
effort: "medium", // 'none' | 'minimal' | 'low' | 'medium' | 'high'
summary: "auto", // 'auto' | 'detailed'
},
},
});
return toServerSentEventsResponse(stream);
}

reasoning.summary를 설정하면 어댑터가 추론 요약 텍스트를 thinking 콘텐츠로 스트리밍합니다. 이를 설정하지 않아도 추론 토큰은 내부적으로 사용되지만, 모델에 따라 표면에 표시되지 않을 수 있습니다.

GPT-5 및 이후 모델도 추론을 지원합니다. reasoning.effort"none" | "minimal" | "low" | "medium" | "high"를 허용하며, none이 아닌 값이면 추론이 활성화됩니다:

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

export async function POST(request: Request) {
const { messages } = await request.json();
const stream = chat({
adapter: openaiText("gpt-5.5"),
messages,
modelOptions: {
reasoning: { effort: "high" },
},
});
return toServerSentEventsResponse(stream);
}

React에서 렌더링

Thinking 파트는 텍스트 및 도구 호출과 마찬가지로 message.parts에 나타납니다. 일반적으로 UI를 과도하게 차지하지 않도록 접을 수 있는 요소에 렌더링합니다:

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>
);
}

Quick Start 가이드에서는 응답 위에 thinking을 기울임꼴 텍스트로 렌더링하는 더 간단한 인라인 패턴을 보여줍니다.

스트리밍 동작

Thinking 콘텐츠는 최종 텍스트 응답 전에 스트리밍됩니다. 추론 토큰이 도착하면 응답 텍스트의 TextPart.content와 같은 방식으로 ThinkingPart.content에 토큰 단위로 누적됩니다.

일반적인 스트리밍 순서는 다음과 같습니다:

  1. 추론이 시작됩니다(REASONING_START / REASONING_MESSAGE_START). 암호화된 blob에는 REASONING_ENCRYPTED_VALUE를 사용합니다.
  2. 추론 토큰이 스트리밍되고(REASONING_MESSAGE_CONTENT) ThinkingPart.content에 누적됩니다.
  3. TEXT_MESSAGE_START가 표시되는 응답을 시작합니다.
  4. TEXT_MESSAGE_CONTENT가 응답 텍스트를 스트리밍합니다.

STEP_STARTEDSTEP_FINISHEDstepName만 전달합니다. thinking 텍스트는 전달하지 않습니다.

@tanstack/ai-reactuseChat(또는 Solid/Vue/Svelte의 해당 기능)를 사용하면 도착하는 대로 messages 배열이 thinking 파트와 텍스트 파트 모두를 반영해 업데이트됩니다.

다음 단계