An OpenAI-compatible /v1/chat/completions endpoint backed by a stateful
codex app-server subprocess. When the model emits a tool call, codex-proxy
intercepts it and returns it to the calling harness as an OpenAI
tool_calls response instead of letting codex execute it. The harness runs the
tool and sends the result back on its next request, which resumes the same codex
turn.
This is a subprocess manager, not a token-forwarding proxy: every upstream
request is made by the genuine codex binary using your existing codex
configuration and auth — a ChatGPT login, an OpenAI or Azure API key, Bedrock,
or any provider your ~/.codex/config.toml already sets up. It is single-user
by design.
Note on system-prompt replacement: codex's base prompt is only pinned by the ChatGPT subscription backend. On an API-key/Azure provider that restriction does not apply, so
baseInstructionsreplacement works there. Dynamic tool interception rides on the Responses API; if your provider is configured for a non-Responses wire format, verify tool registration surfaces. (Verified working against an Azure.../openai/v1/responsesdeployment.)
Model selection. codex-proxy honors the
modela client requests, so a harness can switch per request between whatever model/deployment names your provider exposes. Switching mid-conversation re-sends the override on the next turn. When the client omits a model — or sends the placeholdercodex/default— codex uses the model in your~/.codex/config.toml. codex-proxy never injects a model of its own, so it cannot 404 on a deployment name you didn't ask for.CODEX_PROXY_PIN_MODEL=1ignores the client entirely and always usesconfig.toml. Note that a requested model must be a real deployment/model your provider knows, or the upstream request will 404 — exactly as the codex CLI behaves.
See ARCHITECTURE.md for the design and the reasoning
behind the statefulness model.
harness ──POST /v1/chat/completions──▶ codex-proxy ──JSON-RPC/stdio──▶ codex app-server ──▶ ChatGPT
▲ │
└──────── tool_calls / result ───────────┘ (dynamic tools; codex never runs them)
- Your harness's
toolsare registered with codex as dynamic tools (experimental app-server API). A model tool call arrives as anitem/tool/callJSON-RPC request, which we hold open while surfacing it to the harness. The blocked request is the mid-turn state — it keeps the codex turn (and its reasoning-item chain) alive across the harness's execute step. - Between turns, state is codex's rollout on disk. codex-proxy fingerprints
each incoming transcript with a rolling hash chain, matches it to a thread,
and resumes cold threads. When a harness mutates history (compaction, edits),
it falls back to a lossy rebuild via
thread/inject_items. - codex's native shell/patch tools are neutralized (
sandbox: read-only,approvalPolicy: never, web search off) so the harness's tools are the only live ones. The harness system prompt is layered in asdeveloperInstructions(codex's base prompt is pinned by the server and cannot be replaced).
- Node.js >= 20
codexCLI (tested againstcodex-cli 0.146.1) already working — i.e.codexruns against whatever provider you've configured. codex-proxy uses that same config; it adds no auth of its own. (codex loginonly if you intend to use a ChatGPT account and haven't logged in.)
npm install
npm run build
node dist/index.js # listens on http://127.0.0.1:11440/v1Point any OpenAI-compatible harness at it:
curl http://127.0.0.1:11440/v1/chat/completions \
-H 'content-type: application/json' \
-d '{"model":"gpt-5.1-codex","messages":[{"role":"user","content":"hi"}]}'api_key is ignored (auth is codex's own login). Both streaming (SSE) and
non-streaming responses are supported, including tools, parallel tool calls,
response_format (json_schema → codex outputSchema), and
stream_options.include_usage. The base_instructions and
capability_preamble fields (sent via OpenAI/LiteLLM extra_body) override the
corresponding env defaults per request.
| Var | Default | Meaning |
|---|---|---|
CODEX_PROXY_HOST |
127.0.0.1 |
Bind host (keep local). |
CODEX_PROXY_PORT |
11440 |
Bind port. |
CODEX_PROXY_MODEL |
codex |
Placeholder id advertised by /v1/models. Treated as "unspecified" so echoing it does not force a model. |
CODEX_PROXY_PIN_MODEL |
(unset) | Unset = honor the client's model. 1/true/config = ignore it and use codex's config.toml model. Any other value = pin to that model/deployment name (run two instances to give grader and agent different pinned models). |
CODEX_PROXY_BASE_INSTRUCTIONS |
(unset) | Unset = keep codex's pinned base prompt. Empty string zeroes it out (API-key/Azure only). Any other string replaces it. |
CODEX_PROXY_CAPABILITY_PREAMBLE |
(unset) | 1/true prepends a built-in note to the system prompt clarifying the harness prompt is authoritative over tools/purpose and the agent is not limited to coding. Any other string is used verbatim. |
CODEX_PROXY_CODEX_BIN |
codex |
Path to the codex binary. |
CODEX_PROXY_REASONING |
0 |
1 streams reasoning summaries as delta.reasoning_content. |
CODEX_PROXY_TOOL_TIMEOUT_MS |
600000 |
How long a turn waits for a tool result before failing cleanly. |
CODEX_PROXY_TOOL_BATCH_MS |
40 |
Window for coalescing parallel tool calls. |
CODEX_PROXY_DATA_DIR |
~/.codex-proxy |
Session index + scratch working dirs. |
CODEX_PROXY_MAX_SESSIONS |
200 |
In-memory session LRU cap. |
CODEX_PROXY_LOG |
info |
debug/info/warn/error (to stderr). |
npm test # unit + e2e against a fake app-server (no auth needed)
npm run smoke # live check against real codex (requires codex login)The e2e suite drives the full stack against test/fake-appserver.mjs, which
speaks the same JSON-RPC subset codex does, so the session-matching, tool
interception, resume, and lossy-rebuild paths are all covered without hitting
the network.
- Changing tool sets: a harness may add or remove tools mid-conversation (e.g. a dynamic toolbelt). Because codex fixes dynamic tools at thread start, codex-proxy detects the change and rebuilds the thread transparently — replaying the full transcript as rollout history (preserving tool calls and results) and continuing. Session identity is keyed on the transcript, not the tool set, so the conversation is never lost.
- Images: image content is passed through as codex image items rather than
flattened to text — in user turn input, in injected history, and in tool
results. For non-Anthropic models, LiteLLM splits a tool's image out into a
separate user message after the tool result; codex-proxy folds that deferred
image back into the tool output as an
inputImageitem.
- codex's base prompt is always present (responses are codex-flavored); this is not a general-inference endpoint. On the ChatGPT subscription backend it also cannot be replaced; API-key/Azure providers allow replacement.
- After a harness mutates earlier history, the affected rebuild drops reasoning-item continuity for prior turns.
logprobs,n > 1, and exact non-codexusageaccounting are unsupported.- Single account only; do not expose beyond localhost.