fix(agent): stop collapsing the output cap to 1 token near a full context window - #300
Merged
Merged
Conversation
…text window
output_tokens_that_fit() returned its ``min_output`` floor (1) when the
reserved input filled the window, and both callers consumed that fabricated
1 as a real budget.
The reservation is multiplicative — int(est * 1.2) + 1024 + 512 — so it
exhausts the window once the rough estimate passes ~(ctx - 1536) / 1.2, about
82.7% fill. Past that point:
* the pre-flight clamp in build_api_kwargs set max_tokens=1 on EVERY request,
with no provider error involved at all; and
* the reactive output-cap retry clamped a healthy provider-authoritative cap
down to 1 — directly contradicting the branch immediately above it, which
deliberately falls back to the provider's number precisely because "the
rough local estimate can overshoot the real request size".
Measured against a 200,000-token window: a ~170k-token prompt (85% fill) with
the provider reporting available_tokens=25,000 produced a retry cap of 1
instead of 24,936. At 80% fill the cap is 6,455; at 83% it is 1 — a cliff, not
a degradation.
max_tokens=1 is the worst kind of failure here: the provider accepts it and
returns a single truncated token, so the turn reports success while handing
back output the user cannot use. Nothing fails loudly.
Fix: report None ("no usable cap — leave max_tokens to your own budget logic")
instead of fabricating a floor. Both call sites already guard None correctly,
so neither needed a code change; the reactive one gains a comment so the floor
is not reinstated. Above 82.7% fill the pre-flight clamp now no-ops and the
provider reports its own authoritative budget on the reactive path.
Introduced in #271; the vLLM/deepseek-v4-flash-w2 convergence property that
motivated #271 is unaffected — that regime sits at ~44% fill, where the fit
stays positive and the retry still converges in one step.
Also reverts the test half of #295 for test_output_cap_retry_request_pressure_
lower_bound. That assertion was loosened to match the defect; the original
arithmetic was right and now passes for the right reason. #295's other change
(threading FailoverReason into the fallback switch) is correct and stands.
Tests: 5 cases in tests/test_output_fit_preflight_clamp.py covering the
82.7%-100% fill band at both call sites. The sweep case holds every reported
fit to the documented "server tokenizes ~15% denser" safety property, which a
fabricated floor fails by construction. Verified they fail without the
production change and pass with it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
OmarB97
added a commit
that referenced
this pull request
Aug 2, 2026
…f giving up (#302) #300 stopped output_tokens_that_fit() from fabricating a max_tokens=1 floor and made it return None once the reserved input filled the window. That was right, but it left a gap: above ~82.7% fill the pre-flight clamp in build_api_kwargs stops firing entirely, so every turn in that regime sends the full configured max_tokens, eats a provider 400, and only then gets a usable cap from the reactive retry in conversation_loop. A guaranteed extra round-trip per turn. The 1.2x factor is an uncertainty allowance for servers that tokenize denser than the ~4 chars/token heuristic, not a real token cost — but near a full window that allowance (0.2 * est) exceeds the entire remaining headroom, so holding it reports "nothing fits" while thousands of output tokens genuinely do. The documented safety property is looser than the allowance: a server tokenizing ~15% denser must still leave the clamped request in-window. At est=166,008 in a 200,000-token window the 1.2x reservation admits nothing while the 1.15x contract admits ~9,000 tokens. Fix: reserve in two tiers. The preferred 1.2x + 1024 cushion is kept whenever the window can pay for it and still leave a usable cap — below ~82.5% fill the returned value is byte-identical to before, so nothing that works today gets looser. Past that point the reservation drops to exactly what the safety contract requires (23/20, kept rational so the ceiling is exact integer arithmetic) and hands back the rest, tapering from ~9,700 tokens at 82.5% fill down to the usable floor rather than falling off a cliff. The fixed pad is dropped in the degraded tier on purpose: it is a rounding cushion for small prompts, and there the 0.15 * est term is orders of magnitude larger. None is still the answer past the 1/1.15 ceiling (~87% fill), where the contract admits nothing usable — swept over every estimate in a 200,000-token window, the largest contract headroom ever declined is 1,024 tokens, exactly the usable floor plus the window margin we deliberately hold back. min_output now defaults to _OUTPUT_FIT_MIN_USABLE (512) rather than 1, so the "never hand back a cap too small to use" rule is the default rather than something each caller has to know. Neither call site needed a code change. The #271 vLLM/deepseek-v4-flash-w2 convergence property is strengthened, not weakened: in the 82.5%-87% band the local anchor used to be None, so the reactive retry fell back to whatever the provider's max_tokens-dependent lower bound reported. It now exists and pulls the retry straight to a fitting cap in one step from any starting value. Tests: 7 cases in tests/test_output_fit_preflight_clamp.py covering the 82.7%-100% fill band at both call sites — the reported fit where the preferred cushion admits none, band coverage without a hole, the no-cliff property (wherever None is still reported, the contract really does admit nothing usable, swept in 500-token steps), the no-unusable-cap property, a pin that low fill is unchanged, the pre-flight clamp firing in the band, and one-step reactive convergence in the band. Verified 6 of the 7 fail against the previous formula; the seventh is the low-fill pin, which must pass on both. Also re-runs green: tests/run_agent/test_run_agent.py, tests/test_output_cap_parsing.py, tests/test_ctx_halving_fix.py. Co-authored-by: Omar Baradei <omar@kostudios.io> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.
What does this PR do?
output_tokens_that_fit()returned itsmin_outputfloor (1) when the reserved input filled the window, and both callers consumed that fabricated 1 as a real budget — sendingmax_tokens=1to the provider.The reservation is multiplicative —
int(est * 1.2) + 1024 + 512— so it exhausts the window once the rough estimate passes~(ctx - 1536) / 1.2, about 82.7% fill. Past that point:build_api_kwargssetmax_tokens=1on every request, with no provider error involved at all; andMeasured against a 200,000-token window:
available_tokensA cliff, not a degradation — and it applies to any window size, not just this shape.
max_tokens=1is the worst available failure mode: the provider accepts it and returns a single truncated token, so the turn reports success while handing back output the user cannot use. Nothing fails loudly.Why this approach: the fix is to report
None— "no usable cap; leavemax_tokensto your own budget logic" — instead of fabricating a floor. Both call sites already guardedNonecorrectly, so neither needed a code change.min_outputhas no external callers, so the semantic change is fully contained in one function.Regression introduced in #271. That PR's vLLM / deepseek-v4-flash-w2 convergence property is unaffected — that regime sits at ~44% fill, where the fit stays positive and the reactive retry still converges in one step.
Relationship to #295
#295 refreshed two assertions. One was right, one was not:
test_codex_content_filter_incomplete_routes_to_policy_fallback— correct, and stands.conversation_loop.py:2174really does pass the classifiedFailoverReasoninto the fallback switch since fix: make provider fallback policy explicit and visible #269; asserting it is strictly stronger.test_output_cap_retry_request_pressure_lower_bound— reverted here. That assertion was loosened tomax(1, min(expected_cap, local_fit)), which amounts to asserting whatever the code did. The original arithmetic was right; it now passes for the right reason (903, not 1).Known tradeoff
Above 82.7% fill the pre-flight clamp now no-ops, so those turns eat a provider 400 before the reactive retry corrects it with the provider's authoritative budget. That is strictly better than silently returning a 1-token answer, but it costs a round-trip. Bounding the safety reservation so the clamp degrades gracefully instead of going silent is worth a follow-up; it is a reservation-policy redesign and does not belong in a defect fix.
Related Issue
Regression from #271. Test-side follow-up to #295.
Type of Change
Changes Made
agent/model_metadata.py—output_tokens_that_fit()returnsNoneinstead of themin_outputfloor when the reserved input leaves no positive cap. Docstring now states thatNonemeans "no usable cap" (never a licence to shrink), thatmin_outputis the smallest fit worth reporting rather than a fallback value, and documents the ~82.7% fill threshold explicitly.agent/conversation_loop.py— comment only, at the reactive output-cap retry: records why theNonecase must keep the provider-authoritativesafe_outso the floor is not reinstated.tests/test_output_fit_preflight_clamp.py— newTestNearFullWindowNeverYieldsAUselessCap(5 cases) covering the 82.7–100% fill band at both call sites.tests/run_agent/test_run_agent.py— reverts the test half of test(agent): refresh two assertions that predate current retry/fallback behavior #295 fortest_output_cap_retry_request_pressure_lower_bound.How to Test
main,output_tokens_that_fit(131072, [{"role": "user", "content": "x" * 460000}])returns1. That prompt is ~115k tokens in a 131,072 window, leaving ~16k of genuine headroom.scripts/run_tests.sh tests/test_output_fit_preflight_clamp.py -qwith this PR's tests butmain'smodel_metadata.pyfails 4 of them, each on the value1:test_reports_none_rather_than_a_floor→assert 1 is Nonetest_preflight_leaves_cap_alone_instead_of_clamping_to_one→assert 1 is Nonetest_reactive_retry_keeps_provider_authoritative_cap→assert 1 > 1000test_every_reported_fit_is_a_real_fit→ reported fit does not fitscripts/run_tests.sh tests/test_output_fit_preflight_clamp.py tests/run_agent/test_run_agent.py -q→ 14 + 436 pass.test_every_reported_fit_is_a_real_fitsweeps the whole fill range and holds every non-Noneanswer to the safety property the function documents (a server tokenizing ~15% denser must still leave the request in-window). A fabricated floor fails that by construction, so a reintroduction is caught at the root rather than by pattern-matching the literal1.Checklist
Code
Full-suite status:
scripts/run_tests.shreports 34 pre-existing failures on unmodifiedorigin/mainin this environment (systemd/WSL paths on macOS, absent Codex credentials, one concurrency flake intest_base_environment.py). This branch reproduces exactly that set — no new failures.tests/agent/test_anthropic_adapter.pyfails identically (3) on both refs. The directly affected files are fully green:tests/run_agent/test_run_agent.py436/436,tests/test_output_fit_preflight_clamp.py14/14.Documentation & Housekeeping
output_tokens_that_fitcontract is the thing that was misread, so it is now explicit about whatNonemeans and whatmin_outputis notcli-config.yaml.example— N/A, no config keysCONTRIBUTING.md/AGENTS.md— N/A, no architecture or workflow change🤖 Generated with Claude Code