fix(thinking): clamp below-range requests down to the cheapest level,β¦#93335
Conversation
|
Codex review: needs real behavior proof before merge. Reviewed July 7, 2026, 1:50 AM ET / 05:50 UTC. Summary PR surface: Source 0, Tests +14. Total +14 across 2 files. Reproducibility: yes. by source inspection. Current main sorts profile levels descending and falls back to the first non-off entry, so a stored Review metrics: 1 noteworthy metric.
Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Proof guidance:
Risk before merge
Maintainer options:
Next step before merge
Maintainer decision needed
Security Review detailsBest possible solution: Land this shared helper fix after real no-off session/provider proof or a maintainer proof override, and coordinate it before or together with the companion Fireworks no-off catalog work. Do we have a high-confidence way to reproduce the issue? Yes by source inspection. Current main sorts profile levels descending and falls back to the first non-off entry, so a stored Is this the best way to solve the issue? Yes for the code fix. The shared helper is the narrowest maintainable fix point because durable model switching, gateway patching, reply execution, agent command execution, and cron execution already delegate unsupported thinking-level remaps there. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 094c0d421faf. Label changesLabel justifications:
Evidence reviewedPR surface: Source 0, Tests +14. Total +14 across 2 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
1af4e7a to
95773d1
Compare
β¦ not up resolveSupportedThinkingLevelFromProfile sorts the supported levels descending, then, when every supported level exceeds the request (e.g. a stored `off` on a profile that has no off level), fell through to `ranked.find(non-off)` β the first entry of a descending list, i.e. the most expensive level. A session carrying thinking `off` that switched onto such a model was silently remapped to maximum reasoning and persisted (src/auto-reply/reply/directive-handling.persist.ts). Use `ranked.findLast(non-off)` so a below-range request resolves to the cheapest non-off level instead. Regression test included (red before, green after). This surfaces with the first bundled no-off thinking profiles (Fireworks GPT-OSS 120B, see openclaw#92217), but the clamp lives in shared core logic, so it ships on its own.
95773d1 to
e2d9002
Compare
|
Land-ready proof for exact head
Known gap: no credentialed external-provider model-switch probe. The deterministic shared resolver and durable persistence path are covered together by focused tests; the direct Codex protocol check confirms the dependency surface. |
|
Merged via squash.
|
β¦ not up (openclaw#93335) resolveSupportedThinkingLevelFromProfile sorts the supported levels descending, then, when every supported level exceeds the request (e.g. a stored `off` on a profile that has no off level), fell through to `ranked.find(non-off)` β the first entry of a descending list, i.e. the most expensive level. A session carrying thinking `off` that switched onto such a model was silently remapped to maximum reasoning and persisted (src/auto-reply/reply/directive-handling.persist.ts). Use `ranked.findLast(non-off)` so a below-range request resolves to the cheapest non-off level instead. Regression test included (red before, green after). This surfaces with the first bundled no-off thinking profiles (Fireworks GPT-OSS 120B, see openclaw#92217), but the clamp lives in shared core logic, so it ships on its own.
β¦ not up (openclaw#93335) resolveSupportedThinkingLevelFromProfile sorts the supported levels descending, then, when every supported level exceeds the request (e.g. a stored `off` on a profile that has no off level), fell through to `ranked.find(non-off)` β the first entry of a descending list, i.e. the most expensive level. A session carrying thinking `off` that switched onto such a model was silently remapped to maximum reasoning and persisted (src/auto-reply/reply/directive-handling.persist.ts). Use `ranked.findLast(non-off)` so a below-range request resolves to the cheapest non-off level instead. Regression test included (red before, green after). This surfaces with the first bundled no-off thinking profiles (Fireworks GPT-OSS 120B, see openclaw#92217), but the clamp lives in shared core logic, so it ships on its own.
β¦ not up (openclaw#93335) resolveSupportedThinkingLevelFromProfile sorts the supported levels descending, then, when every supported level exceeds the request (e.g. a stored `off` on a profile that has no off level), fell through to `ranked.find(non-off)` β the first entry of a descending list, i.e. the most expensive level. A session carrying thinking `off` that switched onto such a model was silently remapped to maximum reasoning and persisted (src/auto-reply/reply/directive-handling.persist.ts). Use `ranked.findLast(non-off)` so a below-range request resolves to the cheapest non-off level instead. Regression test included (red before, green after). This surfaces with the first bundled no-off thinking profiles (Fireworks GPT-OSS 120B, see openclaw#92217), but the clamp lives in shared core logic, so it ships on its own.
TL;DR For session sitting at thinking
offno-off ("reasoning only") models get silently bumped to the most expensive reasoning level β and the wrong value is persisted into the session.Summary
What's wrong?
resolveSupportedThinkingLevelFromProfile(src/auto-reply/thinking.ts) clamps a requested thinking level to the closest supported one. It sorts the supported levels descending by rank, then:The second fallback only fires when every supported level exceeds the request β exactly the case of a stored
offon a profile that has noofflevel.ranked.find(non-off)returns the first entry of a descending list, i.e. the highest level. Sooffβhigh(max spend) instead ofoffβlow.Who hits it?
src/auto-reply/reply/directive-handling.persist.ts: when a session's current thinking level is not supported by a newly-selected model, it callsresolveSupportedThinkingLevel(...)and writes the result back to the session (sessionEntry.thinkingLevel = remappedThinkingLevel). So a user who keeps thinking off and switches their session onto a no-off model is silently remapped to maximum reasoning, persisted, on every later turn β never having asked for it.The fix. One word:
ranked.findβranked.findLaston the second fallback. On a descending list,findLast(non-off)is the lowest non-off level. A below-range request now clamps down to the cheapest reasoning level, never up. Mid-range requests are unaffected (they resolve via the first.findbranch). Regression test included.Real behavior proof
Behavior or issue addressed: a session at thinking
offswitched onto a model whose profile has noofflevel was remapped (and persisted) to the most expensive reasoning level instead of the cheapest.Real environment tested: OpenClaw checkout of this branch on Linux/Node 24 (Oracle Linux
aarch64); the change is pure core clamp logic, exercised by a deterministic unit test that mocks a no-off provider profile (the exact shapedirective-handling.persist.tsfeeds in).Exact steps or command run after this patch:
node scripts/run-vitest.mjs src/auto-reply/thinking.test.ts(green); to prove the regression, swap inmain's pre-fix file and run the new case βgit checkout origin/main -- src/auto-reply/thinking.ts && node scripts/run-vitest.mjs src/auto-reply/thinking.test.ts -t "clamps a below-range request down"(red), thengit checkout HEAD -- src/auto-reply/thinking.tsto restore.Evidence after fix: screenshot of the greeβred run below. With the old
ranked.findthe new test failsAssertionError: expected 'high' to be 'low'; withranked.findLastthe full suite is green (54 passed). The mocked profile is[low, medium, high](no off) and the request isoff, "reasoning-only" models.Observed result after fix:
resolveSupportedThinkingLevel({ provider, model, level: "off" })against a no-off profile returnslow(cheapest), where onmainit returnedhigh(most expensive). Mid-range clamps (adaptiveβmedium,xhighβhigh) are unchanged.What was not tested: a live end-to-end model switch needs a bundled no-off model in the catalog β fireworks/gpt-oss-120b
Tests and validation
src/auto-reply/thinking.test.ts: 54 passed, incl. the newclamps a below-range request down to the cheapest level on a no-off profilecase β proven red onmain(expected 'high' to be 'low') and green with the fix.tsgocore + core-test lanes: clean. oxfmt: clean.Risk checklist
.find(closest level at or below the request) and the final"off"default are unchanged. Off-only, binary, and full profiles are unaffected (they either contain the requested level or have a level at/below it).Current review state
What is the next action? Maintainer review.
What is still waiting on author, maintainer, CI, or external proof?
Maintainer: review/land + ordering vs the sibling PR.
CI: standard checks. No external proof outstanding.
Which bot or reviewer comments were addressed? None posted yet.
Review state guidance
Keep this as the durable state for review progress. If useful information appears in comments, fold the current next action or blocker back here so maintainers and ClawSweeper do not need to reconstruct state from comment history.