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
Moving Sampling Options into modelOptions
Move temperature, topP, and maxTokens into modelOptions under the provider key. At the root of chat() they do not type-check and they do nothing at runtime.
A codemod moves them for you. metadata is not a sampling option, so it stays at the root.
chat({
adapter: openaiText('gpt-6.1-sol'),
messages,
- temperature: 0.3,
- topP: 0.9,
- maxTokens: 100,
+ modelOptions: {
+ temperature: 0.3,
+ top_p: 0.9,
+ max_output_tokens: 100,
+ },
})Run the codemod#
Run the codemod on your source files:
shellnpx jscodeshift \ --parser=tsx \ -t https://raw.githubusercontent.com/TanStack/ai/main/codemods/move-sampling-to-model-options/transform.ts \ "src/**/*.{ts,tsx}"To see the changes before the codemod writes them, add --dry --print.
Read the report lines in the output. Each line is a call that the codemod did not change. Move those options by hand with the key table.
If you use Grok, change max_tokens to max_output_tokens in each Grok modelOptions. The codemod writes max_tokens for Grok, but the Grok adapter reads max_output_tokens.
Run your type checker.
Provider key reference#
The root names were the same for every provider. The key in modelOptions is different for each provider:
| Root option | OpenAI | Anthropic | Gemini | Grok | Groq | OpenRouter | Ollama (in options) |
|---|---|---|---|---|---|---|---|
| temperature | temperature | temperature | temperature | temperature | temperature | temperature | options.temperature |
| topP | top_p | top_p | topP | top_p | top_p | topP | options.top_p |
| maxTokens | max_output_tokens | max_tokens | maxOutputTokens | max_output_tokens | max_completion_tokens | maxCompletionTokens | options.num_predict |
Before and after#
Every provider starts from the same root options:
// Before
chat({
adapter: anthropicText('claude-sonnet-4-5'),
messages,
temperature: 0.3,
topP: 0.9,
maxTokens: 1024,
})OpenAI#
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
const messages = [{ role: 'user' as const, content: 'Hello' }]
chat({
adapter: openaiText('gpt-6.1-sol'),
messages,
threadId: 'thread-1',
modelOptions: {
temperature: 0.3,
top_p: 0.9,
max_output_tokens: 100,
},
})Anthropic#
import { chat } from '@tanstack/ai'
import { anthropicText } from '@tanstack/ai-anthropic'
const messages = [{ role: 'user' as const, content: 'Hello' }]
chat({
adapter: anthropicText('claude-sonnet-4-5'),
messages,
threadId: 'thread-1',
modelOptions: {
temperature: 0.3,
top_p: 0.9,
max_tokens: 1024,
},
})Gemini#
import { chat } from '@tanstack/ai'
import { geminiText } from '@tanstack/ai-gemini'
const messages = [{ role: 'user' as const, content: 'Hello' }]
chat({
adapter: geminiText('gemini-3.8-flash'),
messages,
threadId: 'thread-1',
modelOptions: {
temperature: 0.3,
topP: 0.9,
maxOutputTokens: 2048,
},
})Ollama#
Ollama is the one provider that puts the sampling options in an options object inside modelOptions. The token limit is num_predict:
import { chat } from '@tanstack/ai'
import { ollamaText } from '@tanstack/ai-ollama'
chat({
adapter: ollamaText('llama3'),
messages,
modelOptions: {
options: {
temperature: 0.3,
top_p: 0.9,
num_predict: 1000,
},
},
})metadata stays at the root#
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
const messages = [{ role: 'user' as const, content: 'Hello' }]
chat({
adapter: openaiText('gpt-6.1-sol'),
messages,
threadId: 'thread-1',
metadata: { requestId: 'abc-123' },
modelOptions: {
temperature: 0.3,
max_output_tokens: 100,
},
})Done#
Your type checker passes, and no chat() call has temperature, topP, or maxTokens at the root. For the full list of keys that each provider accepts, see its adapter page: OpenAI, Anthropic, Gemini, or Ollama.
Advanced#
Why the options moved#
- Each provider has its own names. OpenAI Responses uses max_output_tokens, Anthropic uses max_tokens, Gemini uses maxOutputTokens, and Ollama puts them in options. One root maxTokens had to guess the target for each provider. Now sampling has one place, and it uses the names of the provider API.
- The keys are typed. modelOptions is typed for each adapter and model. You get autocomplete and type errors for the exact keys that your model accepts.
- Reasoning models differ. Some reasoning models ignore temperature, and some reject max_tokens below the thinking budget. A root mapping hid these differences. In modelOptions, each adapter handles them for its provider.
How the adapter and model pick the modelOptions type: see Per-Model Type Safety.
What the codemod does#
- It changes chat(), ai(), generate(), and createChatOptions() calls that are imported from @tanstack/ai. In 1.0, @tanstack/ai exports only chat() and createChatOptions() of these.
- It finds the provider from the adapter: factory call (for example, openaiText('gpt-6.1-sol') is OpenAI). Then it renames each root option to the key of that provider.
- For Ollama, it puts the renamed keys in modelOptions.options.
- If a modelOptions object literal exists, it adds the keys to that object. It keeps the original value expressions. It expands shorthand props ({ temperature } becomes temperature: temperature).
The codemod never changes part of a call. In these cases, it leaves the call as it is and writes an api.report(...) line:
- It cannot find the adapter. One of these is true:
- The call has no adapter prop.
- The adapter is not a known provider factory call (for example makeAdapter()).
- The adapter is dynamic or a spread.
- modelOptions is not a plain object literal. For example, a spread or an identifier.
- A key conflict. A renamed key already exists in modelOptions (or in modelOptions.options for Ollama). Merge these by hand.
If you have a clone of the TanStack AI repo, you can also run the codemod from the repo root:
pnpm codemod:move-sampling-to-model-options "src/**/*.{ts,tsx}"For the full transform details and limits, see codemods/move-sampling-to-model-options/README.md.