What happens
chat() accepts an object containing a providerOptions key, runs without warning, and never sends it. No error, no log, no debug signal — the model just behaves as if no configuration was passed.
This is caller error, and I want to be upfront about that
modelOptions is the intended and only channel, and the types say so:
TextActivityOptions (activities/chat/index.d.ts:91) declares modelOptions?: TAdapter['~types']['providerOptions'] and no top-level providerOptions.
- A literal
providerOptions: on chat() is rejected — TS2353, "Object literal may only specify known properties".
- At runtime,
activities/chat/index.js reads only options.modelOptions. The string providerOptions does not appear in the compiled activity at all (grep -c → 0, versus 9 for modelOptions).
skills/ai-core/chat-experience/SKILL.md §g already lists "HIGH: Using providerOptions instead of modelOptions" as a known mistake.
So I am not asking you to honour providerOptions.
The ask
The one shape that escapes the type system fails completely silently.
TypeScript exempts spread-in properties from excess-property checking, so this compiles:
chat({
adapter,
messages,
modelOptions: { max_tokens: 16384 },
...(thinking ? { providerOptions: thinking } : {}),
})
That is not an exotic pattern — it is the ordinary way to pass an option conditionally. In our codebase it survived two minor upgrades before anyone noticed that extended thinking had never once been enabled: no thinking field on the wire, no ThinkingPart / STEP_STARTED / STEP_FINISHED events, and reasoning that should have streamed as collapsible thinking content instead leaking into TEXT_MESSAGE_CONTENT. Nothing anywhere said the requested configuration had been dropped.
Reproduction
No API key needed; it inspects the outgoing request body against a local server.
// node repro.mjs — deps: @tanstack/ai@0.43.0, @tanstack/ai-anthropic@0.16.4
import { createServer } from 'node:http'
import { chat } from '@tanstack/ai'
import { createAnthropicChat } from '@tanstack/ai-anthropic'
let captured = null
const server = createServer((req, res) => {
const chunks = []
req.on('data', (c) => chunks.push(c))
req.on('end', () => {
captured = JSON.parse(Buffer.concat(chunks).toString())
res.writeHead(400, { 'content-type': 'application/json' })
res.end('{"type":"error","error":{"type":"invalid_request_error","message":"probe"}}')
})
})
await new Promise((r) => server.listen(0, '127.0.0.1', r))
const baseURL = `http://127.0.0.1:${server.address().port}`
const thinking = { thinking: { type: 'enabled', budget_tokens: 4096 } }
async function send(extra) {
captured = null
const adapter = createAnthropicChat('claude-sonnet-4-5', 'sk-probe', { baseURL, maxRetries: 0 })
const stream = chat({
adapter,
messages: [{ role: 'user', content: 'hi' }],
modelOptions: { max_tokens: 16384 },
...extra,
})
for await (const _ of stream) { /* drain */ }
return captured
}
// The spread is load-bearing: a literal `providerOptions:` is a compile error.
console.log('providerOptions:', JSON.stringify(await send({ ...(thinking ? { providerOptions: thinking } : {}) })))
console.log('modelOptions :', JSON.stringify(await send({ modelOptions: { max_tokens: 16384, ...thinking } })))
server.close()
Output:
providerOptions: {"model":"claude-sonnet-4-5","max_tokens":16384,"messages":[...],"tools":[],"stream":true}
modelOptions : {"model":"claude-sonnet-4-5","max_tokens":16384,"messages":[...],"tools":[],"thinking":{"type":"enabled","budget_tokens":4096},"stream":true}
The first request reaches the provider with no thinking field.
Suggested fix
Warn on unrecognised top-level keys in chat() under the existing errors debug category. There is precedent one layer down — ai-anthropic already does this for its own bag:
anthropic.mapCommonOptionsToAnthropic dropped unknown modelOptions key(s): …
The activity layer having no equivalent is the gap. A single line naming providerOptions → "did you mean modelOptions?" would have turned a multi-release silent no-op into a first-run log line.
Prior art checked
#593, #501 and #92 all concern provider options on the media activities (type extraction / model typing), not the chat path or the silent-drop behaviour. I could not find an existing issue for this.
Environment
@tanstack/ai@0.43.0, @tanstack/ai-anthropic@0.16.4, Node 22, TypeScript 5.x strict.
What happens
chat()accepts an object containing aproviderOptionskey, runs without warning, and never sends it. No error, no log, no debug signal — the model just behaves as if no configuration was passed.This is caller error, and I want to be upfront about that
modelOptionsis the intended and only channel, and the types say so:TextActivityOptions(activities/chat/index.d.ts:91) declaresmodelOptions?: TAdapter['~types']['providerOptions']and no top-levelproviderOptions.providerOptions:onchat()is rejected — TS2353, "Object literal may only specify known properties".activities/chat/index.jsreads onlyoptions.modelOptions. The stringproviderOptionsdoes not appear in the compiled activity at all (grep -c→ 0, versus 9 formodelOptions).skills/ai-core/chat-experience/SKILL.md§g already lists "HIGH: Using providerOptions instead of modelOptions" as a known mistake.So I am not asking you to honour
providerOptions.The ask
The one shape that escapes the type system fails completely silently.
TypeScript exempts spread-in properties from excess-property checking, so this compiles:
That is not an exotic pattern — it is the ordinary way to pass an option conditionally. In our codebase it survived two minor upgrades before anyone noticed that extended thinking had never once been enabled: no
thinkingfield on the wire, noThinkingPart/STEP_STARTED/STEP_FINISHEDevents, and reasoning that should have streamed as collapsible thinking content instead leaking intoTEXT_MESSAGE_CONTENT. Nothing anywhere said the requested configuration had been dropped.Reproduction
No API key needed; it inspects the outgoing request body against a local server.
Output:
The first request reaches the provider with no
thinkingfield.Suggested fix
Warn on unrecognised top-level keys in
chat()under the existingerrorsdebug category. There is precedent one layer down —ai-anthropicalready does this for its own bag:The activity layer having no equivalent is the gap. A single line naming
providerOptions→ "did you mean modelOptions?" would have turned a multi-release silent no-op into a first-run log line.Prior art checked
#593, #501 and #92 all concern provider options on the media activities (type extraction / model typing), not the chat path or the silent-drop behaviour. I could not find an existing issue for this.
Environment
@tanstack/ai@0.43.0,@tanstack/ai-anthropic@0.16.4, Node 22, TypeScript 5.x strict.