Repository navigation
[Bug]: Metadata augmentation re-adds an id that authoritative live discovery just dropped (provider roster 2 vs Models inventory 3 on deepseek) #4688
Description
Environment
- opencodex
2.55.0(npm global, Linux) - Codex CLI
0.154.0, proxy on0.0.0.0:10100,liveModelsdiscovery enabled for the provider - Provider: custom
deepseek(adapter: openai-responses,baseUrl: https://api.deepseek.com,authMode: key)
Summary
For a provider whose roster is discovered live, augmentRoutedModelsWithMetadata() appends every bundled metadata row unconditionally, even when authoritative live discovery has just dropped that same id. The result is a roster that disagrees with the upstream provider and with opencodex's own discovery pass:
- provider view (authoritative live roster): 2 models —
deepseek-flash,deepseek-v4-pro - Models inventory (
/api/models),/api/catalog, the on-disk Codex catalog andocx models live: 3 models —deepseek-flash,deepseek-v4-flash,deepseek-v4-pro
deepseek-v4-flash is not advertised by the upstream /models endpoint any more, and the same process logs that it was dropped from the authoritative live catalog:
[opencodex] Provider model discovery for "deepseek" omitted configured model ids; dropping them from the authoritative live catalog: deepseek-chat, deepseek-reasoner, deepseek-v4-flash, deepseek-v4-flash-vision-exp.
So live discovery is working correctly — a later stage puts the id back.
Reproduction
- Configure a
deepseekprovider (https://api.deepseek.com), leave live discovery on (providers.deepseek.liveModelsunset/true). - List what upstream publishes and what opencodex advertises:
curl -s -H "Authorization: Bearer $DEEPSEEK_KEY" https://api.deepseek.com/models
# {"data":[{"id":"deepseek-flash"},{"id":"deepseek-v4-pro"}]} <- 2 ids
ocx models live
# deepseek/deepseek-flash [routed, enabled]
# deepseek/deepseek-v4-flash [routed, enabled] <- not published upstream
# deepseek/deepseek-v4-pro [routed, enabled]- Compare the two pipeline stages behind that difference:
// fetchProviderModels() = upstream /models + drop diagnostics, i.e. the authoritative live catalog
await fetchProviderModels("deepseek", config.providers.deepseek, 0)
// -> deepseek-flash, deepseek-v4-pro
// gatherRoutedModels() = the roster every consumer (GUI inventory, /api/models, /api/catalog,
// disk catalog, codex picker) actually reads
(await gatherRoutedModels(config)).filter(m => m.provider === "deepseek")
// -> deepseek-flash, deepseek-v4-flash, deepseek-v4-proRoot cause
src/codex/catalog/provider-fetch.ts:2902 augmentRoutedModelsWithMetadata() iterates the bundled metadata table for every provider in the augment allowlist and pushes any row whose provider/id is not already present:
if (!JAWCODE_CATALOG_AUGMENT_PROVIDERS.has(provider)) continue; // parsing.ts:178 -> {"opencode-go","deepseek"}
if (providers?.[provider]?.liveModels === false) continue; // only a fully-static provider is skipped
for (const meta of listModelMetadata(jawcodeProvider)) { ... out.push(...) }The bundled table only has two DeepSeek rows (src/generated/model-metadata.ts:45):
"deepseek": [["deepseek-v4-flash",...],["deepseek-v4-pro",...]]
deepseek-v4-pro happens to still match upstream, but deepseek-v4-flash is the legacy alias of today's deepseek-flash, so the augmentation re-adds a row that authoritative discovery had just removed. Existing retention paths are not the cause — I ruled them out with in-memory isolation runs against the real modules:
| variant | deepseek roster |
|---|---|
| as-is | deepseek-flash, deepseek-v4-flash, deepseek-v4-pro |
deepseek-v4-flash removed from providers.deepseek.models |
unchanged (3) |
providers.deepseek.models = [] |
unchanged (3) |
modelDiscovery.knownModels.deepseek deleted |
unchanged (3) |
mergeConfiguredModelsIntoLiveCatalog() called directly with the live pair |
correctly returns 2, drops deepseek-v4-flash |
Only the augmentation stage reintroduces it, which is why neither remove, models edits, retainModels nor the MODEL_REMOVAL_GRACE_FETCHES baseline can clear it.
Expected
The roster should be derivable from the authoritative live result. Either:
- treat "id absent from authoritative live discovery for a live provider" as authoritative for the augmentation path too (skip the metadata row instead of merging it), or
- if the bundled row is meant to win in some cases, surface the disagreement (e.g. mark the row
configured-only/stale) instead of silently listing it as a normal routed model, or - repo-idiomatic stopgap: add
deepseek/deepseek-v4-flashtoROUTED_MODEL_COMPATIBILITY_EXCLUSIONS(src/codex/catalog/parsing.ts:180), which is how [Bug]: Catalog exposes four uncallable OpenCode models from augmentation or stale discovery #2330 handled augmentation-only uncallable rows onopencode-go.
Additional note
The bundled DeepSeek metadata rows look stale relative to the live endpoint: they still name deepseek-v4-flash/deepseek-v4-pro, while upstream now publishes deepseek-flash/deepseek-v4-pro. Today deepseek-v4-flash still answers (upstream aliases it to deepseek-flash, response body reports "model":"deepseek-flash"), so this is a consistency/naming bug rather than a broken model — but on a provider that fully retires an alias, the same path would advertise an uncallable row, which is the failure mode #2330 already demonstrated.
Workaround
ocx models disable deepseek/deepseek-v4-flash hides the row from the inventory and the Codex picker (verified against the real filterCatalogVisibleModels() path), but the underlying disagreement remains and the same thing will recur for any other augmented provider.
Activity
lidge-jun commentedon Sep 15, 2026
리뷰 · 우선순위 58 / 80
이 이슈는 live discovery가 이미 버린 모델 id를, 나중 단계에서 번들 메타데이터가 다시 넣는 버그다. 지금
devHEAD270291170(2.56.0 tip, #4683 머지 후) 기준으로 보면 설명과 코드 위치가 맞는다. 다만 리포터의 줄 번호provider-fetch.ts:2902는 예전 모놀리스 좌표다. godfile round3에서provider-fetch가 쪼개진 뒤, 실제 함수는src/codex/catalog/routed-gather.ts의augmentRoutedModelsWithMetadata(약 816행)에 있다. facade인provider-fetch.ts는 그걸 다시보내기만 한다.흐름을 짧게 말하면 이렇다.
fetchProviderModels가 업스트림/models를 읽고, 설정에만 있던 id는mergeConfiguredModelsIntoLiveCatalog(model-visibility.ts)로 떨어뜨린다. 리포터가 찍은 로그처럼deepseek-v4-flash는 여기서 이미 빠진다. 그다음gatherRoutedModels끝에서augmentRoutedModelsWithMetadata가 돌고,JAWCODE_CATALOG_AUGMENT_PROVIDERS(parsing.ts:178,opencode-go와deepseek만)에 들어 있으면 번들 메타 테이블의 모든 행을 “아직 없으면” 무조건 붙인다. 번들(src/generated/model-metadata.ts의deepseek키)에는 아직deepseek-v4-flash와deepseek-v4-pro만 있다. 업스트림은 지금deepseek-flash와deepseek-v4-pro만 주므로, flash 쪽 옛 이름만 다시 살아난다. 그 결과가 GUI 인벤토리,/api/models,/api/catalog, 디스크 Codex 카탈로그,ocx models live까지 같이 3개로 보이게 된다.리포터가 표로 보여 준 격리 실험도 현재 구조와 맞다. 설정
models를 비우거나knownModels를 지워도 증강 단계가 다시 넣으면 3개가 유지된다.mergeConfiguredModelsIntoLiveCatalog만 돌리면 2개로 맞는다. 그래서 retain/grace/ocx models remove로는 근본이 안 잡히고,ocx models disable은filterCatalogVisibleModels로 화면만 가린다.비슷한 전례는 닫힌 #2330이다. 그때도 증강으로만 살아 있는 호출 불가 행을
ROUTED_MODEL_COMPATIBILITY_EXCLUSIONS(parsing.ts:180)에 넣어 막았다. 그 집합은shouldExposeRoutedModel에서 걸러지고,gatherRoutedModels는 증강 직후 그 필터를 탄다(routed-gather.ts약 349–359행). 다만 지금 제외 목록에는 게이트웨이 쪽…/deepseek-v4-pro슬러그만 있고(command-code,baseten등),deepseek/deepseek-v4-flash는 없다. 그래서 오늘 재현이 그대로 열린다.당장 증상은 “이름 불일치”에 가깝다. 리포터 말대로 옛 id가 아직 alias로 응답하면 호출은 될 수 있다. 하지만 권위 있는 live 목록과 소비자가 보는 목록이 갈라지는 구조 버그이고, 업스트림이 alias를 끊는 순간 #2330과 같은 “목록에는 있는데 못 씀”으로 바뀐다. 2.56.0 promote(#4687)와는 별 트랙이다. 릴리스를 막을 정도는 아니지만, catalog 정합 버그라서 다음 레인에서 빨리 닫을 가치가 있다.
라인
src/codex/catalog/routed-gather.ts816–857 (augmentRoutedModelsWithMetadata) - live provider인데도 번들 메타 id를 “없으면 무조건 push”한다. authoritative live가 방금 뺀 id를 다시 넣는다.라인
src/generated/model-metadata.tsdeepseek 행 - 번들이 아직deepseek-v4-flash/deepseek-v4-pro만 들고 있어, 업스트림의deepseek-flashrename과 어긋난다.경로
src/codex/catalog/parsing.tsROUTED_MODEL_COMPATIBILITY_EXCLUSIONS- #2330 패턴의 응급 제외에deepseek/deepseek-v4-flash가 없다. 게이트웨이v4-pro제외만 있다.경로 리포터 좌표
provider-fetch.ts:2902- 현재 트리에서는 틀리다. 구현은routed-gather.ts다. PR을 쓸 때 옛 줄 번호로 패치하지 말 것.메인테이너의 판단이 필요한 지점
deepseek/deepseek-v4-flash만 exclusion에 넣고, 메타 테이블 rename은 별 PR로 둘지.configured-only/stale로 표시할지, 아니면 목록에서 아예 뺄지.deepseek-flash를 넣거나v4-flash를 빼는 갱신을 2.56.0 직후 트레인에 넣을지.너의 추천
근본 쪽을 권한다.
augmentRoutedModelsWithMetadata에서 해당 provider의 live 결과(또는 그 직전 authoritative set)에 없는 id는 push하지 않게 고친다. 회귀 테스트로 “live 2개 + 번들에 옛 id 1개 → gather 결과는 2개”를 고정한다. 바로 막는 응급 패치가 필요하면ROUTED_MODEL_COMPATIBILITY_EXCLUSIONS에deepseek/deepseek-v4-flash를 추가하는 #2330식 PR을 짧게 열고, 이어서 근본 수정을 붙인다. 이슈는bug유지, help wanted 가능. #4687 2.56.0 promote와는 묶지 말 것.이 댓글은 grok-bot이 작성했습니다
lidge-jun commentedon Sep 16, 2026
Closing as working-as-intended rather than a defect.
deepseek-v4-flashis a retained compatibility alias, declared deliberately atsrc/providers/registry/entries-core.ts:1000-1013, andsrc/codex/catalog/routed-gather.ts:853-870is the code that implements that retention. The provider roster reporting two ids while the Models inventory reports three is that policy showing through, not augmentation re-adding an id that live discovery dropped.The legitimate gap underneath is that nothing marks a row as a compatibility alias, so the two counts cannot be reconciled by looking at them. If that is what you want, open a focused enhancement for alias provenance in the catalog and it will be treated on its own merits.