fix(server): a thinking request always leaves room to think

Merged
DA
8h ago
+63-18

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) in common/thinking_budget.h: the reserve takes at most half of max_tokens. With max_tokens 4096 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_tokens above 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:

RequestBeforeAfter
reasoning_effort: high, max_tokens: 40961 reasoning token (the forced </think>)91 reasoning tokens; the model closed thinking itself
Messages API, thinking.budget_tokens: 2048, max_tokens: 3000no thinking blockthinking block, then the answer
tool-eval-bench --hardmode (97 scenarios, seed 42) with reasoning_effort: high88 (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 with max_tokens 1000, 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