Inference APIs
Blog/codex, chat

What Codex CLI actually sends to a third-party model provider

We captured every request Codex CLI 0.154.0 makes when pointed at a custom provider: a 17,000-character system prompt on every turn, 17,500 characters of tool definitions, a stable installation id, and two request shapes that only OpenAI understands. Here is what is in them, why MCP tools and sub-agents break on open models, and what a provider can do about it.

Taylor Hawkes · September 17, 2026

Codex CLI lets you point it at any provider that speaks OpenAI's Responses API. We run one of those providers, and when people reported that MCP tools went silent on open models, we put a logging proxy between Codex and our endpoint and read what actually goes over the wire. This is a description of the requests, with numbers from codex-cli 0.154.0. The captured data is from our own test sessions.

Every turn is a full resend

Codex is stateless towards the provider. It sends store: false, never uses previous_response_id, and puts the whole conversation into input on every turn. For a one-line task with no history, the first request was 40,802 characters of JSON and was billed at 8,775 input tokens. The user's actual request was 309 characters.

Where the rest goes:

  • 16,979 characters of system prompt, in the instructions field. It opens "You are a coding agent running in the Codex CLI" and covers tool use, planning, sandboxing, patch format and response style. It is identical on every turn of every session.
  • 17,544 characters of tool definitions. The table below.
  • A developer message of about 2,900 characters listing the skills available on the machine.
  • A user message named environment_context with the working directory, shell, date, timezone and workspace roots.
  • Your message.

The practical consequence is that prompt caching decides what Codex costs. After the first turn, the 17,000-character prompt and the tool block are cache hits on any provider that supports them. On ours the repeated part bills at the cached-input rate, which is 80 to 98 percent below the normal input price depending on the model, and a small session with three tool calls came to under a cent on DeepSeek V4 Flash. Without caching, every turn pays for the full 8,000 tokens again.

The tools

ToolTypeSize
exec_command, write_stdinfunction2,454 chars
request_user_input, view_imagefunction1,816 chars
get_goal, create_goal, update_goalfunction3,045 chars
list_mcp_resources, list_mcp_resource_templates, read_mcp_resourcefunctionpresent when any MCP server is configured
multi_agent_v1: spawn_agent, wait_agent, send_input, resume_agent, close_agentnamespace10,178 chars
mcp__<server>: your MCP server's toolsnamespaceone per server
web_searchweb_search51 chars

Two things in that table are not part of the Responses API as documented. web_search is OpenAI's hosted search tool; a third-party provider cannot run it and has to drop it. And two entries are of type namespace.

The namespace wrapper, and why MCP tools vanish

Codex does not send an MCP server's tools as functions. It sends the whole server as one tool of type namespace with the functions nested inside, and it does the same for its own sub-agent tools:

One of the tools in every request
{
  "type": "namespace",
  "name": "mcp__orders",
  "tools": [
    { "type": "function", "name": "lookup_order", "description": "...", "parameters": { "...": "..." } },
    { "type": "function", "name": "add_note",     "description": "...", "parameters": { "...": "..." } }
  ]
}

OpenAI's backend expands this. Everyone else's does one of three things: rejects the request, silently drops the entry, or hands the model one uncallable tool named mcp__orders. In every case the model never learns the tools exist. When it does guess a flat name, Codex's router refuses it with unsupported call, because Codex routes MCP calls by a separate namespace field on the call:

What Codex needs back for that tool
{ "type": "function_call", "namespace": "mcp__orders", "name": "lookup_order", "call_id": "call_abc", "arguments": "{\"order_id\":\"A-1042\"}" }

The failure can hide well. In our first capture we gave Codex an MCP server holding order data the model could not know. The model never called the MCP tool. Instead it found the server's Python script on disk, wrote a shell pipeline that fed JSON-RPC into it by hand, and answered correctly. The MCP server's log showed zero tool calls. Anyone judging by the answer would have said MCP worked.

The sub-agent tools go the same way. On a provider that drops namespaces, spawn_agent does not exist, so Codex's own multi-agent feature is unavailable on any non-OpenAI model, and nothing tells you why.

This is openai/codex #26234. The workarounds people have built are a patched Codex and a local proxy that flattens the tools on the way out and restores the namespace on the way back.

agent_message: the sub-agent hand-off

With Multi-Agent V2 enabled, a parent hands its child a task as an input item of type agent_message:

How a sub-agent receives its task under Multi-Agent V2
{
  "type": "agent_message",
  "author": "/root",
  "recipient": "/root/sha_digest",
  "content": [
    { "type": "input_text", "text": "Message Type: NEW_TASK\nTask name: /root/sha_digest\nSender: /root\nPayload:\n" },
    { "type": "encrypted_content", "encrypted_content": "Run a shell command to compute the SHA-256 hex digest of ..." }
  ]
}

The block is called encrypted_content, and when the parent is an OpenAI model it is: Fernet ciphertext beginning gAAAAA, readable only by OpenAI. When the parent is any other model, the same field holds the task in plain text. Either way the item type is unknown to other Responses implementations. Some return 422 ("data did not match any variant of untagged enum ModelInput"); others accept the request and drop the item, so the child starts with a header that ends in Payload: and nothing after it, and reports that it was given no task. That is openai/codex #33551.

Metadata that goes to whoever you point Codex at

Every request carries a client_metadata object and a prompt_cache_key:

client_metadata, sent with every request
{
  "session_id": "01a0b1a5-37a6-...",
  "x-codex-installation-id": "4ec1f87e-3474-4738-b6fe-73b4ed1f7ed5",
  "turn_id": "01a0b1a5-37af-...",
  "x-codex-turn-metadata": "{\"installation_id\":\"4ec1f87e-...\",\"thread_id\":\"...\",\"agent_name\":\"/root\",\"sandbox_mode\":\"danger-full-access\",\"turn_started_at_unix_ms\":1789686790064, ...}"
}

The installation id is stable across sessions. It is there so OpenAI's backend can group requests, and it is sent unchanged to any provider you configure, along with your sandbox mode, session and turn ids, and timestamps. The environment_context message adds your working directory path and timezone. None of this is secret, but if you assumed a third-party provider sees only your prompt and your code, it sees a little more than that. Ours stores none of it after the response; other providers' policies vary, and this is a reasonable thing to ask them.

Two smaller items: Codex asks for include: ["reasoning.encrypted_content"], which providers without OpenAI's reasoning encryption can ignore, and sends reasoning: {"summary": "auto"} with no effort level unless you set model_reasoning_effort in the config.

What a provider can do about it

The two breakages above are translation problems, and translation can live on the provider's side. We did that on our Responses endpoint:

  • Each namespace is expanded into flat function tools named namespace__tool. Names over the 64-character limit are shortened and mapped back.
  • Namespaced calls that Codex replays in the history are flattened the same way, so a multi-step session stays consistent.
  • Every call comes back with namespace and name restored, in the streamed output_item events as well as the final response.
  • Models often call the tool by its bare name, and GPT-OSS writes collaboration.spawn_agent; both are mapped back when unambiguous.
  • agent_message items become user messages made of the header and the task text. Real ciphertext cannot be read by anyone but OpenAI, so in that case the model is told the task was unreadable and the response carries a header saying so.

Tested with real Codex runs. MCP: a local server with data the model cannot know, pass meaning the server logged the calls and the answer was right. Nine of ten chat models passed; Llama 3.3 70B made the call and then stopped, which is the model rather than the wire. Sub-agents: the parent has to delegate a hash of a random string, pass meaning the parent ran no shell, a child was spawned and the right digest came back. Eight models passed on both V1 and V2; GPT-OSS 120B passes V1 and is unreliable on V2 because it sometimes calls spawn_agent from inside the child, where it is not offered. The per-model tables and the captured shapes are on the two reference pages: MCP tools on a custom provider and sub-agents and agent_message.

None of this replaces a fix in Codex. If Codex starts sending flat tools and plain messages to non-OpenAI providers, there is nothing left to translate and requests pass through unchanged. Until then, the wrapper is something every provider that wants Codex to work has to handle, and most do not know it is there.

How the capture was done

A 40-line Python HTTP proxy on localhost that writes each request body to disk, forwards it upstream, and streams the response back while also writing it to disk. Codex's base_url pointed at the proxy. The MCP server was a 30-line stdio server with two tools. The scripts are not clever, and we will share them on request.

More posts