Integrating with Codex CLI¶
Codex CLI is OpenAI's terminal coding agent. Its provider table lets it call any server that speaks the OpenAI Responses API or the OpenAI Chat Completions API, so a Codex CLI client can run against Continuum Router. This page covers that direction: the CLI is the client and the router is the server.
The opposite direction is a different subject. The router can also use the OpenAI Codex backend, including a ChatGPT Plus, Pro, or Enterprise subscription, as an upstream for its own clients. That setup is documented in ChatGPT subscription / Codex headless login and is not needed here.
Choose a wire¶
Codex CLI selects a wire per provider with wire_api. Both wires work with the router, under different conditions:
wire_api | Endpoint the CLI calls | What the router needs |
|---|---|---|
responses | POST /v1/responses | A backend that serves the Responses API itself through pass-through, or a conversion backend that lists the CLI's advisory fields in responses_drop. |
chat | POST /v1/chat/completions | Any backend the router serves. The CLI replays the conversation itself. |
The Responses wire carries everything the CLI can do, but its request body contains fields and tool shapes that only a native Responses backend can read. The Chat Completions wire works everywhere, because the CLI sends ordinary chat requests, at the cost of the Responses-only behavior described in What does not work today.
Responses wire¶
Add this provider entry to ~/.codex/config.toml, point the CLI at the router's /v1 prefix, and let it append /responses:
model = "gpt-5"
model_provider = "continuum"
[model_providers.continuum]
name = "Continuum Router"
base_url = "http://localhost:8000/v1"
env_key = "CONTINUUM_ROUTER_API_KEY"
wire_api = "responses"
env_key names the environment variable that holds the router API key. The CLI sends it as Authorization: Bearer <value>, so the variable must hold a key the router accepts, or any non-empty value when the router runs API-key authentication in permissive mode. model must be a model id one of your backends serves; the router answers 404 model_not_found otherwise.
Chat Completions wire¶
Use the same provider entry in ~/.codex/config.toml with wire_api = "chat":
model = "my-model"
model_provider = "continuum"
[model_providers.continuum]
name = "Continuum Router"
base_url = "http://localhost:8000/v1"
env_key = "CONTINUUM_ROUTER_API_KEY"
wire_api = "chat"
On this wire the CLI sends the whole conversation as chat messages on every turn. Nothing relies on server-side response state, and any Responses-only field or hosted tool the CLI would send on the other wire has no Chat Completions equivalent and is not sent.
Backend precondition¶
The Responses wire requires a backend whose own /v1/responses endpoint the router forwards to unchanged. backends[].responses_api selects that behavior, and its default, auto, decides by type:
openaiandazure: pass-through by default. No extra setting.- Every other type that speaks the OpenAI wire format (
generic,continuumrouter,vllm,sglang,tensorrt-llm,ollama,llamacpp,mlxcel,lmstudio): Chat Completions conversion by default. Addresponses_api: nativewhen the backend implements the Responses API itself. anthropic,bedrock, andgemini: no Responses endpoint to pass through to, soresponses_api: nativeis a load error on them. The Responses wire can only be converted there. A conversion serves the CLI's default body only after the backend opts into dropping the advisory fields the CLI sends (Conversion backends); otherwise use the Chat Completions wire.
A self-hosted backend that serves the Responses API, such as an agent runtime that owns its conversation state, needs one line:
backends:
- name: agent-runtime
type: generic
url: https://agents.example.internal
responses_api: native
api_key: "${AGENT_RUNTIME_KEY}"
models:
- gpt-5
The full contract, including lifecycle calls and hot reload, is in Native Responses API.
Conversion backends and responses_drop¶
A conversion backend can serve the Responses wire when it accepts the advisory fields the CLI sends unconditionally on every turn. backends[].responses_drop names them:
backends:
- name: vllm-b300
type: vllm
url: http://10.0.0.21:8000
responses_api: auto
responses_drop: [include, prompt_cache_key, reasoning.summary]
models:
- gpt-5
The CLI's default body carries all three fields (include: ["reasoning.encrypted_content"], a prompt_cache_key, and reasoning: {"summary": "auto"}), so an operator serving it must list all three. The list is per backend and empty by default: a field stays refused until the selected backend names it.
Only advisory fields may be listed: include (only when every value is allowlisted, today only reasoning.encrypted_content), prompt_cache_key, safety_identifier, and reasoning.summary. Semantic fields (text, conversation, max_tool_calls, reasoning.mode, reasoning.context, and every tool or input-item refusal) always refuse. An unknown or semantic name is a configuration load error.
Dropping costs the client two enhancements:
- No encrypted reasoning replay: the next turn carries no encrypted reasoning content, so the model sees only the visible transcript. A conversion backend's clients already behave this way.
- No upstream prompt-cache hint: the backend never receives
prompt_cache_key, so any provider-side prefix caching keyed on it is not used. The router's own prompt, prefix, and semantic caches are unaffected because they key on request content.
The reply discloses what was dropped in x-continuum-dropped-fields, for example include, prompt_cache_key, sorted and deduplicated. The same header appears on a response-cache hit, and the responses_drop_fields_total counter records one increment per dropped field per served request. responses_api: native ignores the list entirely.
Verify¶
Start the router, then run one turn with the CLI:
The reply comes from the model the backend serves. On the Responses wire, the router log records a POST /v1/responses forwarded to that backend instead of a converted /v1/chat/completions call.
Errors and remedies¶
The router refuses a field it cannot carry rather than dropping it, so the error names the field that reached a wire it cannot be written to. The native-only refusal reads:
Request field 'prompt_cache_key' requires a backend with native Responses API support; the selected convert_to_chat_completions strategy would lose it. Set 'responses_api: native' on a backend that serves the Responses API itself, or send the request over the Chat Completions wire from a client that can omit the field
| Observed error | Cause | Remedy |
|---|---|---|
400 unsupported_request_parameter naming prompt_cache_key, include, reasoning.mode/context/summary, text, safety_identifier, conversation, or max_tool_calls | The selected backend is served through a conversion strategy, which cannot carry a native-only field. The CLI sends prompt_cache_key, include: ["reasoning.encrypted_content"], and reasoning: {"summary": "auto"} on every turn. | Set responses_api: native on a backend that serves the Responses API itself, add the advisory field to responses_drop on a conversion backend, or set wire_api = "chat" in the CLI. Only advisory fields may be listed; the message still names responses_drop when the refused field is semantic. |
400 unsupported_request_parameter naming tools[custom], tools[local_shell], or a namespace member such as tools[custom] member 'run_command' of namespace 'ns' | A conversion has no Chat Completions, Anthropic, or Gemini shape for that tool, and describing a different calling convention to the model would change its behavior. | Use a native Responses backend, or a CLI and wire that do not send the tool. |
400 unsupported_request_parameter naming tools[<type>] with "not permitted by this router's responses_tools policy" | responses_tools.unmodeled_types defaults to deny, so a tool type the router does not model is refused on every backend until an operator allows it. | Add the type to responses_tools.allow after confirming the backend supports it, or use a client version that does not send it. On a conversion backend, allowing it moves the refusal to the native-only tool check. |
400 unsupported_request_parameter naming a hosted tool such as tools[web_search] from the policy | The operator denied a modeled hosted tool type. | Remove the type from responses_tools.deny, or set modeled_hosted_types: allow. |
422 Unprocessable Entity | A modeled field is malformed, for example a function tool without name, or an invalid tool_choice. | Fix the request. An unknown tool type is not a 422: the router keeps it verbatim on a native backend and refuses it by policy or conversion as described above. |
404 model_not_found | The model in config.toml is not served by any enabled backend. | Set model to an id the router lists on GET /v1/models. |
The responses_tools section is described in Responses Tool Policy; a request refused there reaches no backend.
What does not work today¶
- A conversion carries the Responses-only fields only when the backend opts in.
text,safety_identifier,conversation,max_tool_calls,reasoning.mode, andreasoning.contextreach a native backend only. The advisory fields (include,prompt_cache_key,safety_identifier, andreasoning.summary) are dropped only when the selected backend lists them inresponses_drop; otherwise they are refused. The router never emulates them on Chat Completions, Anthropic, or Gemini backends. - The CLI's default body cannot use a converted backend without
responses_drop. Codex sendsprompt_cache_key,include, andreasoning.summaryon the Responses wire by default, so a conversion refuses the first turn until the backend lists all three. The Chat Completions wire remains available for backends that should not accept the drop. client_metadatais not modeled. Codex attaches it to each request. A native backend receives it unchanged; a conversion ignores it.- Unmodeled tool types are denied by default. A future Codex tool type is refused by the
responses_toolspolicy until an operator allows it, even on a backend that would have forwarded it. - Hosted tools depend on the backend. The CLI's
web_searchtool reaches a native backend unchanged, and the backend decides whether to run it. A conversion has no hosted execution for it: the Chat Completions conversion describes it as a function tool namedweb_search, and the Anthropic conversion drops it. On a converted backend the CLI's default body meets the native-only field refusal first unlessresponses_droplists those fields. - The ChatGPT subscription upstream is not a backend for the CLI. Logging the router in to a ChatGPT subscription does not make that subscription serve the CLI directly; the two directions are configured separately.
See also¶
- Native Responses API: the
responses_apifield and the pass-through contract. - Responses API: the router's
/v1/responsessurface. - ChatGPT subscription / Codex headless login: the opposite direction, where Codex is the upstream.
- Integrating with Claude Code: the same client-to-router pattern for an Anthropic Messages client.