Skip to content
17.7k

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.

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 table
model_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 = true

The 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.

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.

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.

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 = true

On 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.

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.

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.provider to the id of a custom API-key openai-responses provider 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; omit images.provider to use the built-in OpenAI tiers.
  • xAI Imagine (Grok OAuth) relay: when images.bridgeEnabled is true, images.provider is omitted, and an xai provider is configured, /v1/images/generations and /v1/images/edits are sent to https://api.x.ai/v1. The credential depends on the provider’s authMode: with "oauth" the relay reuses the Grok CLI grant from ocx 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. Setting images.provider explicitly hands /v1/images to that provider; its own validation errors are returned as-is and the xAI relay is never tried. The relay maps Codex size / aspect_ratio onto xAI’s Imagine body and returns the same {created, data:[{b64_json}]} shape. Combined decoded bytes and base64-encoded output across the batch (inline b64_json and 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, no file:, 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 the gemini-3.1-flash-image model. 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 requires ocx login google-antigravity; the OAuth token is sent only to the pinned CCA registry host, never to a config-level baseUrl override. 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_generation tool relay; if you don’t want the tool offered at all, disable it in Codex with codex features disable image_generation ([features] image_generation = false in config.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_generation tool (/images/generations relay). Gemini models that are image-capable produce inline images natively through the google adapter (via responseModalities: ["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 keys
model_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 = true
env_key = "OPENCODEX_API_AUTH_TOKEN"
# supports_websockets = true # only when config.websockets is true

When 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.

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.json

On 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.

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:

Terminal window
ocx system settings --client-compaction on # or "codexClientCompaction": true in config.json
ocx sync # rewrites the active config (default: ~/.codex/config.toml); restart Desktop

The 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 = true

Codex 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.

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.

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:

Terminal window
ocx system settings --desktop-authless on # or "codexDesktopAuthless": true in config.json
ocx sync # rewrites ~/.codex/config.toml; restart Desktop

With 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 = false

Desktop 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 opencodex provider, 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 Custom picker in this mode as well; requests still use the configured model. Set model = "<provider>/<id>" in config.toml as 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.

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.

Codex shows models from an on-disk catalog ($CODEX_HOME/opencodex-catalog.json by default). On start and on ocx sync, opencodex:

  1. Backs up the pristine catalog once to ~/.opencodex/catalog-backup.json (so featuring is reversible).
  2. 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 its GetUsableModels RPC rather than /models.
  3. Merges routed models in as namespaced entries (provider/model), cloned from a native Codex catalog template so Codex’s strict parser accepts them.
  4. Filters config.disabledModels and each provider’s non-empty selectedModels allowlist.
  5. 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.

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:

Terminal window
ocx doctor --recover-zero-byte-coordinator --yes
ocx sync

Recovery 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.

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.

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.

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):

Terminal window
ocx models add deepseek deepseek-v4 --display-name "DeepSeek V4" --context-window 128000

For 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:

Terminal window
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-cache

The 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.

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.

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:

  1. No type: "tool_search" in the incoming request: the active Codex client/session did not advertise the explicit tool_search surface. OpenCodex cannot invent that declaration. This does not mean normal code-mode tools are unavailable: check whether the routed model can use exec and discover the needed nested tool through tools / ALL_TOOLS first.
  2. The incoming declaration exists, but no tool_search function 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.
  3. 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’s tool_choice: "auto" permits tool use; it does not force the model to call this function.
  4. A call is emitted repeatedly or loaded tools never become usable: capture the redacted tool_search_call / tool_search_output item 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.

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.

If a model is missing from Codex, or the catalog order/visibility looks wrong, check in order:

  1. selectedModels on 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.
  2. disabledModels (top level) — hides models from both the catalog and /v1/models, and flips bare native GPT slugs to visibility: "hide".
  3. liveModels: false — With liveModels: false, an empty or omitted models list seeds the configured defaultModel first, followed by retainModels; duplicate ids are removed while preserving first occurrence. A nonempty explicit models list instead seeds models followed by retainModels, without implicitly adding a different defaultModel. That default can still be listed explicitly in models or retainModels. 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, disabledModels and 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.
  4. Cursor GetUsableModels — the Cursor adapter discovers models through its protobuf GetUsableModels RPC, not /models, so a Cursor-side change can alter which ids are visible independently of other providers.
  5. Cache and ocx sync — live catalogs are cached for about five minutes (modelCacheTtlMs, default 300000). Run ocx sync to force a fresh fetch and rewrite the catalog immediately.
  6. Running Codex app-server — rewriting the on-disk catalog is not enough while a long-lived Codex app-server (Desktop / CLI background host) keeps the previous list in memory. ocx sync and ocx sync-cache warn when those processes are detected. ocx sync --restart-codex restarts 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-only or stop the matching app-server processes yourself.

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:

Terminal window
ocx start # foreground
ocx service install # persistent: auto-starts on login and respawns on crash

ocx 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).

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.

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 normal Authorization: Bearer credential and adds an x-openai-agent-assertion header. 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 with Authorization: AgentAssertion <jwt>. opencodex refuses these requests with HTTP 400 and error code agent_assertion_unsupported. It never substitutes a Pool or provider credential for the assertion, and it never forwards or logs the assertion. Use dual_header, or turn the feature off, when Codex runs through opencodex.

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:

Terminal window
. "$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.

  1. 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.com traffic.
  2. 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.
  3. 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.

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:

  1. Identify the actual process and transport that sends each authoritative gate request.
  2. Prove that an app-origin request reaches the intended relay, separately from test clients.
  3. Verify the response schema and the app’s cache/update path for the exact installed build.
  4. On natural account exhaustion, verify original-composer submission, independent-provider completion, and preservation of sign-in, Chat, existing threads and remote connections.
  5. 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.

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 /v1 endpoint.
  • 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.

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.

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.

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.

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.

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.

Terminal window
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.

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.

Terminal window
ocx stop # stop the proxy + service, restore native Codex
ocx restore # restore without stopping (alias: ocx eject)
ocx restore back # point plain Codex at the running proxy again

When 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.

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.

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.

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.

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.

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.

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.

From a source checkout, run the offline positive control:

Terminal window
bun scripts/steering-smoke.ts --self-test

Plan a comparison without reading tokens or opening any connection:

Terminal window
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.

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.

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:

Terminal window
ocx codex-provider allow chatgpt-http
ocx start

The 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.

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.