Architecture
opencodex is a single Bun process. A request enters as OpenAI Responses, is normalized to an internal model, routed, sent to a provider via an adapter, and bridged back to Responses SSE. See How It Works for the end-to-end flow.
Module map
Section titled “Module map”src/├── cli/ # ocx command dispatch, init, status, provider commands├── server/ # Bun.serve, /v1/* proxy, /api/* management API, WS bridge├── codex/ # Codex config injection, catalog sync, auth/account integration├── providers/ # provider metadata, API-key pool, quota and labels├── adapters/ # wire adapters, shared guards/utilities, Cursor protobuf transport├── oauth/ # OAuth providers, API-key catalog, token store/refresh├── usage/ # request usage extraction, JSONL logs, summaries, totals├── lib/ # runtime, process, retry, privacy, token estimate helpers├── web-search/ # web-search sidecar (synthetic tool, loop, executor, parser)├── vision/ # vision sidecar (describe + plan)├── config.ts # ~/.opencodex/config.json, defaults, PID, env resolution├── router.ts # model id → provider + adapter├── bridge.ts # AdapterEvent stream → Responses SSE / JSON├── reasoning-effort.ts # reasoning-effort translation, clamping, and catalog levels├── responses/│ ├── parser.ts # Responses request → OcxParsedRequest│ ├── schema.ts # Zod validation│ └── compaction.ts # remote compaction prompts, envelopes, compact history├── service.ts # launchd / systemd / Task Scheduler background service├── types.ts # core interfaces + helpers (modelInList, namespacedToolName)└── index.ts # public entryThree formerly large entry files now preserve compatibility as facades: codex/catalog.ts exports
the seven focused codex/catalog/*.ts modules, server/management-api.ts dispatches to the nine
server/management/*.ts modules, and server/responses.ts exports the five
server/responses/*.ts modules.
Request flow
Section titled “Request flow”server/index.ts owns the HTTP boundary and delegates the Responses data plane to
the server/responses.ts facade and its server/responses/*.ts modules:
server/index.tsapplies CORS and API authentication, rejects new work while draining, and records request lifecycle metadata. It servesGET /v1/models,POST /v1/responses,POST /v1/responses/compact,POST /v1/images/generations/POST /v1/images/edits(relayed to an OpenAI-family upstream byserver/images.tsfor codex’s built-inimage_gentool),POST /v1/live/POST /v1/realtime/calls(ChatGPT / Codex App voice and OpenAI Realtime call-create, relayed byserver/live.ts), sideband WebSocket joins on/v1/live/{callId}(and/v1/realtime?call_id=), and the optional WebSocket upgrade on/v1/responses.server/responses/core.tsdecompresses and parses JSON, expands locally rememberedprevious_response_idinput when available, then callsresponses/parser.ts.router.tsresolves a bare orprovider/modelid. The server then resolves Codex account affinity, refreshes provider OAuth when needed, and applies the selected credential to the route.- Before the main call,
vision/describes images for models innoVisionModels; if no safe sidecar path exists, images are removed rather than sent to a text-only upstream. server/adapter-resolve.tsapplies any model-specific wire override and constructs one of the registered adapters. Responses passthrough relays the native body, Cursor runs its bidirectionalrunTurntransport, and translated adapters build/fetch/parse an upstream request.- For routed models with a hosted
web_searchtool,web-search/exposes a synthetic function, executes the real search through the configured backend (the OpenAI/ChatGPT sidecar or Anthropic), feeds results back to the routed model, and repeats within the configured loop limit. This loop supports only the standard HTTP path; adapters that implementrunTurn, such as Cursor, bypass it. bridge.tsproduces Responses SSE or JSON.server/request-log.tsandusage/collect terminal status, latency, provider/model labels, and best-effort token usage without changing the response.
The parser
Section titled “The parser”responses/parser.ts validates the incoming request with responses/schema.ts (Zod), then builds an
OcxParsedRequest:
- Messages —
inputitems become a normalizedOcxMessage[]: user / developer / assistant / toolResult.reasoningitems become thinking blocks;function_call,custom_tool_call, andtool_search_callitems become tool calls; their*_outputcounterparts become tool results. - Tools — function tools pass through; namespaced (MCP) tools are flattened to
namespace__name(and restored on the way back); freeform tools (e.g.apply_patch) and tool_search discovery tools are flagged; hosted tools (web_search, image gen, …) are dropped and re-injected by a sidecar only if it will handle them. - Images — preserved as real content parts (data URL or remote https), never inlined as text.
- Feature flags —
_webSearch(hosted web search requested),_structuredOutput(text.formatis json_schema / json_object), and_compactionRequest(remote compaction v2).
The bridge
Section titled “The bridge”bridge.ts turns the adapter’s internal AdapterEvent stream back into Responses SSE that Codex
understands:
| AdapterEvent | Responses SSE emitted |
|---|---|
text_delta |
response.output_text.delta → …done, response.content_part.done, response.output_item.done |
thinking_delta |
response.reasoning_summary_text.delta → …done, item close |
reasoning_raw_delta |
A raw reasoning_text item (or a hidden round-trip envelope) |
thinking_signature / redacted_thinking |
Preserved in an encrypted_content reasoning envelope |
tool_call_start |
response.output_item.added (type: function_call / custom_tool_call / tool_search_call) |
tool_call_delta |
response.function_call_arguments.delta (skipped for freeform / tool_search) |
tool_call_end |
response.function_call_arguments.done → response.output_item.done |
web_search_call_begin / web_search_call_end |
One live web_search_call item plus URL citations |
heartbeat |
Marks upstream activity; no user-visible output item |
done |
response.completed (with usage) |
error |
response.failed (with last_error) |
The bridge also runs a heartbeat keep-alive (RC3): during upstream silence, it emits an SSE
comment line (: opencodex heartbeat) every 2 seconds to re-arm Codex’s idle timer. Comment lines
are discarded by every eventsource parser without producing an event, so strict Responses decoders
never see an unknown variant. The default stall deadline is 300 seconds (stallTimeoutSec);
reaching it aborts the upstream and emits response.incomplete with reason
upstream_stall_timeout, preventing a hung connection from blocking Codex indefinitely.
Tool calls are disambiguated into three Responses item types using the namespace map, the freeform
set, and the tool-search set captured by the parser — so MCP namespaces, apply_patch-style freeform
tools, and client-executed tool_search all round-trip. A buildResponseJSON() variant produces a
single non-streaming response object from the same events.
Management API, OAuth, and usage
Section titled “Management API, OAuth, and usage”server/management-api.ts backs the dashboard and dispatches focused route groups to
server/management/*.ts. Its /api/* routes cover safe config/settings,
provider CRUD and key pools, model selection/context caps/v2 controls, catalog sync, diagnostics and
debug logs, usage and quotas, sidecar settings, updates, generated client API keys, OAuth login/status/
logout and account selection, Codex account management, and graceful stop. server/auth-cors.ts
requires OPENCODEX_API_AUTH_TOKEN for both /api/* and /v1/* when the proxy binds beyond
loopback; configured corsAllowOrigins entries extend the local-origin allowlist.
OAuth implementations live in oauth/; access tokens are loaded or refreshed immediately before a
routed call, while oauth/token-guardian.ts can proactively refresh only providers whose policy
allows it. Refresh is coordinated with in-process single-flight, a per-account file lock, and
generation CAS so concurrent writers cannot clobber a newer credential. A shared health projection
(oauth/health.ts) feeds ocx status, ocx doctor, the management API, and the dashboard.
Codex/ChatGPT pool credentials and process-local thread affinity live under codex/ and are kept out
of management responses; affinity clears on 401 / 403 / 429 (not pinned through rate limits)
and is not persisted across restarts. Request usage is normalized to OcxUsage, surfaced in
Responses terminal events, and aggregated by usage/ for the dashboard and optional JSONL
diagnostics.
Transport and compaction
Section titled “Transport and compaction”server/index.ts serves HTTP/SSE on /v1/responses by default. If Codex attempts a Responses
WebSocket upgrade while websockets is false, opencodex returns 426 upgrade_required; Codex then
falls back to HTTP for that session. When "websockets": true is set, the same endpoint accepts the
upgrade and uses the WebSocket bridge.
Independently of that client-facing setting, canonical ChatGPT forward requests with root-level
stream: true may use Codex’s upstream WebSocket transport on stable Bun 1.4.0 or newer.
The canonical ChatGPT path preserves HTTP Responses Lite intent in WS frame metadata
and derives its routing hint from the actual outgoing model and service tier.
Initial upstream quota/model metadata becomes bounded HTTP response headers;
later quota updates are attributed to the serving account, not retroactively
added to headers already sent. A failure after a WS request was sent does not
trigger an automatic HTTP resend. These mappings do not enable the client-facing
WebSocket setting or change other providers’ transport selection.
Bundled Bun 1.3.14, prereleases, and unverifiable runtime identities use HTTP/SSE. Successful
upstream WS responses keep the downstream SSE contract and bypass tee() through a bounded eager
single-reader relay (4 MiB per raw/enveloped frame and an 8 MiB producer queue). Queue overflow
closes the upstream and emits a terminal downstream response.failed event followed by [DONE].
When a provider rejects a streaming request with HTTP 413 before SSE begins, OpenCodex emits one
terminal response.failed event with context_length_exceeded instead of relaying the retryable
unknown status. This lets Codex stop its reconnect loop and apply its own context-compaction policy
on the next turn. OpenCodex does not silently delete prompts or images; reduce the current input or
retry after compaction. Non-streaming API callers continue to receive the provider’s HTTP 413.
Codex context compaction works for routed models. server/responses/compact.ts handles
POST /v1/responses/compact by running an internal routed summarization turn and returning compacted
history, while responses/parser.ts and bridge.ts handle remote compaction v2
compaction_trigger turns by emitting exactly one synthetic compaction output item.
Caching & the catalog
Section titled “Caching & the catalog”codex/model-cache.tskeeps a per-provider, in-memory TTL cache of live/modelsresults (default 5 min, matching Codex’s own cache), with a stale-fallback when a fetch fails.codex/catalog/sync.ts, exported through thecodex/catalog.tsfacade, merges routed models into Codex’s catalog as namespaced entries, ranks featured subagent models first, filtersdisabledModels, and can fully restore the pristine catalog from a one-time backup.
Reasoning effort
Section titled “Reasoning effort”reasoning-effort.ts translates Codex’s reasoning labels into each provider’s wire values. The
Codex catalog advertises labels Codex accepts (low / medium / high / xhigh / max), but
upstream providers may support only a smaller subset or require a real alias. The module:
- Defines the canonical
CODEX_REASONING_LEVELSand their sort order. - Clamps a requested effort to the closest supported tier when the exact level is unavailable.
- Resolves per-model and per-provider
reasoningEffortMapoverrides for custom wire mappings. - Drops the effort entirely for models listed in
noReasoningModels.
Qwen3.8-Max is an explicit direct-effort exception to the older Qwen3.x budget contract. Alibaba
Token Plan records its upstream-supported ladder as low, medium, and xhigh (the default), and
sends the effective value as reasoning_effort; Codex-only compatibility tops are clamped to
xhigh on the wire. Runtime registry enrichment repairs older persisted preset metadata that still
classifies this model as a thinking_budget model.
Core types
Section titled “Core types”The internal model lives in types.ts: OcxParsedRequest, OcxContext, the OcxMessage union,
OcxContentPart (text / image), OcxToolCall, OcxTool, AdapterEvent, and the config types
(OcxConfig, OcxProviderConfig). Two helpers are widely used: namespacedToolName() and
modelInList() (tolerant :size-tag matching for noVisionModels / noReasoningModels).
Incomplete quota terminals
Section titled “Incomplete quota terminals”A native forward response that ends with quota or rate-limit evidence in an
incomplete terminal records account quota failure and spawn-fallback health.
Structured incomplete_details.reason and error codes are accepted without a
message; ordinary output-limit, filtering, steering and stall incompletes do not
cool an account. Cyber-policy classification retains precedence. The terminal is
not replayed after output, and fixed-account request selection remains fixed.
Remote compact requests can buffer their response for longer than the server’s request-idle timeout. That listener timeout is disabled after the request body is accepted; client cancellation and upstream operation deadlines still apply.
Buffered routed compaction treats nonempty text and reasoning deltas as progress without exposing partial summary text. Comments, empty deltas and gateway keepalives do not reset the adapter-event stall watchdog. The default stall timeout stays 300 seconds; encrypted compaction content is preserved unchanged.
Native compact response buffering also enforces a body-byte inactivity deadline
using stallTimeoutSec (300 seconds by default). Nonempty chunks reset that
deadline; a stalled body returns HTTP 504, client cancellation retains HTTP 499,
and cleanup does not wait for a stuck upstream cancellation promise. The 32 MiB
response ceiling and the original body bytes are preserved.
A canonical upstream WebSocket refused-create error can become an HTTP 4xx only before the response is committed and after stream correlation checks. Permitted quota headers are bounded and rebuilt without upstream framing headers; the JSON response is not cacheable. Post-commit and 5xx errors keep the no-resend path.
When encrypted agent-task recovery refuses a routed task, its existing 400 error
can include a bounded recovery_reason: unsupported_envelope,
admission_denied, recovery_unavailable, caller_cancelled, input_changed,
recovery_http_rejected, recovery_timeout, recovery_aborted,
recovery_transport_error, or recovery_invalid_output.
HTTP rejection requires an observed non-success response. Invalid output includes
invalid UTF-8, oversized bodies, malformed or incomplete recovery streams, and
invalid or conflicting assignments. A caller’s cancellation takes precedence over
an owned deadline, which takes precedence over decode/transport failures.
recovery_aborted describes a shared recovery cancelled independently of that caller.
Shared-flight waiters receive the same underlying failure unless individually cancelled;
only successful plaintext is cached. Diagnostics contain no upstream error or payload text.
The field is omitted when no classified recovery result exists, and existing combo
branches that return the original target failure keep that response.
recovery_unavailable includes cache/singleflight capacity and does not prove an
upstream request was attempted. No retry or broader envelope acceptance is enabled.

