Use your own OpenCode Go and OpenCode Zen credentials in Codex through a local protocol adapter.
Codex expects a Responses API (/v1/responses). OpenCode Go's documented
models span Responses, Chat Completions, and Anthropic Messages, while OpenCode
Zen also includes Gemini-family routes. This proxy selects the documented wire
protocol for each model in one local process:
Codex app
│
│ POST /v1/responses (Responses API)
▼
opencode-go-proxy ←── localhost:8787, one runtime dep (zstandard)
│
│ POST per family: chat/completions · responses · messages · models/<id>
▼
OpenCode Go / OpenCode Zen ── 28 documented Go models · optional Zen models
OpenCode Go Proxy is an independent, single-user local adapter. It is not an OpenAI or OpenCode product, and neither company sponsors or endorses it. It does not provide shared accounts, pooled credentials, subscription resale, or mechanisms to evade provider limits or safeguards.
The proxy uses Codex's documented openai_base_url, custom provider, and model
catalog configuration surfaces. When native OpenAI models are enabled, it
relays the user's existing Codex authorization only to the configured native
OpenAI endpoint over HTTPS; it never stores that authorization or uses it as
an OpenCode credential. OpenCode credentials are likewise never sent to the
native OpenAI endpoint.
Users are responsible for using their own accounts and following the current OpenAI Terms of Use, OpenAI Services Agreement, OpenCode terms, and any model-provider policies that apply to them. See Codex advanced configuration and SECURITY.md. This project documents technical safeguards; it does not provide legal or policy certification.
OpenCode Go is a $10/month subscription for coding-agent access to a changing, provider-curated model set. Codex speaks the Responses API, while Go models currently require three different upstream protocols. This proxy provides that adapter without replacing Codex's native OpenAI authorization.
# Configure Codex (writes one marker-delimited block to ~/.codex/config.toml)
uvx --from git+https://github.com/kartikkabadi/opencode-go-proxy \
opencode-go-proxy config enable
# Run the loopback-only adapter
uvx --from git+https://github.com/kartikkabadi/opencode-go-proxy \
opencode-go-proxy \
--bind 127.0.0.1 \
--port 8787
# Fully restart Codex, then select an explicit Go model
codex -m opencode-go/muse-spark-1.3-contributorInstall the official Codex CLI using OpenAI's current Codex CLI instructions. The standalone macOS/Linux installer is:
curl -fsSL https://chatgpt.com/codex/install.sh | shThen run codex app to open the installed Codex desktop app or start OpenAI's
official desktop installer. Run opencode-go-proxy config enable once, start
the loopback proxy, fully quit and reopen Codex, and select an
opencode-go/<model-id> entry.
The config command edits only its marker-delimited block in Codex's existing
configuration. It preserves unrelated settings, native ChatGPT authorization,
sessions, and desktop state. Run opencode-go-proxy config disable to remove
only that managed block.
The checked-in catalog supports every model ID documented for OpenCode Go.
Use the explicit opencode-go/<model-id> form in Codex; it avoids ambiguity
when a Go model and a native OpenAI model have the same ID.
| Upstream protocol | Documented model IDs |
|---|---|
Responses /responses |
grok-4.6, gpt-5.6-luna, muse-spark-1.3-contributor, muse-spark-1.2-contributor |
Chat Completions /chat/completions |
glm-5.3-flash, glm-5.3, glm-5.2, glm-5.1, kimi-k3, kimi-k2.7-code, kimi-k2.6, longcat-2.0, deepseek-v4.1-flash, deepseek-v4-pro, deepseek-v4-flash, deepseek-v4-flash-vision-exp, mimo-v2.5, mimo-v2.5-pro, hy4-preview, hy3 |
Anthropic Messages /messages |
minimax-m3, minimax-m2.7, minimax-m2.5, qwen3.8-max, qwen3.8-flash, qwen3.7-max, qwen3.7-plus, qwen3.6-plus |
The live /models response can change. Runtime refresh accepts only documented
or protocol-certified IDs, so an unknown ID is not guessed into the wrong
protocol. The checked-in seed keeps startup and model selection working
offline.
The one listener accepts all three documented client formats, with or without
the /v1 prefix:
| Client request | Supported Go models | Upstream behavior |
|---|---|---|
POST /v1/responses |
all 28 documented models | translated to each model's certified Responses, Chat Completions, or Messages family |
POST /v1/chat/completions |
the 16 Chat Completions models | relayed in Chat Completions format |
POST /v1/messages |
the 8 Anthropic Messages models | relayed in Messages format |
Streaming and non-streaming requests are supported on every row. The Responses
aliases /responses/compact and /v1/responses/compact are also available for
compaction. A model sent to an incompatible direct-format endpoint is rejected
before any upstream request rather than silently translated to a different
client response shape.
codex -m opencode-go/deepseek-v4-pro
codex -m opencode-go/glm-5.2
codex -m opencode-go/minimax-m3The proxy routes the exact model Codex sends:
opencode-go/<id>explicitly selects a known OpenCode Go catalog entry.zen/<id>explicitly selects a known OpenCode Zen catalog entry.- A bare ID in the captured native catalog routes to the native OpenAI endpoint.
- A known, non-colliding bare Go ID remains a compatibility alias; a known Zen-only bare ID routes to Zen.
- An omitted model uses
deepseek-v4-flash. An unknown or malformed model is rejected before any upstream request; the proxy never silently substitutes another model.
When images are present in a turn with tools, the proxy captions the latest image
(older ones are stubbed) and routes the main turn to your configured model. Image
turns without tools stay on the requested model when the catalog marks it
image-capable (input_modalities contains image); a text-only requested model
falls back to the image default. If the upstream still rejects the image payload
at runtime (400/404/415/422), the proxy captions the images and retries the
requested model once before failing the turn. The caption
engine auto-picks the cheapest image-capable model from the catalog (input_modalities
contains image) with mimo-v2.5 as the fallback, or a local vision runtime (Ollama,
llama.cpp server, LM Studio) that answers a read-only probe. Captions are cached by
image bytes for an hour, sent with detail: low to cut upstream vision cost, and every
non-cached read is metered with kind=vision; the caption budget is 30s with no retries,
so a failed caption degrades to a placeholder instead of stalling the turn. Override the
engine with CODEX_IMAGE_MODEL or OPENCODE_GO_PROXY_CAPTION_MODEL (a model slug,
local, or default auto = catalog pick); configure a local runtime with
OPENCODE_GO_PROXY_VISION_LOCAL_BASE_URL / OPENCODE_GO_PROXY_VISION_LOCAL_MODEL.
Disable detail: low with OPENCODE_GO_PROXY_CAPTION_DETAIL=none (a rejected detail
value also falls back to a sips-downscaled image with no detail).
Since 0.4.0 the proxy also serves OpenCode Zen, the
pay-as-you-go gateway with GPT, Claude, Gemini, Grok, DeepSeek, GLM, Kimi, and Qwen
models. Zen models are auto-discovered from https://opencode.ai/zen/v1/models (no
auth), merged with models.dev metadata, and appear in the catalog and /v1/models
as zen/<id> slugs (e.g. zen/claude-sonnet-4-5). Select one in the model
picker or run codex -m zen/claude-sonnet-4-5.
Requests route by model family, translated from the Responses API the proxy always speaks to the surface the gateway expects:
| Model ids | Upstream surface | Auth header |
|---|---|---|
claude-*, qwen* |
/zen/v1/messages (Anthropic Messages) |
x-api-key |
gemini-* |
/zen/v1/models/<id> (Gemini API) |
x-goog-api-key |
gpt-*, grok-* |
/zen/v1/responses (Responses API, verbatim) |
Authorization: Bearer |
| everything else | /zen/v1/chat/completions |
Authorization: Bearer |
The proxy resolves the same key as Go — $OPENCODE_GO_API_KEY or the macOS keychain
entry opencode-go-api-key — and maps the auth header per family. Zen turns meter
with provider="zen", so they never count against your Go quota.
Free models need no credits: deepseek-v4-flash-free, mimo-v2.5-free, and
big-pickle are always available.
The menu bar app shows Go usage polled from https://opencode.ai/zen/go/v1/usage
(60s cache) next to the fixed Go plan limits ($60/month, $30/week, $12 per rolling
5 hours), and a Zen rollup of today's turns/tokens and the last 7 days from the
local usage meter.
- Sign in to OpenCode Zen, subscribe to OpenCode Go, and copy your API key. OpenCode currently permits one Go subscriber per workspace.
- Optionally verify the key in OpenCode itself: run
/connect, select OpenCode Go, paste the key, then run/models. - Make the same user-owned key available to this proxy using one of the methods below.
The proxy resolves your OpenCode Go API key in this order:
$OPENCODE_GO_API_KEYenvironment variable- macOS keychain entry
opencode-go-api-key(override withCODEX_KEYCHAIN_SERVICE; macOS only)
# Option 1: env var (works everywhere)
export OPENCODE_GO_API_KEY="your-key-here"
# Option 2: macOS keychain (macOS only)
security add-generic-password -a "$USER" -s opencode-go-api-key -wThe key is used only for Go or Zen upstream requests. It is never substituted for Codex's native OpenAI authorization, and the separate remote caller token is never forwarded upstream.
- OpenCode Go's current limits are dollar-based: $12 per rolling five hours, $30 per week, and $60 per month. Model prices differ, so request counts vary.
- OpenCode may monitor traffic for abuse. The proxy identifies itself as
opencode-go-proxy/<version>and sends a stablex-opencode-sessionvalue; override the user agent only when you can still identify the client truthfully withOPENCODE_GO_PROXY_USER_AGENT. - Muse Spark Contributor models are region-limited and may use prompts or completions for training. They are not zero-data-retention models.
- DeepSeek V4 prices vary during documented peak periods. Vision model image inputs are billed from image dimensions as well as text.
- After Go limits are exhausted, OpenCode's console can optionally use a separate Zen balance. The proxy does not enable that setting or bypass a limit.
- Availability, pricing, retention, and limits are provider policies and may change. This adapter does not alter them; review the current OpenCode Go documentation before use.
lazycodex is a Codex plugin that adds multi-model orchestration, parallel background agents, and LSP/AST-aware tools. It pairs naturally with this proxy — you get OpenCode Go's models as the backend and lazycodex's agent harness on top.
npm install -g lazycodex-aiSee the lazycodex docs for setup.
- OpenCode Zen support: auto-discovered
zen/<id>catalog merged with models.dev metadata, per-family wire translation (Anthropic Messages / Gemini / Responses / chat-completions) with per-family auth header mapping, free models, andprovider="zen"metering - Responses
inputto chatmessagestranslation instructionsanddeveloperroles mapped to system messages- Function tool schema passthrough
- Custom/freeform tool adaptation (Codex
apply_patchworks) - Reasoning content replay across tool-call turns
- Real-time SSE streaming (not synthesized)
- Cached image captioning when tools are present: cheapest catalog vision engine (or a probed local runtime) by default,
detail: lowinput, 30s no-retry budget, MiMo V2.5 fallback, reads metered withkind=vision - SSRF protection on image URLs (
data:image/andhttps://only) - Configurable body cap, peer-address guard, authenticated remote mode, keychain credential resolution
- Local health and model-list endpoints
- Prefix caching: byte-stable request prefixes plus
include_usage, with per-model hit ratio on/cache - Honest usage meter: append-only
usage-events.jsonlin the state dir (truncated or empty responses never count as success) - Upstream retry with bounded exponential backoff on transient failures (429/5xx/network/timeout)
- Ops CLI:
doctor(reference-style checks with--fixfor safe repairs),smoke-test(marker prompt through the local proxy),support-bundle(JSON schema v1, mode 0600),install(points at the macOS menu bar app; no launchd agent),install-skills,refresh-runtime, andstatus - Spawned threads inherit the parent session's model (
create_thread;chatgptWorkCloudtargets are skipped) - Correctness contract: empty upstream completions are retried once (a second empty stream answers an
empty_completionerror), zero-input-token reports are estimated for compaction (OPENCODE_GO_PROXY_ESTIMATE_ZERO_INPUT=0disables), and keepalive comments run until the stream truly ends without interleaving into data frames - Auth transport guard (zero config): the default listener and accepted peers are loopback-only, forged
Host: localhostdoes not admit a remote peer, browser-originated requests answer403, non-JSON POSTs answer415, and OPTIONS preflight stays blocked - Family-aware Go routing for Responses, Chat Completions, and Anthropic Messages, including streaming and non-streaming requests with the family-specific authentication header
- Rate-limit harvesting (plan 011): upstream
x-ratelimit-*andanthropic-ratelimit-*headers are parsed into per-provider quota snapshots, the latest snapshot per provider is kept, andGET /quotaexposesquota-state.json - Menu bar state contract (plan 013):
GET /statereturns one JSON document (status, port, upstream, latest quota snapshot, today's turns/tokens, last-7-day token bars, current model) computed from the meter file and quota state - WebSocket upgrade requests answered with
426 Upgrade Required(desktop app falls back to HTTP streaming) - zstd-compressed request bodies decompressed (
Content-Encoding: zstd; the desktop app sends them) - Single-port guard in the menu bar app: refuses Start when 8787 is already owned
OpenCode Go bills cached reads at a fraction of normal input (Cached Read in the
pricing table), and its usage estimates assume most tokens per request are served from
cache. For that to happen the request prefix must be byte-stable: the same system
prompt, tools, and earlier conversation, in the same order, on every request.
The proxy is built for exactly that:
- The system prompt (
instructions/developerroles) is always the first message. - Conversation history is appended in order; translation is deterministic, so the serialized prefix is identical across requests in a session — and across sessions that share the same system prompt and tools.
- Streaming requests set
stream_options: { "include_usage": true }so the upstream reports its cache accounting (prompt_cache_hit_tokens/cached_tokens) in the stream; without this most OpenAI-compatible endpoints omit usage entirely. - Cache hits are surfaced to Codex in the standard Responses shape
(
usage.input_tokens_details.cached_tokens), so the app shows them in its token display.
Every request with cache accounting is logged (look for "cache": {...} on
response.converted trace lines). The aggregate is also exposed on a metrics
endpoint:
curl http://127.0.0.1:8787/cache{
"models": [
{
"model": "deepseek-v4-flash",
"cache_hit_tokens": 100000,
"cache_miss_tokens": 1000,
"requests": 50,
"hit_ratio": 0.990099
}
],
"totals": { "...": "..." }
}Within a session, every request after the first shares the full previous prefix, so cache-eligible requests typically run at 99%+ hit ratio; only the genuinely new delta misses. The first request of a brand-new context always misses (nothing is cached yet).
When the upstream answers 200 with rate-limit headers, the proxy records the latest
per-provider snapshot (limit / remaining / resetAt / sampledAt) to
quota-state.json in the state dir and exposes it as JSON:
curl http://127.0.0.1:8787/quota{
"providers": {
"openai": {
"provider": "openai",
"limit": 500,
"remaining": 432,
"resetAt": "2026-08-11T06:30:00.000Z",
"sampledAt": "2026-08-11T06:00:00.000Z"
}
}
}A headerless upstream leaves the state empty ("providers": {}); the proxy never
invents quota numbers.
GET /state (or /v1/state) composes the same local files into the single
contract the macOS menu bar reads. quota is the latest provider snapshot by
sampledAt (or null), usage.last7d is always seven entries (oldest first,
including today) so the UI renders a stable bar list, and model is the model of
the most recent meter event, falling back to the default:
curl http://127.0.0.1:8787/state{
"status": "ok",
"port": 8787,
"upstream": "https://opencode.ai/zen/go/v1",
"quota": {
"provider": "openai",
"remaining": 432,
"limit": 500,
"resetAt": "2026-08-11T06:30:00.000Z"
},
"usage": {
"todayTurns": 12,
"todayTokens": 3456,
"last7d": [
{ "date": "2026-08-05", "tokens": 0 },
{ "date": "2026-08-06", "tokens": 1200 },
{ "date": "2026-08-07", "tokens": 900 },
{ "date": "2026-08-08", "tokens": 2100 },
{ "date": "2026-08-09", "tokens": 1800 },
{ "date": "2026-08-10", "tokens": 2700 },
{ "date": "2026-08-11", "tokens": 3456 }
],
"go": {
"rolling": { "...": "..." },
"weekly": { "...": "..." },
"monthly": { "...": "..." }
},
"goLimits": {
"monthlyDollars": 60,
"weeklyDollars": 30,
"rolling5hDollars": 12
},
"zen": {
"todayTurns": 3,
"todayTokens": 812,
"last7d": [0, 0, 0, 0, 1200, 400, 812]
}
},
"model": "deepseek-v4-flash"
}usage.go is the raw response of GET https://opencode.ai/zen/go/v1/usage
(rolling / weekly / monthly windows), TTL-cached for 60s and null when
the fetch fails. usage.goLimits holds the fixed Go dollar budgets the menu bar
renders next to it. usage.zen is the Zen-only rollup — turns and tokens for
today plus a seven-entry daily token list — from the local usage meter; the
legacy todayTurns / todayTokens / last7d keys stay as the all-provider
totals.
Missing or corrupt meter/quota files degrade to zeros or null; the endpoint
always returns this shape.
uvx --from git+https://github.com/kartikkabadi/opencode-go-proxy opencode-go-proxy --helpuv sync
uv run opencode-go-proxy --helpThe supported way to run the proxy on macOS is the menu bar app in
macos/MenuBarApp. Build it in Xcode (or swift build), launch it, and it
spawns the proxy itself and shows status, quota, and today's usage. There is
no launchd agent anymore: contrib/launchd/ was removed in 0.3.0, and the
install ops command points at the menu bar app instead of a plist. The app
is the only supervisor: do not run another proxy service beside it.
Releases are small patch bumps pushed to the same git URL, so updating means re-installing from the newest tag. There is no PyPI and no separate package channel.
The menu bar app checks the proxy version on start and on demand. When a newer release exists it shows an "Update Available" row: click it, confirm, and the app re-installs the proxy from the new tag and restarts it. Use "Check for Updates" to force a fresh check. The menu bar app binary itself is a manual rebuild; only the proxy it runs is auto-updated.
If you installed the proxy as a tool, check and apply updates with the CLI:
opencode-go-proxy update # check for a newer release
opencode-go-proxy update --apply # install the newer releaseupdate prints the installed version and the latest release (exit code 3
when an update is available). --apply re-installs from the new tag with
uv tool install --force; when the proxy was not installed as a tool it
prints the exact one-liner to run instead.
Install the checked-in user service, then start its single proxy process:
mkdir -p ~/.config/systemd/user
curl -fsSL \
https://raw.githubusercontent.com/kartikkabadi/opencode-go-proxy/v0.4.10/contrib/systemd/opencode-go-proxy.service \
-o ~/.config/systemd/user/opencode-go-proxy.service
systemctl --user daemon-reload
systemctl --user enable --now opencode-go-proxy
curl http://127.0.0.1:8787/healthThe unit owns the same one listener used on macOS; do not run a second manual proxy while it is active. For a foreground/manual Linux run, use:
uvx --from git+https://github.com/kartikkabadi/opencode-go-proxy@v0.4.10 \
opencode-go-proxy --bind 127.0.0.1 --port 8787Replace v0.4.10 with the newest tag from
releases.
For updates, change the pinned ExecStart URL in
contrib/systemd/opencode-go-proxy.service to the same pinned form, then
systemctl --user daemon-reload && systemctl --user restart opencode-go-proxy.
Release cadence: one small patch release per change, so updates arrive frequently and each one is small.
All flags have environment variable defaults:
| Flag | Env var | Default |
|---|---|---|
--bind |
OPENCODE_GO_PROXY_BIND |
127.0.0.1 |
--port |
OPENCODE_GO_PROXY_PORT |
8787 |
--chat-base-url |
OPENCODE_GO_BASE_URL then OPENCODE_ZEN_BASE_URL then CHAT_COMPLETIONS_BASE_URL |
https://opencode.ai/zen/go/v1 |
--api-key-env |
OPENCODE_GO_PROXY_API_KEY_ENV |
OPENCODE_GO_API_KEY |
--timeout-sec |
OPENCODE_GO_PROXY_TIMEOUT_SEC |
180 |
--max-body-mb |
OPENCODE_GO_PROXY_MAX_BODY_MB |
20 |
The proxy accepts both /responses and /v1/responses.
The upstream base URL resolves in this order: the --chat-base-url flag, then OPENCODE_GO_BASE_URL, then OPENCODE_ZEN_BASE_URL, then the legacy CHAT_COMPLETIONS_BASE_URL, then the built-in default.
Keep the proxy on loopback whenever possible. A non-loopback bind fails closed unless both remote mode and a separate caller capability are configured:
export OPENCODE_GO_PROXY_ALLOW_REMOTE=1
export OPENCODE_GO_PROXY_CALLER_TOKEN="$(openssl rand -hex 32)"
opencode-go-proxy --bind 0.0.0.0Every non-loopback client must send the token in
X-OpenCode-Go-Proxy-Token. For a Codex custom provider, pass it from the
environment rather than writing the value into config:
[model_providers.opencode-go-remote]
name = "OpenCode Go Remote"
base_url = "https://proxy.example.com/v1"
wire_api = "responses"
env_http_headers = { "X-OpenCode-Go-Proxy-Token" = "OPENCODE_GO_PROXY_CALLER_TOKEN" }Use TLS and network-level access controls for any remote deployment. The caller token is only a proxy access capability; it is never used as or forwarded as a provider credential. Browser-originated requests remain blocked in remote mode.
One service and one HTTP port only. The proxy binds a single listener:
OPENCODE_GO_PROXY_PORT (default 8787). There is no admin port, control
channel, or secondary service. The macOS menu bar app or the Linux systemd user
unit supervises that one process. If something else already listens on the port,
the proxy fails to bind — check with lsof -nP -iTCP:8787 -sTCP:LISTEN before
starting another instance. The menu bar app refuses Start when 8787 is owned.
Short provider name. The long "opencode go/" label in the Codex model picker comes
from the provider config in ~/.codex/config.toml, not from this proxy. Shorten it by
editing the provider name:
[model_providers.opencode-go]
name = "Go" # shows as "Go" instead of "opencode go/"The reference catalog's per-model display_name values are already short
("DeepSeek V4 Flash", "Kimi K2.7 Code", etc).
All commands run as subcommands of the console script, for example opencode-go-proxy doctor --json.
doctor [--json] [--fix]- runs the check set: API key resolution (env or keychain), config.toml presence and proxy pointer, catalog readability, port free/owned, service health, meter writability, log writability, and best-effort upstream reachability.--fixrepairs only what is safe without writing config.toml (log/meter directories, catalog render). Config writes are handled by the config-manager step and stay approval-gated.config enable|disable|status [--json]- owns one marker-commented block in~/.codex/config.toml(# BEGIN opencode-go-proxy-managedto# END opencode-go-proxy-managed): enable writesopenai_base_urlandmodel_catalog_jsonand never replaces user-owned values, disable removes only the managed block (and deletes the file when the block was its only content), and Codex Voice realtime keys are preserved on native endpoints unless you set them yourself. Point it at another file for testing withOPENCODE_GO_PROXY_CONFIG_PATH.smoke-test [--base-url URL]- posts one marker prompt to the local proxy athttp://127.0.0.1:8787/v1/responsesand asserts the marker comes back. Point it at an isolated scratch proxy with--base-urlorOPENCODE_GO_PROXY_BASE_URL.support-bundle [--output PATH]- writes the JSON schema v1 diagnostic bundle (version, generatedAt, env summary, redacted config snapshot, meter tail, log tail, catalog model count, doctor checks) to a mode-600 file. Secret-shaped values are redacted.install- prints how to run the macOS menu bar app (there is no launchd agent in 0.3.0).install-skills- copies the checked-inopencode-go-proxyskill into~/.codex/skills/.refresh-runtime [--force]- re-runs native model capture and re-renders the merged catalog; the menu bar's Refresh Catalog calls this.status [--json]- reports whether the proxy is running, who owns the port, and where logs live.
The proxy serves /v1/models from a runtime catalog in the state dir
(OPENCODE_GO_PROXY_STATE_DIR, default ~/.codex/opencode-go-proxy/). At startup it renders
the state-dir compact catalog (or the checked-in seed at contrib/opencode-go-models.json)
immediately, then refreshes in the background: the authenticated OpenCode Go
/models endpoint confirms currently available protocol-certified IDs,
TTL-gated so a fresh catalog never hits the network, and the full catalog is
written to opencode-go-catalog.json under the state dir. Runtime refresh never
writes the repo's contrib/ files; maintain the checked-in seed with
opencode-go-proxy --refresh-catalog.
Rendered models follow the exact key set Codex reads in codex-router's merged-models.json:
multi_agent_version lives at the model top level, comp_hash/availability_nux/tool_mode
are present, and model_messages carries approvals, collaboration_modes, permissions,
token_budget, and auto_review instead of dropping them. A parity test pins the renderer to
that key set.
To keep Codex from printing a model metadata warning every turn, point model_catalog_json at
a full-shape catalog. The rendered one in the state dir works; copy it where you like:
mkdir -p ~/.codex/model-catalogs
cp ~/.codex/opencode-go-proxy/opencode-go-catalog.json ~/.codex/model-catalogs/opencode-go.jsonmodel_catalog_json = "/absolute/path/to/.codex/model-catalogs/opencode-go.json"The catalog ships with the ModelsCache wrapper (fetched_at/etag/client_version/models).
Codex 0.142+ desktop app requires all four top-level fields — a bare {"models": [...]} catalog
causes the model picker to fall back to "Custom" instead of showing the full list. The CLI
(codex debug models) tolerates the bare format, so this only surfaces in the desktop app.
The proxy also captures the models your logged-in Codex account can use
(codex debug models), filters out every opencode-go/ slug, and merges
them with the OpenCode Go catalog into merged-models.json in the state dir.
/v1/models serves the merged catalog: the app's model picker shows official
GPT models and custom models side by side, with official OAuth untouched.
Routing is by model: a slug in the captured native set goes verbatim to
https://chatgpt.com/backend-api/codex (override with
OPENCODE_GO_PROXY_NATIVE_BASE_URL), while known Go and Zen catalog entries go
through their translation paths. Unknown IDs are rejected. Run
refresh-runtime after logging in or out of an account to recapture.
Only set the native base URL override to an endpoint you trust with the
client's native Codex authorization.
Custom models are layered on top of the merged catalog at runtime, never by editing
contrib/. user-models.json in the state dir holds entries keyed by slug: a full record adds
a model, a partial record edits display fields, "hide": true hides one:
{
"version": 1,
"models": [
{"slug": "my-custom-model", "display_name": "My Custom", "context_window": 200000},
{"slug": "deepseek-v4-flash", "display_name": "Flash (edited)", "priority": 3},
{"slug": "deepseek-v4-pro", "hide": true}
]
}Hidden-model flags from model-picker.json ({"version": 1, "hidden": ["slug"]}) hide models
from the picker too. Newly added models get a seven-day availability_nux announcement,
tracked in announced-models.json under the state dir. The proxy re-reads the rendered catalog
on each /v1/models request (cached by file mtime), so editing the overlay and refreshing grows
the live list without a restart.
If you want Codex's full base_instructions for each model, copy your Codex installation's
bundled models.json and append the OpenCode Go entries from the reference catalog (keep the
ModelsCache wrapper).
Every request emits compact JSON lines on stderr. Important events:
server.startrequest.receivedrequest.convertedcredential.sourceupstream.startupstream.doneresponse.convertedrequest.failed
Model metadata warning every turn
Set model_catalog_json in Codex config and copy the rendered catalog:
cp ~/.codex/opencode-go-proxy/opencode-go-catalog.json ~/.codex/model-catalogs/opencode-go.json
Connection refused on localhost:8787
Proxy isn't running. Start it: opencode-go-proxy or check launchctl list | grep opencode.
Port already in use
Only one proxy instance can bind 8787. Confirm what is listening:
lsof -nP -iTCP:8787 -sTCP:LISTEN. If a stale instance holds the port, stop it before
starting another.
API key not found
Set OPENCODE_GO_API_KEY env var or add to macOS keychain:
security add-generic-password -a "$USER" -s opencode-go-api-key -w
Upstream rate limited (429) OpenCode Go has 5-hour/weekly/monthly usage limits. Switch to a cheaper model (DeepSeek V4 Flash or MiMo V2.5) to stretch your quota. See usage limits.
Streaming not working
Codex sends stream: true — the proxy handles this. If you see no SSE events, check stderr trace for upstream.error or upstream.network_error.
Codex says "model is not supported when using ChatGPT account"
Run opencode-go-proxy config enable, fully restart Codex, and confirm
openai_base_url points at the local proxy. The managed setup uses Codex's
documented proxy configuration so native and routed catalog models can coexist.
uv run python -m pytest tests -v
uvx ruff check
uv buildSee CONTRIBUTING.md for guidelines.
MIT. See LICENSE.