fix(agent): preflight output-token-budget gate to prevent context-overflow death-loop - #70152
fix(agent): preflight output-token-budget gate to prevent context-overflow death-loop#701527MS8 wants to merge 1 commit into
Conversation
…rflow death-loop (NousResearch#61761) Observed live on a local vLLM (Qwen3.6-35B) instance: a turn requesting a large max_tokens against an already-large prompt hit HTTP 400 (input_tokens + max_tokens > context_length). The existing reactive retry (parse available_output_tokens from the error, shrink max_tokens, retry) chased a moving target -- the input token estimate grew ~65 tokens per attempt for reasons not fully isolated, exactly offsetting the shrink and pinning the requested total at context_length + 1 on every attempt: Attempt 1: input 111,073, max_tokens 20,000 -> sum 131,073 (over by 1) Attempt 2: input 111,138, max_tokens 19,935 -> sum 131,073 (over by 1) Attempt 3: input 111,203, max_tokens 19,870 -> sum 131,073 (over by 1) Attempt 4: input 111,268, max_tokens 19,805 -> sum 131,073 (over by 1) After max_compression_attempts the whole turn was lost with no result ("Max compression attempts (3) reached."). This matches NousResearch#61761 exactly (same failure shape: sum pinned at context_length+1 every retry). This adds a preflight clamp on every attempt (not just reactively after a 400): compute the current request's token pressure using the existing request_pressure_tokens estimator, cross-checked against a conservative char/2.2 ratio, and cap max_tokens against the actual remaining budget before the call goes out. It can only tighten an already-set reactive ephemeral cap, never loosen it, so it composes safely with the existing output-cap retry path (including NousResearch#61846's proposed exponential-margin approach, which addresses the same symptom from the reactive side). Also adds debug-only instrumentation (logger.debug, never touches the payload) to help pin down the exact source of the per-attempt input growth next time this path fires -- our own investigation could not isolate it with certainty (see linked report for what was ruled out). Verified: full existing test suite green (2014 passed, 3 skipped, 0 failed) plus a new isolated repro reproducing the death-loop with the old code and confirming convergence with the new gate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Thanks for documenting the vLLM/Qwen failure shape. Current main still applies the parsed output cap only after a rejected request in Problems
Suggested changes
Automated hermes-sweeper review. |
SummaryFive PRs address related context-window overflow paths: #14858 guards against an untrusted context-tier shrink, #58869 clamps outbound output budgets and repairs vLLM lower-bound parsing, #58981 adds the same clamp while propagating custom-provider caps across agent constructors, #61228 adds custom-profile clamping plus retry-burst recovery, and #70152 implements a conversation-loop preflight clamp with diagnostics. Related pull requests
Duplicates#70152 is substantially duplicated by #58869. #58981 incorporates the same clamp and parser changes as #58869 but has distinct custom-provider cap-propagation work; #61228 overlaps on strict-endpoint clamping while adding separate retry-burst and compression behavior. Suggested consolidationClose #70152 as a duplicate of #58869; this explicitly departs from its keep_open review because the visible diffs show the same preflight-budget remedy, while #58869 places it at the final outbound layer and includes request-level coverage. Keep #58869 and #61228 open with their documented salvage paths, and require author action on #58981 to rebase onto main, remove the prohibited delegation cap surface, split OpenViking batching, and retain only the non-overlapping provider-cap propagation. Cross-PR triage: Reviewed 5 pull requests and 0 issues in this complex. Each diff was read against this issue; Assessment working set: 93 kB of PR diffs, 12 kB of issue/PR text, 6 kB of discussion (9 comments), 0 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch. |
Summary
Fixes the same failure shape as #61761 (still open, no merged fix): a turn
requesting a large
max_tokensagainst an already-large prompt hits HTTP 400(
input_tokens + max_tokens > context_length). The existing reactive retry(parse
available_output_tokensfrom the error, shrinkmax_tokens, retry)chased a moving target on my setup — the input token estimate grew ~65 tokens
per attempt for reasons I could not fully isolate, exactly offsetting the
shrink and pinning the requested total at
context_length + 1on everyattempt:
After
max_compression_attemptsthe whole turn was lost with no result(
Max compression attempts (3) reached.). Setup: local vLLM, Qwen3.6-35B,context window 131072.
Change
Adds a preflight clamp on every attempt (not just reactively after a
400): compute the current request's token pressure using the existing
request_pressure_tokensestimator, cross-checked against a conservativechar/2.2 ratio, and cap
max_tokensagainst the actual remaining budgetbefore the call goes out. It can only tighten an already-set reactive
ephemeral cap, never loosen it, so it composes safely with the existing
output-cap retry path — including #61846's exponential-margin approach,
which addresses the same symptom from the reactive side. I'd see the two as
complementary rather than competing (this one tries to avoid the 400 in the
first place; #61846 makes the reactive recovery converge faster if a 400
happens anyway).
Also adds debug-only instrumentation (
logger.debug, never touches thepayload) to help pin down the exact source of the per-attempt input growth
next time this path fires. I was not able to isolate it with certainty on my
setup — I ruled out three candidates with code-level evidence (see below) but
couldn't verify a fourth, more plausible one (a local
llm_requestmiddleware coupled to the same shrinking
max_tokensvalue) without sideeffects on a live production session, so I left it as an open question with
logging in place to answer it definitively next occurrence.
Candidates ruled out:
before a 400.
retry path.
messages.append/.insertcall site is reachable between theoutput-cap
breakand the retrycontinue; the message-repair helperonly merges/drops, never adds.
Verification
limit) reproduces the death-loop with the old code and confirms
convergence with the new gate, including an edge case where almost no
output budget remains.
Note
I run a local/customized deployment (Pythia project) with some plugins that
hook
llm_request, so I can't 100% rule out that my ~65-token/attempt driftis specific to my setup rather than universal — but the failure shape
(pinned at exactly
context_length + 1every retry) matches #61761's traceclosely enough that I think this is worth sharing regardless.