Repository navigation
[Bug][2.36.0] /api/models drops entitled GPT-5.6 rows after the 5-minute entitlement cache expires #3023
Description
Client or integration
OpenCodex dashboard
Area
Catalog / models
Summary
On OpenCodex 2.36.0, the dashboard/management model list loses the entitled native OpenAI GPT-5.6 rows when the five-minute account-model roster cache expires.
Immediately after a successful entitlement discovery, GET /api/models contains seven native openai rows:
gpt-5.5gpt-5.4gpt-5.4-minigpt-5.3-codex-sparkgpt-5.6-solgpt-5.6-terragpt-5.6-luna
After five minutes, with no account or configuration change, the same endpoint returns only the first four. Calling GET /v1/models immediately restores all seven rows in /api/models for another five minutes.
The account is entitled to the three GPT-5.6 models: /v1/models returns them, the generated Codex catalog contains them with visibility: "list", and they remain usable. This appears to be a management/catalog projection bug rather than a loss of model access.
Expected: management readers should trigger or receive a current entitlement discovery before projecting account-gated native rows. The dashboard and exports should not silently change from 7 to 4 solely because the in-memory evidence cache expired.
Reproduction
-
Start OpenCodex 2.36.0 with an authenticated ChatGPT/Codex account entitled to
gpt-5.6-sol,gpt-5.6-terra, andgpt-5.6-luna. -
Set
ADMIN_TOKENto a valid local management API token. Do not print it. -
Warm entitlement discovery once:
curl -s -H "Authorization: Bearer $ADMIN_TOKEN" \ http://127.0.0.1:10100/v1/models > /dev/null
-
Count the native OpenAI rows returned by the management endpoint every 30 seconds:
curl -s -H "Authorization: Bearer $ADMIN_TOKEN" \ http://127.0.0.1:10100/api/models \ | grep -o '"provider":"openai"' | wc -l
-
Observed timeline:
02:48:32 openai_rows=7 02:49:03 openai_rows=7 02:49:33 openai_rows=7 02:50:03 openai_rows=7 02:50:33 openai_rows=4 # roster cache expired 02:50:50 GET /v1/models 02:51:04 openai_rows=7 # restored immediately 02:51:34 openai_rows=7 ... 02:55:38 openai_rows=7 -
A separate cold/warm check produced the same result:
BEFORE /v1/models count=4 gpt-5.3-codex-spark,gpt-5.4,gpt-5.4-mini,gpt-5.5 GET /v1/models returned gpt-5.6-luna,gpt-5.6-sol,gpt-5.6-terra AFTER /v1/models count=7 gpt-5.3-codex-spark,gpt-5.4,gpt-5.4-mini,gpt-5.5,gpt-5.6-luna,gpt-5.6-sol,gpt-5.6-terra
Suspected root cause
src/codex/catalog/native-models.ts: Sol/Terra/Luna are members ofACCOUNT_GATED_NATIVE_OPENAI_MODELS.src/codex/catalog/metadata.ts:nativeModelRows()filters gated slugs throughcachedAvailableAccountGatedNativeModels().src/codex/model-entitlements.ts: the roster evidence is stored in the process-localaccountModelsCache;MODEL_ROSTER_TTL_MS = 5 * 60_000. The cache projection requiresentry.expiresAt > now.src/server/management/model-routes.ts:GET /api/modelscalls onlylistManagementModelRows(config).src/server/management/model-rows.ts:listManagementModelRows()callsfetchAllModels(config)and thennativeModelRows(config), but it does not callresolveCodexModelEntitlements(config)first.- In contrast,
src/server/index.tsrunsresolveCodexModelEntitlements(config, ...)on theGET /v1/modelspath. The resolver reuses a live cache entry and performs a new upstream roster fetch on a cache miss/expiry.
This also affects /api/client-config because loadExportModels() delegates to listManagementModelRows(). The live-service path used by ocx export reads /api/models, so an export performed while the cache is cold can silently omit the three GPT-5.6 entries.
A narrow fix may be to refresh entitlement evidence in the shared listManagementModelRows() path (or pass an explicit entitlement snapshot into the native-row projection) so /api/models, /api/client-config, and CLI exports stay aligned. The existing five-minute resolver cache would still prevent a remote request on every dashboard refresh.
Relationship to existing issues
- Related to [Bug][2.35][Codex App] Entitled GPT-5.6 Sol/Terra/Luna disappear from the native catalog #2886 and fix(codex): ask upstream for the entitlement roster under a real client version #2891, but not the same failure. Those addressed the client-version used for entitlement roster discovery.
- This reproduction begins with a successful, confirmed entitlement discovery and shows deterministic disappearance only after its five-minute cache expires.
- [Bug]: account-unentitled gpt-5.6-sol/terra/luna are advertised as native models and relayed as 502 stream errors #2548 is the inverse safety issue: advertising gated models to an unentitled account. This report does not propose removing fail-closed entitlement gating.
Version
2.36.0
Operating system
Microsoft Windows 11 Pro, version 10.0.26200, build 26200, x64
Provider and model
Canonical openai provider using ChatGPT/Codex account authentication:
gpt-5.6-solgpt-5.6-terragpt-5.6-luna
Logs or error output
GET /api/models while entitlement cache is cold: openai_rows=4
GET /v1/models: GPT-5.6 Sol/Terra/Luna returned
GET /api/models immediately afterwards: openai_rows=7Screenshots and supporting files
The complete redacted timeline and source trace are included above. No local patch has been installed.
Redacted configuration
{
"providers": {
"openai": {
"adapter": "openai-responses",
"authMode": "forward"
}
}
}Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
Activity
lidge-jun commentedon Aug 30, 2026
리뷰 · 우선순위 71 / 80
이 이슈가 말하는 구멍은 지금
devHEAD870a2adb6에도 그대로 있다. 패키지 버전은2.37.0이다. 작성자가 본 설치본은v2.36.0이고, 그 사이 올라간 것은 버전 줄과 릴리스 워크플로(#3013)다. 계정 게이트 네이티브 목록의 5분 캐시 만료 뒤 관리 API가 자격 증명을 다시 안 읽는 구멍은 수리되지 않았다.증상은 단순하다. ChatGPT/Codex 계정에
gpt-5.6-sol,gpt-5.6-terra,gpt-5.6-luna자격이 있어도, 한 번 성공한 뒤 5분이 지나면GET /api/models의 네이티브 openai 줄이 7개에서 4개로 줄어든다. 같은 시각에GET /v1/models를 한 번 부르면 다시 7개가 된다. 모델 자체는 살아 있다. 대시보드와 export 쪽 투영만 캐시가 식으면 게이트된 세 줄을 숨긴다.코드 길은 이렇게 갈라진다.
src/codex/catalog/native-models.ts에서 Sol/Terra/Luna 는ACCOUNT_GATED_NATIVE_OPENAI_MODELS멤버다. 관리 목록의nativeModelRows()는cachedAvailableAccountGatedNativeModels()가 그 슬러그를 줄 때만 행을 남긴다. 그 함수는 프로세스 안accountModelsCache를 읽기만 한다.expiresAt > now이고confirmed인 칸만 센다. 캐시가 비었거나 만료되면 빈 집합을 돌려주고, 업스트림을 다시 치지 않는다.MODEL_ROSTER_TTL_MS는5 * 60_000이다. 작성자 타임라인의 5분 경계와 맞다.반대로 데이터면
GET /v1/models는resolveCodexModelEntitlements(config, { clientVersion })를fetchAllModels와 같이 부른다. 캐시가 살아 있으면 재사용하고, 없거나 만료되면 계정 로스터를 다시 가져온다. 그래서/v1/models한 번이/api/models를 다시 채운다. 관리 쪽listManagementModelRows()는fetchAllModels와nativeModelRows만 부르고, 그 리졸버를 먼저 부르지 않는다.GET /api/models는 그 함수만 감싼다.loadExportModels()도 같은 함수를 쓰므로/api/client-config와ocx export가 찬 캐시에서 GPT-5.6 세 줄을 빠뜨릴 수 있다.이건 #2886 / #2891 의 남은 구멍이다. 그 수리는
client_version플레이스홀더(0.0.0) 때문에 자격 있는 계정이 빈 로스터를 받던 거짓 음수였다. 이 이슈는 이미 성공한 디스커버리 뒤, 관리 리더만 만료된 증거를 다시 안 채우는 쪽이다. 닫힌 형제 #3022 는 Plus 2.36.0 업그레이드 회귀 보고이고 OS 누락으로 quality-bot 이 닫았다. 증상도 다르다. 중복으로 묶지 마라. 열린 PR 중에서 이 관리면 갱신을 겨냥한 수리는 없다. types.ts/config.ts 분할과도 안 겹친다. 미리보기 배포는 계획에 없다.라인 src/server/management/model-rows.ts 50 -
listManagementModelRows가fetchAllModels와nativeModelRows만 부른다.resolveCodexModelEntitlements를 먼저 부르지 않는다.라인 src/codex/catalog/metadata.ts 424 -
nativeModelRows가cachedAvailableAccountGatedNativeModels(...)동기 읽기만 한다. 캐시가 식으면 게이트된 Sol/Terra/Luna 가 필터로 빠진다.라인 src/codex/model-entitlements.ts 617 -
cachedAvailableAccountGatedNativeModels가entry.expiresAt > now를 요구한다. 만료된 확인 칸은 자격이 있어도 없는 것과 같다.라인 src/codex/model-entitlements.ts 137 -
MODEL_ROSTER_TTL_MS = 5 * 60_000. 작성자가 잰 5분 소실과 같은 시계다.라인 src/server/management/model-routes.ts 352 -
GET /api/models가listManagementModelRows(config)만 호출한다. 자격 갱신 입구가 없다.라인 src/server/index.ts 1163 -
GET /v1/models는resolveCodexModelEntitlements를 같이 부른다. 캐시 미스에서 업스트림을 다시 친다. 관리면과 데이터면이 여기서 갈라진다.라인 src/server/management/model-rows.ts 164 -
loadExportModels가 같은listManagementModelRows를 탄다. 찬 캐시 export 가 GPT-5.6 을 조용히 뺄 수 있다.경로 src/codex/catalog/native-models.ts ACCOUNT_GATED_NATIVE_OPENAI_MODELS -
gpt-5.6-sol/gpt-5.6-terra/gpt-5.6-luna멤버십이 맞다. 게이트 집합 자체는 버그가 아니다.메인테이너의 판단이 필요한 지점
listManagementModelRows한곳으로 둘지,nativeModelRows를 async 로 바꿔 스냅샷을 받게 할지client_version을 런타임/플로어로 잡을지, 요청에 없는 값이라 [Bug]: account-unentitled gpt-5.6-sol/terra/luna are advertised as native models and relayed as 502 stream errors #2548 광고 과잉을 어떻게 막을지너의 추천
열어 둔다. 라벨을 바꾸지 마라. 닫지 마라. 중복(#3022)으로 닫지 마라. 고치는 자리는
listManagementModelRows다. 그 함수 앞에서await resolveCodexModelEntitlements(config)를 한 번 부르면/api/models,/api/client-config, export 가 같이 맞춰진다. 핫 캐시에서는 원격 호출이 나가지 않는다. fail-closed 게이트는 유지하라. #2548 을 풀지 마라. types/config 분할에 깔리지 않는 좁은 PR 로dev에 올려라.이 댓글은 grok-bot이 작성했습니다
kaicot commentedon Aug 30, 2026
The review lists
/api/client-configandocx exportas surfaces that may silently omit the gated rows. I measured it, so that can be upgraded from "may" to confirmed.Same install, OpenCodex 2.36.0. The two captures below are 3 seconds apart, with a single
GET /v1/modelsin between and nothing else changed:GET /v1/modelsGET /api/models(native openai rows)GET /api/client-config?client=opencodegpt-5.6-*refsocx export --client opencode --jsongpt-5.6-*refsSo
ocx exportemits a 537-byte-smaller client config, silently missing all three entitled GPT-5.6 models, purely as a function of whether the 5-minute cache happens to be warm. There is no error, no warning on the export path, andGET /api/providersstill reportsdiscovery: { "status": "ok" }. A user who runsocx exportduring a cold window and merges the result gets a config that quietly lacks Sol/Terra/Luna.I think this is the more damaging half of the bug: a stale dashboard corrects itself on the next refresh, but an exported config is copied out of the process and persists.
The TTL clock also lines up exactly. In this run the cache was warmed at
04:04:12and/api/modelsdropped back to 4 rows at04:09:12— 5:00.000 later, matchingMODEL_ROSTER_TTL_MS = 5 * 60_000.This also supports the recommendation to fix at
listManagementModelRowsrather than at the route: all three surfaces above move together in both directions, which is what you would expect if they share that one entry point, and it means a singleawait resolveCodexModelEntitlements(config)there fixes the dashboard,/api/client-config, andocx exportin one change.Reproduction, if useful — no config or service changes required:
lidge-jun commentedon Aug 31, 2026
Confirmed, and thank you for measuring the two surfaces the review left as "may". Upgrading them in the record:
GET /api/client-config?client=opencode— confirmed to omit the entitled rows on a cold cacheocx export --client opencode --json— confirmed, and this is the damaging oneYour framing of why is the part I want to keep: a stale dashboard corrects itself on the next refresh, but an exported config leaves the process and persists. A user who exports during a cold window merges a file that silently lacks Sol/Terra/Luna, with no error and
discovery: { "status": "ok" }still reported. That moves this from a display bug to a config-corruption bug, and it is why the priority stands at 71/80.Your TTL arithmetic also matches the source:
MODEL_ROSTER_TTL_MS = 5 * 60_000atsrc/codex/model-entitlements.ts:137, and your 04:04:12 → 04:09:12 drop is exactly that interval.On the fix location — agreed, and your evidence is what settles it. All three surfaces moving together in both directions is the signature of a shared entry point, so the repair belongs at
listManagementModelRowsrather than at each route. Fixing it at the route would leaveocx exportbroken, which is the surface that actually persists the damage.One thing I want to get right before writing it: the cache exists so the management API does not make a network call on every dashboard poll, so the fix cannot be an unconditional
await resolveCodexModelEntitlements(config)on a hot path shared by polling. The shape I am aiming for is that an expired roster is refreshed rather than treated as empty, and that a refresh failure is surfaced instead of silently yielding a short list —discovery: { "status": "ok" }alongside missing rows is arguably a separate defect in the same area, and I will treat it as part of this one.No action needed from you. Your reproduction is precise enough to work from directly, and the cold/warm 3-second capture is what I will use as the regression shape.
docs(devlog): plan the priority-70+ entitlement and spill train
fix(codex): restore GPT-5.6 for entitled accounts by raising the gate…
docs(devlog): close wp2 planning after four audit rounds and record t…
docs(devlog): close wp2 planning after four audit rounds and record t…
docs(devlog): record the v2.37.0 release and the live #3022 verificat…
lidge-jun commentedon Aug 31, 2026
Fixed on
devin 0844dc9 (PR #3054).What was wrong.
/api/models,/api/client-configandocx exportsharelistManagementModelRows, and the entitlement snapshot behind it carried an expiry but no binding to the credentials that produced it. Two failure modes followed from that single gap:MODEL_ROSTER_TTL_MSelapsed, the surfaces kept answering from the expired snapshot instead of re-deriving. That is the row loss you reported.auth.jsonwas replaced out of band, the stale snapshot stayed authoritative. A newly entitled account saw the old roster; a logged-out one still saw models it no longer had.What landed. A new
src/codex/credential-mutation-epoch.tsholds a monotonic counter bumped at every credential mutation point. A snapshot records the epoch it was derived under, and a read is a hit only when both the epoch and the expiry still match. Otherwise it re-derives. The logged-out result is memoized deliberately, because the dashboard polls this path around 24 times a minute.Cost contract, since this sits on a polling path. Steady state with nothing mutated and the snapshot live: 0 additional
accountCredentialSnapshotcalls, 0 token refreshes, 0 network requests.Verification. Nine regressions written red-first, including three added in review specifically because the first versions of the race tests were vacuous — they returned usable credentials, so they never reached the negative-memo publication fence, and deleting that fence left them green. The corrected versions fail when the epoch fence, the identity fence, or the absence-observation-time rule is removed. Rejection-path tests are labeled characterization rather than claimed as red.
Full suite on a Linux host at the merged head: 16696 pass / 0 fail / 16 skip, privacy scan and typecheck clean.
Residual. The epoch is process-local, so two proxies sharing one
OPENCODEX_HOMEstill observe each other only through the expiry. Not reachable in the supported single-proxy topology.Two follow-ups are already implemented and queued behind this: a tri-state entitlement authority so a roster fetched below a model's known minimum yields
unknownrather than a decided denial, and an honest entitlement status field on/api/providerssodiscovery: okbeside missing rows stops being the only signal available. The second one is the part of this report I owed you an answer for.Closing now that it is on
dev; GitHub only auto-closes on merges intomain.docs(devlog): record the wp6/wp5/wp4 entitlement stack and two CI tes…
docs(devlog): record the wp6/wp5/wp4 entitlement stack outcome (lidge…
fix(codex): restore GPT-5.6 for entitled accounts by raising the gate…
docs(devlog): close wp2 planning after four audit rounds and record t…
docs(devlog): record the v2.37.0 release and the live lidge-jun#3022 …
docs(devlog): record the wp6/wp5/wp4 entitlement stack outcome (lidge…
fix(codex): restore GPT-5.6 for entitled accounts by raising the gate…
docs(devlog): close wp2 planning after four audit rounds and record t…
docs(devlog): record the v2.37.0 release and the live lidge-jun#3022 …
docs(devlog): record the wp6/wp5/wp4 entitlement stack outcome (lidge…