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:

Alpha1.0
Provider adapters423, plus openaiCompatible for any OpenAI-style API
UI frameworksReact, Solid, vanilla JSReact, Vue, Solid, Svelte, Preact, Angular, Octane, Remix, vanilla JS
Coding agentsNoneClaude Code, Codex, OpenCode, Grok Build, any ACP agent
Sandbox providersNone11

Two rules from the post matter more than anything else when you upgrade:

  1. Upgrade every stable @tanstack/ai* package together. A 1.0 client needs a 1.0 server.
  2. The new agent harness is not part of the stable promise. @tanstack/ai-harness and @tanstack/ai-harness-cli ship as 0.x (npm shows 0.1.0 for both today), and their APIs “can change in a minor release”.

Before you start

  • Node and modules. @tanstack/ai 1.0.0 declares "engines": { "node": ">=18" } and "type": "module". It is ESM only. In our test, require('@tanstack/ai') failed with ERR_PACKAGE_PATH_NOT_EXPORTED (No "exports" main defined), while import worked. If you have CommonJS scripts or older Jest setups that require() the package, move them to ESM or dynamic import().
  • Peer dependencies. @tanstack/ai lists @opentelemetry/api as an optional peer. @tanstack/ai-react lists react >=18 as required, and react-dom and @mcp-ui/client as 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_KEY at 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:

FrameworkUI packageHook or factory
React@tanstack/ai-reactuseChat
Vue@tanstack/ai-vueuseChat (refs, read with .value in <script setup>)
Solid@tanstack/ai-soliduseChat (accessors, call as functions)
Svelte@tanstack/ai-sveltecreateChat (reactive getters)
Preact@tanstack/ai-preactuseChat
Angular@tanstack/ai-angularinjectChat (Signals)
Vanilla JS@tanstack/ai-clientChatClient
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:

  1. Run the codemod.
  2. Update every @tanstack/ai* package to 1.0 at the same time, and remove @tanstack/ai-react-ui, @tanstack/ai-solid-ui and @tanstack/ai-vue-ui.
  3. Run your type checker and fix each error.
  4. If you keep data from 0.x (chats, runs, files), do the saved-data steps before you deploy.
  5. 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:

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 })

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 threadId and runId from generationParamsFromRequest(), which throws when the body has no AG-UI envelope.
  • Subagent run functions: ctx.chat() fills it in, but a plain chat() call needs threadId: ctx.threadId.
  • @tanstack/ai-compaction: CompactionContext.threadId and SummarizeInput.threadId. The guide suggests giving the summary call its own id, such as ${threadId}:compaction.
  • @tanstack/ai-code-mode-snippets and the @tanstack/ai-sandbox tool-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 with RUN_FINISHED. The client “cannot parse a [DONE] line, and it reports an error.”
  • Put message at the top level of RUN_ERROR. A nested error.message is ignored.
  • Send thinking as REASONING_* events, not STEP_STARTED/STEP_FINISHED.
  • Send approvals and client tools as interrupts on RUN_FINISHED, not the old approval-requested and tool-input-available custom 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:

  1. Finish or clear runs in progress if you use @tanstack/ai-sandbox-cloudflare, then clear old rec: records.
  2. Resolve or drop pending interrupts; @tanstack/ai-persistence resumes only bound interrupts.
  3. Add blobKey to artifact records. 0.x records without it used artifacts/<runId>/<artifactId>.
  4. Make client persistence getItem return { 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

SymptomLikely causeFix
400 with Invalid AG-UI request bodyRequest 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 HonoThrown Response from chatParamsFromRequest is not caughtWrap in try/catch and return the Response
ERR_PACKAGE_PATH_NOT_EXPORTEDrequire() on an ESM-only packageUse import or dynamic import()
Codemod reports 0 okGlob not matchedPass src --extensions=ts,tsx and check the result line
Client shows an error at the end of every replyCustom server still sends data: [DONE]End with RUN_FINISHED
Every message starts a new conversationthreadId regenerated on each renderUse a stable route or database id
Old chats disappear after upgradegetItem returns an arrayWrap it as { messages }
Client tool never runs, run waitsServer and client tool schemas differShare one toolDefinition()
Input tokens look too high on ClaudeCode still adds cache tokens to promptTokensUse usage.promptTokens as is
Type errors on mixed versionsSome @tanstack/ai* packages still on 0.xUpgrade 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.