You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
在 agent-default-model 里选了 OpenRouter 的推理模型(我用的是 qwen/qwen3.8-max),
并且没有设置 reasoningEffort 时,每一次请求都会失败: 400 {"message":"Reasoning is mandatory for this endpoint and cannot be disabled."}。
dsh: INVALID_REQUEST: 400: {"message":"Reasoning is mandatory for this endpoint and cannot be disabled.","code":400,"metadata":{"provider_name":null}}
Adding reasoningEffort: high to the same block makes it work, so the model and the key are fine.
The failure only happens when no effort is selected, which is the documented way to ask for the
provider's own default.
Root cause
Line numbers are on a66e4702 (0.1.2-rc.1), pi-ai 0.84.2 as pinned by packages/llm/llm-pi-ai/package.json.
packages/core/agent-default-model/src/index.ts:29 states the promise: reasoningEffort is
"Adapter-owned reasoning effort, or provider/default behavior when absent."
With nothing selected, packages/llm/llm-pi-ai/src/adapter.ts:339 resolves the level to undefined, and adapter.ts:120 turns that into "no reasoning option" on the pi-ai call at adapter.ts:375. So far so good.
pi-ai does not read an absent option as "say nothing". In @earendil-works/pi-ai/dist/api/openai-completions.js:631, the OpenRouter branch is:
An absent option means "disable reasoning", and the wire spelling comes from thinkingLevelMap.off. null there is pi-ai's "send nothing at all". The DeepSeek branch at openai-completions.js:615 does the same with thinking: { type: "disabled" }.
The Harness never fills that map for a catalog model. packages/llm/llm-pi-ai/src/catalog.ts:674
returns { reasoning: base?.reasoning ?? false } and lets the installed entry's own map ride
along, and in pi-ai 0.84.2 the OpenRouter catalog has no map for 255 of its 259 reasoning
models, qwen/qwen3.8-max among them.
So an unset effort silently became "disable reasoning", and an endpoint that mandates reasoning
answers 400.
This is not only a stale-catalog problem. pi-ai 0.84.4 adds thinkingLevelMap.off: null for this
model, so bumping the pin hides it for qwen/qwen3.8-max, but 99 OpenRouter reasoning models in
0.84.4 still carry no off entry, and every hand-declared gateway model in settings.yaml starts
in the same state. The Harness makes the "provider default" promise, so I think the Harness should
be the one that keeps it.
One request-time helper in packages/llm/llm-pi-ai/src/adapter.ts. When a request selects no
effort and the model declares no off wire value, it is dispatched under a model whose off is
pinned to null, which is pi-ai's "send nothing":
A model whose entry names an off value keeps deciding its own no-effort request.
Selecting Off explicitly still asks the endpoint to stop thinking.
The offered effort list is untouched, because the pin is per request, not in the catalog.
Two tests in packages/llm/llm-pi-ai/tests/adapter.spec.ts, in the style of the ones around them:
one drives a real OpenRouter catalog model through the mock server and asserts the request carries
no reasoning field, the other asserts a declared off: none still reaches the wire.
How I verified it
Same checkout, same pi-ai 0.84.2, same settings with no reasoningEffort, one prompt through dsh --profile headless:
adapter.ts from a66e4702: dsh: INVALID_REQUEST: 400: {"message":"Reasoning is mandatory for this endpoint and cannot be disabled.",...}
adapter.ts with the fix: normal answer, model reasoning visibly on.
The new unit test fails on the parent commit with expected { model: 'aion-labs/aion-2.0', … } to not have property "reasoning" and passes with the
fix. packages/llm/llm-pi-ai is green (278 tests), and the package typechecks and lints clean.
#4555 is a different code path (a reasoning effort not forwarded to subagents), but it lands in the
same place: a request whose reasoning setting is not what the user selected. It may be worth
checking every seam where an effort is resolved, not only these two.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
中文摘要
在
agent-default-model里选了 OpenRouter 的推理模型(我用的是qwen/qwen3.8-max),并且没有设置
reasoningEffort时,每一次请求都会失败:400 {"message":"Reasoning is mandatory for this endpoint and cannot be disabled."}。原因不在 OpenRouter,而在 Harness 这边。不选 effort 的含义是“用服务商自己的默认值”,
但 pi-ai 把“没有 reasoning 选项”理解成“关闭推理”,于是发出
reasoning: { effort: "none" }。这个 dispatch 读的是model.thinkingLevelMap.off,而 pi-ai 0.84.2 的 OpenRouter 目录里,259 个推理模型中有 255 个根本没有这一项。
我的修复:当这一次请求没有选择任何 effort、而且模型本身没有声明
off的线上写法时,就把
off固定为null(pi-ai 里null表示“什么都不发”)。声明了off值的模型行为不变,用户显式选择 Off 时也照旧发送关闭指令。分支和验证过程见下。
English below.
What happens
With this in
~/.dsh/settings.yamland no reasoning effort anywhere:every request fails:
Adding
reasoningEffort: highto the same block makes it work, so the model and the key are fine.The failure only happens when no effort is selected, which is the documented way to ask for the
provider's own default.
Root cause
Line numbers are on
a66e4702(0.1.2-rc.1), pi-ai0.84.2as pinned bypackages/llm/llm-pi-ai/package.json.packages/core/agent-default-model/src/index.ts:29states the promise:reasoningEffortis"Adapter-owned reasoning effort, or provider/default behavior when absent."
With nothing selected,
packages/llm/llm-pi-ai/src/adapter.ts:339resolves the level toundefined, andadapter.ts:120turns that into "noreasoningoption" on the pi-ai call atadapter.ts:375. So far so good.pi-ai does not read an absent option as "say nothing". In
@earendil-works/pi-ai/dist/api/openai-completions.js:631, the OpenRouter branch is:An absent option means "disable reasoning", and the wire spelling comes from
thinkingLevelMap.off.nullthere is pi-ai's "send nothing at all". The DeepSeek branch atopenai-completions.js:615does the same withthinking: { type: "disabled" }.The Harness never fills that map for a catalog model.
packages/llm/llm-pi-ai/src/catalog.ts:674returns
{ reasoning: base?.reasoning ?? false }and lets the installed entry's own map ridealong, and in pi-ai 0.84.2 the OpenRouter catalog has no map for 255 of its 259 reasoning
models,
qwen/qwen3.8-maxamong them.So an unset effort silently became "disable reasoning", and an endpoint that mandates reasoning
answers 400.
This is not only a stale-catalog problem. pi-ai 0.84.4 adds
thinkingLevelMap.off: nullfor thismodel, so bumping the pin hides it for
qwen/qwen3.8-max, but 99 OpenRouter reasoning models in0.84.4 still carry no
offentry, and every hand-declared gateway model insettings.yamlstartsin the same state. The Harness makes the "provider default" promise, so I think the Harness should
be the one that keeps it.
The fix
Branch: https://github.com/kaiomp/deepseek-harness/tree/fix/openrouter-reasoning-off-default
Commit: kaiomp@0a68052
One request-time helper in
packages/llm/llm-pi-ai/src/adapter.ts. When a request selects noeffort and the model declares no
offwire value, it is dispatched under a model whoseoffispinned to
null, which is pi-ai's "send nothing":What does not change:
offvalue keeps deciding its own no-effort request.Two tests in
packages/llm/llm-pi-ai/tests/adapter.spec.ts, in the style of the ones around them:one drives a real OpenRouter catalog model through the mock server and asserts the request carries
no
reasoningfield, the other asserts a declaredoff: nonestill reaches the wire.How I verified it
Same checkout, same pi-ai 0.84.2, same settings with no
reasoningEffort, one prompt throughdsh --profile headless:adapter.tsfroma66e4702:dsh: INVALID_REQUEST: 400: {"message":"Reasoning is mandatory for this endpoint and cannot be disabled.",...}adapter.tswith the fix: normal answer, model reasoning visibly on.The new unit test fails on the parent commit with
expected { model: 'aion-labs/aion-2.0', … } to not have property "reasoning"and passes with thefix.
packages/llm/llm-pi-aiis green (278 tests), and the package typechecks and lints clean.Relation to #4555
#4555 is a different code path (a reasoning effort not forwarded to subagents), but it lands in the
same place: a request whose reasoning setting is not what the user selected. It may be worth
checking every seam where an effort is resolved, not only these two.
All reactions