Current section
Files
Jump to
Current section
Files
lib/codex/config/overrides.ex
defmodule Codex.Config.Overrides do
@moduledoc """
Config override serialization, nested map flattening, and TOML value validation.
"""
alias Codex.Options
alias Codex.Thread.Options, as: ThreadOptions
@type config_override_scalar :: String.t() | boolean() | integer() | float()
@type config_override_value ::
config_override_scalar
| [config_override_value()]
| %{optional(String.t() | atom()) => config_override_value()}
@type config_override :: String.t() | {String.t(), config_override_value()}
@spec derived_overrides(Options.t(), ThreadOptions.t() | nil) :: [{String.t(), term()}]
def derived_overrides(%Options{} = codex_opts, thread_opts) do
[]
|> maybe_put_override("model_provider", thread_value(thread_opts, :model_provider))
|> maybe_put_override(
"approvals_reviewer",
thread_value(thread_opts, :approvals_reviewer)
|> encode_approvals_reviewer()
)
|> maybe_put_override("base_instructions", thread_value(thread_opts, :base_instructions))
|> maybe_put_override(
"developer_instructions",
thread_value(thread_opts, :developer_instructions)
)
|> maybe_put_override(
"model_reasoning_summary",
pick(
thread_value(thread_opts, :model_reasoning_summary),
codex_opts.model_reasoning_summary
)
)
|> maybe_put_override(
"model_personality",
pick(thread_value(thread_opts, :personality), codex_opts.model_personality)
|> encode_personality()
)
|> maybe_put_override(
"model_verbosity",
pick(thread_value(thread_opts, :model_verbosity), codex_opts.model_verbosity)
)
|> maybe_put_override(
"model_context_window",
pick(thread_value(thread_opts, :model_context_window), codex_opts.model_context_window)
)
|> maybe_put_override(
"model_auto_compact_token_limit",
codex_opts.model_auto_compact_token_limit
)
|> maybe_put_override("review_model", codex_opts.review_model)
|> maybe_put_override(
"model_supports_reasoning_summaries",
pick(
thread_value(thread_opts, :model_supports_reasoning_summaries),
codex_opts.model_supports_reasoning_summaries
)
)
|> maybe_put_override("compact_prompt", thread_value(thread_opts, :compact_prompt))
|> maybe_put_override("hide_agent_reasoning", codex_opts.hide_agent_reasoning)
|> maybe_put_override(
"show_raw_agent_reasoning",
thread_value(thread_opts, :show_raw_agent_reasoning)
)
|> maybe_put_override("tool_output_token_limit", codex_opts.tool_output_token_limit)
|> maybe_put_override("agents.max_threads", codex_opts.agent_max_threads)
|> maybe_put_override(
"history.persistence",
pick(thread_value(thread_opts, :history_persistence), codex_opts.history_persistence)
)
|> maybe_put_override(
"history.max_bytes",
pick(thread_value(thread_opts, :history_max_bytes), codex_opts.history_max_bytes)
)
|> maybe_put_override(
"features.apply_patch_freeform",
thread_value(thread_opts, :apply_patch_freeform_enabled)
)
|> maybe_put_override(
"features.view_image_tool",
thread_value(thread_opts, :view_image_tool_enabled)
)
|> maybe_put_override(
"features.unified_exec",
thread_value(thread_opts, :unified_exec_enabled)
)
|> maybe_put_override("features.skills", thread_value(thread_opts, :skills_enabled))
|> maybe_put_override("web_search", web_search_mode_override(thread_opts))
|> maybe_put_override_true(
"features.web_search_request",
web_search_request_override(thread_opts)
)
|> add_shell_environment_policy_overrides(
thread_value(thread_opts, :shell_environment_policy)
)
|> add_sandbox_policy_overrides(thread_value(thread_opts, :sandbox_policy))
|> add_provider_tuning_overrides(codex_opts, thread_opts)
end
@spec merge_config(map() | nil, Options.t(), ThreadOptions.t()) :: map() | nil
def merge_config(config, %Options{} = codex_opts, %ThreadOptions{} = thread_opts) do
base = normalize_config_map(config)
overrides = derived_overrides(codex_opts, thread_opts) ++ options_config_overrides(codex_opts)
merged =
Enum.reduce(overrides, base, fn {key, value}, acc ->
if has_config_key?(acc, key) do
acc
else
Map.put(acc, key, value)
end
end)
case merged do
%{} = map when map_size(map) == 0 -> nil
%{} = map -> map
_ -> nil
end
end
@doc """
Flattens a nested map into a list of `{dotted_key, value}` tuples.
Nested maps are recursively traversed and their keys are joined with dots.
Non-map leaf values are kept as-is.
## Examples
iex> Codex.Config.Overrides.flatten_config_map(%{"model" => %{"personality" => "friendly"}})
[{"model.personality", "friendly"}]
iex> Codex.Config.Overrides.flatten_config_map(%{"timeout" => 5000})
[{"timeout", 5000}]
"""
@spec flatten_config_map(map() | nil) :: [{String.t(), term()}]
def flatten_config_map(nil), do: []
def flatten_config_map(map) when map_size(map) == 0, do: []
def flatten_config_map(%{} = map) do
do_flatten(map, [])
end
defp do_flatten(%{} = map, prefix) do
Enum.flat_map(map, fn {key, value} ->
key_str = to_string(key)
full_key =
case prefix do
[] -> key_str
parts -> Enum.join(parts ++ [key_str], ".")
end
case value do
%{} = nested when map_size(nested) > 0 ->
do_flatten(nested, prefix ++ [key_str])
_ ->
[{full_key, value}]
end
end)
end
@spec cli_args([ThreadOptions.config_override()]) :: [String.t()]
def cli_args(overrides) do
overrides
|> List.wrap()
|> Enum.flat_map(&config_override_arg/1)
end
@spec normalize_config_overrides(term()) :: {:ok, [config_override()]} | {:error, term()}
def normalize_config_overrides(nil), do: {:ok, []}
def normalize_config_overrides(%{} = overrides) do
normalized =
if has_nested_map?(overrides) do
flatten_config_map(overrides)
else
Enum.map(overrides, fn {key, value} -> {to_string(key), value} end)
end
validate_overrides(normalized)
end
def normalize_config_overrides(overrides) when is_list(overrides) do
cond do
Keyword.keyword?(overrides) ->
overrides
|> Enum.map(fn {key, value} -> {to_string(key), value} end)
|> validate_overrides()
Enum.all?(overrides, &is_binary/1) ->
validate_overrides(overrides)
Enum.all?(overrides, &match?({_, _}, &1)) ->
overrides
|> Enum.map(fn {key, value} -> {to_string(key), value} end)
|> validate_overrides()
true ->
{:error, {:invalid_config_overrides, overrides}}
end
end
def normalize_config_overrides(value), do: {:error, {:invalid_config_overrides, value}}
defp config_override_arg({key, value}) do
["--config", "#{key}=#{encode_override_value(value)}"]
end
defp config_override_arg(value) when is_binary(value) and value != "" do
["--config", value]
end
defp config_override_arg(_), do: []
defp encode_override_value(value) when is_binary(value), do: inspect(value)
defp encode_override_value(value) when is_boolean(value), do: to_string(value)
defp encode_override_value(value) when is_integer(value) or is_float(value),
do: to_string(value)
defp encode_override_value(value) when is_list(value) do
"[" <> Enum.map_join(value, ",", &encode_override_value/1) <> "]"
end
defp encode_override_value(%{} = value) do
"{" <>
Enum.map_join(value, ",", fn {key, entry} ->
"#{encode_override_key(key)}=#{encode_override_value(entry)}"
end) <> "}"
end
defp encode_override_value(value) do
raise ArgumentError, "unsupported config override value: #{inspect(value)}"
end
defp encode_override_key(key), do: inspect(to_string(key))
defp maybe_put_override(overrides, _key, nil), do: overrides
defp maybe_put_override(overrides, _key, ""), do: overrides
defp maybe_put_override(overrides, key, value), do: overrides ++ [{key, value}]
defp maybe_put_override_true(overrides, key, true), do: overrides ++ [{key, true}]
defp maybe_put_override_true(overrides, _key, _value), do: overrides
defp pick(nil, fallback), do: fallback
defp pick(value, _fallback), do: value
defp encode_personality(nil), do: nil
defp encode_personality(value) when is_atom(value), do: Atom.to_string(value)
defp encode_personality(value) when is_binary(value), do: value
defp encode_personality(_), do: nil
defp encode_approvals_reviewer(nil), do: nil
defp encode_approvals_reviewer(value) when is_atom(value), do: Atom.to_string(value)
defp encode_approvals_reviewer(value) when is_binary(value), do: value
defp encode_approvals_reviewer(_), do: nil
defp web_search_mode_override(%ThreadOptions{} = opts) do
case normalize_web_search_mode(thread_value(opts, :web_search_mode)) do
:disabled ->
if web_search_mode_explicit?(opts), do: "disabled", else: nil
:cached ->
"cached"
:live ->
"live"
_ ->
nil
end
end
defp web_search_mode_override(_), do: nil
defp normalize_web_search_mode(:disabled), do: :disabled
defp normalize_web_search_mode("disabled"), do: :disabled
defp normalize_web_search_mode(:cached), do: :cached
defp normalize_web_search_mode("cached"), do: :cached
defp normalize_web_search_mode(:live), do: :live
defp normalize_web_search_mode("live"), do: :live
defp normalize_web_search_mode(_), do: nil
defp web_search_mode_explicit?(%ThreadOptions{web_search_mode_explicit: explicit?})
when is_boolean(explicit?),
do: explicit?
defp web_search_mode_explicit?(_), do: false
defp web_search_request_override(%ThreadOptions{} = opts) do
case thread_value(opts, :web_search_mode) do
:live -> true
"live" -> true
:cached -> nil
"cached" -> nil
:disabled -> nil
"disabled" -> nil
nil -> thread_value(opts, :web_search_enabled)
_ -> thread_value(opts, :web_search_enabled)
end
end
defp web_search_request_override(_), do: nil
defp normalize_config_map(nil), do: %{}
defp normalize_config_map(%{} = config), do: config
defp normalize_config_map(_), do: %{}
defp has_config_key?(%{} = config, key) when is_binary(key) do
Map.has_key?(config, key) ||
has_existing_atom_key?(config, key) ||
has_nested_key?(config, String.split(key, "."))
end
defp has_config_key?(_config, _key), do: false
defp has_nested_key?(_config, []), do: false
defp has_nested_key?(config, [segment]) do
case fetch_key(config, segment) do
nil -> false
_ -> true
end
end
defp has_nested_key?(config, [segment | rest]) do
case fetch_key(config, segment) do
%{} = nested -> has_nested_key?(nested, rest)
_ -> false
end
end
defp fetch_key(map, segment) when is_map(map) and is_binary(segment) do
if Map.has_key?(map, segment) do
Map.get(map, segment)
else
case existing_atom(segment) do
{:ok, atom} -> Map.get(map, atom)
:error -> nil
end
end
end
defp has_existing_atom_key?(map, key) do
case existing_atom(key) do
{:ok, atom} -> Map.has_key?(map, atom)
:error -> false
end
end
defp existing_atom(value) when is_binary(value) do
{:ok, String.to_existing_atom(value)}
rescue
ArgumentError -> :error
end
defp add_shell_environment_policy_overrides(overrides, nil), do: overrides
defp add_shell_environment_policy_overrides(overrides, policy) do
overrides
|> maybe_put_override(
"shell_environment_policy.inherit",
fetch_any(policy, ["inherit", :inherit])
)
|> maybe_put_override(
"shell_environment_policy.ignore_default_excludes",
fetch_any(policy, ["ignore_default_excludes", :ignore_default_excludes])
)
|> maybe_put_override(
"shell_environment_policy.exclude",
fetch_any(policy, ["exclude", :exclude])
)
|> maybe_put_override(
"shell_environment_policy.include_only",
fetch_any(policy, ["include_only", :include_only])
)
|> maybe_put_override("shell_environment_policy.set", fetch_any(policy, ["set", :set]))
end
defp add_sandbox_policy_overrides(overrides, nil), do: overrides
defp add_sandbox_policy_overrides(overrides, policy) do
policy = normalize_sandbox_policy(policy)
type = normalize_sandbox_policy_type(fetch_any(policy, ["type", :type]))
network_access =
normalize_network_access(
fetch_any(policy, ["network_access", :network_access, "networkAccess", :networkAccess])
)
overrides =
overrides
|> maybe_put_override(
"sandbox_workspace_write.writable_roots",
fetch_any(policy, ["writable_roots", :writable_roots, "writableRoots", :writableRoots])
)
|> maybe_put_override(
"sandbox_workspace_write.exclude_tmpdir_env_var",
fetch_any(policy, [
"exclude_tmpdir_env_var",
:exclude_tmpdir_env_var,
"excludeTmpdirEnvVar",
:excludeTmpdirEnvVar
])
)
|> maybe_put_override(
"sandbox_workspace_write.exclude_slash_tmp",
fetch_any(policy, [
"exclude_slash_tmp",
:exclude_slash_tmp,
"excludeSlashTmp",
:excludeSlashTmp
])
)
case {type, network_access} do
{"external-sandbox", value} when is_boolean(value) ->
maybe_put_override(overrides, "sandbox_external.network_access", value)
{_type, value} when is_boolean(value) ->
maybe_put_override(overrides, "sandbox_workspace_write.network_access", value)
_ ->
overrides
end
end
defp normalize_sandbox_policy(%{} = policy), do: policy
defp normalize_sandbox_policy(list) when is_list(list), do: Map.new(list)
defp normalize_sandbox_policy(policy) when is_atom(policy), do: %{type: policy}
defp normalize_sandbox_policy(policy) when is_binary(policy), do: %{type: policy}
defp normalize_sandbox_policy(_), do: %{}
defp normalize_sandbox_policy_type(nil), do: nil
defp normalize_sandbox_policy_type(:external_sandbox), do: "external-sandbox"
defp normalize_sandbox_policy_type("external_sandbox"), do: "external-sandbox"
defp normalize_sandbox_policy_type("externalSandbox"), do: "external-sandbox"
defp normalize_sandbox_policy_type("external-sandbox"), do: "external-sandbox"
defp normalize_sandbox_policy_type(value) when is_atom(value), do: Atom.to_string(value)
defp normalize_sandbox_policy_type(value) when is_binary(value), do: value
defp normalize_sandbox_policy_type(_), do: nil
defp normalize_network_access(nil), do: nil
defp normalize_network_access(value) when is_boolean(value), do: value
defp normalize_network_access(:enabled), do: true
defp normalize_network_access(:restricted), do: false
defp normalize_network_access("enabled"), do: true
defp normalize_network_access("restricted"), do: false
defp normalize_network_access(_), do: nil
defp options_config_overrides(%Options{} = codex_opts) do
codex_opts
|> Map.get(:config_overrides, [])
|> List.wrap()
|> Enum.flat_map(fn
{key, value} -> [{to_string(key), value}]
_ -> []
end)
end
defp has_nested_map?(map) when is_map(map) do
Enum.any?(map, fn {_key, value} -> is_map(value) and map_size(value) > 0 end)
end
defp validate_overrides(overrides) do
Enum.reduce_while(overrides, {:ok, []}, fn override, {:ok, acc} ->
case validate_override(override) do
{:ok, normalized} -> {:cont, {:ok, [normalized | acc]}}
{:error, _} = error -> {:halt, error}
end
end)
|> case do
{:ok, list} -> {:ok, Enum.reverse(list)}
{:error, _} = error -> error
end
end
defp validate_override(value) when is_binary(value) do
if String.trim(value) == "" do
{:error, {:invalid_config_override, value}}
else
{:ok, value}
end
end
defp validate_override({key, value}) do
with {:ok, key} <- validate_override_key(key),
:ok <- validate_override_value(value, key) do
{:ok, {key, value}}
end
end
defp validate_override(other), do: {:error, {:invalid_config_override, other}}
defp validate_override_key(key) when is_binary(key) do
if String.trim(key) == "" do
{:error, {:invalid_config_override_key, key}}
else
{:ok, key}
end
end
defp validate_override_key(key), do: key |> to_string() |> validate_override_key()
defp validate_override_value(nil, path),
do: {:error, {:invalid_config_override_value, path, nil}}
defp validate_override_value(value, _path) when is_binary(value), do: :ok
defp validate_override_value(value, _path) when is_boolean(value), do: :ok
defp validate_override_value(value, _path) when is_integer(value), do: :ok
defp validate_override_value(value, path) when is_float(value) do
if finite_float?(value) do
:ok
else
{:error, {:invalid_config_override_value, path, value}}
end
end
defp validate_override_value(value, path) when is_list(value) do
value
|> Enum.with_index()
|> Enum.reduce_while(:ok, fn {entry, idx}, :ok ->
case validate_override_value(entry, "#{path}[#{idx}]") do
:ok -> {:cont, :ok}
{:error, _} = error -> {:halt, error}
end
end)
end
defp validate_override_value(%{} = value, path) do
Enum.reduce_while(value, :ok, fn {key, entry}, :ok ->
with {:ok, normalized_key} <- validate_override_key(key),
:ok <- validate_override_value(entry, "#{path}.#{normalized_key}") do
{:cont, :ok}
else
{:error, _} = error -> {:halt, error}
end
end)
end
defp validate_override_value(value, path),
do: {:error, {:invalid_config_override_value, path, value}}
defp finite_float?(value) when is_float(value) do
_ = :erlang.float_to_binary(value)
true
rescue
_ -> false
end
defp add_provider_tuning_overrides(
overrides,
%Options{} = codex_opts,
%ThreadOptions{} = thread_opts
) do
provider_id = provider_id_for_tuning(codex_opts, thread_opts)
overrides
|> maybe_put_override(
"model_providers.#{provider_id}.request_max_retries",
thread_opts.request_max_retries
)
|> maybe_put_override(
"model_providers.#{provider_id}.stream_max_retries",
thread_opts.stream_max_retries
)
|> maybe_put_override(
"model_providers.#{provider_id}.stream_idle_timeout_ms",
thread_opts.stream_idle_timeout_ms
)
end
defp add_provider_tuning_overrides(overrides, _codex_opts, _thread_opts), do: overrides
defp provider_id_for_tuning(_codex_opts, %ThreadOptions{model_provider: provider})
when is_binary(provider) and provider != "" do
provider
end
defp provider_id_for_tuning(_codex_opts, %ThreadOptions{oss: true, local_provider: provider})
when is_binary(provider) and provider != "" do
provider
end
defp provider_id_for_tuning(%Options{model_payload: payload}, _thread_opts)
when is_map(payload) do
backend = Map.get(payload, :provider_backend, Map.get(payload, "provider_backend"))
metadata =
Map.get(payload, :backend_metadata, Map.get(payload, "backend_metadata", %{}))
|> case do
value when is_map(value) -> value
_ -> %{}
end
case backend do
:oss -> Map.get(metadata, "oss_provider", "openai")
"oss" -> Map.get(metadata, "oss_provider", "openai")
:model_provider -> Map.get(metadata, "model_provider", "openai")
"model_provider" -> Map.get(metadata, "model_provider", "openai")
_ -> "openai"
end
end
defp provider_id_for_tuning(_codex_opts, _thread_opts), do: "openai"
defp thread_value(nil, _field), do: nil
defp thread_value(%ThreadOptions{} = thread_opts, field), do: Map.get(thread_opts, field)
defp fetch_any(map, keys) when is_map(map) and is_list(keys) do
Enum.reduce_while(keys, nil, fn key, _acc ->
case Map.fetch(map, key) do
{:ok, value} -> {:halt, value}
:error -> {:cont, nil}
end
end)
end
end