Skip to content

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.

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 entry

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

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:

  1. server/index.ts applies CORS and API authentication, rejects new work while draining, and records request lifecycle metadata. It serves GET /v1/models, POST /v1/responses, POST /v1/responses/compact, POST /v1/images/generations / POST /v1/images/edits (relayed to an OpenAI-family upstream by server/images.ts for codex’s built-in image_gen tool), POST /v1/live / POST /v1/realtime/calls (ChatGPT / Codex App voice and OpenAI Realtime call-create, relayed by server/live.ts), sideband WebSocket joins on /v1/live/{callId} (and /v1/realtime?call_id=), and the optional WebSocket upgrade on /v1/responses.
  2. server/responses/core.ts decompresses and parses JSON, expands locally remembered previous_response_id input when available, then calls responses/parser.ts.
  3. router.ts resolves a bare or provider/model id. The server then resolves Codex account affinity, refreshes provider OAuth when needed, and applies the selected credential to the route.
  4. Before the main call, vision/ describes images for models in noVisionModels; if no safe sidecar path exists, images are removed rather than sent to a text-only upstream.
  5. server/adapter-resolve.ts applies any model-specific wire override and constructs one of the registered adapters. Responses passthrough relays the native body, Cursor runs its bidirectional runTurn transport, and translated adapters build/fetch/parse an upstream request.
  6. For routed models with a hosted web_search tool, 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 implement runTurn, such as Cursor, bypass it.
  7. bridge.ts produces Responses SSE or JSON. server/request-log.ts and usage/ collect terminal status, latency, provider/model labels, and best-effort token usage without changing the response.

responses/parser.ts validates the incoming request with responses/schema.ts (Zod), then builds an OcxParsedRequest:

  • Messagesinput items become a normalized OcxMessage[]: user / developer / assistant / toolResult. reasoning items become thinking blocks; function_call, custom_tool_call, and tool_search_call items become tool calls; their *_output counterparts 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.format is json_schema / json_object), and _compactionRequest (remote compaction v2).

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

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.

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.

  • codex/model-cache.ts keeps a per-provider, in-memory TTL cache of live /models results (default 5 min, matching Codex’s own cache), with a stale-fallback when a fetch fails.
  • codex/catalog/sync.ts, exported through the codex/catalog.ts facade, merges routed models into Codex’s catalog as namespaced entries, ranks featured subagent models first, filters disabledModels, and can fully restore the pristine catalog from a one-time backup.

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_LEVELS and their sort order.
  • Clamps a requested effort to the closest supported tier when the exact level is unavailable.
  • Resolves per-model and per-provider reasoningEffortMap overrides 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.

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

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.