Codex CLI and Desktop App with Local Models

Point OpenAI Codex at your own Macs. One provider block in config.toml, no model map to maintain, and the same fleet routing that serves Claude Code. Verified end to end on both the CLI and the desktop app.

TL;DR

Add this to ~/.codex/config.toml, then run codex:

model_provider = "herd"

[model_providers.herd]
name = "Ollama Herd"
base_url = "http://localhost:11435/v1"
wire_api = "responses"

No model map needed. Herd auto-routes whatever model id Codex sends to the best coding model you actually have loaded. Pull a model and go:

ollama pull qwen3-coder:30b

The same config block also works for the Codex desktop app, which reads the same file. See Quickstart if the herd is not running yet.

First, the wall you probably just hit

If you tried pointing Codex at Ollama, LM Studio, or any OpenAI-compatible server and it failed, this is why: Codex removed Chat Completions support in February 2026. wire_api = "responses" is the only valid value now, and it is the default.

That breaks most advice you will find. A server that implements only /v1/chat/completions cannot serve Codex at all, no matter how OpenAI-compatible it is otherwise. Guides still showing wire_api = "chat" predate the change.

Ollama Herd implements the Responses API directly at /v1/responses, so there is no translating gateway in the middle. That is what the wire_api = "responses" line above is telling Codex.

The config mistake that costs an hour

If you are appending to an existing ~/.codex/config.toml, model_provider = "herd" must go above the first [table] header. TOML assigns a bare key to whatever table precedes it, so pasting it at the bottom silently turns it into desktop.model_provider. Codex never sees it, no error is raised, and it quietly keeps using the default provider.

model_provider = "herd"     # first line, before ANY [section]

[some.existing.section]
...

[model_providers.herd]      # a table header, so this can live anywhere

Using the Codex desktop app

No extra setup. The macOS desktop app reads the same ~/.codex/config.toml as the CLI, so the one provider block above serves both surfaces. Configure it once, and the app routes to your fleet the next time you open it.

Three things behave differently in the app, all verified against a real conversation:

  • It sends more than one model id. The conversation uses the id from the in-app picker, and a second id fires alongside it for chat-title generation. Both auto-routed with no map. This is the clearest argument for auto-routing over a hand written model map: the map would have had to predict an id you never chose.
  • The model picker may look empty. Codex decodes /v1/models against its own undocumented schema, so the picker can fail to populate. This is cosmetic. Inference is unaffected, every turn still routes to your fleet, and the app carries its own model list regardless. Set the model in config.toml rather than the picker.
  • There is no -m flag. Where the CLI takes codex -m qwen3-coder:30b, the app relies on config plus auto-routing. Pin a model with FLEET_ANTHROPIC_MODEL_MAP if you want a specific one every time.

Everything else on this page, the provider block, the TOML gotcha, model resolution, and troubleshooting, applies identically to both surfaces.

Verified end to end, on both surfaces

Plenty of projects document Codex on local models. This one was run against a real client on a real fleet, and the findings are published rather than asserted.

  • Codex CLI against the live fleet: provider: herd, model id gpt-5.6-sol auto-routed to qwen3-coder:30b, x-fleet-served-model set on the response, traced as original_format='responses'.
  • Codex desktop app, same config file, no extra setup: a real multi turn conversation served entirely by the fleet, three streaming turns of 6,209 to 8,579 prompt tokens each, all completed on one node via qwen3-coder:30b.
  • Three different model ids auto-routed with no map: gpt-5-codex, gpt-5.6-sol, and gpt-5.6-luna. That last one fired for chat title generation. A hand written map would have had to guess it existed, which is exactly the fragility auto-routing removes.
  • Real agentic coding, 4 tasks out of 4. Codex fixed two bugs and created a new module from scratch to satisfy a failing import, then ran pytest itself and reached green. Not model-specific: qwen3-coder:30b and gpt-oss:120b both drove the full loop. File editing and file creation both work.

Then a 26 hour soak across every code change in the release: 11,925 requests at 99.85% success, including 302 requests to /v1/responses with zero failures. The 18 failures all trace to a known slowness issue with one specific model, not to the Codex path.

Verification also found bugs unit tests had not, including tool calls being dropped in both directions and a model-listing schema that failed Codex's whole decode. Those are fixed. Spec-complete is not the same as client-verified.

Getting good results

Three things make the difference between a loop that finishes and one that wanders, all measured on local models:

  • Name the tool in your prompt. This is the single biggest quality lever. "Diagnose the root cause and fix it" sent qwen3-coder:30b exploring until it exhausted its budget. The same task phrased as "use the apply_patch command to fix X, then re-run pytest" succeeded.
  • If it stops after announcing an action, say "continue". About 3% of turns end with the model saying "Let me run pytest, then fix it" and stopping, which the protocol reads as done. It is stochastic, so a fresh chat will not help.
  • Check the diff, not the model's summary. In one run that produced a correct fix, the model also claimed it could not run pytest, minutes after running it successfully. The code was right; the narration was not.

Budget for tokens too: a one-line fix cost about 109K tokens end to end, because agentic loops resend the whole conversation each turn and Codex's system prompt alone is around 27 KB.

How model selection works

Codex sends a model id such as gpt-5-codex. Herd resolves it in this order:

  1. You pinned it. An explicit FLEET_ANTHROPIC_MODEL_MAP entry for that id wins.
  2. It names a real local model. If the id matches a model your fleet actually has (qwen3-coder:30b), it is used as-is. This is why codex -m qwen3-coder:30b works.
  3. Auto-routing, the default. Otherwise Herd picks the best coding model currently loaded across the fleet, falling back to the best one on disk.
  4. Nothing available. A clear 404 telling you to pull a model.

To pin one id without affecting anything else:

export FLEET_ANTHROPIC_MODEL_MAP='{"gpt-5-codex": "qwen3-coder:30b"}'

To disable auto-routing and require an explicit map, set FLEET_ANTHROPIC_AUTO_ROUTE=false.

What the fleet gives you

  • Multi-node routing. Every request is scored across all your Macs on thermal state, memory fit, queue depth, model affinity, and context fit, then sent to the best one.
  • No model map to maintain. Auto-routing tracks what you have actually pulled instead of a list you keep in sync by hand.
  • Routing around the Mac you are using. A machine in a video call or thermally throttling is scored down, so your agent does not fight you for your own laptop.
  • Images route themselves. When Codex sends a picture, Herd routes that request to a vision-capable model on your fleet even when your coding model cannot see. Verified with gemma3:27b. If you have no vision model, ollama pull gemma3:4b is about 6 GB.
  • Both of Codex's tool protocols work. Codex sends tools two different ways depending on the model slug, and the newer shape (openai/codex#31894) leaves the model with nothing callable even against OpenAI's own hosted models. Herd translates both, so you do not hit it.
  • One endpoint for both agent CLIs. The same herd serves Codex here and Claude Code on /v1/messages.
  • Observability. Every request lands in the trace store, and X-Fleet-Served-Model tells you exactly which model and node answered.

Choosing a model

Codex calls tools constantly, so tool-use quality matters more than chat quality. Coding-tuned models with a real tool-call parser hold up; general chat models drop calls and hallucinate arguments.

We verified with qwen3-coder:30b (about 19 GB, 256K context). Newer coding families have since landed in Ollama's library: Qwen 3.6, Gemma 4, GLM-5, Kimi K2 Code, DeepSeek V4. See the current tool-capable list.

You do not need to tell Herd which one you picked. Auto-routing sends Codex to the best coding model you have loaded, so upgrading is ollama pull and nothing else.

Limitations, stated plainly

  • MLX-backed models are not served on this path yet. Auto-routing skips mlx: models for Codex so you always land on a working Ollama-backed model. An explicit mlx: mapping returns a clear 503. Claude Code's /v1/messages does serve MLX.
  • No stateful chaining. Herd does not persist responses, so previous_response_id is rejected with a 400. Codex's default stateless mode, resending the conversation each turn, is what is supported and what it actually does.
  • Hosted tools are dropped. web_search, file_search, and MCP tool items have no local equivalent. Function tools pass through normally.
  • The model picker may not populate. Codex decodes /v1/models against its own undocumented schema rather than OpenAI's. This is cosmetic, inference is unaffected, and every turn routes normally. Specify the model with -m or in config rather than the picker.
  • Codex will not recognise your model's name. It keys metadata off its own table of OpenAI slugs, so expect Model metadata for <name> not found. Harmless: tool calling, editing, and multi-turn all work anyway.

Troubleshooting

404 not available on any node. You have not pulled a chat or coding model, or the one you pinned is not on the fleet. Run ollama pull qwen3-coder:30b, or check curl localhost:11435/v1/models.

Codex errors about the wire protocol. Confirm wire_api = "responses" in ~/.codex/config.toml. wire_api = "chat" was removed from Codex in February 2026.

400 previous_response_id not supported. Your client is using server-side conversation state. Stateless mode, which Codex uses by default, is what Herd supports.

Codex ignores your config entirely. Check that model_provider = "herd" sits above the first [table] header. See the config mistake above.

Tool calls come back as plain text. The model is too small or not coding-tuned. Switch to one of the recommended models above.

Related Reading