Skip to content

[WRONG BRANCH] fix(combos): reserve output headroom before a combo fallback - #4750

Merged
lidge-jun merged 5 commits into
codex/cf1-definite-context-overflowfrom
codex/cf2-output-headroom
Sep 16, 2026
Merged

lidge-jun merged 5 commits into
codex/cf1-definite-context-overflowfrom
codex/cf2-output-headroom

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Sep 16, 2026

Copy link
Copy Markdown
Owner

Summary

Carries #4664 by @RHODIZSECURITY onto current dev, stacked on #4744. Base is codex/cf1-definite-context-overflow; retarget to dev once the parent lands.

A combo could route a large turn onto a fallback whose total context window cannot hold the input plus the output allowance the caller asked for. That target answers 200, emits a few hundred tokens, and stops on finish_reason: length — which the Anthropic surface renders as Claude's response exceeded the 64000 output token maximum, naming a limit the model never approached. Raising CLAUDE_CODE_MAX_OUTPUT_TOKENS only changes the number in that message. By the time it happens output has committed, so no later target may be tried.

A combo child is now admitted against both budgets before dispatch. When the caller declared max_output_tokens, checkComboTargetInputAdmission requires estimated input <= input ceiling and estimated input + min(declared output, target output ceiling) <= context window, and refuses locally with 413 input_admission_refused before any upstream bytes are sent. Combo policy already treats that local code as a safe hop, so the ladder selects a larger-context target without replaying committed output.

Three things differ from the original patch:

  • The reserve is counted once. resolveInputCeiling answers "how much input may this target take", and modelMaxInputTokens can tighten it below the window. The original checked input + headroom <= ceiling, which charges the output reserve against an already-tightened input budget and would skip a target that fits. The window is what input and output actually share, so the two conditions are now evaluated separately against the budget each one belongs to.
  • The generated-metadata fallback is described honestly. The original called it a fallback for "supported native slugs"; gpt-5.3-codex-spark is in RETIRED_NATIVE_OPENAI_MODELS on current dev. The fallback deliberately covers retired slugs, because a retired slug is still dispatchable when an operator names it explicitly in a combo target — which is precisely the configuration where the gate was inert. The comment and the test now say that.
  • Reservation size is justified against the alternative. The common reservation used by other harnesses is min(max_output, 20k); that leaves 100k + 20k inside a 128k window, so the reported turn would still be admitted and still fail upstream. min(declared, target ceiling) is what actually catches it.

Scope stays narrow. Direct and single-target requests keep the deliberately loose 2.5x pathological-input gate, because they have nowhere to hop. Compaction turns stay exempt. Unknown context and a caller that declared no output allowance both remain fail-open, so no limits are invented for custom providers.

Verification

No local suite, no focused test file, no typecheck, no build and no dependency install was run — this lane is under an explicit owner instruction forbidding local execution. Evidence is static source reading plus hosted CI.

Static checks performed:

  • Verified the generated metadata rows by hand in src/generated/model-metadata.ts: openai carries ["gpt-5.3-codex-spark",128000,32000,...] and openai-codex carries ["gpt-5.3-codex-spark",128000,128000,...]. Output ceiling therefore resolves through nativeOpenAiMaxOutputTokens, which reads the openai row (32k), not the openai-codex row.
  • Confirmed the slug is absent from NATIVE_OPENAI_MODELS and the capability-alias list (it appears only in RETIRED_NATIVE_OPENAI_MODELS, native-models.ts:181), so PINNED_NATIVE_CAPABILITY_ENTRIES does not hold it and the generated fallback is the path actually taken.
  • Re-derived every existing resolveInputCeiling assertion in tests/server/input-admission.test.ts against the refactor. The generated fallback is consulted only when the native lookup returns null, so the pinned 272k Sol window, the impostor-provider null, the routed provider/model null and the explicit 50k override are all unchanged.
  • Searched tests/ for assertions that depend on canonical native admission resolving nothing for a previously unknown slug; there are none.
  • Confirmed the import stays inside the core boundary: src/generated/model-metadata.ts has no imports at all, so core.ts → request-prepare.ts → input-admission.ts → generated/model-metadata.ts cannot reach src/lab (tests/lab/core-lab-boundary.test.ts).
  • File-size ratchet: input-admission.ts 194 → 304, request-prepare.ts 970 → 975, both far under the 2,000-line threshold and under the separate RESPONSES_CORE_MODULES 2,000-line owner assertion. server-combo-failover-e2e.test.ts 4,128 → 4,110 against its frozen 4,166 cap.
  • structure/transports/responses.md owns src/server/ per structure/manifest.json and is updated; it is already in grace.oversizeDocs, which is a boolean allowlist with no recorded size, so the addition does not trip the budget check.

Hosted CI: this layer is not the lane tip, so its head commit carries [skip ci] under the maintainer-approved tip-only policy. The gating run is on the tip PR of this stack.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

…[skip ci]

A combo could route a large turn onto a fallback whose total context window
cannot hold the input plus the output allowance the caller asked for. That target
answers 200, emits a few hundred tokens and stops on finish_reason: length, which
the Anthropic surface renders as "response exceeded the output token maximum"
naming a limit the model never approached. Raising CLAUDE_CODE_MAX_OUTPUT_TOKENS
only changes the number in that message. By the time it happens, output has
committed and no later target may be tried.

Admit a combo child against both budgets before dispatch. When the caller
declared max_output_tokens, require estimated input <= input ceiling AND
estimated input + min(declared output, target output ceiling) <= context window,
and refuse locally with 413 input_admission_refused before any upstream bytes are
sent. Combo policy already treats that local code as a safe hop, so the ladder
selects a larger-context target without replaying committed output.

The two budgets are checked separately on purpose. resolveInputCeiling already
answers "how much input may this target take", and modelMaxInputTokens can
tighten it below the window; charging the output reserve against that tightened
number would count the reserve twice and skip a target that fits. The window is
what input and output actually share, so the reserve belongs there.

Reserving min(declared, target ceiling) rather than a fixed slice is what makes
this catch the reported case: the common industry reservation of
min(max_output, 20k) leaves 100k + 20k inside a 128k window, so the turn is
admitted and fails upstream anyway.

Canonical native slugs that the narrower pinned table does not carry now resolve
their window from the generated in-tree bundle. That table gap is why the gate
was completely inert on the route where this was observed. The bundle is
compiled in, not a catalog read, so this adds no I/O, and explicit provider and
operator caps may only narrow the result. It deliberately covers slugs retired
from the picker, because a retired slug is still dispatchable when an operator
names it explicitly in a combo target, which is exactly that configuration.

Scope stays narrow. Direct and single-target requests keep the deliberately loose
2.5x pathological-input gate, because they have nowhere to hop. Compaction turns
stay exempt. Unknown context and a caller that declared no output allowance both
remain fail-open, so no limits are invented for custom providers.

Closes #4664

Co-authored-by: RHODIZ IT <info.rhodiz@gmail.com>
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 16, 2026 02:14
@coderabbitai

coderabbitai Bot commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (2)
  • ^dev$
  • ^preview$

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: a8ab25e3-ddf8-4fe7-9743-05eef49fec1a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 16, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-16T02:19:30.105251Z bc648a2 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the bug Something isn't working label Sep 16, 2026
@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 76 / 80

이 PR은 RHODIZSECURITY의 #4664를 현재 dev tip 3070d64d8(package 2.57.0, #4714 forced default effort + #4713 Usage a11y) 위에 다시 올린 콤보 프리플라이트 수정이다. 부모 스택은 #4744(codex/cf1-definite-context-overflow, 제로 출력 context overflow hop)이고, 이 레이어 head는 codex/cf2-output-headroom / bc648a2f8다. 지금 tip의 checkInputAdmission은 입력만 본다. 그래서 콤보 사다리가 큰 턴을 “입력은 들어가지만 입력+요청 출력은 창에 안 들어가는” 작은 폴백으로 보낼 수 있다. 그 타깃은 200을 내고 수백 토큰만 쓴 뒤 finish_reason: length로 끊긴다. Anthropic 표면은 이걸 “출력 토큰 최대치를 넘겼다”처럼 보여 주는데, 실제 원인은 총 컨텍스트 창이다. 출력 바이트가 이미 커밋되면 다음 타깃으로 못 간다.

고침은 콤보 자식(options.comboAttempt)만 엄격 게이트로 바꾼다. src/server/responses/input-admission.tsresolveContextLimits / resolveOutputCeiling / checkComboTargetInputAdmission을 두고, 호출자가 max_output_tokens를 밝힌 경우에만 (1) estimated input <= ceiling(입력 전용 예산)과 (2) estimated input + min(선언 출력, 타깃 출력 천장) <= window(입출력이 실제로 나누는 창)를 둘 다 본다. 거절은 로컬 413 input_admission_refused라서 기존 콤보 정책이 안전 hop으로 처리한다. 직접·단일 타깃은 예전 2.5배 pathological 게이트를 유지하고, 컴팩션·알 수 없는 창·출력 미선언은 fail-open이다. 원본 #4664와 다른 세 가지는 본문이 말한 그대로다. 예약을 한 번만 세고, retired slug(gpt-5.3-codex-spark)도 생성 메타로 창을 열어 게이트가 눈먼 경로를 막으며, min(선언, 타깃 천장)으로 20k 같은 약한 예약이 구멍을 남기지 않게 한다.

request-prepare.ts 호출부는 tip의 기존 admission 자리(comboAttempt 분기)만 갈아끼운다. 회귀는 tests/server/input-admission.test.ts의 콤보 블록과 tests/helpers/combo-context-headroom-cases.ts(스킵 전 커밋 금지 / 출력 미선언 시 느슨 게이트 / prompt-too-long 400 hop)로 e2e에 꽂힌다. structure/transports/responses.md에 Combo output headroom 절이 추가됐다. 파일 크기도 본문대로 admission 194→304, request-prepare 970→975, e2e는 헬퍼 추출로 줄어 cap 안이다. types/config 분할·pre-split godfile 모놀리스를 안 건드린다. close-don't-rebase 대상이 아니다.

다만 머지 레인은 아직 막혀 있다. base가 dev가 아니라 #4744 브랜치다. mergeStateStatus는 UNSTABLE이고, head에 [skip ci]가 있어 resolve/hygiene/label만 돌고 본 스위트는 없다. 본문은 “스택 tip이 게이트”라고 쓰지만, codex/cf2-output-headroom을 base로 한 열린 tip PR은 지금 없다. 원본 #4664도 아직 OPEN이다. 그래서 이 SHA만으로는 hosted 증거가 비어 있고, #4744가 먼저 랜딩한 뒤 retarget하거나 실제 tip에서 그린 CI를 봐야 한다.

라인/심볼로 보면 아래가 맞다.

라인 266-285 (input-admission.ts · checkComboTargetInputAdmission) - 창/입력 예산을 분리해 예약을 한 번만 센다. 콤보에서만 쓰는 계약이 tip 방향과 맞다.
라인 884-887 (request-prepare.ts · headroom 거절 메시지) - 문구는 “context window”인데 숫자는 ceiling이다. modelMaxInputTokens로 ceiling이 window보다 작아진 뒤 입력 캡만 실패한 경우에도 headroom 문장이 나가고, headroom 실패인데도 window가 아니라 ceiling이 찍힌다. 결과에 window를 실어 메시지에 쓰거나, 실패 원인별로 문장을 갈라야 한다.
라인 207-208 (input-admission.ts · generatedNativeWindow) - OPENAI_CODEX_PROVIDER_ID가 이미 "openai"라서 getModelMetadata("openai", …)를 두 번 같은 키로 조회한다. 동작은 spark 128k를 여는 쪽에 맞지만, 두 번째 인자가 죽은 코드다. openai-codex를 의도했다면 키를 고치고, 아니면 한 줄로 줄이면 된다.
라인 231-246 (input-admission.ts · resolveOutputCeiling) - 출력 천장은 nativeOpenAiMaxOutputTokens → 생성 openai 행( spark 32k )을 타서 테스트 기대와 맞다. 창 폴백과 달리 생성 번들을 직접 읽지 않아도 spark에는 충분하다.
tests/helpers/combo-context-headroom-cases.ts - “작은 타깃 0히트 / 큰 타깃 1히트”와 출력 미선언 fail-open을 e2e로 고정한다. tip의 combo-context-overflow 헬퍼 패턴과 같다.
CI / [skip ci] / base=#4744 / mergeStateStatus=UNSTABLE - 이 exact SHA 본 스위트 증거가 없다. enforce-target은 cancelled.
PR #4664 (OPEN) - 원본 carry 대상. 머지 후 landed-via로 닫을 이슈/PR이다.

메인테이너의 판단이 필요한 지점

  • #4744를 먼저 머지한 뒤 이 PR을 dev로 retarget할지, 아니면 두 레이어를 한 스택 tip에서 같이 게이트할지
  • 본문이 말한 tip 게이트 PR이 아직 없는데, 이 head의 [skip ci]를 그대로 둘지 이 SHA에서 본 CI를 돌릴지
  • headroom 거절 메시지에 ceiling 대신 window(또는 실패 원인)를 노출하는 수정을 이 PR에 넣을지 후속으로 둘지
  • 머지 후 #4664를 Landed via #4750 + landed-via-maintainer로 닫을지

너의 추천
방향은 tip에 맞고, 콤보가 “길이로 죽은 것처럼 보이는” 가짜 출력 한도를 사다리 앞에서 잘라 내는 실사용 버그 픽스다. 범위도 admission + request-prepare + 회귀 + structure 문서로 깨끗하다. 지금은 #4744 랜딩·dev retarget(또는 실제 tip CI) 전에는 머지하지 말고 스택을 유지하라. 메시지 ceiling/window 불일치와 중복 getModelMetadata("openai")는 작지만 이 PR에서 고치면 더 좋다. types/config 분할에 걸려 닫을 대상은 아니다. 머지 후 #4664는 landed-via로 닫아 open 카운트를 줄여라.

이 댓글은 grok-bot이 작성했습니다

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bc648a2f84

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +237 to +238
const configured = positive(modelRecordValue(provider.modelMaxOutputTokens, modelId))
?? positive(provider.defaultMaxOutputTokens);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Stop treating wire defaults as output ceilings

modelMaxOutputTokens and defaultMaxOutputTokens are documented wire defaults, not capability limits (structure/config.md:212-217), and adapters such as openai-chat.ts:49-52 and anthropic.ts:963-967 let an explicit request override them. Thus, for example, a 128k-window combo target with an 80k input, a configured 32k default, and an explicit 64k output request is admitted after reserving only 32k, but the adapter sends 64k and recreates the context-overflow/truncation this change is intended to prevent. Reserve the full explicit allowance unless a genuine per-model capability ceiling is obtained through the canonical catalog/derivation path.

AGENTS.md reference: src/AGENTS.md:L18-L18

Useful? React with 👍 / 👎.

lidge-jun and others added 4 commits September 16, 2026 11:28
…ws [skip ci]

OPENAI_CODEX_PROVIDER_ID is the routing provider name, and its value is the
string "openai". Using it to index the generated bundle therefore skipped the
native Codex rows entirely and read the public API rows instead. The two agree on
Spark's 128k window, so the case that motivated the fallback still resolved, but
any slug where they differ would have taken the wrong window -- and
gpt-5-codex-mini exists only in the native catalog, so it resolved nothing at all.

Name the catalog keys explicitly and say in a comment why the provider id is not
one of them.

Co-authored-by: RHODIZ IT <info.rhodiz@gmail.com>
…llowance [skip ci]

The no-declared-allowance row passed `undefined` as the second argument of a
builder whose parameter has a default. A default parameter applies to an explicit
`undefined`, so the row built a request carrying 64,000 max output tokens and then
asserted that no output reserve was applied. It would have asserted the opposite
of what it covers, and it would have done so by passing.

Split the builder in two so the no-allowance case cannot silently acquire one.
The remembered model id is provider-reported and arrives on the response, so
nothing upstream of the recall store bounds its length. Lane keys are already
SHA-256 digests, which means the 256-lane cap bounded the number of entries but
not the bytes those entries held. A long-running process could accumulate
arbitrarily large remembered strings.

Bound retention on two more axes: 1 KiB per remembered model id and 64 KiB in
aggregate. The size test runs on code units before encoding, because a UTF-8
encoding is never smaller than its code-unit count, so the bound never pays the
allocation it exists to prevent. Aggregate eviction drops the least recently
written lane, which is the front of the map because every write re-inserts its
own lane at the back. A single entry is capped far below the aggregate budget, so
a write can never evict itself.

Every removal now goes through one helper that releases the entry's bytes, so the
counter cannot drift from the map through the read-time invalidation path, the
reconciliation path, or a lane rewrite.

An unretainable model id DECLINES the write rather than clearing the lane. That
is the ordering-sensitive part. This callback carries a config generation, not a
request order, so two accepted completions on one lane under the same generation
can arrive out of order; a clearing branch would let the older one erase the
newer selection. Declining matches how every other rejection in
rememberComboForLane already returns, and leaves the established contract intact:
an older response never overwrites or clears a newer one.

Register the store for periodic expiry as well. The TTL was previously evaluated
only on read or on a generation change, so a lane that is never read again held
its entry until the process exited.

Closes #4525

Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
fix(responses): bound combo recall model retention
@lidge-jun

Copy link
Copy Markdown
Owner Author

Cascading the lane downward. This branch now carries the whole lane; its head tree matches the verified tip tree exactly.

Evidence at the exact head 246d703 (tree 4610771b34d2db6bc9d077257d51c145e3dc702e):

  • Heavy jobs actually executed rather than being path-filtered, read through the check-runs API rather than the check rollup: test 1-4/4 all completed with conclusion success, macos 1-2/2 succeeded, and the aggregate ci check completed with conclusion success.
  • gates, changes, storage policy, api usage, hygiene, docker smoke, keyring on three platforms, npm-global on three platforms and react-doctor all succeeded.
  • The windows shard matrix and macos control are workflow_dispatch-only and always skipped on pull_request. This lane carries no Windows-specific change, so that skip withholds no relevant evidence.
  • enforce-target is cancelled by workflow concurrency on the pr-gate-comment group, with a rerun queued behind the runner backlog. The conditions that check validates were confirmed directly: the base is the layer below, all three template sections are present, 6 changed files with none under gui/ and no truncated file list.
  • Ancestry verified so each layer closes as MERGED: codex/cf1-definite-context-overflow and codex/cf2-output-headroom are both ancestors of this tip.

Chained-child stacks merge top-down, so this lands in the parent branch and cascades to dev. CI evidence transfers by tree identity at each step.

Maintainer integration decision under MAINTAINERS.md / AGENTS.md, recorded with the exact-head evidence above.

@lidge-jun
lidge-jun merged commit 99749e1 into codex/cf1-definite-context-overflow Sep 16, 2026
6 checks passed
@lidge-jun
lidge-jun deleted the codex/cf2-output-headroom branch September 16, 2026 03:49
@github-actions

github-actions Bot commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

⏳ DRAFT

  • wrong target branch (codex/cf1-definite-context-overflow); retarget to dev.

What to do

  • Retarget this PR to dev — all contributions go to dev.

Its title has been prefixed with [WRONG BRANCH].
Automatic draft conversion failed (token cannot change draft status). Please convert this pull request to a draft manually. The required enforce-target check will keep failing until every issue above is resolved.

@github-actions github-actions Bot changed the title fix(combos): reserve output headroom before a combo fallback [WRONG BRANCH] fix(combos): reserve output headroom before a combo fallback Sep 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant