System Overview
The Elixir Codex SDK is a layered architecture that wraps the codex-rs CLI executable and provides an idiomatic OTP-based interface. The system is designed around three core principles:
- Process Isolation: Each turn execution runs in its own GenServer
- Clean Separation: Clear boundaries between client API, process management, and IPC
- Robust Error Handling: Failures are isolated and cleanly propagated
Separate from thread/turn execution, the SDK also exposes a thin command-surface
passthrough layer (Codex.CLI and Codex.CLI.Session) for CLI-only workflows
such as codex completion, codex cloud, codex features, codex mcp-server,
and the root interactive client. One-shot non-PTY passthrough goes through the
shared CliSubprocessCore.Command lane, while Codex.CLI.Session preserves the
historical mailbox-facing session API on top of CliSubprocessCore.RawSession.
Transports
codex_sdk supports two upstream external transports:
- Exec JSONL (default
:execcompatibility selector): spawnscodex exec --jsonand parses JSONL events - App-server JSON-RPC (optional): maintains a stateful
codex app-serversubprocess and speaks newline-delimited JSON-RPC over stdio
The app-server path is the parity transport for upstream v2 features such as fs/*, plugin/read,
thread/shellCommand, structured item/permissions/requestApproval responses,
mcpServer/startupStatus/updated, guardian review notifications, and serverRequest/resolved.
Typed plugin params and responses live locally under Codex.Protocol.Plugin.*;
they do not move into the shared runtime-core repos.
Local manifest, marketplace, and scaffold authoring live separately under
Codex.Plugins.*; those helpers use direct local file IO and are not disguised
app-server filesystem wrappers.
Transport selection is per-thread via Codex.Thread.Options.transport:
{:ok, conn} = Codex.AppServer.connect(codex_opts)
{:ok, thread_opts} = Codex.Thread.Options.new(%{transport: {:app_server, conn}})Codex.AppServer.connect/2 can also isolate the managed child with cwd: and
process_env: launch overrides when you need a temporary CODEX_HOME.
Native OAuth is a separate subsystem centered on Codex.OAuth. Persistent
OAuth writes upstream-compatible auth.json under the effective CODEX_HOME.
Memory-mode OAuth is used for external app-server auth: connect/2 performs
account/login/start with chatgptAuthTokens and, when enabled, starts a
connection-owned refresh responder for account/chatgptAuthTokens/refresh
without pushing that ownership into the lower-level app-server transport layer.
Across both transports, TLS configuration is centralized in Codex.Net.CA: subprocess
environment injection, Req clients, :httpc, and realtime websocket SSL options all resolve
CODEX_CA_CERTIFICATE first, then SSL_CERT_FILE.
Runtime Ownership Boundary
Shared core ownership:
Codex.ExeconCliSubprocessCore.SessionCodex.CLI.run/2and the synchronous CLI wrappers onCliSubprocessCore.CommandCodex.CLI.SessiononCliSubprocessCore.RawSession- the subprocess lifecycle behind
Codex.AppServer.connect/2andCodex.MCP.Transport.StdioonCliSubprocessCore.RawSession - one-shot hosted shell execution and
Codex.Sessions.apply/2on the shared command lane
Codex-owned semantics above the core:
Codex.CLI.Sessionas the public Codex session API for PTY and long-lived CLI sessions- the app-server connection process used by
Codex.AppServer.connect/2for the provider-nativecodex app-servercontrol protocol - the MCP stdio transport used by
codex mcp-server - realtime and voice clients, which call OpenAI APIs directly instead of using the CLI runtime
The publication boundary on that split is now:
cli_subprocess_coreowns every Codex subprocess-backed lifecycle and the only native subprocess dependency in the stackcodex_sdkremains the home of app-server, MCP, realtime, voice, and other Codex-native semantics- optional ASM integration may exist only as an explicit bridge above the normalized kernel; it does not re-home these families or widen the core
Centralized Model Selection
cli_subprocess_core is the only model-policy owner in the Codex stack.
codex_sdk consumes the resolved selection payload and never implements a
second defaulting or fallback path.
Resolution ownership:
CliSubprocessCore.ModelRegistry.resolve/3CliSubprocessCore.ModelRegistry.validate/2CliSubprocessCore.ModelRegistry.default_model/2CliSubprocessCore.ModelRegistry.build_arg_payload/3
Codex-side consumption points:
Codex.Options.new/1resolvesmodel_payload,model, andreasoning_effortCodex.Modelsprojects visible models and defaults from the shared catalogCodex.Runtime.Execrenders CLI args from the current resolved options state
Contract rules:
- no repo-local model catalog owns policy
- no implicit provider fallback exists outside the core registry
- no silent acceptance of blank, placeholder, or invalid model requests
- no
--model nil,--model null, or blank--modelemission
Codex Local OSS And Ollama
The current external-model path implemented end-to-end is local Ollama through Codex OSS mode.
The flow is:
Codex.Options.new/1forwardsprovider_backend: :ossandoss_provider: "ollama"intoCliSubprocessCore.ModelRegistry- the core validates the Ollama runtime and selected local model id
- the resolved payload carries:
provider_backend: :ossbackend_metadata["oss_provider"] = "ollama"
- exec and app-server startup render:
--oss--local-provider ollama--model <resolved local model>
That keeps Codex backend selection centralized while still preserving the upstream Codex routing concepts.
Component Architecture
High-Level Component Diagram
┌───────────────────────────────────────────────────────────────┐
│ Client Code │
│ (User application using Codex SDK) │
└────────────────┬──────────────────────────────────────────────┘
│
│ Public API
▼
┌───────────────────────────────────────────────────────────────┐
│ Codex Module │
│ - start_thread/2 │
│ - resume_thread/3 │
│ (Factory for Thread instances) │
└────────────────┬──────────────────────────────────────────────┘
│
│ Returns Thread struct or CLI session helpers
▼
┌───────────────────────────────────────────────────────────────┐
│ Codex.Thread Module │
│ - run/3 (blocking) │
│ - run_streamed/3 (streaming) │
│ (Manages turn execution lifecycle) │
└────────────────┬──────────────────────────────────────────────┘
│
│ Transport dispatch / raw CLI passthrough
▼
┌───────────────────────────────────────────────────────────────┐
│ Codex.Transport (behaviour) │
│ - Exec JSONL: Codex.Exec │
│ - App-server: Codex.AppServer.Connection │
│ - Raw CLI / PTY: Codex.CLI, Codex.CLI.Session │
└────────────────┬──────────────────────────────────────────────┘
│
│ Port (stdin/stdout)
▼
┌───────────────────────────────────────────────────────────────┐
│ codex-rs Process │
│ - OpenAI API integration │
│ - Command execution │
│ - File operations │
│ - Event emission │
└───────────────────────────────────────────────────────────────┘Module Breakdown
1. Codex Module
Purpose: Main entry point and factory for thread instances.
Responsibilities:
- Validate global options (API key, base URL, codex path)
- Create new thread instances
- Resume existing threads from saved sessions
State: Stateless module (pure functions)
Key Functions:
@spec start_thread(Codex.Options.t(), Codex.Thread.Options.t()) ::
{:ok, Codex.Thread.t()} | {:error, term()}
@spec resume_thread(String.t(), Codex.Options.t(), Codex.Thread.Options.t()) ::
{:ok, Codex.Thread.t()} | {:error, term()}Error Handling:
- Validates codex binary exists and is executable
- Validates options format
- Returns descriptive errors for invalid configurations
2. Codex.Thread Module
Purpose: Manages individual conversation threads.
Responsibilities:
- Execute turns (blocking and streaming modes)
- Maintain thread ID and options
- Coordinate with the exec runtime kit
- Handle structured output schemas and rate limit snapshots
State: Encapsulated in %Codex.Thread{} struct (includes transport metadata)
defstruct [
:thread_id, # String.t() | nil (populated after first turn)
:codex_opts, # %Codex.Options{}
:thread_opts, # %Codex.Thread.Options{}
:rate_limits, # latest rate limit snapshot (if provided)
:transport # :exec compatibility selector | {:app_server, pid()}
]Key Functions:
@spec run(t(), String.t() | [map()], Codex.Turn.Options.t()) ::
{:ok, Codex.Turn.Result.t()} | {:error, term()}
@spec run_streamed(t(), String.t() | [map()], Codex.Turn.Options.t()) ::
{:ok, Enumerable.t()} | {:error, term()}App-server transport accepts UserInput block lists (text/image/localImage/skill/mention); exec JSONL accepts prompt strings plus the SDK's normalized JSONL user-input variants (text/image/local_image/skill/mention).
Execution Flow (Blocking Mode):
- Create output schema file if needed
- Start
Codex.Exec, which bootsCodex.Runtime.ExeconCliSubprocessCore.Session - Project core session events into
%Codex.Events{}values and accumulate items - Extract final response from last
AgentMessage - Return
TurnResultwhen the core-backed session completes - Clean up schema file and the ephemeral session process
Execution Flow (Streaming Mode):
- Create output schema file if needed
- Start
Codex.Exec, which bootsCodex.Runtime.ExeconCliSubprocessCore.Session - Return Stream that yields projected
%Codex.Events{}values as they arrive - Clean up when the underlying session completes or the stream is halted
3. Codex.Exec And Runtime Kit
Purpose: Preserve the public exec JSONL API while delegating common CLI
process ownership and parsing to cli_subprocess_core.
Responsibilities:
- Translate SDK thread/turn options into the common CLI session invocation
- Start a
CliSubprocessCore.SessionthroughCodex.Runtime.Exec - Project core runtime events back into typed
%Codex.Events{}structs - Track stderr tails, timeouts, cancellation tokens, and non-zero exits
- Clean up the ephemeral session process on completion or crash
State:
defstruct [
:session, # pid() for CliSubprocessCore.Session
:session_ref, # reference() for subscriber mailbox routing
:projection_state, # runtime-kit projection state
:stderr, # bounded stderr tail
:timeout_ms, # blocking idle timeout
:idle_timeout_ms # streaming idle timeout
]Lifecycle:
- Build session options and start
Codex.Runtime.Exec - Subscribe to
CliSubprocessCore.Sessionevents using the per-run session ref - Project core events into
%Codex.Events{}values as they arrive - Convert terminal core exit events into
Codex.TransportErrorwhen needed - Stop the session and flush any remaining internal session messages for that run
Error Scenarios:
- Spawn failure: Return error immediately
- Parse failure: Log and continue; the core remains the only JSONL parser
- Non-zero exit: Surface
Codex.TransportErrorwith bounded stderr - Unexpected session shutdown: Treat as an exec transport failure
4. Type Modules
Codex.Events
Defines all event types emitted during turn execution.
TypedStruct Definitions:
defmodule Codex.Events.ThreadStarted do
use TypedStruct
typedstruct do
field :type, :thread_started, enforce: true
field :thread_id, String.t(), enforce: true
end
end
# Similar for:
# - TurnStarted
# - TurnCompleted (with Usage)
# - TurnFailed (with ThreadError)
# - ItemStarted (with ThreadItem)
# - ItemUpdated (with ThreadItem)
# - ItemCompleted (with ThreadItem)Codex.Items
Defines all item types and their variants.
Item Types:
AgentMessage: Text or JSON responseReasoning: Agent's thinking summaryCommandExecution: Command with output and exit codeFileChange: File modifications with changes arrayMcpToolCall: MCP tool invocationWebSearch: Search queryTodoList: Agent's task listError: Non-fatal error
Example:
defmodule Codex.Items.CommandExecution do
use TypedStruct
typedstruct do
field :id, String.t(), enforce: true
field :type, :command_execution, default: :command_execution
field :command, String.t(), enforce: true
field :aggregated_output, String.t(), default: ""
field :exit_code, integer()
field :status, atom(), enforce: true
end
endCodex.Options
Configuration structs for each level.
defmodule Codex.Options do
use TypedStruct
typedstruct do
field :codex_path_override, String.t()
field :base_url, String.t()
field :api_key, String.t()
end
end
defmodule Codex.Thread.Options do
use TypedStruct
typedstruct do
field :model, String.t()
field :sandbox_mode, atom() # :read_only | :workspace_write | :danger_full_access
field :working_directory, String.t()
field :skip_git_repo_check, boolean(), default: false
end
end
defmodule Codex.Turn.Options do
use TypedStruct
typedstruct do
field :output_schema, map()
end
end5. Utility Modules
Codex.OutputSchemaFile
Manages temporary JSON schema files.
Functions:
@spec create(map() | nil) :: {:ok, {String.t() | nil, function()}} | {:error, term()}Implementation:
- Creates temp directory in system tmp
- Writes schema JSON to file
- Returns path and cleanup function
- Cleanup function removes directory recursively
- Handles nil schema (no file created)
Data Flow Diagrams
Blocking Turn Execution
Client Thread Exec Runtime Core Session
| | | |
|-- run(input) -------->| | |
| |-- run_turn ---------->| |
| | |-- start ----------->|
| | | |-- codex-rs starts
| | |<------ event -------|
| |<------- event --------| |
| | |<------ event -------|
| |<------- event --------| |
| | | |-- codex-rs exits
| | |<------ exit --------|
|<-- {:ok, result} -----| | |Streaming Turn Execution
Client Thread Exec Runtime Core Session
| | | |
|-- run_streamed() ---->| | |
| |-- run_turn ---------->| |
| | |-- start ----------->|
|<-- {:ok, stream} -----| | |
| | | |-- codex-rs starts
| | | |
|-- next event -------->|-- fetch event ------->| |
|<-- ItemStarted -------|<----------------------|<------ event -------|
| | | |
|-- next event -------->|-- fetch event ------->| |
|<-- ItemCompleted -----|<----------------------|<------ event -------|
| | | |
|-- next event -------->|-- fetch event ------->| |
|<-- TurnCompleted -----|<----------------------|<------ event -------|
| | | |-- codex-rs exits
| | |<------ exit --------|
|-- stream done ------->| | |Process Model
Process Hierarchy
Application Supervisor
│
└─── Client Process (caller)
│
└─── CliSubprocessCore.Session (per turn)
│
└─── ExternalRuntimeTransport.Transport
│
└─── codex-rsKey Points:
- The core session process is ephemeral (one per turn)
- No persistent supervision tree needed
- Client monitors the session process through
Codex.Exec Codex.Runtime.Execpreserves the public event surface by projection- Clean shutdown cascades down hierarchy
Message Passing
Client → Thread (synchronous):
{:run, input, options}
{:run_streamed, input, options}Thread → Exec (GenServer call):
{:run_turn, input, codex_args}Port → Exec (Port messages):
{port, {:data, binary}}
{port, {:exit_status, integer}}
{:EXIT, port, reason}Exec → Client (via reference):
{:event, ref, event_struct}
{:error, ref, error_term}
{:done, ref}Error Handling Strategy
Error Categories
Configuration Errors (fail fast)
- Invalid options
- Missing codex binary
- Bad API credentials
- Return:
{:error, {:config, reason}}
Process Errors (recoverable)
- Spawn failure
- Port crash
- Return:
{:error, {:process, reason}}
Communication Errors (retryable)
- JSON parse error
- Protocol mismatch
- Return:
{:error, {:communication, reason}}
Turn Errors (expected)
- Agent failure
- API rate limit
- Model error
- Return:
{:error, {:turn_failed, error_struct}}
Error Propagation
codex-rs exit code ≠ 0
↓
CliSubprocessCore.Session emits terminal error event
↓
Codex.Runtime.Exec captures stderr + exit details
↓
Codex.Exec returns/raises Codex.TransportError
↓
Thread receives error
↓
Client gets {:error, {:turn_failed, details}}Cleanup Guarantees
All cleanup happens when the ephemeral session process stops:
- Close the core session
- Let the shared transport close the subprocess
- Remove temporary schema file
- Send telemetry event
Cleanup is guaranteed even on:
- Normal completion
- Client crash
- Runtime/session crash
- VM shutdown
Streaming Implementation
Stream Creation
def run_streamed(thread, input, opts) do
{schema_path, cleanup_fn} = OutputSchemaFile.create(opts.output_schema)
stream = Stream.resource(
fn ->
{:ok, stream} = Codex.Exec.run_stream(input, ...)
{stream, cleanup_fn}
end,
fn {stream, cleanup_fn} = acc ->
next_stream_chunk_from_runtime(stream, acc)
end,
fn {_stream, cleanup_fn} ->
cleanup_fn.()
end
)
{:ok, stream}
endKey Properties:
- Lazy evaluation (events fetched on demand)
- Backpressure support (caller controls rate)
- Automatic cleanup (even if stream halted early)
- Timeout protection via the exec runtime kit
Event Buffering
- Shared parser + transport sequencing
- Tagged subscriber delivery into
Codex.Exec
In Thread/Client:
- No buffering (events consumed immediately)
- Client controls pace via Stream consumption
Performance Considerations
Memory
Per Turn Overhead:
- GenServer state: ~1 KB
- Event buffers: ~10 KB
- Port buffers: ~4 KB
- Total: ~15 KB per concurrent turn
Streaming Benefits:
- Constant memory (O(1) per turn)
- Events processed and discarded
- No accumulation of full turn history
Latency
Event Propagation:
- codex-rs → stdout: < 1 ms
- Port → Exec: < 1 ms
- Exec → Client: < 1 ms
- Total: < 5 ms end-to-end
Optimization Opportunities:
- Batch small events
- Binary protocol (vs JSON)
- NIF for JSON parsing
Throughput
Bottlenecks:
- OpenAI API rate limits (primary)
- JSON parsing (secondary)
- Process scheduling (minimal)
Scalability:
- 100s of concurrent turns easily
- 1000s possible with tuning
- Limited by API, not SDK
Telemetry Integration
Events
[:codex, :turn, :start]
Measurements: %{system_time: integer()}
Metadata: %{thread_id: string(), input_length: integer()}
[:codex, :turn, :stop]
Measurements: %{duration: integer()}
Metadata: %{thread_id: string(), usage: Usage.t()}
[:codex, :turn, :exception]
Measurements: %{duration: integer()}
Metadata: %{thread_id: string(), error: term()}
[:codex, :item, :completed]
Measurements: %{system_time: integer()}
Metadata: %{thread_id: string(), item_type: atom(), item_id: string()}Usage
:telemetry.attach_many(
"codex-handler",
[
[:codex, :turn, :start],
[:codex, :turn, :stop],
[:codex, :turn, :exception]
],
&MyApp.TelemetryHandler.handle_event/4,
nil
)Security Considerations
Sandbox Modes
:read_only: Codex can read files but not write:workspace_write: Codex can write within working directory:danger_full_access: Codex has unrestricted access
Recommendations:
- Use
:read_onlyfor analysis tasks - Use
:workspace_writefor development - Avoid
:danger_full_accessunless necessary
Input Validation
- Sanitize file paths
- Validate schema JSON
- Escape shell arguments (handled by codex-rs)
Secrets Management
- Never log API keys
- Use environment variables
- Rotate keys regularly
- Use per-project API keys
Extension Points
Custom Event Handlers
defmodule MyApp.CodexHandler do
def handle_event(%ItemCompleted{item: %CommandExecution{} = cmd}) do
Logger.info("Command: #{cmd.command}, exit: #{cmd.exit_code}")
end
def handle_event(_), do: :ok
end
# Use with streaming
{:ok, stream} = Thread.run_streamed(thread, input)
Enum.each(stream, &MyApp.CodexHandler.handle_event/1)Custom Telemetry
defmodule MyApp.Metrics do
def track_usage(%Usage{} = usage) do
:telemetry.execute(
[:my_app, :codex, :tokens],
%{total: usage.input_tokens + usage.output_tokens},
%{source: :codex}
)
end
endSupervision
defmodule MyApp.CodexSupervisor do
use Supervisor
def start_link(opts) do
Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
end
def init(_opts) do
children = [
{Task.Supervisor, name: MyApp.CodexTaskSupervisor}
]
Supervisor.init(children, strategy: :one_for_one)
end
end
# Use supervised tasks for concurrent turns
Task.Supervisor.async(MyApp.CodexTaskSupervisor, fn ->
Thread.run(thread, input)
end)Shared Runtime Modules
Extracted from duplicated patterns across the codebase, these modules centralize cross-cutting concerns:
Codex.IO.Transport: Codex-branded transport surface backed byExternalRuntimeTransport.Transport; preserves the historical Codex event contract for app-server and MCP while leaving subprocess ownership in the shared substrateCodex.Runtime.Env: Subprocess environment construction shared between Exec and AppServer.Connection; setsCODEX_INTERNAL_ORIGINATOR_OVERRIDE=codex_sdk_elixirby defaultCodex.Runtime.KeyringWarning: Deduplicated warn-once logic from Auth and MCP.OAuthCodex.Config.BaseURL:OPENAI_BASE_URLenv fallback with explicit option precedence (option → env → default)Codex.Config.OptionNormalizers: Shared validation for reasoning summary, verbosity, and history persistence across Options and Thread.OptionsCodex.Config.Overrides: Config override serialization, nested map auto-flattening (flatten_config_map/1), TOML value validation, and deduplicatednormalize_config_overrides/1
Realtime and Voice Modules
The SDK includes two subsystems for voice interactions that make direct API calls to OpenAI rather than wrapping the codex CLI.
Realtime API (Codex.Realtime.*)
Full integration with OpenAI's Realtime API for bidirectional voice streaming:
Codex.Realtime.Session: WebSocket-based GenServer using WebSockex; traps linked socket exits and runs tool calls outside the callback path so the session stays responsiveCodex.Realtime.Runner: High-level orchestrator for agent sessions with automatic tool call handling, handoff execution, and guardrail integrationCodex.Realtime.Agent: Agent configuration with instructions, tools, and handoffs- PubSub-based event broadcasting with idempotent subscribe/unsubscribe
- Semantic VAD turn detection with eagerness, silence duration, and prefix padding
Voice Pipeline (Codex.Voice.*)
Non-realtime STT -> Workflow -> TTS processing:
Codex.Voice.Pipeline: Orchestrates speech-to-text, workflow processing, and text-to-speech withasync_nolinkvia ephemeralTaskSupervisorCodex.Voice.Workflow: Behaviour for custom workflow implementations (SimpleWorkflow,AgentWorkflow)Codex.Voice.Model.*: Behaviours and implementations for STT/TTS models (OpenAIgpt-4o-transcribeandgpt-4o-mini-tts)StreamQueue-backed audio queues replacing Agent-backed queues for backpressure and close semantics
Auth precedence for both: CODEX_API_KEY → auth.json OPENAI_API_KEY → OPENAI_API_KEY.
Future Enhancements
Potential Improvements
- Native JSON Parsing: NIF for faster event parsing
- Binary Protocol: Reduce overhead vs JSONL
- WebSocket Streaming: Alternative to Port for long-running sessions
- Event Persistence: Store events for replay/debugging
- Distributed Turns: Run turns on remote nodes
- Rate Limiting: Built-in API rate limiting
- Caching: Cache common responses
- Metrics Dashboard: Real-time monitoring UI
API Stability
Stable (v1.0+):
- Core module interfaces
- Event/item struct shapes
- Option struct fields
Unstable (may change):
- Telemetry event names
- Internal GenServer implementation
- Error tuple formats
Experimental:
- Custom event handlers
- Advanced streaming modes
- Performance optimizations
Model Selection Architecture Update
The long-term architecture now places all model policy in /home/home/p/g/n/cli_subprocess_core. /home/home/p/g/n/codex_sdk receives a resolved payload and does not implement provider fallback, defaulting, placeholder sanitization policy, or reasoning-effort validation on its own.
Authoritative APIs: