fix(core): recover model requests rejected with HTTP 413 via one-shot compaction - #10408
Conversation
… compaction When an OpenAI-compatible endpoint sits behind a reverse proxy with a request-body byte limit, a session below the token-based auto-compaction threshold can still have its serialized request rejected with a bare HTTP 413 (often an HTML error page). The token-wording overflow detector never matched it, reactive compression never fired, and every subsequent prompt re-sent the same oversized history, leaving the session permanently broken (#10380). - Add a model-request-scoped 413 detector (utils/request-payload-error.ts) consulted only by llm-chat's send catch, so upload/file 413s are never misclassified as context overflow. - Route a payload-overflow 413 into the existing one-shot reactive compression path and retry once. - Truncate oversized tool-result/text payloads in the compaction side-query input on this path so the side-query itself fits under the same gateway byte limit. - Surface an actionable "start a new session" error when recovery cannot shrink the request under the limit, preserving the HTTP status. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
|
|
Thanks for the PR — fifth triage pass. The head moved once since the last pass:
Moving on to code review. 🔍 中文说明感谢贡献!这是第五次 triage。head 自上一轮以来移动了一次:
进入代码审查。🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewFifth pass — new head B1 — post-compaction accounting on the 413 path: fixed. B2 — transient side-query failures: fixed, in the measured M12 shape. Main's try/catch around the cold side-query ( Surrogate pairs: fixed correctly. Tests pin the fixes. The new Non-blocking, carried over: the Test Plan's combined sequenceDiagram
participant P1 as Prompt send (llm-chat)
participant P2 as Gateway with byte limit
participant P3 as ChatCompressionService
P1->>P2: model request (oversized bytes, under token threshold)
P2-->>P1: HTTP 413, often an HTML page
Note over P1: getRequestPayloadTooLargeInfo<br>cause-aware walk, model-request path only
P1->>P3: tryCompress force=true, requestPayloadTooLarge
Note over P3: slim side-query text to 4000 chars<br>skip cache sharing, suppress file and image restoration<br>account from the local estimator, not slimmed usage
alt side-query succeeds
P3-->>P1: COMPRESSED, triggerReason payload_overflow
P1->>P2: retry once with compacted history
alt retry fits
P2-->>P1: 200, turn completes
else second 413, NOOP, or spent one-shot
P1-->>P1: actionable error by outcome, status 413 kept
end
else side-query API failure (returned, not thrown) or throw
P3-->>P1: COMPRESSION_FAILED_API_ERROR
P1-->>P1: transient - propagate the original 413<br>next prompt gets a fresh one-shot
end
Files changed (12, against main)
Testing evidenceLocal invocation, but no PR code was built or executed by this review — the gate's static-review rule applies, and the live-behaviour lanes are the isolated ones named below. Evidence carried: the PR's own CI fetched via the API (table below, settled), the three sandboxed CI on Final CI results for
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。
Real-scenario testing (Stage 2c): N/A — the behaviour under review only manifests behind a byte-limited reverse proxy, which cannot be staged against the real API in a tmux session, and this gate does not execute PR-derived code. The live-behaviour evidence belongs to the isolated lanes: 中文说明代码审查第五次审查——新 head B1——413 路径的压缩后记账:已修复。 B2——瞬时侧查询失败:已修复,形态即已度量的 M12。 main 在冷侧查询外的 try/catch( 代理对:修复正确。 测试钉住了修复。 非阻塞,延续前几轮:Test Plan 里的合并命令 (流程图与文件表见英文部分,内容一致。) 测试证据本地调用,但本审查未构建或执行任何 PR 代码——门禁的静态审查规则适用,实测行为交由下方点名的隔离泳道。本节携带的证据:经 API 获取的 PR 自身 CI(下表,已结束)、三次沙箱
真实场景测试(Stage 2c):N/A——被审查的行为只在字节受限反向代理之后显现,无法在 tmux 会话中对真实 API 搭出该场景,且本门禁不执行 PR 衍生代码。实测行为证据属于隔离泳道: — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 4/5 — the two blocking defects are fixed exactly as prescribed and re-verified line by line at this head; the only reason approval is not posted in this run is that nothing has executed this head yet, and every execution lane is currently broken by repo-wide failures this PR did not cause. This is the pass the previous one set up. The increment is one commit, +136/−5 of production-plus-test lines, and it does precisely the three things named: the one-line accounting guard that routes payload-overflow compactions to the local estimator (B1), the returned-status classification that makes transient side-query API failures propagate the original 413 instead of the /clear advice (B2, in the exact shape the second What keeps this at 4 rather than 5 is evidence, not code. The post-fix head has never run to completion in any harness: the unit suite on this head hit the repo-wide timeout wall (same 1h30m annotation as Verdict: approve — deferred, because approving against checks that settled non-green (even for provably external reasons) would attest to execution evidence that doesn't exist yet. The approval below is wired to fire automatically if this head's checks are re-run green; otherwise it lands when the evidence does. What clears it, in order of least effort: (1) 中文说明置信度:4/5——两个阻塞缺陷已按处方精确修复,并在本 head 上逐行复核;本轮没有直接批准的唯一原因,是这个 head 尚未被任何工具执行过,而每条执行通道目前都被本 PR 并未造成的仓库范围失败阻断。 本轮正是上一轮铺好的那一轮。增量是一个提交,+136/−5 行(含测试),恰好做了点名的三件事:把 payload 溢出压缩路由到本地估计器的一行记账守卫(B1);让瞬时侧查询 API 失败传播原始 413 而非 /clear 建议的返回状态分类(B2,形态与第二次 停在 4 而非 5 的是证据,不是代码。修复后的 head 尚未在任何工具中完整跑过:本 head 的单测套件撞上仓库范围的超时墙(与 裁决:批准——暂缓执行,因为对已落地为红(即便原因可证为外部)的检查直接批准,等于为尚不存在的执行证据背书。下方的批准指令被接成自动触发:若本 head 的检查被重跑为全绿即生效;否则等证据到位再落。按省力顺序,解锁路径:(1) — Qwen Code · qwen3.8-max Reviewed at |
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship — CI landed green after the review. ✅
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Test Plan (not a blocker): src/core/openaiContentGenerator/http413-payload.test.ts — no such file or directory; src/core/llm-chat.test.ts — no such file or directory; src/utils/request-payload-error.test.ts — no such file or directory.
中文说明
Test Plan(非阻断):src/core/openaiContentGenerator/http413-payload.test.ts — no such file or directory; src/core/llm-chat.test.ts — no such file or directory; src/utils/request-payload-error.test.ts — no such file or directory。
— qwen3.8-max via Qwen Code /review (v0.22.2)
…action Restoration re-embeds full-size image payloads and file blocks that the slimmed 413 side-query never carried, re-inflating the rebuilt retry request back over the gateway byte limit. On the requestPayloadTooLarge path compose post-compact history with maxFiles/maxImages=0 so the retry is dominated by the summary that just fit. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Truncated text parts were rebuilt as bare { text }, dropping sibling
properties such as thought/thoughtSignature that the converter pipeline
keys reasoning content off. Spread the part instead. Also truncate
oversized top-level functionCall string args (write_file/edit carry
whole file contents there) on the payload-overflow path, matching what
estimatePartChars already bills.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Detection walks the .cause chain five levels, but the status copy onto the surfaced actionable error used the shallow top-level lookup, so a cause-wrapped 413 lost its .status and downstream bucketing recorded unknown. Expose the deep-found status on RequestPayloadTooLargeInfo and reuse it when copying. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…ditional /clear The one-shot payload-overflow wrap fired the same "start a new session (/clear)" error for every non-COMPRESSED outcome, including two outcomes where that advice is false (#10380): - Transient compaction failure: a side-query 504/reset made compress() throw, the catch swallowed it, and the wrap advised /clear — but reactiveCompressionAttempted is per-send, so the next prompt gets a fresh one-shot and may recover. Keep the original 413 instead so the next send can retry recovery. - NOOP compaction: no earlier history to compress means the oversize sits in the current request itself; /clear + retry reproduces the identical failure. Surface a "reduce the current request" variant instead. Only the genuine "compaction ran and still did not fit" outcome keeps the new-session advice. Adds the NOOP message constant and two regression tests that fail when the outcome split is reverted. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not explored to full depth (tool budget reached): "agent 1a": could not execute the new unit tests ( request-payload-error.test.ts , compactionInputSlimming.test.ts , etc.) — neither the worktree nor the parent checkout h….
Test Plan (not a blocker): src/core/openaiContentGenerator/http413-payload.test.ts — no such file or directory; src/core/llm-chat.test.ts — no such file or directory; src/utils/request-payload-error.test.ts — no such file or directory.
中文说明
未探索到全部深度(达到工具调用预算):"agent 1a":could not execute the new unit tests ( request-payload-error.test.ts , compactionInputSlimming.test.ts , etc.) — neither the worktree nor the parent checkout h…。
Test Plan(非阻断):src/core/openaiContentGenerator/http413-payload.test.ts — no such file or directory; src/core/llm-chat.test.ts — no such file or directory; src/utils/request-payload-error.test.ts — no such file or directory。
— qwen3.8-max via Qwen Code /review (v0.22.2)
…compactions A successful 413-driven compaction reported triggerReason 'token_limit' (the reactive path passes trigger 'auto' and only 'image_overflow' was ever upgraded), so the CLI notice claimed the conversation "approached the input token limit" and the recorded payload logged a fabricated window-sized token count — contradicting the very premise that 413 fires below the token threshold (#10380). Add a 'payload_overflow' member to CompactionTriggerReason, set it in compress() when opts.requestPayloadTooLarge, and give the CLI notice a matching clause ("exceeded the endpoint request-body limit"). The reactive 413 path is force=true, so the screenshot-trigger upgrade in the non-forced gate cannot overwrite the reason. Covered by a service test asserting the COMPRESSED result's triggerReason and a hook test asserting the notice wording; both fail when the fix is reverted. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
The new recovery path was silent or mislabeled exactly where an incident needs it (#10380): - No log at the decisive wrap transition: add a warn when recovery is exhausted and the actionable 413 error is surfaced. - The two adjacent warns still said "context overflow" on the HTTP 413 payload-overflow path; split them on requestPayloadOverflow.isTooLarge the way the first branch already does. - stats.textPartsTruncated was written but never logged, and the runColdCompression slimming log was gated on images/documents being non-zero, so text-only slimming (the typical 413 case) logged nothing; include it in both the condition and the message. Logging-only change; no behavior impact. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
A bare HTTP 413 carries no provider token counts, so the reactive originalTokenCount fell through to the full context window. compress() then anchored its newTokenCount math on that window and stamped the post-compaction count at roughly window minus visible history — orders of magnitude above the real size. The inflated count persists into the next turn, force-re-compacting the just-compacted history or false-tripping a configured sessionTokenLimit right after a successful recovery (#10380). On the payload-overflow route, anchor on estimateContentTokens over the actual history — the same estimator the missing-usage accounting path already uses — instead of the window. Provider-reported counts (actualTokens / limitTokens) still take precedence, and the token-wording overflow path keeps its existing projection. Witness: new llm-chat test asserts the reactive compress() call carries an estimate far below the window; it fails (200000 >= 10000) when the anchor reverts to the window fallback. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
The cache-sharing request is built from the UNSLIMMED history, but a payload-overflow compaction exists because that exact payload was just rejected at the gateway byte limit. On mixed errors (413 status plus provider-reported counts that fit the window) canShareCache flips true and re-uploads the rejected payload at full size, takes another 413, logs a misleading cache-sharing failure, and only then recovers via the slimmed cold path — a wasted round-trip and a hole in the byte-limit invariant the slimming exists to enforce. It widens further now that the 413 anchor is a truthful estimate (sharedRequestFits flips true for every 413 compaction). Add !opts.requestPayloadTooLarge to canShareCache so payload-overflow recoveries use the slimmed cold path exclusively (#10380). Witness: cache-sharing fixture where every other conjunct holds; compress() with requestPayloadTooLarge must never call generateText. Removing the conjunct makes the cache-sharing call reappear (red). Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…pression The suppression test added for #10380 builds its history exclusively from screenshot payloads, so only the maxImages: 0 wiring can turn it red; mutating maxFiles back to tuning.maxRecentFiles kept every PR test green. Large sessions are dominated by read_file blocks (~20KB each), so a regression on the file half re-inflates the rebuilt retry request past the same gateway byte limit — a second 413 after recovery was already achievable. Add a sibling test with a real read_file result on disk: on the token-driven path the file content IS re-embedded (live comparator), and with requestPayloadTooLarge the serialized post-compact history carries no file-restoration block. Goes red when maxFiles reverts to tuning.maxRecentFiles. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Spreading the mocked Config loses its member types, so the baseOpts config no longer satisfied CompressOptions after the getTargetDir override. Restore the Config type with an assertion. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- reverse-audit round-1 chunk-4 non-string-carrier slimming escape — already reported as R2-2/R2-3 (comments 3883345354 / 3883345360) and re-posted this round under those ids
Test Plan (not a blocker): src/core/openaiContentGenerator/http413-payload.test.ts — no such file or directory; src/core/llm-chat.test.ts — no such file or directory; src/utils/request-payload-error.test.ts — no such file or directory.
Deferred under the convergence posture (round 3, not a blocker) — recorded, not requested in this round:
packages/core/src/core/llm-chat.test.ts:18418 — [review] New #10380 describe is nested inside the XML fallback describe — the -t filter selects 8 unrelated tests
Convergence: round 3 posted 4 inline comment(s), 1 of them reported for the first time; the previous round posted 10 (6 new). Findings keep coming back to the same files: packages/core/src/core/llm-chat.ts (findings in rounds 1, 2; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)
中文说明
已审查。 建议见行内评论。
本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
Test Plan(非阻断):src/core/openaiContentGenerator/http413-payload.test.ts — no such file or directory; src/core/llm-chat.test.ts — no such file or directory; src/utils/request-payload-error.test.ts — no such file or directory。
收敛姿态下延后(第 3 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 3 轮发布了 4 条行内评论,其中 1 条是首次提出;上一轮发布了 10 条(其中 6 条首次提出)。发现反复回到同一批文件:packages/core/src/core/llm-chat.ts(第 1、2 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)
— qwen3.8-max via Qwen Code /review (v0.22.2)
Local verification of #10408 — real byte-limited gateway, real TUI, real daemonI built the PR in a dedicated worktree ( Verdict: the fix works, and it works where the issue reporter actually is. 1.
|
|
@qwen-code /triage |
|
Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 39 passed · 0 failed · 39 total Flakiness gate: ✅ 6 changed test file(s) x 5 identical rounds, no divergence 中文 — 判定:✅ 通过 · 可合入(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:39 通过 · 0 失败 · 39 总计 抖动门:✅ 6 changed test file(s) x 5 identical rounds, no divergence Verification reportPR #10408 — fix(core): recover model requests rejected with HTTP 413 via one-shot compactionVerdict: 中文摘要
Central claim + A/BCentral claim: a model-request 413 (bare HTML error page, no token wording) recovers through the one-shot reactive compaction path; on base it never fires and the session is permanently stuck. Harness:
Witnesses: Secondary claims verified: side-query slimming keeps text intact without options (token-driven compactions untouched); the retried request is dominated by the summary (restoration suppressed); the actionable error preserves Mutation matrix (vacuity + attribution)Run in a scratch worktree; each row reverts one PR guard, runs the test the commit says pins it, then restores. Unmutated controls green first (8/8 and 4/4), so the kills are meaningful. Raw log:
No surviving mutants; the layered restoration guards are proven load-bearing by the M7a/M7b/M7c triple (each single revert kills only its own test, the set kills both). Findings (informational, non-blocking)
Not covered
MethodologyEnvironment: Flakiness gate logEvidence imagesHarness scripts and raw logs are in the workflow run artifacts (7-day retention). — Qwen Code · sandboxed verification |
|
Status at head
Decision needed from maintainer/author: (a) a human applies the one-line F1 fix + a regression test here (bot won't, scope-fused), or (b) merge as-is and track F1–F4 as follow-up issues. (中文)head |
|
@qwen-code /triage |
|
Sandboxed verification: ❌ not passed — non-deterministic tests (flakiness gate) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 57 passed · 2 failed · 59 total Flakiness gate: ❌ 2 of 6 changed test file(s) returned different results across identical re-runs (1 full round(s)) The deterministic flakiness gate re-ran the test files this PR changes and got different outcomes from identical runs (agent verdict: 中文 — 判定:❌ 不通过 · 测试结果不确定(抖动门)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:57 通过 · 2 失败 · 59 总计 抖动门:❌ 2 of 6 changed test file(s) returned different results across identical re-runs (1 full round(s)) 确定性抖动门将本 PR 改动的测试文件原样重跑了多轮,得到了不一致的结果(agent 判定: Verification reportPR #10408 — fix(core): recover model requests rejected with HTTP 413 via one-shot compactionVerdict: 中文摘要
Previous-finding status (follow-up round)Previous round (
Central claim + A/BCentral claim: a model-request 413 (bare HTML error page, no token wording) recovers through the one-shot reactive compaction path with the side-query input slimmed; on base it never fires and the session is permanently stuck. Harness:
Witnesses: Secondary claims verified: side-query slimming truncates oversized text end-to-end (side-query Mutation matrix (vacuity + attribution)Run in a scratch
No surviving mutants; the layered restoration guards are again proven load-bearing by the M7a/M7b/M7c triple. M11's failing test name was verified against the assertion error in Findings1. Merge interaction: transient side-query failures on the 413 path now surface destructive "/clear" advice (the PR's documented transient semantics are dead code after the merge)Mechanism. Between the two bases,
Evidence (scripted). Why the PR's own tests miss it. Blast radius.
Nuance on the sq413 instance: there the side-query failure is size-persistent (the slimmed payload still exceeds the limit), and the /clear advice is at least directionally actionable for that sub-case — though the code comment and the transient test classify the whole failure class as "advice unearned". The sq504 instance (persistent 5xx) is unambiguous. Both share the one dead branch; the M12 fix restores the documented behavior for the class, and whether size-persistent failures additionally deserve a distinct message is an author judgment, not a blocker on the fix. Not demonstrated / bounded. No data loss or wrong-content outcome: the advice is destructive only if the user follows it; status 413 and the cause chain are preserved on the surfaced error; the happy path and the wording path are unaffected (cells above). The defect is confined to the recovery-failure branch. Candidate fix (measured, M12). Treat the API-error status as the transient class on the payload-overflow path: minimal diff (packages/core/src/core/llm-chat.ts) if (
isCompressionFailureStatus(reactiveInfo.compressionStatus)
) {
+ // COMPRESSION_FAILED_API_ERROR is the side-query failure
+ // class: on the payload-overflow path it is transient —
+ // keep the original 413 so the next send retries recovery
+ // (#10380). compress() no longer throws it.
+ if (
+ requestPayloadOverflow.isTooLarge &&
+ reactiveInfo.compressionStatus ===
+ CompressionStatus.COMPRESSION_FAILED_API_ERROR
+ ) {
+ payloadRecoveryThrew = true;
+ }
// Reactive compression is force=true so tryCompress's
...Measured in a scratch worktree (
Green on both sides of the suite is the un-pinned-axis signal: the fixture that would pin the fix is one where 2. Tight side-query margin at small gateways (previous P1 — worsened)Re-measured at the new head: the per-part cap is still 3. Cause-chain status walk depth = 5 (previous P2 — stands, informational)Re-measured: a 413 status nested at the 5th node is detected; at the 6th it is not (wording patterns would still catch reason phrases). Real provider errors carry 4. String status
|
Maintainer verification — local end-to-end reproduction of #10380 against a real byte-limited gatewayI rebuilt this PR locally and drove the bundled CLI ( Result: the PR does what it claims, the recovery is bounded, the pre-existing token-overflow path is unchanged, Setup
Lane results (all on the wire)
1. The reported failure, before and afterOn the merge base the session is dead exactly as the issue describes: the 413 is classified by nothing, reactive 2. Both actionable errors, and they are outcome-accurateThis is the part I most wanted to check, because "always tell the user to
Recovery is bounded in both lanes: exactly one compaction, exactly one retry, then a terminal message. No loop. 3. The two compaction paths put different things on the wireSame history, same tool result; only the rejection differs. Two behaviours are visible and both match the PR's claims:
4. The user-visible notice (real TUI, real pty)The PR body says the change is non-user-visible; the compaction notice actually is. Driving the interactive TUI 5. Is the side-query slimming load-bearing? YesTool-heavy turn (one assistant message issuing 6 parallel
So 6. Mutation matrix — 16/16 killedBaseline first: Scope notes (not blockers)
StateAll CI checks are green (Test, Integration Tests no-AK, web-shell E2E, secret scan, CVE audit, Reproduction scripts (gateway + runner + mutation harness) were written for this review and are not part of the 中文说明维护者验证 —— 在本地用真实字节上限网关端到端复现 #10380我在本地重建了这个 PR,并用打包后的 CLI( 结论:PR 的行为与其声明一致,恢复过程有界,原有 token 超长路径完全未变,新增的每个生产分支都被测试钉住。我认为可以合入。 环境搭建
各泳道结果(均为链路数据)
图 1(A/B 复现):merge base 上会话就此报废,与 issue 描述一致 —— 413 无人识别、反应式压缩从不触发、用户看到的就是那张 HTML 页; 图 2(两条可操作错误):这是我最想核对的部分,因为「一律叫用户 图 3(两条压缩路径在链路上的差异):历史相同、工具结果相同,只有拒绝方式不同。两处行为可见且与 PR 声明相符: 图 4(真实 TUI + 真实 pty 的用户可见提示):PR 描述里写的是「非用户可见路径」,但压缩提示其实是可见的。 图 5(侧查询裁剪是不是承重件?是):工具密集的一轮(一条 assistant 消息并发 6 个 图 6(变异矩阵 16/16 全杀):先跑基线 —— 范围说明(非阻塞)
现状CI 全绿(Test、Integration Tests no-AK、web-shell E2E、secret scan、CVE audit、 复现脚本(网关 + 运行器 + 变异夹具)是为这次评审现写的,未进仓库;需要的话我可以附上。 |
|
Triage re-run completed without a new review.
The stage comments above were updated with the latest result. View workflow run. 上方各阶段评论已更新为最新结果。查看工作流运行。 |
|
@qwen-code /triage |
|
Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 68 passed · 2 failed · 70 total Flakiness gate: 中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:68 通过 · 2 失败 · 70 总计 抖动门: Verification reportPR #10408 — fix(core): recover model requests rejected with HTTP 413 via one-shot compactionVerdict: 中文 — 判定:findings(存在合并交互缺陷,附已度量的候选修复)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock 线级 harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。
Previous-finding status (follow-up round)Previous round (workflow run 33307317592, verdict
Central claim + A/BCentral claim: a model-request 413 (bare HTML error page, no token wording) recovers through the one-shot reactive compaction path with the side-query input slimmed; on base it never fires and the session is permanently stuck. Harness:
Secondary claims verified: slimming truncates oversized text end-to-end (side-query carries the 4,000-char run, not the 40,960-char payload); the retry is strictly smaller than the rejected request (436 B vs 41,229 B); status 413 survives the cause-wrapped actionable error; token-wording overflow behaves identically on both arms. Mutation matrix (vacuity + attribution)Run in a scratch
No surviving mutants. ctrl-10380-service note: the in-run red showed the same test pair (the two restoration-suppression tests) that the M7 rows target, on the first two invocations of that file in the run; it did not reproduce across 3 clean cache-wiped re-runs on the checksum-verified-clean tree (nor in the main-tree gates), so it is recorded as a harness-warmup artifact, not a test failure of the merged code — and the affected M-rows (M5–M8) each killed their own target test regardless. Findings1. Merge interaction (carried from the previous round — stands, re-measured): transient side-query failures on the 413 path surface destructive "/clear" adviceMechanism. Between the PR's original base and the current one,
Evidence (scripted). Reproduce (from node harness/ab-harness.mjs --tree /__w/qwen-code/qwen-code --cell sq504 --out logs/head-sq504.json
node harness/ab-harness.mjs --tree /__w/qwen-code/qwen-code --cell sq413 --out logs/head-sq413.json
node harness/ab-assert.mjs --dir logs # the two sq* design-intent checks FAILWhy the PR's own tests miss it. Blast radius.
Nuance carried from the previous round: in the sq413 instance the side-query failure is size-persistent, and the /clear advice is at least directionally actionable for that sub-case — though the code comment and the transient test classify the whole failure class as "advice unearned". The sq504 instance (persistent 5xx) is unambiguous. Both share the one dead branch; the M12 fix restores the documented behavior for the class, and whether size-persistent failures additionally deserve a distinct message is an author judgment, not a blocker on the fix. Not demonstrated / bounded. No data loss or wrong-content outcome: the advice is destructive only if the user follows it; status 413 and the cause chain are preserved on the surfaced error; the happy path and the wording path are unaffected (cells above). The defect is confined to the recovery-failure branch. Candidate fix (re-measured, M12). Treat the API-error status as the transient class on the payload-overflow path: minimal diff (packages/core/src/core/llm-chat.ts) if (
isCompressionFailureStatus(reactiveInfo.compressionStatus)
) {
+ // COMPRESSION_FAILED_API_ERROR is the side-query failure
+ // class: on the payload-overflow path it is transient —
+ // keep the original 413 so the next send retries recovery
+ // (#10380). compress() no longer throws it.
+ if (
+ requestPayloadOverflow.isTooLarge &&
+ reactiveInfo.compressionStatus ===
+ CompressionStatus.COMPRESSION_FAILED_API_ERROR
+ ) {
+ payloadRecoveryThrew = true;
+ }
// Reactive compression is force=true so tryCompress's
...Measured in a scratch worktree (
Green on both sides of the suite is the un-pinned-axis signal: the fixture that would pin the fix is one where 2. Tight side-query margin at small gateways (carried — stands)Re-measured at the new head: the per-part cap is still 3. Cause-chain status walk depth = 5 (carried — stands, informational)Re-measured ( 4. String status
|
|
@qwen-code /triage |
|
Triage re-run completed without a new review.
The stage comments above were updated with the latest result. View workflow run. 上方各阶段评论已更新为最新结果。查看工作流运行。 |
|
Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 57 passed · 2 failed · 59 total Flakiness gate: 中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:57 通过 · 2 失败 · 59 总计 抖动门: Verification reportPR #10408 — fix(core): recover model requests rejected with HTTP 413 via one-shot compactionVerdict: 中文 — 判定:findings(合并交互缺陷在新 head 上依旧成立,附已重测的候选修复)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock 线级 harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。
Previous-finding status (follow-up round)Previous round (workflow run 33317329608, verdict
Central claim + A/BCentral claim: a model-request 413 (bare HTML error page, no token wording) recovers through the one-shot reactive compaction path with the side-query input slimmed; on base it never fires and the session is permanently stuck. Harness:
Secondary claims verified on the wire: slimming truncates the oversized tool result end-to-end (assertion on the recorded side-query body); the retry is strictly smaller than the rejected request (509 B vs 41,479 B); status 413 survives the cause-wrapped actionable error ( Mutation matrix (vacuity + attribution)
No surviving mutants. Kill attribution spot-checked from the row logs: M1 fails exactly the six 413-recovery tests; M2 fails exactly the anchor test with the expected-vs-actual message quoted above. Findings1. Merge interaction (carried from the previous round — stands, re-measured): transient side-query failures on the 413 path surface destructive "/clear" adviceMechanism. Between the PR's original base and the current one,
Evidence (scripted). Reproduce (from node harness/ab-harness.mjs --tree /__w/qwen-code/qwen-code --cell sq504 --out logs/head-sq504.json
node harness/ab-harness.mjs --tree /__w/qwen-code/qwen-code --cell sq413 --out logs/head-sq413.json
node harness/ab-assert.mjs logs # the two sq* design-intent checks FAILWhy the PR's own tests miss it. Blast radius.
Nuance carried from the previous round: in the sq413 instance the side-query failure is size-persistent, and the /clear advice is at least directionally actionable for that sub-case — though the code comment and the transient test classify the whole failure class as "advice unearned". The sq504 instance (persistent 5xx) is unambiguous. Whether size-persistent failures additionally deserve a distinct message is an author judgment, not a blocker on the fix. Not demonstrated / bounded. No data loss or wrong-content outcome: the advice is destructive only if the user follows it; status 413 and the cause chain are preserved on the surfaced error; the happy path and the wording path are unaffected (cells above). The defect is confined to the recovery-failure branch. Candidate fix (re-measured at this head, M12). Treat the API-error status as the transient class on the payload-overflow path: minimal diff (packages/core/src/core/llm-chat.ts, inside the isCompressionFailureStatus branch) if (
isCompressionFailureStatus(reactiveInfo.compressionStatus)
) {
+ // COMPRESSION_FAILED_API_ERROR is the side-query failure
+ // class: on the payload-overflow path it is transient —
+ // keep the original 413 so the next send retries recovery
+ // (#10380). compress() no longer throws it.
+ if (
+ requestPayloadOverflow.isTooLarge &&
+ reactiveInfo.compressionStatus ===
+ CompressionStatus.COMPRESSION_FAILED_API_ERROR
+ ) {
+ payloadRecoveryThrew = true;
+ }
// Reactive compression is force=true so tryCompress's
...Measured in a scratch worktree (
Green on both sides of the suite is the un-pinned-axis signal: the fixture that would pin the fix is one where 2. Tight side-query margin at small gateways (carried — stands)Re-measured at the new head: the per-part cap is still 3. Cause-chain status walk depth = 5 (carried — stands, informational)Re-measured ( 4. String status
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Test Plan (not a blocker): src/core/openaiContentGenerator/http413-payload.test.ts — no such file or directory; src/core/llm-chat.test.ts — no such file or directory; src/services/chatCompressionService.test.ts — no such file or directory; src/services/compactionInputSlimming.test.ts — no such file or directory; src/utils/request-payload-error.test.ts — no such file or directory.
Deferred under the convergence posture (round 4, not a blocker) — recorded, not requested in this round:
packages/core/src/core/llm-chat.test.ts:18417 — [review] #10380 describe accidentally nested inside the XML fallback describe — the -t filter selects 8 unrelated testspackages/core/src/core/llm-chat.test.ts:18492 — [review] COMPRESSED event's triggerReason is never pinned at the llm-chat layer — a stripped yield silently restores the #10380 misattributionpackages/core/src/services/chatCompressionService.test.ts:885 — [probe] shape tests discard a compress() return that is a failure status under their own fixtures
Convergence: round 4 posted 7 inline comment(s), 3 of them reported for the first time; the previous round posted 4 (1 new). Findings keep coming back to the same files: packages/core/src/core/llm-chat.ts (findings in round 3; 1 more now); packages/core/src/services/chatCompressionService.ts (findings in round 1; 1 more now); packages/core/src/services/compactionInputSlimming.ts (findings in round 2; 1 more now). The rate of new findings is not falling. A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. Batching the remaining fixes and verifying them before the next push, or dropping this PR's reviews to --severity-floor critical, keeps the loop from re-deriving the same set. (Observation only — nothing was withheld from this review because of this observation.)
中文说明
Test Plan(非阻断):src/core/openaiContentGenerator/http413-payload.test.ts — no such file or directory; src/core/llm-chat.test.ts — no such file or directory; src/services/chatCompressionService.test.ts — no such file or directory; src/services/compactionInputSlimming.test.ts — no such file or directory; src/utils/request-payload-error.test.ts — no such file or directory。
收敛姿态下延后(第 4 轮,非阻断)——已记录,本轮不要求修改:共 3 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 4 轮发布了 7 条行内评论,其中 3 条是首次提出;上一轮发布了 4 条(其中 1 条首次提出)。发现反复回到同一批文件:packages/core/src/core/llm-chat.ts(第 3 轮已出过发现,本轮又有 1 条);packages/core/src/services/chatCompressionService.ts(第 1 轮已出过发现,本轮又有 1 条);packages/core/src/services/compactionInputSlimming.ts(第 2 轮已出过发现,本轮又有 1 条)。新发现的产出速度没有下降。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。把剩余修复攒成一批、验证后再推送,或将本 PR 的评审降到 --severity-floor critical,可以避免循环反复推导同一组发现。(仅为观察——本轮评审未因此扣留任何内容。)
— qwen3.8-max via Qwen Code /review (v0.22.3)
Treat returned compression API failures like transient side-query failures so a recoverable 413 does not receive permanent new-session advice. Keep payload-overflow token accounting off the slimmed side-query usage branch, and avoid splitting surrogate pairs when trimming request text for compaction. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
Closeout: fixed the three #10380 payload-overflow edge cases in 46aace5: returned compaction API failures now preserve the original 413, payload-overflow accounting no longer uses slimmed side-query usage, and text slimming avoids splitting surrogate pairs. Local checkout was blocked by partial-clone object fetch, so I ran the available Prettier check on the touched files and left full verification to the new CI run. |
|
@qwen-code /triage |
|
Sandboxed verification: ❌ not passed — the PR could not be built - workflow run The PR could not be built because 中文 — 判定:❌ 不通过 · PR 构建失败由于 Install/build log— Qwen Code · sandboxed verification |
|
Triage re-run completed without a new review.
The stage comments above were updated with the latest result. View workflow run. 上方各阶段评论已更新为最新结果。查看工作流运行。 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- the #10380 describe nested inside the XML fallback describe (llm-chat.test.ts:18417) — already reported and deferred in rounds 3 and 4
Test Plan (not a blocker): src/core/openaiContentGenerator/http413-payload.test.ts — no such file or directory; src/core/llm-chat.test.ts — no such file or directory; src/services/chatCompressionService.test.ts — no such file or directory; src/services/compactionInputSlimming.test.ts — no such file or directory; src/utils/request-payload-error.test.ts — no such file or directory.
Convergence: round 5 posted 6 inline comment(s), 2 of them reported for the first time; the previous round posted 7 (3 new). Findings keep coming back to the same files: packages/core/src/services/chatCompressionService.ts (findings in rounds 1, 4; 2 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)
中文说明
本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
Test Plan(非阻断):src/core/openaiContentGenerator/http413-payload.test.ts — no such file or directory; src/core/llm-chat.test.ts — no such file or directory; src/services/chatCompressionService.test.ts — no such file or directory; src/services/compactionInputSlimming.test.ts — no such file or directory; src/utils/request-payload-error.test.ts — no such file or directory。
收敛情况:第 5 轮发布了 6 条行内评论,其中 2 条是首次提出;上一轮发布了 7 条(其中 3 条首次提出)。发现反复回到同一批文件:packages/core/src/services/chatCompressionService.ts(第 1、4 轮已出过发现,本轮又有 2 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)
— qwen3.8-max via Qwen Code /review (v0.22.3)
🔬 Maintainer review + tmux E2E report — no Critical found at head
|
| Wire request | Bytes | Limit | Result |
|---|---|---|---|
| turn 3, first attempt | 202,223 | 153,175 | HTTP 413 (bare nginx page) |
| compaction side-query | 14,068 | 153,175 | accepted — filler text truncated by the 4000-char cap, side-query itself fits |
| retry after compaction | 95,606 (3 msgs: summary replaces history) | 153,175 | accepted — turn completes normally (exit 0) |
Scenario B — NOOP actionable error (oversize in the current request, no history). Fresh session, limit 50,000 B, baseline request 103,175 B:
[API Error: Model request exceeds the endpoint request-body limit (HTTP 413) and there is no earlier conversation history to compress. Reduce the current request (smaller message or smaller/fewer attachments) and retry.
(cause: 413 <html>…413 Request Entity Too Large…nginx…</html>)]
Exactly one over-limit request hit the wire (no retry loop), the actionable NOOP message surfaced, and the original 413 is preserved as cause with its status intact.
CI / verification notes for maintainers
Integration Tests (no-AK, No Sandbox)passed on this head.Test (ubuntu-latest)was cancelled at ~1h31m andweb-shell E2E Smokefailed with the runner-sideECONNREFUSED 127.0.0.1:4170— the same contended-runner pattern other PRs are hitting today, not a failure attributable to this diff.- The latest sandboxed verify's "could not be built" verdict is the pre-existing
packages/sdk-typescriptbrowser-bundle size guard failure on current main (220220 > 215*1024, introduced by fix(sdk): Surface daemon JSON-RPC error details #10571 without a limit bump). It reproduces identically on a clean local build of this PR, and the guard's bundle contains none of this PR's code (symbol-scanned) — it is not caused by fix(core): recover model requests rejected with HTTP 413 via one-shot compaction #10408.
Status
No Critical → no Request Changes. Not approving yet: CI is not fully green on this head and the standing CHANGES_REQUESTED state predates the closeout commit.
— qqqys · code review + tmux/headless E2E
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…visible history The payload-overflow tests mocked a 90K/180K-token payload over a ~14-token visible history, so the estimate-based accounting (which 413 recoveries now use) correctly flagged the summary as an inflation. Give the fixtures large visible content, matching the real shape of a gateway-rejected payload, so they verify trigger reason and cold-path routing against a genuine reduction. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No blocking issues. LGTM! ✅
Not explored to full depth (tool budget reached): chunk 4: local execution of the five new tests ( npx vitest run … -t '#10380' ) — the worktree lacks built dist/ prerequisites and I did not run a full monorepo build …; chunk 7: executing request-payload-error.test.ts via vitest — the review worktree has no node_modules /built dist and npm ci + build exceeds the tool budget; all ….
Test Plan (not a blocker): src/core/openaiContentGenerator/http413-payload.test.ts — no such file or directory; src/core/llm-chat.test.ts — no such file or directory; src/services/chatCompressionService.test.ts — no such file or directory; src/services/compactionInputSlimming.test.ts — no such file or directory; src/utils/request-payload-error.test.ts — no such file or directory.
Deferred under the convergence posture (round 6, not a blocker) — recorded, not requested in this round; 1 Critical(s) among them are deferred by their axes — fails-closed on new surface, where no wrong result is certified and the merge base had neither the surface nor the defect — and remain follow-up work recorded in the findings artifact:
packages/core/src/services/chatCompressionService.ts:1200 — [review] Critical [fails-closed] [new-surface] R4-2: still stands (narrowed) — CI symptom fixed by c3dc520d (146/146 green); the estimate-branch inflation veto still aborts 413 rec…packages/core/src/services/chatCompressionService.ts:1200 — [review] R5-1: still stands — the estimate branch's debug log labels intentional payload-overflow routing 'usage metadata missing'packages/core/src/services/chatCompressionService.ts:257 — [review] R1-2: still stands — per-carrier UTF-16 char cap, no aggregate side-query byte budget (author declined as out of scope)packages/core/src/services/compactionInputSlimming.ts:410 — [review] R2-2: still stands — functionResponse slimming walks only top-level string carriers (author declined as out of scope)packages/core/src/services/compactionInputSlimming.ts:438 — [review] R2-3: still stands — same top-level-only walk at the functionCall.args site (author declined as out of scope)packages/core/src/core/llm-chat.ts:3808 — [review] R3-1: still stands — no second-send test after a transiently failed 413 compaction (author-confirmed real, deferred under the scope fuse)packages/core/src/core/turn.ts:421 — [probe] D6-1: opentui event-adapter renders payload_overflow with the token-limit wording (ink renderer updated; adapter latent — no production callers yet)packages/core/src/services/compactionInputSlimming.ts:377 — [probe] D6-2: maxTextChars propagation into the nested functionResponse.parts recursion has zero test pin (options-drop mutant survives 43/43)packages/core/src/services/chatCompressionService.ts:1231 — [probe] D6-3: no test pins the estimate branch's unslimmed curatedHistory input (slimmed-input mutant survives all 7 #10380 tests)packages/core/src/services/compactionInputSlimming.ts:389 — [probe] D6-4: the nextPart flow-through (nested-media rebuild feeding the output/error truncation) has zero test pin (both mutants keep 43/43 green)packages/core/src/core/llm-chat.ts:3827 — [probe] D6-5: first-pass INFLATED/OUTPUT_TRUNCATED outcomes surface the /clear RECOVERY message although no shrunken request was built or sentpackages/core/src/utils/request-payload-error.ts:50 — [probe] D6-6: MAX_CAUSE_DEPTH bound and cycle protection have no test pin (5-to-2 mutant keeps the suite 8/8 green)
中文说明
无阻断问题。LGTM!✅
未探索到全部深度(达到工具调用预算):chunk 4:local execution of the five new tests ( npx vitest run … -t '#10380' ) — the worktree lacks built dist/ prerequisites and I did not run a full monorepo build …;chunk 7:executing request-payload-error.test.ts via vitest — the review worktree has no node_modules /built dist and npm ci + build exceeds the tool budget; all …。
Test Plan(非阻断):src/core/openaiContentGenerator/http413-payload.test.ts — no such file or directory; src/core/llm-chat.test.ts — no such file or directory; src/services/chatCompressionService.test.ts — no such file or directory; src/services/compactionInputSlimming.test.ts — no such file or directory; src/utils/request-payload-error.test.ts — no such file or directory。
收敛姿态下延后(第 6 轮,非阻断)——已记录,本轮不要求修改;其中 1 条 Critical 按其失败方向与对照基线延后——fails-closed 且 new-surface:未认证任何错误结果,且 merge base 既无该功能面也无该缺陷——作为后续工作记录在 findings 工件中:共 12 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.22.3)
|
Released in v0.23.0. |
































What this PR does
A model generation request rejected by a gateway with HTTP 413 (request body byte limit) now recovers through the same one-shot reactive compaction path as token-based context overflow. The send catch classifies a model-request 413 with a new typed detector that is only consulted on the model-request path, triggers one compression attempt with the side-query input trimmed (oversized tool-result/text payloads truncated so the compression request itself fits under the same gateway limit), retries once, and — if the request still cannot fit — surfaces an actionable "start a new session" error instead of repeating the bare 413 on every prompt.
Why it's needed
Fixes #10380. When an OpenAI-compatible endpoint sits behind a reverse proxy with a request-body byte limit, a session whose token count is still below the auto-compaction threshold can have its serialized JSON request rejected with a bare HTTP 413 (often an HTML error page). The existing overflow detection only matches provider token wording (
context_length_exceeded,prompt too long, ...), so the 413 is never classified, reactive compression never fires, and every later prompt re-sends the same oversized history — the session is permanently unusable until the user clears it.Reviewer Test Plan
How to verify
The failure shape is reproduced end-to-end against a real local HTTP server with an 8KB body limit returning an HTML 413 page, driving the real OpenAI provider path:
npx vitest run src/core/openaiContentGenerator/http413-payload.test.ts src/core/llm-chat.test.ts src/services/chatCompressionService.test.ts src/services/compactionInputSlimming.test.ts src/utils/request-payload-error.test.ts -t "10380"(all green). Onmainthe three llm-chat recovery tests fail (compression never invoked; raw 413 propagates). Confirm: (1) a 413 triggers exactly one compaction and the retry succeeds; (2) a second 413 after compaction, or a failed compaction, throws the actionable error withstatus: 413preserved; (3) token-wording overflow behaves exactly as before; (4)slimCompactionInputwithout options keeps text intact (upload/file 413s and token-driven compactions are untouched).Evidence (Before & After)
N/A (non-user-visible error-recovery path; covered by the regression tests above)
Tested on
Environment (optional)
Unit/integration tests only (
packages/corevitest); real local HTTP server standing in for the byte-limited proxy.Risk & Scope
maxRequestBytesprovider config, exact-route compaction policy (exact routes keep skipping compression), daemonturn_errorstructured-error plumbing — per the issue's suggested follow-up split.slimCompactionInputgains an optional third argument andstats.textPartsTruncatedfield, default behavior unchanged.Linked Issues
Fixes #10380
中文说明
本 PR 做了什么
模型生成请求被网关以 HTTP 413(请求体字节上限)拒绝时,现在会走与"上下文超长"相同的一次性反应式压缩恢复路径:发送捕获处用一个新的类型化检测器识别"模型请求 413"(该检测器只在模型请求路径上被消费),触发一次压缩——压缩侧查询输入会裁剪超大的工具结果/文本以让压缩请求本身也能装进同一网关限制——然后重试一次;若仍超限,则抛出"请新建会话"的可操作错误,而不是每条 prompt 都重复裸 413。
为什么需要
修复 #10380:OpenAI-compatible endpoint 位于带请求体字节上限的反向代理之后时,会话 token 数低于自动压缩阈值、但序列化后的 JSON 请求超过代理上限,会被裸 HTTP 413(可能是 HTML 错误页)拒绝。现有溢出判定只匹配 provider 的 token 措辞,413 永远不会被识别,反应式压缩不触发,之后每条 prompt 都重发同一份超大历史,会话永久不可用。
审阅测试计划
如何验证
用真实的本地 HTTP server(8KB 请求体上限、超限返回 HTML 413 页面)驱动真实 OpenAI provider 路径完成端到端复现:运行
npx vitest run src/core/openaiContentGenerator/http413-payload.test.ts src/core/llm-chat.test.ts src/services/chatCompressionService.test.ts src/services/compactionInputSlimming.test.ts src/utils/request-payload-error.test.ts -t "10380"(全部通过)。在main上其中三个 llm-chat 恢复测试失败(压缩从未被调用、裸 413 直接抛出)。请确认:(1) 413 恰好触发一次压缩且重试成功;(2) 压缩后再次 413 或压缩失败时抛出保留status: 413的可操作错误;(3) token 措辞溢出的原路径行为不变;(4) 不传选项的slimCompactionInput保留完整文本(上传类 413 与常规压缩不受影响)。前后对比证据
N/A(非用户可见的错误恢复路径,由上述回归测试覆盖)
测试环境
环境(可选)
仅单元/集成测试(
packages/corevitest),用本地 HTTP server 模拟带字节上限的代理。风险与范围
maxRequestBytesprovider 配置、exact-route 压缩策略(exact route 仍跳过压缩)、daemonturn_error结构化错误透传——按 issue 建议拆为后续。slimCompactionInput新增可选第三参数与stats.textPartsTruncated字段,默认行为不变。关联 Issue
Fixes #10380