Codex Desktop resolves namespaced current model against wrong list, shows "Custom" and gpt-5.3-codex

Open 💬 1 comment Opened Aug 9, 2026 by itsHabib

What version of the Codex App are you using (From “About Codex” dialog)?

Codex Desktop 26.803.5235.0 (bundled codex-cli 0.147.0-alpha.6.5)

What subscription do you have?

Company-managed custom OpenAI-compatible provider; no direct ChatGPT subscription is used for this reproduction.

What platform is your computer?

Microsoft Windows 11 Pro 10.0.26200 (Build 26200.8875), x64

What issue are you seeing?

When Codex is configured with a custom model_provider and model_catalog_json, the Desktop model picker renders the custom catalog correctly but resolves the currently selected model against a different list than the one it displays.

The lookup misses, so:

  • The model picker chip displays "Custom" instead of the catalog entry's display_name.
  • /model reports gpt-5.3-codex as the current/default model, even though that model exists in neither the custom catalog nor the user configuration.
  • The reasoning-effort selector and model list render correctly from the custom catalog.
  • Routing still works: turns run on the configured model and the thread records that provider-prefixed model correctly.

Sanitized configuration shape:

# managed_config.toml
model_provider = "custom_provider"
model_catalog_json = "<path-to-custom-catalog>"

# config.toml
model = "custom_provider/gpt-5.6-sol"

The provider identifier, endpoint, credentials, and nonessential catalog identifiers are intentionally omitted or anonymized.

Potential root cause from the bundled app-initial-*.js

The current-model resolver matches the .model field using exact string equality:

function She(models, cfg) {
  return cfg == null ? null : models.find(m => m.model === cfg) ?? null;
}

function The({ automation, models }) {
  let n = She(models, automation.model);
  if (n != null) {
    return { model: n.model, reasoningEffort: Che({ model: n, ... }) };
  }

  // ...
  let r = whe(models);
  return r == null
    ? { model: automation.model ?? "gpt-5.3-codex", reasoningEffort: ... }
    : { model: r.model, reasoningEffort: ... };
}

At picker-resolution time, the models passed to The() appears to be the app's built-in cache, whose .model values are bare OpenAI slugs such as gpt-5.6-sol and gpt-5.3-codex. The model/list result rendered by the picker instead contains provider-prefixed values such as custom_provider/gpt-5.6-sol.

Therefore:

She(builtinList, "custom_provider/gpt-5.6-sol")

returns null, after which the label falls back to the localized composer.mode.local.model.custom value and the current model falls back to the hardcoded gpt-5.3-codex.

The routing layer resolves the same configured model against the custom catalog successfully, which explains why routing works while the current-model UI does not.

What steps can reproduce the bug?

  1. Configure a custom model_provider with a model_catalog_json whose model IDs carry a provider prefix, such as custom_provider/....
  2. Set config.toml model to one of those provider-prefixed IDs.
  3. Restart Codex Desktop so the managed configuration and catalog are loaded.
  4. Open the Desktop model picker.
  5. Observe that the model list and reasoning-effort choices render from the custom catalog, but the current-model chip displays "Custom".
  6. Run /model and observe that it reports gpt-5.3-codex.
  7. Start a turn and inspect the thread record or gateway routing; the configured provider-prefixed model is used correctly.

What is the expected behavior?

The picker should resolve the current model against the same model/list result it renders. A configured model that is present in that list should display its catalog name and supported reasoning options instead of falling back to "Custom" or an unrelated hardcoded model.

The preferred fix is to use the effective model/list result for current-model resolution. If that is not possible, namespace normalization could be a fallback before exact comparison, while preserving the custom catalog's metadata as authoritative.

Additional information

Evidence that display and resolution use different sources:

  • model/list over codex app-server returns the provider-prefixed model IDs that the picker renders.
  • A live turn records the same provider-prefixed model on the thread, confirming that routing resolved it.
  • The picker chip still displays "Custom" and /model reports gpt-5.3-codex.

Related but distinct reports:

  • #19694 and #34487 concern custom catalog models being hidden or omitted. In this reproduction, the catalog and model list render correctly.
  • #36582 reports a related custom-provider label problem plus incorrect session persona. In this reproduction, routing and recorded thread model are correct; the isolated failure is current-selection resolution, with the exact hardcoded gpt-5.3-codex fallback identified.
  • Merged PR #12658 added namespaced-model matching in the open-source model metadata path (oai/gpt-5.3-codexgpt-5.3-codex). That is useful precedent for handling provider-prefixed IDs, although the Desktop UI should preferably resolve against its effective custom catalog rather than substitute built-in metadata.

View original on GitHub ↗

1 Comment

rnett · 16 days ago

Additional evidence (Windows, Codex Desktop) supporting the "Custom" / gpt-5.3-codex fallback for namespaced custom models.

Versions

  • codex-cli 0.147.0, Desktop MSIX 26.803.10989.0 (internal 26.803.81509), auth mode ApiKey, custom model_provider = "litellm".

Verbatim resolver fragment from the app renderer bundle (webview/assets/app-initial-KpqQCW_k.js) — when a model has no displayName in the app's known model list, the current-model label falls back to the Custom localization string:

o=se?.displayName??U.formatMessage({id:`composer.mode.local.model.custom`,defaultMessage:`Custom`,description:`Custom model from config`})

Full current-model resolver (label fallback to "Custom"):

let r=C===!0,i=`${d}:${oe}`,o;t[98]!==U||t[99]!==se?.displayName?
(o=se?.displayName??U.formatMessage({id:`composer.mode.local.model.custom`,defaultMessage:`Custom`,description:`Custom model from config`}),...)
:o=t[100];...
l={id:i,model:d,modelLabel:o,reasoningEffort:oe}

Also present in the same bundle — the hardcoded default-model fallback when no model can be resolved:

return r==null?{model:e.model??`gpt-5.3-codex`,reasoningEffort:e.reasoningEffort??`medium`}:{...}
Uhe=`gpt-5.3-codex`

Observed: a namespaced custom model such as neuralwatt/deepseek-v4-flash has no displayName in the app's model list, so the resolver falls back to Custom (and to gpt-5.3-codex when resolution fails) — matching the reported behavior. The CLI (codex debug models) lists all 14 custom slugs correctly with visibility=list.