How to Upgrade to TanStack AI 1.0: Streaming Chat, threadId, Codemod and Breaking Changes
If you build AI features in TypeScript with TanStack AI, 1.0 is the release to build on: the core APIs are now stable, but upgrading from 0.x is not a version bump. threadId is now required on every chat and generation hook, body became forwardedProps, the ai-*-ui component packages are gone, and the server only accepts a full AG-UI request body. This guide gets a new project streaming in a few minutes, then walks a 0.x app through the codemod and every manual fix, with the errors you will see if you miss one.
TanStack announced 1.0 on October 23, 2026. The npm registry shows @tanstack/ai, @tanstack/ai-react, @tanstack/ai-openai and @tanstack/ai-client at 1.0.0, all published on October 22, 2026 (UTC). Everything below comes from that post, the official Quick Start, the Upgrade to 1.0 guide and the testing guide. Where we say “we tested”, we installed the 1.0.0 packages on Node 22 and ran the code ourselves, using the library’s own fake model so no API key was involved.
What 1.0 actually changes
According to the release post, 1.0 “has no new concepts”. The team removed every API that 0.x had marked deprecated, and every replacement already shipped in 0.x. If you followed the deprecation warnings, most of your code already uses the new names.
The headline numbers from the post, comparing the December 2025 alpha with 1.0:
| Alpha | 1.0 | |
|---|---|---|
| Provider adapters | 4 | 23, plus openaiCompatible for any OpenAI-style API |
| UI frameworks | React, Solid, vanilla JS | React, Vue, Solid, Svelte, Preact, Angular, Octane, Remix, vanilla JS |
| Coding agents | None | Claude Code, Codex, OpenCode, Grok Build, any ACP agent |
| Sandbox providers | None | 11 |
Two rules from the post matter more than anything else when you upgrade:
- Upgrade every stable
@tanstack/ai*package together. A 1.0 client needs a 1.0 server. - The new agent harness is not part of the stable promise.
@tanstack/ai-harnessand@tanstack/ai-harness-cliship as0.x(npm shows0.1.0for both today), and their APIs “can change in a minor release”.
Before you start
- Node and modules.
@tanstack/ai1.0.0 declares"engines": { "node": ">=18" }and"type": "module". It is ESM only. In our test,require('@tanstack/ai')failed withERR_PACKAGE_PATH_NOT_EXPORTED(No "exports" main defined), whileimportworked. If you have CommonJS scripts or older Jest setups thatrequire()the package, move them to ESM or dynamicimport(). - Peer dependencies.
@tanstack/ailists@opentelemetry/apias an optional peer.@tanstack/ai-reactlistsreact >=18as required, andreact-domand@mcp-ui/clientas optional. You do not need to install the optional ones for a basic chat. - A provider key on the server. The Quick Start uses OpenAI, and the adapter reads
OPENAI_API_KEYat runtime. The Quick Start suggests OpenRouter if you want one key for 300+ models.
Part 1: A new streaming chat in about five minutes
If you are starting fresh, this is the official Quick Start flow, trimmed to React. Other frameworks swap only the UI package.
Step 1: Install
pnpm add @tanstack/ai @tanstack/ai-react @tanstack/ai-openai
The release post says to change the last package if OpenAI is not your provider. For other UI frameworks, the Quick Start lists the package to swap in:
| Framework | UI package | Hook or factory |
|---|---|---|
| React | @tanstack/ai-react | useChat |
| Vue | @tanstack/ai-vue | useChat (refs, read with .value in <script setup>) |
| Solid | @tanstack/ai-solid | useChat (accessors, call as functions) |
| Svelte | @tanstack/ai-svelte | createChat (reactive getters) |
| Preact | @tanstack/ai-preact | useChat |
| Angular | @tanstack/ai-angular | injectChat (Signals) |
| Vanilla JS | @tanstack/ai-client | ChatClient |
| Octane | @tanstack/ai-octane plus octane | |
| Remix | @tanstack/ai-remix plus remix |
Step 2: Add the API key
Put it in the server environment only, for example .env:
OPENAI_API_KEY=your-openai-api-key
The Quick Start is explicit: “Do not send this key to the browser.” If you want users to paste their own key instead, 1.0 adds Bring Your Own Key support (defineByok), covered in the BYOK docs.
Step 3: Create the server route
In Next.js this is app/api/chat/route.ts. This is the Quick Start code:
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
export async function POST(request: Request) {
const { messages, threadId, runId } = await chatParamsFromRequest(request);
const stream = chat({
adapter: openaiText("gpt-6.1-sol"),
messages,
threadId,
runId,
});
return toServerSentEventsResponse(stream);
}
The route returns a standard Web Response, so the docs list TanStack Start, Next.js, SvelteKit, Hono and Remix controller actions as hosts that work directly. Express and other Node-stream servers should follow the separate “Quick Start: Server Only” page.
The model id gpt-6.1-sol is the one the docs use. If you are still deciding on a model, our GPT-6.1 Sol migration guide covers what changed in that model.
Step 4: Render the chat
import { useState } from "react";
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
export function Chat() {
const [input, setInput] = useState("");
const { messages, sendMessage, isLoading, stop } = useChat({
connection: fetchServerSentEvents("/api/chat"),
threadId: "support-chat",
});
return (
<>
{messages.map((message) => (
<div key={message.id}>
{message.parts.map((part, index) =>
part.type === "text" ? <p key={index}>{part.content}</p> : null,
)}
</div>
))}
<form
onSubmit={(event) => {
event.preventDefault();
if (input.trim() === "") return;
sendMessage(input);
setInput("");
}}
>
<input value={input} onChange={(event) => setInput(event.target.value)} />
{isLoading ? (
<button type="button" onClick={stop}>Stop</button>
) : (
<button type="submit">Send</button>
)}
</form>
</>
);
}
Note threadId: "support-chat". In 1.0 it is required. A hard-coded string is fine for a demo, but in a real app it should be the conversation id from your route or database (more on this below).
How the pieces connect
The Quick Start describes the flow in five steps: sendMessage posts to /api/chat; chatParamsFromRequest reads the messages and run ids; chat() calls the model through the adapter and returns a stream of events; toServerSentEventsResponse sends those events as Server-Sent Events; and fetchServerSentEvents reads them so the hook can append each chunk to messages. Each message has parts. The example renders only text parts; tool calls and other content arrive as other part types.
Part 2: Test the server route without an API key
1.0 ships a scripted fake model, fakeText() from @tanstack/ai/testing, documented in Test with a Fake Model. It is the fastest way to check your route, middleware and client wiring before you spend tokens. We used it to test the route above, swapping only the adapter:
import { chat, chatParamsFromRequest, toServerSentEventsResponse } from "@tanstack/ai";
import { fakeText } from "@tanstack/ai/testing";
const fake = fakeText();
export async function POST(request: Request) {
const { messages, threadId, runId } = await chatParamsFromRequest(request);
fake.appendResponses([{ text: "Hello from TanStack AI 1.0!" }]);
const stream = chat({ adapter: fake, messages, threadId, runId });
return toServerSentEventsResponse(stream);
}
Per the testing docs, each model call takes the next queued answer, and an empty queue ends the call with a RUN_ERROR (“No more fake responses queued”), which is why we append one answer per request.
The curl gotcha: a “simple” body returns 400
The first thing most people do is curl the endpoint with a messages array. In 1.0 that fails. We sent:
curl -i -X POST localhost:3005/api/chat \
-d '{"messages":[{"role":"user","content":"hi"}]}'
and got HTTP/1.1 400 Bad Request with the body Invalid AG-UI request body. See docs/migration/ag-ui-compliance.md. The Quick Start notes that chatParamsFromRequest throws a Response with status 400 when the body is invalid.
In our tests with 1.0.0, chatParamsFromRequest rejected a body that was missing any of threadId, runId, tools or context, and rejected messages without an id. state and forwardedProps could be left out. This body worked:
curl -N -X POST localhost:3005/api/chat \
-H 'content-type: application/json' \
-d '{"threadId":"t1","runId":"r1","messages":[{"id":"m1","role":"user","content":"hi"}],"tools":[],"context":[]}'
The response was a stream of standard AG-UI events. Trimmed, it looked like this:
data: {"type":"RUN_STARTED","runId":"r1","threadId":"t1",...}
data: {"type":"TEXT_MESSAGE_START","messageId":"r1-text","role":"assistant",...}
data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"r1-text","delta":"Hell",...}
data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"r1-text","delta":"o fr",...}
...
data: {"type":"TEXT_MESSAGE_END","messageId":"r1-text",...}
data: {"type":"RUN_FINISHED","runId":"r1","threadId":"t1","usage":[...],...}
Two things to notice. There is no data: [DONE] line at the end; the run ends with RUN_FINISHED. And you never have to build this body by hand in an app, because useChat and the other hooks send the full envelope for you. It only matters for curl tests, webhooks, and backends in other languages that call your route.
Catch the 400 on Next.js, SvelteKit and Hono
The thrown Response is not handled for you everywhere. The Quick Start says TanStack Start, SolidStart, Remix and React Router 7 return it to the client automatically, but Next.js Route Handlers, SvelteKit, Hono and raw Node do not. On those hosts, wrap the call:
export async function POST(request: Request) {
try {
const { messages, threadId, runId } = await chatParamsFromRequest(request);
const stream = chat({ adapter: openaiText("gpt-6.1-sol"), messages, threadId, runId });
return toServerSentEventsResponse(stream);
} catch (error) {
if (error instanceof Response) return error;
throw error;
}
}
Without this, a bad request body shows up as a 500 instead of a 400, which makes client bugs look like server bugs.
Part 3: Upgrade a 0.x app to 1.0
The Upgrade to 1.0 guide lays it out in five steps:
- Run the codemod.
- Update every
@tanstack/ai*package to 1.0 at the same time, and remove@tanstack/ai-react-ui,@tanstack/ai-solid-uiand@tanstack/ai-vue-ui. - Run your type checker and fix each error.
- If you keep data from 0.x (chats, runs, files), do the saved-data steps before you deploy.
- Run the “done” check.
Do it on a branch, and commit after the codemod so you can review its diff separately from your hand edits.
Step 1: Run the codemod, and preview it first
The official command is:
npx jscodeshift \
--parser=tsx \
-t https://raw.githubusercontent.com/TanStack/ai/main/codemods/ag-ui-compliance/transform.ts \
"src/**/*.{ts,tsx}"
The guide says to add --dry --print to preview the changes without writing them.
Gotcha we hit: with jscodeshift 17.4.0 (what npx fetched for us), the quoted glob above matched nothing. The output was Skipping path src/**/*.{ts,tsx} which does not exist and 0 ok, which looks like success but changed no files. Passing the directory and the extensions worked:
npx jscodeshift --parser=tsx --extensions=ts,tsx \
-t https://raw.githubusercontent.com/TanStack/ai/main/codemods/ag-ui-compliance/transform.ts \
src --dry --print
That run reported 2 ok on our two test files. Always check the Results: line; 0 ok means nothing was transformed.
The codemod does these renames, per the guide:
| 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 }) |
It does not fix server code that reads body.data, body inside createChatHook, useAppChat overrides or Angular injectChat, or calls made through a re-export (barrel) file. The guide says a file changes only when it imports the hook or class directly from a TanStack AI package.
It also renames the key but not the value. On our 0.x server route, this line:
chat({ adapter, messages: params.messages, conversationId: params.forwardedProps.conversationId })
became:
chat({ adapter, messages: params.messages, threadId: params.forwardedProps.conversationId })
That compiles, but it is wrong for 1.0. Fix it by hand, as shown next.
Step 2: Pass a real threadId everywhere
threadId is now required in the browser and on the server, and, in the guide’s words, “the client does not make one for you.” It applies to useChat and the other chat hooks, Angular injectChat, the hook returned by createChatHook (pass it per call, for example useAppChat({ threadId })), ChatClient, and the generation hooks such as useGenerateImage and useGeneration.
// In the browser
- useChat({ connection })
+ useChat({ connection, threadId: chatId })
// On the server
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages: params.messages,
- conversationId: params.forwardedProps.conversationId,
+ threadId: params.threadId,
})
Use a stable id per conversation, such as your route param or database id. The guide warns: “Do not make a new random id on each render: a new id starts a new conversation.” In React, that means no crypto.randomUUID() directly in the component body.
On the server, a few more places now need a threadId, according to the guide:
- Generation routes: use
threadIdandrunIdfromgenerationParamsFromRequest(), which throws when the body has no AG-UI envelope. - Subagent
runfunctions:ctx.chat()fills it in, but a plainchat()call needsthreadId: ctx.threadId. @tanstack/ai-compaction:CompactionContext.threadIdandSummarizeInput.threadId. The guide suggests giving the summary call its own id, such as${threadId}:compaction.@tanstack/ai-code-mode-snippetsand the@tanstack/ai-sandboxtool-bridge functions.
Step 3: Read forwardedProps, not body.data
Extra fields from the client now arrive 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.provider
Generation hooks send their input in forwardedProps too, so a custom generation route that does const { prompt } = body.data must read body.forwardedProps instead. Also, chatParamsFromRequestBody(body).context is now aguiContext.
Careful: some options keep the name body because they are different options. The connection option fetchServerSentEvents(url, { body }), the per-call sendMessage(content, { body }) and the generation hooks’ useGenerateImage({ body }) are unchanged.
Step 4: Replace tool approvals with interrupts
addToolApprovalResponse(), pendingInterrupts and resumeInterrupts() are gone. Read interrupts and resolve each one:
- const { addToolApprovalResponse } = useChat({ connection, body: { userId } })
- addToolApprovalResponse({ id, approved: true })
+ const { interrupts } = useChat({
+ connection,
+ threadId: chatId,
+ forwardedProps: { userId },
+ })
+ const approval = interrupts.find((item) => item.kind === 'tool-approval')
+ if (approval?.kind === 'tool-approval') approval.resolveInterrupt(true)
Other renames from the guide: ChatClient.getPendingInterrupts() is now getInterrupts(), and interrupt.error is now interrupt.errors[0]. An interrupt from a 0.x server with no binding shows up as kind: 'unbound' and cannot be resumed.
If you use client tools, the server tool and the browser .client() tool must have identical schemas, outputSchema included. If they differ, the guide says the client does not run the tool and the run waits on a generic interrupt. Define each tool once with toolDefinition() and import that file on both sides.
Step 5: Rebuild the UI without the ai-*-ui packages
@tanstack/ai-react-ui, @tanstack/ai-solid-ui and @tanstack/ai-vue-ui have no 1.0 release; on npm, @tanstack/ai-react-ui is still at 0.9.8. The ready-made Chat, ChatMessages, ChatMessage, ChatInput and ToolApproval components are gone. Build the UI with createChatHook from the /ui subpath of your framework package:
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({ threadId }: { threadId: string }) {
const chat = useAppChat({ threadId })
return <chat.AppChat />
}
This is the guide’s example. TextPart and ThinkingPart still exist; import them from the same /ui subpath.
Step 6: Fix custom servers, usage numbers and renamed types
If you have your own backend that streams to a TanStack AI client (for example, a Python service), the guide lists four changes:
- Stop sending
data: [DONE]and end the run withRUN_FINISHED. The client “cannot parse a[DONE]line, and it reports an error.” - Put
messageat the top level ofRUN_ERROR. A nestederror.messageis ignored. - Send thinking as
REASONING_*events, notSTEP_STARTED/STEP_FINISHED. - Send approvals and client tools as interrupts on
RUN_FINISHED, not the oldapproval-requestedandtool-input-availablecustom events.
Usage accounting changed too. TokenUsage.durationSeconds and unitsBilled are replaced by usage.billed, which holds a quantity and a unit. And on Anthropic, Bedrock and Claude Code adapters, promptTokens now includes cache reads and writes. The guide names the old versions that did not: @tanstack/ai-anthropic before 0.21.0, @tanstack/ai-bedrock before 0.5.0 and @tanstack/ai-claude-code before 0.8.0. If your cost dashboard adds the cache counts to promptTokens, remove that addition or you will double-count cached tokens.
A few renames you are likely to hit in middleware or devtools: AGUIEventType and StreamChunkType are replaced by the EventType enum; toolName on TOOL_CALL_START is now toolCallName; and provider tool aliases such as WebSearchTool are now WebSearchToolConfig.
Step 7: Migrate saved data before you deploy
Passing the type check does not migrate stored conversations, runs or files. The guide lists four steps:
- Finish or clear runs in progress if you use
@tanstack/ai-sandbox-cloudflare, then clear oldrec:records. - Resolve or drop pending interrupts;
@tanstack/ai-persistenceresumes only bound interrupts. - Add
blobKeyto artifact records. 0.x records without it usedartifacts/<runId>/<artifactId>. - Make client persistence
getItemreturn{ messages }, not a bare array. The guide’s shim:
getItem: (id) => {
const saved = JSON.parse(localStorage.getItem(id) ?? 'null')
- return saved
+ return Array.isArray(saved) ? { messages: saved } : saved
},
Step 4 is the one that bites quietly: users reload the page and their old chats are just gone.
Step 8: Run the “done” check
The guide says you are on 1.0 when three things are true: the type checker passes; this search finds nothing:
grep -rnE "pendingInterrupts|resumeInterrupts\(|addToolApprovalResponse|conversationId|getVideoUrl|durationSeconds|unitsBilled|body\.data" src
and a chat message streams a reply while a saved chat still loads after a page reload. Given the codemod behaviour above, expect this grep to flag params.forwardedProps.conversationId values the codemod left in place.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
400 with Invalid AG-UI request body | Request body is not a full AG-UI envelope (in our tests: missing threadId, runId, tools, context, or message id) | Let the hooks send requests, or include those fields in manual calls |
Bad requests show up as 500 on Next.js or Hono | Thrown Response from chatParamsFromRequest is not caught | Wrap in try/catch and return the Response |
ERR_PACKAGE_PATH_NOT_EXPORTED | require() on an ESM-only package | Use import or dynamic import() |
Codemod reports 0 ok | Glob not matched | Pass src --extensions=ts,tsx and check the result line |
| Client shows an error at the end of every reply | Custom server still sends data: [DONE] | End with RUN_FINISHED |
| Every message starts a new conversation | threadId regenerated on each render | Use a stable route or database id |
| Old chats disappear after upgrade | getItem returns an array | Wrap it as { messages } |
| Client tool never runs, run waits | Server and client tool schemas differ | Share one toolDefinition() |
| Input tokens look too high on Claude | Code still adds cache tokens to promptTokens | Use usage.promptTokens as is |
| Type errors on mixed versions | Some @tanstack/ai* packages still on 0.x | Upgrade all stable packages together |
What about the agent harness?
The release also introduces @tanstack/ai-harness, a long-lived session layer for agents: durable session logs, waits that survive restarts, typed subagents, plugins and background work. The post shows a coding agent built from first-party plugins and exposed via runCli, with flags for one-shot prompts (-p), HTTP (--serve), ACP for editors such as Zed (--acp) and MCP for Claude Code and Cursor (--mcp). It can also hand work to Claude Code, Codex or Grok Build in a sandbox.
It is worth a weekend experiment, but keep it out of production paths for now. The team calls it “experimental, on purpose”, the packages are 0.x, and the post itself notes the permissions plugin “isn’t a sandbox”: those tools run on the host with its permissions. The deploy docs also list known recovery limits, such as in-memory child-agent waits that do not resume across hosts.
FAQ
Do I have to upgrade right away? Nothing in the post sets a deadline for 0.x, but 1.0 is the version the maintainers say to build on, and the removed APIs will not come back. If you already cleared deprecation warnings in 0.x, the upgrade is mostly the codemod, threadId and a type check.
Can I upgrade the client first and the server later? No. The post says “a 1.0 client needs a 1.0 server”, so deploy them together.
Does TanStack AI 1.0 still support many providers? Yes. The post counts 23 adapters, including new ones for Cloudflare Workers AI and AI Gateway, Azure OpenAI and Lovable AI Gateway, plus openaiCompatible for any OpenAI-style API.
Is there a typed yes/no or classification helper? 1.0 adds decide() for typed choice, score and boolean decisions. If you are calling OpenAI for that kind of task, compare it with OpenAI’s own Decisions API.
Where do I report migration problems? The maintainers ask for feedback through the TanStack AI GitHub repo and Discord, linked from the release post.
For more hands-on guides to new AI developer tools, see the 7minai homepage.