Models and Reasoning Configuration

Copy Markdown View Source

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:

ContextDefaultSource
API auth modeCodex.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 modeCodex.Models.default_model(:chatgpt)First picker-visible ChatGPT model from the active catalog, with Codex.Config.Defaults.default_chatgpt_model/0 as fallback
Realtime sessionsCodex.Realtime.Agent.default_model()@default_model in Codex.Realtime.Agent
Speech-to-textCodex.Voice.Models.OpenAISTT.model_name()@default_model in OpenAISTT
Text-to-speechCodex.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:

  1. CODEX_MODEL
  2. OPENAI_DEFAULT_MODEL
  3. CODEX_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 identifier
  • description - short human-readable description
  • default_reasoning_effort - the effort level used when none is specified
  • supported_reasoning_efforts - the effort levels the model accepts
  • is_default - whether this is the default for the auth mode
  • upgrade - 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

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

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

  1. Bundled catalog defaults - Codex.Models.default_model() picks the first visible model for the inferred auth mode (falling back to Codex.Config.Defaults only if catalog resolution fails), with :medium effort
  2. Environment variables - CODEX_MODEL, etc.
  3. Codex.Options - :model and :reasoning_effort fields
  4. Codex.Thread.Options - per-thread overrides
  5. Codex.Thread.Options.config_overrides - TOML-style key/value pairs
  6. Per-turn config_overrides - passed to Codex.Thread.run/3

openai_base_url and model_providers

Layered config.toml files can also affect provider resolution:

  • openai_base_url overrides the built-in openai provider base URL and wins over OPENAI_BASE_URL
  • user [model_providers.<id>] entries extend the built-in provider set
  • reserved built-ins such as openai, ollama, and lmstudio cannot 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" => true

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

ConstantModuleUsed By
Bundled priv/models.json catalogCodex.ModelsVisible model listing, default selection, upgrade metadata
Codex.Config.Defaults.default_api_model/0 and default_chatgpt_model/0Codex.Config.DefaultsFallback when catalog-based default selection cannot resolve
@default_modelCodex.Realtime.AgentCodex.Realtime.Session, examples
@default_modelCodex.Voice.Models.OpenAISTTOpenAIProvider, examples
@default_modelCodex.Voice.Models.OpenAITTSOpenAIProvider, examples
@efforts_fullCodex.ModelsShared across model presets with full effort support
@efforts_miniCodex.ModelsShared across mini-class model presets
@efforts_standardCodex.ModelsShared 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()
end

Available fixtures: default_model/0, alt_model/0, max_model/0, realtime_model/0, stt_model/0, tts_model/0.