Skip to content

Commit 6df32b5

Browse files
AlemTuzlakautofix-ci[bot]tombeckenhamclaude
authored
feat: move sampling options (temperature/topP/maxTokens) into modelOptions (#660)
* refactor(ai-openai): read sampling options from modelOptions * refactor(openai-base): read sampling options from modelOptions in chat-completions base * refactor(ai-anthropic): read sampling options from modelOptions, drop cast * fix(ai-anthropic): exempt max_tokens from dropped-key warning * refactor(ai-gemini): read sampling options from modelOptions * fix(ai-ollama): read sampling from nested modelOptions.options, drop cast and flat root reads * refactor(ai-openrouter): read sampling options from modelOptions, drop cast * refactor(ai): remove root sampling options; modelOptions is the sole sampling surface * fix(ai): preserve summarize maxLength per-provider + fix otel sampling attribute spellings * refactor(ai-openrouter): read sampling from modelOptions in responses adapter * refactor(ai-gemini): read sampling from modelOptions in text-interactions adapter * test: migrate remaining root sampling usages to modelOptions * feat(codemods): add move-sampling-to-model-options codemod * docs: document sampling options under modelOptions + migration guide * docs(skills): sampling options now live in modelOptions * chore: changeset for sampling-options-to-modelOptions move * docs: correct sampling migration framing to breaking change * ci: apply automated fixes * fix(sampling): address PR #660 review feedback Blocking fixes (codemod CI + Ollama silent drops): - codemod: Prettier-normalize the transform test harness so recast's print style no longer diverges from the Prettier-formatted fixtures (20/20 green), and add a `test:lib` script to the codemods package so `nx affected` (and thus `test:pr`) actually runs the suite instead of skipping it. - summarize: place the default `temperature` where the wrapped provider reads it — nested under `options` for Ollama (a flat value was dropped at the wire while still surfacing in OTel). Honor caller-set flat token limits in the Ollama branch, and warn instead of silently dropping `maxLength` for an unrecognised adapter name. - code-mode eval harness: nest Ollama `num_predict`/`num_ctx` under `options`. - examples: route the generic `maxTokens` through provider-native `modelOptions` (shared `maxTokensModelOptions` helper for dynamically resolved adapters; native keys inline where the adapter is static). Other review items: - anthropic: `max_tokens ?? 1024` so an explicit `0` reaches validation instead of being coerced to the default. - openrouter (chat): forward root `metadata` like the responses adapter (+ test). - otel: `firstNumber` now requires `Number.isFinite` (rejects NaN/Infinity). - logger: add `InternalLogger.warn`, gated by the `errors` category so `debug: false` still silences it. - docs: fix the migration example to put sampling under `modelOptions`; drop leaked trailing tags from the sampling guide; note the removed `OllamaTextProviderOptions` export in the changeset; split the codemod README's OpenAI row into Responses vs Chat Completions. - codemod: emit ES6 shorthand when a moved value matches its key; drop an unsafe `as RootSamplingKey` cast. * refactor(ai): share max-token spelling table between summarize and otel Extract the provider-native max-output-token key spellings into `utilities/sampling-keys.ts` so the summarize wrapper's caller-limit detection and the OTel middleware's sampling-attribute extraction read from one source and cannot drift. Also document the intentional non-native-spelling caveat in `applyMaxLength`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8aabeec commit 6df32b5

96 files changed

Lines changed: 2729 additions & 422 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
'@tanstack/ai': minor
3+
'@tanstack/openai-base': minor
4+
'@tanstack/ai-openai': minor
5+
'@tanstack/ai-anthropic': minor
6+
'@tanstack/ai-gemini': minor
7+
'@tanstack/ai-grok': minor
8+
'@tanstack/ai-groq': minor
9+
'@tanstack/ai-ollama': minor
10+
'@tanstack/ai-openrouter': minor
11+
---
12+
13+
**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:
14+
15+
- OpenAI (Responses): `modelOptions: { temperature, top_p, max_output_tokens }`
16+
- Anthropic: `modelOptions: { temperature, top_p, max_tokens }`
17+
- Gemini: `modelOptions: { temperature, topP, maxOutputTokens }`
18+
- Grok: `modelOptions: { temperature, top_p, max_tokens }`
19+
- Groq: `modelOptions: { temperature, top_p, max_completion_tokens }`
20+
- Ollama: `modelOptions: { options: { temperature, top_p, num_predict } }` (nested)
21+
- OpenRouter (chat): `modelOptions: { temperature, topP, maxCompletionTokens }`
22+
23+
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.
24+
25+
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.
26+
27+
Migrate automatically with the codemod, which resolves the provider from the adapter and rewrites the keys for you:
28+
29+
```bash
30+
pnpm codemod:move-sampling-to-model-options "src/**/*.{ts,tsx}"
31+
```
32+
33+
See the [Sampling Options migration guide](https://tanstack.com/ai/latest/docs/migration/sampling-options-to-model-options) for details.

codemods/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ Each codemod lives in its own subdirectory and is named after the migration it c
66

77
## Available codemods
88

9-
| Codemod | Migrates |
10-
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
11-
| [`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 })`. |
9+
| Codemod | Migrates |
10+
| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
11+
| [`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 })`. |
12+
| [`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`). |
1213

1314
## Running a codemod
1415

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# `move-sampling-to-model-options`
2+
3+
Moves the root-level convenience sampling props — `temperature`, `topP`, and
4+
`maxTokens` — off `chat()` / `ai()` / `generate()` / `createChatOptions()`
5+
calls (imported from `@tanstack/ai`) and into the provider-native
6+
`modelOptions` object, renaming each one to its provider's canonical option
7+
name.
8+
9+
This is a **breaking change**: the root-level props have been removed, so run
10+
this codemod to migrate existing call sites onto the new `modelOptions` shape.
11+
12+
## What it changes
13+
14+
The provider is resolved from the `adapter:` property's factory call (e.g.
15+
`openaiText('gpt-4o')``openai`). Each present root prop is moved into
16+
`modelOptions` under its provider-specific name:
17+
18+
| Root prop | openai (Responses) | anthropic | gemini | grok | groq | openrouter | ollama (nested) |
19+
| ------------- | ------------------- | ------------- | ----------------- | ------------- | ----------------------- | --------------------- | --------------------- |
20+
| `temperature` | `temperature` | `temperature` | `temperature` | `temperature` | `temperature` | `temperature` | `options.temperature` |
21+
| `topP` | `top_p` | `top_p` | `topP` | `top_p` | `top_p` | `topP` | `options.top_p` |
22+
| `maxTokens` | `max_output_tokens` | `max_tokens` | `maxOutputTokens` | `max_tokens` | `max_completion_tokens` | `maxCompletionTokens` | `options.num_predict` |
23+
24+
The `openai` column above is the **Responses** adapter (`openaiText`), whose
25+
`maxTokens` key is `max_output_tokens`. The **Chat Completions** adapter
26+
(`openaiChatCompletions`) instead uses `max_tokens`, and is _not_ auto-resolved
27+
by this codemod — those call sites are left untouched and reported, so migrate
28+
them by hand: `temperature → temperature`, `topP → top_p`,
29+
`maxTokens → max_tokens`.
30+
31+
For **ollama**, the renamed keys are nested inside a `options` object **within**
32+
`modelOptions` (e.g. `modelOptions: { options: { temperature, num_predict } }`).
33+
34+
### Example (openai)
35+
36+
```ts
37+
// before
38+
chat({
39+
adapter: openaiText('gpt-4o'),
40+
messages,
41+
temperature: 0.3,
42+
maxTokens: 100,
43+
})
44+
45+
// after
46+
chat({
47+
adapter: openaiText('gpt-4o'),
48+
messages,
49+
modelOptions: {
50+
temperature: 0.3,
51+
max_output_tokens: 100,
52+
},
53+
})
54+
```
55+
56+
If `modelOptions` already exists (as an object literal), the renamed keys are
57+
merged into it. Original value expressions are preserved; a shorthand prop
58+
(`{ temperature }`) whose provider key is unchanged stays shorthand
59+
(`{ temperature }`), and one whose key is renamed becomes `newKey: temperature`.
60+
61+
## Running it
62+
63+
From this repo:
64+
65+
```bash
66+
pnpm codemod:move-sampling-to-model-options "src/**/*.{ts,tsx}"
67+
```
68+
69+
Or directly against the published transform — no clone needed:
70+
71+
```bash
72+
npx jscodeshift \
73+
--parser=tsx \
74+
-t https://raw.githubusercontent.com/TanStack/ai/main/codemods/move-sampling-to-model-options/transform.ts \
75+
src/**/*.{ts,tsx}
76+
```
77+
78+
Add `--dry --print` to preview the rewrite without modifying files.
79+
80+
## Report / skip behavior
81+
82+
The codemod never partially transforms a single call. It leaves the call
83+
untouched and emits an `api.report(...)` message in these cases:
84+
85+
- **Unresolvable adapter** — no `adapter` prop, the adapter value isn't a
86+
recognized provider-factory call (e.g. `makeAdapter()`), or it's
87+
dynamic/spread.
88+
- **`modelOptions` is not a plain object literal** — e.g. a spread or an
89+
identifier reference.
90+
- **Key conflict** — a target renamed key already exists in `modelOptions`
91+
(or in `modelOptions.options` for ollama). Resolve these by hand.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { chat } from '@tanstack/ai'
2+
import { anthropicText } from '@tanstack/ai-anthropic'
3+
4+
export function run(messages: Array<unknown>) {
5+
const temperature = 0.5
6+
return chat({
7+
adapter: anthropicText('claude-3-5-sonnet-latest'),
8+
messages,
9+
modelOptions: { top_k: 40 },
10+
temperature,
11+
})
12+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { chat } from '@tanstack/ai'
2+
import { anthropicText } from '@tanstack/ai-anthropic'
3+
4+
export function run(messages: Array<unknown>) {
5+
const temperature = 0.5
6+
return chat({
7+
adapter: anthropicText('claude-3-5-sonnet-latest'),
8+
messages,
9+
10+
modelOptions: {
11+
top_k: 40,
12+
temperature,
13+
},
14+
})
15+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Conflict case: root `temperature` AND `modelOptions.temperature` are
2+
// both present. The codemod must leave the whole call alone and report.
3+
4+
import { chat } from '@tanstack/ai'
5+
import { openaiText } from '@tanstack/ai-openai'
6+
7+
export function run(messages: Array<unknown>) {
8+
return chat({
9+
adapter: openaiText('gpt-4o'),
10+
messages,
11+
modelOptions: { temperature: 0.9 },
12+
temperature: 0.3,
13+
})
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Conflict case: root `temperature` AND `modelOptions.temperature` are
2+
// both present. The codemod must leave the whole call alone and report.
3+
4+
import { chat } from '@tanstack/ai'
5+
import { openaiText } from '@tanstack/ai-openai'
6+
7+
export function run(messages: Array<unknown>) {
8+
return chat({
9+
adapter: openaiText('gpt-4o'),
10+
messages,
11+
modelOptions: { temperature: 0.9 },
12+
temperature: 0.3,
13+
})
14+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { createChatOptions } from '@tanstack/ai'
2+
import { openaiText } from '@tanstack/ai-openai'
3+
4+
export const options = createChatOptions({
5+
adapter: openaiText('gpt-4o'),
6+
temperature: 0.2,
7+
topP: 0.8,
8+
})
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { createChatOptions } from '@tanstack/ai'
2+
import { openaiText } from '@tanstack/ai-openai'
3+
4+
export const options = createChatOptions({
5+
adapter: openaiText('gpt-4o'),
6+
7+
modelOptions: {
8+
temperature: 0.2,
9+
top_p: 0.8,
10+
},
11+
})
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { chat } from '@tanstack/ai'
2+
import { geminiText } from '@tanstack/ai-gemini'
3+
4+
export function run(messages: Array<unknown>) {
5+
return chat({
6+
adapter: geminiText('gemini-1.5-pro'),
7+
messages,
8+
topP: 0.9,
9+
maxTokens: 512,
10+
})
11+
}

0 commit comments

Comments
 (0)