fix(server): a thinking request always leaves room to think
Merged
DA
6h ago+63+63+01234567890123456789-18-18-01234567890123456789
The reply reserve (hard_limit_reply_budget, 4096 on the Qwen cards) came off max_tokens in full. A request that asked for thinking with max_tokens 4096 or less got none: the phase-1 cap was 4096 − 4096 = 0, and the budget hook closed thinking on the first token. tool-eval-bench sends max_tokens: 4096, so reasoning_effort had no effect there, and neither did an Anthropic thinking budget with a small max_tokens.
Change
reply_reserve_within(reserve, max_tokens)incommon/thinking_budget.h: the reserve takes at most half ofmax_tokens. Withmax_tokens4096 on a 4096 card, the reply gets 2048 and thinking up to 2048.- One helper for the three places that size the envelope:
apply_request_reasoning(the phase-1 cap),prepare_generation_inputs(the single-request path) and the scheduler's admission. Each subtracted the full reserve on its own before. - Requests with
max_tokensabove twice the reserve (8192 on a 4096 card) are unchanged. docs/specs/thinking-budget.md§3.5, §4.4 and §5.3 describe the new reserve.
Measured
Qwen 3.8 27B UD-IQ4_XS on the R9700, with its DFlash2 drafter:
| Request | Before | After |
|---|---|---|
reasoning_effort: high, max_tokens: 4096 | 1 reasoning token (the forced </think>) | 91 reasoning tokens; the model closed thinking itself |
Messages API, thinking.budget_tokens: 2048, max_tokens: 3000 | no thinking block | thinking block, then the answer |
tool-eval-bench --hardmode (97 scenarios, seed 42) with reasoning_effort: high | 88 (no thinking ran) | 91 |
The bench was measured on this change applied to 54132cac; after the rebase onto af91ead7 the unit tests were rerun.
Tests
test_reply_reserve_leaves_room_to_think: the helper, and the effort cap withmax_tokens1000, 150 and 100 on a 100-token reserve.test_server_unit: 1002 passed, 0 failed (HIP build). The change is host C++ only.
🤖 Generated with Claude Code