# 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](#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](#in-the-browser) and [On the server](#on-the-server).
4. If you keep data from 0.x (chats, runs, files), do the steps in [Saved data](#saved-data) before you deploy.
5. Do the [done check](#done).

## Run the codemod

```bash
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:

- Server code that reads `body.data`. See [Read `forwardedProps`, not `data`](#read-forwardedprops-not-data).
- The value of `threadId` after the rename. The codemod changes the key only. See [`conversationId` is now `threadId`](#conversationid-is-now-threadid).
- `body` in `createChatHook`, in `useAppChat` overrides, and in Angular `injectChat`.

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
```

| 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](../interrupts/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.

To keep the schemas the same, define the tool one time with `toolDefinition()`, and share that definition between the server and the client:

```typescript ignore
// 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](../tools/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](../ui/recipes/basic-chat). Then open the guide for your framework: [React](../ui/react), [Solid](../ui/solid), [Vue](../ui/vue), or [Remix](../ui/remix). If you build your own framework adapter, use `createChatUI`. See [Custom adapters](../ui/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

| 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](#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`](#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](../interrupts/overview).

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](../media/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](../advanced/otel).

### `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](../chat/stream-events#token-usage).

### Renamed types and helpers

| 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'`.

### Provider adapters

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

## 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](../persistence/client-persistence) and [Keep generated files](../persistence/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:

   ```bash
   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](./migration) first. For the wire format, read [AG-UI Client Compliance](./ag-ui-compliance). For interrupts in depth, read [Migrate to AG-UI interrupts](../interrupts/migration).
