fix: a route change must not carry a model's reasoning effort with it - #39
Merged
Merged
Conversation
A desktop session picked `opencode/space-bunny-free` at effort `high`; the
ladder rewrote the route to `onegw/execution` on step one, the effort rode
along, and every turn died with `UNSUPPORTED_REASONING_EFFORT` before the
model was reached. The session log for that turn has exactly one step and one
turn/end carrying the error — the loop never spoke to a model at all.
Two halves, both required:
- `routeForStep` dropped `Route.reasoningEffort`, the field the ladder
advertises. A rung that configures an effort silently got the session's
instead. It now hands its own over (branded, since `LlmCallConfig` requires
`ReasoningEffortId`).
- The `agent/request` merge was `{ ...resolved, ...routed }`, which keeps
every key `routed` does not mention. An effort belongs to a model, so the
merge now drops the inherited one before applying the rung — the same rule
the harness's own `model-selection` applies when its selected effort is
absent.
The shipped `cordis.patch.yml` gains the second cause: its ladder pointed at
onegw role aliases while the profile routed `space-bunny-free` in the UI, and
a hand-written pi-ai model with no `reasoningEfforts` reports no reasoning
capability at all. The ladder now names the profile's own models, with a
`reasoningEffort` the first rung's model actually offers, and prices both
keys (an unpriced route falls through to `unpricedFallback`, so a mismatched
key prices a lie).
test/routing-effort.test.ts drives the real `agent/request` handler: it fails
2 of 3 assertions on the old code and passes on the new, and it covers both
halves because either alone still loses the session. Local `make test` 688
green, `make typecheck` clean. Verified against the deployed desktop profile:
`llm-pi-ai` now reports reasoning for onegw/execution and onegw/planning.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Root cause
A desktop session's durable log (
~/.dsh/sessions/--Users-linh.doan-work-harvey-freepeak--/session-d9fffefc-.../session.v4.jsonl.zstd) records the whole failure in one row:{"type": "model/selection", "data": {"provider": "deepseek-official", "model": "opencode/space-bunny-free", "reasoningEffort": "high"}} ... {"type": "turn/end", "data": {"reason": {"kind": "error", "error": { "message": "provider \"onegw\" model \"execution\" does not support reasoning effort \"high\"", "code": "UNSUPPORTED_REASONING_EFFORT"}}}}One step, one turn, no assistant message. The model was never reached.
Two independent causes, both required for that exact row:
src/plugin.ts:2368— the merge kept the inherited effort. Theagent/requesthandler returned{ ...resolved, ...routed }. Spread keeps every keyrouteddoes not mention, so the ladder's rewrite ofprovider/modelleftreasoningEffort: 'high'in place. The harness'sdsh-llmrefuses any explicit effort a model does not advertise —resolveCallWithInfothrowsUNSUPPORTED_REASONING_EFFORTbefore provider I/O, wheninfo.reasoning === undefined.onegw/execution, declared in the profile with noreasoningEfforts, resolves to exactly that.src/plugin.ts:683—routeForStepdropped the rung's own effort.Route.reasoningEffortandControllerSpec.ladder[].reasoningEffortare advertised in the type and inspec.ts, then never read. A rung that configured an effort silently inherited the session's instead, so there was no way to say "this route takes no effort" even in principle.A third, contributing cause — the reason the failure was so total rather than intermittent — is the profile itself:
~/.dsh/profiles/desktop/cordis.patch.ymldeclaresonegw/executionandonegw/planningwith noreasoningEfforts.dsh-llm-pi-ai'sresolveModelReasoningreadsentry.reasoningEfforts; omitted, it falls back tobase?.reasoning ?? falsewherebaseis the installed pi-ai catalog entry, and these role aliases exist nowhere in that catalog. Soreasoning: false— the model advertises no effort at all, and every explicit effort is refused, including'off'.The fix
Both halves in
src/plugin.ts:routeForStephands the rung's own effort over, branded viaReasoningEffortIdbecauseLlmCallConfig.reasoningEffortis a branded type.agent/requesthandler strips the inherited effort before applying the rung — the same rule the harness's ownmodel-selectionapplies when its selected effort is absent (it destructuresreasoningEffortout ofresolvedfor the identical reason).Tested at
test/routing-effort.test.ts, driving the real handler: fails 2 of 3 assertions on the old code.What I changed outside the repo
~/.dsh/profiles/desktop/cordis.patch.yml— declaredreasoningEffortsfor both onegw models (off: null, low, high, max), so the role aliases can carry an effort if the ladder is ever pointed back at them. The shipped ladder in the repo now names the profile's own models (deepseek-official/opencode/space-bunny-freeathigh, escalating toxai/grok-4.5), with prices keyed to match.Verification
make test— 688 pass, 0 failmake typecheck— clean.githooks/pre-commit --all— cleanexecution/planningnow report{"off":null,"low":"low","high":"high","max":"max"}Not verified: a live desktop turn against the rebuilt plugin. The profile's plugin copy is a
file:link to the repo'slib/, which the worktree builds separately — reinstall or merge and rebuild before driving the UI.Collateral
While re-basing a check I disturbed two old stashes in the main checkout.
stash@{0}(2026-09-29,src/dashboard-page.ts, +205 lines) is preserved as branchrescue/stash-optimize-loop-20260929.stash@{1}is intact. The working tree is back toorigin/mainforsrc/.