Skip to content

Commit 0020f43

Browse files
committed
Adopt core model normalization in codex_sdk
Delegate mixed raw/payload model input normalization to\nCliSubprocessCore.ModelInput and keep model_payload\nauthoritative.\n\nUpdate Ollama docs, examples, and tests, and switch\nDialyzer to analyze the sibling core workspace app live\ninstead of through a stale PLT snapshot.
1 parent 3db046a commit 0020f43

10 files changed

Lines changed: 279 additions & 112 deletions

File tree

README.md

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,13 @@ The authoritative path is:
140140
- `CliSubprocessCore.ModelRegistry.validate/2`
141141
- `CliSubprocessCore.ModelRegistry.default_model/2`
142142
- `CliSubprocessCore.ModelRegistry.build_arg_payload/3`
143+
- `CliSubprocessCore.ModelInput.normalize/3`
143144

144-
`Codex.Options.new/1` resolves a shared `model_payload`, then projects the
145-
current `model` and `reasoning_effort` from that payload. `Codex.Models` is now
146-
a read-only projection of the shared core catalog. It no longer owns a separate
147-
catalog or a separate fallback/defaulting path.
145+
`Codex.Options.new/1` now delegates mixed-input normalization to
146+
`CliSubprocessCore.ModelInput.normalize/3`, then projects the current `model`
147+
and `reasoning_effort` from the authoritative shared `model_payload`.
148+
`Codex.Models` is now a read-only projection of the shared core catalog. It no
149+
longer owns a separate catalog or a separate fallback/defaulting path.
148150

149151
Operationally, that means:
150152

@@ -173,12 +175,22 @@ That causes the shared core registry to:
173175

174176
- validate the Ollama runtime
175177
- validate the local model id
176-
- enforce the Codex OSS default and reasoning rules
178+
- keep `gpt-oss:20b` as the default validated OSS model when no explicit model
179+
is supplied
177180
- return a payload that renders `--oss --local-provider ollama --model llama3.2`
178181

179182
The SDK does not infer those flags on its own.
180183
- CLI argument rendering only emits `--model` from a non-empty resolved value
181184

185+
If `ollama_base_url:` is supplied, that endpoint is carried inside the
186+
payload-owned env overrides as `CODEX_OSS_BASE_URL`. The exec and app-server
187+
transports both consume that payload data directly instead of keeping a second
188+
raw base-url path alive downstream.
189+
190+
When the chosen local model is outside Codex's built-in model metadata catalog,
191+
the upstream CLI may warn that it is using fallback metadata. That is an
192+
upstream degraded-mode distinction, not a hard model rejection in `codex_sdk`.
193+
182194
For the stateful app-server transport, the same resolved payload is rendered into
183195
supported `codex app-server --config ...` startup overrides plus `thread/start`
184196
`modelProvider` selection. The SDK does not pass unsupported exec-only OSS flags

examples/README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,24 @@ Run the same CLI-backed example set against local Codex OSS + Ollama:
3939

4040
```bash
4141
./examples/run_all.sh --ollama
42+
./examples/run_all.sh --ollama --ollama-model gpt-oss:20b
4243
./examples/run_all.sh --ollama --ollama-model llama3.2
4344
```
4445

4546
`--ollama` sets:
4647

4748
- `CODEX_PROVIDER_BACKEND=oss`
4849
- `CODEX_OSS_PROVIDER=ollama`
49-
- `CODEX_MODEL=llama3.2` by default
50+
- `CODEX_MODEL=gpt-oss:20b` by default
5051

5152
The runner checks that the requested Ollama model is installed before starting
5253
the examples.
5354

55+
`gpt-oss:20b` remains the default validated Codex/Ollama example model, but
56+
the runner also accepts other installed Ollama models such as `llama3.2`.
57+
Those non-default models may trigger upstream fallback metadata warnings and
58+
can behave less reliably under the full Codex agent prompt/tool stack.
59+
5460
In `--ollama` mode, the runner:
5561

5662
- executes the full CLI-backed example suite against the local Ollama-backed Codex route

examples/run_all.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Usage: bash examples/run_all.sh [--ollama] [--ollama-model MODEL] [--help]
1010
1111
Options:
1212
--ollama Run all examples against local Codex OSS + Ollama.
13-
--ollama-model MODEL Override the Ollama model. Default: llama3.2
13+
--ollama-model MODEL Override the Ollama model. Default: gpt-oss:20b
1414
--help Show this help text.
1515
EOF
1616
}
@@ -53,7 +53,7 @@ else
5353
fi
5454

5555
if [[ "${CODEX_PROVIDER_BACKEND:-}" == "oss" && "${CODEX_OSS_PROVIDER:-}" == "ollama" ]]; then
56-
OLLAMA_MODEL="${CODEX_MODEL:-llama3.2}"
56+
OLLAMA_MODEL="${CODEX_MODEL:-gpt-oss:20b}"
5757
export CODEX_MODEL="${OLLAMA_MODEL}"
5858

5959
if ! command -v ollama >/dev/null 2>&1; then

guides/03-api-guide.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ opts.model_payload.backend_metadata["oss_provider"]
8888
# => "ollama"
8989
```
9090

91+
`gpt-oss:20b` remains the default validated OSS example when no explicit model
92+
is provided, but other installed Ollama models can be passed through this same
93+
route.
94+
9195
---
9296

9397
## Codex.CLI and Codex.CLI.Session
@@ -204,6 +208,27 @@ Bearer headers are only attached for `wss://` or loopback `ws://` endpoints.
204208
Remote OAuth only supports `oauth: [storage: :memory]`; `:file` / `:auto`
205209
storage is rejected because remote mode does not prepare a child `CODEX_HOME`.
206210

211+
---
212+
213+
## Shared Model Normalization
214+
215+
`Codex.Options.new/1` does not own a second model-resolution pipeline. It now
216+
delegates mixed raw/payload input normalization to
217+
`CliSubprocessCore.ModelInput.normalize/3`, then projects `model`,
218+
`reasoning_effort`, and transport settings from the resulting
219+
`model_payload`.
220+
221+
That matters most on the local Ollama route:
222+
223+
- raw attrs such as `model`, `provider_backend`, `oss_provider`, and
224+
`ollama_base_url` normalize once in the shared core
225+
- an explicit `model_payload` is authoritative when supplied
226+
- conflicting raw attrs fail during normalization instead of being silently
227+
reinterpreted in the SDK
228+
- a custom Codex/Ollama base URL is carried inside payload env overrides as
229+
`CODEX_OSS_BASE_URL`, so exec and app-server transports can rely on the
230+
payload alone after normalization
231+
207232
`connect/2` also accepts `oauth:` for child-auth-aware startup:
208233

209234
```elixir

lib/codex/examples_support.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ defmodule Codex.ExamplesSupport do
1313

1414
@spec ollama_model() :: String.t()
1515
def ollama_model do
16-
System.get_env("CODEX_MODEL") || "llama3.2"
16+
System.get_env("CODEX_MODEL") || "gpt-oss:20b"
1717
end
1818

1919
@spec example_model(String.t() | nil) :: String.t()

lib/codex/options.ex

Lines changed: 47 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ defmodule Codex.Options do
66
"""
77

88
require Bitwise
9-
alias CliSubprocessCore.ModelRegistry
9+
alias CliSubprocessCore.ModelInput
1010
alias Codex.Auth
1111
alias Codex.Config.BaseURL
1212
alias Codex.Config.OptionNormalizers
@@ -80,22 +80,26 @@ defmodule Codex.Options do
8080
{:ok, base_url} <- fetch_base_url(attrs),
8181
{:ok, override} <- fetch_codex_path_override(attrs),
8282
{:ok, telemetry_prefix} <- fetch_telemetry_prefix(attrs),
83-
{:ok, model_payload} <- resolve_model_payload(attrs, auth_mode_for(api_key)),
83+
{:ok, model_input} <- normalize_model_input(attrs),
84+
model_payload = model_input.selection,
85+
normalized_attrs = model_input.attrs,
8486
{:ok, model} <- fetch_model(model_payload),
8587
{:ok, reasoning_effort} <- fetch_reasoning_effort(model_payload),
86-
{:ok, model_personality} <- fetch_model_personality(attrs),
87-
{:ok, reasoning_summary} <- fetch_reasoning_summary(attrs),
88-
{:ok, model_verbosity} <- fetch_model_verbosity(attrs),
89-
{:ok, model_context_window} <- fetch_model_context_window(attrs),
90-
{:ok, supports_reasoning_summaries} <- fetch_supports_reasoning_summaries(attrs),
91-
{:ok, model_auto_compact_token_limit} <- fetch_model_auto_compact_token_limit(attrs),
92-
{:ok, review_model} <- fetch_review_model(attrs),
93-
{:ok, history_persistence} <- fetch_history_persistence(attrs),
94-
{:ok, history_max_bytes} <- fetch_history_max_bytes(attrs),
95-
{:ok, hide_agent_reasoning} <- fetch_hide_agent_reasoning(attrs),
96-
{:ok, tool_output_token_limit} <- fetch_tool_output_token_limit(attrs),
97-
{:ok, agent_max_threads} <- fetch_agent_max_threads(attrs),
98-
{:ok, config_overrides} <- fetch_config_overrides(attrs) do
88+
{:ok, model_personality} <- fetch_model_personality(normalized_attrs),
89+
{:ok, reasoning_summary} <- fetch_reasoning_summary(normalized_attrs),
90+
{:ok, model_verbosity} <- fetch_model_verbosity(normalized_attrs),
91+
{:ok, model_context_window} <- fetch_model_context_window(normalized_attrs),
92+
{:ok, supports_reasoning_summaries} <-
93+
fetch_supports_reasoning_summaries(normalized_attrs),
94+
{:ok, model_auto_compact_token_limit} <-
95+
fetch_model_auto_compact_token_limit(normalized_attrs),
96+
{:ok, review_model} <- fetch_review_model(normalized_attrs),
97+
{:ok, history_persistence} <- fetch_history_persistence(normalized_attrs),
98+
{:ok, history_max_bytes} <- fetch_history_max_bytes(normalized_attrs),
99+
{:ok, hide_agent_reasoning} <- fetch_hide_agent_reasoning(normalized_attrs),
100+
{:ok, tool_output_token_limit} <- fetch_tool_output_token_limit(normalized_attrs),
101+
{:ok, agent_max_threads} <- fetch_agent_max_threads(normalized_attrs),
102+
{:ok, config_overrides} <- fetch_config_overrides(normalized_attrs) do
99103
{:ok,
100104
%__MODULE__{
101105
api_key: api_key,
@@ -214,46 +218,34 @@ defmodule Codex.Options do
214218

215219
defp pick(_attrs, [], default), do: default
216220

217-
defp resolve_model_payload(attrs, _auth_mode) do
218-
requested_model = pick(attrs, [:model, "model"])
219-
220-
provider_backend =
221-
pick(
222-
attrs,
223-
[:provider_backend, "provider_backend"],
224-
System.get_env("CODEX_PROVIDER_BACKEND")
225-
)
226-
227-
oss_provider =
228-
pick(attrs, [:oss_provider, "oss_provider"], System.get_env("CODEX_OSS_PROVIDER"))
229-
230-
ollama_base_url =
231-
pick(attrs, [:ollama_base_url, "ollama_base_url"], System.get_env("CODEX_OLLAMA_BASE_URL"))
232-
233-
requested_reasoning =
234-
pick(attrs, [:reasoning_effort, "reasoning_effort", :reasoning, "reasoning"])
235-
236-
opts =
237-
[]
238-
|> maybe_put_env_model(
239-
System.get_env("CODEX_MODEL") ||
240-
System.get_env("OPENAI_DEFAULT_MODEL") ||
241-
System.get_env("CODEX_MODEL_DEFAULT")
242-
)
243-
|> maybe_put_reasoning(requested_reasoning)
244-
|> maybe_put_registry_opt(:provider_backend, provider_backend)
245-
|> maybe_put_registry_opt(:oss_provider, oss_provider)
246-
|> maybe_put_registry_opt(:ollama_base_url, ollama_base_url)
247-
|> maybe_put_registry_opt(
248-
:ollama_http,
249-
pick(attrs, [:ollama_http, "ollama_http"])
250-
)
251-
|> maybe_put_registry_opt(
252-
:ollama_timeout_ms,
253-
pick(attrs, [:ollama_timeout_ms, "ollama_timeout_ms"])
254-
)
255-
256-
ModelRegistry.build_arg_payload(:codex, requested_model, opts)
221+
defp normalize_model_input(attrs) do
222+
attrs
223+
|> apply_model_env_defaults()
224+
|> then(&ModelInput.normalize(:codex, &1, []))
225+
end
226+
227+
defp apply_model_env_defaults(attrs) when is_map(attrs) do
228+
attrs
229+
|> put_missing_attr(
230+
:env_model,
231+
System.get_env("CODEX_MODEL") ||
232+
System.get_env("OPENAI_DEFAULT_MODEL") ||
233+
System.get_env("CODEX_MODEL_DEFAULT")
234+
)
235+
|> put_missing_attr(:provider_backend, System.get_env("CODEX_PROVIDER_BACKEND"))
236+
|> put_missing_attr(:oss_provider, System.get_env("CODEX_OSS_PROVIDER"))
237+
|> put_missing_attr(:ollama_base_url, System.get_env("CODEX_OLLAMA_BASE_URL"))
238+
end
239+
240+
defp put_missing_attr(attrs, _key, nil), do: attrs
241+
defp put_missing_attr(attrs, _key, ""), do: attrs
242+
243+
defp put_missing_attr(attrs, key, value) when is_map(attrs) and is_atom(key) do
244+
cond do
245+
Map.has_key?(attrs, key) -> attrs
246+
Map.has_key?(attrs, Atom.to_string(key)) -> attrs
247+
true -> Map.put(attrs, key, value)
248+
end
257249
end
258250

259251
defp fetch_model(model_payload) when is_map(model_payload) do
@@ -267,16 +259,6 @@ defmodule Codex.Options do
267259
{:ok, normalize_reasoning_atom(reasoning)}
268260
end
269261

270-
defp maybe_put_env_model(opts, nil), do: opts
271-
defp maybe_put_env_model(opts, env_model), do: Keyword.put(opts, :env_model, env_model)
272-
273-
defp maybe_put_reasoning(opts, nil), do: opts
274-
defp maybe_put_reasoning(opts, reasoning), do: Keyword.put(opts, :reasoning_effort, reasoning)
275-
276-
defp maybe_put_registry_opt(opts, _key, nil), do: opts
277-
defp maybe_put_registry_opt(opts, _key, ""), do: opts
278-
defp maybe_put_registry_opt(opts, key, value), do: Keyword.put(opts, key, value)
279-
280262
defp normalize_reasoning_atom(nil), do: nil
281263
defp normalize_reasoning_atom(value) when is_atom(value), do: value
282264
defp normalize_reasoning_atom(value) when is_binary(value), do: String.to_atom(value)
@@ -423,9 +405,6 @@ defmodule Codex.Options do
423405
|> Overrides.normalize_config_overrides()
424406
end
425407

426-
defp auth_mode_for(api_key) when is_binary(api_key) and api_key != "", do: :api
427-
defp auth_mode_for(_), do: Auth.infer_auth_mode()
428-
429408
defp normalize_string(nil), do: nil
430409

431410
defp normalize_string(value) when is_binary(value) do

0 commit comments

Comments
 (0)