Skip to content

fix(codex): gate account-native models by entitlement - #2146

Merged
lidge-jun merged 1 commit into
devfrom
codex/absorb-account-entitlement-stacked
Aug 20, 2026
Merged

fix(codex): gate account-native models by entitlement#2146
lidge-jun merged 1 commit into
devfrom
codex/absorb-account-entitlement-stacked

Conversation

@lidge-jun

Copy link
Copy Markdown
Owner

Summary

gpt-daybreak-blue-latest lives in the static native set, so catalog sync copied it onto every account selector and Pool could bind a bare Daybreak request to an account whose authenticated roster never contained it. The upstream answered The 'gpt-daybreak-blue-latest' model is not supported when using Codex with a ChatGPT account. (#2097).

This makes the authenticated ChatGPT roster the source of truth: entitlement is discovered per account, the gated row is advertised only where an eligible account confirms it, and an account that cannot serve the model is skipped before selection. Discovery failures fail closed — the row disappears rather than being offered on unproven evidence.

Credit: @Ingwannu's #2101 is the implementation, the 333-line entitlement module, and the test suite.

Stacked on #2137 — merge that first

Stack (merge bottom-up):

# PR Layer Review focus
2 this PR account entitlement gating entitlement discovery, catalog projection, Pool/Direct selection
1 #2137 ← base bearer admission predicate the substituteMainCredential contract

Depends on #2137. Review this PR's diff only.

This is a real dependency, not a convenience: this change passes substituteMainCredentialForDirect: substituteMainCredential into resolveCodexAuthContext, consuming the exact value #2137 corrects. Landing it on dev alone would silently reintroduce #2132 for every routed provider.

It is the only genuine stack edge in this backlog — every other absorbed PR was disjoint and shipped as a sibling.

Three corrections

Selector compact missed the wire rewrite. accountGatedCompactWireModel was derived from the caller's raw model string, and an account-qualified selector like side/gpt-daybreak-blue-latest does not match the gated map (slugsEquivalent reads the account namespace as a routed provider prefix). It still took the native compact endpoint the guard exists to avoid. Now derived from route.modelId, the same value applyCodexAccountGatedWireNormalization uses.

Direct callers shared one 64-entry roster cache with main/Pool. Direct keys are per-credential and unbounded in practice, while main/Pool keys are what the catalog projects from — so a burst of distinct Direct callers evicted the gated row until rediscovery. The two classes now evict separately.

Comment rot in native-models.ts claimed routing never collapses Daybreak into gpt-5.6-sol, which the wire normalization does exactly. Corrected to describe what the code does.

Verification

Known gaps, not fixed here

Named rather than left for a reviewer to find: nativeOpenAiSlugs() / visibleNativeSlugs() ignore account mode, so a Direct /v1/models listing without selectors can still advertise Daybreak from a Pool-only grant. That is advertisement only — Direct dispatch still checks the caller/main credential — and it predates this change. Same-account gated-400 retry remains Pool-only.

Supersedes

Closes #2101 (@Ingwannu) once merged, with attribution.

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. This touches the credential/account boundary and warrants explicit security review per MAINTAINERS.md. The gate is fail-closed on every unproven path: discovery errors yield an empty confirmed set, an unentitled account is unusable before selection and re-asserted after every selector, exact selectors never fail over to another account, and roster fetches are bounded (2 MiB body, 8s timeout, 15s negative TTL). No token or account id is logged. Privacy scan green.

Closes #2097

gpt-daybreak-blue-latest is in the static native set, so catalog sync copied it
onto every account selector and Pool could bind a bare Daybreak request to an
account whose authenticated roster never contained it. The upstream answered
"The 'gpt-daybreak-blue-latest' model is not supported when using Codex with a
ChatGPT account."

Make the authenticated ChatGPT roster the source of truth: discover per-account
entitlement, advertise the gated row only where an eligible account confirms it,
and refuse selection of an account that cannot serve it. Discovery failures fail
closed - the row disappears rather than being offered on unproven evidence.

Carries @Ingwannu's #2101, with three corrections:

Selector compact missed the wire rewrite. accountGatedCompactWireModel was
derived from the caller's raw model string, and an account-qualified selector
like side/gpt-daybreak-blue-latest does not match the gated map, so it still
took the native compact endpoint the guard exists to avoid. It now derives from
route.modelId, the same value core.ts normalizes from.

Direct callers shared one 64-entry roster cache with main/Pool. A burst of
distinct Direct callers evicted the very entries the catalog projects from, so
the gated row vanished until rediscovery. The two classes now evict separately.

A comment in native-models.ts still claimed routing never collapses Daybreak
into gpt-5.6-sol, which the wire normalization does exactly.

Stacked on #2137: this consumes the substituteMainCredential value that PR
corrects, so it must not land ahead of it.

Closes #2097
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner August 19, 2026 19:52
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@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 Aug 19, 2026
@coderabbitai

coderabbitai Bot commented Aug 19, 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: Pro Plus

Run ID: 971dfb4e-a96b-432e-955a-4734f38a54b3

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.

@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 66 / 80

#2097 실화임. 지금 native-models.tsgpt-daybreak-blue-latestNATIVE_OPENAI_MODELS에 박아놔서 catalog sync가 계정마다 복사함. Pool이 자격 없는 계정에 붙이면 업스트림이 model is not supported when using Codex with a ChatGPT account 냄. 주석이 그 트레이드오프를 인정함. 이 PR이 인증된 ChatGPT 로스터를 소스로 바꾸고 발견 실패는 fail-closed.

#2101을 가져온 거임. src/codex/model-entitlements.ts가 새로 생김. 셀렉터 compact가 호출자 raw 문자열로 게이트해서 side/gpt-daybreak-blue-latest가 맵을 빗나감. 지금은 route.modelId 씀. Direct가 main/Pool이랑 64-entry 캐시를 공유해서 Direct 폭주가 gated row를 쫓아냄. 캐시 둘로 나눔. native-models.ts 주석이 와이어 정규화가 Daybreak를 gpt-5.6-sol로 접는 걸 부정하고 있었음. 그거 고침.

base가 codex/fix-bearer-admission-2132임. #2137 먼저. 이 변경이 substituteMainCredentialForDirectresolveCodexAuthContext에 넘김. #2137 없이 dev에 넣으면 #2132가 라우티드마다 다시 살아남. core.ts#2145 #2147도 건드림. 엔타이틀먼트 쪽은 ~530-700/~2133이라 겹은 컨텍스트 리베이스 정도.

남은 구멍은 본문이 말함. nativeOpenAiSlugs()/visibleNativeSlugs()가 계정 모드를 무시해서 Direct /v1/models가 Pool-only 권한을 광고할 수 있음. 디스패치는 막음. 광고만. 같은 계정 gated-400 재시도는 여전히 Pool-only. types.ts/config.ts 안 건드림. 분할에 깔림 없음. #2101은 이미 닫힘. #2097은 머지되면 닫으면 됨.

점수는 66임. 맞는데 2.27 불은 #2143 로그가드랑 #2137 bearer임. 예전에 엔타이틀먼트는 마지막에 두기로 했음. 그 순서 유지.

해결방안: #2137 머지 후 이 PR. #2097 attribution. Direct 광고 구멍은 후속.

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

@Ingwannu Ingwannu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I cannot approve the current entitlement-gating head because it is 22 commits behind the current dev tip (caf20353f). This change crosses catalog, account usability, authentication context, convergence, and request admission, so stale validation is not sufficient.

Please rebase the focused entitlement work onto the latest dev, resolve the current catalog/auth conflicts, and rerun the focused account-entitlement regressions plus full exact-head CI. Re-request review after the final file set and current-head behavior are stable.

@lidge-jun
lidge-jun changed the base branch from codex/fix-bearer-admission-2132 to dev August 20, 2026 02:57
@lidge-jun
lidge-jun merged commit aa07bc3 into dev Aug 20, 2026
41 of 43 checks passed
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.

2 participants