Skip to content

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:

  • openai and azure: 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. Add responses_api: native when the backend implements the Responses API itself.
  • anthropic, bedrock, and gemini: no Responses endpoint to pass through to, so responses_api: native is 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:

export CONTINUUM_ROUTER_API_KEY="your-router-key"
codex exec "say ok"

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, and reasoning.context reach a native backend only. The advisory fields (include, prompt_cache_key, safety_identifier, and reasoning.summary) are dropped only when the selected backend lists them in responses_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 sends prompt_cache_key, include, and reasoning.summary on 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_metadata is 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_tools policy until an operator allows it, even on a backend that would have forwarded it.
  • Hosted tools depend on the backend. The CLI's web_search tool 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 named web_search, and the Anthropic conversion drops it. On a converted backend the CLI's default body meets the native-only field refusal first unless responses_drop lists 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