You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 and ocx 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 deepseek provider (https://api.deepseek.com), leave live discovery on (providers.deepseek.liveModels unset/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 catalogawaitfetchProviderModels("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(awaitgatherRoutedModels(config)).filter(m=>m.provider==="deepseek")// -> deepseek-flash, deepseek-v4-flash, deepseek-v4-pro
Root cause
src/codex/catalog/provider-fetch.ts:2902augmentRoutedModelsWithMetadata() 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 skippedfor(constmetaoflistModelMetadata(jawcodeProvider)){ ... out.push(...)}
The bundled table only has two DeepSeek rows (src/generated/model-metadata.ts:45):
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:
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
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.
Environment
2.55.0(npm global, Linux)0.154.0, proxy on0.0.0.0:10100,liveModelsdiscovery enabled for the providerdeepseek(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:deepseek-flash,deepseek-v4-pro/api/models),/api/catalog, the on-disk Codex catalog andocx models live: 3 models —deepseek-flash,deepseek-v4-flash,deepseek-v4-prodeepseek-v4-flashis not advertised by the upstream/modelsendpoint any more, and the same process logs that it was dropped from the authoritative live catalog:So live discovery is working correctly — a later stage puts the id back.
Reproduction
deepseekprovider (https://api.deepseek.com), leave live discovery on (providers.deepseek.liveModelsunset/true).Root cause
src/codex/catalog/provider-fetch.ts:2902augmentRoutedModelsWithMetadata()iterates the bundled metadata table for every provider in the augment allowlist and pushes any row whoseprovider/idis not already present:The bundled table only has two DeepSeek rows (
src/generated/model-metadata.ts:45):deepseek-v4-prohappens to still match upstream, butdeepseek-v4-flashis the legacy alias of today'sdeepseek-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:deepseek-flash, deepseek-v4-flash, deepseek-v4-prodeepseek-v4-flashremoved fromproviders.deepseek.modelsproviders.deepseek.models = []modelDiscovery.knownModels.deepseekdeletedmergeConfiguredModelsIntoLiveCatalog()called directly with the live pairdeepseek-v4-flashOnly the augmentation stage reintroduces it, which is why neither
remove,modelsedits,retainModelsnor theMODEL_REMOVAL_GRACE_FETCHESbaseline can clear it.Expected
The roster should be derivable from the authoritative live result. Either:
configured-only/stale) instead of silently listing it as a normal routed model, ordeepseek/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 publishesdeepseek-flash/deepseek-v4-pro. Todaydeepseek-v4-flashstill answers (upstream aliases it todeepseek-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-flashhides the row from the inventory and the Codex picker (verified against the realfilterCatalogVisibleModels()path), but the underlying disagreement remains and the same thing will recur for any other augmented provider.