Vercel AI SDK에서 마이그레이션
이 가이드는 Vercel AI SDK(ai + @ai-sdk/*)에서 TanStack AI로 마이그레이션하는 데 도움이 됩니다. 두 라이브러리는 LLM 호출, 스트리밍, 도구 사용, 구조화된 출력, 프레임워크 훅이라는 동일한 문제 영역을 다루지만, TanStack AI는 향상된 타입 안전성, 트리 셰이킹 가능한 어댑터, 동형 도구 시스템, 일급 미들웨어 파이프라인을 갖춘 다른 아키텍처를 사용합니다.
"이전" 예제는 AI SDK v5 및 v6을 대상으로 합니다. 이전 v4 명칭과 다른 부분은 본문에서 별도로 표시합니다.
왜 마이그레이션하나요?
TanStack AI는 다음과 같은 여러 장점을 제공합니다.
- 트리 셰이킹 가능한 어댑터 - 필요한 것만 가져와 번들 크기를 줄입니다.
- 동형 도구 - 도구를 한 번 정의하고 서버와 클라이언트에서 별도로 구현합니다.
- 모델별 타입 안전성 - TypeScript가 각 모델에서 사용할 수 있는 정확한 옵션을 인식합니다.
- 프레임워크 비종속 - React, Vue, Solid, Svelte 및 vanilla JS에서 작동합니다.
- 완전한 스트리밍 타입 안전성 - 스트림 청크와 메시지 파트에 타입이 지정됩니다.
빠른 참조
| Vercel AI SDK | TanStack AI |
|---|---|
ai | @tanstack/ai |
@ai-sdk/openai | @tanstack/ai-openai |
@ai-sdk/anthropic | @tanstack/ai-anthropic |
@ai-sdk/google | @tanstack/ai-gemini |
@ai-sdk/google-vertex | @tanstack/ai-vertex (Gemini), @tanstack/ai-anthropic/vertex (Claude), @tanstack/ai-grok/vertex (Grok), 그리고 @tanstack/ai-mistral/vertex (Mistral) |
@ai-sdk/react | @tanstack/ai-react |
@ai-sdk/vue | @tanstack/ai-vue |
@ai-sdk/solid | @tanstack/ai-solid |
@ai-sdk/svelte | @tanstack/ai-svelte |
참고: AI SDK v5부터 프레임워크 훅이
ai/react(v4)에서@ai-sdk/react와 같은 전용 패키지로 이동했습니다. v4를 사용 중이라면 이전 서브패스를 v5에 해당하는 경로로 바꾸세요.
설치
이전(Vercel AI SDK)
# v5+ (framework hook lives in @ai-sdk/react)
npm install ai @ai-sdk/react @ai-sdk/openai @ai-sdk/anthropic
이후(TanStack AI)
npm install @tanstack/ai @tanstack/ai-react @tanstack/ai-openai @tanstack/ai-anthropic
서버 측 마이그레이션
기본 텍스트 생성
이전(Vercel AI SDK)
import { streamText, convertToModelMessages } from 'ai'
import { openai } from '@ai-sdk/openai'
export async function POST(request: Request) {
const { messages } = await request.json()
const result = streamText({
model: openai('gpt-4o'),
messages: await convertToModelMessages(messages),
})
return result.toUIMessageStreamResponse()
// (v4: result.toDataStreamResponse())
}
이후(TanStack AI)
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-4o'),
messages,
})
return toServerSentEventsResponse(stream)
}
주요 차이점
| Vercel AI SDK | TanStack AI | 주석 |
|---|---|---|
streamText() | chat() | 주요 텍스트 생성 함수 |
generateText() | chat({ stream: false }) | Promise<string> 을 반환합니다 |
generateObject() / streamObject() / Output.object() | chat({ outputSchema }) | Promise<T> 을 반환합니다 — 구조화된 출력 참조 |
openai('gpt-4o') | openaiText('gpt-4o') | 어댑터 |
result.toUIMessageStreamResponse() / .toTextStreamResponse() | toServerSentEventsResponse(stream) / toHttpResponse(stream) | 별도의 유틸리티 함수 |
model 파라미터 | adapter 파라미터 | 어댑터에 내장된 모델 |
전체 streamText → chat() 옵션 매핑
다음은 AI SDK v6 기준으로 streamText가 허용하는 옵션과 각 옵션이 TanStack AI의 chat()에서 위치하는 곳을 보여줍니다. 양쪽에 존재하는 옵션은 별도로 언급하지 않는 한 의미가 동일합니다.
streamText 옵션 | chat() 동등값 | 참고 |
|---|---|---|
model: openai('gpt-4o') | adapter: openaiText('gpt-4o') | 활동별 어댑터 |
prompt: 'Hello' | messages: [{ role: 'user', content: 'Hello' }] | TanStack 은 메시지 전용입니다 |
messages | messages | 동일한 개념이지만 콘텐츠 구성 요소는 다릅니다 (참조 멀티모달) |
system: 'You are…' | systemPrompts: ['You are…'] | 루트 레벨 string[] |
tools: { name: tool({…}) } | tools: [toolInstance, …] | 키가 있는 객체 대신 도구 인스턴스 배열 |
toolChoice: 'auto' | 'required' | 'none' | { type, toolName } | modelOptions.toolChoice (제공자별) | 최상위 옵션이 아님 — 어댑터의 modelOptions 에서 설정 |
activeTools: string[] | tools 를 직접 필터링하거나, prepareStep 와 동등한 것을 미들웨어를 통해 사용 | 전용 옵션이 없음 — Middleware 를 참조하여 동적 도구 필터링 확인 |
maxOutputTokens | maxTokens | 원래 OpenAI 명명 규칙과 일치하도록 이름 변경됨 |
temperature | temperature | 동일 |
topP | topP | 동일 |
topK | modelOptions.topK (제공자가 지원하는 경우) | typed modelOptions 하에 위치함 |
presencePenalty | modelOptions.presencePenalty | 타입이 지정된 modelOptions 아래에 위치합니다 |
frequencyPenalty | modelOptions.frequencyPenalty | 타입이 지정된 modelOptions 아래에 위치합니다 |
seed | modelOptions.seed | 타입이 지정된 modelOptions 아래에 위치합니다 |
stopSequences | modelOptions.stop (제공자별) | 타입이 지정된 modelOptions 아래에 위치합니다 |
maxRetries | fetch/어댑터를 감싸거나 재시도 middleware 추가 | chat()에 내장되어 있지 않음 |
timeout | abortController + AbortSignal.timeout(ms) 결합 | chat()에 내장되어 있지 않음 |
abortSignal: controller.signal | abortController: controller | signal만이 아니라 controller 자체를 전달 |
headers | 어댑터에서 설정(예: openaiText({ headers })) | 호출별 옵션이 아님 |
providerOptions: { openai: { … } } | modelOptions: { … } | 평평함; 어댑터는 이미 해당 제공자를 알고 있습니다. 모델별로 타입화됨 |
stopWhen: stepCountIs(5) | agentLoopStrategy: maxIterations(5) | 에이전트 루프 제어 를 참조하세요 |
stopWhen: hasToolCall('x') | 사용자 정의 AgentLoopStrategy 로 messages 를 검사함 | 특정 도구에서 멈추는 내장 "preset" 은 아직 없습니다 — 단일 줄 사용자 정의 전략; 에이전트 루프 제어 를 참조하세요 |
stopWhen: [a, b] | agentLoopStrategy: combineStrategies([a, b]) | 여러 조건, AND 의미 |
prepareStep | middleware 및 onConfig/onIteration | 미들웨어 참조 |
experimental_transform | middleware.onChunk (변환 / 삭제 / 확장된 청크) | Middleware 를 참조하세요 |
experimental_context | context (루트 레벨) | 미들웨어 훅 및 도구 구현에 전달되는 타입화된 런타임 컨텍스트 |
experimental_telemetry | middleware + 선택한 트레이서 | Observability 를 참조하세요 |
experimental_repairToolCall | middleware.onBeforeToolCall | 변환된 인수를 반환하거나 결정을 내리세요 |
experimental_download | messages 을 호출하기 전에 chat() 로 전처리하세요 | 내장된 후크 없음 |
onChunk (streamText) | middleware.onChunk | 반환된 이터러블을 소비하여도 접근 가능합니다 |
onError | middleware.onError | 터미널 후크 |
onStepFinish | middleware.onIteration / onToolPhaseComplete / onUsage | 더 세분화된 후크로 분할하세요 |
onFinish | middleware.onFinish | 터미널 훅 |
onAbort | middleware.onAbort | 터미널 훅 |
output: Output.object({ schema }) | outputSchema | 구조화된 출력 를 참조하세요 |
| — | conversationId / threadId / runId | TanStack 전용으로, 시스템 및 AG-UI 간 요청을 상관관계화하기 위해 |
streamText 결과 → TanStack AI 대응 항목
streamText는 접근자 프로미스를 포함한 객체를 반환하지만 TanStack AI는 스트림을 직접 반환합니다. 해당 객체에서 얻을 수 있는 모든 정보는 스트림 소비, 미들웨어 또는 응답 헬퍼를 통해 사용할 수 있습니다.
streamText 결과 멤버 | TanStack AI 동등체 |
|---|---|
result.textStream | 비동기 이터러블을 필터링: for await (const c of stream) if (c.type === 'text-delta') … |
result.fullStream | chat()이 반환하는 stream 자체가 전체 스트림(AsyncIterable<StreamChunk>)입니다 |
result.text | await streamToText(stream) or chat({ …, stream: false }) |
result.content | middleware.onChunk에 파트를 누적하거나 최종 UIMessage를 onFinish에서 읽기 |
result.toolCalls / result.toolResults | middleware.onChunk / onAfterToolCall 에서 조각별로 읽기 |
result.usage / result.totalUsage | middleware.onUsage(ctx, usage) |
result.finishReason | middleware.onFinish(ctx, info) |
result.steps | middleware.onIteration / onToolPhaseComplete 를 통해 누적합니다 |
result.toUIMessageStreamResponse() | toServerSentEventsResponse(stream) |
result.toTextStreamResponse() | streamToText(stream) 로 수집하여 평범한 Response 을 반환하거나, fetchHttpStream 와 toHttpResponse(stream) 를 통해 페어링합니다 |
result.pipeUIMessageStreamToResponse(res) | toServerSentEventsStream(stream).pipeTo(…) |
result.consumeStream() | for await (const _ of stream) {} |
생성 옵션
TanStack AI는 여러 프로바이더에서 공통으로 사용하는 일부 옵션(temperature, topP, maxTokens)을 최상위로 올리고, 프로바이더별 옵션은 하나의 타입 지정 modelOptions 묶음으로 이동합니다. providerOptions: { openai: {…} } 중첩은 없습니다. 어댑터가 이미 프로바이더를 알고 있으므로 modelOptions는 평평하며 선택한 모델에 맞게 타입이 지정됩니다.
이전 (Vercel AI SDK v5+)
import { streamText } from 'ai'
import { openai } from '@ai-sdk/openai'
const messages = [{ role: 'user' as const, content: 'Hello' }]
const result = streamText({
model: openai('gpt-4o'),
messages,
temperature: 0.7,
maxOutputTokens: 1000, // (v5+); `maxTokens` on v4
topP: 0.9,
topK: 40,
presencePenalty: 0.1,
frequencyPenalty: 0.1,
seed: 42,
stopSequences: ['\n\nUser:'],
// Provider-specific options (v5+)
providerOptions: {
openai: {
responseFormat: { type: 'json_object' },
},
},
})
이후 (TanStack AI)
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
const messages = [{ role: 'user' as const, content: 'Hello' }]
const stream = chat({
adapter: openaiText('gpt-4o'),
messages,
// Provider-specific options typed for gpt-4o specifically
modelOptions: {
temperature: 0.7,
max_output_tokens: 1000,
top_p: 0.9,
service_tier: 'default',
store: true,
parallel_tool_calls: false,
},
})
modelOptions의 자동 완성은 전달한 정확한 어댑터와 모델을 반영합니다.openaiText('gpt-4o')를anthropicText('claude-sonnet-4-5')로 바꾸면 Anthropic 옵션에 맞게 형태가 변경됩니다.
시스템 메시지
TanStack AI는 systemPrompts 옵션을 통해 최상위 수준에서 시스템 프롬프트를 받습니다. 문자열 배열을 전달하면 각 어댑터가 프로바이더가 요구하는 형식으로 병합합니다. system 메시지를 messages 배열 앞에 직접 추가할 필요가 없습니다.
이전 (Vercel AI SDK)
import { streamText } from 'ai'
import { openai } from '@ai-sdk/openai'
const messages = [{ role: 'user' as const, content: 'Hello' }]
const result = streamText({
model: openai('gpt-4o'),
system: 'You are a helpful assistant.',
messages,
})
이후 (TanStack AI)
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
const messages = [{ role: 'user' as const, content: 'Hello' }]
const stream = chat({
adapter: openaiText('gpt-4o'),
systemPrompts: ['You are a helpful assistant.'],
messages,
})
여러 시스템 프롬프트를 지원하므로 문자열을 이어 붙이지 않고 페르소나, 정책, 도구 사용 지침을 구성할 때 유용합니다.
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
const messages = [{ role: 'user' as const, content: 'Hello' }]
const stream = chat({
adapter: openaiText('gpt-4o'),
systemPrompts: [
'You are a helpful assistant.',
'Respond in concise, plain English.',
'Never fabricate citations.',
],
messages,
})
클라이언트 측 마이그레이션
기본 useChat 훅
이전 (Vercel AI SDK v5+)
import { useChat } from '@ai-sdk/react'
import { DefaultChatTransport } from 'ai'
import { useState } from 'react'
export function Chat() {
const [input, setInput] = useState('')
const { messages, sendMessage, status } = useChat({
transport: new DefaultChatTransport({ api: '/api/chat' }),
})
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (input.trim() && status !== 'streaming') {
sendMessage({ text: input })
setInput('')
}
}
return (
<div>
{messages.map((m) => (
<div key={m.id}>
{m.role}:{' '}
{m.parts.map((p, i) => (p.type === 'text' ? <span key={i}>{p.text}</span> : null))}
</div>
))}
<form onSubmit={handleSubmit}>
<input value={input} onChange={(e) => setInput(e.target.value)} />
<button type="submit">Send</button>
</form>
</div>
)
}
이후 (TanStack AI)
import { useState } from 'react'
import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
export function Chat() {
const [input, setInput] = useState('')
const { messages, sendMessage, isLoading } = useChat({
connection: fetchServerSentEvents('/api/chat'),
})
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (input.trim() && !isLoading) {
sendMessage(input)
setInput('')
}
}
return (
<div>
{messages.map((message) => (
<div key={message.id}>
{message.role}:{' '}
{message.parts.map((part, idx) =>
part.type === 'text' ? <span key={idx}>{part.content}</span> : null
)}
</div>
))}
<form onSubmit={handleSubmit}>
<input value={input} onChange={(e) => setInput(e.target.value)} />
<button type="submit">Send</button>
</form>
</div>
)
}
useChat API 차이점
Vercel AI SDK v5+는 이미 v4의 마법 같은 input/handleInputChange/handleSubmit에서 벗어나 자체 입력 상태를 관리하도록 요구합니다. TanStack AI도 같은 철학을 따릅니다. 훅은 헤드리스이며 폼 연결 코드 대신 구성 요소를 제공합니다.
| Vercel AI SDK (v5+) | TanStack AI | 참고 |
|---|---|---|
transport: new DefaultChatTransport({ api: '/api/chat' }) | connection: fetchServerSentEvents('/api/chat') | 플러그형 연결 어댑터 |
sendMessage({ text }) | sendMessage(text) | 일반 문자열을 받습니다. UIMessage 객체는 append()을 통해 전달합니다. |
status ('submitted' | 'streaming' | 'ready' | 'error') | isLoading (불리언) | TanStack에서는 더 단순합니다. 전체 스트림 상태는 이벤트를 통해 확인할 수 있습니다. |
regenerate() | reload() | 마지막 어시스턴트 턴을 다시 실행합니다. |
stop() | stop() | 진행 중인 스트림을 취소합니다. |
setMessages(messages) | setMessages(messages) | 메시지를 직접 교체합니다. |
addToolOutput({ tool, toolCallId, output }) (v6, v5에서는 addToolResult였음) | addToolResult({ tool, toolCallId, output }) | 클라이언트 측 도구 호출을 해결합니다. |
addToolApprovalResponse({ id, approved }) (v6) | addToolApprovalResponse({ id, approved }) | 도구에 대한 일급 사용자 승인 흐름입니다. |
m.parts (타입이 지정된 유니온) | message.parts (타입이 지정된 유니온) | 둘 다 구조화된 파트를 통해 렌더링합니다. |
메시지 구조
이전 (Vercel AI SDK)
interface Message {
id: string
role: 'user' | 'assistant' | 'system'
content: string
toolInvocations?: ToolInvocation[]
}
이후 (TanStack AI)
다음은 @tanstack/ai-client 형태(useChat이 제공하는 형태)입니다. 핵심 @tanstack/ai 메시지 타입도 유사하지만 input(파싱된 도구 입력)은 클라이언트 계층의 투영입니다. 서버 측 코드는 arguments에서 원시 JSON을 직접 읽습니다.
interface UIMessage<TTools extends ReadonlyArray<AnyClientTool> = any> {
id: string
role: 'system' | 'user' | 'assistant'
parts: Array<MessagePart<TTools>>
createdAt?: Date
}
type MessagePart<TTools> =
| TextPart
| ToolCallPart<TTools>
| ToolResultPart
| ThinkingPart
interface TextPart {
type: 'text'
content: string
}
interface ThinkingPart {
type: 'thinking'
content: string
}
interface ToolCallPart {
type: 'tool-call'
id: string
name: string
arguments: string // Raw JSON string (may be partial while streaming)
input?: unknown // Parsed input (typed when tools are typed)
output?: unknown // Execution output once available
state: ToolCallState
approval?: {
id: string // Approval request ID
needsApproval: boolean
approved?: boolean // undefined until the user responds
}
}
interface ToolResultPart {
type: 'tool-result'
toolCallId: string
content: string
state: ToolResultState
error?: string // Present when state is 'error'
}
type ToolCallState =
| 'awaiting-input'
| 'input-streaming'
| 'input-complete'
| 'approval-requested'
| 'approval-responded'
type ToolResultState = 'streaming' | 'complete' | 'error'
TanStack AI에는 다른 SDK에서 볼 수 있는 별도의
reasoning,source-url,source-document,file파트 타입이 없습니다. 프로바이더별 추론 추적은thinking파트로 전달되며, 인용과 인라인 파일은 텍스트 파트의metadata또는 도구 출력으로 제공됩니다.
메시지 렌더링
이전 (Vercel AI SDK)
{messages.map((m) => (
<div key={m.id}>
{m.role}: {m.content}
{m.toolInvocations?.map((tool) => (
<div key={tool.toolCallId}>
Tool: {tool.toolName} - {JSON.stringify(tool.result)}
</div>
))}
</div>
))}
이후 (TanStack AI)
{messages.map((message) => (
<div key={message.id}>
{message.role}:{' '}
{message.parts.map((part, idx) => {
if (part.type === 'text') {
return <span key={idx}>{part.content}</span>
}
if (part.type === 'thinking') {
return <em key={idx}>Thinking: {part.content}</em>
}
if (part.type === 'tool-call') {
return (
<div key={part.id}>
Tool: {part.name} - {JSON.stringify(part.output)}
</div>
)
}
return null
})}
</div>
))}
도구 / 함수 호출
TanStack AI는 스키마를 한 번 정의한 뒤 서버와 클라이언트에서 별도로 구현하는 동형 도구 시스템을 사용합니다.
기본 도구 정의
이전 (Vercel AI SDK v5+)
import { streamText, tool } from 'ai'
import { openai } from '@ai-sdk/openai'
import { z } from 'zod'
import { fetchWeather } from './weather'
const messages = [{ role: 'user' as const, content: 'Hello' }]
const result = streamText({
model: openai('gpt-4o'),
messages,
tools: {
getWeather: tool({
description: 'Get weather for a location',
inputSchema: z.object({ // renamed from `parameters` in v5
location: z.string(),
}),
execute: async ({ location }) => {
const weather = await fetchWeather(location)
return weather
},
}),
},
})
이후 (TanStack AI)
import { chat, toolDefinition, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { z } from 'zod'
import { fetchWeather } from './weather'
// Step 1: Define the tool schema
const getWeatherDef = toolDefinition({
name: 'getWeather',
description: 'Get weather for a location',
inputSchema: z.object({
location: z.string(),
}),
outputSchema: z.object({
temperature: z.number(),
conditions: z.string(),
}),
})
// Step 2: Create server implementation
const getWeather = getWeatherDef.server(async ({ location }) => {
const weather = await fetchWeather(location)
return weather
})
// Step 3: Use in chat
export async function POST(request: Request) {
const { messages } = await request.json()
const stream = chat({
adapter: openaiText('gpt-4o'),
messages,
tools: [getWeather],
})
return toServerSentEventsResponse(stream)
}
도구 스키마 차이점
| Vercel AI SDK | TanStack AI |
|---|---|
parameters (v4) / inputSchema (v5+) | inputSchema |
| N/A | outputSchema (선택 사항 — 엔드 투 엔드 타입 안전성을 활성화합니다) |
execute 서버에서 인라인으로 | .server() 또는 .client() 메서드 (동형 정의) |
| 도구 이름을 키로 하는 객체 | 도구 인스턴스 배열 |
클라이언트 측 도구
이전 (Vercel AI SDK v5+)
import { DefaultChatTransport } from 'ai'
import { useChat } from '@ai-sdk/react'
const { messages, addToolOutput } = useChat({
transport: new DefaultChatTransport({ api: '/api/chat' }),
onToolCall: async ({ toolCall }) => {
if (toolCall.toolName === 'showNotification') {
// v6: addToolOutput (was addToolResult in v5)
addToolOutput({
tool: 'showNotification',
toolCallId: toolCall.toolCallId,
output: { success: true },
})
}
},
})
이후 (TanStack AI)
import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
import { toolDefinition } from '@tanstack/ai'
import { z } from 'zod'
import { toast } from './toast'
// Define once (can be shared with server)
const showNotificationDef = toolDefinition({
name: 'showNotification',
description: 'Show a toast notification in the browser',
inputSchema: z.object({ message: z.string() }),
outputSchema: z.object({ success: z.boolean() }),
})
// Client implementation
const showNotification = showNotificationDef.client(({ message }) => {
toast(message)
return { success: true }
})
// Use in component — passing the client tools in the `tools` array wires each
// tool's `.client(...)` handler to run automatically when the server-side
// agent calls it; you don't need an onToolCall handler or an
// addToolOutput/addToolResult call.
const { messages } = useChat({
connection: fetchServerSentEvents('/api/chat'),
tools: [showNotification],
})
도구 승인 흐름
두 라이브러리 모두 일급 human-in-the-loop 승인을 제공합니다. 형태는 유사합니다. 도구가 needsApproval: true로 활성화되고, 클라이언트가 approval-requested 상태에서 UI를 렌더링하며, 승인 ID와 함께 addToolApprovalResponse를 호출합니다.
이전 (Vercel AI SDK v6)
// Tool definition (server)
import { tool, DefaultChatTransport, lastAssistantMessageIsCompleteWithApprovalResponses } from 'ai'
import { useChat } from '@ai-sdk/react'
import { z } from 'zod'
import { bookingService } from './booking'
const bookFlight = tool({
description: 'Book a flight',
inputSchema: z.object({ flightId: z.string() }),
needsApproval: true, // v6: first-class approval
execute: async ({ flightId }) => bookingService.book(flightId),
})
// Client
const { messages, addToolApprovalResponse } = useChat({
transport: new DefaultChatTransport({ api: '/api/chat' }),
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
})
이후 (TanStack AI)
// Built-in approval support
const bookFlightDef = toolDefinition({
name: 'bookFlight',
description: 'Book a flight on behalf of the user',
inputSchema: z.object({ flightId: z.string() }),
needsApproval: true, // Request user approval
})
// In component
const { messages, addToolApprovalResponse } = useChat({
connection: fetchServerSentEvents('/api/chat'),
})
// Render approval UI
{message.parts.map((part, idx) => {
if (
part.type === 'tool-call' &&
part.state === 'approval-requested' &&
part.approval
) {
return (
<div key={idx}>
<p>Approve booking flight {part.input?.flightId}?</p>
<button
onClick={() => addToolApprovalResponse({ id: part.approval!.id, approved: true })}
>
Approve
</button>
<button
onClick={() => addToolApprovalResponse({ id: part.approval!.id, approved: false })}
>
Deny
</button>
</div>
)
}
return null
})}
part.input은 파싱된 도구 입력입니다(타입이 지정된tools배열을 전달하면 타입이 지정됩니다). 입력 파싱이 완료되기 전에 진행 상황을 표시해야 한다면 원시 스트리밍 JSON을part.arguments에서 사용할 수 있습니다.
구조화된 출력
이 섹션에서는 generateObject / streamObject / Output.object(...) 마이그레이션 경로를 다룹니다. AI SDK v6에서는 generateText / streamText의 output: 매개변수(예: Output.object({ schema }))를 통해 구조화된 생성을 수행합니다. 전용 generateObject / streamObject 함수는 지원 중단 예정이지만 여전히 존재합니다. TanStack AI도 동일한 "하나의 함수" 철학을 따릅니다. outputSchema를 chat()에 전달하면 전체 에이전트 루프(도구, 재시도, 루프 전략)를 실행하고 타입이 지정되고 검증된 값을 반환합니다.
이전 (Vercel AI SDK v6)
import { generateText, Output } from 'ai'
import { openai } from '@ai-sdk/openai'
import { z } from 'zod'
const { output } = await generateText({
model: openai('gpt-4o'),
prompt: 'Extract the user profile from this bio…',
output: Output.object({
schema: z.object({
name: z.string(),
age: z.number(),
interests: z.array(z.string()),
}),
}),
})
// output is typed as { name: string; age: number; interests: string[] }
이후 (TanStack AI)
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { z } from 'zod'
const profile = await chat({
adapter: openaiText('gpt-4o'),
messages: [{ role: 'user', content: 'Extract the user profile from this bio…' }],
outputSchema: z.object({
name: z.string(),
age: z.number(),
interests: z.array(z.string()),
}),
})
// profile: { name: string; age: number; interests: string[] }
참고
outputSchema는 Standard Schema와 호환되는 모든 라이브러리를 허용합니다. Zod v4.2+, ArkType v2.1.28+, Valibot v1.2+(toStandardJsonSchema()사용) 또는 일반 JSON Schema 객체를 사용할 수 있습니다(이 경우 TS 추론이 손실되고unknown으로 대체됩니다).outputSchema가 설정되면chat()은 항상Promise<T>를 반환합니다. 스키마가 최종 출력을 검증한 뒤에만 값이 의미를 가지므로stream플래그는 무시됩니다.- 어댑터는 각 프로바이더에 가장 적합한 방식으로 구조화된 출력을 구현합니다. OpenAI는
response_format: json_schema, Anthropic은 도구 기반 추출, Gemini는responseSchema, Ollama는 JSON 모드를 사용합니다. 전략을 직접 선택할 필요는 없습니다. - 배열은 단순히
z.array(z.object({ … }))로 표현합니다. TanStack AI는 아직 Vercel 측의streamObject().elementStream처럼 부분 객체를 스트리밍하지 않습니다. 이 기능이 필수라면 당분간streamText를 사용하고, 부분 스트리밍이 추가되면 객체 사례를 마이그레이션하세요.
에이전트 루프 제어
두 SDK 모두 모델이 루프에서 도구를 호출하도록 할 수 있습니다. 제어 옵션의 형태가 다릅니다.
| Vercel AI SDK v6 | TanStack AI |
|---|---|
stopWhen: stepCountIs(5) | agentLoopStrategy: maxIterations(5) |
stopWhen: hasToolCall('bookFlight') | 도구 이름을 위해 사용자 지정 AgentLoopStrategy가 messages를 검사합니다. |
stopWhen: untilFinishReason(['stop']) (사용자 정의 조건) | agentLoopStrategy: untilFinishReason(['stop']) |
stopWhen: [stepCountIs(20), hasToolCall('done')] | agentLoopStrategy: combineStrategies([maxIterations(20), /* your hasToolCall */ ]) |
prepareStep({ stepNumber, messages, steps, model }) | middleware.onConfig(ctx, config) + middleware.onIteration(ctx, info) |
기본 루프 예산: 전략을 전달하지 않으면 TanStack AI는 기본값으로 maxIterations(5)를 사용합니다.
이전 (Vercel AI SDK v6)
import { streamText, stepCountIs } from 'ai'
import { openai } from '@ai-sdk/openai'
import { getWeather } from './tools'
const messages = [{ role: 'user' as const, content: 'What is the weather?' }]
const result = streamText({
model: openai('gpt-4o'),
messages,
tools: { getWeather },
stopWhen: stepCountIs(10),
prepareStep: async ({ stepNumber, messages: stepMessages }) => {
// log/rewrite messages between steps, filter tools, etc.
if (stepNumber > 0) return {}
return {}
},
})
이후 (TanStack AI)
import {
chat,
combineStrategies,
maxIterations,
untilFinishReason,
toServerSentEventsResponse,
type ChatMiddleware,
type ChatMiddlewareContext,
type ChatMiddlewareConfig,
} from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { getWeather } from './tools'
export async function POST(request: Request) {
const { messages } = await request.json()
const stream = chat({
adapter: openaiText('gpt-4o'),
messages,
tools: [getWeather],
agentLoopStrategy: combineStrategies([
maxIterations(10),
untilFinishReason(['stop']), // stop when the model says it's done
]),
middleware: [
{
// `prepareStep` analogue: inspect/rewrite config at the start of each iteration
onConfig: (ctx: ChatMiddlewareContext, config: ChatMiddlewareConfig) => {
if (ctx.iteration > 0) {
// e.g. return a Partial<ChatMiddlewareConfig> to filter tools, trim
// messages, or change modelOptions for this iteration
return undefined
}
},
} satisfies ChatMiddleware,
],
})
return toServerSentEventsResponse(stream)
}
루프 중간 모델 전환
AI SDK v6의 prepareStep에서는 단계마다 다른 model을 반환할 수 있습니다. TanStack AI는 하나의 chat() 실행 중에 어댑터를 교체할 수 없습니다. modelOptions가 어댑터별로 타입 지정되기 때문에 컴파일 시점의 모델 안전성이 보장됩니다. 이에 해당하는 방법은 현재 루프를(agentLoopStrategy를 통해) 종료하고, 진행 중인 메시지를 전달하면서 다른 어댑터로 새 chat()을 시작하는 것입니다.
import { chat, maxIterations } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { getWeather } from './tools'
const messages = [{ role: 'user' as const, content: 'Hello' }]
// Stage 1: heavy model for the opening turn
const firstPass = await chat({
adapter: openaiText('gpt-4o'),
messages,
agentLoopStrategy: maxIterations(1),
stream: false,
})
// Stage 2: cheaper model for the rest
const followUp = chat({
adapter: openaiText('gpt-4o-mini'),
messages: [...messages, { role: 'assistant' as const, content: firstPass }],
tools: [getWeather],
})
미들웨어
AI SDK v6에는 미들웨어와 유사한 확장 지점이 두 가지 있습니다.
wrapLanguageModel({ model, middleware })— 로깅, 캐싱, 가드레일, RAG를 위한 프로바이더 수준 가로채기(transformParams,wrapGenerate,wrapStream)입니다.- **
experimental_transform**을streamText에서 사용합니다. 청크 스트림을 변환합니다.
TanStack AI는 두 지점을 middleware: ChatMiddleware[]라는 chat()의 하나의 일급 옵션으로 통합합니다. 모델 호출이나 청크 스트림에만 연결되는 것이 아니라 전체 수명 주기에 연결되며, 로깅, 추적, 캐싱, 삭제, 도구 가로채기를 수행하는 권장 위치입니다.
이전 (Vercel AI SDK v6)
import { wrapLanguageModel, streamText, type LanguageModelMiddleware } from 'ai'
import { openai } from '@ai-sdk/openai'
const messages = [{ role: 'user' as const, content: 'Hello' }]
const loggingMiddleware = {
specificationVersion: 'v3' as const,
wrapGenerate: async ({ doGenerate, params }) => {
console.log('params', params)
const result = await doGenerate()
console.log('content', result.content)
return result
},
wrapStream: async ({ doStream, params }) => doStream(),
transformParams: async ({ params }) => params,
} satisfies LanguageModelMiddleware
const wrapped = wrapLanguageModel({
model: openai('gpt-4o'),
middleware: [loggingMiddleware],
})
const result = streamText({ model: wrapped, messages })
이후 (TanStack AI)
import { chat, toServerSentEventsResponse, type ChatMiddleware } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
type AppContext = { userId: string }
const loggingMiddleware: ChatMiddleware<AppContext> = {
onStart: (ctx) => console.log('start', { requestId: ctx.requestId, userId: ctx.context.userId }),
onConfig: (ctx, config) => console.log('config', config),
onChunk: (ctx, chunk) => { /* observe or transform; return null to drop */ },
onUsage: (ctx, usage) => console.log('usage', usage),
onFinish: (ctx, info) => console.log('finish', info),
onError: (ctx, err) => console.error('error', err),
}
export async function POST(request: Request) {
const { messages } = await request.json()
const stream = chat({
adapter: openaiText('gpt-4o'),
messages,
middleware: [loggingMiddleware],
context: { userId: 'u_123' }, // passed to every hook as typed ctx.context
})
return toServerSentEventsResponse(stream)
}
전체 훅 목록
각 미들웨어는 일반 객체입니다. 모든 훅은 선택 사항이므로 필요한 것을 선택하면 됩니다.
| 훅 | 호출 시점 | 변환 가능 여부 |
|---|---|---|
onStart(ctx) | 채트 실행이 시작될 때 | — |
onConfig(ctx, config) | 초기화 및 각 모델 호출 전 | Partial<ChatMiddlewareConfig> 를 반환하여 메시지, 시스템 프롬프트, 도구, 온도 등을 수정합니다 |
onIteration(ctx, info) | 각 에이전트 루프 반복 시작 시 | — |
onChunk(ctx, chunk) | StreamChunk 를 생성할 때마다 | null 를 반환하여 버리거나 |
onBeforeToolCall(ctx, hookCtx) | 도구가 실행되기 전에 | BeforeToolCallDecision 를 반환하여 인수를 재작성하거나, 건너뛰거나, 중단합니다 |
onAfterToolCall(ctx, info) | 도구 실행 후 (성공 또는 실패) | — |
onToolPhaseComplete(ctx, info) | 한 번의 반복에서 모든 도구가 완료됨 | — |
onUsage(ctx, usage) | 공급자가 토큰 사용량을 보고합니다 | — |
onFinish(ctx, info) | 실행이 정상적으로 종료됨 (터미널) | — |
onAbort(ctx, info) | 실행이 중단됨 (터미널) | — |
onError(ctx, info) | 처리되지 않은 오류 발생 (터미널) | — |
ctx에는 requestId, streamId, threadId, iteration, model, provider, systemPrompts, toolNames, messages, context(타입이 지정된 런타임 값), abort(reason), defer(promise), createId(prefix) 등이 포함됩니다. 전체 참조는 미들웨어 가이드와 런타임 컨텍스트 가이드를 참조하세요.
기본 제공: 도구 호출 캐시
TanStack AI는 toolCacheMiddleware를 제공하며, name + args를 기준으로 도구 결과를 메모이제이션합니다. Vercel에는 직접 대응하는 기능이 없으므로 Vercel 측에서는 wrapGenerate 또는 각 도구의 execute 내부에서 직접 구성해야 합니다. 예시는 다음과 같습니다.
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import { toolCacheMiddleware } from '@tanstack/ai/middlewares'
import { openaiText } from '@tanstack/ai-openai'
import { searchDocs, getWeather } from './tools'
export async function POST(request: Request) {
const { messages } = await request.json()
const stream = chat({
adapter: openaiText('gpt-4o'),
messages,
tools: [searchDocs, getWeather],
middleware: [
toolCacheMiddleware({
maxSize: 100,
ttl: 5 * 60_000, // 5 minutes
toolNames: ['searchDocs'], // only cache these
// storage: redisStorage, // plug in Redis / localStorage / custom
}),
],
})
return toServerSentEventsResponse(stream)
}
일반적인 Vercel 패턴과 TanStack 미들웨어의 매핑
| Vercel 패턴 | TanStack 미들웨어 훅 |
|---|---|
experimental_transform (채널 변환) | onChunk |
experimental_repairToolCall | onBeforeToolCall |
prepareStep (동적 모델/도구/메시지) | onConfig + onIteration |
wrapLanguageModel logging | onStart + onConfig + onFinish |
wrapGenerate 내의 사용자 정의 캐싱 | toolCacheMiddleware 또는 자체 onBeforeToolCall/onAfterToolCall |
experimental_telemetry | 임의 터미널 훅 (onFinish/onAbort/onError) + 추적기 |
관측 가능성(로깅, 메트릭, 트레이싱)
두 라이브러리 모두 전송 계층을 원하는 트레이서(OpenTelemetry, Sentry, Datadog, …)에 맡깁니다. 연결 지점은 다릅니다.
- Vercel AI SDK:
streamText의experimental_telemetry와 수명 주기 콜백(onChunk,onStepFinish,onFinish,onError)입니다. - TanStack AI: 필요한 훅을 포함한 미들웨어입니다.
onStart+onFinish/onAbort/onError는 요청 수준 스팬을 처리하고,onChunk+onUsage는 세밀한 타이밍을 제공하며,onBeforeToolCall/onAfterToolCall은 도구 스팬에 사용합니다.
ctx.requestId와 ctx.streamId는 모든 훅에서 안정적이므로 ID를 직접 전달하지 않아도 요청마다 하나의 트레이스를 얻을 수 있습니다.
프로바이더 어댑터
TanStack AI는 최적의 트리 셰이킹을 위해 활동별 어댑터를 사용합니다.
OpenAI
이전 (Vercel AI SDK)
import { openai } from '@ai-sdk/openai'
// Chat
streamText({ model: openai('gpt-4o'), ... })
// Embeddings
embed({ model: openai.embedding('text-embedding-3-small'), ... })
// Image generation
generateImage({ model: openai.image('dall-e-3'), ... })
이후 (TanStack AI)
import {
openaiText,
openaiImage,
openaiSpeech,
openaiEmbedding,
} from '@tanstack/ai-openai'
// Chat
chat({ adapter: openaiText('gpt-4o'), ... })
// Image generation
generateImage({ adapter: openaiImage('dall-e-3'), ... })
// Text to speech
generateSpeech({ adapter: openaiSpeech('tts-1'), ... })
// Embeddings
embed({ adapter: openaiEmbedding('text-embedding-3-small'), ... })
Anthropic
이전 (Vercel AI SDK)
import { anthropic } from '@ai-sdk/anthropic'
streamText({ model: anthropic('claude-sonnet-4-5-20250514'), ... })
이후 (TanStack AI)
import { anthropicText } from '@tanstack/ai-anthropic'
chat({ adapter: anthropicText('claude-sonnet-4-5-20250514'), ... })
Google (Gemini)
이전 (Vercel AI SDK)
import { google } from '@ai-sdk/google'
streamText({ model: google('gemini-1.5-pro'), ... })
이후 (TanStack AI)
import { geminiText } from '@tanstack/ai-gemini'
chat({ adapter: geminiText('gemini-2.5-flash'), ... })
스트리밍 응답
서버 응답 형식
이전 (Vercel AI SDK v5+)
// UI message stream (default, for useChat)
return result.toUIMessageStreamResponse()
// Plain text stream
return result.toTextStreamResponse()
이후 (TanStack AI)
import {
chat,
toServerSentEventsResponse,
toServerSentEventsStream,
toHttpResponse,
toHttpStream,
} from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
const abortController = new AbortController()
const traceId = 'trace-123'
const messages = [{ role: 'user' as const, content: 'Hello' }]
async function handler() {
const stream = chat({ adapter: openaiText('gpt-4o'), messages })
// SSE response (recommended; pairs with fetchServerSentEvents on the client).
// Both response helpers accept a ResponseInit with an optional abortController
// — merge custom headers, status, or cancellation without unwrapping the helper.
return toServerSentEventsResponse(stream, {
abortController,
status: 200,
headers: { 'X-Trace-Id': traceId },
})
}
async function handler2() {
const stream = chat({ adapter: openaiText('gpt-4o'), messages })
// Newline-delimited JSON response (pairs with fetchHttpStream on the client).
return toHttpResponse(stream, { abortController })
}
// Or grab the raw ReadableStream if you need to pipe it somewhere else
// (writing to a Node ServerResponse, wrapping in a transform, etc.).
const stream2 = chat({ adapter: openaiText('gpt-4o'), messages })
const sseStream = toServerSentEventsStream(stream2, abortController)
const ndjsonStream = toHttpStream(stream2, abortController)
클라이언트 연결 어댑터
이전 (Vercel AI SDK v5+)
import { DefaultChatTransport } from 'ai'
import { useChat } from '@ai-sdk/react'
useChat({
transport: new DefaultChatTransport({ api: '/api/chat' }),
})
이후 (TanStack AI)
import { useChat, fetchServerSentEvents, fetchHttpStream, stream } from '@tanstack/ai-react'
import { customServerFn } from './server'
// SSE (matches toServerSentEventsResponse)
useChat({ connection: fetchServerSentEvents('/api/chat') })
// HTTP stream (matches toHttpResponse / toHttpStream on the server)
useChat({ connection: fetchHttpStream('/api/chat') })
// Custom adapter: return an AsyncIterable<StreamChunk>, e.g. from a TanStack
// Start server function or an RPC client
useChat({
connection: stream((messages, data) => customServerFn({ messages, data })),
})
AbortController / 취소
이전 (Vercel AI SDK)
const result = streamText({
model: openai('gpt-4o'),
messages,
abortSignal: controller.signal,
})
이후 (TanStack AI)
TanStack AI는 단순 signal이 아니라 AbortController를 받으므로 toServerSentEventsStream과 같은 헬퍼가 응답 스트림에 취소를 연결해 줍니다.
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
const messages = [{ role: 'user' as const, content: 'Hello' }]
const abortController = new AbortController()
const stream = chat({
adapter: openaiText('gpt-4o'),
messages,
abortController,
})
// Cancel the stream
abortController.abort()
콜백과 이벤트
스트림 콜백
이전 (Vercel AI SDK v5+)
import { DefaultChatTransport } from 'ai'
import { useChat } from '@ai-sdk/react'
const { messages } = useChat({
transport: new DefaultChatTransport({ api: '/api/chat' }),
onFinish: ({ message }) => console.log('Finished:', message),
onError: (error) => console.error('Error:', error),
})
이후 (TanStack AI)
import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
const { messages } = useChat({
connection: fetchServerSentEvents('/api/chat'),
onResponse: (response) => console.log('Response started'),
onChunk: (chunk) => console.log('Chunk received:', chunk),
onFinish: (message) => console.log('Finished:', message),
onError: (error) => console.error('Error:', error),
})
TanStack AI에서는 chat()이 반환하는 비동기 이터러블을 구독해 서버 측 스트림 수명 주기에도 연결할 수 있습니다. 이 방식은 타입이 지정된 전체 StreamChunk 유니온을 보존하므로 로깅, 분석 또는 응답과 함께 사용자 지정 SSE 이벤트를 전송할 때 유용합니다.
멀티모달 콘텐츠
이미지 입력
이전 (Vercel AI SDK)
import { streamText } from 'ai'
import { openai } from '@ai-sdk/openai'
import { imageUrl } from './images'
streamText({
model: openai('gpt-4o'),
messages: [
{
role: 'user',
content: [
{ type: 'text', text: 'Describe this image' },
{ type: 'image', image: imageUrl },
],
},
],
})
이후 (TanStack AI)
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { imageUrl, imageData } from './images'
chat({
adapter: openaiText('gpt-4o'),
messages: [
{
role: 'user',
content: [
{ type: 'text', content: 'Describe this image' },
{ type: 'image', source: { type: 'url', value: imageUrl } },
// Or inline base64 data
{ type: 'image', source: { type: 'data', value: imageData, mimeType: 'image/png' } },
],
},
],
})
소스 판별자는
'url'또는'data'입니다. 둘 다value에 페이로드(URL 또는 base64 문자열)를 담습니다.mimeType은'data'에 필수이고'url'에서는 선택 사항입니다.
동적 프로바이더 전환
이전 (Vercel AI SDK)
import { streamText } from 'ai'
import { openai } from '@ai-sdk/openai'
import { anthropic } from '@ai-sdk/anthropic'
const messages = [{ role: 'user' as const, content: 'Hello' }]
const providers = {
openai: openai('gpt-4o'),
anthropic: anthropic('claude-sonnet-4-5-20250514'),
}
const selectedProvider: keyof typeof providers = 'openai'
streamText({
model: providers[selectedProvider],
messages,
})
이후 (TanStack AI)
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { anthropicText } from '@tanstack/ai-anthropic'
const messages = [{ role: 'user' as const, content: 'Hello' }]
const adapters = {
openai: () => openaiText('gpt-4o'),
anthropic: () => anthropicText('claude-sonnet-4-5'),
} as const
const selectedProvider: keyof typeof adapters = 'openai'
chat({
adapter: adapters[selectedProvider](),
messages,
})
타입 안전성 향상
TanStack AI는 Vercel AI SDK에 없는 향상된 타입 안전성을 제공합니다.
타입이 지정된 메시지 파트
import { createChatClientOptions, type InferChatMessages } from '@tanstack/ai-client'
import { fetchServerSentEvents } from '@tanstack/ai-react'
import { updateUI, saveData } from './tools'
const tools = [updateUI, saveData]
const chatOptions = createChatClientOptions({
connection: fetchServerSentEvents('/api/chat'),
tools,
})
// Infer fully typed messages
type ChatMessages = InferChatMessages<typeof chatOptions>
// Now TypeScript knows:
// - Exact tool names available
// - Input types for each tool
// - Output types for each tool
모델별 타입 안전성
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
const messages = [{ role: 'user' as const, content: 'Hello' }]
const adapter = openaiText('gpt-4o')
chat({
adapter,
messages,
modelOptions: {
// TypeScript autocompletes options specific to gpt-4o
service_tier: 'default',
store: true,
},
})
비스트리밍 생성(generateText)
TanStack AI는 별도의 generateText 함수를 제공하지 않습니다. 동일한 chat()이 두 모드를 모두 처리합니다. stream: false를 전달하면 반환 타입이 AsyncIterable<StreamChunk>에서 Promise<string>으로 바뀝니다.
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
const text = await chat({
adapter: openaiText('gpt-4o'),
messages: [{ role: 'user', content: 'Summarize TanStack AI in one sentence.' }],
stream: false,
})
// text: string
다른 이유로 이미 스트림을 보유하고 있다면 streamToText(stream)으로 스트림을 문자열로 수집할 수 있습니다.
import { chat, streamToText } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
const messages = [{ role: 'user' as const, content: 'Hello' }]
const stream = chat({ adapter: openaiText('gpt-4o'), messages })
const text = await streamToText(stream)
구조화된(비스트리밍) 출력, 즉 generateObject에 해당하는 기능을 사용하려면 대신 outputSchema를 전달하세요. 구조화된 출력을 참조하세요.
임베딩
Vercel의 embed와 embedMany는 모두 TanStack AI의 단일 embed() 함수에 해당합니다. input은 하나의 항목 또는 배열을 허용하며, 결과에는 항상 입력 항목마다 하나의 벡터가 포함됩니다.
이전 (Vercel AI SDK)
import { embed, embedMany } from 'ai'
import { openai } from '@ai-sdk/openai'
const { embedding } = await embed({
model: openai.embedding('text-embedding-3-small'),
value: 'Hello, world!',
})
const { embeddings } = await embedMany({
model: openai.embedding('text-embedding-3-small'),
values: ['one', 'two'],
})
이후 (TanStack AI)
import { embed } from '@tanstack/ai'
import { openaiEmbedding } from '@tanstack/ai-openai'
const single = await embed({
adapter: openaiEmbedding('text-embedding-3-small'),
input: 'Hello, world!',
})
const vector = single.embeddings[0]?.vector
const batch = await embed({
adapter: openaiEmbedding('text-embedding-3-small'),
input: ['one', 'two'],
})
const vectors = batch.embeddings.map((e) => e.vector)
TanStack AI의 embed()는 Cohere embed-v4.0 및 Amazon Titan Multimodal과 같은 모델에서 멀티모달(텍스트 + 이미지) 입력도 지원합니다. 임베딩 가이드를 참조하세요.
아직 지원되지 않는 기능
현재 일부 AI SDK 기능에는 TanStack AI의 직접적인 대응 기능이 없습니다.
부분 객체 스트리밍 (streamObject().elementStream / partialObjectStream)
TanStack AI의 outputSchema는 전체 응답이 검증되면 항상 Promise<T>를 반환합니다. 스트리밍 중 부분 JSON을 렌더링해야 한다면 해당 사례에서는 streamText + onChunk를 계속 사용하세요(또는 자체 증분 JSON 파서로 TanStack의 원시 스트림을 파싱하세요).
내장 재시도 및 시간 제한
Vercel의 maxRetries / timeout 옵션에는 직접 대응하는 chat() 기능이 없습니다. AbortSignal.timeout(ms)을 abortController를 통해 사용하고, 사용자 지정 미들웨어 또는 fetch 계층에 재시도를 추가하세요.
전체 마이그레이션 예제
Vercel 측에서는 v5+ 서버 핸들러가 수신한 UI 메시지를
convertToModelMessages(messages)로 처리한 후streamText에 전달합니다. TanStack AI의chat()은 UI 메시지 형태를 직접 허용하고 어댑터가 내부에서 변환하므로, After 측에서는 이에 해당하는 줄이 사라집니다.
이전 (Vercel AI SDK v5+)
// server/api/chat.ts
import { streamText, tool, convertToModelMessages, DefaultChatTransport } from 'ai'
import { openai } from '@ai-sdk/openai'
import { useChat } from '@ai-sdk/react'
import { z } from 'zod'
import { fetchWeather } from './weather'
import { useState } from 'react'
export async function POST(request: Request) {
const { messages } = await request.json()
const result = streamText({
model: openai('gpt-4o'),
system: 'You are a helpful assistant.',
messages: await convertToModelMessages(messages),
temperature: 0.7,
tools: {
getWeather: tool({
description: 'Get weather',
inputSchema: z.object({ city: z.string() }),
execute: async ({ city }) => fetchWeather(city),
}),
},
})
return result.toUIMessageStreamResponse()
}
// components/Chat.tsx
export function Chat() {
const [input, setInput] = useState('')
const { messages, sendMessage, status } = useChat({
transport: new DefaultChatTransport({ api: '/api/chat' }),
})
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (input.trim() && status !== 'streaming') {
sendMessage({ text: input })
setInput('')
}
}
return (
<div>
{messages.map((m) => (
<div key={m.id}>
{m.parts.map((p, i) => (p.type === 'text' ? <span key={i}>{p.text}</span> : null))}
</div>
))}
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
disabled={status === 'streaming'}
/>
<button type="submit">Send</button>
</form>
</div>
)
}
이후 (TanStack AI)
// server/api/chat.ts
import { chat, toServerSentEventsResponse, toolDefinition } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { z } from 'zod'
import { fetchWeather } from './weather'
const getWeatherDef = toolDefinition({
name: 'getWeather',
description: 'Get weather',
inputSchema: z.object({ city: z.string() }),
outputSchema: z.object({ temp: z.number(), conditions: z.string() }),
})
const getWeather = getWeatherDef.server(async ({ city }) => fetchWeather(city))
export async function POST(request: Request) {
const { messages } = await request.json()
const stream = chat({
adapter: openaiText('gpt-4o'),
systemPrompts: ['You are a helpful assistant.'],
messages,
temperature: 0.7,
tools: [getWeather],
})
return toServerSentEventsResponse(stream)
}
// components/Chat.tsx
import { useState } from 'react'
import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
export function Chat() {
const [input, setInput] = useState('')
const { messages, sendMessage, isLoading } = useChat({
connection: fetchServerSentEvents('/api/chat'),
})
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (input.trim() && !isLoading) {
sendMessage(input)
setInput('')
}
}
return (
<div>
{messages.map((message) => (
<div key={message.id}>
{message.parts.map((part, idx) =>
part.type === 'text' ? <span key={idx}>{part.content}</span> : null
)}
</div>
))}
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
disabled={isLoading}
/>
<button type="submit">Send</button>
</form>
</div>
)
}
도움이 필요하신가요?
여기에서 다루지 않은 내용을 만났다면 이 가이드가 끝나는 지점부터 심화 문서에서 이어서 설명합니다.