codex-python 1.0.0__tar.gz → 1.0.0b3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-python
3
- Version: 1.0.0
3
+ Version: 1.0.0b3
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3 :: Only
6
6
  Classifier: Programming Language :: Python :: 3.13
@@ -106,9 +106,27 @@ thread.run("Continue from previous context")
106
106
 
107
107
  ## Options
108
108
 
109
- - `CodexOptions`: `codex_path_override`, `base_url`, `api_key`
110
- - `ThreadOptions`: `model`, `sandbox_mode`, `working_directory`, `skip_git_repo_check`
111
- - `TurnOptions`: `output_schema`
109
+ - `CodexOptions`: `codex_path_override`, `base_url`, `api_key`, `config`, `env`
110
+ - `ThreadOptions`: `model`, `sandbox_mode`, `working_directory`, `skip_git_repo_check`, `model_reasoning_effort`, `network_access_enabled`, `web_search_mode`, `web_search_enabled`, `approval_policy`, `additional_directories`
111
+ - `TurnOptions`: `output_schema`, `signal`
112
+
113
+ ## Cancellation
114
+
115
+ ```python
116
+ import threading
117
+
118
+ from codex import Codex, TurnOptions
119
+
120
+ cancel = threading.Event()
121
+
122
+ client = Codex()
123
+ thread = client.start_thread()
124
+ stream = thread.run_streamed("Long running task", TurnOptions(signal=cancel))
125
+
126
+ cancel.set()
127
+ for event in stream.events:
128
+ print(event)
129
+ ```
112
130
 
113
131
  ## Bundled binary behavior
114
132
 
@@ -88,9 +88,27 @@ thread.run("Continue from previous context")
88
88
 
89
89
  ## Options
90
90
 
91
- - `CodexOptions`: `codex_path_override`, `base_url`, `api_key`
92
- - `ThreadOptions`: `model`, `sandbox_mode`, `working_directory`, `skip_git_repo_check`
93
- - `TurnOptions`: `output_schema`
91
+ - `CodexOptions`: `codex_path_override`, `base_url`, `api_key`, `config`, `env`
92
+ - `ThreadOptions`: `model`, `sandbox_mode`, `working_directory`, `skip_git_repo_check`, `model_reasoning_effort`, `network_access_enabled`, `web_search_mode`, `web_search_enabled`, `approval_policy`, `additional_directories`
93
+ - `TurnOptions`: `output_schema`, `signal`
94
+
95
+ ## Cancellation
96
+
97
+ ```python
98
+ import threading
99
+
100
+ from codex import Codex, TurnOptions
101
+
102
+ cancel = threading.Event()
103
+
104
+ client = Codex()
105
+ thread = client.start_thread()
106
+ stream = thread.run_streamed("Long running task", TurnOptions(signal=cancel))
107
+
108
+ cancel.set()
109
+ for event in stream.events:
110
+ print(event)
111
+ ```
94
112
 
95
113
  ## Bundled binary behavior
96
114
 
@@ -26,10 +26,21 @@ from codex.items import (
26
26
  TodoListItem,
27
27
  WebSearchItem,
28
28
  )
29
- from codex.options import ApprovalMode, CodexOptions, SandboxMode, ThreadOptions, TurnOptions
29
+ from codex.options import (
30
+ ApprovalMode,
31
+ CancelSignal,
32
+ CodexConfigObject,
33
+ CodexConfigValue,
34
+ CodexOptions,
35
+ ModelReasoningEffort,
36
+ SandboxMode,
37
+ ThreadOptions,
38
+ TurnOptions,
39
+ WebSearchMode,
40
+ )
30
41
  from codex.thread import Input, RunResult, RunStreamedResult, Thread, UserInput
31
42
 
32
- __version__ = "1.0.0"
43
+ __version__ = "1.0.0-beta.3"
33
44
 
34
45
  __all__ = [
35
46
  "Codex",
@@ -47,6 +58,11 @@ __all__ = [
47
58
  "TurnOptions",
48
59
  "ApprovalMode",
49
60
  "SandboxMode",
61
+ "ModelReasoningEffort",
62
+ "WebSearchMode",
63
+ "CodexConfigValue",
64
+ "CodexConfigObject",
65
+ "CancelSignal",
50
66
  "ThreadEvent",
51
67
  "ThreadStartedEvent",
52
68
  "TurnStartedEvent",
@@ -10,7 +10,11 @@ class Codex:
10
10
 
11
11
  def __init__(self, options: CodexOptions | None = None) -> None:
12
12
  resolved = options or CodexOptions()
13
- self._exec = CodexExec(resolved.codex_path_override)
13
+ self._exec = CodexExec(
14
+ resolved.codex_path_override,
15
+ env_override=resolved.env,
16
+ config_overrides=resolved.config,
17
+ )
14
18
  self._options = resolved
15
19
 
16
20
  def start_thread(self, options: ThreadOptions | None = None) -> Thread:
@@ -0,0 +1,313 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import math
5
+ import os
6
+ import re
7
+ import shutil
8
+ import subprocess
9
+ from collections.abc import Iterator
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+
13
+ from codex._binary import bundled_codex_path
14
+ from codex.errors import CodexExecError
15
+ from codex.options import (
16
+ ApprovalMode,
17
+ CancelSignal,
18
+ CodexConfigObject,
19
+ CodexConfigValue,
20
+ ModelReasoningEffort,
21
+ SandboxMode,
22
+ SupportsAborted,
23
+ SupportsIsSet,
24
+ WebSearchMode,
25
+ )
26
+
27
+ INTERNAL_ORIGINATOR_ENV = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE"
28
+ PYTHON_SDK_ORIGINATOR = "codex_sdk_py"
29
+ TOML_BARE_KEY = re.compile(r"^[A-Za-z0-9_-]+$")
30
+
31
+
32
+ @dataclass(slots=True, frozen=True)
33
+ class CodexExecArgs:
34
+ input: str
35
+ base_url: str | None = None
36
+ api_key: str | None = None
37
+ thread_id: str | None = None
38
+ images: list[str] | None = None
39
+ model: str | None = None
40
+ sandbox_mode: SandboxMode | None = None
41
+ working_directory: str | None = None
42
+ additional_directories: list[str] | None = None
43
+ skip_git_repo_check: bool = False
44
+ output_schema_file: str | None = None
45
+ model_reasoning_effort: ModelReasoningEffort | None = None
46
+ signal: CancelSignal | None = None
47
+ network_access_enabled: bool | None = None
48
+ web_search_mode: WebSearchMode | None = None
49
+ web_search_enabled: bool | None = None
50
+ approval_policy: ApprovalMode | None = None
51
+
52
+
53
+ class CodexExec:
54
+ def __init__(
55
+ self,
56
+ executable_path: str | None = None,
57
+ env_override: dict[str, str] | None = None,
58
+ config_overrides: CodexConfigObject | None = None,
59
+ ) -> None:
60
+ if executable_path is not None:
61
+ path = Path(executable_path)
62
+ else:
63
+ try:
64
+ path = bundled_codex_path()
65
+ except CodexExecError as bundled_error:
66
+ system_codex = shutil.which("codex")
67
+ if system_codex is None:
68
+ raise CodexExecError(
69
+ f"{bundled_error} Also failed to find `codex` on PATH."
70
+ ) from bundled_error
71
+ path = Path(system_codex)
72
+ self.executable_path = str(path)
73
+ self._env_override = env_override
74
+ self._config_overrides = config_overrides
75
+
76
+ def run(self, args: CodexExecArgs) -> Iterator[str]:
77
+ if is_signal_aborted(args.signal):
78
+ raise CodexExecError("Codex exec aborted before start")
79
+
80
+ command_args: list[str] = ["exec", "--experimental-json"]
81
+
82
+ if self._config_overrides is not None:
83
+ for override in serialize_config_overrides(self._config_overrides):
84
+ command_args.extend(["--config", override])
85
+
86
+ if args.model is not None:
87
+ command_args.extend(["--model", args.model])
88
+ if args.sandbox_mode is not None:
89
+ command_args.extend(["--sandbox", args.sandbox_mode])
90
+ if args.working_directory is not None:
91
+ command_args.extend(["--cd", args.working_directory])
92
+ if args.additional_directories:
93
+ for directory in args.additional_directories:
94
+ command_args.extend(["--add-dir", directory])
95
+ if args.skip_git_repo_check:
96
+ command_args.append("--skip-git-repo-check")
97
+ if args.output_schema_file is not None:
98
+ command_args.extend(["--output-schema", args.output_schema_file])
99
+ if args.model_reasoning_effort is not None:
100
+ command_args.extend(
101
+ [
102
+ "--config",
103
+ f'model_reasoning_effort="{args.model_reasoning_effort}"',
104
+ ]
105
+ )
106
+ if args.network_access_enabled is not None:
107
+ command_args.extend(
108
+ [
109
+ "--config",
110
+ (
111
+ "sandbox_workspace_write.network_access="
112
+ f"{format_toml_bool(args.network_access_enabled)}"
113
+ ),
114
+ ]
115
+ )
116
+ if args.web_search_mode is not None:
117
+ command_args.extend(["--config", f'web_search="{args.web_search_mode}"'])
118
+ elif args.web_search_enabled is True:
119
+ command_args.extend(["--config", 'web_search="live"'])
120
+ elif args.web_search_enabled is False:
121
+ command_args.extend(["--config", 'web_search="disabled"'])
122
+ if args.approval_policy is not None:
123
+ command_args.extend(["--config", f'approval_policy="{args.approval_policy}"'])
124
+ if args.thread_id is not None:
125
+ command_args.extend(["resume", args.thread_id])
126
+ if args.images is not None:
127
+ for image in args.images:
128
+ command_args.extend(["--image", image])
129
+
130
+ env = self.build_env(base_url=args.base_url, api_key=args.api_key)
131
+
132
+ try:
133
+ child = subprocess.Popen(
134
+ [self.executable_path, *command_args],
135
+ stdin=subprocess.PIPE,
136
+ stdout=subprocess.PIPE,
137
+ stderr=subprocess.PIPE,
138
+ text=True,
139
+ encoding="utf-8",
140
+ env=env,
141
+ )
142
+ except OSError as exc:
143
+ raise CodexExecError(
144
+ f"Failed to spawn codex executable at '{self.executable_path}': {exc}"
145
+ ) from exc
146
+
147
+ if child.stdin is None:
148
+ terminate_child(child)
149
+ raise CodexExecError("Child process has no stdin")
150
+ if child.stdout is None:
151
+ terminate_child(child)
152
+ raise CodexExecError("Child process has no stdout")
153
+ if child.stderr is None:
154
+ terminate_child(child)
155
+ raise CodexExecError("Child process has no stderr")
156
+
157
+ if is_signal_aborted(args.signal):
158
+ terminate_child(child)
159
+ _ = child.stderr.read()
160
+ child.stderr.close()
161
+ raise CodexExecError("Codex exec aborted before start")
162
+
163
+ try:
164
+ child.stdin.write(args.input)
165
+ child.stdin.close()
166
+ except OSError as exc:
167
+ terminate_child(child)
168
+ raise CodexExecError(f"Failed to write input to codex process: {exc}") from exc
169
+
170
+ if is_signal_aborted(args.signal):
171
+ terminate_child(child)
172
+ _ = child.stderr.read()
173
+ child.stderr.close()
174
+ raise CodexExecError("Codex exec aborted")
175
+
176
+ aborted = False
177
+ try:
178
+ for line in child.stdout:
179
+ if is_signal_aborted(args.signal):
180
+ aborted = True
181
+ break
182
+ yield line.rstrip("\r\n")
183
+ except GeneratorExit:
184
+ terminate_child(child)
185
+ child.stderr.close()
186
+ raise
187
+ finally:
188
+ child.stdout.close()
189
+
190
+ if aborted:
191
+ terminate_child(child)
192
+ stderr = child.stderr.read()
193
+ child.stderr.close()
194
+ raise CodexExecError(build_abort_message(stderr))
195
+
196
+ exit_code = child.wait()
197
+ stderr = child.stderr.read()
198
+ child.stderr.close()
199
+
200
+ if exit_code != 0:
201
+ raise CodexExecError(f"Codex exec exited with code {exit_code}: {stderr}")
202
+
203
+ def build_env(self, base_url: str | None, api_key: str | None) -> dict[str, str]:
204
+ env: dict[str, str]
205
+ if self._env_override is None:
206
+ env = os.environ.copy()
207
+ else:
208
+ env = dict(self._env_override)
209
+
210
+ if INTERNAL_ORIGINATOR_ENV not in env:
211
+ env[INTERNAL_ORIGINATOR_ENV] = PYTHON_SDK_ORIGINATOR
212
+ if base_url is not None:
213
+ env["OPENAI_BASE_URL"] = base_url
214
+ if api_key is not None:
215
+ env["CODEX_API_KEY"] = api_key
216
+ return env
217
+
218
+
219
+ def terminate_child(child: subprocess.Popen[str]) -> None:
220
+ try:
221
+ child.kill()
222
+ except Exception:
223
+ pass
224
+ try:
225
+ child.wait()
226
+ except Exception:
227
+ pass
228
+
229
+
230
+ def build_abort_message(stderr: str) -> str:
231
+ if stderr == "":
232
+ return "Codex exec aborted"
233
+ return f"Codex exec aborted: {stderr}"
234
+
235
+
236
+ def is_signal_aborted(signal: CancelSignal | None) -> bool:
237
+ if signal is None:
238
+ return False
239
+ if isinstance(signal, SupportsAborted):
240
+ return signal.aborted
241
+ if isinstance(signal, SupportsIsSet):
242
+ return signal.is_set()
243
+ raise TypeError("signal must expose `aborted` or `is_set()`")
244
+
245
+
246
+ def serialize_config_overrides(config_overrides: CodexConfigObject) -> list[str]:
247
+ overrides: list[str] = []
248
+ flatten_config_overrides(config_overrides, "", overrides)
249
+ return overrides
250
+
251
+
252
+ def flatten_config_overrides(
253
+ value: CodexConfigValue | CodexConfigObject, prefix: str, overrides: list[str]
254
+ ) -> None:
255
+ if not isinstance(value, dict):
256
+ if prefix == "":
257
+ raise ValueError("Codex config overrides must be a plain object")
258
+ overrides.append(f"{prefix}={to_toml_value(value, prefix)}")
259
+ return
260
+
261
+ entries = list(value.items())
262
+ if prefix == "" and not entries:
263
+ return
264
+ if prefix != "" and not entries:
265
+ overrides.append(f"{prefix}={{}}")
266
+ return
267
+
268
+ for key, child in entries:
269
+ if not isinstance(key, str) or key == "":
270
+ raise ValueError("Codex config override keys must be non-empty strings")
271
+ path = f"{prefix}.{key}" if prefix else key
272
+ if isinstance(child, dict):
273
+ flatten_config_overrides(child, path, overrides)
274
+ else:
275
+ overrides.append(f"{path}={to_toml_value(child, path)}")
276
+
277
+
278
+ def to_toml_value(value: CodexConfigValue, path: str) -> str:
279
+ if isinstance(value, str):
280
+ return json.dumps(value)
281
+ if isinstance(value, bool):
282
+ return format_toml_bool(value)
283
+ if isinstance(value, int):
284
+ return f"{value}"
285
+ if isinstance(value, float):
286
+ if not math.isfinite(value):
287
+ raise ValueError(f"Codex config override at {path} must be a finite number")
288
+ return f"{value}"
289
+ if isinstance(value, list):
290
+ rendered_items = [
291
+ to_toml_value(item, f"{path}[{index}]") for index, item in enumerate(value)
292
+ ]
293
+ return f"[{', '.join(rendered_items)}]"
294
+ if isinstance(value, dict):
295
+ parts: list[str] = []
296
+ for key, child in value.items():
297
+ if not isinstance(key, str) or key == "":
298
+ raise ValueError("Codex config override keys must be non-empty strings")
299
+ parts.append(f"{format_toml_key(key)} = {to_toml_value(child, f'{path}.{key}')}")
300
+ return f"{{{', '.join(parts)}}}"
301
+ if value is None:
302
+ raise ValueError(f"Codex config override at {path} cannot be null")
303
+ raise ValueError(f"Unsupported Codex config override value at {path}: {type(value).__name__}")
304
+
305
+
306
+ def format_toml_key(key: str) -> str:
307
+ if TOML_BARE_KEY.match(key):
308
+ return key
309
+ return json.dumps(key)
310
+
311
+
312
+ def format_toml_bool(value: bool) -> str:
313
+ return "true" if value else "false"
@@ -29,11 +29,37 @@ class FileChangeItem(TypedDict):
29
29
  status: PatchApplyStatus
30
30
 
31
31
 
32
+ class McpTextContent(TypedDict):
33
+ type: Literal["text"]
34
+ text: str
35
+
36
+
37
+ class McpImageContent(TypedDict):
38
+ type: Literal["image"]
39
+ data: str
40
+ mimeType: str
41
+
42
+
43
+ McpContentBlock = McpTextContent | McpImageContent | dict[str, object]
44
+
45
+
46
+ class McpToolCallResult(TypedDict):
47
+ content: list[McpContentBlock]
48
+ structured_content: object
49
+
50
+
51
+ class McpToolCallError(TypedDict):
52
+ message: str
53
+
54
+
32
55
  class McpToolCallItem(TypedDict):
33
56
  id: str
34
57
  type: Literal["mcp_tool_call"]
35
58
  server: str
36
59
  tool: str
60
+ arguments: NotRequired[object]
61
+ result: NotRequired[McpToolCallResult]
62
+ error: NotRequired[McpToolCallError]
37
63
  status: McpToolCallStatus
38
64
 
39
65
 
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Literal, Protocol, runtime_checkable
5
+
6
+ ApprovalMode = Literal["never", "on-request", "on-failure", "untrusted"]
7
+ SandboxMode = Literal["read-only", "workspace-write", "danger-full-access"]
8
+ ModelReasoningEffort = Literal["minimal", "low", "medium", "high", "xhigh"]
9
+ WebSearchMode = Literal["disabled", "cached", "live"]
10
+
11
+ type CodexConfigValue = (
12
+ str | int | float | bool | list["CodexConfigValue"] | dict[str, "CodexConfigValue"]
13
+ )
14
+ type CodexConfigObject = dict[str, CodexConfigValue]
15
+
16
+
17
+ @runtime_checkable
18
+ class SupportsIsSet(Protocol):
19
+ def is_set(self) -> bool: ...
20
+
21
+
22
+ @runtime_checkable
23
+ class SupportsAborted(Protocol):
24
+ @property
25
+ def aborted(self) -> bool: ...
26
+
27
+
28
+ type CancelSignal = SupportsIsSet | SupportsAborted
29
+
30
+
31
+ @dataclass(slots=True, frozen=True)
32
+ class CodexOptions:
33
+ codex_path_override: str | None = None
34
+ base_url: str | None = None
35
+ api_key: str | None = None
36
+ config: CodexConfigObject | None = None
37
+ env: dict[str, str] | None = None
38
+
39
+
40
+ @dataclass(slots=True, frozen=True)
41
+ class ThreadOptions:
42
+ model: str | None = None
43
+ sandbox_mode: SandboxMode | None = None
44
+ working_directory: str | None = None
45
+ skip_git_repo_check: bool = False
46
+ model_reasoning_effort: ModelReasoningEffort | None = None
47
+ network_access_enabled: bool | None = None
48
+ web_search_mode: WebSearchMode | None = None
49
+ web_search_enabled: bool | None = None
50
+ approval_policy: ApprovalMode | None = None
51
+ additional_directories: list[str] | None = None
52
+
53
+
54
+ @dataclass(slots=True, frozen=True)
55
+ class TurnOptions:
56
+ output_schema: dict[str, object] | None = None
57
+ signal: CancelSignal | None = None
@@ -3,16 +3,20 @@ from __future__ import annotations
3
3
  import json
4
4
  from collections.abc import Iterator, Mapping, Sequence
5
5
  from dataclasses import dataclass
6
- from typing import Any, Literal, TypedDict, cast
6
+ from typing import Any, Literal, Protocol, TypedDict, cast
7
7
 
8
8
  from codex.errors import CodexParseError, ThreadRunError
9
9
  from codex.events import ThreadEvent, Usage
10
- from codex.exec import CodexExec, CodexExecArgs
10
+ from codex.exec import CodexExecArgs
11
11
  from codex.items import ThreadItem
12
12
  from codex.options import CodexOptions, ThreadOptions, TurnOptions
13
13
  from codex.output_schema_file import create_output_schema_file
14
14
 
15
15
 
16
+ class ExecRunner(Protocol):
17
+ def run(self, args: CodexExecArgs) -> Iterator[str]: ...
18
+
19
+
16
20
  class TextInput(TypedDict):
17
21
  type: Literal["text"]
18
22
  text: str
@@ -42,7 +46,7 @@ class RunStreamedResult:
42
46
  class Thread:
43
47
  def __init__(
44
48
  self,
45
- exec_runner: CodexExec,
49
+ exec_runner: ExecRunner,
46
50
  options: CodexOptions,
47
51
  thread_options: ThreadOptions,
48
52
  thread_id: str | None = None,
@@ -77,8 +81,15 @@ class Thread:
77
81
  model=options.model,
78
82
  sandbox_mode=options.sandbox_mode,
79
83
  working_directory=options.working_directory,
84
+ additional_directories=options.additional_directories,
80
85
  skip_git_repo_check=options.skip_git_repo_check,
81
86
  output_schema_file=schema_file.schema_path,
87
+ model_reasoning_effort=options.model_reasoning_effort,
88
+ signal=effective_turn_options.signal,
89
+ network_access_enabled=options.network_access_enabled,
90
+ web_search_mode=options.web_search_mode,
91
+ web_search_enabled=options.web_search_enabled,
92
+ approval_policy=options.approval_policy,
82
93
  )
83
94
  try:
84
95
  for item in self._exec.run(exec_args):
@@ -95,7 +106,6 @@ class Thread:
95
106
  final_response = ""
96
107
  usage: Usage | None = None
97
108
  turn_failure: str | None = None
98
- saw_turn_complete = False
99
109
  for event in generator:
100
110
  event_dict = cast(dict[str, Any], event)
101
111
  event_type = event_dict.get("type")
@@ -111,7 +121,6 @@ class Thread:
111
121
  usage_value = event_dict.get("usage")
112
122
  if isinstance(usage_value, dict):
113
123
  usage = cast(Usage, usage_value)
114
- saw_turn_complete = True
115
124
  elif event_type == "turn.failed":
116
125
  error_value = event_dict.get("error")
117
126
  if isinstance(error_value, dict):
@@ -119,17 +128,8 @@ class Thread:
119
128
  if isinstance(message, str):
120
129
  turn_failure = message
121
130
  break
122
- elif event_type == "error":
123
- message_value = event_dict.get("message")
124
- if isinstance(message_value, str):
125
- turn_failure = message_value
126
- break
127
131
  if turn_failure is not None:
128
132
  raise ThreadRunError(turn_failure)
129
- if not saw_turn_complete:
130
- raise ThreadRunError(
131
- "stream disconnected before completion: missing turn.completed event"
132
- )
133
133
  return RunResult(items=items, final_response=final_response, usage=usage)
134
134
 
135
135
 
@@ -20,7 +20,7 @@ dependencies = [
20
20
 
21
21
  [[package]]
22
22
  name = "codex_native"
23
- version = "1.0.0"
23
+ version = "1.0.0-beta.3"
24
24
  dependencies = [
25
25
  "pyo3",
26
26
  ]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "codex_native"
3
- version = "1.0.0"
3
+ version = "1.0.0-beta.3"
4
4
  edition = "2021"
5
5
 
6
6
  [lib]
@@ -65,6 +65,7 @@ ignore_missing_imports = true
65
65
  exclude = [
66
66
  "^codex-proj/",
67
67
  "^\\.generated/",
68
+ "^crates/codex_native/codex/",
68
69
  "^crates/.*/target/",
69
70
  "^dist/",
70
71
  ]
@@ -1,118 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import os
4
- import shutil
5
- import subprocess
6
- from collections.abc import Iterator
7
- from dataclasses import dataclass
8
- from pathlib import Path
9
-
10
- from codex._binary import bundled_codex_path
11
- from codex.errors import CodexExecError
12
- from codex.options import SandboxMode
13
-
14
- INTERNAL_ORIGINATOR_ENV = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE"
15
- PYTHON_SDK_ORIGINATOR = "codex_sdk_py"
16
-
17
-
18
- @dataclass(slots=True, frozen=True)
19
- class CodexExecArgs:
20
- input: str
21
- base_url: str | None = None
22
- api_key: str | None = None
23
- thread_id: str | None = None
24
- images: list[str] | None = None
25
- model: str | None = None
26
- sandbox_mode: SandboxMode | None = None
27
- working_directory: str | None = None
28
- skip_git_repo_check: bool = False
29
- output_schema_file: str | None = None
30
-
31
-
32
- class CodexExec:
33
- def __init__(self, executable_path: str | None = None) -> None:
34
- if executable_path is not None:
35
- path = Path(executable_path)
36
- else:
37
- try:
38
- path = bundled_codex_path()
39
- except CodexExecError as bundled_error:
40
- system_codex = shutil.which("codex")
41
- if system_codex is None:
42
- raise CodexExecError(
43
- f"{bundled_error} Also failed to find `codex` on PATH."
44
- ) from bundled_error
45
- path = Path(system_codex)
46
- self.executable_path = str(path)
47
-
48
- def run(self, args: CodexExecArgs) -> Iterator[str]:
49
- command_args: list[str] = ["exec", "--experimental-json"]
50
-
51
- if args.model is not None:
52
- command_args.extend(["--model", args.model])
53
- if args.sandbox_mode is not None:
54
- command_args.extend(["--sandbox", args.sandbox_mode])
55
- if args.working_directory is not None:
56
- command_args.extend(["--cd", args.working_directory])
57
- if args.skip_git_repo_check:
58
- command_args.append("--skip-git-repo-check")
59
- if args.output_schema_file is not None:
60
- command_args.extend(["--output-schema", args.output_schema_file])
61
- if args.images is not None:
62
- for image in args.images:
63
- command_args.extend(["--image", image])
64
- if args.thread_id:
65
- command_args.extend(["resume", args.thread_id])
66
-
67
- env = os.environ.copy()
68
- if INTERNAL_ORIGINATOR_ENV not in env:
69
- env[INTERNAL_ORIGINATOR_ENV] = PYTHON_SDK_ORIGINATOR
70
- if args.base_url is not None:
71
- env["OPENAI_BASE_URL"] = args.base_url
72
- if args.api_key is not None:
73
- env["CODEX_API_KEY"] = args.api_key
74
-
75
- try:
76
- child = subprocess.Popen(
77
- [self.executable_path, *command_args],
78
- stdin=subprocess.PIPE,
79
- stdout=subprocess.PIPE,
80
- stderr=subprocess.PIPE,
81
- text=True,
82
- encoding="utf-8",
83
- env=env,
84
- )
85
- except OSError as exc:
86
- raise CodexExecError(
87
- f"Failed to spawn codex executable at '{self.executable_path}': {exc}"
88
- ) from exc
89
-
90
- if child.stdin is None:
91
- child.kill()
92
- raise CodexExecError("Child process has no stdin")
93
- if child.stdout is None:
94
- child.kill()
95
- raise CodexExecError("Child process has no stdout")
96
- if child.stderr is None:
97
- child.kill()
98
- raise CodexExecError("Child process has no stderr")
99
-
100
- try:
101
- child.stdin.write(args.input)
102
- child.stdin.close()
103
- except OSError as exc:
104
- child.kill()
105
- raise CodexExecError(f"Failed to write input to codex process: {exc}") from exc
106
-
107
- try:
108
- for line in child.stdout:
109
- yield line.rstrip("\r\n")
110
- finally:
111
- child.stdout.close()
112
-
113
- exit_code = child.wait()
114
- stderr = child.stderr.read()
115
- child.stderr.close()
116
-
117
- if exit_code != 0:
118
- raise CodexExecError(f"Codex exec exited with code {exit_code}: {stderr}")
@@ -1,27 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from typing import Literal
5
-
6
- ApprovalMode = Literal["never", "on-request", "on-failure", "untrusted"]
7
- SandboxMode = Literal["read-only", "workspace-write", "danger-full-access"]
8
-
9
-
10
- @dataclass(slots=True, frozen=True)
11
- class CodexOptions:
12
- codex_path_override: str | None = None
13
- base_url: str | None = None
14
- api_key: str | None = None
15
-
16
-
17
- @dataclass(slots=True, frozen=True)
18
- class ThreadOptions:
19
- model: str | None = None
20
- sandbox_mode: SandboxMode | None = None
21
- working_directory: str | None = None
22
- skip_git_repo_check: bool = False
23
-
24
-
25
- @dataclass(slots=True, frozen=True)
26
- class TurnOptions:
27
- output_schema: dict[str, object] | None = None
File without changes