Blog

Codex CLI Custom Providers: model_providers, wire_api, and the Responses-Only Rule

Pointing Codex at another provider is a few lines of config.toml, plus one hard constraint that decides whether the provider works at all: wire_api only accepts responses.

Codex CLI Custom Providers: model_providers, wire_api, and the Responses-Only Rule

Pointing Codex CLI at a different provider is a small change to ~/.codex/config.toml: one line picks the provider, a table defines it, and three fields usually complete it. But there is one hard constraint that decides whether the provider works at all, and it is not obvious from the shape of the config file: the wire protocol setting accepts a single value, responses, so the endpoint you point Codex at must speak the Responses API shape. A provider that only implements Chat Completions is not misconfigured; it is shape-incompatible, which is a different problem with a different fix.

Everything below follows the official Codex manual, which is the source for the examples, field names, and defaults quoted here. Verify against the current page before you ship a config.

The minimal configuration, as published

The manual's example defines three providers at once. Reduced to the essential pattern, the configuration looks like this:

model = "gpt-6.1-sol"
model_provider = "proxy"

[model_providers.proxy]
name = "OpenAI using LLM proxy"
base_url = "http://proxy.example.com"
env_key = "OPENAI_API_KEY"

The top two lines select the model and the provider ID. The table then defines that ID: a display name, the base URL Codex calls, and the environment variable that supplies the key. Two variations in the same official example matter for understanding the model:

  • A local Ollama entry uses base_url = "http://localhost:11434/v1" and no env_key at all, because there is no key to supply.
  • A Mistral entry uses base_url = "https://api.mistral.ai/v1" with env_key = "MISTRAL_API_KEY".

Notice the base URLs: the Ollama example carries a /v1 suffix, the proxy example does not. That is not an inconsistency to average out; it reflects how each endpoint mounts its API. More on that in the troubleshooting section.

The field table, field by field

The manual documents the custom provider fields individually. The core rows are restated below, with defaults marked as official defaults:

| Field | Type | Meaning |
| --- | --- | --- |
| name | string | Display name for the custom provider |
| base_url | string | API base URL for the model provider |
| env_key | string | Environment variable supplying the provider API key |
| wire_api | responses | Protocol used by the provider; responses is the only supported value and the default when omitted |
| query_params | map | Extra query parameters appended to provider requests |
| http_headers | map | Static HTTP headers added to provider requests |
| env_http_headers | map | HTTP headers populated from environment variables when present |
| request_max_retries | number | Retry count for HTTP requests to the provider (official default: 4) |
| stream_max_retries | number | Retry count for SSE streaming interruptions (official default: 5) |
| stream_idle_timeout_ms | number | Idle timeout for SSE streams in milliseconds (official default: 300000) |
| supports_standalone_web_search | boolean | Advertise support for a compatible standalone web search endpoint (official default: false) |

Two rows deserve emphasis before you write a line of config. wire_api is covered in the next section because it decides compatibility. And stream_idle_timeout_ms is in milliseconds, so the default is three hundred thousand milliseconds; write it as the manual does and do the arithmetic once, rather than pasting a number that looks like seconds.

The hard constraint: wire_api only accepts responses

This is the sentence the whole article exists to deliver, quoted from the manual's field table:

responses is the only supported value, and it is the default when omitted.

There is no chat value to switch to. If your endpoint implements only the Chat Completions shape, the correct conclusion is not "adjust the config"; it is "this endpoint does not speak the shape Codex requires." The fix lives on the endpoint side: run a translation layer that exposes the Responses shape, or choose a provider that implements it natively. The differences between the two shapes, and how to test which one an endpoint really implements, are covered in the API shape guide; the broader compatibility checklist is in the integration checklist.

This is also why so many "compatible" endpoints fail with Codex while working fine with other tools: compatibility with Chat Completions is a different claim from compatibility with the Responses shape, and the config file cannot bridge the gap.

Reserved IDs: do not name your provider openai

Custom provider IDs cannot reuse the built-in ones. The manual names three reserved IDs: openai, ollama, and lmstudio. If you need a different base URL for the built-in OpenAI provider, the manual points to a dedicated setting, openai_base_url, rather than an override table, because built-in provider IDs cannot be overridden. For everything else, pick a distinct ID such as proxy or, as the official example does for a local Ollama instance, local_ollama.

This is a small rule with a large failure mode: a provider named openai in your config will not do what the name suggests, and the resulting confusion usually surfaces as a mysterious "wrong endpoint" debugging session.

Keys and headers without putting secrets in the file

The configuration file describes the key; it does not contain it. env_key names the environment variable that supplies the API key, and Codex reads that variable at request time. That has a practical consequence worth designing around: rotating a key means changing an environment variable and restarting, not editing a config file that might be committed somewhere. The credential-storage and login modes around Codex are covered in the credentials guide, and the broader discipline of keeping keys out of files that get copied, logged, or screenshotted is in the key hygiene guide.

Headers split along the same line. http_headers holds static header values that are fine to write down. env_http_headers holds header names whose values come from environment variables, for the headers that are effectively credentials in disguise. If a header carries a token, it belongs in the second map, not the first.

Troubleshooting in order

When a custom provider "does not connect," the useful move is to turn that sentence into decidable checks. In order of how often they are the actual cause:

  1. Does the endpoint speak the Responses shape? This is the most common cause by a wide margin, and it is invisible in the config. Test the endpoint directly with a Responses-shaped request before blaming Codex.
  2. Is the base URL path right? Follow the official examples for their respective targets, and confirm the mount path your endpoint actually uses. The Ollama example carries /v1; the proxy example does not. Do not copy a suffix across examples without checking.
  3. Is the environment variable exported in the shell that launches Codex? A variable set in another terminal, or in a service definition that was not restarted, does not exist for this process.
  4. Does the provider ID collide with a reserved name? openai, ollama, and lmstudio are taken.
  5. Do timeouts and retries fit your network? The official defaults are 4 request retries, 5 stream retries, and a 300000 millisecond stream idle timeout. If your network is slow or flaky, adjust deliberately rather than piling on retries.
  6. Is a corporate TLS proxy in the way? Certificate trust problems produce connection failures that look like configuration errors; the certificate and proxy configuration is covered in the corporate network guide.

The last field in the table is easy to over-read. The manual says the setting advertises support for a compatible standalone web search endpoint, defaults to false, and notes that standalone search remains under development and off by default; provider compatibility alone does not enable it. Setting the capability to true does not enable search either: the provider must support a compatible endpoint, and the selected model and runtime must support standalone search, with the configured web search mode and managed restrictions still applying.

In practice: do not treat this flag as a switch that makes web search work. Treat it as an honest declaration about your endpoint, and leave it false unless you have verified the endpoint side.

The checklist

  1. Confirm the endpoint speaks the Responses shape before anything else.
  2. Pick a provider ID that is not one of the reserved names.
  3. Write name, base_url, and env_key for the provider.
  4. Export the key variable in the shell that launches Codex.
  5. Put static headers in http_headers and secret-bearing headers in env_http_headers.
  6. Confirm the base URL path against the official example for your endpoint type.
  7. Send one real request through Codex and read the error, if any, as a shape, path, or credential problem in that order.
  8. Remember the official retry defaults (4, 5, and 300000 milliseconds) so changes from them are deliberate, and rotate keys by changing the environment variable.

A custom provider is a small table with a strict shape requirement attached. Get the shape right and the rest is bookkeeping; get it wrong and no amount of retry tuning will help. Platform-side details for the API this site documents are in its documentation, and every field name and default quoted here should be re-checked against the official manual current at build time.