Skip to content

[Bug]: Metadata augmentation re-adds an id that authoritative live discovery just dropped (provider roster 2 vs Models inventory 3 on deepseek) #4688

Description

@Jioyzen

Environment

  • opencodex 2.55.0 (npm global, Linux)
  • Codex CLI 0.154.0, proxy on 0.0.0.0:10100, liveModels discovery 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 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

  1. Configure a deepseek provider (https://api.deepseek.com), leave live discovery on (providers.deepseek.liveModels unset/true).
  2. 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]
  1. 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-pro

Root 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-flash to ROUTED_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 on opencode-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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions