Skip to content

feat: move sampling options (temperature/topP/maxTokens) into modelOptions#660

Open
AlemTuzlak wants to merge 18 commits into
mainfrom
feat/sampling-options-to-modeloptions
Open

feat: move sampling options (temperature/topP/maxTokens) into modelOptions#660
AlemTuzlak wants to merge 18 commits into
mainfrom
feat/sampling-options-to-modeloptions

Conversation

@AlemTuzlak
Copy link
Copy Markdown
Contributor

@AlemTuzlak AlemTuzlak commented May 30, 2026

Summary

Moves sampling options — temperature, topP, maxTokensoff the root of chat() / ai() / generate() and into provider-native, fully-typed modelOptions. modelOptions is now the single sampling surface; there is no generic root-level mapping anymore. Each provider exposes its real SDK/API key names.

This continues and completes #499 by @harry-whorlow (which was built on the old packages/typescript/ layout and could not be rebased onto the restructured packages/ tree). Supersedes #499.

BREAKING for a 0.x SDK. A provider-aware codemod migrates existing code automatically.

Provider-native modelOptions keys

Provider temperature topP maxTokens
OpenAI (Responses) temperature top_p max_output_tokens
Anthropic temperature top_p max_tokens
Gemini temperature topP maxOutputTokens
Grok temperature top_p max_tokens
Groq temperature top_p max_completion_tokens
Ollama options.temperature options.top_p options.num_predict (nested)
OpenRouter (chat) temperature topP maxCompletionTokens
OpenRouter (Responses) temperature topP maxOutputTokens

What changed

  • Core (@tanstack/ai): removed temperature/topP/maxTokens from TextOptions, the public activity options, and ChatMiddlewareConfig (+ structured-output config). Removed all engine plumbing. Middleware now adjusts sampling via config.modelOptions. metadata is unchanged and stays at the root.
  • All 7 text providers read sampling from typed modelOptions (native keys, verified against each provider's API). The provider-options types gained the sampling fields where missing (OpenAI, Anthropic, Gemini, Ollama; Grok/Groq/OpenRouter already had them).
  • Casts removed on the sampling path (Anthropic, Ollama, OpenRouter). No new casts introduced.
  • Bugs fixed along the way: a pre-existing Ollama double-nesting mismatch (typed nested options vs flat runtime spread); an OpenRouter variant field leaking into the request body; OTel gen_ai.request.* attributes now read a union of provider key spellings; summarize({ maxLength }) now resolves the correct per-provider token key from the wrapped adapter (was about to become a silent no-op).
  • Codemod move-sampling-to-model-options (jscodeshift): resolves the provider from the adapter factory and rewrites keys per provider (incl. Ollama nesting); reports + skips on conflicts or unresolvable adapters. 12 fixture cases.
  • Docs: new migration guide (docs/migration/sampling-options-to-model-options), updated adapter pages, middleware dynamic-temperature example, API reference, and typed-options.
  • Agent skills: adapter-configuration, middleware, chat-experience SKILL.md + reference files updated to the new surface.
  • Changeset: minor across @tanstack/ai + all affected adapter packages.

Migration

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

Testing

  • Per-adapter unit tests assert sampling reaches the wire under the correct native key (incl. Ollama no-double-nesting, Anthropic max_tokens default + no spurious dropped-key warning).
  • Codemod fixture tests (12 cases). OTel + summarize regression tests.
  • E2E suite verified green (already routes sampling via modelOptions).
  • Full pnpm test:pr green: test:sherif, test:knip, test:docs, test:eslint, test:lib, test:types, test:build, build across 33 projects.

Known follow-up (not blocking)

The ts-code-mode-web / ts-react-chat examples still pass a generic maxTokens on calls with dynamically-resolved adapters — the codemod can't infer a provider there, and they're excluded from CI (examples/**). They need per-example provider mapping in a follow-up.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Breaking Changes

    • Sampling options (temperature, topP, maxTokens) are no longer accepted at the root of chat() / ai() / generate() calls. These must now be passed under provider-native modelOptions with provider-specific key names.
  • New Features

    • Added automated codemod to migrate existing code to use modelOptions for sampling configuration.
  • Documentation

    • Updated adapter guides and middleware documentation with migration instructions and examples showing the new modelOptions structure.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 30, 2026

📝 Walkthrough

Walkthrough

This PR executes a breaking change removing root-level sampling options (temperature, topP, maxTokens) from TanStack AI public APIs. All sampling configuration now flows through provider-native modelOptions keys. The change includes adapter refactoring across all providers, an automated jscodeshift codemod, comprehensive documentation with before/after examples, and full test coverage.

Changes

Sampling Options Migration to modelOptions

Layer / File(s) Summary
Public API contract removal
packages/ai/src/types.ts, packages/ai/src/activities/chat/middleware/types.ts
TextOptions and ChatMiddlewareConfig remove temperature, topP, maxTokens properties. These sampling fields are no longer part of the public contract.
Core engine refactoring
packages/ai/src/activities/chat/index.ts
TextActivity removes all root-level sampling destructuring from beforeRun, streamModelResponse, runStructuredFinalization, and middleware config wiring. Only metadata and modelOptions flow through the engine.
OpenAI base adapter and sampling consolidation
packages/ai-openai/src/text/text-provider-options.ts, packages/openai-base/src/adapters/*, packages/ai-openai/tests/*
Introduce OpenAISamplingOptions interface; base adapters (ChatCompletions, Responses) now read all sampling from modelOptions using OpenAI-native keys. Add type-safety tests and sampling forwarding verification.
Anthropic adapter sampling consolidation
packages/ai-anthropic/src/adapters/text.ts, packages/ai-anthropic/src/text/text-provider-options.ts, packages/ai-anthropic/tests/anthropic-adapter.test.ts
Refactor mapCommonOptionsToAnthropic to read temperature, top_p, max_tokens from modelOptions. Reorganize sampling JSDoc. Add tests for modelOptions forwarding and default token limits.
Gemini adapter sampling via modelOptions
packages/ai-gemini/src/adapters/text.ts, packages/ai-gemini/src/experimental/text-interactions/adapter.ts, packages/ai-gemini/src/text/text-provider-options.ts, packages/ai-gemini/tests/*
Remove root-level sampling conditionals; add temperature, topP, maxOutputTokens to provider options. Update tests to verify modelOptions forwarding using Gemini-native field names.
Ollama nested modelOptions.options structure
packages/ai-ollama/src/adapters/text.ts, packages/ai-ollama/src/index.ts, packages/ai-ollama/src/meta/models-meta.ts, packages/ai-ollama/tests/text-adapter.test.ts
Update adapter to forward sampling via nested modelOptions.options. Remove public OllamaTextProviderOptions type. Add temperature and top_p to OllamaOptions. Add comprehensive nested-options contract tests.
Groq, Grok, and OpenRouter adapters
packages/ai-groq/tests/groq-adapter.test.ts, packages/ai-grok/tests/grok-adapter.test.ts, packages/ai-openrouter/src/adapters/*.ts, packages/ai-openrouter/tests/*.ts
Update Groq and Grok adapters to forward sampling from modelOptions using provider-native keys. Refactor OpenRouter adapters to extract variant from modelOptions and forward remaining sampling. Add sampling-option forwarding tests.
Middleware telemetry and summarize adapter
packages/ai/src/middlewares/otel.ts, packages/ai/src/activities/summarize/chat-stream-summarize.ts
OTel middleware extracts sampling attributes from modelOptions with support for nested modelOptions.options and multiple provider-native key spellings. Summarize adapter adds provider-specific token-limit key mapping.
Codemod for automated migration
codemods/move-sampling-to-model-options/*, codemods/package.json, codemods/README.md, package.json
Implement jscodeshift codemod with provider detection, key renaming, Ollama nesting, conflict detection, and non-partial validation. Includes 16 test fixtures covering all providers and edge cases.
Documentation and migration guidance
docs/migration/sampling-options-to-model-options.md, docs/migration/migration.md, docs/adapters/*.md, docs/advanced/middleware.md, docs/config.json, packages/ai/skills/*
Comprehensive migration guide with before/after examples per provider, key reference table, and codemod instructions. Update all adapter docs and middleware guidance. Add migration page to site navigation.
Test updates
packages/ai/tests/chat.test.ts, packages/ai/tests/middleware.test.ts, packages/ai/tests/middlewares/otel.test.ts, packages/ai/tests/summarize-max-length.test.ts
Update chat, middleware, and OTel middleware tests to verify sampling flows through modelOptions. Add comprehensive summarize adapter tests for provider-specific token-limit mapping.
Code evaluation and NPM scripts
packages/ai-code-mode/models-eval/judge.ts, packages/ai-code-mode/models-eval/run-eval.ts
Update evaluation tools to use modelOptions with provider-native token-limit keys via new maxTokensModelOptions helper. Add NPM scripts for codemod execution.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • crutchcorn
  • schiller-manuel

Poem

🐰 Hops through the code with glee,
Sampling options move with grace,
From root to modelOptions' place,
Provider-native keys now free!
A codemod lights the way,
Breaking changes → better code today.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sampling-options-to-modeloptions

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 30, 2026

🚀 Changeset Version Preview

9 package(s) bumped directly, 21 bumped as dependents.

🟥 Major bumps

Package Version Reason
@tanstack/ai-anthropic 0.11.2 → 1.0.0 Changeset
@tanstack/ai-gemini 0.12.1 → 1.0.0 Changeset
@tanstack/ai-grok 0.9.1 → 1.0.0 Changeset
@tanstack/ai-groq 0.2.8 → 1.0.0 Changeset
@tanstack/ai-ollama 0.6.23 → 1.0.0 Changeset
@tanstack/ai-openai 0.10.4 → 1.0.0 Changeset
@tanstack/ai-openrouter 0.10.0 → 1.0.0 Changeset
@tanstack/openai-base 0.4.1 → 1.0.0 Changeset
@tanstack/ai-code-mode 0.1.23 → 1.0.0 Dependent
@tanstack/ai-code-mode-skills 0.1.23 → 1.0.0 Dependent
@tanstack/ai-elevenlabs 0.2.14 → 1.0.0 Dependent
@tanstack/ai-event-client 0.4.1 → 1.0.0 Dependent
@tanstack/ai-fal 0.7.16 → 1.0.0 Dependent
@tanstack/ai-isolate-node 0.1.23 → 1.0.0 Dependent
@tanstack/ai-isolate-quickjs 0.1.23 → 1.0.0 Dependent
@tanstack/ai-preact 0.7.0 → 1.0.0 Dependent
@tanstack/ai-react 0.13.0 → 1.0.0 Dependent
@tanstack/ai-react-ui 0.8.4 → 1.0.0 Dependent
@tanstack/ai-solid 0.11.0 → 1.0.0 Dependent
@tanstack/ai-solid-ui 0.7.4 → 1.0.0 Dependent
@tanstack/ai-svelte 0.11.0 → 1.0.0 Dependent
@tanstack/ai-vue 0.11.0 → 1.0.0 Dependent

🟨 Minor bumps

Package Version Reason
@tanstack/ai 0.23.0 → 0.24.0 Changeset

🟩 Patch bumps

Package Version Reason
@tanstack/ai-client 0.14.0 → 0.14.1 Dependent
@tanstack/ai-devtools-core 0.4.1 → 0.4.2 Dependent
@tanstack/ai-isolate-cloudflare 0.2.14 → 0.2.15 Dependent
@tanstack/ai-vue-ui 0.2.7 → 0.2.8 Dependent
@tanstack/preact-ai-devtools 0.1.44 → 0.1.45 Dependent
@tanstack/react-ai-devtools 0.2.44 → 0.2.45 Dependent
@tanstack/solid-ai-devtools 0.2.44 → 0.2.45 Dependent

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented May 30, 2026

View your CI Pipeline Execution ↗ for commit 4e8afb8

Command Status Duration Result
nx run-many --targets=build --exclude=examples/... ✅ Succeeded 1m 10s View ↗

☁️ Nx Cloud last updated this comment at 2026-05-30 14:27:43 UTC

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 30, 2026

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/@tanstack/ai@660

@tanstack/ai-anthropic

npm i https://pkg.pr.new/@tanstack/ai-anthropic@660

@tanstack/ai-client

npm i https://pkg.pr.new/@tanstack/ai-client@660

@tanstack/ai-code-mode

npm i https://pkg.pr.new/@tanstack/ai-code-mode@660

@tanstack/ai-code-mode-skills

npm i https://pkg.pr.new/@tanstack/ai-code-mode-skills@660

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/@tanstack/ai-devtools-core@660

@tanstack/ai-elevenlabs

npm i https://pkg.pr.new/@tanstack/ai-elevenlabs@660

@tanstack/ai-event-client

npm i https://pkg.pr.new/@tanstack/ai-event-client@660

@tanstack/ai-fal

npm i https://pkg.pr.new/@tanstack/ai-fal@660

@tanstack/ai-gemini

npm i https://pkg.pr.new/@tanstack/ai-gemini@660

@tanstack/ai-grok

npm i https://pkg.pr.new/@tanstack/ai-grok@660

@tanstack/ai-groq

npm i https://pkg.pr.new/@tanstack/ai-groq@660

@tanstack/ai-isolate-cloudflare

npm i https://pkg.pr.new/@tanstack/ai-isolate-cloudflare@660

@tanstack/ai-isolate-node

npm i https://pkg.pr.new/@tanstack/ai-isolate-node@660

@tanstack/ai-isolate-quickjs

npm i https://pkg.pr.new/@tanstack/ai-isolate-quickjs@660

@tanstack/ai-ollama

npm i https://pkg.pr.new/@tanstack/ai-ollama@660

@tanstack/ai-openai

npm i https://pkg.pr.new/@tanstack/ai-openai@660

@tanstack/ai-openrouter

npm i https://pkg.pr.new/@tanstack/ai-openrouter@660

@tanstack/ai-preact

npm i https://pkg.pr.new/@tanstack/ai-preact@660

@tanstack/ai-react

npm i https://pkg.pr.new/@tanstack/ai-react@660

@tanstack/ai-react-ui

npm i https://pkg.pr.new/@tanstack/ai-react-ui@660

@tanstack/ai-solid

npm i https://pkg.pr.new/@tanstack/ai-solid@660

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/@tanstack/ai-solid-ui@660

@tanstack/ai-svelte

npm i https://pkg.pr.new/@tanstack/ai-svelte@660

@tanstack/ai-utils

npm i https://pkg.pr.new/@tanstack/ai-utils@660

@tanstack/ai-vue

npm i https://pkg.pr.new/@tanstack/ai-vue@660

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/@tanstack/ai-vue-ui@660

@tanstack/openai-base

npm i https://pkg.pr.new/@tanstack/openai-base@660

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/@tanstack/preact-ai-devtools@660

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/@tanstack/react-ai-devtools@660

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/@tanstack/solid-ai-devtools@660

commit: 4e8afb8

@AlemTuzlak AlemTuzlak requested a review from tombeckenham May 30, 2026 14:32
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/ai-ollama/src/adapters/text.ts (1)

36-39: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Narrow the fallback modelOptions type to the subset this adapter actually forwards.

For arbitrary model strings, ResolveModelOptions falls back to the full ollama ChatRequest, but mapCommonOptionsToOllama() only reads modelOptions.options (and then format, keep_alive, logprobs, top_logprobs, and think when present). It sources model/messages from options.model/options.messages and converts tools only from options.tools, so request-level keys like model, messages, stream, and tools typed via the fallback can be silently ignored at runtime.

type ResolveModelOptions<TModel extends string> =
  TModel extends keyof OllamaChatModelOptionsByName
    ? OllamaChatModelOptionsByName[TModel]
    : ChatRequest
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-ollama/src/adapters/text.ts` around lines 36 - 39, The fallback
for ResolveModelOptions is too broad (falls back to ChatRequest) even though
mapCommonOptionsToOllama only forwards specific fields; change the fallback to a
narrowed type that only includes the fields actually forwarded (e.g., an object
with an optional options property limited to Pick<ChatRequest['options'],
'format'|'keep_alive'|'logprobs'|'top_logprobs'|'think'> and an optional tools
property of ChatRequest['tools']) so ResolveModelOptions<TModel> returns either
OllamaChatModelOptionsByName[TModel] or that minimal subset; update the type
alias ResolveModelOptions accordingly so mapCommonOptionsToOllama and related
code use the correct, narrower modelOptions shape.
🧹 Nitpick comments (4)
codemods/move-sampling-to-model-options/README.md (1)

18-23: ⚡ Quick win

Clarify OpenAI adapter split in the mapping table.

The table currently presents a single OpenAI mapping to max_output_tokens, which can confuse openaiChatCompletions users (they need max_tokens). Please add an explicit note or split rows for OpenAI Responses vs Chat Completions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@codemods/move-sampling-to-model-options/README.md` around lines 18 - 23, The
mapping table currently lists a single OpenAI entry mapping `maxTokens` to
`max_output_tokens`, which is ambiguous for users of `openaiChatCompletions`;
update the table to split the OpenAI row into two rows (e.g., "openai
(Responses)" and "openai (Chat Completions)") and set `maxTokens` ->
`max_output_tokens` for the Responses row and `maxTokens` -> `max_tokens` for
the Chat Completions row; mention the `openaiChatCompletions` identifier in the
note so readers know which row to use.
packages/ai/tests/summarize-max-length.test.ts (1)

1-139: ⚡ Quick win

Place this unit test alongside the summarize source file.

This new suite lives under packages/ai/tests/, but the repo convention is to colocate *.test.ts files with the source they cover. Moving it next to packages/ai/src/activities/summarize/chat-stream-summarize.ts will keep the summarize contract and its regression coverage together.

As per coding guidelines, "Place unit tests in *.test.ts files alongside source files".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai/tests/summarize-max-length.test.ts` around lines 1 - 139, Move
the test file packages/ai/tests/summarize-max-length.test.ts next to the
implementation file chat-stream-summarize.ts so the test is colocated with its
source; update imports in summarize-max-length.test.ts to reference the local
module path (adjust any ../src/... imports) and keep references to
ChatStreamSummarizeAdapter, createRecordingTextAdapter, and the test helpers
(resolveDebugOption, ev) intact so the suite continues to import the same
symbols from the colocated files. Ensure the new location preserves the same
filename and that any path changes are minimal and correct for the package
module resolution.
codemods/move-sampling-to-model-options/transform.ts (1)

341-356: 💤 Low value

Consider removing the type cast for clarity.

Line 351 casts key.name as RootSamplingKey, but key.name could be any string. While safe at runtime (Set.has() returns false for non-members), the cast is technically incorrect.

♻️ Clearer type-safe alternative
       const key = (prop as Property).key
       if (
         key.type === 'Identifier' &&
-        movedSet.has(key.name as RootSamplingKey)
+        ROOT_SAMPLING_KEYS.includes(key.name as RootSamplingKey) &&
+        movedSet.has(key.name as RootSamplingKey)
       ) {
         return false

Or use a type guard:

+      const isRootSamplingKey = (name: string): name is RootSamplingKey =>
+        ROOT_SAMPLING_KEYS.includes(name as RootSamplingKey)
+
       ...
       if (
         key.type === 'Identifier' &&
-        movedSet.has(key.name as RootSamplingKey)
+        isRootSamplingKey(key.name) &&
+        movedSet.has(key.name)
       ) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@codemods/move-sampling-to-model-options/transform.ts` around lines 341 - 356,
The cast key.name as RootSamplingKey is unnecessary/unsafe; instead make the set
string-typed and check the identifier name directly: change movedSet's type from
Set<RootSamplingKey> to Set<string> when creating it (const movedSet = new
Set<string>(presentKeys)) and inside the obj.properties filter, after confirming
key.type === 'Identifier', use const name = key.name and call movedSet.has(name)
(no cast) to decide removal.
codemods/move-sampling-to-model-options/__testfixtures__/shorthand.output.ts (1)

14-16: 💤 Low value

Consider using shorthand property syntax.

The codemod could be enhanced to preserve or use ES6 shorthand syntax when the property name matches the identifier name. Line 15 uses temperature: temperature, but modern JavaScript/TypeScript convention prefers the shorthand form temperature, for better readability.

Both forms correctly reference the identifier rather than inlining the literal value, so the functional requirement is met.

♻️ More idiomatic shorthand syntax
 modelOptions: {
-  temperature: temperature,
+  temperature,
 },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@codemods/move-sampling-to-model-options/__testfixtures__/shorthand.output.ts`
around lines 14 - 16, The object inside modelOptions uses verbose property
syntax "temperature: temperature,"—update the codemod transform that emits the
modelOptions object so when a property key equals its identifier (e.g.,
temperature) it emits the ES6 shorthand (temperature,) instead; locate the code
that constructs or prints the modelOptions object (the logic producing the
"modelOptions" node and its properties) and change it to detect identical
key+identifier pairs and output the shorthand property form.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/migration/migration.md`:
- Around line 200-201: The "Complete Migration Example" still places sampling
props (temperature, maxTokens) at the chat() root even though the guide states
they were moved into provider-native modelOptions; update the "After" snippet(s)
referenced around the "Complete Migration Example" so that temperature and
maxTokens are removed from the chat() root and instead placed inside
modelOptions using provider-native keys (e.g., provider-specific names) while
keeping metadata at the root; ensure both affected blocks (around lines 461–468
and the final After snippet) reflect modelOptions usage for sampling.

In `@docs/migration/sampling-options-to-model-options.md`:
- Around line 210-211: The file ends with leaked XML/HTML tags that break
rendering; remove the accidental trailing tags "</content>" and "</invoke>" from
the end of docs/migration/sampling-options-to-model-options.md so the document
ends cleanly with the previous markdown content (ensure there are no other stray
markup fragments left).

In `@packages/ai-anthropic/src/adapters/text.ts`:
- Line 365: The code uses `modelOptions?.max_tokens || 1024` which treats
explicit 0 as falsy and overrides it; change this to use the nullish coalescing
operator so explicit 0 (or other falsy but defined values) are preserved:
replace that expression with `modelOptions?.max_tokens ?? 1024` (or an explicit
undefined check) so `validateTextProviderOptions()` / `validateMaxTokens()` can
correctly detect and reject invalid zero values.

In `@packages/ai-code-mode/models-eval/run-eval.ts`:
- Around line 224-225: The Ollama branch currently returns sampling params at
the top level (case 'ollama'), but the adapter expects them nested under
modelOptions.options; modify the 'ollama' case to return an object with an
options property containing num_predict: maxTokens and num_ctx: 32768 (i.e.,
move num_predict and num_ctx inside options) so the sampling parameters are
forwarded correctly to the Ollama adapter.

In `@packages/ai-openai/src/text/text-provider-options.ts`:
- Around line 17-38: OpenAIBaseOptions currently inherits temperature/top_p via
OpenAISamplingOptions which makes those fields valid for reasoning models;
remove temperature and top_p from the shared base by stopping OpenAIBaseOptions
from extending OpenAISamplingOptions (keep max_output_tokens in the base),
create a new OpenAISamplingOptions (or NonReasoningSamplingOptions) interface
that includes temperature/top_p and apply that only to non-reasoning model
option types in model-meta.ts (e.g., O1/O3 should not use OpenAIBaseOptions with
sampling fields); additionally ensure adapters/responses-text.ts
mapOptionsToRequest() does not blindly spread modelOptions into
ResponseCreateParams for reasoning models—either filter out temperature/top_p
there or only pass the sampling interface for non-reasoning models so the client
never sends temperature/top_p to the Responses API for reasoning models.

In `@packages/ai-openrouter/src/adapters/text.ts`:
- Around line 1148-1153: The request builder currently spreads only
restModelOptions into the ChatRequest (const request) so a root-level metadata
passed to chat({ metadata }) is dropped; update the construction of request in
the text adapter (the const request: Omit<ChatRequest, 'stream'> block) to also
include the root metadata (e.g., preserve options.metadata or a top-level
metadata parameter) alongside model, messages and tools so the ChatRequest
forwarded to OpenRouter contains metadata the same way responses adapter does.

In `@packages/ai/src/activities/summarize/chat-stream-summarize.ts`:
- Around line 80-87: In the Ollama branch (the adapterName === 'ollama' block)
ensure we don’t override caller-set token limits: before injecting
merged.options = { num_predict: maxLength, ...existing } check if existing
already contains any recognized flat token-limit keys (e.g., num_predict,
max_tokens, max_length, max_output_tokens) and if so return merged unchanged;
only set num_predict when none of those keys are present. Update the guard
around merged.options / existing and the assignment in that block to honor those
keys instead of always injecting num_predict.

In `@packages/ai/src/middlewares/otel.ts`:
- Around line 169-172: The helper firstNumber currently returns any value of
typeof 'number' (including NaN and Infinity); update its check in the
firstNumber function so it only returns numbers that are finite by using
Number.isFinite(candidate) (i.e., return candidate only if typeof candidate ===
'number' && Number.isFinite(candidate)), leaving the rest of the control flow
unchanged so invalid numeric values are filtered out and undefined is returned
when no finite number is found.

---

Outside diff comments:
In `@packages/ai-ollama/src/adapters/text.ts`:
- Around line 36-39: The fallback for ResolveModelOptions is too broad (falls
back to ChatRequest) even though mapCommonOptionsToOllama only forwards specific
fields; change the fallback to a narrowed type that only includes the fields
actually forwarded (e.g., an object with an optional options property limited to
Pick<ChatRequest['options'],
'format'|'keep_alive'|'logprobs'|'top_logprobs'|'think'> and an optional tools
property of ChatRequest['tools']) so ResolveModelOptions<TModel> returns either
OllamaChatModelOptionsByName[TModel] or that minimal subset; update the type
alias ResolveModelOptions accordingly so mapCommonOptionsToOllama and related
code use the correct, narrower modelOptions shape.

---

Nitpick comments:
In
`@codemods/move-sampling-to-model-options/__testfixtures__/shorthand.output.ts`:
- Around line 14-16: The object inside modelOptions uses verbose property syntax
"temperature: temperature,"—update the codemod transform that emits the
modelOptions object so when a property key equals its identifier (e.g.,
temperature) it emits the ES6 shorthand (temperature,) instead; locate the code
that constructs or prints the modelOptions object (the logic producing the
"modelOptions" node and its properties) and change it to detect identical
key+identifier pairs and output the shorthand property form.

In `@codemods/move-sampling-to-model-options/README.md`:
- Around line 18-23: The mapping table currently lists a single OpenAI entry
mapping `maxTokens` to `max_output_tokens`, which is ambiguous for users of
`openaiChatCompletions`; update the table to split the OpenAI row into two rows
(e.g., "openai (Responses)" and "openai (Chat Completions)") and set `maxTokens`
-> `max_output_tokens` for the Responses row and `maxTokens` -> `max_tokens` for
the Chat Completions row; mention the `openaiChatCompletions` identifier in the
note so readers know which row to use.

In `@codemods/move-sampling-to-model-options/transform.ts`:
- Around line 341-356: The cast key.name as RootSamplingKey is
unnecessary/unsafe; instead make the set string-typed and check the identifier
name directly: change movedSet's type from Set<RootSamplingKey> to Set<string>
when creating it (const movedSet = new Set<string>(presentKeys)) and inside the
obj.properties filter, after confirming key.type === 'Identifier', use const
name = key.name and call movedSet.has(name) (no cast) to decide removal.

In `@packages/ai/tests/summarize-max-length.test.ts`:
- Around line 1-139: Move the test file
packages/ai/tests/summarize-max-length.test.ts next to the implementation file
chat-stream-summarize.ts so the test is colocated with its source; update
imports in summarize-max-length.test.ts to reference the local module path
(adjust any ../src/... imports) and keep references to
ChatStreamSummarizeAdapter, createRecordingTextAdapter, and the test helpers
(resolveDebugOption, ev) intact so the suite continues to import the same
symbols from the colocated files. Ensure the new location preserves the same
filename and that any path changes are minimal and correct for the package
module resolution.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1e00706c-024b-453a-a0d4-8a667e653722

📥 Commits

Reviewing files that changed from the base of the PR and between 548e113 and 4e8afb8.

📒 Files selected for processing (86)
  • .changeset/sampling-options-to-model-options.md
  • codemods/README.md
  • codemods/move-sampling-to-model-options/README.md
  • codemods/move-sampling-to-model-options/__testfixtures__/anthropic-merge.input.ts
  • codemods/move-sampling-to-model-options/__testfixtures__/anthropic-merge.output.ts
  • codemods/move-sampling-to-model-options/__testfixtures__/conflict.input.ts
  • codemods/move-sampling-to-model-options/__testfixtures__/conflict.output.ts
  • codemods/move-sampling-to-model-options/__testfixtures__/create-chat-options.input.ts
  • codemods/move-sampling-to-model-options/__testfixtures__/create-chat-options.output.ts
  • codemods/move-sampling-to-model-options/__testfixtures__/gemini-rename.input.ts
  • codemods/move-sampling-to-model-options/__testfixtures__/gemini-rename.output.ts
  • codemods/move-sampling-to-model-options/__testfixtures__/generate-and-ai.input.ts
  • codemods/move-sampling-to-model-options/__testfixtures__/generate-and-ai.output.ts
  • codemods/move-sampling-to-model-options/__testfixtures__/groq-maxtokens.input.ts
  • codemods/move-sampling-to-model-options/__testfixtures__/groq-maxtokens.output.ts
  • codemods/move-sampling-to-model-options/__testfixtures__/no-import.input.ts
  • codemods/move-sampling-to-model-options/__testfixtures__/no-import.output.ts
  • codemods/move-sampling-to-model-options/__testfixtures__/ollama-nested.input.ts
  • codemods/move-sampling-to-model-options/__testfixtures__/ollama-nested.output.ts
  • codemods/move-sampling-to-model-options/__testfixtures__/openai-basic.input.ts
  • codemods/move-sampling-to-model-options/__testfixtures__/openai-basic.output.ts
  • codemods/move-sampling-to-model-options/__testfixtures__/openrouter-maxtokens.input.ts
  • codemods/move-sampling-to-model-options/__testfixtures__/openrouter-maxtokens.output.ts
  • codemods/move-sampling-to-model-options/__testfixtures__/shorthand.input.ts
  • codemods/move-sampling-to-model-options/__testfixtures__/shorthand.output.ts
  • codemods/move-sampling-to-model-options/__testfixtures__/unresolvable-adapter.input.ts
  • codemods/move-sampling-to-model-options/__testfixtures__/unresolvable-adapter.output.ts
  • codemods/move-sampling-to-model-options/transform.test.ts
  • codemods/move-sampling-to-model-options/transform.ts
  • codemods/package.json
  • docs/adapters/anthropic.md
  • docs/adapters/gemini.md
  • docs/adapters/grok.md
  • docs/adapters/groq.md
  • docs/adapters/ollama.md
  • docs/adapters/openai.md
  • docs/adapters/openrouter.md
  • docs/advanced/middleware.md
  • docs/advanced/typed-options.md
  • docs/api/ai.md
  • docs/config.json
  • docs/migration/migration.md
  • docs/migration/sampling-options-to-model-options.md
  • package.json
  • packages/ai-anthropic/src/adapters/text.ts
  • packages/ai-anthropic/src/text/text-provider-options.ts
  • packages/ai-anthropic/tests/anthropic-adapter.test.ts
  • packages/ai-code-mode/models-eval/judge.ts
  • packages/ai-code-mode/models-eval/run-eval.ts
  • packages/ai-gemini/src/adapters/text.ts
  • packages/ai-gemini/src/experimental/text-interactions/adapter.ts
  • packages/ai-gemini/src/text/text-provider-options.ts
  • packages/ai-gemini/tests/gemini-adapter.test.ts
  • packages/ai-gemini/tests/text-interactions-adapter.test.ts
  • packages/ai-grok/tests/grok-adapter.test.ts
  • packages/ai-groq/tests/groq-adapter.test.ts
  • packages/ai-ollama/src/adapters/text.ts
  • packages/ai-ollama/src/index.ts
  • packages/ai-ollama/src/meta/models-meta.ts
  • packages/ai-ollama/tests/text-adapter.test.ts
  • packages/ai-openai/src/text/text-provider-options.ts
  • packages/ai-openai/tests/chat-per-model-type-safety.test.ts
  • packages/ai-openai/tests/openai-adapter.test.ts
  • packages/ai-openrouter/src/adapters/responses-text.ts
  • packages/ai-openrouter/src/adapters/text.ts
  • packages/ai-openrouter/tests/openrouter-adapter.test.ts
  • packages/ai-openrouter/tests/openrouter-responses-adapter.test.ts
  • packages/ai/skills/ai-core/adapter-configuration/SKILL.md
  • packages/ai/skills/ai-core/adapter-configuration/references/anthropic-adapter.md
  • packages/ai/skills/ai-core/adapter-configuration/references/gemini-adapter.md
  • packages/ai/skills/ai-core/adapter-configuration/references/ollama-adapter.md
  • packages/ai/skills/ai-core/adapter-configuration/references/openai-adapter.md
  • packages/ai/skills/ai-core/chat-experience/SKILL.md
  • packages/ai/skills/ai-core/middleware/SKILL.md
  • packages/ai/src/activities/chat/index.ts
  • packages/ai/src/activities/chat/middleware/types.ts
  • packages/ai/src/activities/summarize/chat-stream-summarize.ts
  • packages/ai/src/middlewares/otel.ts
  • packages/ai/src/types.ts
  • packages/ai/tests/chat.test.ts
  • packages/ai/tests/middleware.test.ts
  • packages/ai/tests/middlewares/otel.test.ts
  • packages/ai/tests/summarize-max-length.test.ts
  • packages/openai-base/src/adapters/chat-completions-text.ts
  • packages/openai-base/src/adapters/responses-text.ts
  • packages/openai-base/tests/responses-text.test.ts
💤 Files with no reviewable changes (4)
  • packages/ai-gemini/src/adapters/text.ts
  • packages/ai-gemini/src/experimental/text-interactions/adapter.ts
  • packages/ai/src/types.ts
  • packages/ai-ollama/src/index.ts

Comment on lines +200 to +201
> **Heads up — sampling has since moved (breaking).** In a later release, the sampling props (`temperature`, `topP`, `maxTokens`) were removed from the root of `chat()` and now live in provider-native `modelOptions`. Passing them at the root no longer type-checks or takes effect. See [Moving Sampling Options into modelOptions](./sampling-options-to-model-options) for the codemod and provider-native key names. `metadata` stays at the root.

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

The migration guide now contradicts itself on sampling placement.

Line 200 says root-level temperature / maxTokens are removed, but the “Complete Migration Example” still uses those root props at Lines 464-465. Please move those values into modelOptions with provider-native keys in that final “After” snippet too, otherwise readers will copy a broken migration target.

Also applies to: 461-468

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/migration/migration.md` around lines 200 - 201, The "Complete Migration
Example" still places sampling props (temperature, maxTokens) at the chat() root
even though the guide states they were moved into provider-native modelOptions;
update the "After" snippet(s) referenced around the "Complete Migration Example"
so that temperature and maxTokens are removed from the chat() root and instead
placed inside modelOptions using provider-native keys (e.g., provider-specific
names) while keeping metadata at the root; ensure both affected blocks (around
lines 461–468 and the final After snippet) reflect modelOptions usage for
sampling.

Comment on lines +210 to +211
</content>
</invoke>
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove accidental trailing tags from the new migration doc.

</content> and </invoke> at the end of the file look like leaked markup artifacts. They should be removed to avoid rendering/build issues in docs pipelines.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/migration/sampling-options-to-model-options.md` around lines 210 - 211,
The file ends with leaked XML/HTML tags that break rendering; remove the
accidental trailing tags "</content>" and "</invoke>" from the end of
docs/migration/sampling-options-to-model-options.md so the document ends cleanly
with the previous markdown content (ensure there are no other stray markup
fragments left).

? validProviderOptions.thinking.budget_tokens
: undefined
const defaultMaxTokens = options.maxTokens || 1024
const defaultMaxTokens = modelOptions?.max_tokens || 1024
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the fallback in the adapter.
rg -n "defaultMaxTokens|max_tokens \\|\\| 1024" packages/ai-anthropic/src/adapters/text.ts

# Demonstrate the runtime semantics that make `||` incorrect here.
node - <<'NODE'
console.log({
  withOr: (0 || 1024),
  withNullish: (0 ?? 1024),
})
NODE

Repository: TanStack/ai

Length of output: 331


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="packages/ai-anthropic/src/adapters/text.ts"

# Print the relevant sections around line 365 and any validation logic for max_tokens.
sed -n '330,420p' "$file"

# Find the validateMaxTokens definition and all its call sites in this file.
rg -n "validateMaxTokens|max_tokens" "$file" | head -n 200

Repository: TanStack/ai

Length of output: 4291


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="packages/ai-anthropic/src/adapters/text.ts"

# Narrow further around where validateMaxTokens is called (if present).
# (Grepping line numbers first, then printing context around them.)
lines=$(rg -n "validateMaxTokens" "$file" | cut -d: -f1 || true)
if [[ -n "${lines}" ]]; then
  for ln in $lines; do
    start=$((ln-40))
    end=$((ln+40))
    echo "---- context around validateMaxTokens at line $ln ----"
    sed -n "${start},${end}p" "$file"
  done
else
  echo "No validateMaxTokens symbol found in $file"
fi

Repository: TanStack/ai

Length of output: 135


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search repo for validateMaxTokens usage/definition.
rg -n "validateMaxTokens" -S . | head -n 200

Repository: TanStack/ai

Length of output: 278


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="packages/ai-anthropic/src/text/text-provider-options.ts"

# Print validateMaxTokens implementation around the reported lines.
sed -n '280,380p' "$file"

# Find call sites of validateMaxTokens across ai-anthropic.
rg -n "validateMaxTokens\\(" packages/ai-anthropic/src | head -n 50

# Also inspect around where max_tokens is processed in that provider options file.
rg -n "max_tokens" "$file" | head -n 80

Repository: TanStack/ai

Length of output: 2229


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="packages/ai-anthropic/src/adapters/text.ts"

# Where validation is invoked for provider options.
rg -n "validateTextProviderOptions" "$file"

# Also locate construction of max_tokens / modelOptions flow in this adapter.
rg -n "defaultMaxTokens|maxTokens|max_tokens" "$file" -n

# Show a wider slice around where requestParams is assembled and any validation call nearby.
sed -n '240,520p' "$file"

Repository: TanStack/ai

Length of output: 10472


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find type declarations for max_tokens in ai-anthropic adapter text.
rg -n "max_tokens\\??\\s*:" packages/ai-anthropic/src/adapters/text.ts packages/ai-anthropic/src/text/text-provider-options.ts | head -n 200

# Also print the surrounding type block for the public modelOptions definition.
# (This gives context for whether 0 is allowed at the type level.)
rg -n "export (type|interface) .*Anthropic.*TextProviderOptions|interface .*TextProviderOptions|type .*TextProviderOptions" packages/ai-anthropic/src/adapters/text.ts packages/ai-anthropic/src/text/text-provider-options.ts | head -n 50

Repository: TanStack/ai

Length of output: 684


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="packages/ai-anthropic/src/text/text-provider-options.ts"

# Print around the max_tokens declarations and the interfaces.
sed -n '180,280p' "$file"

Repository: TanStack/ai

Length of output: 4055


Preserve explicit max_tokens values (don’t use ||)
In packages/ai-anthropic/src/adapters/text.ts, modelOptions?.max_tokens || 1024 turns max_tokens: 0 into 1024, so validateTextProviderOptions()/validateMaxTokens() never rejects the invalid value (max_tokens must be at least 1).

Suggested fix
-    const defaultMaxTokens = modelOptions?.max_tokens || 1024
+    const defaultMaxTokens = modelOptions?.max_tokens ?? 1024
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const defaultMaxTokens = modelOptions?.max_tokens || 1024
const defaultMaxTokens = modelOptions?.max_tokens ?? 1024
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-anthropic/src/adapters/text.ts` at line 365, The code uses
`modelOptions?.max_tokens || 1024` which treats explicit 0 as falsy and
overrides it; change this to use the nullish coalescing operator so explicit 0
(or other falsy but defined values) are preserved: replace that expression with
`modelOptions?.max_tokens ?? 1024` (or an explicit undefined check) so
`validateTextProviderOptions()` / `validateMaxTokens()` can correctly detect and
reject invalid zero values.

Comment on lines +224 to +225
case 'ollama':
return { num_predict: maxTokens, num_ctx: 32768 }
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.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: Ollama requires nested options structure.

The Ollama adapter expects sampling options nested under modelOptions.options, not at the top level of modelOptions. This will cause the sampling parameters to be ignored.

Based on learnings from layer 6: "Update Ollama adapter to forward sampling via nested modelOptions.options".

🐛 Proposed fix
     case 'ollama':
-      return { num_predict: maxTokens, num_ctx: 32768 }
+      return { options: { num_predict: maxTokens, num_ctx: 32768 } }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case 'ollama':
return { num_predict: maxTokens, num_ctx: 32768 }
case 'ollama':
return { options: { num_predict: maxTokens, num_ctx: 32768 } }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-code-mode/models-eval/run-eval.ts` around lines 224 - 225, The
Ollama branch currently returns sampling params at the top level (case
'ollama'), but the adapter expects them nested under modelOptions.options;
modify the 'ollama' case to return an object with an options property containing
num_predict: maxTokens and num_ctx: 32768 (i.e., move num_predict and num_ctx
inside options) so the sampling parameters are forwarded correctly to the Ollama
adapter.

Comment on lines +17 to +38
/** Sampling controls shared by all Responses-API models. */
export interface OpenAISamplingOptions {
/**
* Sampling temperature, 0–2. Higher = more random. Recommend altering this or top_p, not both.
* Note: OpenAI reasoning models (o-series, GPT-5 reasoning) reject temperature/top_p.
* https://platform.openai.com/docs/api-reference/responses/create#responses_create-temperature
*/
temperature?: number
/**
* Nucleus sampling. 0.1 = only the top 10% probability mass is considered.
* https://platform.openai.com/docs/api-reference/responses/create#responses_create-top_p
*/
top_p?: number
/**
* Upper bound on generated tokens (visible output + reasoning tokens).
* https://platform.openai.com/docs/api-reference/responses/create#responses_create-max_output_tokens
*/
max_output_tokens?: number
}

// Core, always-available options for Responses API
export interface OpenAIBaseOptions {
export interface OpenAIBaseOptions extends OpenAISamplingOptions {
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Do OpenAI Responses API reasoning models (for example o3 and GPT-5 reasoning models) accept the temperatureandtop_p parameters, or are those parameters rejected?

💡 Result:

OpenAI's dedicated reasoning models, such as the o1 and o3 series (including o3-mini) and the GPT-5 series, generally do not accept the traditional temperature and top_p sampling parameters via the API [1][2][3][4]. Attempts to include these parameters in API requests for these models typically result in a 400-level invalid_request_error stating that they are unsupported [1][2]. These reasoning models instead utilize specialized parameters to control their behavior, most notably reasoning.effort (or simply reasoning_effort in some environments), which allows developers to adjust the depth of the model's internal thinking process (e.g., minimal, low, medium, high, or xhigh) [5][6][7][8][3]. The exclusion of sampling parameters like temperature is a deliberate design choice by OpenAI to maintain the reliability and determinism required for complex, multi-step reasoning tasks [2]. Because these models are optimized to generate a high-quality, singular "best" chain-of-thought, standard sampling techniques used for creative text generation are often incompatible with their internal architecture [2]. Developers building on these models should omit temperature and top_p from their API calls to avoid request errors [1][2][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find all references to the shared sampling/base types
rg -n "OpenAISamplingOptions|OpenAIBaseOptions" packages/ai-openai/src/text/text-provider-options.ts packages/ai-openai/src -S

# Show the rest of the file around where base/model option maps are composed
FILE="packages/ai-openai/src/text/text-provider-options.ts"
if [ -f "$FILE" ]; then
  echo "---- $FILE (context) ----"
  # print around the shown snippet and further down for composition
  nl -ba "$FILE" | sed -n '1,220p'
fi

# Look for option maps / reasoning models naming patterns
rg -n "reasoning|o3|o1|gpt-5|responses.*(create|temperature|top_p)" packages/ai-openai/src -S

# Identify where modelOptions types are built for text providers
rg -n "modelOptions|ModelOptions|options.*model" packages/ai-openai/src/text packages/ai-openai/src -S

Repository: TanStack/ai

Length of output: 9554


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show key type definitions and where reasoning models include base options.
rg -n "interface OpenAIReasoningOptions|type OpenAIReasoningOptions|OpenAIReasoningOptions" packages/ai-openai/src/text/text-provider-options.ts packages/ai-openai/src/model-meta.ts -S

# Print relevant ranges from text-provider-options.ts around sampling/base/reasoning definitions
FILE="packages/ai-openai/src/text/text-provider-options.ts"
# Get line numbers via python and print small windows around important anchors
python3 - <<'PY'
import re, itertools
file_path="packages/ai-openai/src/text/text-provider-options.ts"
anchors=["export interface OpenAISamplingOptions","export interface OpenAIBaseOptions","OpenAIReasoningOptions","ExternalTextProviderOptions"]
with open(file_path,'r',encoding='utf-8') as f:
    lines=f.readlines()
for i,line in enumerate(lines,1):
    for a in anchors:
        if a in line:
            print(f"--- anchor:{a} at {i} ---")
            start=max(1,i-25); end=min(len(lines),i+120)
            for j in range(start,end+1):
                # trim long lines a bit? just output as-is
                print(f"{j}:{lines[j-1].rstrip()}")
PY

# Print relevant ranges from model-meta.ts around reasoning models (O3/O1) where OpenAIBaseOptions is used
python3 - <<'PY'
file_path="packages/ai-openai/src/model-meta.ts"
targets=["[O3.name]","[O1.name]","[O3_PRO.name]","[O1_PRO.name]","reasoning"]
with open(file_path,'r',encoding='utf-8') as f:
    lines=f.readlines()
def match_any(line):
    return any(t in line for t in targets)
for i,line in enumerate(lines,1):
    if match_any(line):
        print(f"--- match at {i}: {line.strip()} ---")
        start=max(1,i-20); end=min(len(lines),i+40)
        for j in range(start,end+1):
            print(f"{j}:{lines[j-1].rstrip()}")
PY

# Search for how temperature/top_p is serialized into API requests.
rg -n "temperature|max_output_tokens|top_p" packages/ai-openai/src -S

Repository: TanStack/ai

Length of output: 50367


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show key reasoning type definitions and where reasoning models include base options.
rg -n "OpenAIReasoningOptions" packages/ai-openai/src/text/text-provider-options.ts packages/ai-openai/src/model-meta.ts -S

# Print relevant windows from text-provider-options.ts around sampling/base/reasoning/external option composition.
python3 - <<'PY'
file_path="packages/ai-openai/src/text/text-provider-options.ts"
anchors=[
  "export interface OpenAISamplingOptions",
  "export interface OpenAIBaseOptions",
  "OpenAIReasoningOptions",
  "export type ExternalTextProviderOptions"
]
with open(file_path,'r',encoding='utf-8') as f:
    lines=f.readlines()

for i,line in enumerate(lines,1):
    for a in anchors:
        if a in line:
            print(f"\n--- anchor:{a} at {i} ---")
            start=max(1,i-25); end=min(len(lines),i+180)
            for j in range(start,end+1):
                print(f"{j}:{lines[j-1].rstrip()}")
PY

# Print relevant windows from model-meta.ts around reasoning models (O3/O1) where OpenAIBaseOptions is used.
python3 - <<'PY'
file_path="packages/ai-openai/src/model-meta.ts"
with open(file_path,'r',encoding='utf-8') as f:
    lines=f.readlines()

# Print around O3/O1 entries explicitly
for i,line in enumerate(lines,1):
    if "[O3.name]" in line or "[O1.name]" in line:
        print(f"\n--- model-meta match at {i}: {line.strip()} ---")
        start=max(1,i-25); end=min(len(lines),i+60)
        for j in range(start,end+1):
            print(f"{j}:{lines[j-1].rstrip()}")
PY

# Search for how temperature/top_p is serialized into API requests.
rg -n "temperature|max_output_tokens|top_p" packages/ai-openai/src -S

Repository: TanStack/ai

Length of output: 50367


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find non-type usages/serialization points for temperature/top_p.
# Limit output to keep it readable.
rg -n --type ts "(^|[^a-zA-Z0-9_])(temperature|top_p|max_output_tokens)([^a-zA-Z0-9_]|$)" packages/ai-openai/src -S \
  | head -n 80

# Look specifically for payload construction / Responses create call sites.
rg -n --type ts "responses\.create|Responses\.create|createResponse|response.*payload|payload\s*=" packages/ai-openai/src -S \
  | head -n 80

# Look for any conditional gating related to reasoning models.
rg -n --type ts "reasoning.*temperature|temperature.*reasoning|top_p.*reasoning|reasoning.*top_p" packages/ai-openai/src -S \
  | head -n 80

rg -n --type ts "O1\.name|O3\.name|o-series|o3|gpt-5.*reasoning|reasoning model" packages/ai-openai/src -S \
  | head -n 80

Repository: TanStack/ai

Length of output: 8286


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for where temperature/top_p are mapped into Responses API payloads for text.
rg -n --type ts "top_p\s*:|temperature\s*:" packages/ai-openai/src/adapters packages/ai-openai/src/text -S

# Look for any explicit filtering based on model capability/name.
rg -n --type ts "O1\.name|O3\.name|o3|o1|reasoning model|reasoning" packages/ai-openai/src/adapters packages/ai-openai/src/text -S | head -n 200

# Also check the legacy/openai adapter entrypoint if it builds payload for responses.create.
rg -n --type ts "responses\.create|OpenAI\.responses\.create|create\(" packages/ai-openai/src/adapters packages/ai-openai/src/text -S | head -n 200

Repository: TanStack/ai

Length of output: 37


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/ai-openai/src/adapters/text.ts"
python3 - <<'PY'
import itertools, pathlib
path=pathlib.Path("packages/ai-openai/src/adapters/text.ts")
text=path.read_text(encoding="utf-8").splitlines()
# print whole file if small? otherwise print key regions
print("LINES", len(text))
# find occurrences of responses.create or payload building
need=[]
for i,line in enumerate(text,1):
    if "responses.create" in line or "Responses.create" in line or "openai.responses.create" in line:
        need.append(i)
    if "temperature" in line or "top_p" in line:
        need.append(i)
if not need:
    # still print first 260 lines and any later sections likely containing request building
    slices=[(1,220),(220,440),(440,660),(660,880)]
else:
    slices=[]
    for i in sorted(set(need)):
        slices.append((max(1,i-60), min(len(text), i+80)))
# dedupe slices by start
seen=set()
for a,b in slices:
    key=(a,b)
    if key in seen: continue
    seen.add(key)
    print(f"\n---- slice {a}-{b} ----")
    for j in range(a,b+1):
        print(f"{j}:{text[j-1]}")
PY

# Broader scan in adapters for responses payload creation / temperature
rg -n --type ts "responses\.create|OpenAI.*responses|response[s]?\s*\(" packages/ai-openai/src/adapters -S

rg -n --type ts "temperature|max_output_tokens|top_p" packages/ai-openai/src/adapters -S || true

Repository: TanStack/ai

Length of output: 5664


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "OpenAIBaseResponsesTextAdapter" -S packages

# If found, show the key parts where it builds the ResponseCreateParams payload.
# Otherwise, we may need to determine that the filtering (if any) lives in an external package.
rg -n "mapOptionsToRequest\\(|ResponseCreateParams|responses\\.create|temperature|top_p" -S packages | head -n 200

Repository: TanStack/ai

Length of output: 28149


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/openai-base/src/adapters/responses-text.ts"

# show the region around the comment mentioning modelOptions is sole sampling surface
# (use python to avoid relying on nl)
python3 - <<'PY'
import pathlib
path=pathlib.Path("packages/openai-base/src/adapters/responses-text.ts")
lines=path.read_text(encoding="utf-8").splitlines()
# print around the first occurrence of the anchor text
anchor="modelOptions is the sole sampling surface"
idx=None
for i,l in enumerate(lines):
    if anchor in l:
        idx=i
        break
print("ANCHOR_LINE_INDEX", idx+1 if idx is not None else None)
if idx is None:
    raise SystemExit("anchor not found")
start=max(0, idx-80)
end=min(len(lines), idx+220)
for j in range(start,end):
    print(f"{j+1}:{lines[j]}")
PY

# Then narrow specifically for temperature/top_p serialization in this file
rg -n "temperature|top_p|max_output_tokens" "$FILE" -S

# Print small windows around each temperature/top_p occurrence (keep concise)
python3 - <<'PY'
import pathlib,re
path=pathlib.Path("packages/openai-base/src/adapters/responses-text.ts")
lines=path.read_text(encoding="utf-8").splitlines()
hits=[]
for i,l in enumerate(lines,1):
    if re.search(r"\b(temperature|top_p|max_output_tokens)\b", l):
        hits.append(i)
# keep up to 10 hits for brevity
for i in hits[:12]:
    start=max(1,i-15); end=min(len(lines),i+25)
    print(f"\n--- window around line {i} ---")
    for j in range(start,end+1):
        print(f"{j}:{lines[j-1]}")
PY

Repository: TanStack/ai

Length of output: 113


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/openai-base/src/adapters/responses-text.ts"

# Find where mapOptionsToRequest starts and where it mentions modelOptions/sampling.
rg -n --type ts "mapOptionsToRequest|modelOptions|sampling|temperature|top_p" "$FILE" -S

# Print around mapOptionsToRequest and around any temperature/top_p mentions.
python3 - <<'PY'
import pathlib,re
path=pathlib.Path("packages/openai-base/src/adapters/responses-text.ts")
lines=path.read_text(encoding="utf-8").splitlines()
# line numbers with temperature/top_p/max_output_tokens
targets=[]
for i,l in enumerate(lines,1):
    if re.search(r"\b(temperature|top_p|max_output_tokens)\b", l):
        targets.append(i)
# add mapOptionsToRequest region start
for i,l in enumerate(lines,1):
    if "mapOptionsToRequest" in l:
        targets.append(i)
targets=sorted(set(targets))
print("HIT_COUNT", len(targets))
# print windows
for i in targets[:30]:
    start=max(1,i-25); end=min(len(lines), i+50)
    print(f"\n---- window around {i} ----")
    for j in range(start,end+1):
        print(f"{j}:{lines[j-1]}")
PY

Repository: TanStack/ai

Length of output: 37172


Don’t expose temperature/top_p on the shared OpenAI base options for reasoning models

  • OpenAIBaseOptions currently extends OpenAISamplingOptions (temperature/top_p), and model-meta.ts assigns OpenAIBaseOptions to reasoning models like O1/O3, so these fields become valid for all models.
  • At runtime, packages/openai-base/src/adapters/responses-text.ts mapOptionsToRequest() spreads ...modelOptions directly into ResponseCreateParams (the sole sampling surface), with no capability-based filtering—so temperature/top_p will be sent to /v1/responses and rejected by OpenAI reasoning models.

Keep max_output_tokens in the shared base if needed, but gate/remove temperature/top_p from the shared base and put them only on the non-reasoning model option types.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-openai/src/text/text-provider-options.ts` around lines 17 - 38,
OpenAIBaseOptions currently inherits temperature/top_p via OpenAISamplingOptions
which makes those fields valid for reasoning models; remove temperature and
top_p from the shared base by stopping OpenAIBaseOptions from extending
OpenAISamplingOptions (keep max_output_tokens in the base), create a new
OpenAISamplingOptions (or NonReasoningSamplingOptions) interface that includes
temperature/top_p and apply that only to non-reasoning model option types in
model-meta.ts (e.g., O1/O3 should not use OpenAIBaseOptions with sampling
fields); additionally ensure adapters/responses-text.ts mapOptionsToRequest()
does not blindly spread modelOptions into ResponseCreateParams for reasoning
models—either filter out temperature/top_p there or only pass the sampling
interface for non-reasoning models so the client never sends temperature/top_p
to the Responses API for reasoning models.

Comment on lines 1148 to 1153
const request: Omit<ChatRequest, 'stream'> = {
...modelOptions,
...restModelOptions,
model: options.model + variantSuffix,
messages,
...(options.temperature !== undefined && {
temperature: options.temperature,
}),
...(options.maxTokens !== undefined && {
maxCompletionTokens: options.maxTokens,
}),
...(options.topP !== undefined && { topP: options.topP }),
...(tools && tools.length > 0 && { tools }),
}
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve root metadata in the chat-completions request mapping.

This request builder now only forwards modelOptions, so chat({ metadata }) gets dropped on the OpenRouter chat adapter even though root metadata is still part of the migration contract and the responses adapter still preserves it.

Suggested fix
     const request: Omit<ChatRequest, 'stream'> = {
       ...restModelOptions,
       model: options.model + variantSuffix,
+      ...(options.metadata !== undefined && { metadata: options.metadata }),
       messages,
       ...(tools && tools.length > 0 && { tools }),
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-openrouter/src/adapters/text.ts` around lines 1148 - 1153, The
request builder currently spreads only restModelOptions into the ChatRequest
(const request) so a root-level metadata passed to chat({ metadata }) is
dropped; update the construction of request in the text adapter (the const
request: Omit<ChatRequest, 'stream'> block) to also include the root metadata
(e.g., preserve options.metadata or a top-level metadata parameter) alongside
model, messages and tools so the ChatRequest forwarded to OpenRouter contains
metadata the same way responses adapter does.

Comment on lines +80 to +87
if (adapterName === 'ollama') {
const existing =
merged.options && typeof merged.options === 'object'
? (merged.options as Record<string, unknown>)
: undefined
if (existing && typeof existing.num_predict === 'number') return merged
merged.options = { num_predict: maxLength, ...existing }
return merged
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Honor caller-set token limits in the Ollama branch too.

This branch only treats options.num_predict as caller-owned. If the caller already provided a recognized flat token limit (for example during migration or via shared config), maxLength still injects options.num_predict and silently overrides the effective limit for Ollama.

Suggested fix
   if (adapterName === 'ollama') {
+    const callerSetFlatLimit = KNOWN_MAX_TOKENS_KEYS.some(
+      (k) => typeof merged[k] === 'number',
+    )
     const existing =
       merged.options && typeof merged.options === 'object'
         ? (merged.options as Record<string, unknown>)
         : undefined
-    if (existing && typeof existing.num_predict === 'number') return merged
+    if (callerSetFlatLimit || (existing && typeof existing.num_predict === 'number')) {
+      return merged
+    }
     merged.options = { num_predict: maxLength, ...existing }
     return merged
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai/src/activities/summarize/chat-stream-summarize.ts` around lines
80 - 87, In the Ollama branch (the adapterName === 'ollama' block) ensure we
don’t override caller-set token limits: before injecting merged.options = {
num_predict: maxLength, ...existing } check if existing already contains any
recognized flat token-limit keys (e.g., num_predict, max_tokens, max_length,
max_output_tokens) and if so return merged unchanged; only set num_predict when
none of those keys are present. Update the guard around merged.options /
existing and the assignment in that block to honor those keys instead of always
injecting num_predict.

Comment on lines +169 to +172
function firstNumber(...candidates: Array<unknown>): number | undefined {
for (const candidate of candidates) {
if (typeof candidate === 'number') return candidate
}
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reject non-finite sampling values here.

Line 171 currently accepts NaN and Infinity even though the helper promises a finite number. That can leak invalid numeric values into gen_ai.request.* span attributes and cause exporter/backend issues.

Suggested fix
 function firstNumber(...candidates: Array<unknown>): number | undefined {
   for (const candidate of candidates) {
-    if (typeof candidate === 'number') return candidate
+    if (typeof candidate === 'number' && Number.isFinite(candidate)) {
+      return candidate
+    }
   }
   return undefined
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function firstNumber(...candidates: Array<unknown>): number | undefined {
for (const candidate of candidates) {
if (typeof candidate === 'number') return candidate
}
function firstNumber(...candidates: Array<unknown>): number | undefined {
for (const candidate of candidates) {
if (typeof candidate === 'number' && Number.isFinite(candidate)) {
return candidate
}
}
return undefined
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai/src/middlewares/otel.ts` around lines 169 - 172, The helper
firstNumber currently returns any value of typeof 'number' (including NaN and
Infinity); update its check in the firstNumber function so it only returns
numbers that are finite by using Number.isFinite(candidate) (i.e., return
candidate only if typeof candidate === 'number' && Number.isFinite(candidate)),
leaving the rest of the control flow unchanged so invalid numeric values are
filtered out and undefined is returned when no finite number is found.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant