Repository navigation
Catalog sync skips stale model_catalog_json rewrite; disabled native slugs persist; no warning for unexposed pinned default model #4646
Description
Activity
- addedcatalogModel catalog, slugs, visibility, routed entriesModel catalog, slugs, visibility, routed entries
on Sep 14, 2026 리뷰 · 우선순위 55 / 80
설명
이 이슈는 Codex가 읽는
model_catalog_json(~/.codex/opencodex-catalog.json)이 프록시가 실제로 노출하는 모델 집합과 어긋난 채로 남는다는 운영 사고 보고입니다. 작성자 환경(opencodex 2.55.0, macOS arm64,model_provider="opencodex")에서는 네이티브 OpenAI 슬러그 다섯 개(gpt-5.5,gpt-5.6-sol/terra/luna,gpt-6-astra)가disabledModels에 들어가GET /v1/models에도 없는데, 카탈로그 파일에는visibility:"hide"행으로 남아 있었고, Codexconfig.toml의model="gpt-5.6-luna"핀은 그대로여서 기본 실행이rate_limit_exceeded로만 깨졌다고 합니다. 요청은 세 갈래입니다. (1)POST /api/sync가 파일이 있으면 갱신을 건너뛰지 말고 슬러그 집합(또는 해시) 드리프트를 보고 다시 쓰게 할 것, (2) disabled 네이티브 슬러그를 카탈로그에서 아예 빼거나visibility:"hide"가 Desktop에서 무시될 수 있음을 문서화할 것, (3) 핀된 기본 모델이 노출 집합에 없으면ocx doctor//api/startup-health에 경고를 넣을 것.현재
devtip은2b43c14c0(package 2.56.0)이고, 최근 랜딩은#4546cost-guard 스택의wph/wpd/wpi(#4639/#4640/#4641)입니다. 이 이슈는 그 라우팅·센드버짓 레인과 파일이 겹치지 않는catalog레인입니다. 릴리스를 막는 크래시는 아니지만, 쿼타로 모델이 빠질 때마다 기본 경로가 조용히 죽는 운영 구멍이라 중간 우선순위입니다. 라벨은enhancement/catalog이고 작성자가 재현·워크어라운드(파일 삭제 후 재동기화)까지 적어 두었습니다.요청 (1)의 “파일이 있으면 스킵” 진단은 현재 HEAD 코드와는 조금 다릅니다.
src/codex/catalog/sync.ts의 쓰기 경로는 파일이 있다는 이유만으로 건너뛰지 않습니다. 준비된 JSON 바이트와 디스크 바이트가 완전히 같으면catalogWritten: false를 돌리고, 그 이유는 주석에 명시되어 있습니다. 바이트가 같아도 mtime만 올리면 app-server 신선도 판정(#857/#1407)이 “카탈로그가 바뀌었다”고 오인해 이미 떠 있는 Codex의 모델 가이드를 평생 꺼 버리기 때문입니다. 그래서 작성자가 본catalogWritten:false는 “존재만으로 스킵”이 아니라, 재생성 결과가 디스크와 바이트 동일했다는 뜻에 가깝습니다. 파일을 지우고 다시 sync 했는데도 hide 행이 돌아온 현상과도 맞습니다. 즉 구멍의 중심은 “스킵 조건”보다 준비 단계가 disabled 네이티브를 어떤 형태로 남기느냐입니다.요청 (2)는 코드 주석과 일치합니다.
src/codex/catalog/metadata.ts의desktopAllowlistSuppressedNativeSlugs는 Desktop remote allowlist가visibility:"hide"를 무시할 수 있어서, 네이티브 앨리어스 호환이 켜져 있을 때만 disabled 네이티브 행을 숨기지 말고 아예 빼는 경로입니다. 앨리어스 콤보가 없으면applyNativeVisibility가visibility:"hide"행을 남기는 쪽이 현재 계약입니다. 작성자가 본 “hide로 남음”은 버그라기보다 이 분기입니다. 다만 Desktop이 hide를 무시하면 피커에 죽은 모델이 보일 수 있다는 제품 리스크는 주석이 이미 인정하고 있습니다.요청 (3)도 맞습니다.
src/cli/doctor.ts와src/server/startup-health-cache.ts/src/codex/autostart-health.ts쪽은 프록시·부팅 보호·라우팅 요약을 보지만, Codexconfig.toml의model=핀이 지금 프록시 노출 집합에 있는지는 검사하지 않습니다. 쿼타로 노출이 바뀐 뒤 기본 실행이rate_limit_exceeded만 내는 실패 모드는 doctor가 아직 잡기 어렵습니다.#4584카탈로그 자동 갱신과는 인접하지만, “핀된 기본 모델 vs 노출 집합” 경고는 별도 체크입니다.types.ts/config.ts 분할 캠페인과는 무관하고, 중복 이슈로 바로 닫을 만한 열린 PR도 이 번호 기준으로는 보이지 않습니다. 다음에 올 패치는 sync 스킵 문구를 고치기보다 (a) hide vs omit 계약 정리, (b) doctor/startup-health의 핀 모델 경고, 둘을 나누는 편이
#1407mtime 함정을 건드리지 않습니다.라인 / 심볼 문제
src/codex/catalog/sync.ts:2411-2413 -
catalogWritten:false는 “파일 존재”가 아니라 준비 바이트 == 디스크 바이트일 때다. 이슈 본문의 “existing catalog file as up-to-date” 서술은 HEAD 기준으로 고쳐 적어야 기여자·리뷰어가 틀린 스킵 조건을 고치려 들지 않는다src/codex/catalog/sync.ts:2398-2410 - no-op write 스킵은
#1407가이던스 침묵을 막는 의도적 동작이다. 슬러그 드리프트만으로 mtime을 올리는 방식은 그대로 넣으면 회귀다. 드리프트 감지는 “다시 쓰기”가 아니라 응답 필드(catalogDrift)나 doctor 경고로 분리하는 편이 안전하다src/codex/catalog/metadata.ts:92-106 (
desktopAllowlistSuppressedNativeSlugs) - disabled 네이티브 omit은 네이티브 앨리어스 옵트인일 때만이다 hide 잔류는 기본 경로다. “무조건 omit”으로 바꾸면 앨리어스 없는 사용자의 복구/재활성화·백업 병합(catalogModelsForMergeWithNativeRecovery) 가정이 깨질 수 있다src/codex/catalog/metadata.ts:512-526 (
applyNativeVisibility) - disabled면visibility:"hide"로 남긴다. 작성자가 본 재생성 후에도 hide 행이 있는 현상과 일치한다. 여기 계약을 바꾸지 않으면 sync를 몇 번 돌려도 hide 행은 남는다src/cli/doctor.ts / startup-health - Codex 핀
model=이 노출 집합에 없는 경우를 검사하지 않는다. 작성자 요청 (3)의 공백이 여기다.rate_limit_exceeded만 보이는 UX와 직결된다이슈 본문 요청 (1) 예시 -
catalogDrift.removed를 sync 성공 응답에 넣는 설계는 유용하지만, 바이트 동일 스킵과 동시에 “다시 썼다”고 말하면 모순이다. drift는 읽기 전용 진단으로 두고 write는 내용이 바뀔 때만 하는 쪽이#1407과 맞다메인테이너의 판단이 필요한 지점
- disabled 네이티브 슬러그를 항상 카탈로그에서 omit할지, 지금처럼 앨리어스 옵트인일 때만 omit하고 기본은
visibility:"hide"로 둘지 (Desktop allowlist 무시 리스크 vs 복구 메타데이터 보존) visibility:"hide"를 유지한다면 Desktop/피커 쪽 문서에 “hide는 best-effort”를 공식화할지- 핀된 Codex 기본 모델이 비노출일 때 doctor /
/api/startup-health경고를 이번 이슈 범위에 넣을지, 별도 이슈로 쪼갤지 - sync 응답에
catalogDrift같은 진단 필드를 추가할지, 쓰기 동작을 바꾸지 않고 doctor만으로 끝낼지 - 2.55.0에서 관측된 증상과 2.56.0
dev의 바이트 비교 스킵이 동일한지, 재현 전 버전 확인을 요구할지
너의 추천
이슈는 열어 두세요. 바로 닫을 중복·무효는 아닙니다. 다만 본문 요청 (1)의 “파일 있으면 스킵” 서술은 HEAD(
sync.ts바이트 동일 스킵)에 맞게 정정하는 댓글/편집을 먼저 하고, 구현은 (A) hide vs omit 제품 계약을 한 줄로 잠근 뒤 (B)ocx doctor(+ 가능하면 startup-health)에 “핀된 기본 모델 ∉ 노출 집합” 경고를 넣는 순서가 안전합니다. sync의 no-op write 스킵(#1407)을 풀어서 mtime을 올리는 패치는 비추천입니다. 패치 PR이 오면catalog라벨 유지,#4546스택과 섞지 말 것.이 댓글은 grok-bot이 작성했습니다
- disabled 네이티브 슬러그를 항상 카탈로그에서 omit할지, 지금처럼 앨리어스 옵트인일 때만 omit하고 기본은
Partially fixed on
devby #4963, squashed ase61a407a8d8abf12ccf5c8365a0f32b3a9642dca, and partially working as designed. Closing with both halves stated, because a bare "won't fix" on the first ask would teach you nothing.Ask 3 was the real defect and is fixed. The doctor now warns when the configured Codex default model is not exposed by the proxy.
Ask 1 is working as designed, and the guard is load-bearing.
syncbyte-compares the regenerated catalog against what is on disk; it does not test for file existence. That comparison landed on 2026-08-11 inc7eec01ca4and642805c11e. Implementing the ask as written would rewrite the catalog on every sync forever, and a no-op rewrite bumpsmtime, which the #857 classifier compares against each running Codex's start time and which #1407 turns into a lifetime-long silence.One correction to the report's premise, found while implementing. A disabled-but-hidden native slug does not fail at request time.
src/router.tsnever consultsdisabledModels, anddocs-sitealready documents that disabling does not reject a direct request, so a hidden slug picked in Desktop is routed by the ordinary rules as though it were enabled. If you were relying on the opposite, that is worth knowing.Shipping in the next release off
dev.
Area
Catalog / models
What are you trying to accomplish?
I need the Codex model catalog (model_catalog_json) to stay truthful as provider quotas shift, so that Codex's model picker and its pinned default model never point at models the proxy no longer exposes.
Concrete failure we hit (opencodex 2.55.0, macOS arm64, Codex CLI with model_provider="opencodex"):
What prevents this today?
What should OpenCodex do?
Example usage or interface
Before/after for request 1:
For request 3:
Alternatives or workarounds
Workaround we now run locally: delete ~/.codex/opencodex-catalog.json and re-post /api/sync to force regeneration, then manually verify the pinned default model against GET /v1/models. This is operational toil that recurs every time a provider quota resets.
Additional context
Docs consulted: https://opencodex.me/guides/model-routing/ and https://opencodex.me/reference/cli/. This failure mode (pinned default model silently dying after quota exhaustion) recurred three times in two weeks of our ops logs, which is why a built-in doctor check would be high-leverage. Happy to test a patch.
Checks