Skip to content

Commit b4bddb2

Browse files
committed
Implement local Ollama support for app-server transport and example suite
- Update app-server connection to pass configuration via --config overrides instead of deprecated OSS-specific CLI flags. - Introduce Codex.ExamplesSupport to provide centralized Ollama-aware model and reasoning effort defaults for example scripts. - Modify example runner to support --ollama mode, which enables CLI-backed examples against Ollama while skipping unsupported OpenAI-only subsystems. - Add deterministic fallback logic in example scripts for features currently unreliable in local OSS mode, such as strict structured output schemas. - Improve CLI command generation to correctly inject model_provider and model settings into the codex app-server startup sequence.
1 parent 1c3c5ce commit b4bddb2

16 files changed

Lines changed: 421 additions & 111 deletions

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,15 @@ That causes the shared core registry to:
174174
The SDK does not infer those flags on its own.
175175
- CLI argument rendering only emits `--model` from a non-empty resolved value
176176

177+
For the stateful app-server transport, the same resolved payload is rendered into
178+
supported `codex app-server --config ...` startup overrides plus `thread/start`
179+
`modelProvider` selection. The SDK does not pass unsupported exec-only OSS flags
180+
to `codex app-server`.
181+
182+
`./examples/run_all.sh --ollama` uses that same route. It runs the CLI-backed
183+
example suite against local Ollama and skips the direct OpenAI realtime/voice
184+
examples, which are a separate subsystem and are not Ollama-backed.
185+
177186
Use `Codex.Models.default_model/0`, `Codex.Models.list_visible/1`, and
178187
`Codex.Models.default_reasoning_effort/1` as convenience readers over that
179188
shared contract.

examples/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,17 @@ Run the same CLI-backed example set against local Codex OSS + Ollama:
5151
The runner checks that the requested Ollama model is installed before starting
5252
the examples.
5353

54+
In `--ollama` mode, the runner:
55+
56+
- executes the full CLI-backed example suite against the local Ollama-backed Codex route
57+
- keeps app-server examples enabled by configuring `codex app-server` with supported
58+
`--config` overrides instead of unsupported OSS argv flags
59+
- uses deterministic local fallbacks where upstream features are not reliable on the
60+
local OSS path (for example strict structured-output assertions or live web-search
61+
event enforcement)
62+
- skips the direct OpenAI realtime/voice examples, because those examples are not
63+
Ollama-backed and use a separate direct API subsystem
64+
5465
If direct API credentials are missing, realtime/voice examples are reported as `SKIPPED` and do not fail the run.
5566
If credentials exist but direct API access is unavailable (for example `insufficient_quota`, missing realtime model access, or an upstream Realtime `server_error`), direct API examples print `SKIPPED: <reason>`. Realtime demos now run a minimal raw-WebSocket health probe first and include the upstream `session_id` in the skip reason when OpenAI fails before any example-specific logic.
5667
The native OAuth example also self-skips in runner contexts unless you point it
@@ -109,6 +120,9 @@ The `live_*.exs` scripts hit the live Codex CLI (no OPENAI_API_KEY needed if you
109120

110121
These examples use the OpenAI Realtime API directly (not via Codex CLI). They demonstrate real-time bidirectional voice interactions:
111122

123+
`./examples/run_all.sh --ollama` skips this entire section on purpose. Those examples are
124+
OpenAI-only and do not participate in the local Codex OSS + Ollama route.
125+
112126
- `examples/live_realtime_voice.exs` — full realtime voice interaction demo with real audio I/O
113127
- `examples/realtime_basic.exs` — basic realtime session setup with real audio input
114128
- `examples/realtime_tools.exs` — using function tools with realtime agents

examples/conversation_and_resume.exs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
#!/usr/bin/env mix run
22

3+
alias Codex.ExamplesSupport
34
alias Codex.Items
45

56
defmodule Examples.Conversation do
67
@moduledoc false
78

89
def multi_turn do
910
{:ok, codex_opts} =
10-
Codex.Options.new(%{model: Codex.Models.default_model()})
11+
Codex.Options.new(%{model: ExamplesSupport.example_model()})
1112

1213
{:ok, thread} = Codex.start_thread(codex_opts)
1314

@@ -36,7 +37,7 @@ defmodule Examples.Conversation do
3637

3738
def resume_existing(thread_id) do
3839
{:ok, codex_opts} =
39-
Codex.Options.new(%{model: Codex.Models.default_model()})
40+
Codex.Options.new(%{model: ExamplesSupport.example_model()})
4041

4142
{:ok, thread} = Codex.resume_thread(thread_id, codex_opts)
4243

@@ -48,7 +49,7 @@ defmodule Examples.Conversation do
4849

4950
def save_and_resume_demo do
5051
{:ok, codex_opts} =
51-
Codex.Options.new(%{model: Codex.Models.default_model()})
52+
Codex.Options.new(%{model: ExamplesSupport.example_model()})
5253

5354
{:ok, thread} = Codex.start_thread(codex_opts)
5455
{:ok, result1} = Codex.Thread.run(thread, "Remember the number 42 for me.")

examples/live_collaboration_modes.exs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
Mix.Task.run("app.start")
22

3+
alias Codex.ExamplesSupport
34
alias Codex.{AppServer, Items, Models, Options, Thread}
45
alias Codex.Protocol.CollaborationMode
56

@@ -236,18 +237,25 @@ defmodule LiveCollaborationModes do
236237
defp normalize_effort_value(_), do: nil
237238

238239
defp resolve_selected_model(selected_mode) do
239-
case selected_mode.model do
240-
model when is_binary(model) and model != "" ->
240+
case {ExamplesSupport.ollama_mode?(), selected_mode.model} do
241+
{true, model} ->
242+
{ExamplesSupport.example_model(model),
243+
" (Ollama mode forces the selected local OSS model)"}
244+
245+
{false, model} when is_binary(model) and model != "" ->
241246
{model, " (advertised by the server preset)"}
242247

243248
_ ->
244-
{Models.default_model(), " (server omitted model; using the SDK default)"}
249+
{ExamplesSupport.example_model(), " (server omitted model; using the SDK default)"}
245250
end
246251
end
247252

248253
defp resolve_selected_effort(selected_mode, model) do
249-
case selected_mode.reasoning_effort do
250-
effort when not is_nil(effort) ->
254+
case {ExamplesSupport.ollama_mode?(), selected_mode.reasoning_effort} do
255+
{true, _effort} ->
256+
{nil, " (Ollama mode does not force a reasoning effort)"}
257+
258+
{false, effort} when not is_nil(effort) ->
251259
note =
252260
if effort == :low do
253261
" (advertised by the server preset)"
@@ -258,7 +266,7 @@ defmodule LiveCollaborationModes do
258266
{effort, note}
259267

260268
_ ->
261-
effort = Models.default_reasoning_effort(model)
269+
effort = ExamplesSupport.example_reasoning(Models.default_reasoning_effort(model))
262270

263271
note =
264272
" (server omitted effort; using the selected model default)"

examples/live_mcp_and_sessions.exs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ Mix.Task.run("app.start")
33

44
alias Codex.{AgentRunner, Events, RunConfig, Tools}
55
alias Codex.Agent, as: CodexAgent
6+
alias Codex.ExamplesSupport
67
alias Codex.Items.AgentMessage
78

89
defmodule CodexExamples.StubMcpTransport do
@@ -175,7 +176,7 @@ defmodule CodexExamples.LiveMcpAndSessions do
175176
{:ok, codex_opts} =
176177
Codex.Options.new(%{
177178
codex_path_override: fetch_codex_path!(),
178-
model: Codex.Models.default_model()
179+
model: ExamplesSupport.example_model()
179180
})
180181

181182
{:ok, thread} = Codex.start_thread(codex_opts)

examples/live_session_walkthrough.exs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
Mix.Task.run("app.start")
22

3+
alias Codex.ExamplesSupport
4+
35
defmodule CodexExamples.LiveSessionWalkthrough do
46
def main(argv) do
57
prompt =
@@ -14,7 +16,7 @@ defmodule CodexExamples.LiveSessionWalkthrough do
1416
codex_opts =
1517
Codex.Options.new(%{
1618
codex_path_override: fetch_codex_path!(),
17-
model: Codex.Models.default_model()
19+
model: ExamplesSupport.example_model()
1820
})
1921
|> unwrap!("codex options")
2022

examples/live_subagent_host_controls.exs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
Mix.Task.run("app.start")
22

33
alias Codex.{AppServer, Events, Items, Options, RunResultStreaming, Subagents, Thread}
4+
alias Codex.ExamplesSupport
45

56
defmodule CodexExamples.LiveSubagentHostControls do
67
@moduledoc false
@@ -28,14 +29,17 @@ defmodule CodexExamples.LiveSubagentHostControls do
2829
prompt = parse_prompt(argv)
2930
cwd = File.cwd!()
3031
codex_path = fetch_codex_path!()
31-
model = System.get_env("CODEX_MODEL") || Codex.Models.default_model()
32-
reasoning_effort = Codex.Models.default_reasoning_effort(model)
32+
model = ExamplesSupport.example_model(System.get_env("CODEX_MODEL"))
33+
34+
reasoning_effort =
35+
ExamplesSupport.example_reasoning(Codex.Models.default_reasoning_effort(model))
36+
3337
ensure_app_server_supported!(codex_path)
3438

3539
IO.puts("""
3640
Starting live subagent host-controls example.
3741
model: #{model}
38-
reasoning_effort: #{reasoning_effort}
42+
reasoning_effort: #{reasoning_effort || "none"}
3943
working_directory: #{cwd}
4044
codex_path: #{codex_path}
4145
""")

examples/live_telemetry_stream.exs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
alias Codex.{Error, Models, Options, RunResultStreaming, Thread, TransportError}
2+
alias Codex.ExamplesSupport
23

34
defmodule LiveTelemetryStream do
45
@moduledoc false
@@ -22,13 +23,13 @@ defmodule LiveTelemetryStream do
2223
prompt = parse_prompt(args)
2324
handler_id = "codex-live-telemetry-#{System.unique_integer([:positive])}"
2425

25-
model = Models.default_model()
26-
reasoning = :low
26+
model = ExamplesSupport.example_model(Models.default_model())
27+
reasoning = ExamplesSupport.example_reasoning(:low)
2728

2829
IO.puts("""
2930
Streaming live Codex telemetry (thread/diff/usage/compaction).
3031
Auth will use CODEX_API_KEY if set, otherwise your Codex CLI login.
31-
Using model=#{model} reasoning_effort=#{reasoning}.
32+
Using model=#{model} reasoning_effort=#{reasoning || "none"}.
3233
Starting live stream; you should see a thread start notice shortly.
3334
Some telemetry (usage/diff/compaction) may only appear at completion, and
3435
tool-heavy prompts can take 30-60s.

examples/live_usage_and_compaction.exs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
alias Codex.{Error, Events, Items, Models, Options, RunResultStreaming, Thread, TransportError}
2+
alias Codex.ExamplesSupport
23

34
defmodule LiveUsageAndCompaction do
45
@moduledoc false
56

67
def main(args) do
78
prompt = parse_prompt(args)
89

9-
model = Models.default_model()
10-
reasoning = Models.default_reasoning_effort(model)
10+
model = ExamplesSupport.example_model(Models.default_model())
11+
reasoning = ExamplesSupport.example_reasoning(Models.default_reasoning_effort(model))
1112
tools? = Models.tool_enabled?(model)
1213

1314
IO.puts("""

examples/live_web_search_modes.exs

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
Mix.Task.run("app.start")
22

33
alias Codex.{Error, Events, Items, Options, RunResultStreaming, Thread, TransportError}
4+
alias Codex.ExamplesSupport
45

56
defmodule LiveWebSearchModes do
67
@moduledoc false
@@ -17,6 +18,14 @@ defmodule LiveWebSearchModes do
1718
def main(args) do
1819
{modes, prompt} = parse_args(args)
1920
codex_path = fetch_codex_path!()
21+
22+
if ExamplesSupport.ollama_mode?() do
23+
IO.puts("""
24+
Ollama mode detected. This local OSS route does not guarantee Codex web-search events.
25+
Running a request-plumbing demo instead of enforcing live web-search event assertions.
26+
""")
27+
end
28+
2029
failures = Enum.flat_map(modes, &run_mode(&1, prompt, codex_path))
2130

2231
if failures != [] do
@@ -96,18 +105,25 @@ defmodule LiveWebSearchModes do
96105

97106
defp validate_mode_expectation(:disabled, final_state), do: {:ok, final_state}
98107

99-
defp validate_mode_expectation(:live, %{web_search?: true} = final_state),
100-
do: {:ok, final_state}
101-
102-
defp validate_mode_expectation(:live, _final_state) do
103-
{:retry_required, {:expected_web_search_events, :none_observed}}
108+
defp validate_mode_expectation(:live, final_state) do
109+
if ExamplesSupport.ollama_mode?() do
110+
{:ok, final_state}
111+
else
112+
validate_live_mode_expectation(final_state)
113+
end
104114
end
105115

106116
# Cached mode only permits cached search results. A turn may legitimately emit
107117
# no web-search events when no cached result is available or the model answers
108118
# without using the tool.
109119
defp validate_mode_expectation(:cached, final_state), do: {:ok, final_state}
110120

121+
defp validate_live_mode_expectation(%{web_search?: true} = final_state),
122+
do: {:ok, final_state}
123+
124+
defp validate_live_mode_expectation(_final_state),
125+
do: {:retry_required, {:expected_web_search_events, :none_observed}}
126+
111127
defp report_final_state(mode, final_state) do
112128
if final_state.web_search? do
113129
IO.puts("Observed web search events.")
@@ -124,6 +140,16 @@ defmodule LiveWebSearchModes do
124140
)
125141
end
126142

143+
defp report_no_web_search_events(:live) do
144+
if ExamplesSupport.ollama_mode?() do
145+
IO.puts(
146+
"No web search events observed. Ollama mode validates request plumbing only for live web_search."
147+
)
148+
else
149+
IO.puts("No web search events observed.")
150+
end
151+
end
152+
127153
defp report_no_web_search_events(_mode), do: IO.puts("No web search events observed.")
128154

129155
defp handle_event(%Events.ItemStarted{item: %Items.WebSearch{query: query}}, state) do

0 commit comments

Comments
 (0)