Current section
Files
Jump to
Current section
Files
lib/codex/options.ex
defmodule Codex.Options do
@moduledoc """
Global configuration for Codex interactions.
Options are built from caller-supplied values merged with environment defaults.
"""
require Bitwise
alias Codex.Models
@default_base_url "https://api.openai.com/v1"
@enforce_keys []
defstruct api_key: nil,
base_url: @default_base_url,
codex_path_override: nil,
telemetry_prefix: [:codex],
model: Models.default_model(),
reasoning_effort: Models.default_reasoning_effort()
@type t :: %__MODULE__{
api_key: String.t() | nil,
base_url: String.t(),
codex_path_override: String.t() | nil,
telemetry_prefix: [atom()],
model: String.t() | nil,
reasoning_effort: Models.reasoning_effort() | nil
}
@doc """
Builds a validated options struct.
The API key is required. It can be provided directly or via the `CODEX_API_KEY`
environment variable.
"""
@spec new(map() | keyword()) :: {:ok, t()} | {:error, term()}
def new(attrs \\ %{}) do
attrs = Map.new(attrs)
with {:ok, api_key} <- fetch_api_key(attrs),
{:ok, base_url} <- fetch_base_url(attrs),
{:ok, override} <- fetch_codex_path_override(attrs),
{:ok, telemetry_prefix} <- fetch_telemetry_prefix(attrs),
{:ok, model} <- fetch_model(attrs),
{:ok, reasoning_effort} <- fetch_reasoning_effort(attrs, model) do
{:ok,
%__MODULE__{
api_key: api_key,
base_url: base_url,
codex_path_override: override,
telemetry_prefix: telemetry_prefix,
model: model,
reasoning_effort: reasoning_effort
}}
end
end
@doc """
Determines the executable path to `codex-rs`.
Order of precedence:
1. Explicit override on the struct.
2. `CODEX_PATH` environment variable.
3. `System.find_executable("codex")`.
"""
@spec codex_path(t()) :: {:ok, String.t()} | {:error, term()}
def codex_path(%__MODULE__{codex_path_override: override}) when is_binary(override) do
validate_executable(override)
end
def codex_path(%__MODULE__{} = opts) do
env_path = System.get_env("CODEX_PATH")
path =
if env_path && env_path != "" do
env_path
else
System.find_executable("codex")
end
case path do
nil -> {:error, :codex_binary_not_found}
path -> validate_executable(path)
end
|> add_override_ref(opts)
end
defp add_override_ref(result, %__MODULE__{codex_path_override: nil}), do: result
defp add_override_ref(result, _opts), do: result
defp fetch_api_key(attrs) do
case pick(attrs, [:api_key, "api_key"], System.get_env("CODEX_API_KEY")) do
key when is_binary(key) and key != "" ->
{:ok, key}
_ ->
{:ok, fetch_cli_api_key()}
end
end
defp fetch_cli_api_key do
cli_auth_paths()
|> Enum.find_value(&read_auth_token/1)
end
defp cli_auth_paths do
codex_home =
System.get_env("CODEX_HOME") ||
Path.join(System.user_home!(), ".codex")
[
Path.join(codex_home, "auth.json"),
Path.join(codex_home, ".credentials.json"),
Path.join(System.user_home!(), ".config/codex/credentials.json"),
Path.join(System.user_home!(), ".config/openai/codex.json"),
Path.join(System.user_home!(), ".codex/credentials.json")
]
|> Enum.uniq()
end
defp read_auth_token(path) do
with true <- File.exists?(path),
{:ok, contents} <- File.read(path),
{:ok, decoded} <- Jason.decode(contents),
token when is_binary(token) and token != "" <- extract_token(decoded) do
token
else
_ -> nil
end
end
defp extract_token(%{"OPENAI_API_KEY" => token}), do: token
defp extract_token(%{"access_token" => token}), do: token
defp extract_token(%{"tokens" => %{"access_token" => token}}), do: token
defp extract_token(%{"tokens" => %{"token" => token}}), do: token
defp extract_token(_), do: nil
defp fetch_base_url(attrs) do
case pick(attrs, [:base_url, "base_url"], @default_base_url) do
url when is_binary(url) and url != "" -> {:ok, url}
_ -> {:error, :invalid_base_url}
end
end
defp fetch_codex_path_override(attrs) do
case pick(attrs, [:codex_path_override, "codex_path_override", :codex_path, "codex_path"]) do
nil -> {:ok, nil}
"" -> {:error, :invalid_codex_path}
override -> {:ok, override}
end
end
defp fetch_telemetry_prefix(attrs) do
case pick(attrs, [:telemetry_prefix, "telemetry_prefix"], [:codex]) do
prefix when is_list(prefix) ->
if Enum.all?(prefix, &is_atom/1) do
{:ok, prefix}
else
{:error, {:invalid_telemetry_prefix, prefix}}
end
other ->
{:error, {:invalid_telemetry_prefix, other}}
end
end
defp validate_executable(path) do
with true <- File.exists?(path) || {:error, {:codex_binary_missing, path}},
{:ok, stat} <- File.stat(path),
true <- stat.type == :regular || {:error, {:codex_binary_not_regular, path}},
true <-
Bitwise.band(stat.mode, 0o111) > 0 || {:error, {:codex_binary_not_executable, path}} do
{:ok, path}
else
{:error, _} = error -> error
false -> {:error, {:codex_binary_missing, path}}
error when is_atom(error) -> {:error, error}
end
end
defp pick(attrs, keys, default \\ nil)
defp pick(attrs, [key | rest], default) do
case Map.get(attrs, key) do
nil -> pick(attrs, rest, default)
value -> value
end
end
defp pick(_attrs, [], default), do: default
defp fetch_model(attrs) do
case pick(attrs, [:model, "model"], Models.default_model()) do
nil -> {:ok, nil}
"" -> {:ok, nil}
model -> {:ok, model}
end
end
defp fetch_reasoning_effort(attrs, model) do
default = Models.default_reasoning_effort(model)
attrs
|> pick([:reasoning_effort, "reasoning_effort", :reasoning, "reasoning"], default)
|> Models.normalize_reasoning_effort()
end
end