You wired up a custom provider, codex runs fine from the terminal, but the Desktop model picker acts like your models do not exist.
That gap is almost never your API key or your config syntax. It is the Desktop client quietly filtering models the backend already loaded. The requests work. The dropdown just refuses to show them.
This guide does two things. First: the concrete fixes, ordered from a one-line workaround to a clean, switchable picker. Second — the part most guides skip — what a UI bug in a tool you depend on actually teaches you about how your AI stack is wired.
If you'd rather have someone harden the whole layer for you, we do that too.
30-SECOND DIAGNOSIS
Match your symptom before you edit anything
Most people are one config line away from a fix. Match what you see to the table, then act.
| What you see | What is actually wrong | First move |
|---|---|---|
| Picker shows only "Custom", no model name | Model set inline, no catalog metadata | Fix 1: keep the inline model — it works |
| codex CLI /model lists it, Desktop does not | Desktop client-side filter (issue #19694) | Fix 1 now, Fix 4 for a real listing |
| Picker dropdown is empty | Missing or malformed catalog | Fix 2 or Fix 4 |
| Nothing loads, startup prints a warning | Provider in project-local .codex/config.toml | Fix 3: move it to ~/.codex/ |
| cc-switch native provider, still hidden | Pre-v3.16.5 catalog format | Fix 4: update, then re-save |
The single fastest check: open a terminal in the same folder, run codex (the CLI), then type /model. If the CLI lists your model but Desktop does not, you have the client-side filter bug — and no amount of config editing on the Desktop side will surface it. Skip to Fix 1.
FIRST DECISION
Fix the display, or fix the route?
This is the decision that saves you an afternoon of editing the wrong layer.
Fix the picker (Fixes 1–4) when
- Your requests succeed from the CLI or a direct curl, and the model just does not appear in the Desktop dropdown.
- You are looking at the #19694 client-side filter or a catalog gap — display problems, not routing ones.
- You want a clean, switchable model list for a team that swaps models often.
Fix the route (not the picker) when
- curl against your gateway comes back 401, 404, or model-not-found. The provider or model string is wrong.
- Every request 404s because wire_api is set to the removed chat path or the gateway has no /responses endpoint.
- The model runs but returns the wrong output — your catalog slug does not match the provider's model ID.
Stop rule. If your only goal is to send requests to a custom model, Fix 1 alone gets you there: set model inline, restart, done. The rest is about making the picker show a clean, switchable list. If you never open the dropdown, stop after Fix 1.
ROOT CAUSES
Four reasons the picker hides your model
They need different fixes. Picking the wrong one is why people spend an afternoon re-typing a working config.
| Root cause | Why the picker hides the model | Which setups hit it | Fix |
|---|---|---|---|
| Desktop client-side filter | App-server returns the model via model/list; the Desktop renderer drops it. | Any custom provider with a catalog | Fix 1 + Fix 4 |
| No catalog metadata | model set inline, picker has nothing to describe. | Minimal config.toml setups | Fix 1 or Fix 2 |
| Malformed / stale catalog | Catalog written in a format Codex will not enumerate. | Older cc-switch, hand-edited JSON | Fix 2 / Fix 4 |
| Project-local provider | model_provider ignored outside ~/.codex/config.toml. | .codex/config.toml in a repo | Fix 3 |
The first one surprises people. Per the open codex issue #19694, the app-server loads the catalog correctly and returns your models through its model/list endpoint — but the Desktop UI applies an extra filter and drops locally configured models before rendering the picker. The backend knows about your model. The frontend refuses to show it. That is a client bug, not a config error, and it was filed on 2026-04-26 and is still open at time of writing.
The second cause is the most common and the least alarming. If you only wrote model = "some-id" with no catalog, Codex has the string but no display name, no context window, no capability flags. The picker shows the label "Custom" and moves on. Your requests still go to the right model. This trips people up because "Custom" looks broken when it is actually working.
HOW CODEX LOADS MODELS
Where the Desktop diverges from the CLI
Codex builds its model list in layers, and the order tells you which fix will actually land.
Three sources feed the list: a catalog bundled with the binary, a remote catalog fetched from OpenAI, and your local model_catalog_json. The local file wins — it overrides both the bundled and the remote catalog on startup, which is why a correct model_catalog_json is the real lever for third-party models rather than a nice-to-have.
Here is the part that trips everyone. When Codex starts, the app-server reads all three layers, resolves them, and exposes the result through an internal model/list endpoint. The CLI renders that list directly, so custom models show up. The Desktop app renders the same list through an extra client step that, per issue #19694, applies its own allowlist and drops locally configured entries before they reach the dropdown. Same backend, same catalog, two different pickers. That single divergence is why the CLI is a dependable escape hatch and Desktop is not.
There is also a cache to watch. Codex keeps ~/.codex/models_cache.json, and after you switch providers or edit a catalog it does not always resync. A stale cache can keep showing an old list — or an empty one — even after your config is correct. Deleting that file forces a clean rebuild on the next launch, which is worth trying before you assume the catalog itself is wrong.
THE FIXES
How to make Codex Desktop see your models
Work top to bottom. Fix 1 always works. The later fixes make the picker pretty and the list switchable.
THE RELIABLE WORKAROUND
Fix 1: Set the model inline (the reliable workaround)
This is the workaround the #19694 thread converged on, and the one to reach for first. Put the model string directly in config.toml and let the picker say "Custom".
# ~/.codex/config.toml (user-level, not a project folder) model = "your-provider/your-model-id" model_provider = "my-gateway" [model_providers.my-gateway] name = "my-gateway" base_url = "https://your-gateway.example/v1" env_key = "MY_GATEWAY_API_KEY" wire_api = "responses"
Export the key (export MY_GATEWAY_API_KEY=sk-... in your shell profile), fully quit Codex Desktop, and reopen it. The picker will read "Custom" but every request goes to the model you set. Swap the model string to switch models — no new provider block each time.
Note on wire_api: Codex removed its older chat/completions path in early February 2026 (discussion #7782); responses is now the only supported value and the default when the key is omitted. A provider still set to wire_api = "chat" fails on startup. Your gateway must expose an OpenAI-compatible Responses endpoint at {base_url}/responses. Point Codex at a gateway that only speaks /chat/completions and every request 404s — which looks like a model problem but is a protocol mismatch.
MAKE THE PICKER LIST IT
Fix 2: Add a model catalog so the picker lists it
If you want a real name in the dropdown instead of "Custom", you need a model_catalog_json. Point config.toml at a JSON file with a top-level models array; each entry describes one model.
# top of ~/.codex/config.toml model_catalog_json = "/Users/you/.codex/my-models.json" model = "your-provider/your-model-id" model_provider = "my-gateway"
{
"models": [
{
"slug": "your-provider/your-model-id",
"display_name": "Your Model (gateway)",
"description": "Coding model via the gateway",
"context_window": 262000,
"max_context_window": 262000,
"supported_in_api": true,
"visibility": "list",
"priority": 1
}
]
}The slug must equal the string you send to the provider. The official catalog carries many more fields per model (reasoning levels, tool types, modality flags), and hand-authoring the full shape is fiddly — which is exactly why Fix 4 exists. Restart Desktop after any catalog change. This layer overrides both the bundled and the remote catalog, and it only re-reads on startup, so a per-thread config change will not reapply it.
FIX THE CONFIG LAYER
Fix 3: Move the provider to the user-level config
If Codex prints a startup warning about ignored provider settings, your provider block is in the wrong file. model_provider and model_providers only work in ~/.codex/config.toml. A repo's .codex/config.toml cannot define providers.
# check where your provider actually lives grep -rn "model_providers" ~/.codex/config.toml ./.codex/config.toml 2>/dev/null # if it is in ./.codex/config.toml, move the [model_providers.*] block # and model_provider = "..." into ~/.codex/config.toml, then restart
Keep repo-specific things like instructions files in the project config. Keep the provider and the model catalog user-level. One caveat: project-local model_catalog_json is a separate case — by design it should still be read, but on fresh Desktop threads it currently isn't, which is an open bug (#26308) rather than intended behavior.
FIX THE TOOLING LAYER
Fix 4: Let cc-switch v3.16.5 generate the catalog
If you manage Codex through cc-switch, update to v3.16.5 or later. That release is the one that fixes the empty picker for native providers. It generates ~/.codex/cc-switch-model-catalog.json for suppliers on native Responses endpoints (apiFormat: "openai_responses"), which is what lets Codex Desktop actually see the models and use built-in tools.
- Re-save each native provider once. The catalog is only regenerated on save. Existing providers keep their old (broken) state until you open and re-save them. There is no automatic migration.
- Understand the decoupling. In v3.16.5 the model catalog no longer rides on the "local routing" toggle. Native Responses suppliers now generate the catalog whether or not local routing is on, while Chat-format suppliers keep going through proxy conversion as before.
# confirm the catalog exists and lists your models after re-saving cat ~/.codex/cc-switch-model-catalog.json | grep -o '"slug":[^,]*' # if this is empty, re-save the provider in cc-switch and check again
One caveat cc-switch itself disables: a few first-party Chinese models (MiMo, LongCat, MiniMax, Qwen3-Coder) do not support OpenAI's built-in web_search on their gateways, so v3.16.5 turns that tool off for them by default to stop Codex throwing a hard 400. If you route those through a gateway, expect web search to be off.
ERRORS THAT MIMIC THE PICKER BUG
Rule these out before you touch the catalog
Half the "custom models not showing" reports are a broken route wearing a display costume. Each one produces a symptom that looks like the picker hiding your model, when the request never had a chance to land.
| Symptom | Real cause | Fix |
|---|---|---|
| Startup error, or every request 404s | wire_api = "chat" (removed Feb 2026) or a gateway with no /responses endpoint | Set wire_api = "responses" |
| Every request returns 401 | env_key named but the variable never exported | Export the key in your shell profile |
| Model runs but returns the wrong output | Catalog slug does not match the provider's model ID | Match slug to the exact model string |
| Provider ignored, startup warning printed | Block lives in project-local .codex/config.toml | Move it to ~/.codex/config.toml |
| Intermittent connection resets | Trailing slash or wrong path on base_url | End the URL at /v1, no trailing slash |
The tell that separates these from the real #19694 filter is one command: run the same request from the CLI. If the CLI also fails, it is one of the errors above, not the Desktop picker, and no catalog edit will fix it. If the CLI succeeds and only Desktop stays blind, then it is the filter — and you are back to Fix 1 and Fix 4.
ISSUE TIMELINE
Known Codex Desktop custom-model issues (2026)
This is not one bug. It is a cluster of related issues in the Codex app and in the tools that write its config. Knowing which one you have saves you from applying the wrong fix.
| Issue | What breaks | Reported | Status |
|---|---|---|---|
| openai/codex #19694 | Desktop picker filters out catalog models the backend loaded | 2026-04-26 | Open |
| openai/codex #26308 | Desktop ignores project-local model_catalog_json on fresh threads | 2026 | Open |
| openai/codex #22160 | CLI /model and Desktop pickers did not expose profile / provider aliases | 2026 | Closed |
| openai/codex #15364 | No UI to pick a custom provider inside the Desktop app | 2026 | Closed |
| cc-switch #3668 | cc-switch catalog format not recognized, /model empty | 2026-06-03 | Fixed in v3.16.5 |
The pattern across all of these: the routing works, the display does not. The CLI has been the reliable escape hatch every time, because it reads the catalog without the Desktop renderer's extra filtering. If you are blocked in Desktop, the CLI (codex in your terminal) will almost always show and use the model.
WHEN THE PICKER STILL WON'T SHOW
Alternatives that work now
If you are done fighting the Desktop UI, here are ways to keep using custom models without waiting on a client fix.
| Path | How the model reaches Codex | Picker behavior | Notes |
|---|---|---|---|
| Codex CLI | Same config, terminal client | Lists custom models reliably | Bypasses the Desktop filter |
| A unified gateway | One base_url, one key, many model IDs | "Custom" or catalog-listed | Swap models with a one-string edit |
| Direct provider key | A provider block per vendor | Same filter applies | More keys, more blocks to maintain |
| Local (Ollama) | base_url at localhost | Warnings unless catalog set | Offline, no gateway |
The point of a gateway here is one endpoint and one key across many models, so switching a model is a one-string edit rather than a new provider block each time. Whichever path you take, the Desktop picker quirk is a display layer on top, not a routing constraint.
VERIFY
How to confirm your models are actually loaded
Do not trust the picker as your source of truth — that is the thing that is broken. Verify at the layer below it.
codex --version # confirm you are on a recent build codex # start the CLI, then type /model # the CLI enumerates the catalog without the Desktop filter
If /model in the CLI shows your model, the catalog and provider are correct and any remaining gap is the Desktop renderer. If the CLI also comes up empty, the problem is upstream: wrong file (Fix 3), malformed catalog (Fix 2 / Fix 4), or a bad slug. For a raw sanity check that the route itself resolves, one curl against the gateway with your key tells you whether the model exists before you blame the client at all. Routing first, display second.
WHAT THIS ACTUALLY TEACHES
Your tooling's reliability shouldn't hinge on a UI filter
The fixes above get your picker back. The rest is the part worth acting on before the next bug.
You can read this bug two ways. The narrow way: a Desktop client filters out your models, set them inline, move on. The useful way: a tool you treated as part of your workflow silently dropped functionality at the rendering layer, and the only signal was that the dropdown looked empty.
We see this in nearly every AI system we audit. A team builds on one client, one UI, one default model, and a bug, a deprecation, or a quiet filter turns a working setup into a half-day of guessing. The Codex picker bug is just the most recent, most specific version of it.
Three things make a setup survive these changes:
Verify at the layer below the UI
Don't trust the picker. Trust the endpoint, the CLI, the curl. The display is the last thing to believe and the first thing to break.
Decouple the model from the tool
One config string swaps the model; one gateway swaps the provider. When a client filters or a vendor deprecates, your integration is a config change, not a rewrite.
Design for the bug, not the demo
Assume every client UI has a filter you can't see and every free path has an expiration date. Build the escape hatch before you need it.
If your workflow collapses when one client hides a model, the workflow failed the design test — not the client.
FAQ
Questions we get about this
Why is Codex Desktop not showing my custom models?+
Usually the model works and the Desktop app is filtering it out of the picker (issue #19694). Secondary causes: no catalog file, a malformed catalog from an older tool, or a provider defined in a project-local .codex/config.toml.
How do I add a custom model to Codex config.toml?+
Define [model_providers.NAME] with base_url, env_key, and wire_api, then set model_provider and model. Keep it in ~/.codex/config.toml. For a gateway, base_url ends at /v1 with no trailing slash.
Does cc-switch work with Codex Desktop?+
Yes, from v3.16.5, which generates ~/.codex/cc-switch-model-catalog.json for native Responses providers. Re-save each provider once after updating — the catalog only regenerates on save.
Where is the Codex model catalog file located?+
Wherever model_catalog_json points in ~/.codex/config.toml. cc-switch uses ~/.codex/cc-switch-model-catalog.json. Watch for a stale ~/.codex/models_cache.json after switching providers — delete it to force a clean rebuild.
Why does Codex show "Custom" instead of my model name?+
You set model inline with no catalog entry, so the picker has no display metadata. Requests still go to the right model. Add a model_catalog_json (Fix 2) to give it a real name.
Can I use custom models in a project-local .codex/config.toml?+
No. Provider settings only apply in ~/.codex/config.toml. Project-local provider blocks are ignored with a startup warning. Keep the provider user-level; keep repo-specific instructions in the project config.
What is model_catalog_json in Codex?+
A config.toml key pointing to a JSON file, loaded at startup, with a top-level models array of entries carrying slug, display_name, and capability fields. It overrides both the bundled and the remote catalogs.
What is wire_api and why does it matter?+
It selects the request protocol. Codex removed the chat/completions path in early February 2026; responses is now the only supported value and the default. A provider set to chat fails on startup, and a gateway with no /responses endpoint 404s every request.
About FACTA
FACTA helps startups and growth-stage teams turn AI into production systems that keep running — not demos that impress once.
We design the architecture around the parts that actually break under real usage: model abstraction, credentials you own, failover, cost controls, observability. The boring infrastructure that keeps a system alive after launch.
Led by Matías Baglieri and Carolina Fogliato, we focus on one thing:
AI leadership that builds. Not just advises.
Stop firefighting tooling bugs. Automate the layer.
When a client UI quietly drops your models, the fix is rarely one more config edit — it's an automation layer that keeps your tooling reliable, observable, and decoupled from any single client. FACTA designs and builds that layer so your workflow survives the next filter, deprecation, or breaking change.
Explore AI Automation