/responses to the base_url you configure — it never inserts a /v1 of its own. Relay serves it a dedicated base, https://your-relay/codex/v1, so a turn lands on POST /codex/v1/responses.
Relay translates the Responses shape into whatever the routed upstream speaks, so Codex is not limited to OpenAI models — any model your relay key’s policy grants works, including Anthropic, Gemini and a self-hosted Ollama build.
Setup
Add a provider block to~/.codex/config.toml (CODEX_HOME relocates that directory) and point the top-level model_provider at it:
RELAY_API_KEY=sk-wr-your-relay-key codex. The key goes out as Authorization: Bearer, which is how Relay expects a relay key.
Three constraints come from Codex itself. The provider id cannot be openai, ollama, lmstudio or amazon-bedrock* — those are reserved and the built-in definition wins. The name must be neither OpenAI nor azure, both of which switch Codex into vendor-specific behaviour. And wire_api must be responses; "chat" is rejected at config load, which breaks every codex invocation until you remove it.
Model names
Codex resolves the model string against a catalog compiled into its binary, and falls back to synthesised metadata with a warning for anything it does not recognise — so any Relay model name works as-is. Naming a model with a known prefix inherits that model’s capability flags, which is occasionally what you want and otherwise harmless.model_context_window can only lower a recognised model’s window, never raise it: Codex clamps the value against its own catalog maximum.
Model picker
Codex’s/model picker lists a catalog compiled into its binary. For a provider authenticated with env_key it never asks the gateway for a list, so the only way in is model_catalog_json — and that file replaces the bundled catalog wholesale, including the agent prompt Codex sends with every turn, which the catalog is where Codex keeps. A picker file therefore always comes with a prompt file; the two are one step, not a file and an option.
codex --version — the prompt ships with the binary and changes between releases.
Both are top-level keys, so they must sit above the first [table] header in config.toml — above [model_providers.relay], [projects."…"], [profiles.…], all of them. TOML assigns every key after a header to that table, so the same two lines appended to the end of the file silently become projects."…".model_catalog_json and are ignored: the picker keeps showing Codex’s bundled models and nothing is logged.
Verify placement with codex exec --strict-config -m <model> "ok". Strict mode rejects a key Codex does not recognise and prints its file, line and full dotted path, which is the only signal you get — without it a misplaced key is dropped in silence.
Once the keys are genuinely top-level, a missing or empty prompt file is loud: Codex refuses to start and names the file. The catalog file on its own is not — every model listed in it would run with no agent prompt at all, and nothing says so.
Both values are paths, never URLs — Codex reads them off disk at startup. A leading ~ expands to your home directory, and a relative path resolves against the directory of the config file it appears in ($CODEX_HOME, ~/.codex by default), so model_catalog_json = "relay-models.json" picks up ~/.codex/relay-models.json as well. model_catalog_json is also accepted inside a [profiles.<name>] block, and it is applied at startup only.
The document lists exactly the models your relay key’s policy grants, each with the catalog’s context window, its hosts and their per-Mtok rates as the picker subtitle, and reasoning-effort options for models the catalog marks as reasoning. It is a snapshot: re-run the curl after a policy or catalog change.
If you would rather hand-write the file, this is a complete minimal entry — every key shown is required except context_window, and Codex rejects a document with no models:
visibility: "list" is what puts a row in the picker, supported_in_api: true is what keeps it there for a key-authenticated provider, and the list is ordered by ascending priority. context_window is optional and omitted when the catalog declares none; leaving max_context_window out keeps your model_context_window override free, since Codex clamps that value against the catalog maximum when one is present. Codex has no field for a maximum output-token count, so Relay does not project one. Unknown keys are ignored, and a model slug absent from the file still routes — Codex synthesises fallback metadata for it with a warning.
Per-role models
[profiles.<name>] blocks and the -p/--profile <name> file layer ($CODEX_HOME/<name>.config.toml) both carry their own model and model_provider, and agent-role files do too — so one Relay can serve a cheap review model and an expensive build model from a single config.
What you get
- Usage — every turn appears in
GET /logsand the/usage/*endpoints, attributed to your relay key, with token counts and timing. - Payloads — with payload logging enabled, the full request and response bodies are readable at
GET /logs/{request_id}. - Attribution — the client’s
session-id,thread-id,x-client-request-id,x-codex-installation-id,x-codex-window-id,x-codex-parent-thread-id,x-openai-subagentandoriginatorheaders are recorded on the usage event asextras, alongsideclient=codex. That is what lets you cost one session, or one sub-agent turn inside it.
Known limits
- Hosted tools do not cross to other vendors. Codex’s
apply_patch(acustomtool) and itsnamespacegroupings are translated for non-OpenAI upstreams — the model sees a function taking oneinputstring and Codex receives a normalcustom_tool_callback — butweb_searchand other server-side tools have no equivalent elsewhere and are dropped on such routes. Every tool on an OpenAI upstream is forwarded as sent. - The picker’s prompt file is Codex’s generic one.
model_instructions_fileapplies a single prompt to every model, where Codex’s bundled catalog carries a per-model instruction template for each OpenAI model it ships. It also drifts with Codex releases, so re-fetch it at the matching tag when you upgrade. - Codex does not retry a 429 and ignores
Retry-After. Its only backoff hint is atry again in Nsphrase scraped out of the error message text, so a rate-limited turn surfaces to you rather than resolving itself. - A signed-in ChatGPT account overrides
base_url. If Codex is logged in, requests can bypass the gateway entirely and reach no Relay; runcodex logoutfirst. - Use
127.0.0.1, notlocalhost, in a localbase_url— Codex’s name resolution fails on the latter.