Skip to content

Codex CLI and the Responses API: why most relays 404

OpenAI's Codex CLI doesn't call the classic Chat Completions API — it speaks the newer Responses API wire format. Most OpenAI-compatible relays only implement Chat Completions, so Codex pointed at them fails with 404s or malformed-request errors even though the same key works in curl. Router One implements the Responses wire format natively (wire_api = "responses"), so Codex works with a two-minute config change.

The one-line root cause

Codex sends requests to /responses-style routes with a request schema that Chat Completions servers don't recognize. A relay that only speaks Chat Completions answers with 404 (unknown path) or 400 (unknown fields). No amount of key or model fiddling fixes a wire-format mismatch — the gateway itself has to support it.

The working configuration

Point Codex at Router One in ~/.codex/config.toml and export your key in the same shell that runs codex. Keep the model line so Codex runs the model you chose: per the Codex models docs and the 0.159.1 release notes (checked 2026-10-06), Codex uses its recommended model when config.toml names no model, and from 0.159.1 that is gpt-6.1-sol. As of the 2026-10-06 plan response, gpt-5.6-sol and gpt-6.1-sol are both in the Premium models tier of the Pro, Max and Ultra plans. The config:

config.toml
# ~/.codex/config.toml
model = "gpt-5.6-sol"
model_provider = "router"

[model_providers.router]
name = "router"
base_url = "https://api.router.one/v1"
env_key = "ROUTER_ONE_API_KEY"
wire_api = "responses"

# in your shell:
# export ROUTER_ONE_API_KEY=sk-your-router-one-key

Error checklist

If Codex still fails after the config change:

Error checklist
SymptomCauseFix
404 on every requestbase_url points at a relay without Responses API support, or has a typoSet base_url to https://api.router.one/v1 exactly and keep wire_api = "responses".
401 unauthorizedROUTER_ONE_API_KEY isn't visible in the shell running codexExport the key in the same session (or your shell profile), then restart the terminal and re-run codex.
Model not foundModel ID doesn't match the catalogCopy the exact ID from the /models page — IDs are case-sensitive.
402 insufficient balanceWallet or key spend cap exhaustedTop up or raise the key's maxSpend in Dashboard → API Keys.
Config seems ignoredCodex reads a different config file than the one you editedConfirm the file lives at ~/.codex/config.toml (Windows: %USERPROFILE%\.codex\config.toml).
400 mentioning background or image_generationCodex asked for a background response or the image_generation tool — the two Responses features the gateway does not runDisable background mode; generate images through POST /v1/images/generations instead. Hosted tools and previous_response_id keep working on natively served Responses models.
400 saying the model must be called via /v1/messagesA Claude-family model ID was sent to the Responses endpoint; Claude is served on the Anthropic-compatible endpoint and on /v1/chat/completions, never on /v1/responsesSwitch Codex to a model whose detail page lists POST /v1/responses (currently listed GPT-family models, DeepSeek ids and Grok chat models). To work with Claude, use Claude Code instead.
Model not found for codex-auto-reviewWith approvals_reviewer set to auto_review, Codex's Auto-review agent may request the model ID codex-auto-review (per the openai/codex source, rust-v0.159.3, checked 2026-10-01); relays that only know their own model list, or only speak Chat Completions, reject the ID. /review is separate: it uses review_model when that is set, otherwise the session modelNothing to change on Router One — codex-auto-review is in the catalog and is served on /v1/responses and /v1/chat/completions. No subscription plan includes it, so those requests bill to the wallet; pricing is on /models/codex-auto-review. To choose the model /review uses, set review_model in ~/.codex/config.toml.
stream disconnected before completion: idle timeout waiting for SSE (or: stream closed before response.completed)Codex ended the stream itself: no SSE event arrived within its stream_idle_timeout_ms, or the connection closed before the final response.completed eventRaise stream_idle_timeout_ms on your [model_providers.router] block and check your proxy or network path. The section on these two errors below has the details.

What the Responses endpoint accepts — and the two things it rejects

Router One implements the Responses wire format itself, for the models whose detail page lists POST /v1/responses (currently listed GPT-family models, DeepSeek ids and Grok chat models). The table lists current behavior by request feature; each feature it accepts is billed at the model's posted rates.

What the Responses endpoint accepts — and the two things it rejects
Request featureStatusNotes
Function tools (Codex's default tool shape)AcceptedStructured tool calls come back as first-class Responses output items.
Custom tools (Codex's freeform apply_patch and similar)Accepted on natively served Responses modelsUse a model whose page lists POST /v1/responses; other models get 400 invalid_request.
previous_response_id / conversation / prompt (server-side context references)Accepted on natively served Responses modelsSame model rule as custom tools; the reference is resolved on the native path.
Hosted tools: file_search, code_interpreter, computer_use, mcp, web_searchAccepted on natively served Responses modelsForwarded as sent and billed at the model's posted rates — Router One runs none of them itself.
input_file parts with file_id or file_urlAccepted on natively served Responses modelsSame model rule; the file reference travels with the request.
stream / instructions / temperature / max_output_tokensAcceptedSee the Responses API reference.
Long waits before the first event or between events (streaming, named model)Not cut off by the gatewayThe native Responses stream has no gateway-side response-header or idle timeout; it ends when Codex disconnects, the request deadline passes, or the upstream errors or closes the stream. model:auto candidates keep their time budget.
Long non-streaming requests (stream: false)Held open with a JSON keepalive; returned as one bodyThe gateway reads the model's output as a stream and returns the finished response object as one JSON body. After about 25 seconds it commits HTTP 200 and writes JSON leading whitespace, so a failure after that point arrives as HTTP 200 with a top-level error object — check for error before reading output. Use stream: true for long search-backed or agentic requests.
Cancelling mid-stream (Esc in Codex, or a dropped connection)Recorded as HTTP 499 client_cancelledThe gateway waits up to five seconds for the final usage and bills it; if none arrives after output has streamed, the part the upstream did not report is billed from an estimate (as of 2026-10-13). The rest of the reserved balance is released and the request is never failed over (see /llm-streaming).
image_generation tool and image_generation_call input itemsRejected — 400 invalid_requestImage generation is not offered through the Responses endpoint; use POST /v1/images/generations (see /image-generation-api).
background: trueRejected — 400 invalid_requestNo background execution and no GET /v1/responses/{id} to fetch a result later; each request completes within its HTTP connection.

Codex "stream disconnected before completion" errors

Codex CLI prints this prefix for two different stream failures. Both are raised by Codex itself when the SSE stream from the provider ends early, and Codex retries an interrupted stream up to stream_max_retries times (default 5) before it gives up.

stream disconnected before completion: idle timeout waiting for SSE

No SSE event arrived within stream_idle_timeout_ms, 300000 ms (five minutes) by default, so Codex abandoned the stream. A long reasoning pass on a GPT model can stay silent that long, and so can a stalled local network path. Router One does not end a native Responses stream for being idle (see the table above), so the timer that fires is Codex's own. Keepalive comment lines (lines that start with a colon, such as : ping) cannot help, whichever server sends them: Codex's SSE parser drops comment lines, so only real events reset its idle timer.

stream disconnected before completion: stream closed before response.completed

The connection closed before the terminal response.completed event arrived. Check the local path first: a proxy, VPN or firewall that cuts long-lived HTTPS connections produces exactly this. Then look up the attempt in Dashboard → Logs by time and model.

What to change

For long-reasoning GPT runs, raise stream_idle_timeout_ms on the provider block, for example to 600000 (ten minutes) as below. The retry settings live on the same block: stream_max_retries (default 5) for interrupted streams and request_max_retries (default 4) for HTTP requests. All three work only on a custom [model_providers.<id>] entry such as router; the built-in openai, ollama and lmstudio IDs are reserved and cannot be overridden. If the errors continue, try another network or bypass the proxy.

Read the attempt in Logs

When Codex abandons a stream after an idle timeout, it drops the connection, and the gateway records that request as HTTP 499 client_cancelled, billed for the usage the upstream reported or, if none arrived after output had streamed, from an estimate (as of 2026-10-13; see /llm-streaming). A record that shows a time to first token (TTFT) had already produced output before the stream stopped, so the drop came mid-stream; a streamed record without one never produced output, which points at a slow first token or a failure before any output. A missing record is a clue, not proof, that the request never reached the gateway.

config.toml
# ~/.codex/config.toml
[model_providers.router]
name = "router"
base_url = "https://api.router.one/v1"
env_key = "ROUTER_ONE_API_KEY"
wire_api = "responses"
stream_idle_timeout_ms = 600000
# defaults: stream_max_retries = 5, request_max_retries = 4

Verify from the trace

Every Codex call the gateway sends to a model shows up in Dashboard → Logs with model, tokens, cost, total time (plus time to first token on streamed requests that produced output) and status. If the log stays empty while Codex errors, read the status Codex printed before changing the config: a 401 (key), a 402 (funds), a 429 from your key's or account's rate limits, or a 400 or 404 for the model ID or endpoint is refused before any model is called and leaves no record; otherwise check the local side first (config path or env var). Treat the empty log as a clue rather than proof — a record still awaiting pricing can be missing too. An entry with an error status got past those checks; its status and error message name the cause.

FAQ

Why does my key work in curl but not in Codex?

curl exercises the Chat Completions surface; Codex uses the Responses API wire format. A relay can pass the curl test and still 404 for Codex. The gateway must implement the Responses wire format — Router One does.

What does wire_api = "responses" actually do?

It tells Codex which protocol shape to speak with the provider block. With "responses", Codex sends Responses-API-formatted requests; the endpoint on the other side has to understand them natively.

Does the base URL need /v1?

Yes — for Codex the OpenAI-compatible base URL is https://api.router.one/v1 including /v1. That's the opposite of Claude Code, whose Anthropic-compatible base URL has no /v1 suffix.

What does "stream disconnected before completion" mean in Codex, and how do I fix it?

Codex is reporting that its SSE stream ended early, in one of two forms. idle timeout waiting for SSE means no event arrived within stream_idle_timeout_ms (300000 ms by default); for long-reasoning GPT runs, raise it on your custom provider block, for example stream_idle_timeout_ms = 600000. stream closed before response.completed means the connection closed before the final event; check your proxy, VPN or network first. Router One does not end a native Responses stream for being idle, and keepalive comment lines cannot reset Codex's timer. In Dashboard → Logs the abandoned attempt appears as HTTP 499 client_cancelled, billed for the usage the upstream reported or, if none arrived after output had streamed, from an estimate (as of 2026-10-13).

Can I set stream_idle_timeout_ms on the built-in openai provider?

No. Built-in provider IDs (openai, ollama and lmstudio) are reserved and cannot be overridden, so stream_idle_timeout_ms, stream_max_retries and request_max_retries apply only to a custom [model_providers.<id>] entry. The router block this page configures is one, so add the keys there.

Can I set openai_base_url instead of adding a custom provider?

Codex's advanced configuration docs (checked 2026-10-01) present openai_base_url as a way to point the built-in openai provider at a proxy or router; this page uses a custom [model_providers.router] provider instead, and that is the setup we recommend. Per the Codex source (rust-v0.159.3, checked 2026-10-01), openai_base_url replaces the built-in provider's address whatever you are signed in with, so the credential Codex holds for OpenAI goes to that address, a ChatGPT sign-in's tokens included. If you use openai_base_url anyway, sign Codex in with your Router One API key using codex login --with-api-key (Codex's authentication docs, checked 2026-10-01), never with ChatGPT. The custom provider takes its key from env_key instead of your OpenAI sign-in, and it is the only place the stream and retry settings above apply. Per the advanced configuration docs, Codex ignores both openai_base_url and model_providers in a project-local .codex/config.toml, so set either one in ~/.codex/config.toml.

Which models can Codex use through the gateway?

Currently listed GPT-family models served natively over the Responses wire format — GPT-6 Astra (openai/gpt-6-astra, listed 2026-09-05), GPT-6 Sol (openai/gpt-6-sol, listed 2026-09-23) and GPT-6.1 Sol (openai/gpt-6.1-sol, listed 2026-09-30) are among them; the GPT-6 Astra API guide covers Astra, the GPT-6 Sol guide covers Sol's endpoints, long-context tier and Codex setup, and the GPT-6.1 Sol guide covers what changes for 6.1 Sol and Codex's new default model (Codex's bundled model list includes gpt-6.1-sol from 0.159.1). DeepSeek V4.1 Flash (deepseek-v4.1-flash, listed 2026-09-10) and the older deepseek-v4-flash id are served natively on this endpoint as well; DeepSeek V4 Pro is not listed. The Grok chat ids — grok-4.7, grok-4.6, grok-4.5, grok-4.3 and grok-build-0.1 since 2026-09-22, and grok-4.7-build-fast since it was listed on 2026-09-23 — are served natively here as well, so Codex can point at any of them. Use the exact model ID from /models — each model page lists whether it exposes POST /v1/responses. Claude models are not on this endpoint: sending one gets a 400 back before any model is called.

I pointed Codex at a Claude model and got a 400. What now?

The gateway refuses that request before any model runs, and the message spells out where the model lives: model 'anthropic/claude-opus-5' must be called via /v1/messages or /v1/chat/completions. Claude is served on the Anthropic-compatible endpoint (the path Claude Code uses) and on /v1/chat/completions, not on /v1/responses. Point Codex at a currently listed GPT-family model, and use Claude Code when you want to work with Claude — see /claude-code-china.

What is the codex-auto-review model Codex keeps requesting?

It is the model ID Codex's Auto-review may request. With approvals_reviewer set to auto_review, a separate reviewer agent answers sandbox approval prompts instead of you, and per the openai/codex source (rust-v0.159.3, checked 2026-10-01) it may call codex-auto-review. /review is a different feature: it uses review_model from ~/.codex/config.toml when that is set, otherwise the session model. Relays that validate model names against their own list, or only implement Chat Completions, answer model not found. Router One lists codex-auto-review in the catalog, so the request is routed like any other model on /v1/responses and /v1/chat/completions; no subscription plan includes it, so plan users' Auto-review requests bill to the wallet — the current price is on /models/codex-auto-review.

Do previous_response_id and hosted tools work through the gateway?

Yes, on models served natively over the Responses wire format — the model page's endpoint list shows which. They are forwarded as sent and billed at the model's posted rates; only background: true and the image_generation tool are rejected with 400.

Why does Codex get 400 when it tries image generation?

Image generation is not offered through the Responses endpoint, so an image_generation tool or image_generation_call item is refused with 400 invalid_request before any model is called. Generate images with POST /v1/images/generations instead — see /image-generation-api.

Can I run a response in the background?

No. background must be omitted or false; background: true returns 400 invalid_request. The gateway does not run detached responses and has no GET /v1/responses/{id}, so a result cannot be fetched after the connection closes.

Does this work from Mainland China?

Yes. The gateway is reachable from Mainland China without a VPN; the config.toml above is identical everywhere.