Skip to content
Migration

Upgrade to 1.0

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.

Upgrade in 5 steps

  1. Run the codemod. It does the most common renames for you.
  2. Update every @tanstack/ai* package to 1.0 at the same time. A 1.0 client needs a 1.0 server. If you use @tanstack/ai-react-ui, @tanstack/ai-solid-ui, or @tanstack/ai-vue-ui, remove it. These packages have no 1.0 release.
  3. Run your type checker. Fix each error with the sections In the browser and On the server.
  4. If you keep data from 0.x (chats, runs, files), do the steps in Saved data before you deploy.
  5. Do the done check.

Run the codemod

shell
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:

BeforeAfter
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.

In the browser

This section is for code that uses useChat, ChatClient, a chat UI factory, or any other client package.

body is now forwardedProps

The chat-level body option is gone from these places:

  • ChatClient and updateOptions.
  • Every chat hook, and Angular injectChat.
  • createChatHook and useAppChat overrides.

Use forwardedProps:

diff
  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:

  • The connection adapter option, for example fetchServerSentEvents(url, { body }).
  • The per-call option, sendMessage(content, { body }).
  • The generation hooks, for example useGenerateImage({ body }) and its updateBody.

Interrupts

Read interrupts. Resolve each item with its own methods:

diff
- 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
RemovedUse instead
pendingInterruptsinterrupts
ChatClient.getPendingInterrupts()ChatClient.getInterrupts()
resumeInterrupts(items)interrupt.resolveInterrupt(...), interrupt.cancel(), or resolveInterrupts(...). For raw recovery tools, resumeInterruptsUnsafe(items).
interrupt.errorinterrupt.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.

Client tools

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:

ts
// 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.

Chat UI components

These ready-made components are gone from @tanstack/ai-react/ui, @tanstack/ai-solid/ui, @tanstack/ai-vue/ui, and @tanstack/ai-remix/ui:

  • Chat, ChatMessages, ChatMessage, and ChatInput.
  • ToolApproval.
  • The old useChatContext export.

Their prop types are gone too. Build the UI with createChatHook. You supply each component, and the factory types them from your chat options:

diff
- 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.

Pass a threadId to every chat and generation hook

threadId is now required. The client does not make one for you. This applies to:

  • useChat and the other chat hooks, and Angular injectChat.
  • The hook that createChatHook returns. The factory options no longer take threadId. Pass it on each call, for example useAppChat({ threadId }).
  • ChatClient.
  • The generation hooks, for example useGenerateImage and useGeneration. On Octane, the generation hooks take threadId instead of id. A generation fetcher gets the threadId and runId in its second argument: fetcher: (input, { threadId, signal }) => ....
diff
- 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.

Other client renames

RemovedUse instead
RealtimeAdapter, RealtimeConnection from @tanstack/ai-clientImport 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.

On the server

This section is for these parts of your app:

  • Your chat endpoint.
  • Middleware and custom adapters.
  • A server that is not TanStack AI, but streams to a TanStack AI client.

conversationId is now threadId

diff
  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.

chat() and the generation functions need a threadId

threadId is now required on chat(), summarize(), and the generation functions, for example generateImage(). The server does not make one for you.

diff
- chat({ adapter, messages: params.messages })
+ chat({ adapter, messages: params.messages, threadId: params.threadId })
  • In a chat route, use params.threadId from chatParamsFromRequest().
  • In a generation route, use threadId and runId from generationParamsFromRequest(). It throws when the body has no threadId or runId, so a body that holds only the input, with no AG-UI envelope, is rejected. The generation hooks always send the envelope.
  • In a subagent run function, ctx.chat() and the other ctx functions fill threadId from the child run. A plain chat() call in run needs threadId: ctx.threadId.
  • createChatOptions() does not need a threadId. Pass it on the chat() call that uses the options.

These also need a threadId now:

  • codeModeWithSnippets() and selectRelevantSnippets() from @tanstack/ai-code-mode-snippets.
  • CompactionContext.threadId and SummarizeInput.threadId in @tanstack/ai-compaction. This is the conversation id. Give a summary model call its own id, for example ${threadId}:compaction.
  • createToolBridgeCore(), startHostToolBridge(), a custom ToolBridgeProvisioner, and executeHostTool() from @tanstack/ai-sandbox. The bridged tools get this threadId.

Read forwardedProps, not data

The client sends the extra fields only as forwardedProps. The data copy is gone.

diff
  const params = await chatParamsFromRequest(request)
- const provider = params.forwardedProps.provider ?? body.data?.provider
+ const provider = params.forwardedProps.provider

chatParamsFromRequestBody(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:

diff
  const body = await request.json()
- const { prompt } = body.data
+ const { prompt } = body.forwardedProps

Custom servers that stream to a TanStack AI client

If your server does not use chat() and toServerSentEventsResponse(), make these changes:

  • Stop sending data: [DONE]. End the run with a RUN_FINISHED event. The client cannot parse a [DONE] line, and it reports an error.
  • Set message on RUN_ERROR. The client reads only the top-level message and code. It ignores a nested error.message.
  • Send thinking as REASONING_* events. The client does not read STEP_STARTED or STEP_FINISHED for thinking text.
  • Send approvals and client tools as interrupts. End the run with RUN_FINISHED and an interrupt outcome. The client does not read the CUSTOM events approval-requested and tool-input-available. See Interrupts.

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.

Video adapters

getVideo() is now required, and getVideoUrl() is gone from VideoAdapter and BaseVideoAdapter:

diff
  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.

Usage numbers

TokenUsage.durationSeconds and TokenUsage.unitsBilled are gone. Read billed. It holds the quantity and its unit:

diff
- const seconds = result.usage?.durationSeconds
+ const billed = result.usage?.billed
+ const seconds = billed?.unit === 'seconds' ? billed.quantity : undefined

The OpenTelemetry attributes follow the same change:

  • tanstack.ai.usage.duration_seconds and tanstack.ai.usage.units_billed are gone.
  • Read tanstack.ai.usage.billed_quantity and tanstack.ai.usage.billed_unit.

See OpenTelemetry.

promptTokens on Anthropic, Bedrock, and Claude Code

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:

  • @tanstack/ai-anthropic before 0.21.0.
  • @tanstack/ai-bedrock (Converse) before 0.5.0.
  • @tanstack/ai-claude-code before 0.8.0.

If your code adds the cache counts to promptTokens, remove that addition. If you keep it, you count the cache two times:

diff
  const details = usage.promptTokensDetails
- const input = usage.promptTokens + (details?.cachedTokens ?? 0) + (details?.cacheWriteTokens ?? 0)
+ const input = usage.promptTokens

To get only the uncached tokens, subtract cachedTokens and cacheWriteTokens from promptTokens. See Token usage.

Renamed types and helpers

RemovedUse instead
AGUIEventType, StreamChunkTypeThe EventType enum, or `${EventType}` for the string union
UsageTotals, ImageUsageTokenUsage
OtelSpanKindOtelSpanScope (from @tanstack/ai/middlewares/otel)
StreamProcessor.startAssistantMessage()StreamProcessor.prepareAssistantMessage()
ApprovalRequestedEvent, ToolInputAvailableEventThe interrupt outcome on RUN_FINISHED
toolName on TOOL_CALL_START chunkstoolCallName
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'.

Provider adapters

Each provider tool type alias is gone. Use the Config name. For example, WebSearchTool is now WebSearchToolConfig:

PackageRemoved aliases
@tanstack/ai-anthropicBashTool, CodeExecutionTool, ComputerUseTool, CustomTool, MemoryTool, TextEditorTool, WebFetchTool, WebSearchTool
@tanstack/ai-geminiCodeExecutionTool, ComputerUseTool, FileSearchTool, GoogleMapsTool, GoogleSearchRetrievalTool, GoogleSearchTool, UrlContextTool
@tanstack/ai-openai, @tanstack/openai-baseApplyPatchTool, CodeInterpreterTool, ComputerUseTool, CustomTool, FileSearchTool, FunctionTool, ImageGenerationTool, LocalShellTool, MCPTool, ShellTool, WebSearchPreviewTool, WebSearchTool
@tanstack/ai-openrouterWebSearchTool

Other provider changes:

ProviderChange
Anthropic skillsMove modelOptions.container.skills to the code execution tool: codeExecutionTool(config, { skills }).
Anthropic media typeDelete mediaType from image and document metadata. The adapter reads the type from source.mimeType. The types AnthropicImageMediaType and AnthropicDocumentMediaType are gone.
Gemini image modelsGoogle 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 voicegrok-voice-think-fast-1.0 is now grok-voice-think-fast-2.0.

Saved data

1.0 does not read some formats that 0.x saved. Do these steps before you deploy:

  1. Finish or clear the runs in progress. @tanstack/ai-sandbox-cloudflare does not read run-log records in the 0.x layout (status: 'done' | 'error' and createdAt). Finish or delete the runs in progress, then clear the old rec: records. migrateStoredRunRecord is gone.
  2. Resolve or drop the pending interrupts. @tanstack/ai-persistence resumes only bound interrupts. A record saved before interrupt bindings existed does not resume.
  3. Add blobKey to artifact records. ArtifactRecord.blobKey is now required, and a record without it does not load. A 0.x record without blobKey used the key artifacts/<runId>/<artifactId>. Write that value into those records. A custom store must save blobKey. resolveArtifactBlobKey() is gone, so read record.blobKey.
  4. Return { messages } from client persistence. getItem must return { messages, resume? }. A bare UIMessage[] does not load. Wrap old values in getItem:
diff
  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:

  • In @tanstack/ai-sandbox, FileEvent is now SandboxFileEvent.
  • SandboxSnapshotBundle.resolveArtifactBlobKey is gone, because each record has blobKey.

See Client Persistence and Keep generated files.

Done

You are on 1.0 when these three things are true:

  1. Your type checker passes.

  2. A search for the removed names finds nothing:

    shell
    grep -rnE "pendingInterrupts|resumeInterrupts\(|addToolApprovalResponse|conversationId|getVideoUrl|durationSeconds|unitsBilled|body\.data" src
  3. A 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.