Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
356d2d2
refactor(ai-openai): read sampling options from modelOptions
AlemTuzlak May 30, 2026
38a6211
refactor(openai-base): read sampling options from modelOptions in cha…
AlemTuzlak May 30, 2026
81738b5
refactor(ai-anthropic): read sampling options from modelOptions, drop…
AlemTuzlak May 30, 2026
c44b3e0
fix(ai-anthropic): exempt max_tokens from dropped-key warning
AlemTuzlak May 30, 2026
55a26ce
refactor(ai-gemini): read sampling options from modelOptions
AlemTuzlak May 30, 2026
e4495e1
fix(ai-ollama): read sampling from nested modelOptions.options, drop …
AlemTuzlak May 30, 2026
9c70ac3
refactor(ai-openrouter): read sampling options from modelOptions, dro…
AlemTuzlak May 30, 2026
93240a3
refactor(ai): remove root sampling options; modelOptions is the sole …
AlemTuzlak May 30, 2026
014e104
fix(ai): preserve summarize maxLength per-provider + fix otel samplin…
AlemTuzlak May 30, 2026
f91990c
refactor(ai-openrouter): read sampling from modelOptions in responses…
AlemTuzlak May 30, 2026
d9e1f8a
refactor(ai-gemini): read sampling from modelOptions in text-interact…
AlemTuzlak May 30, 2026
1240e32
test: migrate remaining root sampling usages to modelOptions
AlemTuzlak May 30, 2026
3734f55
feat(codemods): add move-sampling-to-model-options codemod
AlemTuzlak May 30, 2026
96ce4c6
docs: document sampling options under modelOptions + migration guide
AlemTuzlak May 30, 2026
ec88a7e
docs(skills): sampling options now live in modelOptions
AlemTuzlak May 30, 2026
0886b1b
chore: changeset for sampling-options-to-modelOptions move
AlemTuzlak May 30, 2026
fd0ad2c
docs: correct sampling migration framing to breaking change
AlemTuzlak May 30, 2026
4e8afb8
ci: apply automated fixes
autofix-ci[bot] May 30, 2026
acb3371
fix(sampling): address PR #660 review feedback
AlemTuzlak Jun 2, 2026
ca91230
Merge remote-tracking branch 'origin/main' into feat/sampling-options…
AlemTuzlak Jun 2, 2026
4b15de3
refactor(ai): share max-token spelling table between summarize and otel
tombeckenham Jun 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/sampling-options-to-model-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
'@tanstack/ai': minor
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please note the removed public export OllamaTextProviderOptions (deleted from ai-ollama/src/index.ts) — it's a breaking API-surface change; mention the migration to the per-model type / SDK ChatRequest.

'@tanstack/openai-base': minor
'@tanstack/ai-openai': minor
'@tanstack/ai-anthropic': minor
'@tanstack/ai-gemini': minor
'@tanstack/ai-grok': minor
'@tanstack/ai-groq': minor
'@tanstack/ai-ollama': minor
'@tanstack/ai-openrouter': minor
---

**BREAKING:** Sampling options (`temperature`, `topP`, `maxTokens`) have moved off the root of `chat()` / `ai()` / `generate()` and into provider-native `modelOptions`. There is no longer a generic root-level sampling surface — each provider accepts its own native keys, fully typed per model:

- OpenAI (Responses): `modelOptions: { temperature, top_p, max_output_tokens }`
- Anthropic: `modelOptions: { temperature, top_p, max_tokens }`
- Gemini: `modelOptions: { temperature, topP, maxOutputTokens }`
- Grok: `modelOptions: { temperature, top_p, max_tokens }`
- Groq: `modelOptions: { temperature, top_p, max_completion_tokens }`
- Ollama: `modelOptions: { options: { temperature, top_p, num_predict } }` (nested)
- OpenRouter (chat): `modelOptions: { temperature, topP, maxCompletionTokens }`

Middleware no longer sees `temperature`/`topP`/`maxTokens` as first-class fields on `ChatMiddlewareConfig`; mutate `config.modelOptions` (with the provider-native keys above) instead. `metadata` is unaffected and stays at the root.

The public `OllamaTextProviderOptions` type export has also been removed from `@tanstack/ai-ollama`. `modelOptions` is now typed per model — use the exported `OllamaChatModelOptionsByName` map (indexed by model name) or the underlying `ChatRequest` from the `ollama` SDK for arbitrary model strings.

Migrate automatically with the codemod, which resolves the provider from the adapter and rewrites the keys for you:

```bash
pnpm codemod:move-sampling-to-model-options "src/**/*.{ts,tsx}"
```

See the [Sampling Options migration guide](https://tanstack.com/ai/latest/docs/migration/sampling-options-to-model-options) for details.
7 changes: 4 additions & 3 deletions codemods/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ Each codemod lives in its own subdirectory and is named after the migration it c

## Available codemods

| Codemod | Migrates |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`ag-ui-compliance`](./ag-ui-compliance) | Client-side renames introduced by the AG-UI client/server compliance release: `body` → `forwardedProps` on `useChat` / `ChatClient` / `updateOptions`, Svelte's `updateBody` → `updateForwardedProps`, and `chat({ conversationId })` → `chat({ threadId })`. |
| Codemod | Migrates |
| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`ag-ui-compliance`](./ag-ui-compliance) | Client-side renames introduced by the AG-UI client/server compliance release: `body` → `forwardedProps` on `useChat` / `ChatClient` / `updateOptions`, Svelte's `updateBody` → `updateForwardedProps`, and `chat({ conversationId })` → `chat({ threadId })`. |
| [`move-sampling-to-model-options`](./move-sampling-to-model-options) | Moves root `temperature` / `topP` / `maxTokens` off `chat()` / `ai()` / `generate()` / `createChatOptions()` into provider-native `modelOptions`, renamed per provider (with ollama nesting under `options`). |

## Running a codemod

Expand Down
91 changes: 91 additions & 0 deletions codemods/move-sampling-to-model-options/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# `move-sampling-to-model-options`

Moves the root-level convenience sampling props — `temperature`, `topP`, and
`maxTokens` — off `chat()` / `ai()` / `generate()` / `createChatOptions()`
calls (imported from `@tanstack/ai`) and into the provider-native
`modelOptions` object, renaming each one to its provider's canonical option
name.

This is a **breaking change**: the root-level props have been removed, so run
this codemod to migrate existing call sites onto the new `modelOptions` shape.

## What it changes

The provider is resolved from the `adapter:` property's factory call (e.g.
`openaiText('gpt-4o')` → `openai`). Each present root prop is moved into
`modelOptions` under its provider-specific name:

| Root prop | openai (Responses) | anthropic | gemini | grok | groq | openrouter | ollama (nested) |
| ------------- | ------------------- | ------------- | ----------------- | ------------- | ----------------------- | --------------------- | --------------------- |
| `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_tokens` | `max_completion_tokens` | `maxCompletionTokens` | `options.num_predict` |

The `openai` column above is the **Responses** adapter (`openaiText`), whose
`maxTokens` key is `max_output_tokens`. The **Chat Completions** adapter
(`openaiChatCompletions`) instead uses `max_tokens`, and is _not_ auto-resolved
by this codemod — those call sites are left untouched and reported, so migrate
them by hand: `temperature → temperature`, `topP → top_p`,
`maxTokens → max_tokens`.

For **ollama**, the renamed keys are nested inside a `options` object **within**
`modelOptions` (e.g. `modelOptions: { options: { temperature, num_predict } }`).

### Example (openai)

```ts
// before
chat({
adapter: openaiText('gpt-4o'),
messages,
temperature: 0.3,
maxTokens: 100,
})

// after
chat({
adapter: openaiText('gpt-4o'),
messages,
modelOptions: {
temperature: 0.3,
max_output_tokens: 100,
},
})
```

If `modelOptions` already exists (as an object literal), the renamed keys are
merged into it. Original value expressions are preserved; a shorthand prop
(`{ temperature }`) whose provider key is unchanged stays shorthand
(`{ temperature }`), and one whose key is renamed becomes `newKey: temperature`.

## Running it

From this repo:

```bash
pnpm codemod:move-sampling-to-model-options "src/**/*.{ts,tsx}"
```

Or directly against the published transform — no clone needed:

```bash
npx jscodeshift \
--parser=tsx \
-t https://raw.githubusercontent.com/TanStack/ai/main/codemods/move-sampling-to-model-options/transform.ts \
src/**/*.{ts,tsx}
```

Add `--dry --print` to preview the rewrite without modifying files.

## Report / skip behavior

The codemod never partially transforms a single call. It leaves the call
untouched and emits an `api.report(...)` message in these cases:

- **Unresolvable adapter** — no `adapter` prop, the adapter value isn't a
recognized provider-factory call (e.g. `makeAdapter()`), or it's
dynamic/spread.
- **`modelOptions` is not a plain object literal** — e.g. a spread or an
identifier reference.
- **Key conflict** — a target renamed key already exists in `modelOptions`
(or in `modelOptions.options` for ollama). Resolve these by hand.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { chat } from '@tanstack/ai'
import { anthropicText } from '@tanstack/ai-anthropic'

export function run(messages: Array<unknown>) {
const temperature = 0.5
return chat({
adapter: anthropicText('claude-3-5-sonnet-latest'),
messages,
modelOptions: { top_k: 40 },
temperature,
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { chat } from '@tanstack/ai'
import { anthropicText } from '@tanstack/ai-anthropic'

export function run(messages: Array<unknown>) {
const temperature = 0.5
return chat({
adapter: anthropicText('claude-3-5-sonnet-latest'),
messages,

modelOptions: {
top_k: 40,
temperature,
},
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Conflict case: root `temperature` AND `modelOptions.temperature` are
// both present. The codemod must leave the whole call alone and report.

import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'

export function run(messages: Array<unknown>) {
return chat({
adapter: openaiText('gpt-4o'),
messages,
modelOptions: { temperature: 0.9 },
temperature: 0.3,
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Conflict case: root `temperature` AND `modelOptions.temperature` are
// both present. The codemod must leave the whole call alone and report.

import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'

export function run(messages: Array<unknown>) {
return chat({
adapter: openaiText('gpt-4o'),
messages,
modelOptions: { temperature: 0.9 },
temperature: 0.3,
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createChatOptions } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'

export const options = createChatOptions({
adapter: openaiText('gpt-4o'),
temperature: 0.2,
topP: 0.8,
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createChatOptions } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'

export const options = createChatOptions({
adapter: openaiText('gpt-4o'),

modelOptions: {
temperature: 0.2,
top_p: 0.8,
},
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { chat } from '@tanstack/ai'
import { geminiText } from '@tanstack/ai-gemini'

export function run(messages: Array<unknown>) {
return chat({
adapter: geminiText('gemini-1.5-pro'),
messages,
topP: 0.9,
maxTokens: 512,
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { chat } from '@tanstack/ai'
import { geminiText } from '@tanstack/ai-gemini'

export function run(messages: Array<unknown>) {
return chat({
adapter: geminiText('gemini-1.5-pro'),
messages,

modelOptions: {
topP: 0.9,
maxOutputTokens: 512,
},
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ai, generate } from '@tanstack/ai'
import { anthropicText } from '@tanstack/ai-anthropic'

export function viaAi(messages: Array<unknown>) {
return ai({
adapter: anthropicText('claude-3-5-sonnet-latest'),
messages,
maxTokens: 64,
})
}

export function viaGenerate(messages: Array<unknown>) {
return generate({
adapter: anthropicText('claude-3-5-sonnet-latest'),
messages,
topP: 0.95,
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ai, generate } from '@tanstack/ai'
import { anthropicText } from '@tanstack/ai-anthropic'

export function viaAi(messages: Array<unknown>) {
return ai({
adapter: anthropicText('claude-3-5-sonnet-latest'),
messages,

modelOptions: {
max_tokens: 64,
},
})
}

export function viaGenerate(messages: Array<unknown>) {
return generate({
adapter: anthropicText('claude-3-5-sonnet-latest'),
messages,

modelOptions: {
top_p: 0.95,
},
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { chat } from '@tanstack/ai'
import { groqText } from '@tanstack/ai-groq'

export function run(messages: Array<unknown>) {
return chat({
adapter: groqText('llama-3.1-70b'),
messages,
maxTokens: 256,
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { chat } from '@tanstack/ai'
import { groqText } from '@tanstack/ai-groq'

export function run(messages: Array<unknown>) {
return chat({
adapter: groqText('llama-3.1-70b'),
messages,

modelOptions: {
max_completion_tokens: 256,
},
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Negative case: no `@tanstack/ai` import. A local `chat` helper that
// happens to share the name and use `temperature`/`maxTokens` must be
// left completely untouched.

function chat(opts: { temperature?: number; maxTokens?: number }) {
return opts
}

export const result = chat({
adapter: 'whatever',
temperature: 0.3,
maxTokens: 100,
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Negative case: no `@tanstack/ai` import. A local `chat` helper that
// happens to share the name and use `temperature`/`maxTokens` must be
// left completely untouched.

function chat(opts: { temperature?: number; maxTokens?: number }) {
return opts
}

export const result = chat({
adapter: 'whatever',
temperature: 0.3,
maxTokens: 100,
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { chat } from '@tanstack/ai'
import { ollamaText } from '@tanstack/ai-ollama'

export function run(messages: Array<unknown>) {
return chat({
adapter: ollamaText('llama3'),
messages,
temperature: 0.7,
maxTokens: 200,
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { chat } from '@tanstack/ai'
import { ollamaText } from '@tanstack/ai-ollama'

export function run(messages: Array<unknown>) {
return chat({
adapter: ollamaText('llama3'),
messages,

modelOptions: {
options: {
temperature: 0.7,
num_predict: 200,
},
},
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'

export function run(messages: Array<unknown>) {
return chat({
adapter: openaiText('gpt-4o'),
messages,
temperature: 0.3,
maxTokens: 100,
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'

export function run(messages: Array<unknown>) {
return chat({
adapter: openaiText('gpt-4o'),
messages,

modelOptions: {
temperature: 0.3,
max_output_tokens: 100,
},
})
}
Loading
Loading