Persistence (Advanced)
- Build your first harness
- Host options
- Send and show media
- Connect clients
- Write a client in any language
- Access and tenants
- Run in the terminal
- Deploy a harness
- Send inputs safely
- Run a tool without a model
- Durable sessions
- Run background work on workers
- List, rename, and fork sessions
- Lifecycle and retention
- Run side effects once
- Wait for a person
- Build on the session log
- Control how a turn ends
- Store settings per thread
- Compact a long session
- Fork and reset a thread
- One log for a session tree
- Write a plugin
- Track usage and cost
- Ask before risky tool calls
- Give your agent coding tools
- Sandbox and extend the coding tools
- Run the coding tools for many threads
- Undo a turn
- Build a coding agent
- Delegate to coding agents
- Switch between agent profiles
- Auth and connectors
- Share a thread between people
- Connect model providers
- Build your own UI
- Code mode in a harness
- Work until a goal is met
- Run agents from a harness
- Hand off a conversation
- Add skills
- Use MCP servers
- Give your agent a computer
- Use from any MCP client
- Use a harness from an editor (ACP)
- Overview
- Quick Start
- ProvidersUpdated
- Harnesses
- Harness Auth
- Workspace
- Tools
- Policy
- Lifecycle & Snapshots
- Portable Snapshots
- Keep Files After Reload
- Save a Named Version
- Branch From a Version
- Send a Frozen File
- Let the Agent Save and Fork
- Pick Which Files to Keep
- What a Snapshot Stores
- Instance Durability
- Durable Runs
- Events
Sandboxes (Advanced)
- Runtime Context
- Bring Your Own Key
- Keep Keys on the Server
- Debug Logging
- Multimodal Content
- Files API
- Per-Model Type Safety
- Runtime Adapter Switching
- Tree-Shaking
- Extend Adapter
- Extend Adapter: Advanced
- Typed Pre-Configured Options
- Test with a Fake Model
- Prompt Caching
- Mid-Conversation Changes
- Approval Flow Processing
Migration Guide
Early 0.x called openai(), passed model to chat(), and used providerOptions or toResponseStream. Replace those first, then follow Upgrade to 1.0.
What changed#
| Old | New | Section |
|---|---|---|
| openai() plus a model option | openaiText('<model>') | 1 |
| options: { temperature, topP, maxTokens } and providerOptions | modelOptions, with the provider's own key names | 2 |
| toResponseStream(stream, { abortController }) | toServerSentEventsStream(stream, abortController) or toServerSentEventsResponse(stream) | 3 |
| embedding() with openaiEmbed() | embed() with openaiEmbedding('<model>') | 4 |
| createWebSearchTool from @tanstack/ai-openrouter | webSearchTool from @tanstack/ai-openrouter/tools | 5 |
| chat({ stream: false }) resolves to a string | It resolves to { text, chunks } | 6 |
Upgrade#
- Go through the sections below in order. Skip a section if your code does not use the old API.
- For section 2, run the sampling codemod. It moves the sampling options into modelOptions for you.
- Run your type checker, and fix the errors that are left.
- Do Upgrade to 1.0.
1. Adapter functions split#
Each provider exports one function for each activity. The model id goes into that function, so chat() has no model option.
// Before
import { chat } from '@tanstack/ai'
import { openai } from '@tanstack/ai-openai'
const stream = chat({
adapter: openai(),
model: 'gpt-5.2',
messages: [{ role: 'user', content: 'Hello!' }],
})// After
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages: [{ role: 'user', content: 'Hello!' }],
threadId: 'thread-1',
})Import only the functions that you use:
import {
openaiText, // Chat/text generation
openaiSummarize, // Summarization
openaiImage, // Image generation
openaiSpeech, // Text-to-speech
openaiTranscription, // Audio transcription
openaiVideo, // Video generation
openaiEmbedding, // Embeddings
} from '@tanstack/ai-openai'import {
anthropicText, // Chat/text generation
anthropicSummarize, // Summarization
} from '@tanstack/ai-anthropic'import {
geminiText, // Chat/text generation
geminiSummarize, // Summarization
geminiImage, // Image generation
geminiSpeech, // Text-to-speech (experimental)
} from '@tanstack/ai-gemini'import {
ollamaText, // Chat/text generation
ollamaSummarize, // Summarization
} from '@tanstack/ai-ollama'If you switch providers at runtime, keep a map of adapter functions:
// Before
import { chat } from '@tanstack/ai'
import { openai } from '@tanstack/ai-openai'
import { anthropic } from '@tanstack/ai-anthropic'
type Provider = 'openai' | 'anthropic'
function getAdapter(provider: Provider) {
switch (provider) {
case 'openai':
return openai()
case 'anthropic':
return anthropic()
}
}
const stream = chat({
adapter: getAdapter(provider),
model: provider === 'openai' ? 'gpt-5.2' : 'claude-sonnet-4-5',
messages,
})// After
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { anthropicText } from '@tanstack/ai-anthropic'
type Provider = 'openai' | 'anthropic'
const messages = [{ role: 'user' as const, content: 'Hello!' }]
const adapters = {
openai: () => openaiText('gpt-6.1-sol'),
anthropic: () => anthropicText('claude-sonnet-5-5'),
}
const provider: Provider = 'openai'
const stream = chat({
adapter: adapters[provider](),
messages,
threadId: 'thread-1',
})For more on the split, see Tree-Shaking.
2. Options moved into modelOptions#
This change came in two releases:
- The options object was flattened. temperature, topP, and maxTokens moved to the root of chat(), and providerOptions was renamed to modelOptions.
- Later, the root temperature, topP, and maxTokens were removed. They now live in modelOptions, under the key name of each provider.
metadata stays at the root of chat().
// Before
const stream = chat({
adapter: openai(),
model: 'gpt-5.2',
messages,
options: {
temperature: 0.7,
maxTokens: 1000,
topP: 0.9,
},
providerOptions: {
responseFormat: { type: 'json_object' },
logitBias: { '123': 1.0 },
},
})// After
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
const messages = [{ role: 'user' as const, content: 'Hello!' }]
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages,
threadId: 'thread-1',
modelOptions: {
temperature: 0.7,
max_output_tokens: 1000,
top_p: 0.9,
},
})The keys in modelOptions are the keys of the provider API. openaiText has no responseFormat or logitBias key. For typed JSON output, use outputSchema.
modelOptions is typed from the adapter and the model that you pass. Your editor shows the keys that your model accepts. For the key names of each provider, see Moving Sampling Options into modelOptions.
3. toResponseStream is now toServerSentEventsStream#
toServerSentEventsStream returns a ReadableStream, not a Response. It takes the AbortController as the second argument, not in an options object.
// Before
import { chat, toResponseStream } from '@tanstack/ai'
import { openai } from '@tanstack/ai-openai'
export async function POST(request: Request) {
const { messages } = await request.json()
const abortController = new AbortController()
const stream = chat({
adapter: openai(),
model: 'gpt-5.2',
messages,
abortController,
})
return toResponseStream(stream, { abortController })
}// After
import { chat, toServerSentEventsStream } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
export async function POST(request: Request) {
const { messages, threadId } = await request.json()
const abortController = new AbortController()
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages,
threadId,
abortController,
})
const readableStream = toServerSentEventsStream(stream, abortController)
return new Response(readableStream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
})
}If you do not need your own Response, return toServerSentEventsResponse(stream, { abortController }). It makes the Response for you.
For newline-delimited JSON, not SSE, use toHttpStream:
import { toHttpStream } from '@tanstack/ai'
const readableStream = toHttpStream(stream, abortController)
return new Response(readableStream, {
headers: {
'Content-Type': 'application/x-ndjson',
},
})4. embedding() is now embed()#
The old embedding() function was removed. Embeddings came back as a new activity, embed(). The old embedding() name and the old adapter factories (like openaiEmbed()) do not come back.
// Before
import { embedding } from '@tanstack/ai'
import { openaiEmbed } from '@tanstack/ai-openai'
const result = await embedding({
adapter: openaiEmbed(),
model: 'text-embedding-3-small',
input: 'Hello, world!',
})// After
import { embed } from '@tanstack/ai'
import { openaiEmbedding } from '@tanstack/ai-openai'
const result = await embed({
adapter: openaiEmbedding('text-embedding-3-small'),
input: 'Hello, world!',
})
console.log(result.embeddings[0]?.vector)What is different:
- The model goes into the adapter function, the same as every other activity.
- One function for one input or many. input takes one item or an array. The result always has an embeddings array, with one vector for each input item.
- Multimodal input. Models like Cohere embed-v4.0 and Amazon Titan Multimodal take image parts, and text and image together.
- dimensions is a root option. Request Matryoshka dimensions with no provider-specific option.
See the Embeddings guide.
5. Provider tools moved to /tools#
Each adapter package exports its provider tools (web search, code execution, computer use, and more) from a /tools subpath.
Only @tanstack/ai-openrouter has a breaking change. createWebSearchTool is gone from the package root. It is now webSearchTool, from @tanstack/ai-openrouter/tools. The provider tools of Anthropic, OpenAI, and Gemini were new exports, so no import breaks there.
// Before
import { createWebSearchTool } from '@tanstack/ai-openrouter'
const tools = [
createWebSearchTool({ engine: 'native', maxResults: 5 }),
]// After
import { webSearchTool } from '@tanstack/ai-openrouter/tools'
const tools = [
webSearchTool({ engine: 'native', maxResults: 5 }),
]- The config and the behavior do not change. The factory takes the same config object. The tool works the same in chat({ tools: [...] }).
- The create prefix is gone, to match the other providers (webSearchTool in @tanstack/ai-anthropic/tools, @tanstack/ai-openai/tools, and more).
- New: type-level gating. The supports.tools list of each model names the provider tools that it supports. If you pass a provider tool that is not in the list, you get a type error on the tools array. Your toolDefinition() tools are not affected.
For each provider tool and the models that support it, see Provider Tools.
6. stream: false returns an object#
chat({ stream: false }) and streamToText(stream) resolve to a ChatResult object, not a string. Read text from the result.
- const text = await chat({ adapter, messages, stream: false })
+ const { text } = await chat({ adapter, messages, stream: false })
- const streamedText = await streamToText(stream)
+ const { text: streamedText } = await streamToText(stream)// After
import { chat, streamToText } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
const { text } = await chat({
adapter: openaiText('gpt-6.1-sol'),
messages: [{ role: 'user', content: 'Hello!' }],
threadId: 'thread-1',
stream: false,
})
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages: [{ role: 'user', content: 'Hello!' }],
threadId: 'thread-1',
})
const { text: streamedText } = await streamToText(stream)text is the same string as before. The new chunks array holds every chunk of the run, so you can also read tool calls, usage, and interrupts. See chat() returns.
With outputSchema, stream: false still resolves to the parsed object.
All changes together#
// Before
import { chat, toResponseStream } from '@tanstack/ai'
import { openai } from '@tanstack/ai-openai'
export async function POST(request: Request) {
const { messages } = await request.json()
const abortController = new AbortController()
const stream = chat({
adapter: openai(),
model: 'gpt-5.2',
messages,
options: {
temperature: 0.7,
maxTokens: 1000,
},
providerOptions: {
responseFormat: { type: 'json_object' },
},
abortController,
})
return toResponseStream(stream, { abortController })
}// After
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
export async function POST(request: Request) {
const { messages, threadId } = await request.json()
const abortController = new AbortController()
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages,
threadId,
modelOptions: {
temperature: 0.7,
max_output_tokens: 1000,
},
abortController,
})
return toServerSentEventsResponse(stream, { abortController })
}Done#
Your code has no openai(), model:, options:, providerOptions, toResponseStream, or embedding() call, and your type checker passes. Next, do Upgrade to 1.0.