Skip to content

fix: Codex preflight summaries fail with unsupported max_output_tokens #663

Closed

Description

@Wuxie233

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

  1. Use the Codex ChatGPT Responses backend with valid authentication (credentials omitted).
  2. Trigger proxy-side preflight compression for an over-window conversation.
  3. src/preflight.ts builds a Responses summary payload with max_output_tokens: MAX_SUMMARY_OUTPUT_TOKENS.
  4. 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

  1. Wuxie233 commented on Sep 9, 2026

    @Wuxie233
    ContributorAuthor

    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_tokens causes 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.

  2. ranxianglei commented on Sep 9, 2026

    @ranxianglei
    Owner

    ework接单

  3. Wuxie233 commented on Sep 9, 2026

    @Wuxie233
    ContributorAuthor

    Follow-up from the real Codex client: after fixing the output-limit rejection, preflight can still fail because requestSummary copies the main request headers into its independently constructed summary body. With X-OpenAI-Internal-Codex-Responses-Lite: true, the backend rejects that body:

    X-OpenAI-Internal-Codex-Responses-Lite requires `reasoning.context` to be `all_turns`.
    

    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.

  4. Wuxie233 commented on Sep 9, 2026

    @Wuxie233
    ContributorAuthor

    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.

  5. added a commit that references this issue on Sep 9, 2026
    3c35be1
  6. ranxianglei commented on Sep 9, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    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 carries x-openai-internal-codex-responses-lite (its body has reasoning.context: all_turns), but the summary body is built independently with no reasoning field — so the backend rejected the summary with 400 "…requires reasoning.context to be all_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)

    • summary/main header separation: summary drops the Lite header, main request keeps it
    • auth + routing preservation: summary keeps the auth header and is routed to the same upstream
    • Anthropic unchanged: the strip is a no-op (Anthropic never sends the Lite header); summary keeps auth, fold + forward OK

    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_summary message in proxy mode) is untouched.

    The PR now carries both fixes:

    1. learn to drop max_output_tokens when the upstream rejects it (scoped per session+upstream+model)
    2. drop the Codex Lite header from the summary call only

    Merge is yours to do (repo rules keep merging human-only).

  7. Wuxie233 commented on Sep 9, 2026

    @Wuxie233
    ContributorAuthor

    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:

    compress requested 1 range: m00206..m00206
    Range ... contains no new compressible messages — every message in it is already covered by active block b8. To rewrite or merge those blocks, reference them by block ID.
    preflight fail-fast 502: no range could be compressed across 1 viable range
    

    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.

  8. Wuxie233 commented on Sep 9, 2026

    @Wuxie233
    ContributorAuthor

    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.

  9. ranxianglei commented on Sep 9, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    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):

    • The 502 is the kernel's new block-boundary contract (acp-kernel 0.0.61 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".
    • The host preflight (src/preflight.ts) selects ranges from processTurn's compressibleRanges and applies them via applyRanges with raw refs; when a selected range turns out already-covered, the current code treats the [Compression FAILED as a hard fail-fast 502 instead of skipping the range.
    • Your fix (preview-without-mutation, skip already-covered ranges, summarize the exact kernel-consumed content including tool/reasoning pair extensions + active child summaries, bounded segments, apply only after all segments succeed) addresses the root cause correctly. Your note that "merely relabeling raw→block" would violate the block boundary contract is right — a raw range's summary doesn't match a block's scope, so the correct move is to skip the already-covered range (or do a proper tier-2 block re-summary), which is what your fix does.

    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.

  10. Wuxie233 commented on Sep 9, 2026

    @Wuxie233
    ContributorAuthor

    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.

  11. ranxianglei commented on Sep 9, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    Reviewed #668 (2 files: src/preflight.ts + new tests/preflight-covered-ranges.test.ts, +167/−28).

    Diff cleanliness (first gate): PASS. No unrelated changes, no whitespace/reformat, no generated artifacts. The renderRange → splitSummaryContent replacement 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:

    • Preview without mutation — deps.core.applyCompression(...) clones the state (kernel applyCompression → cloneState), so the session is not mutated. The kernel catches per-range errors (records them in result.errors, doesn't throw), so an already-covered range produces no new block → planned is undefined → continue (skip). This is the core of the fix: the host no longer fail-fast 502s on an already-covered range.
    • Exact content collection — 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: false is the right choice.
    • Bounded segments — splitSummaryContent reuses the same budget as splitChunks (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 within maxSummaryLength (non-positive = unlimited, matching kernel semantics).
    • New relaxation path — when ordinary candidates are exhausted while over-window, soft protection is relaxed and the loop retries; hard tool protection stays enforced. The relaxed flag ensures this happens at most once.

    Minor note (non-blocking): the unknown-baseline folded estimate (src/preflight.ts, the folded = line) sums planned.directMessageIds char lengths, which excludes tool/reasoning pair expansions (those live in effectiveMessageIds). 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 ENEEDAUTH on 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.ts is 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。

  12. ranxianglei commented on Sep 9, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 Status update on #663:

    Both fixes are now in open, reviewed PRs, ready for merge:

    • PR fix: Codex preflight summary compatibility (max_output_tokens + responses-lite header) #665 (my implementation) — the two Responses-summary compatibility fixes: max_output_tokens capability 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.
    • PR fix(preflight): preserve complete content when recovering blocked ranges #668 (your range-recovery fix) — reviewed against the acp-kernel 0.0.61 source: diff is clean (2 files), logic is correct (preview-without-mutation, skip already-covered ranges, exact content collection with child blocks as summaries, bounded segments, new relaxation path). One minor non-blocking note (the unknown-baseline folded estimate sums directMessageIds, 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:

    1. Run the pre-merge four-phase E2E check for fix(preflight): preserve complete content when recovering blocked ranges #668 (needs a live Responses-compatible upstream — required per AGENTS.md before merging request-pipeline changes; I can't run it here as no upstream is reachable in this environment).
    2. Merge PR fix: Codex preflight summary compatibility (max_output_tokens + responses-lite header) #665 and PR fix(preflight): preserve complete content when recovering blocked ranges #668 (human-only — I can't merge PRs).

    Nothing further is actionable on my end until then.

  13. ranxianglei commented on Sep 9, 2026

    @ranxianglei
    Owner

    继续

  14. ranxianglei commented on Sep 10, 2026

    @ranxianglei
    Owner

    ework 继续

  15. ranxianglei commented on Sep 10, 2026

    @ranxianglei
    Owner

    继续这个

  16. added a commit that references this issue on Sep 10, 2026
    128f9d3
  17. ranxianglei commented on Sep 10, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    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 of c4c2e64, 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:

    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.ts is 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:

    1. Run the four-phase E2E against a live upstream (or confirm coverage another way).
    2. Merge PR #665 and PR #668 — either order, verified conflict-free.

    中文摘要:本轮把 #665 变基到 v0.1.98 后的 master 并重新推送(补丁内容逐字节一致,全量测试 1274/1274 通过、CI ubuntu 矩阵绿),并验证了 #665 与 #668 两个都改 src/preflight.ts 的 PR 互不冲突(实际合并后 1282/1282 测试通过、typecheck/build 绿);剩下的只有人工步骤——合并前补跑四阶段 E2E(本环境无可用上游,跑不了)和合并两个 PR。

  18. added a commit that references this issue on Sep 10, 2026
    de976ae
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions