This guide explains how model selection and reasoning-effort levels work in the Codex SDK, where canonical defaults live, and how to override them at every layer of the configuration stack.
Quick Reference
# Use the SDK default model (currently resolves to gpt-5.4 from the bundled catalog)
{:ok, opts} = Codex.Options.new(%{})
# Explicitly choose a model
{:ok, opts} = Codex.Options.new(%{model: "gpt-5.1-codex-mini"})
# Override reasoning effort
{:ok, opts} = Codex.Options.new(%{model: "gpt-5.2", reasoning_effort: :high})
# Use the realtime default model
agent = %Codex.Realtime.Agent{model: Codex.Realtime.Agent.default_model()}Model Defaults
The SDK derives default text models in Codex.Models:
| Context | Default | Source |
|---|---|---|
| API auth mode | Codex.Models.default_model(:api) | First picker-visible API-supported model from the active catalog, with Codex.Config.Defaults.default_api_model/0 as fallback |
| ChatGPT auth mode | Codex.Models.default_model(:chatgpt) | First picker-visible ChatGPT model from the active catalog, with Codex.Config.Defaults.default_chatgpt_model/0 as fallback |
| Realtime sessions | Codex.Realtime.Agent.default_model() | @default_model in Codex.Realtime.Agent |
| Speech-to-text | Codex.Voice.Models.OpenAISTT.model_name() | @default_model in OpenAISTT |
| Text-to-speech | Codex.Voice.Models.OpenAITTS.model_name() | @default_model in OpenAITTS |
The exact text default is catalog-derived, not a permanent public contract. With
the bundled catalog vendored in this repo, both text auth modes currently resolve
to gpt-5.4 unless env overrides or a fresher ChatGPT /models cache win.
The bundled offline catalog lives in priv/models.json and is synced from the
vendored upstream file at codex/codex-rs/core/models.json.
Persistent Codex.OAuth login participates in the same ChatGPT auth-mode model
selection. Memory-only external app-server auth is connection-local and does not
change the current BEAM process's default-model inference on its own.
Environment Overrides
The SDK checks these environment variables (in order) before falling back to the auth-aware catalog default:
CODEX_MODELOPENAI_DEFAULT_MODELCODEX_MODEL_DEFAULT
CODEX_MODEL=gpt-5.1-codex-max mix run my_script.exs
Available Models
Call Codex.Models.list_visible/1 to see what models are available for the
current auth mode:
iex> Codex.Models.list_visible(:api) |> Enum.map(& &1.id)
#=> [
#=> "gpt-5.3-codex",
#=> "gpt-5.4",
#=> "gpt-5.2-codex",
#=> "gpt-5.1-codex-max",
#=> "gpt-5.2",
#=> "gpt-5.1-codex-mini"
#=> ]That is the current bundled picker-visible snapshot shipped with this repo.
ChatGPT-auth /models refreshes can expose additional runtime-visible models
such as gpt-5.4-mini, so treat the bundled list as the offline baseline rather
than the maximum possible menu.
Each model preset includes:
id/model/display_name- the model identifierdescription- short human-readable descriptiondefault_reasoning_effort- the effort level used when none is specifiedsupported_reasoning_efforts- the effort levels the model acceptsis_default- whether this is the default for the auth modeupgrade- optional upgrade path to a newer model
Reasoning Effort
Reasoning effort controls how much "thinking" the model does before responding. Higher effort produces better answers for complex problems but increases latency and cost.
Valid Levels
| Atom | String | Description |
|---|---|---|
:none | "none" | No reasoning |
:minimal | "minimal" | Minimal reasoning |
:low | "low" | Fast responses with lighter reasoning |
:medium | "medium" | Balanced speed and reasoning depth (default) |
:high | "high" | Greater reasoning depth for complex problems |
:xhigh | "xhigh" | Extra-high reasoning for the most complex problems |
Aliases "extra_high" and "extra-high" are also accepted and normalize to
:xhigh.
Setting Reasoning Effort
At the Options level (applies to all threads):
{:ok, opts} = Codex.Options.new(%{reasoning_effort: :high})At the Thread level (per-thread override):
{:ok, thread_opts} = Codex.Thread.Options.new(%{reasoning_effort: :low})Per-turn (via config overrides):
Codex.Thread.run(thread, "complex question", %{
config_overrides: [{"model_reasoning_effort", "xhigh"}]
})Automatic Coercion
Not all models support all effort levels. When you request an unsupported level, the SDK automatically coerces it to the nearest supported value:
# gpt-5.1-codex-mini only supports :medium and :high
iex> Codex.Models.coerce_reasoning_effort("gpt-5.1-codex-mini", :xhigh)
:high
iex> Codex.Models.coerce_reasoning_effort("gpt-5.1-codex-mini", :low)
:mediumUse Codex.Models.supported_reasoning_efforts/1 to query what a model accepts:
iex> Codex.Models.supported_reasoning_efforts("gpt-5.1-codex-mini")
[
%{effort: :medium, description: "Dynamically adjusts reasoning based on the task"},
%{effort: :high, description: "Maximizes reasoning depth for complex or ambiguous problems"}
]Normalizing Effort Values
Use Codex.Models.normalize_reasoning_effort/1 to parse strings or atoms:
iex> Codex.Models.normalize_reasoning_effort("extra_high")
{:ok, :xhigh}
iex> Codex.Models.normalize_reasoning_effort(:medium)
{:ok, :medium}
iex> Codex.Models.normalize_reasoning_effort("invalid")
{:error, {:invalid_reasoning_effort, "invalid"}}Configuration Layers
Model and reasoning configuration follows the SDK's layered override system. Later layers take precedence:
- Bundled catalog defaults -
Codex.Models.default_model()picks the first visible model for the inferred auth mode (falling back toCodex.Config.Defaultsonly if catalog resolution fails), with:mediumeffort - Environment variables -
CODEX_MODEL, etc. Codex.Options-:modeland:reasoning_effortfieldsCodex.Thread.Options- per-thread overridesCodex.Thread.Options.config_overrides- TOML-style key/value pairs- Per-turn
config_overrides- passed toCodex.Thread.run/3
openai_base_url and model_providers
Layered config.toml files can also affect provider resolution:
openai_base_urloverrides the built-inopenaiprovider base URL and wins overOPENAI_BASE_URL- user
[model_providers.<id>]entries extend the built-in provider set - reserved built-ins such as
openai,ollama, andlmstudiocannot be overridden
Example:
openai_base_url = "https://gateway.example.com/v1"
[model_providers.openai_custom]
name = "OpenAI Custom"
base_url = "https://gateway.example.com/v1"
env_key = "OPENAI_API_KEY"
wire_api = "responses"Use model_provider = "openai_custom" in config, or pass model_provider in thread options,
when you want turns to target the custom provider ID.
Config Overrides
Both Codex.Options and Codex.Thread.Options accept a :config map that
gets serialized as --config key=value CLI flags:
{:ok, opts} = Codex.Options.new(%{
config: %{
"model_reasoning_effort" => "xhigh",
"model_reasoning_summary" => "concise"
}
})Nested maps are automatically flattened with dot notation:
%{"sandbox_workspace_write" => %{"network_access" => true}}
# becomes: "sandbox_workspace_write.network_access" => trueModel Verbosity
Model verbosity is separate from reasoning effort. It controls how much detail the model includes in its responses:
{:ok, thread_opts} = Codex.Thread.Options.new(%{model_verbosity: :low})Valid values: :low, :medium, :high (or their string equivalents).
Realtime and Voice Models
Realtime and voice subsystems use separate model families:
Realtime
# Uses the default realtime model
agent = Codex.Realtime.agent(name: "Assistant")
# Override with a specific model
agent = Codex.Realtime.agent(name: "Mini", model: "gpt-4o-mini-realtime-preview")The default realtime model is accessible via Codex.Realtime.Agent.default_model/0.
Voice (STT/TTS)
# Default STT model
stt = Codex.Voice.Models.OpenAISTT.new()
# Default TTS model
tts = Codex.Voice.Models.OpenAITTS.new()
# Custom models
stt = Codex.Voice.Models.OpenAISTT.new("whisper-1")
tts = Codex.Voice.Models.OpenAITTS.new("tts-1-hd")The OpenAIProvider delegates to the individual STT/TTS module defaults.
Upgrade Paths
Some models have upgrade paths to newer versions. Query them with:
iex> Codex.Models.get_upgrade("gpt-5.1-codex-max")
%{id: target, migration_config_key: key, reasoning_effort_mapping: mapping, ...}Upgrade targets come from the bundled/current catalog and can change across upstream pulls.
Architecture: Where Defaults Live
The SDK follows a single-source-of-truth pattern for model defaults and model metadata:
| Constant | Module | Used By |
|---|---|---|
Bundled priv/models.json catalog | Codex.Models | Visible model listing, default selection, upgrade metadata |
Codex.Config.Defaults.default_api_model/0 and default_chatgpt_model/0 | Codex.Config.Defaults | Fallback when catalog-based default selection cannot resolve |
@default_model | Codex.Realtime.Agent | Codex.Realtime.Session, examples |
@default_model | Codex.Voice.Models.OpenAISTT | OpenAIProvider, examples |
@default_model | Codex.Voice.Models.OpenAITTS | OpenAIProvider, examples |
@efforts_full | Codex.Models | Shared across model presets with full effort support |
@efforts_mini | Codex.Models | Shared across mini-class model presets |
@efforts_standard | Codex.Models | Shared across standard codex presets |
Downstream modules reference these via public functions (default_model/0,
model_name/0) rather than duplicating string literals.
For Tests
Import Codex.Test.ModelFixtures to reference canonical model constants:
import Codex.Test.ModelFixtures
test "uses the default model" do
{:ok, opts} = Options.new(%{})
assert opts.model == default_model()
endAvailable fixtures: default_model/0, alt_model/0, max_model/0,
realtime_model/0, stt_model/0, tts_model/0.