A lightweight local proxy that lets OpenAI Codex (CLI and desktop app) talk to any OpenAI-compatible /v1/chat/completions provider — including those that do not (yet) implement the newer /v1/responses API.
Codex 0.147+ requires the Responses API (wire_api = "responses") and no longer accepts wire_api = "chat". Many third-party gateways, aggregators, and self-hosted backends only expose /v1/chat/completions. This proxy bridges that gap by translating between the two formats on the fly, with correct streaming semantics.
No dependencies. No frameworks. A single Node.js file using only the standard library.
If you point Codex at a provider that only supports /v1/chat/completions, Codex 0.147+ fails because it insists on /v1/responses. Some local routers try to translate between the two formats themselves, but their translation is often buggy: they emit output_item.done events too early (after the first token), so Codex stops reading and you get a one-token reply.
codex-proxy does the translation itself, correctly:
- All
output_text.deltaevents are sent first, as tokens arrive. output_item.doneis sent once, at the very end, with the full accumulated text.
The result: complete, untruncated streaming responses in Codex.
┌─────────┐ POST /v1/responses ┌──────────────┐ POST /v1/chat/completions ┌──────────────┐
│ Codex │ ─────────────────────▶ │ codex-proxy │ ───────────────────────────▶ │ Provider │
│ (CLI / │ ◀───────────────────── │ (local) │ ◀─────────────────────────── │ (chat-only) │
│ GUI) │ responses SSE stream │ port 11434 │ chat SSE stream └──────────────┘
└─────────┘ └──────────────┘
- Codex sends Responses API requests to the proxy.
- The proxy converts them to Chat Completions requests and forwards them to your provider.
- The provider's Chat Completions stream is translated back into a correct Responses SSE stream.
- Codex reads the stream and renders the full response.
Both Codex CLI and Codex Desktop read the same ~/.codex/config.toml, so configuring the proxy once fixes both.
- Node.js v18+ (uses global
fetch, added in Node 18). Node 20 LTS or newer recommended. - An OpenAI-compatible provider that serves
POST /v1/chat/completions(streaming supported). - OpenAI Codex CLI installed (
npm i -g @openai/codex) and/or the Codex desktop app.
git clone https://github.com/priaculun/codex-proxy.git
cd codex-proxyCopy the example config and fill in your provider details:
cp config.example.json config.jsonEdit config.json:
{
"base_url": "https://api.your-provider.com/v1",
"api_key": "sk-your-key-here",
"model": "your-model-name",
"model_vision": "your-vision-model-name",
"extra": {},
"extra_headers": {}
}| Field | Description | Required |
|---|---|---|
base_url |
Your provider base URL. With or without a trailing /v1 — both work. |
Yes |
api_key |
API key sent as Authorization: Bearer <key>. |
Yes |
model |
Default model name (text). Used when the request has no images. | No |
model_vision |
Vision-capable model name. Auto-selected when a request contains image input. | No |
extra |
Extra JSON fields merged into the request body (e.g. {"temperature": 0.7}). |
No |
extra_headers |
Extra headers added to upstream requests. | No |
Auto model swap: When a request contains image parts (
input_image), the proxy automatically forwards tomodel_visioninstead ofmodel. Use a cheap text model for normal chat, vision model only when images present — same endpoint + key. Leavemodel_visionempty to disable (image requests usemodelas-is).
config.jsonis listed in.gitignoreand will never be committed. Keep your keys there.
Run manually:
node proxy.jsThe proxy listens on http://127.0.0.1:11434.
Or run as a background service (macOS, auto-starts on login):
cp com.baim.codex-proxy.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.baim.codex-proxy.plistUse the included setup script (recommended):
./setup.shThis script backs up your current Codex config and rewrites it to use codex-proxy. See Setup script below for details.
To undo, run:
./restore.shcodex exec --skip-git-repo-check "Say hello"Both codex CLI and the Codex desktop app will now route through codex-proxy.
If you prefer to edit Codex config by hand, here is exactly what to change.
Codex config lives at ~/.codex/config.toml.
-
Set the active model and provider (top of the file):
model = "your-model-name" model_provider = "codex-proxy"
-
Add the
codex-proxyprovider block (anywhere in the file):[model_providers.codex-proxy] name = "Codex Proxy" base_url = "http://127.0.0.1:11434/v1" wire_api = "responses" env_key = "CODEX_PROXY_KEY"
base_url— the proxy address (defaulthttp://127.0.0.1:11434/v1).wire_api— must be"responses". The whole point of the proxy is to speak Responses to Codex.env_key— Codex requires an auth env var. The real key lives in the proxy'sconfig.json; Codex just needs any non-empty value. SetCODEX_PROXY_KEY=dummy.
-
Set the env var so Codex sees a non-empty token:
echo 'export CODEX_PROXY_KEY=dummy' >> ~/.zshrc source ~/.zshrbarc
(Use
~/.bashrcif you use bash.)
- Leave any existing
[model_providers.*]blocks (like your previous provider) in place — they do no harm and make rollback easy. Thesetup.shscript keeps them automatically. - Do not set
wire_api = "chat"on thecodex-proxyprovider. Codex 0.147+ rejects it.
setup.sh automates the manual steps above. It is idempotent and safe to run repeatedly.
What it does:
- Verifies the proxy is running (hits
/health). Exits early if not. - Backs up your current
~/.codex/config.tomlto~/.codex/config.toml.bak.<timestamp>. - Reads
modelandbase_urlfromcodex-proxy/config.jsonso Codex uses the same model you configured. - Sets
modelandmodel_provider = "codex-proxy"at the top ofconfig.toml. - Adds (or replaces) the
[model_providers.codex-proxy]block. - Adds
export CODEX_PROXY_KEY=dummyto your shell profile (zsh or bash, if missing).
Usage:
./setup.shYou can also override the detected model/provider name:
./setup.sh --model my-model --provider my-proxy-namerestore.sh reverts the changes made by setup.sh.
What it does:
- Restores the most recent
~/.codex/config.toml.bak.<timestamp>(or a specific backup if given). - Removes the
export CODEX_PROXY_KEY=dummyline from your shell profile. - Removes the
[model_providers.codex-proxy]block if it still exists.
Usage:
# restore the latest backup
./restore.sh
# restore a specific backup
./restore.sh ~/.codex/config.toml.bak.1234567890| Method | Path | Description |
|---|---|---|
POST |
/v1/responses |
Translates to upstream /v1/chat/completions. |
POST |
/v1/chat/completions |
Pass-through (no translation). |
GET |
/health |
Health check. Returns {"status":"ok"}. |
# 1. proxy healthy?
curl http://127.0.0.1:11434/health
# 2. responses stream translates correctly?
curl -N -X POST http://127.0.0.1:11434/v1/responses \
-H "Content-Type: application/json" \
-d '{"model":"your-model-name","input":"Say hi","stream":true}'You should see a sequence of event: response.output_text.delta lines, followed by a single event: response.output_item.done, then event: response.completed. The deltas should contain the full text.
Edit codex-proxy/config.json and change base_url, api_key, and model. Then restart the proxy:
# if running manually
# (stop with Ctrl-C, then)
node proxy.js
# if running as a launchd service
launchctl unload ~/Library/LaunchAgents/com.baim.codex-proxy.plist
launchctl load ~/Library/LaunchAgents/com.baim.codex-proxy.plistIf the new model name differs, re-run ./setup.sh to update Codex config, or edit ~/.codex/config.toml manually.
Make sure Codex is actually pointing at the proxy and not at an old provider:
grep model_provider ~/.codex/config.tomlIt should say model_provider = "codex-proxy". If not, run ./setup.sh again.
The proxy process likely died. Restart it:
node proxy.js
# or
launchctl load ~/Library/LaunchAgents/com.baim.codex-proxy.plistCheck the log:
tail -f /tmp/codex-proxy.logYour config.json api_key is wrong or expired. Fix it and restart the proxy.
You have an old provider block with wire_api = "chat". Either remove that block or change it to wire_api = "responses". The codex-proxy block must use "responses".
Set a different port:
PORT=11435 node proxy.jsThen update base_url in ~/.codex/config.toml to match.
Does this work with the Codex desktop app, or only the CLI?
Both. The desktop app and the CLI read the same ~/.codex/config.toml, so configuring the proxy once fixes both.
Does the proxy store my API key?
Only in config.json on your local machine, which is gitignored. It is never logged, never sent anywhere except to your configured upstream provider as a Bearer token.
Can I use multiple providers at once?
Not with a single proxy instance. The proxy reads one config.json. To use multiple providers, run multiple proxy instances on different ports with different config files, and create separate [model_providers.*] blocks in Codex config. Then switch by changing model_provider.
Is there Windows / Linux support?
The proxy itself (proxy.js) is cross-platform Node.js. The setup.sh / restore.sh scripts and the launchd plist are macOS/Linux-oriented. On Windows, run node proxy.js manually and edit ~/.codex/config.toml by hand following the manual configuration section.
Why port 11434?
It is the default Ollama port, which many people already associate with local LLM tooling. Change it via the PORT env var if it conflicts.
For those who hit the same issue with other routers: the root cause was premature done events. A correct Responses API stream must order events like this:
response.output_item.added (message item)
response.content_part.added
response.output_text.delta "Hel"
response.output_text.delta "lo"
response.output_text.delta "!"
response.output_text.done "Hello!" ← done fires ONCE, at the end
response.content_part.done
response.output_item.done
response.completed
Buggy routers fire response.output_text.done and response.output_item.done after the first delta, then continue sending more deltas for an item Codex already considers closed. Codex reads output_item.done, treats the message as complete, and discards the rest.
codex-proxy accumulates all deltas and fires done exactly once at the end, with the full text.
MIT