Codex Integration
opencodex makes Codex route through the proxy by editing two things Codex reads: its config
($CODEX_HOME/config.toml, default ~/.codex/config.toml) and its model catalog. Every edit is
idempotent and reversible.
The Connect overview has a Codex switch for this native integration. Its switch shows the latest saved desired state from OpenCodex’s configuration, including immediately after a toggle, while the badge reports whether Codex is currently observed using the proxy; during cleanup those can briefly differ while the badge continues to report the observed state. Disabling names the effective Codex config file, removes OpenCodex’s generated routing artifacts, and leaves the proxy running for other clients. Re-enabling rebuilds the catalog from the models available at that time, so it does not restore the Codex files byte for byte.
For bare native Codex models and OpenCodex-generated account-selector rows, the catalog carries authenticated access program metadata. Bare models use the main Codex account; account-qualified models use their selected account. Refreshing the integration updates these rows when upstream changes the account’s access programs. OpenCodex carries the logged-in main account’s live model availability prompt onto bare native model rows.
The proxy exposes one bare openai Codex-login route with Pool(default) and Direct account modes,
plus openai-apikey/<model> for the configured API key. Pool includes main plus added accounts;
Direct uses only the caller/main bearer. The routes do not fall back to one another. Shipped v1
configs migrate to marker 2 and preserve config.json.pre-openai-tiers-v2.bak for manual restore.
Within Pool mode, a request carrying a validated native Codex login can use that login when the selected stored account is cooling down and no eligible stored alternative or recovery probe is available. This also covers a new request blocked before sending, following the same caller validation used after an upstream rejection. Existing model-permission and main-account policy checks still apply. The fallback preserves the stored account’s cooldown and does not persist the caller credential as the Pool selection. An exact account binding remains bound to that account. A manual Pool pin is a preference, not an exact account binding: it can still use this fallback. Use an account-qualified model when a request must remain on one account. Pausing the observed native main blocks fallback for its matching caller credential; it does not identify or pause unrelated caller-owned logins.
Adding or reauthenticating a pool account rejects a duplicate of the native main login with
Account is already configured as the native Codex main account. The check compares ChatGPT
account ID, personal/workspace subscription bucket, and email; a missing email does not prove
a different identity. Different workspaces, distinct workspace members, and separate personal
and workspace subscriptions remain supported. A failed usage lookup does not discard the
subscription plan carried by the login token.
Existing duplicates remain stored but cannot be selected as a second Pool account. The account
list exposes selectionExcludedReason: "duplicate_main" and an actionable selectionExcludedAction. Use native main explicitly,
or sign native Codex into a different account before reusing the stored entry. Detection follows
the latest owned native-login observation, including a later native relogin; passive account
listing and caller-owned requests do not open native credentials to refresh that observation.
Config injection
Section titled “Config injection”ocx sync removes reasoning efforts unsupported by the selected Codex runtime from the catalog.
It reports a clamp when its runtime identity, removed efforts or affected models change; unchanged
syncs preserve the diagnostic timestamp and do not repeat that notice. This compatibility
projection does not require running ocx doctor. If a clamp clears and later returns, it is reported again.
OpenCodex serializes its Codex configuration writes. On Windows, a file temporarily held open by another process gets a short, bounded retry. If access stays busy, retry the operation shortly; OpenCodex preserves the existing lock evidence and does not use that access failure to take over another writer’s lock.
ocx init, ocx start, and ocx sync call the injector. On the default loopback bind, it keeps
Codex’s built-in openai provider id and points that provider at opencodex:
# root keys, before the first tablemodel_catalog_json = "/absolute/path/to/opencodex-catalog.json"# Auto-injected by opencodex (undo: ocx restore)openai_base_url = "http://127.0.0.1:10100/v1"# Auto-injected by opencodex (undo: ocx restore)experimental_realtime_ws_base_url = "http://127.0.0.1:10100/v1"
# only when fastMode is set; unset adds no [features] table[features]fast_mode = trueThe second key is the voice sideband override. Codex creates a WebRTC voice call through
openai_base_url, but since codex 0.146 (openai/codex#35830) it joins that call’s sideband
WebSocket at api.openai.com directly unless experimental_realtime_ws_base_url redirects it. In
Pool mode the call is created under the account opencodex selects, so a direct join under the app’s
own login fails with realtime websocket handshake failed (404). The injected key sends the join
back through opencodex (GET /v1/live/{callId}), where the Pool reuses the account it bound to that
session/thread pair (a process-local binding). A call the client created itself is the exception:
when ChatGPT voice hands a call to a Codex thread, or Codex Desktop creates the call on its own,
the call belongs to your ChatGPT login and only its sideband join reaches opencodex, so opencodex
forwards that join with the caller’s own ChatGPT credential rather than a Pool account. In Direct
mode both legs already use the caller’s
current bearer, so the key only keeps the join on the proxy path. It is written only on the loopback
openai_base_url form, is removed together with it, and a user-owned
experimental_realtime_ws_base_url is never overwritten.
Voice transport and task handoffs
Section titled “Voice transport and task handoffs”Codex owns the microphone and speaker, WebRTC media negotiation, captions, mute controls, and voice cleanup when switching threads. OpenCodex relays call creation and the sideband connection; work delegated by voice uses the normal Responses routing path. Choosing a text provider does not replace the realtime speech model or enable voice in a client that does not support it.
The upstream WebRTC helper change and TUI voice integration describe these client responsibilities, including speaking final answers from voice handoffs. Their merge dates do not establish when the same behavior reached the desktop app.
Optional OCX_LIVE_FRAME_LOG diagnostics write only frame timestamp, direction, kind, byte count,
and a replacement-character flag (ts, dir, kind, bytes, fffd). They do not store voice
text or frame excerpts. For binary frames, UTF-8 decoding can itself produce replacement
characters, so the flag alone does not identify where corruption occurred. Existing log files
are not rewritten.
Fast mode
Section titled “Fast mode”The injected fast_mode follows the tri-state fastMode setting: true writes fast_mode = true,
false writes fast_mode = false, and unset leaves an existing fast_mode untouched without
adding a [features] table.
Fast mode is separate from voice transport. A supported model’s service-tier speed description does not guarantee lower microphone, WebRTC, or end-to-end voice latency through OpenCodex.
ChatGPT-family channel and latency
Section titled “ChatGPT-family channel and latency”Requests routed through opencodex via the canonical ChatGPT-login openai provider — adapter
openai-responses, authMode: "forward", and the https://chatgpt.com/backend-api/codex
endpoint, covering both Pool and Direct modes — use the public ChatGPT endpoint. Provider routing
or account selection does not bypass the upstream ChatGPT channel. The upstream may spend time
queueing a request before the first output even when the local proxy and network path are healthy.
Only some turns take the ChatGPT websocket transport — the same responses_websockets lane Codex
CLI defaults to. A turn is eligible when the Bun runtime supports the bounded relay, the request
is a POST to the canonical Responses URL or a configured WebSocket route, and its JSON body sets
stream to true at the root. Everything else stays on SSE over HTTP, and an eligible turn still
falls back to it when the request cannot be prepared, the response.create frame exceeds its size
limit, or the proxy route cannot carry the socket.
To keep the built-in ChatGPT provider on HTTP/SSE, set providers.openai.upstreamWebsocket
to false in ~/.opencodex/config.json and restart the proxy. Merge this field into the
existing openai provider; preserve its account mode and other settings. Omit the field
to restore the default upstream WebSocket selection. This setting does not change the
client-facing websockets switch or the ChatGPT account used for the request. Native
mid-turn steering and injection need upstream WebSocket and are unavailable while it is off.
Reasoning usage metadata (2.90.0 and later). For HTTP/SSE clients on the canonical
ChatGPT route, the upstream WebSocket relay preserves an actual x-reasoning-included
connection-response header when the Bun runtime exposes it, including on a reused connection.
This tells Codex that the server’s reported usage already includes earlier reasoning. Metadata
sent before the first Responses event takes precedence; later metadata remains in the stream
but cannot change headers already committed. The relay does not invent this signal or alter
usage numbers. A missing upstream signal and Codex’s own accounting lifecycle remain separate
issues, so this change alone does not establish that early compaction is fixed. Client-facing
WebSocket connections have already completed their upgrade before the upstream response and
cannot receive the signal retroactively as an upgrade header.
Local provider pacing can also hold a request before it is dispatched at all. So a slow first
output has several possible contributors, and upstream queueing is only one of them. ocx doctor
classifies configuration and measures none of these: compare actual transport, pacing, network,
and provider observations before concluding.
What decides whether a request takes that public channel is the destination it resolves to, not
the name of the provider entry. A provider that resolves somewhere else — openai-apikey, or a
custom entry pointing at its own API — reaches that endpoint directly and sees no ChatGPT queueing.
A custom-named entry that resolves to https://chatgpt.com/backend-api/codex with forward auth
takes the same public channel as the built-in row, because the classification reads the adapter,
auth mode and destination rather than the entry’s name.
The ocx doctor hint is narrower than the endpoint behavior it describes: it inspects only the
built-in openai row, so its absence tells you nothing about where any other provider resolves.
service_tier: priority is a request preference. On the ChatGPT backend the echoed
service_tier cannot confirm or deny the granted tier: turns scheduled as priority can still
echo default, so request logs show the response tier as an observation with confirmation
assumed. For latency-sensitive work, compare observed first-output times across the providers you
actually use rather than assuming any particular channel is faster.
For a gateway forwarding to a backend with the same metadata limitation, explicitly declare
responseTierAuthoritative: false
on that provider. The request still sends priority and records the raw echo, while actual Fast
scheduling remains unconfirmed. Undeclared gateways and the official API keep their existing
response-based interpretation. Updating OpenCodex alone does not add this declaration to existing
gateway entries: without it, an eligible priority request followed by a default echo still records
response-declined.
The proxy listens on port 10100 by default and serves POST /v1/responses,
POST /v1/responses/compact, POST /v1/images/generations, POST /v1/images/edits,
GET /v1/models, GET /healthz, and the /api/* management surface.
Experimental context management (Codex 0.153+)
Section titled “Experimental context management (Codex 0.153+)”For an eligible ChatGPT account, enable the experimental feature in Codex’s own
config.toml (merge this into an existing [features] table):
[features]context_management.experimental_mode = trueOn the default built-in loopback integration, the next ocx sync or proxy start changes the
managed root openai_base_url to http://127.0.0.1:10100/backend-api/codex. Codex checks this
backend path before enabling its new_context, history, and notes tools. Start a new Codex
session after synchronization. The feature remains opt-in; user-owned base URLs and remote
custom-provider injection are not rewritten. No context-window or compaction-limit override
is needed or added.
The backend prefix aliases the existing data-plane routes, including Responses WebSocket
upgrades. The original /v1 routes and the realtime sideband override remain available. The
proxy also relays the ten native alpha/history/v2/* and alpha/notes/v2/* POST endpoints
through the built-in openai provider with the openai-responses adapter and canonical
ChatGPT forward destination. Direct uses the current caller/main login; Pool selects a Codex
account. openai-apikey uses its configured API key, and custom or noncanonical Responses
providers are not candidates for this context relay and receive no Codex-account credentials
from it. These private endpoints are not implemented by other model providers or the OpenAI
API-key route.
Caller headers are restricted to the shared Codex forward allowlist: authorization,
chatgpt-account-id, and approved OpenAI beta, originator, session, and Codex protocol metadata.
The context relay additionally forwards x-openai-encrypted-tool-arguments and
x-openai-tool-output-truncation-policy; arbitrary caller headers such as cookies are not
forwarded. A proxy data-plane key presented as a bearer is replaced with the selected Codex
credential (the stored main login in Direct); missing credentials fail before forwarding.
Proxy admission credentials never go upstream. Encrypted arguments, response bodies, and
upstream error statuses are preserved.
A successful ChatGPT model response records the root session’s actual serving account in a bounded, process-local ownership registry. History and notes use that recorded owner, including an explicitly selected account, even when the current active account changes. Stored-account token refresh may continue for the same physical account; a replaced account identity is rejected. Direct caller-owned sessions keep the caller credential and cannot be taken over by a proxy bearer. This does not migrate server-side history between accounts.
Ownership is partitioned by the opencodex API key that admitted the request, so two keys never
reach each other’s sessions even when both resolve to the same ChatGPT workspace. A workspace id
names an organization rather than a person, so the registry also binds the stable user carried by
the credential upstream accepted: an ordinary token refresh for that same user continues the
session, while a different user in the same workspace does not. When the accepted credential
proves no stable user, only that exact credential continues. That principal is the opencodex API key the request presents. A remote bind already requires one,
so ownership works there. On the default loopback bind opencodex admits requests without reading a
key, and the built-in loopback injection cannot carry the x-opencodex-api-key header, so Codex
presents no opencodex key and context history returns HTTP 403. The relay is therefore available
on a remote bind with a configured key, or to a client that sends x-opencodex-api-key itself, and
not through the default built-in loopback integration. Whether a loopback-bound proxy should be
able to name a caller at all is an open maintainer decision, so the current behaviour refuses
rather than guessing.
Unknown, expired, evicted, conflicting, or restart-lost ownership returns HTTP 409 before account selection or upstream I/O. The relay does not guess from the current active account. Existing sessions should save a checkpoint or other durable summary before enabling the feature or resetting context. Enabling it does not backfill earlier history or notes, and a new ownership observation does not prove that older backend content exists. After a restart, establish ownership with a successful model request before using context tools; start a new session when ownership conflicts.
Existing model affinity, cooldown, and retry rules are unchanged. Context requests are not automatically retried, including notes writes; ChatGPT forward requests do not use same-key 429 replay. History traffic does not consume or settle a model quota-recovery probe.
One 35-second deadline covers the whole relay operation, starting before the request body is read and covering credential selection, so a stalled client cannot hold an admitted turn slot open. A client disconnect returns 499 and an expired deadline returns 504; nothing is dispatched upstream after either.
To disable the feature, remove the experimental key (or set it to false), run ocx sync,
and start a new Codex session. The managed root base returns to /v1.
While the key is absent or false the relay does not exist: the ten endpoints answer 404 for any
caller, including one that posts them directly, and a model turn records no history ownership.
opencodex decides this by reading Codex own config itself, so the switch does not depend on the
injected URL or on anything a client sends, and turning it off takes effect without a restart.
Hosted image results in Codex App
Section titled “Hosted image results in Codex App”When a routed Responses provider returns a completed hosted image_generation_call
with base64 image data, opencodex saves the validated image under its local
artifacts/ directory and delivers a final assistant image message to a locally
connected Codex client. The image stays outside the collapsible progress section.
This applies to streaming and non-streaming Responses, without another generation
request or a change to the selected provider.
When a client replays these generated image messages as assistant history, opencodex replaces its generated local image links with opaque artifact references before forwarding that history. This protects the display paths without changing the image message already shown in the app. It does not redact unrelated user-supplied paths.
This display compatibility requires loopback admission and a recognized Codex client. Remote and generic API clients retain the provider’s hosted response format. Partial previews and URL-only results are not rendered by this compatibility layer. Artifacts use the existing retention limit, so save images you want to keep before older files are pruned. An invalid image or a failed local write produces a visible failure message instead of a broken image link. Display batches reject duplicate image identities, oversized routing metadata, and output that exceeds the display byte limit; these failures return HTTP 502 before streaming or a failed terminal event after it starts.
Built-in image generation (image_gen)
Section titled “Built-in image generation (image_gen)”Codex’s built-in image_gen tool does not go through /v1/responses — the codex-rs extension
POSTs {base_url}/images/generations (or /images/edits when reference images are attached)
directly, with the same ChatGPT bearer auth it uses for chat. Because the injected base_url
points at opencodex, the proxy relays those calls to the OpenAI upstream.
This is separate from the Image Bridge, which only activates when a
Responses turn lists the hosted image_generation tool while a non-OpenAI model is selected.
Standalone /images/generations calls never enter that bridge.
- One mode-aware forward candidate: Pool selects an eligible main/added account and uses its stored ChatGPT credential even when the client authenticates to opencodex with a proxy admission bearer. Direct uses the caller’s ChatGPT OAuth bearer and cannot forward a proxy admission token. The configured mode applies consistently to the image request.
- OpenAI API-key provider: it is used only when no forward candidate owns an authentication failure. A broken/expired Pool credential is never hidden behind separately billed API usage.
- Explicit custom provider: set
images.providerto the id of a custom API-keyopenai-responsesprovider whose endpoint implements the OpenAI Images API. Explicit selection fails closed and never falls back to a different paid upstream. Registry-managed provider ids are not accepted here; omitimages.providerto use the built-in OpenAI tiers. - xAI Imagine (Grok OAuth) relay: when
images.bridgeEnabledistrue,images.provideris omitted, and anxaiprovider is configured,/v1/images/generationsand/v1/images/editsare sent tohttps://api.x.ai/v1. The credential depends on the provider’sauthMode: with"oauth"the relay reuses the Grok CLI grant fromocx login xai; with any other mode it uses the provider’s API key. An OAuth login does not arm a keyed provider, and vice versa. ChatGPT credentials are not forwarded. If the credential is missing, the proxy returns 400 instead of billing ChatGPT. Settingimages.providerexplicitly hands/v1/imagesto that provider; its own validation errors are returned as-is and the xAI relay is never tried. The relay maps Codexsize/aspect_ratioonto xAI’s Imagine body and returns the same{created, data:[{b64_json}]}shape. Combined decoded bytes and base64-encoded output across the batch (inlineb64_jsonand downloaded URLs) stay under 100 MiB; a batch that would exceed that cap returns 502. When xAI returns an image URL instead of inline bytes, the proxy fetches it itself with no credential: the URL must be public HTTPS (no redirects, nofile:, no loopback or private addresses), each download is capped at 50 MiB, and the result is materialized as a local artifact that is served only through the authenticated management endpoint. This is independent of the Responses Image Bridge loop (which remains API-key-only). - Google Antigravity (CCA) fallback: when neither an OpenAI forward candidate nor a keyed
provider is configured,
/v1/images/generations(not/images/edits) falls back to the Antigravity Cloud Code Assist endpoint using thegemini-3.1-flash-imagemodel. The fallback also fires after OpenAI auth resolution fails (e.g. an expired or missing ChatGPT credential), not only when no OpenAI candidate is configured. This requiresocx login google-antigravity; the OAuth token is sent only to the pinned CCA registry host, never to a config-levelbaseUrloverride. The response is returned in the same{created, data:[{b64_json}]}shape Codex expects. - Neither: the proxy returns a clear error instead of a generic 404. Routed providers
(Cursor, Gemini, Kiro, …) cannot serve the
image_generationtool relay; if you don’t want the tool offered at all, disable it in Codex withcodex features disable image_generation([features] image_generation = falseinconfig.toml).
The tool declaration still travels with the model’s Responses request. For API-key Responses
providers, opencodex lowers Codex’s private image_gen namespace to an upstream-safe
image_gen__<inner-name> alias (for example image_gen__imagegen). When that usable alias replaces
the client declaration, opencodex removes a duplicate hosted image_generation declaration. It maps
the function call to the explicit image_gen namespace before Codex sees it, and encodes the native
call again when later history is replayed upstream. This keeps client-side image generation callable
on public-compatible upstreams that reserve the namespace or reject dotted function names. ChatGPT
forward mode remains untouched and keeps its native Responses Lite shape.
For an OpenAI-compatible custom gateway, configure a dedicated provider and select it only for standalone Images requests:
{ "providers": { "custom-images": { "adapter": "openai-responses", "baseUrl": "https://gateway.example.com/v1", "authMode": "key", "apiKey": "${IMAGE_GATEWAY_API_KEY}" } }, "images": { "provider": "custom-images", "timeoutMs": 300000 }}The custom endpoint must accept POST /v1/images/generations and /v1/images/edits and return the
OpenAI Images response shape expected by Codex. The provider’s configured key replaces any caller
bearer before the upstream request.
Note: This refers only to the Codex
image_generationtool (/images/generationsrelay). Gemini models that are image-capable produce inline images natively through theresponseModalities: ["TEXT", "IMAGE"]), independent of this relay — see Adapters.
For a non-loopback hostname, Codex must send the generated API auth header. The injector therefore
uses a dedicated provider instead:
# root keysmodel_provider = "opencodex"model_catalog_json = "/absolute/path/to/opencodex-catalog.json"
# appended at the end of the file# Auto-injected by opencodex (undo: ocx restore)[model_providers.opencodex]name = "OpenCodex Proxy"base_url = "http://your-host:10100/v1"wire_api = "responses"requires_openai_auth = trueenv_key = "OPENCODEX_API_AUTH_TOKEN"# supports_websockets = true # only when config.websockets is trueWhen OpenCodex owns routing, both modes write $CODEX_HOME/opencodex.config.toml as a
reference/fallback config. On loopback it contains the root keys you can merge manually if automatic
injection was removed; on non-loopback it contains the dedicated provider form. External-provider
mode leaves this profile untouched.
Shared model catalog
Section titled “Shared model catalog”Codex CLI, TUI, App, and SDK all read the same Codex home. opencodex resolves that directory from
CODEX_HOME, falling back to ~/.codex, and manages:
$CODEX_HOME/config.toml$CODEX_HOME/opencodex.config.toml$CODEX_HOME/opencodex-catalog.json$CODEX_HOME/models_cache.jsonOn macOS, opencodex checks the Codex app’s bundled CLI in /Applications/ChatGPT.app and then ~/Applications/ChatGPT.app after explicit selections, shims, PATH, established installs, and the runnable bare codex fallback, requires the bundle’s XML Info.plist identity to be com.openai.codex, and never automatically replaces a runnable persisted selection with an app candidate.
On WSL, if CODEX_HOME is unset and the Linux ~/.codex directory is absent or holds no Codex state
(config.toml, auth.json, sessions, history.jsonl), opencodex also
checks for a single Windows Codex Desktop home at /mnt/c/Users/*/.codex/config.toml. When exactly
one candidate exists, it uses that directory so WSL app-server mode and Windows Codex Desktop share
the same config and auth files. Set CODEX_HOME explicitly to override this detection. When Windows Codex Desktop runs its app-server inside WSL, it ships the Linux Codex binary under that home as bin/wsl/<hash>/codex; opencodex finds it there when the service PATH has no codex, after any explicitly configured runtime and PATH.
If the Codex home exists but Codex has not written config.toml yet (for example a fresh Desktop install
that was never signed in to OpenAI), opencodex creates an empty config.toml there and continues. If
the home directory itself does not exist, start Codex once so it creates it, or set CODEX_HOME.
Codex can keep SQLite-backed thread state in a separate directory. OpenCodex history operations use
the same precedence as Codex: root sqlite_home in config.toml, then CODEX_SQLITE_HOME, then the
effective CODEX_HOME. Relative SQLite homes resolve from the current working directory. When an
explicit CODEX_SQLITE_HOME is present during service installation or repair, the durable launcher
stores its install-time absolute path so the background proxy continues to address the same database.
If config.toml or its root sqlite_home key is absent, OpenCodex continues to the
environment/home fallback. If the file cannot be read or parsed, or the key is present but blank or
not a string, SQLite-home resolution stops instead of risking history operations against a different
database.
On Windows, an Orca shell can set both CODEX_HOME and ORCA_CODEX_HOME to Orca’s bundled runtime
home while the ChatGPT/Codex app still reads %USERPROFILE%\\.codex. ocx status and ocx doctor
warn about this exact mismatch and print redacted target paths. If a background service was installed
from that Orca shell, uninstall it from the original shell first, then set CODEX_HOME to the app
home, unset ORCA_CODEX_HOME, rerun sync/restore, and install the service again.
In dedicated-provider mode, requires_openai_auth = true keeps Codex App/TUI account-gated surfaces
aligned with native Codex. opencodex also serves /v1/responses over WebSocket. The dedicated
provider advertises supports_websockets = true only when "websockets": true; on loopback Codex’s
built-in provider may try WebSocket first, and a disabled proxy returns 426 so Codex falls back to
HTTP/SSE.
The proxy admits up to 128 client-facing Responses WebSocket connections. When a new connection
finds all 128 slots occupied, it closes the least recently used idle connection to make room. Connections
with an active response, pending client tool or approval results, a native steering/injection
session, or buffered output are protected. If no idle connection can be released, the new
connection receives the existing 503 capacity response. A reclaimed client can reconnect for
later requests using the existing continuation handling. This happens only under connection
pressure, needs no setting, and does not change voice connections or upstream connection reuse.
If a canonical ChatGPT forward continuation references expired or missing local replay state,
opencodex returns previous_response_not_found before sending anything upstream. Codex’s
WebSocket client recognizes this error and can reconnect with its full retained context,
including completed tool calls and their results, within its normal stream retry budget. An
idle task therefore does not need a new task solely because the proxy’s replay cache expired.
Replayed continuation state is retained for 24 hours and stays bounded by its existing memory,
disk, and entry ceilings; this does not recover history the client no longer has. HTTP clients
must handle the error explicitly and resend their full context without previous_response_id.
Retrying only the same ID cannot recover missing state.
The same refusal applies when the referenced state belongs to a different client task scope,
including when the new request appears to carry complete input. The proxy cannot prove that input
is complete, so it does not silently remove previous_response_id or reveal whether matching state
exists. Retry with the complete conversation and omit previous_response_id; matching scopes and
legacy continuations where both scopes are absent or blank continue to replay normally.
The same recovery signal applies to every routed destination, because only the native Responses
passthrough can answer a turn whose history this proxy lost — it forwards previous_response_id
to a backend that stored the chain. Every other wire rebuilds the conversation from each request’s
own input, so a missed expansion there would otherwise send the current turn alone and silently
lose the conversation. That includes the three that look stateful: Devin re-sends the whole
conversation every turn, Cursor’s checkpoint reference lives in the same expired store and falls
back to full replay without it, and Kiro rebuilds its conversation history from the turns it was
handed. It also applies to routed Responses providers configured with
statelessResponses: true, and to routed requests where a custom tool was lowered to a function
but a delta result has no local call to establish its original type. Full replay preserves the
call, result, and reasoning together; opencodex does not guess the result type or drop it.
Stateful providers still resolve native function and native-only custom continuations themselves.
These checks follow the selected wire protocol and tool declarations, not the model name. For a
gateway that cannot resolve stored response IDs, enable statelessResponses on that provider;
other providers keep their defaults.
Client-side compaction (opt-in)
Section titled “Client-side compaction (opt-in)”Authenticated loopback routing normally keeps Codex on its built-in openai provider identity.
That preserves native thread identity, but it also makes Codex request native remote compaction.
When a routed provider cannot return a native compaction blob, OpenCodeX stores the summary in its
own ocx1:/ocx2: envelope. Native ChatGPT cannot verify that envelope if OpenCodeX is later removed from
the request path.
On an authenticated loopback route, enable client-side compaction to keep V2 sub-agent routing while preventing new ocx1:/ocx2: compaction summaries. Non-loopback and API-key routes retain their existing provider and authentication behavior:
ocx system settings --client-compaction on # or "codexClientCompaction": true in config.jsonocx sync # rewrites the active config (default: ~/.codex/config.toml); restart DesktopThe setting defaults to off. For authenticated loopback routing, OpenCodeX selects its existing
dedicated provider form with requires_openai_auth = true. If codexDesktopAuthless is also
enabled, that stronger compatibility setting takes precedence and writes
requires_openai_auth = false:
model_provider = "opencodex"
[model_providers.opencodex]name = "OpenCodex Proxy"base_url = "http://127.0.0.1:10100/v1"wire_api = "responses"requires_openai_auth = trueCodex then owns compaction and stores a portable plaintext summary rather than a new OpenCodeX
envelope. The compacting request still routes through OpenCodeX and can consume quota on the
selected provider. V2 sub-agent requests keep their existing provider selection and quota
accounting. Client-side compaction does not change plaintext delivery, encrypted task passthrough
through allowEncryptedV2AgentTasks, or configured recovery and fallback behavior.
This preference affects future compactions only, and it rewrites no existing ocx1:/ocx2: payload in
any configuration, so use the explicit history recovery workflow for a thread that needs one.
Whether resume-history metadata is re-tagged depends on which form the injection takes. On its
own, on an authenticated loopback bind, client-side compaction re-tags nothing: it keeps the root
override instead, as described below. Enabled together with codexDesktopAuthless, or on a
non-loopback bind, the stronger form wins and those forms behave exactly as they do today,
including their existing forward-tagging of resume history with originals backed up for restore.
Going back from one of those forms to plain Design B migrates the re-tagged threads back. Turning
off codexDesktopAuthless while leaving client-side compaction on does not: that lands on the
compaction-only form, which skips the history unit, so threads already tagged opencodex keep
that tag. They still reach this proxy, through the provider table rather than the root override.
On the compaction-only form, existing threads keep working because the injection keeps the root
openai_base_url override alongside the provider table. New threads default to opencodex and
get client-side compaction, while a thread already tagged openai still resolves to Codex’s
built-in provider — which the retained override still points at this proxy. Without it that
thread would resume against OpenAI directly, taking configured routing with it. The authless and
non-loopback forms cannot use the root key, which is why they keep re-tagging instead.
That guarantee covers the override OpenCodeX manages. A root openai_base_url you wrote
yourself is never replaced, and in that case the built-in provider keeps the destination you
chose, so an openai-tagged thread follows your configuration rather than this proxy. Turning
the setting off and syncing removes the table and returns to the plain Design B override, unless
codexDesktopAuthless or non-loopback admission still requires the provider-table form; those
two forms cannot use the root key and are unchanged.
While the mode is active, the realtime voice sideband override
(experimental_realtime_ws_base_url) is not injected — the dedicated provider-table form cannot
carry it — so Codex Desktop voice uses its native endpoint rather than the proxy.
Emergency compaction model (opt-in)
Section titled “Emergency compaction model (opt-in)”When OpenCodeX translates remote compaction for providers such as Google, it replaces images from earlier turns with short reopening notes if a later explicit final answer exists. Existing analysis and file references stay in the summary input; images after the last final answer stay available for unresolved work. Commentary-only and unphased histories are preserved. This affects only the compaction request, not stored attachments or ordinary model requests. It reduces repeated vision input but does not guarantee that a long text history fits the provider’s context limit.
compactionRecovery leaves the initial compaction on the conversation’s selected route. It
permits one emergency attempt only after a supported, pre-output compaction failure. It is
separate from compactionRouting, which chooses another model before compaction starts, and
from codexClientCompaction, which changes Codex’s provider form.
{ "compactionRecovery": { "enabled": true, "model": "provider/emergency-model", "allowDevinInvalidArgument": false }}Use an independently configured, authorized model with enough context for the failed input.
Enabling recovery permits that model’s provider to receive the compaction history and charge
for the extra attempt when recovery runs; ordinary successful compactions incur no extra call.
The option is off when absent or disabled. The existing authenticated management API accepts
this block through PUT /api/settings; send compactionRecovery: null to remove it. A direct
file edit should follow the normal stopped-proxy configuration workflow. This setting does not
change sign-in, the conversation’s ordinary model, Codex’s provider ID, or the desktop composer.
Recovery does not replay after cancellation, semantic output, tool side effects, an exhausted
send budget, or an authentication, admission or policy refusal. Generic 400 errors do not
enable fallback. Devin answers an oversized history with an opaque pre-output
invalid_argument; when the request’s estimated size is at or near the model’s input window,
the adapter reports it as context_length_exceeded instead, so Codex compacts on an ordinary
turn and a failed compaction qualifies as a context overflow without any Devin-specific option.
The estimate uses tool descriptions after Cognition sanitization and truncation, matching the
request sent upstream. A smaller request that gets the same code stays a plain 400. The
separately opted-in allowDevinInvalidArgument case covers only those remaining invalid_argument failures, and
only on an identified compaction request. The emergency attempt shares the original request’s
send budget and never starts a second recovery attempt.
Native encrypted compaction is outside this recovery path: its original error is retained. There is no automatic local truncation mode. A response being accepted is not proof that a long conversation retained its goals; verify the next turn on the original model before treating an emergency summary as a recovered task.
Authless Codex Desktop (opt-in)
Section titled “Authless Codex Desktop (opt-in)”In Dashboard → Overview, Open Codex without signing in controls this existing
opt-in preference. The switch defaults to off when the setting is absent or false;
an existing explicit codexDesktopAuthless: true stays enabled. The dashboard saves
the preference and runs a full sync. Restart Codex Desktop after changing it.
If synchronization fails, the saved preference remains and the dashboard shows the error;
retry Sync before restarting. Account-gated Desktop features may be unavailable
when enabled. Upstream credentials, local eligibility, remote admission authentication
and user-owned gateway settings retain their existing requirements.
Codex Desktop shows its ChatGPT login screen whenever the active provider requires OpenAI auth. If
your OpenCodex setup never uses ChatGPT credentials (routed providers only, or a blocked
chatgpt.com), you can opt out of that gate:
ocx system settings --desktop-authless on # or "codexDesktopAuthless": true in config.jsonocx sync # rewrites ~/.codex/config.toml; restart DesktopWith the switch on, a loopback bind injects the dedicated provider form instead of the root
openai_base_url override:
model_provider = "opencodex"
[model_providers.opencodex]name = "OpenCodex Proxy"base_url = "http://127.0.0.1:10100/v1"wire_api = "responses"requires_openai_auth = falseDesktop then starts without a login and routes every turn through the proxy. The setting survives
ocx start, restart, ocx sync, and ocx ensure; turning it off (--desktop-authless off) makes the
next sync restore the default loopback form, and ocx restore strips it like any other injected
routing. What to expect while it is on:
- ChatGPT-gated Desktop chrome (account, usage, Fast mode) stays dark: Codex derives those surfaces from the provider’s auth requirement.
- New threads are tagged with the
opencodexprovider, as on a non-loopback bind, and history is handled the same way. - Desktop releases that filter the model picker against a native-only allowlist may show an empty
or
Custompicker in this mode as well; requests still use the configured model. Setmodel = "<provider>/<id>"inconfig.tomlas described in Desktop remote servers.
This only changes the Desktop login gate. Non-loopback binds keep requires_openai_auth = true and
the env_key admission credential regardless of the switch; it never exposes an OpenCodex listener
without authentication.
Thread identity and history
Section titled “Thread identity and history”The default loopback form keeps new threads tagged with Codex’s native openai provider, so normal
resume history needs no remapping. Sync and restore apply only a matching backup manifest and
restore each thread’s exact original provider, source, and event marker. A bare opencodex row with
no manifest is left unchanged; use ocx recover-history --legacy-openai --yes only when you explicitly
intend to force that legacy relabel. The command is intentionally broad: it rewrites every thread
with a user message currently tagged opencodex to openai, normalizes exec to cli, and sets
the event marker—including legitimate dedicated-provider history. Back up the state and use it only
when that full scope is intended. Non-loopback dedicated-provider mode still mirrors history
under the opencodex provider while active and restores the backed-up metadata on exit. Set
syncResumeHistory: false to leave history untouched.
Model catalog sync
Section titled “Model catalog sync”Codex shows models from an on-disk catalog ($CODEX_HOME/opencodex-catalog.json by default). On
start and on ocx sync, opencodex:
- Backs up the pristine catalog once to
~/.opencodex/catalog-backup.json(so featuring is reversible). - Fetches eligible providers’ live model catalogs (cached ~5 min; falls back to the last good
list, then configured
models[]). Forward auth has no model endpoint, and Cursor uses itsGetUsableModelsRPC rather than/models. - Merges routed models in as namespaced entries (
provider/model), cloned from a native Codex catalog template so Codex’s strict parser accepts them. - Filters
config.disabledModelsand each provider’s non-emptyselectedModelsallowlist. - Re-ranks so featured models sort first (see below), then writes the merged catalog back.
Routed catalog entries also get their GPT-5 identity rewritten to the real upstream model name.
Reasoning controls come from provider/model metadata across Codex’s low | medium | high | xhigh | max | ultra ladder; unsupported values are mapped or clamped before the upstream request.
Coordinator diagnosis and recovery
Section titled “Coordinator diagnosis and recovery”Native config/history writes use a per-user SQLite coordinator keyed by the canonical CODEX_HOME.
If a process terminates in SQLite’s initial creation window, a zero-byte coordinator can remain even
though it contains no authoritative transition row. ocx doctor reports the exact coordinator path
and distinguishes zero-byte, unversioned, rowless, valid, unsafe, and unreadable states without
creating SQLite sidecars. Automatic sync tolerates only an identity-stable zero-byte file that has
settled for at least one second and whose immutable SQLite snapshot has version zero with no tables;
a newly created zero-byte file remains on the locked coordinator path.
For a state that doctor proves is a zero-byte creation remnant, stop the OpenCodex proxy/service and run:
ocx doctor --recover-zero-byte-coordinator --yesocx syncRecovery moves the still-identical zero-byte file to a same-directory .zero-byte-backup-* path;
it does not delete the evidence or adopt legacy routed state. It refuses a running proxy, lock
contention, symlinks/reparse points, foreign ownership, changed files, every non-empty database,
and any coordinator that already has an authoritative row. Desktop renderer filtering is a
separate layer: a correct catalog and coordinator do not by themselves bypass the Codex App model
allowlist.
Routed local tools
Section titled “Routed local tools”Non-native routed catalog rows use tool_mode: "code_mode_only". This lets Codex expose its official
exec entrypoint and nested MCP tools, including Browser and Computer Use, while opencodex routes
only the model’s ordinary function call. Tool execution, permissions, and confirmations remain
local to Codex; opencodex does not implement a second browser or desktop-control executor.
For key-auth Responses providers that do not accept Codex’s exec custom-tool grammar, opencodex
encodes that declaration and its history as an upstream function tool, then restores the streamed
function-call lifecycle to custom_tool_call before Codex sees it. Native OpenAI forward routing
and the supported apply_patch custom tool stay unchanged.
If a routed model sends a complete patch as the entire code-mode exec input, opencodex
converts it to the nested tools.apply_patch call before the tool-completion events reach
Codex. Native custom calls and converted function calls use the same completion rule;
patch previews are held while their executable form is unresolved. JavaScript that merely
contains patch text and unrelated native custom payloads stay unchanged.
A routed model can also mistakenly send a shell-argument object such as
{"cmd":"git status --short"} to code-mode exec. For a verified code-mode catalog,
opencodex converts an unambiguous shell object into tools.exec_command(...) JavaScript
and forwards its output through text(...). Shell options are preserved, and Codex still
executes and authorizes the command. Valid JavaScript fallback fields, ambiguous objects,
and unrelated tool namespaces are not converted. This compatibility repair does not bypass
provider rate limits or change the configured retry policy.
The same repair covers the goal helpers. A routed model that calls create_goal, get_goal, or
update_goal (or a default.-prefixed spelling of one) as a tool while the catalog declares only
code-mode exec has the call converted into the matching tools.<helper>(...) call inside
exec. A catalog that genuinely declares the bare goal tool keeps it, and a catalog that declares
neither the tool nor exec still rejects the call as undeclared.
On routed conversions with a verified freeform code-mode exec catalog, structured calls
sent directly to
mcp__<server>__<tool> (including a provider-added default. prefix) are also
wrapped as nested host-tool calls. This avoids a retry
caused solely by a model omitting the exec wrapper. Explicitly declared MCP tools
keep their normal behavior; an ordinary JSON function named exec does not enable
this repair. Unknown tools still fail at the host. Tool-call records printed as
ordinary answer text are not executed by this compatibility rule.
For routed Responses turns, an explicit tool-enforcement policy also rejects client tool calls if the request’s declared-tool catalog is unavailable. An empty declared catalog rejects every client tool call; Chat and Anthropic clients retain their own tool-validation responsibility.
Routed code-mode turns are also told the host’s rules for the nested helpers before the first
call: tools.apply_patch takes one string that opens and closes with the bare patch marker lines,
the isolate has no import, and long-running commands are polled through write_stdin. When a
code-mode exec result on the native routed Responses, Kiro, or Cursor path still carries one of the host’s
failure messages, opencodex appends a one-line hint naming the rule. This change does not rewrite
the model’s code or its patch text.
Ordinary routed Responses function calls also use the original declared parameter schema at
completion: integral floats in integer fields and integral numbers in string-only fields are
normalized, while fractions and numeric unions stay unchanged. An explicitly empty completed
argument string becomes {}. Final events and locally stored continuation history agree.
Unambiguous dotted namespace spellings are restored to the declared namespace and tool name.
The selected provider must support function/tool calling. A text-only provider without tool-call
support cannot use exec, Browser, or Computer Use. Native OpenAI rows keep their upstream tool
mode unchanged.
After ocx sync changes this metadata, restart Codex App and open a fresh task. Existing app-server
processes and tasks may retain the catalog and tool plan they loaded at startup.
Inline visualizations with routed models
Section titled “Inline visualizations with routed models”The Codex App’s Visualize plugin asks the model to reply with a reference wrapped in private-use
characters (U+E200 … U+E201). Some providers remove those characters before the model sees them —
every Claude route we checked does — so the model used to answer with a bare
visualize{"path":…} line that Codex App printed as text.
opencodex rewrites those references into the directive the app itself renders,
::codex-inline-vis{path="/absolute/path/chart.html"}, in the conversation text sent to routed
models. Any model can read and repeat that form, so the visualization renders inline. Native OpenAI
passthrough requests are forwarded unchanged. Replies that were already saved in the bare
visualize{…} form stay as they are; ask for the visualization again in a new reply.
Custom model display names
Section titled “Custom model display names”A custom model can carry a human-readable display name that overrides the label Codex shows in
its model picker, without changing anything about how the model is routed. The display name maps to
the catalog entry’s display_name field only — the routing slug (<provider>/<model>), alias
collision order, provider, and native OpenAI marketing names are all left untouched.
Add a display name from the CLI (the proxy syncs the catalog right away when live):
ocx models add deepseek deepseek-v4 --display-name "DeepSeek V4" --context-window 128000For custom providers, omitting an explicit context window lets the catalog use the
context-window fallback rules.
Model-ID estimates apply only when the provider sets inferContextWindow: true; set an explicit
window or Context cap when the relay supports less than the model’s vendor window.
Remote Codex clients can fetch the same generated catalog with an ordinary data-plane key
— the same credential they already use for /v1/responses, not an admin token:
dest="${CODEX_HOME:-$HOME/.codex}/opencodex-catalog.json"tmp="$(mktemp "${dest}.XXXXXX")"curl -fsS -H "x-opencodex-api-key: $OPENCODEX_API_AUTH_TOKEN" \ "https://proxy.example.com/v1/catalog" > "$tmp" \ && mv "$tmp" "$dest"ocx sync-cacheThe response is the raw opencodex-catalog.json document (no provider credentials). When
available, the x-opencodex-codex-version header reports the Codex runtime version on the
server so clients can spot version skew.
GET /v1/catalog exists so that reading a list of models does not cost an admin token. It is
read-only (GET and HEAD), accepts x-opencodex-api-key, a bearer token, or
x-api-key, and serves exactly the same bytes as the management route. Responses carry a
strong ETag — pass it back as If-None-Match to re-validate and get a 304 instead of the
full document — and Cache-Control: private, no-cache, since the body sits behind a
credential.
A data-plane key admitted here gains nothing on the management plane: /api/catalog and
every other /api/* route still require the admin token or a dashboard session. The older
/api/catalog route keeps working unchanged for the dashboard and for scripts that already
hold an admin token.
You can also set or edit it through the management API (POST /api/custom-models,
PUT /api/custom-models/<id> with a displayName string) and the web dashboard. A / is rejected
because it would collide with the routed-slug separator.
The display name is display-only and stable across regeneration. Every ocx sync and catalog
refresh re-derives routed entries from config.json (including customModels), so the configured
name is reapplied instead of drifting back to the routed slug. A managed service restart also attempts
this sync shortly after the proxy binds. If that best-effort boot sync fails, for example during an
offline login, the previously persisted catalog is retained and the next successful ocx sync
reapplies the configured name. Genuine upstream native names (e.g. gpt-5.6-sol →
“GPT-5.6-Sol”) come from the pinned upstream snapshot and are never overridden by a custom display
name.
External provider managers
Section titled “External provider managers”If config.toml already selects a provider other than openai or opencodex, OpenCodex leaves the
file unchanged and skips profile writes, catalog/cache refresh, and both immediate and background
Codex history metadata restoration. Tools that manage a custom provider often tag existing sessions with that
provider id; replacing the active id can make those intact sessions disappear from Codex’s history
view. The same protection applies to an external provider selected by a legacy root profile.
While an external provider owns config.toml, the settings report that
GET /api/settings and ocx system settings --json return a description of the Desktop authless and
client-compaction switches — and the Codex sign-in requirement — as controlled by that
provider instead of showing the effective state OpenCodex would produce. Flipping either
switch still stores the preference, but config.toml is not rewritten; the stored value
takes effect if you switch Codex back to a provider OpenCodex manages and rerun ocx start.
If config.toml exists but cannot be read (permissions, or a delete racing the read), the same
report withholds the effective values and the sign-in answer as undetermined instead of showing
the state OpenCodex would compute locally. The apply record keeps that explanation — including on
a save whose injection gate is already closed by a disabled integration or a stopped proxy — and
marks it retryable, so a later ocx system settings --json read reports the settled answer once
the file is readable again. With Codex integration disabled, ocx sync is catalog-only and cannot apply it.
Keep one tool as the owner of Codex provider configuration. To use OpenCodex behind an existing
provider manager, point that provider at http://127.0.0.1:10100/v1 with Responses passthrough
(wire_api = "responses" in Codex TOML), not Chat Completions translation. When proxy API auth is
enabled, also pass x-opencodex-api-key from OPENCODEX_API_AUTH_TOKEN, matching the non-loopback
provider form above. To let OpenCodex inject routing directly, first switch Codex back to its
built-in openai provider and remove any user-owned root openai_base_url, then rerun ocx start.
Explicit tool_search troubleshooting
Section titled “Explicit tool_search troubleshooting”Routed local tooling has two distinct discovery paths. In normal routed code mode, Codex can expose
deferred MCP/app tools through the official exec tool’s tools global and ALL_TOOLS; that path
does not require the model to see or call tool_search.
Separately, tool_search is a client-executed Codex discovery surface. It is not an OpenCodex
feature flag, and an upstream tool_choice: "auto" value does not create or enable it. OpenCodex can
relay the explicit surface only when Codex already included a declaration like this in the incoming
Responses request:
{ "tools": [ { "type": "tool_search", "description": "Load deferred tools" } ]}For routed chat/local models, OpenCodex exposes that declaration as a normal function named
tool_search. If the model calls it, OpenCodex converts the call back to a Responses
tool_search_call; Codex executes the search and supplies the resulting tool definitions in a
later tool_search_output. Definitions loaded that way are then available on the next model turn.
Check the failure boundary before changing provider settings:
- No
type: "tool_search"in the incoming request: the active Codex client/session did not advertise the explicittool_searchsurface. OpenCodex cannot invent that declaration. This does not mean normal code-mode tools are unavailable: check whether the routed model can useexecand discover the needed nested tool throughtools/ALL_TOOLSfirst. - The incoming declaration exists, but no
tool_searchfunction reaches the routed request: capture only the redacted tool-type/name list and open an OpenCodex bug. Never attach the bearer, account id, conversation input, full headers, or complete request body. - The routed request contains
tool_search, but the local model never calls it: the relay is working. Use a model/template with reliable function calling and instructions that explicitly tell it to search for a deferred tool it needs. LM Studio’stool_choice: "auto"permits tool use; it does not force the model to call this function. - A call is emitted repeatedly or loaded tools never become usable: capture the redacted
tool_search_call/tool_search_outputitem types and call ids. OpenCodex preserves both in history so the model should see the completed search instead of issuing it forever.
See The parser and bridge for the explicit wire mapping.
There is no provider-level setting that can add a missing tool_search declaration; ordinary
code-mode discovery remains a separate path.
Cache-read diagnostics
Section titled “Cache-read diagnostics”Set OPENCODEX_CACHE_DEBUG=1 before starting the proxy to write one diagnostic record per
finalized request to <config-dir>/cache-debug.jsonl. The switch is off by default; set it to 0
or remove it to disable capture. The file is owner-only (0600) in the hardened config directory
and rolls after 200 lines, retaining the newest 100.
Each JSONL record contains the protocol, routed provider/model, cache-counter presence and provenance, process-local equality tags for the account, prompt-cache key, and allowlisted session headers, plus ordered fingerprints for instructions, tools, and message/input blocks. Prefix sections retain at most 128 tags and identify only the first divergent section/index. The diagnostic never stores prompt or message text, tool names, raw headers, raw cache/session/account identifiers, or a durable tag derived from them. Its random HMAC key is created at process start, separate from other debug keys, and is never persisted; tags therefore compare values only within one proxy process.
Catalog troubleshooting
Section titled “Catalog troubleshooting”If a model is missing from Codex, or the catalog order/visibility looks wrong, check in order:
selectedModelson the provider — a non-empty allowlist exposes only those ids to Codex; empty or omitted exposes all discovered models. An id not in the allowlist never reaches the catalog.disabledModels(top level) — hides models from both the catalog and/v1/models, and flips bare native GPT slugs tovisibility: "hide".liveModels: false— WithliveModels: false, an empty or omittedmodelslist seeds the configureddefaultModelfirst, followed byretainModels; duplicate ids are removed while preserving first occurrence. A nonempty explicitmodelslist instead seedsmodelsfollowed byretainModels, without implicitly adding a differentdefaultModel. That default can still be listed explicitly inmodelsorretainModels. If none of these fields supplies an id, the static seed is empty. This is seed order, not a promise of final picker order.selectedModels,disabledModelsand provider-disabled policy still apply.authMode: "forward"keeps its separate branch and does not use this routed static seed. These rules do not change live-discovery failure fallback.- Cursor
GetUsableModels— the Cursor adapter discovers models through its protobufGetUsableModelsRPC, not/models, so a Cursor-side change can alter which ids are visible independently of other providers. - Cache and
ocx sync— live catalogs are cached for about five minutes (modelCacheTtlMs, default300000). Runocx syncto force a fresh fetch and rewrite the catalog immediately. - Running Codex
app-server— rewriting the on-disk catalog is not enough while a long-lived Codexapp-server(Desktop / CLI background host) keeps the previous list in memory.ocx syncandocx sync-cachewarn when those processes are detected.ocx sync --restart-codexrestarts those processes and fully quits and relaunches the Codex desktop app on macOS, Linux, and Windows so the picker re-reads the catalog. To leave the desktop app running, pass--restart-app-server-onlyor stop the matchingapp-serverprocesses yourself.
Proxy connection errors
Section titled “Proxy connection errors”If Codex retries and then fails with an error like
stream disconnected before completion: error sending request for url (http://127.0.0.1:10100/v1/responses)
— or Claude Code reports a similar connection failure — the opencodex proxy is not
running: nothing is listening on the configured port, so the client renders that raw
connection error itself. Restart the proxy:
ocx start # foregroundocx service install # persistent: auto-starts on login and respawns on crashocx status shows whether the proxy is running and prints the same restart hint when
it is not; ocx doctor reports restart safety (service/shim coverage).
Upstream overload before any output
Section titled “Upstream overload before any output”When the ChatGPT backend declines a pooled native Codex turn with server_is_overloaded or
another server error before it has produced any output, opencodex retries the same account
with a short, capped backoff and then moves the turn to one other eligible Pool account.
A pinned account (an account-qualified model) is retried but never moved. Once any output
has reached Codex the turn is never sent again. If every attempt is declined, Codex receives
the same error it would have received without the retries. Native compact requests move to
another Pool account on a server error in the same way.
Runtime agent instances (Codex 0.164+)
Section titled “Runtime agent instances (Codex 0.164+)”Codex 0.164 can sign each request with a runtime agent assertion when
features.runtime_agent_instances is enabled (it is off by default). opencodex supports one of
its two modes:
mode = "dual_header"keeps the normalAuthorization: Bearercredential and adds anx-openai-agent-assertionheader. opencodex treats the request exactly as before. The assertion is forwarded only when the turn goes to ChatGPT Direct with your own credential. It is removed when a Pool account, a stored main credential, a routed provider, or a sidecar serves the request, because it belongs to the credential Codex sent.mode = "assertion_only"replaces the bearer withAuthorization: AgentAssertion <jwt>. opencodex refuses these requests with HTTP 400 and error codeagent_assertion_unsupported. It never substitutes a Pool or provider credential for the assertion, and it never forwards or logs the assertion. Usedual_header, or turn the feature off, when Codex runs through opencodex.
Codex autostart shim
Section titled “Codex autostart shim”Run ocx codex-shim install to install the optional launcher wrapper. It runs
ocx ensure before ordinary Codex launches and then forwards the original arguments
and exit status. The command also works with the standalone ocx shipped in desktop
packages: both the installation probe and the installed wrapper use that executable,
without requiring a separate Bun installation or a source checkout.
On macOS and Linux, activate the private wrapper in your current shell after other PATH setup. With the default OpenCodex home, run:
. "$HOME/.opencodex/codex-shell-env.sh"With a custom OPENCODEX_HOME, use the quoted path printed by ocx codex-shim install.
Add that source line after other PATH setup in your shell startup file to activate future shells.
OpenCodex does not edit that file or change the parent shell. Legacy in-place Unix shims are not
auto-migrated; run ocx codex-shim install explicitly to migrate them.
Use ocx codex-shim status to inspect it. On macOS and Linux, ocx codex-shim uninstall
removes the private overlay files and leaves native Codex intact. Windows keeps in-place
script wrappers; uninstall restores the saved launcher. See the
CLI lifecycle reference.
Experimental Windows desktop compatibility
Section titled “Experimental Windows desktop compatibility”Codex Set → Desktop compatibility provides local controls for a bounded compatibility trial while retaining the native Codex login. This is an experimental relay, not a guarantee that every Codex build or exhausted-account state can recover its composer.
- Inspect status, prepare the encrypted certificate and review its fingerprint before
registering trust. Windows may ask for confirmation. Trust applies to the current Windows
user and allows the local relay to handle
chatgpt.comtraffic. - Start observation. Save drafts, close Codex, then use Open Codex in this panel to launch the packaged app with the managed connection. The panel never forcibly closes an existing app.
- Only after eligible exhaustion is observed, Run 3-minute trial offers a separate account-wide consent. It adjusts two UI flags across the signed-in account, not just the selected model. Actual usage, credits, spending restrictions and server limits remain unchanged. A produced response alone does not establish that the composer recovered.
An original response showing available usage or a protected state ends the trial immediately. Available credits or active Luna Reserve also leave the response unchanged and end the trial, even when ordinary usage is exhausted. This preserves the app’s existing entitlement state. If eligible exhaustion appears again, another explicit trial is required; the earlier consent does not silently reactivate correction.
Return to observation disarms correction. The three-minute limit bounds response correction,
not the time until Codex refreshes its display. Expiry and returning to observation request a
fresh usage response; the app may retain its previous display until that response arrives.
The panel shows this limitation before trial consent and while awaiting the original response;
it cannot confirm the app’s cache refresh. Stop service closes its connections and leaves
the certificate for reuse; active connections may be interrupted. For renewal or trust removal,
stop the service and close Codex first. Renewal prepares a new untrusted certificate; review the
new fingerprint before registering it. An uncertain action is not automatically retried.
Certificates are restricted to TLS server authentication. If status shows renewal-required,
stop the service and close Codex, then renew the older certificate before starting again.
The native API URL must match the proxy’s actual listener address and port. IPv4 and IPv6
loopback addresses are not interchangeable here; ambiguous localhost aliases are refused.
Codex’s automatic MCP connection refresh and configuration formatting do not invalidate
observation. Model, authentication, profile and routing changes still require a fresh
observation context; the service does not silently adopt those changes during a trial.
The service preserves its PAC address and local connection ports for reuse after a restart.
With the same trusted certificate, an already configured app can reconnect through its cached
PAC; restarting the service never resumes a correction trial. If a saved port cannot be bound
or connection metadata is invalid, startup refuses rather than silently assigning another
address. The first successful start owns codex-desktop-compatibility/connection.json under
the OpenCodex home. Do not delete that file as a routine restart fix: a different address
requires launching the app with a fresh managed connection.
Current support is limited to the assessed Windows build 26.924.2738.0, native file-based
login. HTTP/HTTPS proxies and authenticated SOCKS5 proxies are supported through the shared
outbound policy; NO_PROXY can explicitly select a direct route. Failed proxy connections do not
fall back to direct egress. OpenCodex managed client mode is not yet supported by these controls.
For this desktop relay, HTTP(S) ALL_PROXY also applies when HTTPS_PROXY is unset. An invalid
HTTPS_PROXY refuses the operation instead of falling back to ALL_PROXY.
Unknown app builds refuse correction; the integration does not patch application files or
switch to login-free mode.
The native root config and its selected profile must route openai_base_url to this process’s
actual local data listener. A different provider, destination or unsupported override refuses
observation startup. This check does not determine a conversation’s selected model or prove
that project-local overrides are absent. If routing or the installed app build changes during
a trial, correction is refused before the next usage response; periodic checks also return
an idle trial to observation. Original responses and other app traffic continue to relay.
Native login-file replacement or token rotation also invalidates the trial, even when the
same account returns. Stop and start observation to bind the current login before another
explicit trial; old responses cannot authorize the replacement session.
Changes to the root Codex config (except its MCP server table and formatting), provider configuration, model aliases, routing profiles,
fallback, compaction or memory-model routes also invalidate the observation context. Restoring the previous
settings does not restore its consent: stop and start observation, then confirm a new trial
when eligible. This detects changes to those configured routes, not which model a conversation
actually selects; an unchanged mixed-provider configuration is not proof of provider isolation.
After certificate preparation and a successful manual start, enable Resume observation when
OpenCodex starts in the same panel. The optional OpenCodex configuration
"desktopCompatibility": { "startOnProxyStart": true } resumes observation only when the model
proxy starts. Omission or false disables this automatic start. It reuses the saved endpoints
and trusted certificate, never launches Codex or enrolls trust, and never resumes an Apply trial.
Automatic observation waits for startup configuration sync to finish. Failed sync or a wait
longer than two minutes leaves observation off; inspect its status and start it manually after
resolving the startup problem. The model proxy continues to follow its own readiness policy.
If prerequisites are missing, observation stays off and the model proxy continues running.
Saving this preference does not start or stop the current service. Use its separate service
controls for immediate changes. If a save cannot be confirmed, refresh its status before retrying.
A running service does not establish that Codex is connected through it. A normal app launch or an app updater can omit the managed connection argument. Save drafts, close Codex, then use Open Codex in this panel to restore that connection. This control never closes the app for you. After a Codex update, an unassessed app version refuses correction until its compatibility is reviewed; reinstalling the certificate does not make an unassessed build supported.
The panel can also report the last completed build check as diagnostic metadata: the package
version this build was assessed against, the package version observed at that check, whether
they matched, and when the check finished. It is a past observation, not a live reading of the
installed package. After the service stops, the app updates, or a check is still running or was
interrupted, it can lag the actual installation; only a completed probe refreshes it, and a
verified result describes the package observed at that check, not whatever is installed now.
On Windows, an explicit OpenCodex full-app restart preserves an already active loopback compatibility PAC argument only when the current OpenCodex process owns the serving compatibility runtime. A stale or foreign endpoint is refused before stopping Codex; replacement of the runtime during restart also prevents managed relaunch. The app launches through Windows package activation. It checks the package identity and routing argument after launch; unreadable or conflicting main-app routing arguments cause a refusal before the restart. This does not enable a compatibility mode, install a certificate, or watch and restart the app automatically. Normal launches without that routing argument keep their existing behavior.
Transport coverage and recovery evidence
Section titled “Transport coverage and recovery evidence”The compatibility panel reports identity-checked JSON/SSE usage responses and currently bound streams separately from open connections. These are lifetime relay observations, including requests made by diagnostic clients. They do not identify the sending native process, prove that the app updated its authoritative cache, or prove composer recovery. No request body, token, account ID or source IP is added to this diagnostic status.
Issue #6196 reports a macOS build whose authoritative gate requests originate in the bundled Rust app-server and bypass Chromium PAC settings. A healthy PAC, certificate or synthetic request cannot establish coverage of that transport. This Windows experiment is not a fix for that macOS design blocker.
Before adding another platform or transport, document and validate this sequence:
- Identify the actual process and transport that sends each authoritative gate request.
- Prove that an app-origin request reaches the intended relay, separately from test clients.
- Verify the response schema and the app’s cache/update path for the exact installed build.
- On natural account exhaustion, verify original-composer submission, independent-provider completion, and preservation of sign-in, Chat, existing threads and remote connections.
- Verify shutdown, ordinary launch, restart and update behavior; do not infer one from another.
The present policy handles only /backend-api/wham/usage and its /stream endpoint.
Conversation initialization and other endpoints, including their blocked_features and
limits_progress, pass unchanged. Their presence is not permission to clear them: they may
represent unrelated restrictions. The Windows build’s renderer-to-Electron fetch path does
not establish the network path of a different OS/build or every app-server RPC. App-server
routing requires its own reviewed design; changing model-provider URLs alone is not proof
that account, login or remote-control traffic follows the same route. No automatic app-server
wrapper, global proxy change or login rewrite is installed by these controls.
Routed models during Codex reserve mode
Section titled “Routed models during Codex reserve mode”Codex Pool can optionally protect stored pool accounts at a selected 5-hour or weekly usage
threshold. The Desktop/main account keeps its separate 98% hard lock.
Set codexPool.lowQuotaProtection in configuration to pause accounts, record a log-and-API
alert, or both; see routing configuration.
A pause takes effect for the next selection immediately, while saving it to disk is deferred.
Pool quota responses can authorize a pause only with valid usage and a captured credential
that is still current. A response without that proof may refresh displayed usage without pausing an account.
Check this server’s authenticated GET /api/codex-auth/low-quota-events history for logged
alerts or save failures. Manual resume remains in force for the current quota episode. The
default alert reaches only the log and API. Add actions.desktop: true to also show a macOS or
Linux notification on the machine running the proxy; Windows stays log-only.
When the ChatGPT 5-hour quota is exhausted, Codex may offer a reserve fallback model
(gpt-reserve / Luna Reserve). While that state is active, the Codex model picker can make
every other entry unselectable — including opencodex routed models, even though those
run on independent providers and credentials and consume none of the exhausted quota.
This is a Codex client behavior that ordinary model-proxy routing cannot change. The reserve state
arrives from the ChatGPT backend on the client’s own authenticated connection, not through
the proxy. The desktop app polls backend-api/wham/usage and treats reserve as active when
the response carries rate_limit_upsell.banner_type = "luna_reserve", the primary
rate_limit.allowed is false, and additional_rate_limits[] contains an entry with
limit_name = "gpt-reserve" that is still allowed. While that holds, the app forces the
conversation’s model setting to gpt-reserve and rewrites any other pick back to it — the
picker is collapsed to the reserve entry by the client, and a model = value in
config.toml is overridden the same way. None of this consults the model catalog, so no
representation on our side participates in the decision. opencodex has no reserve concept to
adjust, and the alternative — misreporting your own quota back to your own client — would be
a worse bug than the one it papered over.
Workaround: the models themselves stay fully usable; only the Codex app’s model selection is gated. Reach them from a client that does not consult the ChatGPT usage snapshot:
- Claude Code through the proxy (
ocx claude). - Any HTTP client against the local
/v1endpoint. - The dashboard’s own request paths.
Normal picker behavior returns when the 5-hour window resets.
What the proxy answers while that state holds. Once the app has collapsed the picker it sends
gpt-reserve, and without the authless Desktop opt-in opencodex
holds no Reserve entitlement to send with it. That request used to be forwarded as an ordinary
native model and come back as the upstream’s own The usage limit has been reached, which names
neither the real cause nor the setting that would change it. It is now refused locally with an HTTP
400 that says codexDesktopAuthless is off and gives the command that turns it on
(ocx system settings --desktop-authless on). Nothing is sent upstream and no quota is spent.
The refusal is deliberately narrow. It applies only to gpt-reserve on a loopback-admitted request
that would reach the canonical ChatGPT forward route. A gpt-reserve selector an operator has
aliased or routed onto another provider keeps working, a request admitted on a non-loopback listener
still forwards, and a proxy running in the client runtime role is unchanged. Enabling the opt-in
restores the normal Reserve path rather than the refusal.
The subagent picker
Section titled “The subagent picker”Catalog sync makes the selected sub-agent models available to Codex; see Codex App model picker for picker ordering and Sub-agent Surface for v1/base/v2 delegation and fallback behavior.
Codex account warmup
Section titled “Codex account warmup”When a ChatGPT account is added or reauthenticated, OpenCodex normally verifies it before saving with a small streaming request to the Codex Responses backend. It waits for response.completed, defaults to gpt-5.6-luna, and retries with gpt-5.5 on HTTP 400 or HTTP 404. Public errors contain fixed failure categories rather than raw upstream response bodies.
An HTTP 429 from an attempted warmup is reported as codex_warmup_rate_limited. Retry after the temporary restriction clears or the usage limit resets; signing in again does not reset these limits. A failed attempted warmup does not add a new account or replace existing credentials. This differs from quota-confirmed deferred registration below, which can save a restricted account without a model request. HTTP 401/403 failures retain the authentication guidance.
If the new OAuth credential’s authenticated usage lookup confirms an exhausted 5-hour, weekly, or monthly quota, the account is saved without this model request and shows Validation pending. It cannot serve pool requests, even after a restart or token refresh. Once quota recovers, Refresh quotas finishes validation: a fresh, complete usage reading with headroom permits one small model request, and only a completed response enables the account. Failed or incomplete readings and failed validation preserve the restriction. Passive account polling does not trigger deferred validation. Unknown usage during initial registration retains the normal warmup gate.
ocx account refresh openai and ocx account list openai --quota --refresh only read usage. Model validation spends quota and requires a human dashboard session: open ocx gui and click Refresh quotas after recovery. For a headless host, access its dashboard from your browser; an admin token alone does not authorize validation. Validation can complete while an account is paused without resuming or selecting it. Model authorization failures remain visible until successful validation or reauthentication clears them.
In Codex Set → Multi-auth, enable the Codex credits switch in the Codex Auth header to display each main and pool account’s latest observed credits directly below Week. It is off by default and persists as showCodexCredits. The balance is a locale-formatted number, with Unlimited or an overage warning when reported; the bar indicates availability, not a percentage, because no total credit limit is supplied. Hiding credits changes display only, and a new login waits for its own observation.
When an account reaches 100% on a usage window and still holds credits, upstream keeps serving it and draws the balance. OpenCodex does not let that happen by default: an account at 100% is switched out while its weekly or monthly window (only monthly on 30-day plans) or its 5-hour window is full, and used again once that window resets. The order of the other accounts does not change, and when no other account is available, selection finds none rather than spending credits. A request for the main account is refused like a hard-lock refusal until the reset. A full 5-hour window without a reset time holds an account only while that reading is fresh. Allowing the main account does not lift its hard lock (on by default at 98%), which still stops it first; turn the lock off if the main account should spend credits (a lock at 100% still stops it at 100%). To let accounts keep working from their credits, turn on Use credits next to the Codex credits switch in the Codex Auth header. The switch allows every account. To choose accounts one by one, open an account card’s ⋯ menu and use Use credits after limit there; the header switch shows a middle position when only some accounts are on. An account allowed to spend carries a Uses credits badge on its card. New accounts start off. The choice is stored in creditCodexAccountIds and never redeems reset credits; the Codex credits display switch only shows balances and never changes routing.
For an opted-in, unpaused main account with a full usage window, the background recovery cycle renews spendable credit observations from three minutes of age, even while the dashboard is closed. It prepares a valid token before the authenticated usage lookup so the balance can be observed again before its five-minute freshness limit. Missing, zero, restricted or retracted credits keep the account eligible for recovery lookups under the existing retry backoff. Requests remain refused until a fresh positive balance or unlimited credits are observed; has_credits alone or approximate message counts do not prove a spendable balance. With consent on, this credit-evidence refusal’s Retry-After is at most five minutes, or a sooner included-quota reset; without consent it still follows the included-quota reset. A failed or incomplete lookup does not refresh old balance evidence. The separate main-account hard lock, needs-reauth quarantine and upstream retry delays still apply. This metadata lookup does not send a model validation request or set or clear the account’s needs-reauth state.
Background revalidation is separate and off by default. It requires Token Guardian, the openai provider’s proactive refresh policy, and tokenGuardian.codexWarmupEnabled. It skips accounts awaiting deferred registration validation.
After a ChatGPT plan upgrade
Section titled “After a ChatGPT plan upgrade”Refresh the main account’s quota in Codex Auth after changing your ChatGPT plan. When the authenticated usage response confirms a long window has replaced the five-hour window, opencodex removes the old five-hour reading from both request admission and account selection. An allowed response with primary and secondary windows can omit the optional tertiary window. This recovery uses your included plan allowance and does not require enabling Use credits. Partial or invalid usage data retains the previous block until recovery is confirmed.
Cancelling main-account device reauthentication
Section titled “Cancelling main-account device reauthentication”When cancelling main-account device reauthentication, a temporary DELETE or network failure, or a response with an unknown or nonterminal status, keeps the active flow and the cancellation-failure indication so cancellation can be retried. Status polling normally continues so a login that completes can still be detected. If a retryable cancellation failure overlaps a non-2xx GET status response while the flow is pending or committing, either response order preserves cancellation retry on that same flow, with its last server-provided device code, verification URL, and phase. The GET HTTP failure still stops polling, but cancellation can be retried without starting a second login POST. A terminal failed response releases the flow and displays the normalized failure reason; only succeeded reports login success. A confirmed cancelled response releases the flow so a new device login can be started. A definitive HTTP 404 response with code unknown_flow also releases the expired flow ID so a new device login can be started, but does not report a successful login or confirmed cancellation. Late POST, GET, or DELETE responses from an earlier flow cannot change the new flow or report login success for it.
Why an account stopped serving requests
Section titled “Why an account stopped serving requests”When an account leaves pool selection, the reason travels with the decision instead of being recomputed for display, so a surface can never report an account healthy while routing is dropping it. GET /api/codex-auth/accounts carries reauthReason next to needsReauth on each account: missing_credential for a credential that was never stored, refresh_failed for a credential refresh that keeps failing, and quota_unauthorized when the usage lookup itself was rejected.
A main-account refresh that does not complete still answers 503 with Retry-After, because a retry may still succeed. The message now adds that a failure which persists means the main account needs reauthentication, rather than only asking for another attempt.
Optional idle-window steering
Section titled “Optional idle-window steering”codexPool.startIdleWindows is an optional boolean and defaults to false. When enabled, a new
unbound real request may be placed on an eligible account whose observed short quota window is at
0% with evidence that its five-hour clock has not started. This check runs after conversation and family affinity, so an existing
binding remains authoritative; an explicit account pin or manual account preference also wins.
Independent model quota scopes are skipped, and the shared active cursor is unchanged. After the
idle-window choice, normal strategy selection resumes for other conversations.
The observation must be no older than five minutes, and the short window must explicitly be
18,000 seconds. Its reset must be observed within 60 seconds of observation + 5h (rather than an already ticking or elapsed reset). A synchronous,
process-local reservation prevents duplicate selections for the same account and window. The
reservation deadline is at least 5h plus one minute after selection; a fresh observation is
required after that deadline before the same account can be steered again. Reservations are cleared when the proxy
process restarts and are never persisted to disk.
Enable it in your existing configuration:
{ "codexPool": { "startIdleWindows": true }}If no account meets the criteria, ordinary routing applies. This feature only steers an actual incoming request. It creates no synthetic request and no timer.
Keeping a downgraded account out of rotation
Section titled “Keeping a downgraded account out of rotation”codexPool.excludedPlans lists plan keys that automatic pool selection skips, matched case-insensitively against the plan stored on each account. It is absent by default, so an existing install rotates exactly as before.
ocx config set codexPool '{"excludedPlans":["free"]}'This is a selection policy, not a block. An excluded account keeps its credential, quota history, and thread affinity, stays visible on the account surface, and is still reachable by explicit account selection such as work/gpt-5.5. What changes is that automatic rotation stops choosing it, including when it is already the active account or already bound to a thread — which is the state a lapsed subscription leaves behind.
The main Codex account remains exempt from plan exclusion; selection-only routing does not read its fenced native credential. If every eligible pool account is excluded, automatic selection returns no account. Explicit account-qualified routes remain available and still enforce pause, authentication and model entitlement. The account card and CLI show the excluded routing plan separately from credential health. There is no minimumPlan setting because the plan names do not define a total order.
Restoring native Codex
Section titled “Restoring native Codex”ocx stop stops the proxy and any installed background service, then attempts to restore native Codex. OpenCodex removes verified routing artifacts and reports an incomplete restore when it cannot safely recover configuration files.
Recovery may require manual review when the journal cannot verify the current files; see recovery without injection hashes.
ocx stop # stop the proxy + service, restore native Codexocx restore # restore without stopping (alias: ocx eject)ocx restore back # point plain Codex at the running proxy againWhen opencodex runs as a managed background service, it sets
OCX_SERVICE=1 so a service-driven restart does not thrash the Codex config — only an explicit
ocx stop / ocx service stop restores native Codex.
Recovery without injection hashes
Section titled “Recovery without injection hashes”The journal saves the original config.toml and opencodex.config.toml plus hashes of the state
OpenCodex injected. A legacy journal or an interruption before those hashes were recorded cannot
prove that later file contents belong to OpenCodex. If either file differs from its saved original
and lacks its own injected-state hash, automatic journal recovery and native restore report failure
without changing either file or the journal. The saved original remains available for comparison;
review it alongside the current files before choosing a manual recovery action.
Files already equal to their saved originals are accepted without rewriting them. A missing file is distinct from an empty file. Verified injected hashes still allow normal snapshot restoration, and later edits in hash-backed configurations retain the existing owned-field cleanup behavior.
Sync and ocx restore back also reject an existing routed configuration whose hashless journal
does not match the pre-injection baseline. This prevents a new injection hash from being attached
to an older original. A genuinely native configuration can be saved as a fresh baseline before
injection. Explicit external-provider opt-out behavior is unchanged.
Sub-agent fallback and V2 compatibility
Section titled “Sub-agent fallback and V2 compatibility”In Subagents → Delegation settings, edit the ordered fallback chain and its availability polling interval (5000–600000 ms), then save it separately from the featured roster. A configured target that is no longer advertised remains in the chain until you remove it. The roster and fallback chain are separate settings; this editor does not make the roster replace the fallback policy.
When a routed preferred model may receive V2 work from a native ChatGPT parent, the panel explains the upstream encrypted-task limitation. Readable tasks from routed parents are unaffected. The guidance uses /api/v2 mode and native V1 pin state; the current API does not expose recovery activation or request-specific eligibility, so the panel reports those as unknown. V1/plaintext-compatible delegation remains an alternative. Experimental V2 recovery, where eligible and explicitly enabled, adds quota usage, latency, backend dependence and possible fidelity loss; it does not repair the upstream protocol. See sub-agent surfaces and the upstream limitation.
Paginated history safety refusal
Section titled “Paginated history safety refusal”When an affected history store supports paginated records, a provider transition may return history_paginated_requires_native_writer. That reason no longer refuses the Codex configuration, the reference profile, or the model catalog. ocx sync and ocx start still write those files and set model_catalog_json, so the Codex model picker keeps showing every OpenCodex-routed model. Only this one reason stands the conversation-history relabel down, because Codex allocates paginated rollout ordinals in its own writer and no retry changes that. Any other history preflight reason — an unreadable state database, a rollout whose identity changed, or a preflight that could not run — still refuses the whole transition and rolls it back, because those may succeed on a later attempt. OpenCodex never modifies paginated rollout files or thread rows in this state. Existing conversations keep whatever provider they are already tagged with and are not migrated; new conversations route through the proxy normally. When the relabel stands down, a [model_providers.opencodex] table that the home already had is kept rather than retired, even in the root-override (loopback) form, so conversations whose rows are tagged opencodex keep a provider id that still exists. This includes legacy rows in a migration-capable store. The CLI prints Codex resume history: left to Codex's native writer (history_paginated_requires_native_writer).
When returning to the root-override form, OpenCodex retains an existing [model_providers.opencodex] definition before committing the configuration, even if history preflight currently passes. This keeps older opencodex conversations resolvable if Codex migrates history after that commit or while the background worker starts. New conversations still use the selected root provider; explicit restore keeps its separate removal guards.
ocx restore, ocx stop and ocx uninstall no longer refuse on history_paginated_requires_native_writer. They take every OpenCodex root routing key out and keep the [model_providers.opencodex] definition on disk, so conversations whose rows still name that provider keep resolving while plain codex stops pointing at the proxy. The result is reported as a partial restore that names the retained lines, and ocx restore --remove-codex-provider-table removes them too, after which those conversations stop opening.
Enabling the integration in its provider-table form on a home whose openai-tagged conversations Codex has already paginated used to be refused outright with history_paginated_openai_requires_native_writer: nothing was written and the integration stayed disabled. OpenCodex now completes that transition by keeping the managed root openai_base_url override beside the [model_providers.opencodex] table. Codex merges the override onto its built-in openai provider, so those conversations keep reaching the proxy without being relabeled and no rollout byte or thread row is touched. Only a routing form that requires the x-opencodex-api-key admission header still refuses, because Codex’s built-in provider cannot carry that header; its message names the two settings that resolve it — route Codex through the loopback listener so the override can be retained, or set syncResumeHistory to false to accept that those conversations resume against Codex’s own OpenAI endpoint.
Remote thread-list provider filters
Section titled “Remote thread-list provider filters”Provider-table routing changes the default provider id for new conversations to opencodex while
history that cannot safely be relabeled may remain tagged openai. Some native app-server/mobile
versions treat an omitted thread/list.modelProviders filter as the current default provider only,
so those existing conversations can disappear from that remote list even though their database row
and rollout are intact. A compatible list client can send modelProviders: [] to request all
providers. OpenCodex cannot rewrite that RPC because the remote client talks directly to Codex’s
native app-server rather than the inference proxy.
ocx sync and ocx start include the warning when they apply a provider-table route. If a
user-owned root URL sends the command down the no-routing branch, the CLI omits the warning. In
client-compaction mode, the CLI can retain that URL, apply the opencodex provider table, and
include the warning. The dashboard shows a separate preference hint when either setting is enabled.
It appears once if both settings are enabled, regardless of the root URL. The hint reports enabled
preferences; it does not mean Authless Desktop is effective on the current route. Authless Desktop
applies only to effective loopback authless routing and is ignored for remote-client routing or
listeners that require an admission header. The warning is not a migration: OpenCodex does not edit
provider tags merely to influence a client-side list filter. Verify the conversation in native Codex
and the app-server/client version; do not rewrite paginated history to make a remote list include it.
Do not rewrite an active paginated rollout or thread row to migrate those conversations yourself. Close the affected conversation before any recovery, and report the exact error and versions without uploading private history. A backup or a successful script alone does not prove the conversation is visible again. Check the restored conversation in Codex after reopening.
Experimental native mid-turn steering
Section titled “Experimental native mid-turn steering”For a compatible model on the canonical ChatGPT forward route, and a client
that sends response.steer, enable both options in ~/.opencodex/config.json and restart
OpenCodex before starting a fresh turn:
{ "websockets": true, "codexNativeSteering": true}Merge these keys into the existing configuration; do not replace your provider/account settings. This option is off by default. It forwards steering to the same explicitly configured native WebSocket connection and selected account, preserving automatic successor responses and pending saved-tool-result continuations. Acceptance means queued, not yet applied.
Supply the required tool results or approval decisions once per parent, on the same lane.
Results can arrive before response.steer.pending: the relay also matches the completed
parent’s advertised calls and approvals. A name on a pending function-output stub is
optional on the result, as in the native schema. Additional user messages may accompany
these results; system/developer messages, duplicate results and unrelated call IDs are refused.
Do not rerun tools or resend accepted steering text. Model, account, tool declarations and routing stay unchanged. Validated generation settings
may change in an explicit saved-result continuation as described below. Other changes
require an explicitly stopped or finished turn and normal new dispatch. Multiple independent conversations use independent connections.
HTTP fallback, noncanonical gateways, public API-key routes, translated models, sidecars, Combo attempts and plaintext V2 restoration do not support this option. It does not add steering capability to a model or a client that lacks it. Unsupported routes return a protocol error rather than silently ignoring input. Disconnected or timed-out delivery may be unknown: never automatically resubmit tools or steering text. Pending controls have per-submission absolute 90-second confirmation deadlines; saved-tool-result waits have a 30-minute cap.
The implementation has synthetic protocol and regression coverage, not live Astra/client
certification. Keep the option disabled for production work until your client/model path
has been verified. Set codexNativeSteering to false and restart to restore the existing
single-response relay; no account or conversation files need to be deleted.
Steering confirmation deadlines and retained context
Section titled “Steering confirmation deadlines and retained context”Each submitted steer has a fixed 90-second acknowledgement window. Other output and additional steers do not extend it. Once accepted, the input remains queued while the current response reaches a safe boundary; ordinary stream-idle checks still apply. After the response ends, the successor must begin within 90 seconds. A request for tool results or approval allows 30 minutes from the first such notification. Repeated notices do not renew this wait. Submitting saved results starts a new 90-second successor window, including local pacing/auth checks. Missing acknowledgements remain subject to their earlier individual deadlines.
The owned connection itself has no absolute lifetime cap. Up to 128 responses may share it, and each may legitimately consume its own acknowledgement, successor, stream-idle and required-input waits, so the per-stage deadlines above compose to a worst case on the order of tens of hours. During that time the turn holds one physical socket and one pinned credential that cannot rotate, because the channel deliberately never re-enters account selection. Treat an enabled steering connection as a long-lived session resource rather than an ordinary bounded request.
Steering frames also share the proxy’s configured body and memory limits. A control frame above
maxInboundBodyBytes is refused before
it is parsed on an established control connection (an initial frame is still parsed before
its type-based limit applies), and the reconstructed body sent upstream is refused when it exceeds
maxUpstreamBodyBytes. Each
connection’s replay journal is capped at 32 MiB and counted as pinned state against
appOwnedMemoryBudgetMb; admitting a
journal demotes evictable caches first rather than failing, and the aggregate across live journals
is capped at 128 MiB regardless of the configured budget.
A timeout means delivery is unknown, not that the server rejected the input. Do not resend an accepted instruction or rerun a tool automatically. Inspect the actual task state before deciding how to resume. No account switch or paid API fallback is performed. Completed output already received on the wire is retained for local continuation history even when the terminal summary omits it. Conflicting item content or order causes an explicit failure rather than silent context loss.
For a live comparison, use the same supported client version, model and account in isolated test conversations, once without the proxy and once with it enabled. Use a read-only task, steer while output is active, and compare acceptance and the successor’s actual instruction adherence. Repeat while a synthetic tool result or approval is pending and after an explicit disconnect. Record only event types, relative times and redacted outcomes, not credentials or task bodies. Passing mock transport tests does not establish live client/backend support; no real-account smoke test is implied by these instructions.
Experimental native function-result injection
Section titled “Experimental native function-result injection”For a compatible client that sends OpenAI multi-agent response.inject messages,
merge these keys into the existing OpenCodex configuration and restart before a
fresh turn. Do not replace your provider or account settings:
{ "websockets": true, "codexNativeInjection": true}The initial response.create must explicitly include "multi_agent": { "enabled": true }.
OpenCodex does not enable it based on a model name. A public OpenAI API provider
must use adapter: "openai-responses", baseUrl: "https://api.openai.com/v1",
its normal API-key authentication and upstreamWebsocket: true. Route the initial
model through that provider’s configured prefix. The relay adds the required
responses_multi_agent=v1 beta token on that public API connection only, preserving
other configured beta tokens. It does not substitute a subscription credential,
create an API account or automatically switch to a separately billed API.
Canonical ChatGPT forward connections can opt into the same transport experimentally, but the public API contract does not establish ChatGPT subscription or Codex App/CLI support. A compatible upstream model and execution mode are still required. See the OpenAI multi-agent protocol.
Return a saved tool result after the matching developer function call has completed:
{ "type": "response.inject", "response_id": "resp_example", "input": [ { "type": "function_call_output", "call_id": "call_example", "output": "saved result" } ]}Use the response/call IDs from the same connection, not these example IDs.
response.inject accepts string-valued function_call_output only. User/system
messages, rich output arrays, hosted-tool results and simultaneous response.steer
are not accepted by that operation. The wider saved-result continuation below is
a separate response.create operation, not a hidden conversion of rejected injection. Multiple saved function results can share a
single injection. Each call can be submitted only once, including while queued.
Parallel tool results are queued and sent one frame at a time, since the success
event identifies the response rather than an individual injection. The relay
preserves response.inject.created and response.inject.failed. It keeps the
connection alive after a response terminal while submitted results await confirmation
or advertised calls await results, so late asynchronous results are not discarded.
When the server rejects an injection with response_already_completed, use its
returned saved outputs in one client-sent response.create with the completed
previous_response_id, unchanged model/settings and the same lane. Include each
outstanding result exactly once; do not include already accepted outputs. The
relay keeps that continuation on the original account/socket and preserves normal
request pacing. It never runs the tool again or creates a recovery request itself.
Other failures remain visible for the client to handle.
A missing acknowledgement or a disconnect means delivery can be unknown. Do
not automatically resend a result, restart a tool or change accounts to retry it.
The pending queue is limited to 32 frames and 8 MiB, with 1,024 advertised function
calls, a 32 MiB replay journal and at most 128 responses per owned connection.
Injection journals share the pinned memory budget and 128 MiB aggregate ceiling
with steering journals; see steering memory limits.
The configured upstream body limit is checked before a result enters the queue,
even while another result awaits acknowledgement. An outbound_body_too_large
refusal leaves the connection usable for a corrected result without rerunning its tool.
Each sent injection has a 90-second acknowledgement deadline that unrelated output
cannot extend; a saved-result wait is limited to 30 minutes. Existing frame limits
and stall timeouts still apply.
Translated providers, custom gateways, Combo/sidecar paths and HTTP fallback do
not gain injection support. Unsupported attempts return an explicit error instead
of disappearing. The option stays off by default; synthetic transport tests are
not live compatibility certification. Set codexNativeInjection to false and
restart to roll back. No account or conversation files need to be removed.
Rich tool results and explicit approvals after response completion
Section titled “Rich tool results and explicit approvals after response completion”With codexNativeInjection enabled, a client-sent response.create on the same
owned connection can now return unsent function/custom results containing text,
image or file parts after response.completed. Supply the completed response’s
previous_response_id, the same lane and unchanged model/settings. Include every
outstanding result or requested approval exactly once; omit already accepted
injected results. The proxy forwards this caller-sent continuation using the
original account and socket with the existing dispatch checks.
Supported continuation items are function_call_output, custom_tool_call_output
and mcp_approval_response. Tool output may be a string or an array of input_text,
input_image and input_file parts. Image parts require detail (auto, low,
high or original); file detail is optional (auto, low, high). Use exactly
one image/file source. Inline file data requires a filename. Optional
prompt_cache_breakpoint: { "mode": "explicit" } is preserved. Unsupported fields
are rejected, not removed. References are not downloaded or reuploaded by the proxy.
Each result has at most 1,024 content parts within the existing 8 MiB request limit.
A supplied program caller must match the advertised call; it cannot impersonate
another tool or agent. Content order, file references and original spelling survive.
For a server-issued mcp_approval_request, pass its ID as approval_request_id and
an explicit approve: true or approve: false. A refusal is forwarded unchanged.
The proxy does not decide, default, auto-approve or execute the requested tool.
A missing or unrelated decision is rejected. Hosted multi_agent_call actions and
other server-run tools are not developer functions: their events, outputs and
encrypted agent messages are preserved, never executed or injected by OpenCodex.
This does not enable rich/custom/approval mid-response injection, nor simultaneous steering on a multi-agent response. Those operations have different upstream contracts. Unsupported injection is refused before reserving a call, so an unsent result remains available for a later explicit continuation. There is no automatic conversion, retry, tool rerun or account/API switch. A single-agent steering turn can follow a completed multi-agent turn as a new explicit request using ordinary routing. Client support and backend entitlement still require live verification.
Steering continuation settings
Section titled “Steering continuation settings”An explicit saved-result response.create may override reasoning (effort and
summary), text (verbosity and supported structured-output format), and
stream_options. Subscription routes refuse a max_output_tokens override
instead of silently ignoring it. Normal provider pins, subagent caps, effort
mapping and summary/verbosity capability exclusions still apply.
Omitted settings retain the current effective values; explicit null resets that setting where the upstream accepts null. Overrides replace the supplied setting object, not individual nested fields. Changed values carry into later explicit continuations. A rejected override does not reserve the saved result, so a corrected request can be submitted without rerunning its tool. The server still decides which settings the chosen model accepts. Changes to model, account, provider, tools, instructions or service tier require a separate ordinary turn.
Native steering is restricted to the canonical ChatGPT subscription route. Public API-key and gateway routes are not steerable; their ordinary responses are preserved and a steering attempt receives an explanatory error. This prevents successor generations on a retained socket from bypassing normal per-request admission. The separately gated public API multi-agent injection path remains available.
Executable direct-versus-proxy wire probe
Section titled “Executable direct-versus-proxy wire probe”From a source checkout, run the offline positive control:
bun scripts/steering-smoke.ts --self-testPlan a comparison without reading tokens or opening any connection:
bun scripts/steering-smoke.ts --direct wss://api.openai.com/v1/responses \ --proxy ws://127.0.0.1:1455/v1/responses --model <supported-model> \ --proxy-model <provider-prefix/same-model>For a subscription comparison the direct URL is
wss://chatgpt.com/backend-api/codex/responses. Select the same actual model and
account on both routes; the script cannot prove that a proxy configuration selected
the same account. The proxy URL must be a loopback Responses endpoint and must not
contain credentials, query parameters or a fragment.
Only after reviewing the plan, supply STEERING_DIRECT_TOKEN and
STEERING_PROXY_TOKEN through your shell environment and add both --live
and --allow-model-requests. A direct ChatGPT connection may additionally need
STEERING_DIRECT_ACCOUNT_ID; that header is never copied to the public API or the
proxy. Do not put credentials in command arguments, logs, screenshots or PRs.
The script does not read your saved Codex login, refresh tokens or change settings.
Live execution sends four synthetic initial requests (two scenarios per route), plus any resulting successors or required-result continuations, and can consume model usage. One scenario checks an automatic successor; the other returns a fixed synthetic result only for the script’s own advertised function and changes reasoning/verbosity on its explicit continuation. No external tool is executed and no approval is inferred. There are no retries or automatic recovery requests. Each scenario is limited to 120 seconds, 5,000 events and 2 MiB received data.
The JSON report contains only outcomes, timing and boolean checkpoints. A pass
requires queued acceptance, a created successor and the synthetic marker in its
completed output. Missing confirmations are unknown; if the model never enters
the required-input path the result is not_exercised. Neither is counted as pass.
The process exits 0 only if all four live scenarios pass, 1 otherwise, and 2 for
invalid arguments or missing credentials. This is a wire diagnostic, not an
end-to-end Codex App/CLI interface test, live certification or instruction to enable
the experimental feature for production work.
Streaming line endings
Section titled “Streaming line endings”The shared SSE decoder accepts LF, CRLF and standalone CR line endings, even when a delimiter spans network chunks. This allows compatible providers to stream events without requiring LF-only framing.
Account-qualified requests and credits
Section titled “Account-qualified requests and credits”Choosing an account-qualified model does not enable credit spending. Stored accounts obey Use credits after limit during authentication, when credentials are prepared, and before Responses HTTP or WebSocket dispatch after pacing or retry waits. A held request reports the credit policy, not an authentication failure; wait for the reset, choose another account, or explicitly enable that account’s credit spending.
Stored-account vision and web-search helpers also recheck this policy before sending, including retries. If consent changes or a limit is reached after helper selection, the helper reports the policy refusal without sending that attempt. The standalone search relay returns a reset-bound 429. Caller-owned Direct credentials retain their existing behavior.
Opt in a custom provider to managed local routing
Section titled “Opt in a custom provider to managed local routing”A custom provider such as chatgpt-http may use supports_websockets = false
to keep HTTP transport. OpenCodex leaves custom providers untouched by default.
To delegate only that provider’s URL to OpenCodex, run:
ocx codex-provider allow chatgpt-httpocx startThe first command explains the scope and asks for confirmation, defaulting to No.
For an explicitly approved scripted operation, use allow chatgpt-http --yes.
Consent applies to this provider ID in the selected CODEX_HOME; it does not
approve other providers or other homes. Start and sync update only its base_url
to the local proxy. Its identity, WebSocket setting, other TOML fields and history
remain unchanged. Keep the proxy running while using this route.
Turning the Codex integration OFF also prevents approved-provider URL updates on
start or sync; the saved provider consent remains available when you turn it back on.
This mode supports explicit Responses provider tables with requires_openai_auth = true
and no custom credentials or headers. Non-loopback targets, authless Desktop mode
and client compaction require the existing dedicated integration instead.
ocx stop and ocx restore restore recorded URLs while keeping consent for a later
start. Run ocx codex-provider status to list approvals, or
ocx codex-provider revoke chatgpt-http to restore its managed URL and remove consent.
A URL changed outside OpenCodex is preserved and requires revoke followed by a new
approval; OpenCodex does not silently adopt the replacement as a new baseline.
The original URL assignment is retained in private recovery metadata under
OPENCODEX_HOME, separately from the native config journal.
Startup safety recognizes an approved provider’s recorded managed URL as OpenCodex routing. This does not install restart protection: a manually started proxy still needs a viable background service to recover automatically after a restart.
Native Desktop relay CONNECT validation
Section titled “Native Desktop relay CONNECT validation”The Desktop relay’s existing HTTP/HTTPS CONNECT transport now rejects malformed proxy response headers and ambiguous HTTP Basic usernames containing colons. Decoded credential pairs are limited to 4096 UTF-8 bytes. If your selected proxy is rejected, correct its configuration or response; the relay does not silently switch to a direct connection.
Proxy selection, SOCKS5/SOCKS5h support, certificate verification, and connection lifecycle are unchanged. Both SOCKS spellings retain remote target DNS resolution.

