1.0 removes the APIs that 0.x marked as deprecated. Each replacement already shipped in 0.x. Apply this page, then change the version.
npx jscodeshift \
--parser=tsx \
-t https://raw.githubusercontent.com/TanStack/ai/main/codemods/ag-ui-compliance/transform.ts \
"src/**/*.{ts,tsx}"To see the changes before the codemod writes them, add --dry --print.
The codemod does these renames:
| Before | After |
|---|---|
| useChat({ body }) | useChat({ forwardedProps }) |
| new ChatClient({ body }) | new ChatClient({ forwardedProps }) |
| client.updateOptions({ body }) | client.updateOptions({ forwardedProps }) |
| Svelte chat.updateBody(x) | chat.updateForwardedProps(x) |
| chat({ conversationId: x }) | chat({ threadId: x }) |
The codemod does not do these changes. Do them by hand:
The codemod changes a file only when the file imports the hook or class from a TanStack AI package. A call through a re-export (a barrel file) does not change.
This section is for code that uses useChat, ChatClient, a chat UI factory, or any other client package.
The chat-level body option is gone from these places:
Use forwardedProps:
useChat({
connection: fetchServerSentEvents('/api/chat'),
- body: { provider: 'openai' },
+ forwardedProps: { provider: 'openai' },
})On Svelte, updateBody is now updateForwardedProps.
These options keep the name body, because each one is a different option:
Read interrupts. Resolve each item with its own methods:
- const { pendingInterrupts, resumeInterrupts } = useChat(chatOptions)
+ const { interrupts } = useChat(chatOptions)
- const approval = pendingInterrupts.find((item) => item.kind === 'tool-approval')
- await resumeInterrupts(resumeItems)
+ const approval = interrupts.find((item) => item.kind === 'tool-approval')
+ if (approval?.kind === 'tool-approval') approval.resolveInterrupt(true)
- interrupt.error?.message
+ interrupt.errors[0]?.message| Removed | Use instead |
|---|---|
| pendingInterrupts | interrupts |
| ChatClient.getPendingInterrupts() | ChatClient.getInterrupts() |
| resumeInterrupts(items) | interrupt.resolveInterrupt(...), interrupt.cancel(), or resolveInterrupts(...). For raw recovery tools, resumeInterruptsUnsafe(items). |
| interrupt.error | interrupt.errors[0] |
| addToolApprovalResponse() (all frameworks and ChatClient) | resolveInterrupt on the bound tool-approval interrupt |
An interrupt from a 0.x server that has no binding now shows as kind: 'unbound'. You cannot resume it. The error codes legacy-unsupported and legacy-submit-failed are gone. For a full approval screen, see Tool Approval.
A client tool resumes only when the tool on the server and the .client() tool in the browser have the same schemas, outputSchema included. If the schemas are different, the client does not run the tool. The pause shows as a generic interrupt, and it waits until something resolves it. addToolResult with the same toolCallId resolves that generic interrupt.
To keep the schemas the same, define the tool one time with toolDefinition(), and share that definition between the server and the client:
// tools/show-notification.ts: import this file on the server and in the browser
export 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() }),
})
// Browser
const showNotification = showNotificationDef.client(({ message }) => {
toast(message)
return { success: true }
})See Client Tools.
These ready-made components are gone from @tanstack/ai-react/ui, @tanstack/ai-solid/ui, @tanstack/ai-vue/ui, and @tanstack/ai-remix/ui:
Their prop types are gone too. Build the UI with createChatHook. You supply each component, and the factory types them from your chat options:
- import { Chat, ChatMessages, ChatInput } from '@tanstack/ai-react-ui'
+ import { createChatHook } from '@tanstack/ai-react/ui'
+ const { useAppChat } = createChatHook({
+ options: chatOptions,
+ components: {
+ layout: ({ Messages, Input }) => (
+ <main>
+ <Messages />
+ <Input />
+ </main>
+ ),
+ message: ({ message, Parts }) => <article data-role={message.role}><Parts /></article>,
+ input: ChatForm,
+ },
+ partsComponents: { text: ({ part }) => <p>{part.content}</p>, fallback: () => null },
+ })
- export function ChatScreen() {
+ export function ChatScreen({ threadId }: { threadId: string }) {
- return (
- <Chat connection={connection}>
- <ChatMessages />
- <ChatInput />
- </Chat>
- )
+ const chat = useAppChat({ threadId })
+ return <chat.AppChat />
}TextPart and ThinkingPart stay. Import them from the same /ui subpath.
For a complete, runnable version, start with A chat box with no tools. Then open the guide for your framework: React, Solid, Vue, or Remix. If you build your own framework adapter, use createChatUI. See Custom adapters.
threadId is now required. The client does not make one for you. This applies to:
- useChat({ connection })
+ useChat({ connection, threadId: chatId })Use a stable id for each conversation. For example, use the id from your route or your database. Do not make a new random id on each render: a new id starts a new conversation.
The codemod cannot add threadId, because it does not know your ids.
If you call a connection adapter yourself, pass a run context with a threadId as the fourth argument of connect() or send(). The types require it, and the call fails without it.
| Removed | Use instead |
|---|---|
| RealtimeAdapter, RealtimeConnection from @tanstack/ai-client | Import them from @tanstack/ai |
| useWebMCPTools (React, Preact, Solid, Vue, Octane) | useRegisterWebMCPTools |
| injectWebMCPTools (Angular) | injectRegisterWebMCPTools |
| createWebMCPTools (Svelte, Remix) | createRegisterWebMCPTools |
| Client persistence getItem that returns UIMessage[] | Return { messages } (see Saved data) |
Each WebMCP options type follows its function. For example, UseWebMCPToolsOptions is now UseRegisterWebMCPToolsOptions.
This section is for these parts of your app:
const params = await chatParamsFromRequest(request)
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages: params.messages,
- conversationId: params.forwardedProps.conversationId,
+ threadId: params.threadId,
})The client sends threadId as a top-level field, not in forwardedProps. After the codemod, change the value to params.threadId. Do not remove the option: threadId is now required. See chat() and the generation functions need a threadId.
Middleware reads ctx.threadId. ctx.conversationId is gone.
threadId is now required on chat(), summarize(), and the generation functions, for example generateImage(). The server does not make one for you.
- chat({ adapter, messages: params.messages })
+ chat({ adapter, messages: params.messages, threadId: params.threadId })These also need a threadId now:
The client sends the extra fields only as forwardedProps. The data copy is gone.
const params = await chatParamsFromRequest(request)
- const provider = params.forwardedProps.provider ?? body.data?.provider
+ const provider = params.forwardedProps.providerchatParamsFromRequestBody(body).context is now aguiContext.
The generation hooks (for example useGenerateImage) do the same. They send the input only in forwardedProps of the request body. If a custom generation route reads body.data, change it to read body.forwardedProps:
const body = await request.json()
- const { prompt } = body.data
+ const { prompt } = body.forwardedPropsIf your server does not use chat() and toServerSentEventsResponse(), make these changes:
The TanStack AI adapters no longer send STEP_STARTED or STEP_FINISHED for reasoning. If your middleware or devtools read those events for thinking, read the REASONING_* events.
getVideo() is now required, and getVideoUrl() is gone from VideoAdapter and BaseVideoAdapter:
class MyVideoAdapter extends BaseVideoAdapter {
- async getVideoUrl(jobId: string) {
+ async getVideo(jobId: string) {
return { jobId, url: await this.fetchUrl(jobId) }
}
}In app code, call getVideoJobStatus({ adapter, jobId }), not adapter.getVideoUrl(jobId). See Video Generation.
TokenUsage.durationSeconds and TokenUsage.unitsBilled are gone. Read billed. It holds the quantity and its unit:
- const seconds = result.usage?.durationSeconds
+ const billed = result.usage?.billed
+ const seconds = billed?.unit === 'seconds' ? billed.quantity : undefinedThe OpenTelemetry attributes follow the same change:
See OpenTelemetry.
In 1.0, usage.promptTokens is the full input on every adapter: the uncached tokens, plus the cache reads, plus the cache writes. totalTokens uses this value.
Older 0.x releases of these adapters counted only the uncached tokens in promptTokens:
If your code adds the cache counts to promptTokens, remove that addition. If you keep it, you count the cache two times:
const details = usage.promptTokensDetails
- const input = usage.promptTokens + (details?.cachedTokens ?? 0) + (details?.cacheWriteTokens ?? 0)
+ const input = usage.promptTokensTo get only the uncached tokens, subtract cachedTokens and cacheWriteTokens from promptTokens. See Token usage.
| Removed | Use instead |
|---|---|
| AGUIEventType, StreamChunkType | The EventType enum, or `${EventType}` for the string union |
| UsageTotals, ImageUsage | TokenUsage |
| OtelSpanKind | OtelSpanScope (from @tanstack/ai/middlewares/otel) |
| StreamProcessor.startAssistantMessage() | StreamProcessor.prepareAssistantMessage() |
| ApprovalRequestedEvent, ToolInputAvailableEvent | The interrupt outcome on RUN_FINISHED |
| toolName on TOOL_CALL_START chunks | toolCallName |
| resolveDurableThreadId, DurableThreadIdRequiredError (from @tanstack/ai-sandbox) | Nothing. threadId is always set, so pass the run record's threadId to chat() |
prepareAssistantMessage() returns nothing. The processor makes the assistant message when the first content arrives.
In @tanstack/ai-event-client, CompactionAppliedEvent and the 'compaction:applied' event are now CompactionStateEvent and 'compaction:state'.
Each provider tool type alias is gone. Use the Config name. For example, WebSearchTool is now WebSearchToolConfig:
| Package | Removed aliases |
|---|---|
| @tanstack/ai-anthropic | BashTool, CodeExecutionTool, ComputerUseTool, CustomTool, MemoryTool, TextEditorTool, WebFetchTool, WebSearchTool |
| @tanstack/ai-gemini | CodeExecutionTool, ComputerUseTool, FileSearchTool, GoogleMapsTool, GoogleSearchRetrievalTool, GoogleSearchTool, UrlContextTool |
| @tanstack/ai-openai, @tanstack/openai-base | ApplyPatchTool, CodeInterpreterTool, ComputerUseTool, CustomTool, FileSearchTool, FunctionTool, ImageGenerationTool, LocalShellTool, MCPTool, ShellTool, WebSearchPreviewTool, WebSearchTool |
| @tanstack/ai-openrouter | WebSearchTool |
Other provider changes:
| Provider | Change |
|---|---|
| Anthropic skills | Move modelOptions.container.skills to the code execution tool: codeExecutionTool(config, { skills }). |
| Anthropic media type | Delete mediaType from image and document metadata. The adapter reads the type from source.mimeType. The types AnthropicImageMediaType and AnthropicDocumentMediaType are gone. |
| Gemini image models | Google shut down gemini-3.1-flash-image-preview, gemini-3-pro-image-preview, and gemini-2.5-flash-image. Use gemini-3.1-flash-image, gemini-3-pro-image, or gemini-3.1-flash-lite-image. The type Gemini25FlashImageSize is gone. |
| Grok voice | grok-voice-think-fast-1.0 is now grok-voice-think-fast-2.0. |
1.0 does not read some formats that 0.x saved. Do these steps before you deploy:
getItem: (id) => {
const saved = JSON.parse(localStorage.getItem(id) ?? 'null')
- return saved
+ return Array.isArray(saved) ? { messages: saved } : saved
},Two sandbox renames go with these steps:
See Client Persistence and Keep generated files.
You are on 1.0 when these three things are true:
Your type checker passes.
A search for the removed names finds nothing:
grep -rnE "pendingInterrupts|resumeInterrupts\(|addToolApprovalResponse|conversationId|getVideoUrl|durationSeconds|unitsBilled|body\.data" srcA chat message streams a reply, and a saved chat loads after a page reload.
Do you come from an early 0.x release, with openai() adapters or providerOptions? Read the Migration Guide first. For the wire format, read AG-UI Client Compliance. For interrupts in depth, read Migrate to AG-UI interrupts.