Current section
Files
Jump to
Current section
Files
lib/codex/models.ex
defmodule Codex.Models do
@moduledoc """
Codex model metadata projected from `cli_subprocess_core`.
"""
alias CliSubprocessCore.ModelRegistry
alias CliSubprocessCore.ModelRegistry.Model, as: RegistryModel
alias Codex.Auth
alias Codex.Config.BaseURL
alias Codex.Config.Defaults
alias Codex.Config.LayerStack
alias Codex.Net.CA
@type reasoning_effort :: :none | :minimal | :low | :medium | :high | :xhigh
@type reasoning_effort_preset :: %{
effort: reasoning_effort(),
description: String.t()
}
@type model_upgrade :: %{
id: String.t(),
reasoning_effort_mapping: %{reasoning_effort() => reasoning_effort()} | nil,
migration_config_key: String.t(),
model_link: String.t() | nil,
upgrade_copy: String.t() | nil
}
@type model_preset :: %{
id: String.t(),
model: String.t(),
display_name: String.t(),
description: String.t(),
default_reasoning_effort: reasoning_effort(),
supported_reasoning_efforts: [reasoning_effort_preset()],
is_default: boolean(),
upgrade: model_upgrade() | nil,
show_in_picker: boolean(),
supported_in_api: boolean(),
family: String.t() | nil
}
@type client_version :: {non_neg_integer(), non_neg_integer(), non_neg_integer()}
@reasoning_efforts [:none, :minimal, :low, :medium, :high, :xhigh]
@reasoning_effort_aliases %{
"none" => :none,
"extra_high" => :xhigh,
"extra-high" => :xhigh,
"minimal" => :minimal,
"low" => :low,
"medium" => :medium,
"high" => :high,
"xhigh" => :xhigh
}
@doc """
Returns the list of supported models visible for the inferred auth mode.
"""
@spec list() :: [model_preset()]
def list, do: list_visible(Auth.infer_auth_mode())
@doc """
Returns models visible in the model picker.
"""
@spec list_visible() :: [model_preset()]
@spec list_visible(:api | :chatgpt) :: [model_preset()]
@spec list_visible(:api | :chatgpt, keyword()) :: [model_preset()]
def list_visible(_auth_mode \\ :api, opts \\ []) do
family = Keyword.get(opts, :model_family) |> normalize_optional_binary()
default_id = core_default_model!()
:codex
|> ModelRegistry.list_visible(visibility: :public)
|> unwrap_registry!(:list_visible)
|> Enum.map(&fetch_model!/1)
|> Enum.map(®istry_model_to_preset(&1, default_id))
|> Enum.filter(fn preset -> is_nil(family) or preset.family == family end)
end
@doc """
Returns the SDK default model from the shared core registry.
"""
@spec default_model() :: String.t()
@spec default_model(:api | :chatgpt) :: String.t()
def default_model(_auth_mode \\ Auth.infer_auth_mode()), do: core_default_model!()
@doc """
Returns the default reasoning effort for the given model (or the default model).
"""
@spec default_reasoning_effort(String.t() | atom() | nil) :: reasoning_effort() | nil
def default_reasoning_effort(model \\ default_model()) do
case find_model(model) do
%{default_reasoning_effort: effort} when is_atom(effort) -> effort
_ -> :medium
end
end
@doc """
Parses a reasoning effort value into its canonical atom form.
"""
@spec normalize_reasoning_effort(String.t() | atom() | nil) ::
{:ok, reasoning_effort() | nil} | {:error, term()}
def normalize_reasoning_effort(nil), do: {:ok, nil}
def normalize_reasoning_effort(value) when is_atom(value) do
if value in @reasoning_efforts do
{:ok, value}
else
{:error, {:invalid_reasoning_effort, value}}
end
end
def normalize_reasoning_effort(value) when is_binary(value) do
normalized = value |> String.trim() |> String.downcase()
cond do
normalized == "" ->
{:ok, nil}
Map.has_key?(@reasoning_effort_aliases, normalized) ->
{:ok, Map.fetch!(@reasoning_effort_aliases, normalized)}
true ->
{:error, {:invalid_reasoning_effort, normalized}}
end
end
def normalize_reasoning_effort(value), do: {:error, {:invalid_reasoning_effort, value}}
@doc """
Returns `true` when the given model supports tool execution.
"""
@spec tool_enabled?(String.t() | atom() | nil) :: boolean()
def tool_enabled?(model), do: not is_nil(find_model(model))
@doc """
Lists the valid reasoning effort values understood by the SDK.
"""
@spec reasoning_efforts() :: nonempty_list(reasoning_effort())
def reasoning_efforts, do: @reasoning_efforts
@doc """
Renders a normalized reasoning effort as the CLI-friendly string value.
"""
@spec reasoning_effort_to_string(reasoning_effort()) :: String.t()
def reasoning_effort_to_string(effort) when effort in @reasoning_efforts do
Atom.to_string(effort)
end
@doc """
Returns the upgrade information for a model, if available.
"""
@spec get_upgrade(String.t()) :: model_upgrade() | nil
def get_upgrade(model_id) do
case find_model(model_id) do
%{upgrade: upgrade} -> upgrade
_ -> nil
end
end
@doc """
Returns the supported reasoning efforts for a model.
"""
@spec supported_reasoning_efforts(String.t()) :: [reasoning_effort_preset()]
def supported_reasoning_efforts(model_id) do
case find_model(model_id) do
%{supported_reasoning_efforts: efforts} -> efforts
_ -> []
end
end
@doc """
Coerces a reasoning effort to the nearest supported value for a model.
Returns the input effort unchanged when the model is unknown or already supports it.
"""
@spec coerce_reasoning_effort(String.t() | atom() | nil, reasoning_effort() | nil) ::
reasoning_effort() | nil
def coerce_reasoning_effort(_model, nil), do: nil
def coerce_reasoning_effort(nil, effort), do: effort
def coerce_reasoning_effort(model, effort) do
supported = supported_reasoning_efforts(to_string(model)) |> Enum.map(& &1.effort)
cond do
supported == [] -> effort
effort in supported -> effort
true -> nearest_effort(effort, supported)
end
end
@doc """
Returns true if a model is supported via API key authentication.
"""
@spec supported_in_api?(String.t()) :: boolean()
def supported_in_api?(model_id) do
case find_model(model_id) do
%{supported_in_api: supported} -> supported
_ -> false
end
end
@doc """
Returns the display name for a model, if known.
"""
@spec display_name(String.t() | atom()) :: String.t() | nil
def display_name(model_id) do
case find_model(model_id) do
%{display_name: display_name} -> display_name
_ -> nil
end
end
@doc """
Returns the description for a model, if known.
"""
@spec description(String.t() | atom()) :: String.t() | nil
def description(model_id) do
case find_model(model_id) do
%{description: description} -> description
_ -> nil
end
end
@doc false
@spec remote_models_http_options() :: keyword()
def remote_models_http_options do
[timeout: Defaults.remote_models_http_timeout_ms()]
|> CA.merge_httpc_options()
end
@doc false
@spec remote_models_url(String.t() | nil) :: String.t()
def remote_models_url(cwd \\ default_cwd()) do
models_url(cwd)
end
@doc false
@spec parse_client_version(term()) :: client_version()
def parse_client_version([major, minor, patch])
when is_integer(major) and is_integer(minor) and is_integer(patch) and
major >= 0 and minor >= 0 and patch >= 0 do
{major, minor, patch}
end
def parse_client_version(value) when is_binary(value) do
value
|> String.split("-", parts: 2)
|> List.first()
|> String.split(".")
|> Enum.take(3)
|> Enum.map(&Integer.parse/1)
|> case do
[{major, ""}, {minor, ""}, {patch, ""}] when major >= 0 and minor >= 0 and patch >= 0 ->
{major, minor, patch}
_ ->
{0, 0, 0}
end
end
def parse_client_version(_), do: {0, 0, 0}
defp find_model(model_id) do
case normalize_model(model_id) do
nil -> nil
normalized -> ModelRegistry.validate(:codex, normalized) |> normalize_model_result()
end
end
defp normalize_model_result({:ok, %RegistryModel{} = model}) do
registry_model_to_preset(model, core_default_model!())
end
defp normalize_model_result({:error, _reason}), do: nil
defp fetch_model!(model_id) do
:codex
|> ModelRegistry.validate(model_id)
|> unwrap_registry!(:validate)
end
defp registry_model_to_preset(%RegistryModel{} = model, default_id) do
default_reasoning_effort =
model.default_reasoning_effort
|> normalize_reasoning_atom()
|> Kernel.||(:medium)
%{
id: model.id,
model: model.id,
display_name: Map.get(model.metadata, "display_name", model.id),
description: Map.get(model.metadata, "description", ""),
default_reasoning_effort: default_reasoning_effort,
supported_reasoning_efforts: reasoning_presets_from_model(model),
is_default: model.id == default_id,
upgrade: normalize_upgrade(model),
show_in_picker: model.visibility == :public,
supported_in_api: model.visibility == :public,
family: model.family
}
end
defp reasoning_presets_from_model(%RegistryModel{} = model) do
@reasoning_efforts
|> Enum.filter(&Map.has_key?(model.reasoning_efforts, Atom.to_string(&1)))
|> Enum.map(fn effort ->
%{effort: effort, description: reasoning_effort_description(effort)}
end)
end
defp normalize_upgrade(%RegistryModel{metadata: %{"upgrade" => upgrade}})
when is_map(upgrade) do
id = Map.get(upgrade, "id") || Map.get(upgrade, :id)
if is_binary(id) and String.trim(id) != "" do
%{
id: String.trim(id),
reasoning_effort_mapping:
Map.get(upgrade, "reasoning_effort_mapping") ||
Map.get(upgrade, :reasoning_effort_mapping),
migration_config_key:
Map.get(upgrade, "migration_config_key") || Map.get(upgrade, :migration_config_key) ||
String.trim(id),
model_link: Map.get(upgrade, "model_link") || Map.get(upgrade, :model_link),
upgrade_copy: Map.get(upgrade, "upgrade_copy") || Map.get(upgrade, :upgrade_copy)
}
end
end
defp normalize_upgrade(_model), do: nil
defp normalize_reasoning_atom(nil), do: nil
defp normalize_reasoning_atom(value) do
case normalize_reasoning_effort(value) do
{:ok, effort} -> effort
{:error, _reason} -> nil
end
end
defp reasoning_effort_description(effort) when effort in @reasoning_efforts do
effort
|> Atom.to_string()
|> String.replace("_", " ")
|> String.capitalize()
end
defp core_default_model! do
:codex
|> ModelRegistry.default_model()
|> unwrap_registry!(:default_model)
end
defp unwrap_registry!({:ok, value}, _operation), do: value
defp unwrap_registry!({:error, reason}, operation) do
raise ArgumentError, "codex model registry #{operation} failed: #{inspect(reason)}"
end
defp normalize_model(nil), do: nil
defp normalize_model(model) when is_binary(model), do: String.trim(model)
defp normalize_model(model), do: model |> to_string() |> String.trim()
defp nearest_effort(target, supported) do
target_rank = effort_rank(target)
supported
|> Enum.min_by(fn candidate -> abs(effort_rank(candidate) - target_rank) end, fn -> target end)
end
defp effort_rank(:none), do: 0
defp effort_rank(:minimal), do: 1
defp effort_rank(:low), do: 2
defp effort_rank(:medium), do: 3
defp effort_rank(:high), do: 4
defp effort_rank(:xhigh), do: 5
defp models_url(cwd) do
base = resolve_models_base_url(cwd) |> String.trim_trailing("/")
client_version = client_version()
"#{base}/models?client_version=#{client_version}"
end
defp resolve_models_base_url(cwd) do
case LayerStack.load(Auth.codex_home(), cwd) do
{:ok, layers} ->
layers
|> LayerStack.effective_config()
|> BaseURL.resolve()
{:error, _reason} ->
BaseURL.resolve()
end
end
defp client_version do
case Application.spec(:codex_sdk, :vsn) do
nil -> "99.99.99"
vsn -> vsn |> to_string() |> parse_client_version() |> format_client_version()
end
end
defp format_client_version({major, minor, patch}), do: "#{major}.#{minor}.#{patch}"
defp default_cwd do
case File.cwd() do
{:ok, cwd} -> cwd
_ -> nil
end
end
defp normalize_optional_binary(nil), do: nil
defp normalize_optional_binary(value) when is_binary(value) do
case String.trim(value) do
"" -> nil
trimmed -> trimmed
end
end
defp normalize_optional_binary(_other), do: nil
end