Updated after further testing. The original report claimed reasoning could not be disabled at all through this adapter. That was too strong — reasoning: { effort: 'none' } works today and is a complete workaround. The real defect is narrower, and is described below. Apologies for the noise.
Summary
Passing reasoning: { enabled: false } through modelOptions does not disable reasoning. The bundled @openrouter/sdk@0.13.20 types ChatRequestReasoning as effort + summary only, so the enabled key is stripped by the SDK's outbound schema — and what actually goes on the wire is reasoning: {}, an empty object.
That empty object is worse than sending nothing: measured on qwen/qwen3.7-flash, omitting reasoning produced 585 reasoning tokens while reasoning: {} produced 730. The caller asked for reasoning off and got more reasoning than the default, with no error and no warning.
Versions: @tanstack/ai@0.42.0, @tanstack/ai-openrouter@0.15.10, @openrouter/sdk@0.13.20.
There is a working workaround
reasoning: { effort: 'none' } disables reasoning completely and survives the adapter intact. Anyone hitting this should use it:
await chat({
adapter: openRouterText('qwen/qwen3.7-flash'),
messages: [...],
modelOptions: { reasoning: { effort: 'none' } }, // works — 0 reasoning tokens
})
ChatRequestEffort includes a None: "none" member, so this is fully expressible under the current types. It is just not obvious that effort: 'none' is the off switch when the provider's own API documents enabled: false.
Evidence
What reaches the wire
Patching globalThis.fetch before importing the adapter, then calling chat() through the real openRouterText path:
modelOptions.reasoning |
body on the wire |
| omitted |
(no reasoning key) |
{ effort: 'none' } |
{"reasoning":{"effort":"none"}} |
{ enabled: false } |
{"reasoning":{}} ← key stripped, empty object sent |
Confirmed independently against the SDK's outbound schema in isolation:
ChatRequest$outboundSchema.parse({ ..., reasoning: { effort: 'none' } }) -> {"effort":"none"}
ChatRequest$outboundSchema.parse({ ..., reasoning: { enabled: false } }) -> {}
Behavioural effect
Direct to OpenRouter, qwen/qwen3.7-flash, identical prompt, max_tokens: 300:
| request |
reasoning tokens |
completion tokens |
reasoning omitted |
585 |
750 |
reasoning: { effort: 'none' } |
0 |
236 |
reasoning: { effort: 'low' } |
429 |
517 |
reasoning: { enabled: false } |
0 |
247 |
reasoning: {} ← what the adapter sends |
730 |
882 |
So enabled: false is correct at the API level; it simply never survives the SDK schema, and the empty object it degrades into re-enables reasoning.
(Single sample per row — the 0 vs. 585/730 split is far outside sampling noise, but the exact counts will vary.)
The type
node_modules/@openrouter/sdk/esm/models/chatrequest.d.ts:
export type ChatRequestReasoning = {
effort?: ChatRequestEffort | null | undefined;
summary?: ChatReasoningSummaryVerbosityEnum | null | undefined;
};
reasoning is a caller-settable key (ai-openrouter/src/text/text-provider-options.ts, in OpenRouterBaseOptions) and flows through mapOptionsToRequest via ...restModelOptions, so the adapter passes it along faithfully. The loss happens inside the SDK's zod validation.
Suggestions
- Don't send an empty
reasoning object. If schema validation strips every key from reasoning, omitting the field entirely is strictly better than sending {} — it would at least fall back to the model default (585) rather than 730. This is the part that is actionable purely inside this repo, independent of the SDK.
- Document
effort: 'none' as the way to disable reasoning through this adapter. It is not discoverable from the type, and OpenRouter's own docs point users at enabled: false.
- Consider surfacing stripped
modelOptions keys. A caller-set key vanishing silently inside SDK validation cost a bisect against raw HTTP to find. A dev-mode warning when a modelOptions key does not survive would have made it immediate. This generalises beyond reasoning.
- Optionally, accept
enabled and translate it to effort: 'none' at the adapter boundary, so code written against OpenRouter's documented API behaves as expected.
Notes on what I verified
- Verified in source: the
ChatRequestReasoning / ChatRequestEffort definitions, and that reasoning reaches mapOptionsToRequest as a caller-settable key.
- Verified by direct execution: the outbound-schema parse results, the on-the-wire bodies through the real adapter, and every row of the token table.
- Not verified: whether other providers' reasoning models behave the same way; all measurements are
qwen/qwen3.7-flash.
Summary
Passing
reasoning: { enabled: false }throughmodelOptionsdoes not disable reasoning. The bundled@openrouter/sdk@0.13.20typesChatRequestReasoningaseffort+summaryonly, so theenabledkey is stripped by the SDK's outbound schema — and what actually goes on the wire isreasoning: {}, an empty object.That empty object is worse than sending nothing: measured on
qwen/qwen3.7-flash, omittingreasoningproduced 585 reasoning tokens whilereasoning: {}produced 730. The caller asked for reasoning off and got more reasoning than the default, with no error and no warning.Versions:
@tanstack/ai@0.42.0,@tanstack/ai-openrouter@0.15.10,@openrouter/sdk@0.13.20.There is a working workaround
reasoning: { effort: 'none' }disables reasoning completely and survives the adapter intact. Anyone hitting this should use it:ChatRequestEffortincludes aNone: "none"member, so this is fully expressible under the current types. It is just not obvious thateffort: 'none'is the off switch when the provider's own API documentsenabled: false.Evidence
What reaches the wire
Patching
globalThis.fetchbefore importing the adapter, then callingchat()through the realopenRouterTextpath:modelOptions.reasoningreasoningkey){ effort: 'none' }{"reasoning":{"effort":"none"}}{ enabled: false }{"reasoning":{}}← key stripped, empty object sentConfirmed independently against the SDK's outbound schema in isolation:
Behavioural effect
Direct to OpenRouter,
qwen/qwen3.7-flash, identical prompt,max_tokens: 300:reasoningomittedreasoning: { effort: 'none' }reasoning: { effort: 'low' }reasoning: { enabled: false }reasoning: {}← what the adapter sendsSo
enabled: falseis correct at the API level; it simply never survives the SDK schema, and the empty object it degrades into re-enables reasoning.(Single sample per row — the 0 vs. 585/730 split is far outside sampling noise, but the exact counts will vary.)
The type
node_modules/@openrouter/sdk/esm/models/chatrequest.d.ts:reasoningis a caller-settable key (ai-openrouter/src/text/text-provider-options.ts, inOpenRouterBaseOptions) and flows throughmapOptionsToRequestvia...restModelOptions, so the adapter passes it along faithfully. The loss happens inside the SDK's zod validation.Suggestions
reasoningobject. If schema validation strips every key fromreasoning, omitting the field entirely is strictly better than sending{}— it would at least fall back to the model default (585) rather than 730. This is the part that is actionable purely inside this repo, independent of the SDK.effort: 'none'as the way to disable reasoning through this adapter. It is not discoverable from the type, and OpenRouter's own docs point users atenabled: false.modelOptionskeys. A caller-set key vanishing silently inside SDK validation cost a bisect against raw HTTP to find. A dev-mode warning when amodelOptionskey does not survive would have made it immediate. This generalises beyondreasoning.enabledand translate it toeffort: 'none'at the adapter boundary, so code written against OpenRouter's documented API behaves as expected.Notes on what I verified
ChatRequestReasoning/ChatRequestEffortdefinitions, and thatreasoningreachesmapOptionsToRequestas a caller-settable key.qwen/qwen3.7-flash.