fix: Codex preflight summaries fail with unsupported max_output_tokens #663
Description
Problem
With billion-context 0.1.95 proxying Codex through https://chatgpt.com/backend-api/codex/responses, an over-window request enters preflight compression, but the summary request is rejected with:
{"detail":"Unsupported parameter: max_output_tokens"}Bili then returns HTTP 502 (preflight compression could not bring it under ... HTTP 400). Repeated client retries cannot recover the session.
Reproduction
- Use the Codex ChatGPT Responses backend with valid authentication (credentials omitted).
- Trigger proxy-side preflight compression for an over-window conversation.
src/preflight.tsbuilds a Responses summary payload withmax_output_tokens: MAX_SUMMARY_OUTPUT_TOKENS.- The backend rejects that parameter with HTTP 400. A minimal authenticated request with this field reproduces the rejection independently of history size.
Observations
- Ordinary short streaming model requests succeed through the same outbound proxy.
- Existing Bili compression had successfully reduced an earlier turn; context management was active.
- The previous upstream usage was approximately 92k tokens, while the next processed payload was locally estimated at approximately 437k against a configured 272k budget. These are different measurements, not proof of a tokenizer defect.
- The immediate blocker is the unsupported summary parameter. The source of the payload growth is not yet established.
- Existing summary fallback handles
stream must be true, but not this parameter rejection.
Expected behavior
Generate a compatible summary request for the Codex ChatGPT backend (or narrowly learn/retry the explicitly rejected optional parameter), while retaining output limits for standard Responses providers and preserving the preflight overflow guard. Cover both summary generation and successful recovery/forwarding in regression tests.
No credentials, session contents, or private host configuration are included. We are preparing a minimal local fix to restore availability.
Activity
Wuxie233 commentedon Sep 9, 2026
Local candidate verification: the same synthetic over-window request failed with 502 on 0.1.95 and succeeded on the patched candidate against the real Codex backend. Preflight folded two ranges (~34,083 estimated tokens down to ~14,851), then the model completed and retained the synthetic canary fact.
The patch follows the existing stream-required capability-learning approach: only an explicit HTTP 400 rejecting
max_output_tokenscauses Responses summaries to omit it. Learning is scoped to session + upstream URL + model; standard Responses providers retain the existing 8192 output limit. Both rejection orders are covered and compatibility retries are bounded. The context window, compression prompts/ranges/carriers, history retention, and fail-fast overflow guard are unchanged.This addresses the summary-request blocker. It does not claim to explain the original payload growth or alter normal proactive compression policy. Local deployment verification is still in progress.
ranxianglei commentedon Sep 9, 2026
ework接单
Wuxie233 commentedon Sep 9, 2026
Follow-up from the real Codex client: after fixing the output-limit rejection, preflight can still fail because
requestSummarycopies the main request headers into its independently constructed summary body. WithX-OpenAI-Internal-Codex-Responses-Lite: true, the backend rejects that body:A direct A/B request reproduced HTTP 400 with that header and successful completion without it. A synthetic over-window request through the first patched proxy also reproduces the same 502 when it includes the real-client header and the main body uses
reasoning.context: all_turns.We are testing a narrow fix that removes this request-shape-specific header only from independently generated Responses summary calls. The original model request must retain its header and reasoning fields. Earlier short-client tests and a synthetic preflight request without that header did not cover this failure; the expanded regression will.
Wuxie233 commentedon Sep 9, 2026
The follow-up fix is validated: a real over-window request using Lite mode (
reasoning.context: all_turns,parallel_tool_calls: false) now completes after preflight folds two ranges (~34,083 down to ~14,788 estimated tokens), preserving the synthetic canary. The independent summary drops the Lite header, while the main request retains it and its reasoning fields.Regression coverage checks summary/main header separation, auth and routing preservation, and unchanged Anthropic handling. The full suite passes 1,248/1,248 with file concurrency limited to one to avoid unrelated timing flakes.
This is an additional fix beyond the earlier unsupported-output-limit retry. We have not changed the compression policy or model window.
fix: drop the Codex Lite header from preflight summary calls
ranxianglei commentedon Sep 9, 2026
Both fixes are now in a single PR: #665
Follow-up fix (Lite header) — implemented in the repo
Confirmed the root cause in code:
requestSummary(src/preflight.ts) spread all main-request headers into the independently-built summary call. The main Codex request carriesx-openai-internal-codex-responses-lite(its body hasreasoning.context: all_turns), but the summary body is built independently with no reasoning field — so the backend rejected the summary with 400 "…requiresreasoning.contextto beall_turns".The fix strips that one request-shape-specific header from the summary call only. The original model request keeps the header and its reasoning fields; auth and routing headers are preserved. The strip is keyed on the Codex header name, so it is a no-op for Anthropic/OpenAI.
Why this is the right layer: the Lite header is a request-shape signal tied to the main body's
reasoning.context. The summary is a separate, independently-constructed body, so the correct place to drop the header is where the summary request is built — not by rebuilding the whole header set, and not by adding a reasoning field to the summary body (which would change the summary semantics).Regression coverage (per your spec)
Verification
npm run typecheck✓npm test✓ (1252/1252)npm run build✓Both compression modes considered: the change is scoped to the summary request's headers only — the summary carrier logic (the tool call in plugin mode, the
acp_summarymessage in proxy mode) is untouched.The PR now carries both fixes:
max_output_tokenswhen the upstream rejects it (scoped per session+upstream+model)Merge is yours to do (repo rules keep merging human-only).
Wuxie233 commentedon Sep 9, 2026
A distinct preflight failure remains after both summary-compatibility fixes. The summary request succeeds, but applying it fails because preflight uses raw
m...references for content already covered by an active block:This is a different failure from the HTTP 400 compatibility errors in #665. We are validating against v0.1.97 / acp-kernel 0.0.61 while retaining the two compatibility fixes. The investigation is checking both stale token baselines and whether preflight correctly summarizes the rendered active block and applies it using block references. Merely relabeling raw-message summaries as block rewrites would not respect the block boundary contract.
Wuxie233 commentedon Sep 9, 2026
The new preflight failure is now fixed and deployed locally on v0.1.97 (acp-kernel 0.0.61), retaining the earlier Responses-summary compatibility fixes. PR #665 is still open and does not cover this range-selection failure.
The fix previews kernel compression without mutating the session, skips unusable already-covered ranges, and summarizes the exact content the kernel would consume, including tool/reasoning pair extensions and active child summaries. Oversized content is summarized in bounded segments; compression is applied only after all segments succeed and the assembled summary is valid. If normal ranges are exhausted while still over-window, preflight retries with recent soft protection relaxed. Hard tool protection remains enforced. Non-positive maxSummaryLength remains unlimited, matching kernel semantics.
Validation: 1260 existing/current regression tests passed before the final unlimited-length correction; 36 focused preflight tests and the final 8 range-boundary tests passed, with typecheck/build passing. An isolated real-upstream replay of the failing history reduced the estimate from 287565 to 16389 tokens across 3 ranges. Coverage auditing confirmed all 262 sampled positions from the largest tool result entered summary requests; the source session remained unchanged. Synthetic tests additionally verify complete tool-result coverage and preservation when a summary segment fails. Post-deployment shared-daemon reply and harmless execution-tool probes both completed successfully. Recovery in the original interactive task still requires resuming that task.
ranxianglei commentedon Sep 9, 2026
Thanks for the detailed report and validation — this lines up with what I was tracing (I pulled the acp-kernel 0.0.61 source to follow the reject):
applySingleRange): a raw range whose messages are all already covered by active block(s) is rejected with "To rewrite or merge those blocks, reference them by block ID".src/preflight.ts) selects ranges fromprocessTurn'scompressibleRangesand applies them viaapplyRangeswith raw refs; when a selected range turns out already-covered, the current code treats the[Compression FAILEDas a hard fail-fast 502 instead of skipping the range.Since PR #665 only covers the two Responses-summary compatibility fixes (
max_output_tokens+ the Lite header) and not this range-selection failure, this needs its own PR (or an extension of #665).To land it in the repo: could you share the diff/branch (or point me to it)? I'll review it and open the PR. Alternatively, if you'd rather I implement it in the repo from your description, I can — but given the size (preview-without-mutation + segment-based summarization + range-skipping), your validated diff would be more accurate.
Wuxie233 commentedon Sep 9, 2026
I have opened #668 with the validated range-recovery fix and eight focused regressions. The diff is available directly in that PR, and the branch is Wuxie233/billion-context:2026-09-10_preflight-covered-range-recovery (commit 71c6695).
It is based on current master and deliberately excludes the two Responses compatibility fixes already addressed by #665. The recovery implementation is the same patch used in the locally validated combined candidate. Typecheck/build pass; the standalone full suite and GitHub CI are running. The PR documents which live validation used the combined candidate and the remaining pre-merge E2E check, so those results are not confused with standalone coverage.
ranxianglei commentedon Sep 9, 2026
Reviewed #668 (2 files:
src/preflight.ts+ newtests/preflight-covered-ranges.test.ts, +167/−28).Diff cleanliness (first gate): PASS. No unrelated changes, no whitespace/reformat, no generated artifacts. The
renderRange→splitSummaryContentreplacement is clean and the change is scoped to the range-selection path.Correctness: PASS. I traced the key mechanics against the acp-kernel 0.0.61 source:
deps.core.applyCompression(...)clones the state (kernelapplyCompression→cloneState), so the session is not mutated. The kernel catches per-range errors (records them inresult.errors, doesn't throw), so an already-covered range produces no new block →plannedisundefined→continue(skip). This is the core of the fix: the host no longer fail-fast 502s on an already-covered range.collectBlockContent(deps.session.state, planned, messages, { full: false })(kernel index.js:2922) renders nested active child blocks as their summaries (not expanded) and the direct messages in full — i.e. exactly the content the kernel would consume, with child blocks preserved as summaries.full: falseis the right choice.splitSummaryContentreuses the samebudgetassplitChunks(Math.max(MIN_CHUNK_TOKENS, Math.floor(limit * CHUNK_FRACTION)), src/preflight.ts:470), and compression is applied only when every segment succeeds and the assembled summary is withinmaxSummaryLength(non-positive = unlimited, matching kernel semantics).relaxedflag ensures this happens at most once.Minor note (non-blocking): the unknown-baseline
foldedestimate (src/preflight.ts, thefolded =line) sumsplanned.directMessageIdschar lengths, which excludes tool/reasoning pair expansions (those live ineffectiveMessageIds). This slightly under-estimates the fold when the baseline is unknown, but it's a char-based heuristic and your unknown-baseline regression passes, so it's fine in practice.Validation: your real-upstream replay (287565 → 16389 across 3 ranges), the 262-position coverage audit, and the 8 targeted regressions are strong evidence. The test-matrix CI passes; the
ENEEDAUTHon the npm PR-tag publish is a fork-credential limitation, not a compile/test failure.Pre-merge check: the gated four-phase client E2E suite (
tests/e2e/e2e-codex.test.ts) has not been rerun for this standalone branch. Per AGENTS.md it's required before merging changes to the request pipeline (src/preflight.tsis in scope) — it's the only coverage that exercises real client behavior (codex UA, wire quirks, retry loops). Please run it (or confirm it's covered) before merge.Verdict: the fix is correct and the diff is clean; ready to merge once the pre-merge E2E check is done. (I can't merge — that's human-only.)
中文摘要:修复了 preflight 选中"已被活跃 block 覆盖的原始 range"时内核拒绝导致的 fail-fast 502——改为无副作用预览内核压缩边界、跳过已覆盖 range、按内核实际消费内容(含 tool/reasoning 配对扩展与子 block 摘要)分段摘要、全部分段成功且满足长度限制才落盘,并在常规 range 耗尽时放宽软保护重试;diff 干净、逻辑正确,合并前需补跑四阶段 E2E。
ranxianglei commentedon Sep 9, 2026
[bot] 🏷 Status update on #663:
Both fixes are now in open, reviewed PRs, ready for merge:
max_output_tokenscapability learning (scoped to session + upstream URL + model) + the Codex Lite header strip (summary calls drop it, main request keeps it). Typecheck/test/build all passed.foldedestimate sumsdirectMessageIds, which excludes tool/reasoning pair expansions — fine in practice since it's a char-based heuristic and your unknown-baseline regression passes).Both PRs are confirmed open and unmerged. The remaining steps are human-only:
Nothing further is actionable on my end until then.
ranxianglei commentedon Sep 9, 2026
继续
ranxianglei commentedon Sep 10, 2026
ework 继续
ranxianglei commentedon Sep 10, 2026
继续这个
fix: drop the Codex Lite header from preflight summary calls
ranxianglei commentedon Sep 10, 2026
Continued this round — concrete progress on the merge path:
PR #665 rebased onto current master and re-pushed. The branch had been rebased locally in an earlier session but never pushed — the PR head was still on the stale pre-v0.1.98 base (
3c35be1). I verified the local rebase preserved byte-identical patch content, then force-pushed the feature branch. New head:128f9d3(on top ofc4c2e64, the v0.1.98 release merge). Re-ran full validation on the rebased branch: typecheck ✓, 1274/1274 tests ✓. GitHub CI on the new head: ubuntu test matrix green (node 22 + 24), version-guard + build-artifact green; the two windows legs were still running when I checked.Conflict risk between #665 and #668 eliminated. Both PRs touch
src/preflight.ts, so I checked them against each other explicitly:git merge-tree: fix(preflight): preserve complete content when recovering blocked ranges #668 → current master: clean, no conflicts.git merge-tree: fix: Codex preflight summary compatibility (max_output_tokens + responses-lite header) #665 (rebased) + fix(preflight): preserve complete content when recovering blocked ranges #668: clean, no conflicts.Still blocked on one item I can't do here: the gated four-phase client E2E (
tests/e2e/e2e-codex.test.ts) requires a live Responses-compatible upstream — the default local sglang endpoint is connection-refused from this environment, so neither I nor CI can cover it here. Per AGENTS.md it's required before merging request-pipeline changes (src/preflight.tsis in scope). Wuxie233's live replay of the failing history (floor 13) is strong evidence for #668 specifically, but it isn't the four-phase suite.Remaining human-only steps:
中文摘要:本轮把 #665 变基到 v0.1.98 后的 master 并重新推送(补丁内容逐字节一致,全量测试 1274/1274 通过、CI ubuntu 矩阵绿),并验证了 #665 与 #668 两个都改 src/preflight.ts 的 PR 互不冲突(实际合并后 1282/1282 测试通过、typecheck/build 绿);剩下的只有人工步骤——合并前补跑四阶段 E2E(本环境无可用上游,跑不了)和合并两个 PR。
fix: drop the Codex Lite header from preflight summary calls
fix: detect stream-mandating upstreams by 400 shape, not body wording (…
test: calibrate knife-edge fit fixtures for the raised tool-schema ov…
test: calibrate knife-edge fit fixtures for the raised tool-schema ov…
test: calibrate knife-edge fit fixtures for the raised tool-schema ov…
test: calibrate knife-edge fit fixtures for the raised tool-schema ov…
test: restore margin in #663 anthropic fixture knife-edged by the att…
test: calibrate knife-edge fit fixtures for the raised tool-schema ov…
test: calibrate knife-edge fit fixtures for the raised tool-schema ov…
test: #3726 re-tune knife-edge e2e margins for the +~300 est-tok syst…