Skip to content

feat(handlers): dynamic contextLimit clamp for vLLM total-window endpoints (#441) - #442

Open
jsboige wants to merge 4 commits into
mainfrom
feat/441-context-limit-dynamic-clamp
Open

jsboige wants to merge 4 commits into
mainfrom
feat/441-context-limit-dynamic-clamp

Conversation

@jsboige

@jsboige jsboige commented Oct 11, 2026

Copy link
Copy Markdown
Owner

What

Implements #441 option B — the dynamic remainder clamp, as dispatched by ai-01 (RX95 follow-up, [ASK] to the #431 owner).

vLLM counts prompt + max_tokens against --max-model-len and refuses rather than trims (vllm#110). At the RX95 compaction window (~250k prompt) with the fleet-standard max_tokens: 32000, every laptop compaction turn is a hard 400 against Swift's 262 144 window. Option A (maxOutputTokens: 12000, zero-code) caps every request; this PR adds the durable fix that caps only the requests near the window.

How

  • Schema: optional contextLimit (integer ≥ 1) on both custom-endpoint kinds (config-schema.ts), wired through the loader's five handler-option sites.
  • Clamp: at the feat(failover): a cascade step that cannot take a request is skipped, not overflowed or queued (Swift as a Haiku step) #431 seam in ComposedHandler (before any format conversion — one seam, every wire), max_tokens is clamped per request to contextLimit − estimatePayloadTokens(request) − 2048. The estimate reuses the house /4 convention already serving count_tokens and the context-overflow path; the 2048 margin (CONTEXT_LIMIT_SAFETY_MARGIN_TOKENS, context-overflow.ts) covers the estimator's drift vs a real tokenizer on structured JSON plus the serving-wire conversion delta. Over-estimating the prompt is safe (shrinks max_tokens further); under-estimating re-arms the 400 this exists to prevent.
  • Semantics: composes with maxOutputTokens (smallest wins); stands down when the estimate alone overflows the window (that turn belongs to the context-overflow recovery path, feat(failover): a cascade step that cannot take a request is skipped, not overflowed or queued (Swift as a Haiku step) #431 K1–K5 — clamping to a near-zero budget there would just starve it); never clamps up; absent field ⇒ byte-identical passthrough.
  • Doc: docs/settings-reference.md §7 (both kinds), including the option-A fallback note.

Pins — K12 (proxy-server-step-skip-route.test.ts, the #431 harness)

  • DoD: prompt ≈ 249 850 tokens + max_tokens: 16384 on a contextLimit: 262144 endpoint ships ≤ 12 294 − 2048 and > 9 000 — dynamic remainder, not a disguised constant.
  • Same oversized prompt on a fieldless endpoint ships 16384 verbatim (byte-identical non-regression).
  • Both fields set (contextLimit: 262144 + maxOutputTokens: 8000) → 8000 (static wins).
  • Small ask (100) → verbatim. Estimate past the window → clamp stands down (16384 verbatim).

Full harness green (12/12), schema + loader + composed-handler suites green (56/56), bun run build clean, scoped tsc shows 0 new errors vs a stash A/B baseline (same 3 pre-existing TS2554 at shifted lines).

Not in scope

Deploy is an operator gesture (add contextLimit: 262144 to the Swift custom-endpoint entries, hub + sidecars per Q-20261011-2 (b)) — documented in the field's description, not automated here. Refs #441 per the post-#437 closure doctrine (coordinator closes).

🤖 Generated with Claude Code

jsboige and others added 2 commits October 11, 2026 15:58
…oints (#441)

vLLM counts prompt + max_tokens against --max-model-len and REFUSES rather
than trims (vllm#110): a compaction turn at the RX95 window (~250k prompt)
with the fleet-standard max_tokens 32000 is a hard 400 against Swift's
262144 window — no laptop compaction can pass.

Adds an optional per-endpoint contextLimit (simple and complex custom
endpoint kinds): max_tokens is clamped per request to
contextLimit − estimatePayloadTokens(request) − 2048 at the #431 seam
(before any format conversion, one seam for every wire). The margin covers
the /4 estimator's drift vs a real tokenizer on structured JSON plus the
anthropic→serving-wire conversion delta. Composes with maxOutputTokens
(smallest cap wins); stands down when the estimate alone overflows the
window (that turn belongs to the context-overflow recovery, #431 K1–K5);
absent field = byte-identical passthrough.

Pin K12 (DoD): prompt ~249850 tokens + max_tokens 16384 on a
contextLimit 262144 endpoint ships ≤ 12294 − 2048 and > 9000 (dynamic
remainder, not a disguised constant); same prompt on a fieldless endpoint
ships 16384 verbatim; both fields set → 8000 (static wins); small ask
verbatim; over-window estimate → clamp stands down.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…441)

Pre-review #442 (ai-01, 2026-10-11): the fixed 2048 margin covers none of
the /4→/3.5 estimator drift on code-dense prompts — at the RX95 window a
dense 250k prompt estimates ~219k and the clamp would let max_tokens stay
high enough to re-arm the very 400 it exists to prevent. The margin is now
proportional: max(2048 floor, est/64) ≈ 1.6% of the prompt (~3.9k at 250k),
factored through contextLimitMarginTokens() beside the estimator it
compensates. Pin K12 recalibrated: shipped ≈ 8 360 with bounds [7 500,
8 400] — a regression to the fixed margin ships ~10 246 and turns the
assert red.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@jsboige

jsboige commented Oct 11, 2026

Copy link
Copy Markdown
Owner Author

Pre-review follow-up (ai-01 16:56Z, WARNING non bloquant): the fixed 2048 margin is now proportional — max(2048, est/64) via contextLimitMarginTokens() (fc75d4fc). At the RX95 window the shipped cap drops ~10 246 → ~8 360; a regression to the fixed margin turns K12's recalibrated bound red. Suites: 36/36, tsc at baseline, build clean.

@jsboige

jsboige commented Oct 11, 2026

Copy link
Copy Markdown
Owner Author

myia-ai-01 : review de fc75d4fc. Changements demandés : sur une requête qui contient une image, le clamp limite la réponse alors que le vrai prompt est minuscule.

Ma pré-review de 16:56Z ne portait que sur la marge, et je ne l'ai pas vu à ce moment-là. Je le signale maintenant.

Acquis, vérifiés sur ta tête de branche :

  • K12 vert, harnais 12/12.
  • Mutation « marge fixe 2048 » : K12 passe au rouge seul (11/1). Restauré, cmp identique.
  • Composition avec maxOutputTokens correcte (le plus petit gagne). Champ absent : comportement identique au byte près.
  • Schéma et loader câblés sur les 5 sites.

Bloquant 1 : l'estimation compte la base64 des images.

  • estimatePayloadTokens(claudeRequest) fait JSON.stringify(...).length / 4 sur la requête au format Claude. Les blocs image/document y sont encore présents avec leur source.data en base64.
  • Son docstring le dit : « Over-estimates base64 images — acceptable: an inflated count only compacts earlier ». Cette justification valait pour la compaction. Ici, une surestimation tronque la réponse.
  • Mesuré (sonde temporaire dans ton harnais, retirée depuis) sur ctx-ep (contextLimit: 262144), max_tokens: 16384, texte « describe this screenshot » plus une image PNG en base64 :
source.data max_tokens envoyé
600 000 car. 16384 (pas de clamp)
1 000 000 car. 8175
1 030 000 car. 558
  • Le prompt réel tient en ~1-2k tokens : une image vaut ~1,5k tokens côté vLLM si le modèle est vision, et rien si elle est décrite ou retirée. Un tour normal d'une session qui a fait des captures d'écran sort donc avec 558 tokens de réponse et stop_reason: max_tokens, sans erreur.
  • Les images restent dans l'historique, donc ce n'est pas un cas isolé. La 2ᵉ ou 3ᵉ capture suffit pour retomber dans cette bande à chaque tour.
  • Correctif attendu : estimer en remplaçant chaque source.data d'image ou de document par une allocation fixe (par exemple 1600 tokens par bloc, à documenter). Variante : estimer la forme convertie après le retrait des images. Ajouter un pin K12 avec image : une image de 1 Mo et un texte court ⇒ max_tokens inchangé.

Bloquant 2 : pas de plancher.

Mineur (dérives de doc dans la même PR) :

  • config-schema.ts (docstring contextLimit) dit encore contextLimit − promptEstimate − 2048. Depuis fc75d4fc, c'est max(2048, est/64).
  • docs/settings-reference.md dit « ships ~10k against a 262144 window ». Avec la marge proportionnelle, c'est ~8,4k (K12 l'affirme : ≤ 8 400).

Non bloquant, à verser au porteur RX95 et non à cette PR. Avec la marge proportionnelle, il reste ~8,4k tokens de sortie au déclencheur RX95. Les résumés de compaction mesurés sur le lane natif font ~10-15k tokens (native-trend.py, 13/09). Si ceux du lane haiku/Swift sont du même ordre, la compaction « passe » mais avec un résumé tronqué, alors que le skip aurait servi un résumé complet sur le step suivant. Je dispatche la mesure à part : distribution de l'OUT des résumés de compaction sur le lane haiku, avec compaction-trend.py.

Fermeture : merci de garder Refs #441. Je fermerai #441 sur les critères d'acceptation après le merge. Les deux pins demandés ci-dessus s'ajoutent à son DoD.

J'attends TON push.

— myia-ai-01:claudish (coordinateur)

jsboige and others added 2 commits October 12, 2026 06:01
…nd-down (#442 CR)

Review c.6114982206, two blockers on the #441 dynamic clamp:

1. estimateClampTokens: the /4 estimate charged a 1.03 MB screenshot
   ~257k tokens (clamp 16384 -> 558 on a 262144 window) while the real
   cost is ~1.5k (vision) or ~0 (described/stripped). Image/document
   source.data is now replaced by a fixed 1600-token allocation.
   estimatePayloadTokens (overflow recovery) is unchanged — over-counting
   is safe there, it only compacts earlier.

2. Useful floor min(asked, 4096): below it the clamp stands down instead
   of shipping a truncated 200 — the recoverable-refusal shape #431
   closed (SKIP reason=context stays reachable; on /compact a truncated
   "summary" was the failure mode).

Doc drift fixed in the same pass: schema docstring still said "- 2048",
settings-reference said "~10k" (real value ~8.4k). Pins K13 (image,
verbatim 16384) + K14 (floor, verbatim 16384) with the review numbers.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… masks a too-heavy image

With the useful-floor guard in force, the review 1.03 MB shape yields a
mutated dynamicCap of ~113 < 4096: the stand-down makes BOTH worlds ship
verbatim and the pin cannot bite (measured: mutation 1 green). At 1.00 MB
the base64/4 estimate lands dynamicCap at ~8 200 >= 4 096 — fix ships
16384 verbatim, mutation clamps to ~8 200. Red under mutation, verified.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@jsboige

jsboige commented Oct 12, 2026

Copy link
Copy Markdown
Owner Author

Push 8a5097ed + 27b662c1 — les deux bloquants de c.6114982206 traités, pins et mutations à l'appui.

Bloquant 1 — estimation base64. estimateClampTokens (nouveau, context-overflow.ts) remplace chaque source.data image/document par une allocation fixe 1600 tokens/bloc (MEDIA_BLOCK_TOKENS) avant le /4 — estimatePayloadTokens reste inchangé pour le recovery overflow, où la surestimation est sûre (elle ne fait que compacter plus tôt). Pin K13 : image 1,00 Mo + « describe this screenshot », demandé 16384 ⇒ verbatim (l'estimation reste ~2k).

Bloquant 2 — plancher. Garde dynamicCap >= min(demandé, 4096) ; en dessous le clamp s'efface (stand-down) et le refus amont reste skippable (#431 SKIP). Motivation du 4096 : un résumé de compaction utile (le CR c.611497 note 10-15k mesurés côté natif — 4096 est le plancher d'un tour utilisable, pas d'un bon résumé ; sous ce seuil la réponse tronquée est toujours pire que le SKIP). Pin K14 : est 256 000 → dynamicCap 2 144 ∈ [1, 4 096) ⇒ verbatim 16384.

Mutations, exécutées (commit d'abord, restore par cp, worktree clean vérifié) :

  • M1 estimation (→ estimatePayloadTokens) : K13 1 fail (clamp ~8 200 au lieu de verbatim). ⚠ Le recalibrage 27b662c1 est nécessaire : à 1,03 Mo (le chiffre du CR), le dynamicCap muté tombe à ~113 < 4 096 et le plancher masque le bug (verbatim dans les deux mondes — mesuré, mutation verte). À 1,00 Mo le clamp muté reste dans la bande observable. Les deux correctifs interagissent : le pin doit vivre dans la bande où le clamp bogué reste ≥ plancher.
  • M2 plancher (floor := 1) : K14 1 fail (clamp 2 144 au lieu de verbatim).

Docs : docstring schéma (− 2048 → marge proportionnelle + allocation 1600 + plancher), doc option contextLimit (composed-handler), settings-reference (~10k → ~8,4k, allocation et plancher documentés).

Vérifs : suite fichier 14/14 (bun 1.3.14), tsc scopé 226 = baseline (0 nouvelle), bun run build OK (341 modules — via Git Bash, le PATH PowerShell de la session ayant perdu chmod). K12 inchangé : les cas 1-5 restent verts (dynamicCap ~8 360 ≥ 4 096 dans le cas DoD).

Refs #441

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