Skip to content

fix(runtime): clamp output cap to fit context window (deepseek-v4-flash-w2 overflow) - #271

Merged
OmarB97 merged 1 commit into
mainfrom
fix/deepseek-w2-output-cap-overflow
Jul 13, 2026
Merged

fix(runtime): clamp output cap to fit context window (deepseek-v4-flash-w2 overflow)#271
OmarB97 merged 1 commit into
mainfrom
fix/deepseek-w2-output-cap-overflow

Conversation

@OmarB97

@OmarB97 OmarB97 commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Problem

Local Hermes sessions against deepseek-v4-flash-w2 (vLLM on taro via the ko-nas ai-router) death-loop on "Context length exceeded: max compression attempts (3) reached" once the conversation grows past ~half the 131,072 window.

Root cause

The request carries an output cap (max_tokens) of ~65,536 — half the window — so once the prompt exceeds ~65K tokens, prompt + max_tokens overflows and vLLM returns HTTP 400. vLLM reports the offending input as a max_tokens-dependent lower bound ("prompt contains at least N input tokens", where N = max_model_len + 1 − max_tokens), because it caps tokenization at max_input_tokens + 1 for efficiency and never computes the true prompt length. The reactive output-cap retry shrinks max_tokens by the reported delta — but that delta tracks max_tokens in lockstep, so it decrements by ~65 tokens/attempt and gives up after 3, never converging.

Fix (client-side, provider-agnostic)

  • model_metadata.output_tokens_that_fit() — single source of truth for the largest output cap that keeps prompt + output inside the window, from a conservative (over-reserved) local estimate. The rough estimator assumes ~4 chars/token; dense tool/JSON transcripts tokenize closer to ~3.4, so the input is over-reserved (1.2× + pad) to stay strictly in-window despite the gap.
  • Pre-flight clamp in build_api_kwargs (all api_modes, via _ephemeral_max_output_tokens): shrink an oversized cap before sending so the 400 never fires. Only ever shrinks an explicit cap; never sets one where the caller intended the provider default; no-ops when the window is unknown.
  • Converging retry: anchor the reactive output-cap retry to the same estimate so it converges to a fitting cap in one step even when the provider reports a lower bound.

Testing

  • New tests/test_output_fit_preflight_clamp.py (9 cases): clamp fires on overflow, leaves fitting caps + provider-defaults untouched, reactive retry converges.
  • test_output_cap_parsing.py, test_ctx_halving_fix.py, custom/deepseek provider-profile suites all stay green (122 related tests).

Risk

Low. The clamp only reduces an already-oversized cap and no-ops when the window is unknown or the cap fits; behavior for every other provider is unchanged.

…sh-w2 overflow)

Local sessions against deepseek-v4-flash-w2 (vLLM on taro, fronted by the
ko-nas ai-router) death-loop on "Context length exceeded: max compression
attempts (3) reached" once the conversation grows past ~half the window.

Root cause: the request carries an output cap (max_tokens) of ~65,536 — half
the 131,072 window — so once the prompt exceeds ~65K tokens, prompt + max_tokens
overflows the window and vLLM returns HTTP 400. vLLM reports the offending input
as a max_tokens-DEPENDENT LOWER BOUND ("prompt contains at least N input
tokens", where N = max_model_len + 1 - max_tokens) because it caps tokenization
at max_input_tokens + 1 for efficiency and never computes the true prompt length.
The reactive output-cap retry parses that number and shrinks max_tokens by the
reported delta, but the delta tracks max_tokens in lockstep, so it decrements by
~65 tokens per attempt and gives up after 3 — never converging.

Fix (client-side, provider-agnostic):
- New model_metadata.output_tokens_that_fit(): single source of truth for the
  largest output cap that keeps prompt + output inside the window, using a
  conservative (over-reserved) local estimate. The rough estimator assumes
  ~4 chars/token; dense tool/JSON transcripts tokenize closer to ~3.4, so the
  input is over-reserved (1.2x + pad) to stay strictly in-window despite the gap.
- Pre-flight clamp in build_api_kwargs (all api_modes, via
  _ephemeral_max_output_tokens): shrink an oversized cap before sending so the
  400 never fires. Only ever shrinks an explicit cap; never sets one where the
  caller intended the provider default; no-ops when the window is unknown.
- Anchor the reactive output-cap retry to the same estimate so it converges to a
  fitting cap in one step even when the provider reports a lower bound.

Tests: tests/test_output_fit_preflight_clamp.py (9 cases: clamp fires on
overflow, leaves fitting caps and provider-defaults untouched, retry converges).
All existing output-cap / ctx-halving / provider-profile tests stay green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@OmarB97
OmarB97 merged commit 52e21ba into main Jul 13, 2026
18 of 19 checks passed
OmarB97 added a commit that referenced this pull request Jul 20, 2026
…sh-w2 overflow) (#271)

Local sessions against deepseek-v4-flash-w2 (vLLM on taro, fronted by the
ko-nas ai-router) death-loop on "Context length exceeded: max compression
attempts (3) reached" once the conversation grows past ~half the window.

Root cause: the request carries an output cap (max_tokens) of ~65,536 — half
the 131,072 window — so once the prompt exceeds ~65K tokens, prompt + max_tokens
overflows the window and vLLM returns HTTP 400. vLLM reports the offending input
as a max_tokens-DEPENDENT LOWER BOUND ("prompt contains at least N input
tokens", where N = max_model_len + 1 - max_tokens) because it caps tokenization
at max_input_tokens + 1 for efficiency and never computes the true prompt length.
The reactive output-cap retry parses that number and shrinks max_tokens by the
reported delta, but the delta tracks max_tokens in lockstep, so it decrements by
~65 tokens per attempt and gives up after 3 — never converging.

Fix (client-side, provider-agnostic):
- New model_metadata.output_tokens_that_fit(): single source of truth for the
  largest output cap that keeps prompt + output inside the window, using a
  conservative (over-reserved) local estimate. The rough estimator assumes
  ~4 chars/token; dense tool/JSON transcripts tokenize closer to ~3.4, so the
  input is over-reserved (1.2x + pad) to stay strictly in-window despite the gap.
- Pre-flight clamp in build_api_kwargs (all api_modes, via
  _ephemeral_max_output_tokens): shrink an oversized cap before sending so the
  400 never fires. Only ever shrinks an explicit cap; never sets one where the
  caller intended the provider default; no-ops when the window is unknown.
- Anchor the reactive output-cap retry to the same estimate so it converges to a
  fitting cap in one step even when the provider reports a lower bound.

Tests: tests/test_output_fit_preflight_clamp.py (9 cases: clamp fires on
overflow, leaves fitting caps and provider-defaults untouched, retry converges).
All existing output-cap / ctx-halving / provider-profile tests stay green.

Co-authored-by: Omar B <omar@kostudios.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
OmarB97 added a commit that referenced this pull request Aug 2, 2026
…text window (#300)

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: Omar Baradei <omar@kostudios.io>
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant