Repository navigation
fix: guard side requests against over-window payloads (#554) - #557
Conversation
Side requests (max_tokens<=200) bypass preflight by design (#388) and were forwarded verbatim regardless of payload size — a full-history side request over the upstream window is a guaranteed upstream 400 that the learned self-heal can never fix from that path (the gate fires before the self-heal read, the overflow marker was unrecognized, and the fallback estimate ran on an empty kernel view). - sideRequestGuard: estimate the raw body (CJK-aware text + image tokens) against min(resolved, learned) window minus output headroom on non-Anthropic wires; block locally with 413 side_request_payload_too_large instead of forwarding - overflow learning on side requests: recognize the llama.cpp-family exceed_context_size_error marker and parse its '(A / B > W)' window form; when the kernel view is empty (side path), fall back to the raw-body estimate so the real window is learned and the next identical request is blocked locally
📦 Built Package ArtifactBranch: Option A — Install from npm PR tag (recommended)npm install -g billion-context@pr-557Each push to this PR publishes a new version under the Option B — Download artifact
tar xzf billion-context-pr557.tgz
npm install -g packageThis comment is automatically updated on each push. |
[bot] 🏷 Review started on PR #554 ( |
[bot] 🏷 Review 完成:LGTM,可合并。 缺陷三层全部在 master 上独立复核成立,实现与方案一致,pre-flight 通过。 1. 缺陷核实(master @ e710eb0,逐条对码)
层次判断:根因是 side gate 把「小输出预算(max_tokens≤200)」与「可跳过所有输入侧护栏」混为一谈(#388 的隔离设计本身正确,但透传面缺少体积维度);①②③ 是阻止自纠的次级缺陷。PR 同时打掉根因(guard)+ 三个次级缺陷,层次到位。 2. 实现评估
3. 次要观察(非阻塞,记录在案)
4. Pre-flight(本环境实测)
结论:审核通过,建议合并。合并操作按规则由人工执行:#554 |
defaultCountTokens counts CJK per-char (~1.6x real tokenizer cost), and the guard is the only consumer where that bias hard-denies (413, retryable:false) a payload that really fits. Block at estimate >= 1.15x the effective window instead: borderline payloads forward, and a real upstream overflow 400 still teaches the learned limit. Also skip file_data (Responses input_file data-URL) like other binary carriers.
What
Side requests (
max_tokens<=200, title-gen/probes) bypass preflight by design (#388) and were forwarded verbatim regardless of payload size. A full-history side request over the upstream window is a guaranteed upstream 400 — and because title-gen/probe clients re-issue such requests, it becomes a persistent 400 storm that deadlocks large-context sessions (#554).The learned self-heal could never fix this from the side path either — three independent failures:
exceed_context_size_error(llama.cpp family) was not inCONTEXT_OVERFLOW_PATTERNS, soinspectContextOverflownever even classified it as an overflow;prepared.processedMessages, which is empty on the side path → estimate ≈ 0 < 1000 → learning skipped;reqConfig.modelContextLimit.Changes
src/server.ts—sideRequestGuard+ gate inhandle(): before forwarding a side request, estimate the RAW body (CJK-awaredefaultCountTokensover every string leaf via newestimateRawBodyTokens, plus image tokens fromimageTokensInParsedBody) against the effective window = min(resolved, learned) minus the output reservation on non-Anthropic wires. Over-window → local fail-fast 413{error:{code:"side_request_payload_too_large", retryable:false}}, NOT forwarded (mirrors the fix: honor anthropic-beta context-1m to size the context window #301 preflight fail-fast precedent). Unknown window (limit ≤ 0) → forward as before. No history truncation: the client owns its view; truncation has no verifiable semantics.src/util.ts: added/exceed[_\s]?context[_\s]?size/itoCONTEXT_OVERFLOW_PATTERNS;parseOverflowWindownow recognizes(A / B > W)and takes W (the limit after>inside the parens), e.g. 150,528 fromexceed_context_size_error (198,277 / 198,661 > 150,528).src/preflight.ts: new exportedestimateRawBodyTokens(parsed)— protocol-agnostic walk of string leaves, excluding binary-carrying keys (data/url/b64_json) which image-tokens charges separately; slight overcount of structural strings is deliberate (conservative for a fail-closed guard).src/server.ts—forward()overflow branch: hoistedparsedBody; whenprocessedMessagesis empty (side path) the conservative-window fallback now estimates from the raw body — exactly what was forwarded verbatim.Net effect: over-window side requests are blocked locally with a structured error; if an under-estimate ever lets one through and the upstream reports its real window in the overflow body, the proxy learns it and blocks the next identical request locally (covered by test).
Not done (deliberate): no config switch to disable side passthrough — once guarded, passthrough is safe; the switch adds config surface for no gain.
Tests
tests/side-request-isolation.test.ts(+5 tests):estimateRawBodyTokens(string leaves / binary-key exclusion / CJK per-char / non-object bodies);inspectContextOverflow(new marker +(A/B>W)window parse + status gate + existing-marker regression);sideRequestGuard(unknown window, boundary, learned-shrink-only, headroom reservation on OpenAI wire, image cost at boundary);Rig gained
_resetSessionsForTest()per start (clean session map), upstream hit-counting, asideErrorStatus/sideErrorBodyhook, and acompressModelContextLimitoption (operator tuning is the only way to pin the effective window below the registry/static-table resolution — see comment in the test file).Pre-flight
npm run typecheck✅npm test: 1051/1052 pass; the single failure (launcher.test.ts: resolveClientCommand codex/claude resolve to themselves) is pre-existing and environmental — verified failing identically on clean master (e710eb0): this sandbox has/usr/bin/codexon PATH so the resolver returns the absolute path while the test expects the bare name.npm run build✅E2E codex suite (
ACP_TEST_E2E) not run here — requires a reachable Responses-compatible upstream; the change touches the side-gate path and the forward() error branch, both covered by the unit/e2e tests above.