codex-python 1.0.1__tar.gz → 1.114.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. codex_python-1.114.1/PKG-INFO +223 -0
  2. codex_python-1.114.1/README.md +200 -0
  3. codex_python-1.114.1/codex/__init__.py +32 -0
  4. {codex_python-1.0.1 → codex_python-1.114.1}/codex/_binary.py +5 -1
  5. codex_python-1.114.1/codex/_config_types.py +6 -0
  6. codex_python-1.114.1/codex/_file_utils.py +18 -0
  7. codex_python-1.114.1/codex/_runtime.py +122 -0
  8. codex_python-1.114.1/codex/_turn_options.py +28 -0
  9. codex_python-1.114.1/codex/app_server/__init__.py +51 -0
  10. codex_python-1.114.1/codex/app_server/_async_client.py +232 -0
  11. codex_python-1.114.1/codex/app_server/_async_services.py +377 -0
  12. codex_python-1.114.1/codex/app_server/_async_threads.py +503 -0
  13. codex_python-1.114.1/codex/app_server/_payloads.py +59 -0
  14. codex_python-1.114.1/codex/app_server/_protocol_helpers.py +201 -0
  15. codex_python-1.114.1/codex/app_server/_session.py +419 -0
  16. codex_python-1.114.1/codex/app_server/_sync_client.py +358 -0
  17. codex_python-1.114.1/codex/app_server/_sync_services.py +587 -0
  18. codex_python-1.114.1/codex/app_server/_sync_support.py +14 -0
  19. codex_python-1.114.1/codex/app_server/_sync_threads.py +329 -0
  20. codex_python-1.114.1/codex/app_server/_types.py +5 -0
  21. codex_python-1.114.1/codex/app_server/errors.py +43 -0
  22. codex_python-1.114.1/codex/app_server/models.py +260 -0
  23. codex_python-1.114.1/codex/app_server/options.py +246 -0
  24. codex_python-1.114.1/codex/app_server/transports.py +259 -0
  25. codex_python-1.114.1/codex/codex.py +175 -0
  26. {codex_python-1.0.1 → codex_python-1.114.1}/codex/errors.py +10 -0
  27. codex_python-1.114.1/codex/options.py +127 -0
  28. codex_python-1.114.1/codex/output_schema.py +36 -0
  29. {codex_python-1.0.1 → codex_python-1.114.1}/codex/output_schema_file.py +7 -6
  30. codex_python-1.114.1/codex/protocol/__init__.py +3 -0
  31. codex_python-1.114.1/codex/protocol/types.py +6670 -0
  32. codex_python-1.114.1/codex/thread.py +313 -0
  33. {codex_python-1.0.1 → codex_python-1.114.1}/crates/codex_native/Cargo.lock +15 -15
  34. {codex_python-1.0.1 → codex_python-1.114.1}/crates/codex_native/Cargo.toml +1 -1
  35. {codex_python-1.0.1 → codex_python-1.114.1}/pyproject.toml +21 -1
  36. codex_python-1.0.1/PKG-INFO +0 -154
  37. codex_python-1.0.1/README.md +0 -135
  38. codex_python-1.0.1/codex/__init__.py +0 -86
  39. codex_python-1.0.1/codex/codex.py +0 -26
  40. codex_python-1.0.1/codex/events.py +0 -66
  41. codex_python-1.0.1/codex/exec.py +0 -313
  42. codex_python-1.0.1/codex/items.py +0 -110
  43. codex_python-1.0.1/codex/options.py +0 -57
  44. codex_python-1.0.1/codex/thread.py +0 -170
  45. {codex_python-1.0.1 → codex_python-1.114.1}/LICENSE +0 -0
  46. {codex_python-1.0.1 → codex_python-1.114.1}/codex/py.typed +0 -0
  47. {codex_python-1.0.1 → codex_python-1.114.1}/codex/vendor/.gitkeep +0 -0
  48. {codex_python-1.0.1 → codex_python-1.114.1}/crates/codex_native/codex/__init__.py +0 -0
  49. {codex_python-1.0.1 → codex_python-1.114.1}/crates/codex_native/src/lib.rs +0 -0
@@ -0,0 +1,223 @@
1
+ Metadata-Version: 2.4
2
+ Name: codex-python
3
+ Version: 1.114.1
4
+ Classifier: Programming Language :: Python :: 3
5
+ Classifier: Programming Language :: Python :: 3 :: Only
6
+ Classifier: Programming Language :: Python :: 3.12
7
+ Classifier: Programming Language :: Python :: 3.13
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Typing :: Typed
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Dist: pydantic>=2.11.7,<3
12
+ Requires-Dist: websockets>=15.0.1,<16 ; extra == 'websocket'
13
+ Provides-Extra: websocket
14
+ License-File: LICENSE
15
+ Summary: Python SDK for Codex CLI with bundled platform binaries
16
+ Keywords: codex,sdk,cli,automation
17
+ Requires-Python: >=3.12
18
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
19
+ Project-URL: Homepage, https://github.com/gersmann/codex-python
20
+ Project-URL: Issues, https://github.com/gersmann/codex-python/issues
21
+ Project-URL: Repository, https://github.com/gersmann/codex-python
22
+
23
+ # codex-python
24
+
25
+ Python SDK for Codex with bundled `codex` binaries inside platform wheels.
26
+
27
+ This package exposes two supported APIs:
28
+
29
+ - `Codex`: a simple, local convenience interface backed by a private stdio app-server session
30
+ - `AppServerClient`: a richer app-server client for thread management, streaming events, approvals, and typed protocol access
31
+
32
+ Canonical import paths:
33
+
34
+ - use `from codex import ...` for the high-level `Codex` facade
35
+ - use `from codex.app_server import ...` for the raw app-server client and app-server option types
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install codex-python
41
+ ```
42
+
43
+ ## Which API should I use?
44
+
45
+ ### `Codex`
46
+
47
+ Use `Codex` when you want the smallest surface area for local automation:
48
+
49
+ - one private local app-server session per `Codex` instance
50
+ - stateless `run*()` convenience (fresh internal thread per call)
51
+ - stateful thread workflows when needed via `start_thread()` / `resume_thread()`
52
+ - simple request/response usage
53
+ - optional streaming over the exec event stream
54
+ - structured output via `TurnOptions(output_schema=...)`
55
+
56
+ ### `AppServerClient`
57
+
58
+ Use `AppServerClient` when you want a deeper integration:
59
+
60
+ - persistent app-server connection
61
+ - thread objects and turn streams
62
+ - protocol-native notifications
63
+ - server-driven requests such as tool callbacks and approvals
64
+ - typed protocol models and raw JSON-RPC access when needed
65
+
66
+ ## Quickstart: `Codex`
67
+
68
+ ```python
69
+ from codex import Codex
70
+
71
+ client = Codex()
72
+ summary = client.run_text("Diagnose the failing tests and propose a fix")
73
+ print(summary)
74
+ ```
75
+
76
+ More `Codex` examples: [docs/exec_api.md](docs/exec_api.md)
77
+
78
+ ## Quickstart: `AppServerClient`
79
+
80
+ ```python
81
+ from codex.app_server import AppServerClient, AppServerClientInfo, AppServerInitializeOptions
82
+
83
+ initialize_options = AppServerInitializeOptions(
84
+ client_info=AppServerClientInfo(
85
+ name="my_integration",
86
+ title="My Integration",
87
+ version="0.1.0",
88
+ )
89
+ )
90
+
91
+ with AppServerClient.connect_stdio(initialize_options=initialize_options) as client:
92
+ thread = client.start_thread()
93
+ summary = thread.run_text("Briefly summarize this repository's purpose.")
94
+ print(summary)
95
+ ```
96
+
97
+ More app-server examples: [docs/app_server.md](docs/app_server.md)
98
+ For websocket transport, install the optional extra: `pip install "codex-python[websocket]"`.
99
+
100
+ ## Structured output
101
+
102
+ ### `Codex`
103
+
104
+ ```python
105
+ from codex import Codex, TurnOptions
106
+
107
+ schema = {
108
+ "type": "object",
109
+ "properties": {"summary": {"type": "string"}},
110
+ "required": ["summary"],
111
+ "additionalProperties": False,
112
+ }
113
+
114
+ client = Codex()
115
+ payload = client.run_json("Summarize repository status", TurnOptions(output_schema=schema))
116
+ print(payload["summary"])
117
+ ```
118
+
119
+ ### `AppServerClient`
120
+
121
+ ```python
122
+ from pydantic import BaseModel
123
+
124
+ from codex.app_server import AppServerClient, AppServerTurnOptions
125
+
126
+
127
+ class Summary(BaseModel):
128
+ summary: str
129
+
130
+
131
+ with AppServerClient.connect_stdio() as client:
132
+ thread = client.start_thread()
133
+ result = thread.run_model(
134
+ "Summarize repository status",
135
+ Summary,
136
+ )
137
+ print(result.summary)
138
+ ```
139
+
140
+ `run_model()` uses `Summary` both as the validation model and, by default, as the output schema sent
141
+ to Codex. If you want JSON back without validation, you can also pass the model class directly to
142
+ `output_schema`, for example `thread.run_json(..., AppServerTurnOptions(output_schema=Summary))`.
143
+
144
+ ## Streaming
145
+
146
+ ### `Codex` stream
147
+
148
+ ```python
149
+ from codex import Codex
150
+ from codex.protocol import types as protocol
151
+
152
+ client = Codex()
153
+ stream = client.run("Investigate this bug")
154
+ for event in stream:
155
+ if isinstance(event, protocol.ItemAgentMessageDeltaNotification):
156
+ print(event.params.delta, end="", flush=True)
157
+
158
+ print()
159
+ ```
160
+
161
+ `Codex.run*()` starts a fresh internal thread for each call. Use
162
+ `start_thread()` or `resume_thread()` when you want later runs to share context.
163
+
164
+ High-level `Codex` helpers raise `ThreadRunError` on failed or interrupted terminal turns and
165
+ preserve the final turn metadata on the exception for debugging and UI handling.
166
+
167
+ ### App-server stream
168
+
169
+ ```python
170
+ from codex.app_server import AppServerClient
171
+ from codex.protocol import types as protocol
172
+
173
+ with AppServerClient.connect_stdio() as client:
174
+ thread = client.start_thread()
175
+ stream = thread.run("Investigate this bug")
176
+
177
+ for event in stream:
178
+ if isinstance(event, protocol.ItemAgentMessageDeltaNotification):
179
+ print(event.params.delta, end="", flush=True)
180
+
181
+ print()
182
+ ```
183
+
184
+ Advanced app-server usage, including typed stable RPC domains such as `client.models` and the raw `client.rpc` fallback: [docs/app_server_advanced.md](docs/app_server_advanced.md)
185
+
186
+ ## Examples
187
+
188
+ - [examples/basic_conversation.py](examples/basic_conversation.py): minimal `Codex` flow
189
+ - [examples/app_server_conversation.py](examples/app_server_conversation.py): minimal app-server flow
190
+ - [examples/app_server_websocket_conversation.py](examples/app_server_websocket_conversation.py): minimal websocket app-server flow
191
+ - [examples/app_server_stream_events.py](examples/app_server_stream_events.py): protocol-native app-server streaming
192
+ - [examples/app_server_tool_handler.py](examples/app_server_tool_handler.py): typed app-server request handling
193
+
194
+ ## Bundled binary behavior
195
+
196
+ By default, the SDK resolves the bundled binary at:
197
+
198
+ `codex/vendor/<target-triple>/codex/{codex|codex.exe}`
199
+
200
+ If the bundled binary is not present, for example in a source checkout, the SDK falls back to
201
+ `codex` on `PATH`.
202
+
203
+ You can override the executable path with:
204
+
205
+ - `CodexOptions(codex_path_override=...)`
206
+ - `codex.app_server.AppServerProcessOptions(codex_path_override=...)`
207
+
208
+ ## Development
209
+
210
+ ```bash
211
+ make lint
212
+ make test
213
+ ```
214
+
215
+ `make test` emits a terminal coverage report, writes `coverage.xml`, and enforces the repository
216
+ coverage gate.
217
+
218
+ If you want to test vendored-binary behavior locally, fetch binaries into `codex/vendor`:
219
+
220
+ ```bash
221
+ python scripts/fetch_codex_binary.py --target-triple x86_64-unknown-linux-musl
222
+ ```
223
+
@@ -0,0 +1,200 @@
1
+ # codex-python
2
+
3
+ Python SDK for Codex with bundled `codex` binaries inside platform wheels.
4
+
5
+ This package exposes two supported APIs:
6
+
7
+ - `Codex`: a simple, local convenience interface backed by a private stdio app-server session
8
+ - `AppServerClient`: a richer app-server client for thread management, streaming events, approvals, and typed protocol access
9
+
10
+ Canonical import paths:
11
+
12
+ - use `from codex import ...` for the high-level `Codex` facade
13
+ - use `from codex.app_server import ...` for the raw app-server client and app-server option types
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pip install codex-python
19
+ ```
20
+
21
+ ## Which API should I use?
22
+
23
+ ### `Codex`
24
+
25
+ Use `Codex` when you want the smallest surface area for local automation:
26
+
27
+ - one private local app-server session per `Codex` instance
28
+ - stateless `run*()` convenience (fresh internal thread per call)
29
+ - stateful thread workflows when needed via `start_thread()` / `resume_thread()`
30
+ - simple request/response usage
31
+ - optional streaming over the exec event stream
32
+ - structured output via `TurnOptions(output_schema=...)`
33
+
34
+ ### `AppServerClient`
35
+
36
+ Use `AppServerClient` when you want a deeper integration:
37
+
38
+ - persistent app-server connection
39
+ - thread objects and turn streams
40
+ - protocol-native notifications
41
+ - server-driven requests such as tool callbacks and approvals
42
+ - typed protocol models and raw JSON-RPC access when needed
43
+
44
+ ## Quickstart: `Codex`
45
+
46
+ ```python
47
+ from codex import Codex
48
+
49
+ client = Codex()
50
+ summary = client.run_text("Diagnose the failing tests and propose a fix")
51
+ print(summary)
52
+ ```
53
+
54
+ More `Codex` examples: [docs/exec_api.md](docs/exec_api.md)
55
+
56
+ ## Quickstart: `AppServerClient`
57
+
58
+ ```python
59
+ from codex.app_server import AppServerClient, AppServerClientInfo, AppServerInitializeOptions
60
+
61
+ initialize_options = AppServerInitializeOptions(
62
+ client_info=AppServerClientInfo(
63
+ name="my_integration",
64
+ title="My Integration",
65
+ version="0.1.0",
66
+ )
67
+ )
68
+
69
+ with AppServerClient.connect_stdio(initialize_options=initialize_options) as client:
70
+ thread = client.start_thread()
71
+ summary = thread.run_text("Briefly summarize this repository's purpose.")
72
+ print(summary)
73
+ ```
74
+
75
+ More app-server examples: [docs/app_server.md](docs/app_server.md)
76
+ For websocket transport, install the optional extra: `pip install "codex-python[websocket]"`.
77
+
78
+ ## Structured output
79
+
80
+ ### `Codex`
81
+
82
+ ```python
83
+ from codex import Codex, TurnOptions
84
+
85
+ schema = {
86
+ "type": "object",
87
+ "properties": {"summary": {"type": "string"}},
88
+ "required": ["summary"],
89
+ "additionalProperties": False,
90
+ }
91
+
92
+ client = Codex()
93
+ payload = client.run_json("Summarize repository status", TurnOptions(output_schema=schema))
94
+ print(payload["summary"])
95
+ ```
96
+
97
+ ### `AppServerClient`
98
+
99
+ ```python
100
+ from pydantic import BaseModel
101
+
102
+ from codex.app_server import AppServerClient, AppServerTurnOptions
103
+
104
+
105
+ class Summary(BaseModel):
106
+ summary: str
107
+
108
+
109
+ with AppServerClient.connect_stdio() as client:
110
+ thread = client.start_thread()
111
+ result = thread.run_model(
112
+ "Summarize repository status",
113
+ Summary,
114
+ )
115
+ print(result.summary)
116
+ ```
117
+
118
+ `run_model()` uses `Summary` both as the validation model and, by default, as the output schema sent
119
+ to Codex. If you want JSON back without validation, you can also pass the model class directly to
120
+ `output_schema`, for example `thread.run_json(..., AppServerTurnOptions(output_schema=Summary))`.
121
+
122
+ ## Streaming
123
+
124
+ ### `Codex` stream
125
+
126
+ ```python
127
+ from codex import Codex
128
+ from codex.protocol import types as protocol
129
+
130
+ client = Codex()
131
+ stream = client.run("Investigate this bug")
132
+ for event in stream:
133
+ if isinstance(event, protocol.ItemAgentMessageDeltaNotification):
134
+ print(event.params.delta, end="", flush=True)
135
+
136
+ print()
137
+ ```
138
+
139
+ `Codex.run*()` starts a fresh internal thread for each call. Use
140
+ `start_thread()` or `resume_thread()` when you want later runs to share context.
141
+
142
+ High-level `Codex` helpers raise `ThreadRunError` on failed or interrupted terminal turns and
143
+ preserve the final turn metadata on the exception for debugging and UI handling.
144
+
145
+ ### App-server stream
146
+
147
+ ```python
148
+ from codex.app_server import AppServerClient
149
+ from codex.protocol import types as protocol
150
+
151
+ with AppServerClient.connect_stdio() as client:
152
+ thread = client.start_thread()
153
+ stream = thread.run("Investigate this bug")
154
+
155
+ for event in stream:
156
+ if isinstance(event, protocol.ItemAgentMessageDeltaNotification):
157
+ print(event.params.delta, end="", flush=True)
158
+
159
+ print()
160
+ ```
161
+
162
+ Advanced app-server usage, including typed stable RPC domains such as `client.models` and the raw `client.rpc` fallback: [docs/app_server_advanced.md](docs/app_server_advanced.md)
163
+
164
+ ## Examples
165
+
166
+ - [examples/basic_conversation.py](examples/basic_conversation.py): minimal `Codex` flow
167
+ - [examples/app_server_conversation.py](examples/app_server_conversation.py): minimal app-server flow
168
+ - [examples/app_server_websocket_conversation.py](examples/app_server_websocket_conversation.py): minimal websocket app-server flow
169
+ - [examples/app_server_stream_events.py](examples/app_server_stream_events.py): protocol-native app-server streaming
170
+ - [examples/app_server_tool_handler.py](examples/app_server_tool_handler.py): typed app-server request handling
171
+
172
+ ## Bundled binary behavior
173
+
174
+ By default, the SDK resolves the bundled binary at:
175
+
176
+ `codex/vendor/<target-triple>/codex/{codex|codex.exe}`
177
+
178
+ If the bundled binary is not present, for example in a source checkout, the SDK falls back to
179
+ `codex` on `PATH`.
180
+
181
+ You can override the executable path with:
182
+
183
+ - `CodexOptions(codex_path_override=...)`
184
+ - `codex.app_server.AppServerProcessOptions(codex_path_override=...)`
185
+
186
+ ## Development
187
+
188
+ ```bash
189
+ make lint
190
+ make test
191
+ ```
192
+
193
+ `make test` emits a terminal coverage report, writes `coverage.xml`, and enforces the repository
194
+ coverage gate.
195
+
196
+ If you want to test vendored-binary behavior locally, fetch binaries into `codex/vendor`:
197
+
198
+ ```bash
199
+ python scripts/fetch_codex_binary.py --target-triple x86_64-unknown-linux-musl
200
+ ```
@@ -0,0 +1,32 @@
1
+ """Python SDK for embedding Codex via the bundled CLI binary."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from codex.codex import Codex
6
+ from codex.errors import CodexError, CodexExecError, CodexParseError, ThreadRunError
7
+ from codex.options import (
8
+ CancelSignal,
9
+ CodexConfigObject,
10
+ CodexConfigValue,
11
+ CodexOptions,
12
+ ThreadResumeOptions,
13
+ ThreadStartOptions,
14
+ TurnOptions,
15
+ )
16
+
17
+ __version__ = "1.114.1"
18
+
19
+ __all__ = [
20
+ "Codex",
21
+ "CodexError",
22
+ "CodexExecError",
23
+ "CodexParseError",
24
+ "ThreadRunError",
25
+ "CodexOptions",
26
+ "ThreadStartOptions",
27
+ "ThreadResumeOptions",
28
+ "TurnOptions",
29
+ "CodexConfigValue",
30
+ "CodexConfigObject",
31
+ "CancelSignal",
32
+ ]
@@ -6,6 +6,10 @@ from pathlib import Path
6
6
  from codex.errors import CodexExecError
7
7
 
8
8
 
9
+ class BundledCodexNotFoundError(CodexExecError):
10
+ """The platform wheel does not contain a bundled Codex binary."""
11
+
12
+
9
13
  def resolve_target_triple(system_name: str | None = None, machine_name: str | None = None) -> str:
10
14
  system = (system_name or platform.system()).lower()
11
15
  machine = (machine_name or platform.machine()).lower()
@@ -35,7 +39,7 @@ def bundled_codex_path(target_triple: str | None = None) -> Path:
35
39
  binary_name = "codex.exe" if "windows" in triple else "codex"
36
40
  binary_path = package_root / "vendor" / triple / "codex" / binary_name
37
41
  if not binary_path.exists():
38
- raise CodexExecError(
42
+ raise BundledCodexNotFoundError(
39
43
  "Bundled codex binary not found at "
40
44
  f"{binary_path}. Install a platform wheel or provide codex_path_override."
41
45
  )
@@ -0,0 +1,6 @@
1
+ from __future__ import annotations
2
+
3
+ type CodexConfigValue = (
4
+ str | int | float | bool | list["CodexConfigValue"] | dict[str, "CodexConfigValue"]
5
+ )
6
+ type CodexConfigObject = dict[str, CodexConfigValue]
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import tempfile
5
+ from pathlib import Path
6
+
7
+
8
+ def atomic_write_text(path: Path, text: str, *, encoding: str = "utf-8") -> None:
9
+ path.parent.mkdir(parents=True, exist_ok=True)
10
+ fd, temp_path_str = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
11
+ temp_path = Path(temp_path_str)
12
+ try:
13
+ with os.fdopen(fd, "w", encoding=encoding) as handle:
14
+ handle.write(text)
15
+ temp_path.replace(path)
16
+ except Exception:
17
+ temp_path.unlink(missing_ok=True)
18
+ raise
@@ -0,0 +1,122 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import math
5
+ import re
6
+ from collections.abc import Callable, Mapping
7
+ from pathlib import Path
8
+
9
+ from codex._binary import BundledCodexNotFoundError
10
+ from codex._config_types import CodexConfigObject, CodexConfigValue
11
+
12
+ INTERNAL_ORIGINATOR_ENV = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE"
13
+ PYTHON_SDK_ORIGINATOR = "codex_sdk_py"
14
+ TOML_BARE_KEY = re.compile(r"^[A-Za-z0-9_-]+$")
15
+
16
+
17
+ def build_child_env(
18
+ env_override: Mapping[str, str] | None,
19
+ *,
20
+ base_url: str | None = None,
21
+ api_key: str | None = None,
22
+ ) -> dict[str, str]:
23
+ env = {} if env_override is None else dict(env_override)
24
+ if INTERNAL_ORIGINATOR_ENV not in env:
25
+ env[INTERNAL_ORIGINATOR_ENV] = PYTHON_SDK_ORIGINATOR
26
+ if base_url is not None:
27
+ env["OPENAI_BASE_URL"] = base_url
28
+ if api_key is not None:
29
+ env["CODEX_API_KEY"] = api_key
30
+ return env
31
+
32
+
33
+ def resolve_codex_path(
34
+ executable_path: str | None,
35
+ *,
36
+ bundled_path: Callable[[], Path],
37
+ which: Callable[[str], str | None],
38
+ error_type: type[Exception],
39
+ ) -> str:
40
+ if executable_path is not None:
41
+ return str(Path(executable_path))
42
+ try:
43
+ return str(bundled_path())
44
+ except BundledCodexNotFoundError as bundled_error:
45
+ system_codex = which("codex")
46
+ if system_codex is None:
47
+ raise error_type(
48
+ f"{bundled_error} Also failed to find `codex` on PATH."
49
+ ) from bundled_error
50
+ return system_codex
51
+
52
+
53
+ def serialize_config_overrides(config_overrides: CodexConfigObject) -> list[str]:
54
+ overrides: list[str] = []
55
+ _flatten_config_overrides(config_overrides, "", overrides)
56
+ return overrides
57
+
58
+
59
+ def _flatten_config_overrides(
60
+ value: CodexConfigValue | CodexConfigObject,
61
+ prefix: str,
62
+ overrides: list[str],
63
+ ) -> None:
64
+ if not isinstance(value, dict):
65
+ if prefix == "":
66
+ raise ValueError("Codex config overrides must be a plain object")
67
+ overrides.append(f"{prefix}={_to_toml_value(value, prefix)}")
68
+ return
69
+
70
+ entries = list(value.items())
71
+ if prefix == "" and not entries:
72
+ return
73
+ if prefix != "" and not entries:
74
+ overrides.append(f"{prefix}={{}}")
75
+ return
76
+
77
+ for key, child in entries:
78
+ if not isinstance(key, str) or key == "":
79
+ raise ValueError("Codex config override keys must be non-empty strings")
80
+ path = f"{prefix}.{key}" if prefix else key
81
+ if isinstance(child, dict):
82
+ _flatten_config_overrides(child, path, overrides)
83
+ else:
84
+ overrides.append(f"{path}={_to_toml_value(child, path)}")
85
+
86
+
87
+ def _to_toml_value(value: CodexConfigValue, path: str) -> str:
88
+ if isinstance(value, str):
89
+ return json.dumps(value)
90
+ if isinstance(value, bool):
91
+ return format_toml_bool(value)
92
+ if isinstance(value, int):
93
+ return f"{value}"
94
+ if isinstance(value, float):
95
+ if not math.isfinite(value):
96
+ raise ValueError(f"Codex config override at {path} must be a finite number")
97
+ return f"{value}"
98
+ if isinstance(value, list):
99
+ rendered_items = [
100
+ _to_toml_value(item, f"{path}[{index}]") for index, item in enumerate(value)
101
+ ]
102
+ return f"[{', '.join(rendered_items)}]"
103
+ if isinstance(value, dict):
104
+ parts: list[str] = []
105
+ for key, child in value.items():
106
+ if not isinstance(key, str) or key == "":
107
+ raise ValueError("Codex config override keys must be non-empty strings")
108
+ parts.append(f"{format_toml_key(key)} = {_to_toml_value(child, f'{path}.{key}')}")
109
+ return f"{{{', '.join(parts)}}}"
110
+ if value is None:
111
+ raise ValueError(f"Codex config override at {path} cannot be null")
112
+ raise ValueError(f"Unsupported Codex config override value at {path}: {type(value).__name__}")
113
+
114
+
115
+ def format_toml_key(key: str) -> str:
116
+ if TOML_BARE_KEY.match(key):
117
+ return key
118
+ return json.dumps(key)
119
+
120
+
121
+ def format_toml_bool(value: bool) -> str:
122
+ return "true" if value else "false"
@@ -0,0 +1,28 @@
1
+ """Shared turn-option helpers for structured-output runs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from codex.app_server.options import AppServerTurnOptions
8
+ from codex.output_schema import resolve_model_output_schema
9
+
10
+
11
+ def with_model_output_schema(
12
+ options: AppServerTurnOptions | None,
13
+ model_type: type[BaseModel],
14
+ *,
15
+ owner: str,
16
+ option_model: type[AppServerTurnOptions] = AppServerTurnOptions,
17
+ ) -> AppServerTurnOptions:
18
+ if options is None:
19
+ return option_model(output_schema=model_type)
20
+ return options.model_copy(
21
+ update={
22
+ "output_schema": resolve_model_output_schema(
23
+ options.output_schema,
24
+ model_type,
25
+ owner=owner,
26
+ )
27
+ }
28
+ )