codex-python 1.0.0__cp312-abi3-win_arm64.whl

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.
codex/__init__.py ADDED
@@ -0,0 +1,70 @@
1
+ """Python SDK for embedding Codex via the bundled CLI binary."""
2
+
3
+ from codex.codex import Codex
4
+ from codex.errors import CodexError, CodexExecError, CodexParseError, ThreadRunError
5
+ from codex.events import (
6
+ ItemCompletedEvent,
7
+ ItemStartedEvent,
8
+ ItemUpdatedEvent,
9
+ ThreadError,
10
+ ThreadErrorEvent,
11
+ ThreadEvent,
12
+ ThreadStartedEvent,
13
+ TurnCompletedEvent,
14
+ TurnFailedEvent,
15
+ TurnStartedEvent,
16
+ Usage,
17
+ )
18
+ from codex.items import (
19
+ AgentMessageItem,
20
+ CommandExecutionItem,
21
+ ErrorItem,
22
+ FileChangeItem,
23
+ McpToolCallItem,
24
+ ReasoningItem,
25
+ ThreadItem,
26
+ TodoListItem,
27
+ WebSearchItem,
28
+ )
29
+ from codex.options import ApprovalMode, CodexOptions, SandboxMode, ThreadOptions, TurnOptions
30
+ from codex.thread import Input, RunResult, RunStreamedResult, Thread, UserInput
31
+
32
+ __version__ = "1.0.0"
33
+
34
+ __all__ = [
35
+ "Codex",
36
+ "CodexError",
37
+ "CodexExecError",
38
+ "CodexParseError",
39
+ "ThreadRunError",
40
+ "Thread",
41
+ "RunResult",
42
+ "RunStreamedResult",
43
+ "Input",
44
+ "UserInput",
45
+ "CodexOptions",
46
+ "ThreadOptions",
47
+ "TurnOptions",
48
+ "ApprovalMode",
49
+ "SandboxMode",
50
+ "ThreadEvent",
51
+ "ThreadStartedEvent",
52
+ "TurnStartedEvent",
53
+ "TurnCompletedEvent",
54
+ "TurnFailedEvent",
55
+ "ItemStartedEvent",
56
+ "ItemUpdatedEvent",
57
+ "ItemCompletedEvent",
58
+ "ThreadError",
59
+ "ThreadErrorEvent",
60
+ "Usage",
61
+ "ThreadItem",
62
+ "AgentMessageItem",
63
+ "ReasoningItem",
64
+ "CommandExecutionItem",
65
+ "FileChangeItem",
66
+ "McpToolCallItem",
67
+ "WebSearchItem",
68
+ "TodoListItem",
69
+ "ErrorItem",
70
+ ]
codex/_binary.py ADDED
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ import platform
4
+ from pathlib import Path
5
+
6
+ from codex.errors import CodexExecError
7
+
8
+
9
+ def resolve_target_triple(system_name: str | None = None, machine_name: str | None = None) -> str:
10
+ system = (system_name or platform.system()).lower()
11
+ machine = (machine_name or platform.machine()).lower()
12
+
13
+ if system in {"linux", "android"}:
14
+ if machine in {"x86_64", "amd64"}:
15
+ return "x86_64-unknown-linux-musl"
16
+ if machine in {"aarch64", "arm64"}:
17
+ return "aarch64-unknown-linux-musl"
18
+ elif system == "darwin":
19
+ if machine in {"x86_64", "amd64"}:
20
+ return "x86_64-apple-darwin"
21
+ if machine in {"aarch64", "arm64"}:
22
+ return "aarch64-apple-darwin"
23
+ elif system in {"windows", "win32"}:
24
+ if machine in {"x86_64", "amd64"}:
25
+ return "x86_64-pc-windows-msvc"
26
+ if machine in {"aarch64", "arm64"}:
27
+ return "aarch64-pc-windows-msvc"
28
+
29
+ raise CodexExecError(f"Unsupported platform: {system} ({machine})")
30
+
31
+
32
+ def bundled_codex_path(target_triple: str | None = None) -> Path:
33
+ triple = target_triple or resolve_target_triple()
34
+ package_root = Path(__file__).resolve().parent
35
+ binary_name = "codex.exe" if "windows" in triple else "codex"
36
+ binary_path = package_root / "vendor" / triple / "codex" / binary_name
37
+ if not binary_path.exists():
38
+ raise CodexExecError(
39
+ "Bundled codex binary not found at "
40
+ f"{binary_path}. Install a platform wheel or provide codex_path_override."
41
+ )
42
+ return binary_path
codex/codex.py ADDED
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ from codex.exec import CodexExec
4
+ from codex.options import CodexOptions, ThreadOptions
5
+ from codex.thread import Thread
6
+
7
+
8
+ class Codex:
9
+ """Main entrypoint for interacting with Codex threads."""
10
+
11
+ def __init__(self, options: CodexOptions | None = None) -> None:
12
+ resolved = options or CodexOptions()
13
+ self._exec = CodexExec(resolved.codex_path_override)
14
+ self._options = resolved
15
+
16
+ def start_thread(self, options: ThreadOptions | None = None) -> Thread:
17
+ return Thread(self._exec, self._options, options or ThreadOptions())
18
+
19
+ def resume_thread(self, id: str, options: ThreadOptions | None = None) -> Thread:
20
+ if id == "":
21
+ raise ValueError("id must be non-empty")
22
+ return Thread(self._exec, self._options, options or ThreadOptions(), id)
codex/errors.py ADDED
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class CodexError(RuntimeError):
5
+ """Base error for the Python Codex SDK."""
6
+
7
+
8
+ class CodexExecError(CodexError):
9
+ """Raised when the Codex CLI process fails."""
10
+
11
+
12
+ class CodexParseError(CodexError):
13
+ """Raised when streaming JSONL events cannot be parsed."""
14
+
15
+
16
+ class ThreadRunError(CodexError):
17
+ """Raised when a run or stream fails before turn completion."""
codex/events.py ADDED
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal, TypedDict
4
+
5
+ from codex.items import ThreadItem
6
+
7
+
8
+ class ThreadStartedEvent(TypedDict):
9
+ type: Literal["thread.started"]
10
+ thread_id: str
11
+
12
+
13
+ class TurnStartedEvent(TypedDict):
14
+ type: Literal["turn.started"]
15
+
16
+
17
+ class Usage(TypedDict):
18
+ input_tokens: int
19
+ cached_input_tokens: int
20
+ output_tokens: int
21
+
22
+
23
+ class TurnCompletedEvent(TypedDict):
24
+ type: Literal["turn.completed"]
25
+ usage: Usage
26
+
27
+
28
+ class ThreadError(TypedDict):
29
+ message: str
30
+
31
+
32
+ class TurnFailedEvent(TypedDict):
33
+ type: Literal["turn.failed"]
34
+ error: ThreadError
35
+
36
+
37
+ class ItemStartedEvent(TypedDict):
38
+ type: Literal["item.started"]
39
+ item: ThreadItem
40
+
41
+
42
+ class ItemUpdatedEvent(TypedDict):
43
+ type: Literal["item.updated"]
44
+ item: ThreadItem
45
+
46
+
47
+ class ItemCompletedEvent(TypedDict):
48
+ type: Literal["item.completed"]
49
+ item: ThreadItem
50
+
51
+
52
+ class ThreadErrorEvent(TypedDict):
53
+ type: Literal["error"]
54
+ message: str
55
+
56
+
57
+ ThreadEvent = (
58
+ ThreadStartedEvent
59
+ | TurnStartedEvent
60
+ | TurnCompletedEvent
61
+ | TurnFailedEvent
62
+ | ItemStartedEvent
63
+ | ItemUpdatedEvent
64
+ | ItemCompletedEvent
65
+ | ThreadErrorEvent
66
+ )
codex/exec.py ADDED
@@ -0,0 +1,118 @@
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}")
codex/items.py ADDED
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal, NotRequired, TypedDict
4
+
5
+ CommandExecutionStatus = Literal["in_progress", "completed", "failed"]
6
+ PatchChangeKind = Literal["add", "delete", "update"]
7
+ PatchApplyStatus = Literal["completed", "failed"]
8
+ McpToolCallStatus = Literal["in_progress", "completed", "failed"]
9
+
10
+
11
+ class CommandExecutionItem(TypedDict):
12
+ id: str
13
+ type: Literal["command_execution"]
14
+ command: str
15
+ aggregated_output: str
16
+ status: CommandExecutionStatus
17
+ exit_code: NotRequired[int]
18
+
19
+
20
+ class FileUpdateChange(TypedDict):
21
+ path: str
22
+ kind: PatchChangeKind
23
+
24
+
25
+ class FileChangeItem(TypedDict):
26
+ id: str
27
+ type: Literal["file_change"]
28
+ changes: list[FileUpdateChange]
29
+ status: PatchApplyStatus
30
+
31
+
32
+ class McpToolCallItem(TypedDict):
33
+ id: str
34
+ type: Literal["mcp_tool_call"]
35
+ server: str
36
+ tool: str
37
+ status: McpToolCallStatus
38
+
39
+
40
+ class AgentMessageItem(TypedDict):
41
+ id: str
42
+ type: Literal["agent_message"]
43
+ text: str
44
+
45
+
46
+ class ReasoningItem(TypedDict):
47
+ id: str
48
+ type: Literal["reasoning"]
49
+ text: str
50
+
51
+
52
+ class WebSearchItem(TypedDict):
53
+ id: str
54
+ type: Literal["web_search"]
55
+ query: str
56
+
57
+
58
+ class ErrorItem(TypedDict):
59
+ id: str
60
+ type: Literal["error"]
61
+ message: str
62
+
63
+
64
+ class TodoItem(TypedDict):
65
+ text: str
66
+ completed: bool
67
+
68
+
69
+ class TodoListItem(TypedDict):
70
+ id: str
71
+ type: Literal["todo_list"]
72
+ items: list[TodoItem]
73
+
74
+
75
+ ThreadItem = (
76
+ AgentMessageItem
77
+ | ReasoningItem
78
+ | CommandExecutionItem
79
+ | FileChangeItem
80
+ | McpToolCallItem
81
+ | WebSearchItem
82
+ | TodoListItem
83
+ | ErrorItem
84
+ )
codex/options.py ADDED
@@ -0,0 +1,27 @@
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
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import shutil
5
+ import tempfile
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+
10
+ @dataclass(slots=True, frozen=True)
11
+ class OutputSchemaFile:
12
+ schema_path: str | None
13
+ schema_dir: str | None
14
+
15
+ def cleanup(self) -> None:
16
+ if self.schema_dir is not None:
17
+ shutil.rmtree(self.schema_dir, ignore_errors=True)
18
+
19
+
20
+ def create_output_schema_file(schema: object | None) -> OutputSchemaFile:
21
+ if schema is None:
22
+ return OutputSchemaFile(schema_path=None, schema_dir=None)
23
+
24
+ if not isinstance(schema, dict):
25
+ raise ValueError("output_schema must be a plain JSON object")
26
+
27
+ schema_dir = Path(tempfile.mkdtemp(prefix="codex-output-schema-"))
28
+ schema_path = schema_dir / "schema.json"
29
+ try:
30
+ schema_path.write_text(json.dumps(schema), encoding="utf-8")
31
+ except Exception:
32
+ shutil.rmtree(schema_dir, ignore_errors=True)
33
+ raise
34
+ return OutputSchemaFile(schema_path=str(schema_path), schema_dir=str(schema_dir))
codex/py.typed ADDED
@@ -0,0 +1 @@
1
+
codex/thread.py ADDED
@@ -0,0 +1,170 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from collections.abc import Iterator, Mapping, Sequence
5
+ from dataclasses import dataclass
6
+ from typing import Any, Literal, TypedDict, cast
7
+
8
+ from codex.errors import CodexParseError, ThreadRunError
9
+ from codex.events import ThreadEvent, Usage
10
+ from codex.exec import CodexExec, CodexExecArgs
11
+ from codex.items import ThreadItem
12
+ from codex.options import CodexOptions, ThreadOptions, TurnOptions
13
+ from codex.output_schema_file import create_output_schema_file
14
+
15
+
16
+ class TextInput(TypedDict):
17
+ type: Literal["text"]
18
+ text: str
19
+
20
+
21
+ class LocalImageInput(TypedDict):
22
+ type: Literal["local_image"]
23
+ path: str
24
+
25
+
26
+ UserInput = TextInput | LocalImageInput
27
+ Input = str | Sequence[UserInput]
28
+
29
+
30
+ @dataclass(slots=True, frozen=True)
31
+ class RunResult:
32
+ items: list[ThreadItem]
33
+ final_response: str
34
+ usage: Usage | None
35
+
36
+
37
+ @dataclass(slots=True, frozen=True)
38
+ class RunStreamedResult:
39
+ events: Iterator[ThreadEvent]
40
+
41
+
42
+ class Thread:
43
+ def __init__(
44
+ self,
45
+ exec_runner: CodexExec,
46
+ options: CodexOptions,
47
+ thread_options: ThreadOptions,
48
+ thread_id: str | None = None,
49
+ ) -> None:
50
+ self._exec = exec_runner
51
+ self._options = options
52
+ self._thread_options = thread_options
53
+ self._id = thread_id
54
+
55
+ @property
56
+ def id(self) -> str | None:
57
+ return self._id
58
+
59
+ def run_streamed(
60
+ self, input: Input, turn_options: TurnOptions | None = None
61
+ ) -> RunStreamedResult:
62
+ return RunStreamedResult(events=self._run_streamed_internal(input, turn_options))
63
+
64
+ def _run_streamed_internal(
65
+ self, input: Input, turn_options: TurnOptions | None = None
66
+ ) -> Iterator[ThreadEvent]:
67
+ effective_turn_options = turn_options or TurnOptions()
68
+ schema_file = create_output_schema_file(effective_turn_options.output_schema)
69
+ prompt, images = normalize_input(input)
70
+ options = self._thread_options
71
+ exec_args = CodexExecArgs(
72
+ input=prompt,
73
+ base_url=self._options.base_url,
74
+ api_key=self._options.api_key,
75
+ thread_id=self._id,
76
+ images=images,
77
+ model=options.model,
78
+ sandbox_mode=options.sandbox_mode,
79
+ working_directory=options.working_directory,
80
+ skip_git_repo_check=options.skip_git_repo_check,
81
+ output_schema_file=schema_file.schema_path,
82
+ )
83
+ try:
84
+ for item in self._exec.run(exec_args):
85
+ parsed = parse_thread_event(item)
86
+ if parsed["type"] == "thread.started":
87
+ self._id = parsed["thread_id"]
88
+ yield parsed
89
+ finally:
90
+ schema_file.cleanup()
91
+
92
+ def run(self, input: Input, turn_options: TurnOptions | None = None) -> RunResult:
93
+ generator = self._run_streamed_internal(input, turn_options)
94
+ items: list[ThreadItem] = []
95
+ final_response = ""
96
+ usage: Usage | None = None
97
+ turn_failure: str | None = None
98
+ saw_turn_complete = False
99
+ for event in generator:
100
+ event_dict = cast(dict[str, Any], event)
101
+ event_type = event_dict.get("type")
102
+ if event_type == "item.completed":
103
+ item = event_dict.get("item")
104
+ if isinstance(item, dict):
105
+ if item.get("type") == "agent_message":
106
+ text = item.get("text")
107
+ if isinstance(text, str):
108
+ final_response = text
109
+ items.append(cast(ThreadItem, item))
110
+ elif event_type == "turn.completed":
111
+ usage_value = event_dict.get("usage")
112
+ if isinstance(usage_value, dict):
113
+ usage = cast(Usage, usage_value)
114
+ saw_turn_complete = True
115
+ elif event_type == "turn.failed":
116
+ error_value = event_dict.get("error")
117
+ if isinstance(error_value, dict):
118
+ message = error_value.get("message")
119
+ if isinstance(message, str):
120
+ turn_failure = message
121
+ 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
+ if turn_failure is not None:
128
+ raise ThreadRunError(turn_failure)
129
+ if not saw_turn_complete:
130
+ raise ThreadRunError(
131
+ "stream disconnected before completion: missing turn.completed event"
132
+ )
133
+ return RunResult(items=items, final_response=final_response, usage=usage)
134
+
135
+
136
+ def parse_thread_event(raw_line: str) -> ThreadEvent:
137
+ try:
138
+ parsed = json.loads(raw_line)
139
+ except json.JSONDecodeError as exc:
140
+ raise CodexParseError(f"Failed to parse item: {raw_line}") from exc
141
+
142
+ if not isinstance(parsed, Mapping):
143
+ raise CodexParseError(f"Expected object event, received {type(parsed).__name__}")
144
+ event_type = parsed.get("type")
145
+ if not isinstance(event_type, str):
146
+ raise CodexParseError("Event is missing string field 'type'")
147
+ return cast(ThreadEvent, dict(parsed))
148
+
149
+
150
+ def normalize_input(input_value: Input) -> tuple[str, list[str]]:
151
+ if isinstance(input_value, str):
152
+ return input_value, []
153
+
154
+ prompt_parts: list[str] = []
155
+ images: list[str] = []
156
+ for item in input_value:
157
+ item_type = item.get("type")
158
+ if item_type == "text":
159
+ text = item.get("text")
160
+ if not isinstance(text, str):
161
+ raise ValueError("text input item requires string field 'text'")
162
+ prompt_parts.append(text)
163
+ elif item_type == "local_image":
164
+ path = item.get("path")
165
+ if not isinstance(path, str):
166
+ raise ValueError("local_image input item requires string field 'path'")
167
+ images.append(path)
168
+ else:
169
+ raise ValueError(f"Unsupported input item type: {item_type}")
170
+ return "\n\n".join(prompt_parts), images
codex/vendor/.gitkeep ADDED
File without changes
@@ -0,0 +1,5 @@
1
+ from .codex_native import *
2
+
3
+ __doc__ = codex_native.__doc__
4
+ if hasattr(codex_native, "__all__"):
5
+ __all__ = codex_native.__all__
Binary file
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: codex-python
3
+ Version: 1.0.0
4
+ Classifier: Programming Language :: Python :: 3
5
+ Classifier: Programming Language :: Python :: 3 :: Only
6
+ Classifier: Programming Language :: Python :: 3.13
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Typing :: Typed
9
+ Classifier: Operating System :: OS Independent
10
+ License-File: LICENSE
11
+ Summary: Python SDK for Codex CLI with bundled platform binaries
12
+ Keywords: codex,sdk,cli,automation
13
+ Requires-Python: >=3.12
14
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
15
+ Project-URL: Homepage, https://github.com/gersmann/codex-python
16
+ Project-URL: Issues, https://github.com/gersmann/codex-python/issues
17
+ Project-URL: Repository, https://github.com/gersmann/codex-python
18
+
19
+ # codex-python
20
+
21
+ Python SDK for Codex with bundled `codex` binaries inside platform wheels.
22
+
23
+ The SDK mirrors the TypeScript SDK behavior:
24
+ - Spawns `codex exec --experimental-json`
25
+ - Streams JSONL events
26
+ - Supports thread resume, structured output schemas, images, sandbox/model options
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ pip install codex-python
32
+ ```
33
+
34
+ ## Quickstart
35
+
36
+ ```python
37
+ from codex import Codex
38
+
39
+ client = Codex()
40
+ thread = client.start_thread()
41
+
42
+ result = thread.run("Diagnose the failing tests and propose a fix")
43
+ print(result.final_response)
44
+ print(result.items)
45
+ ```
46
+
47
+ ## Streaming
48
+
49
+ ```python
50
+ from codex import Codex
51
+
52
+ client = Codex()
53
+ thread = client.start_thread()
54
+
55
+ stream = thread.run_streamed("Investigate this bug")
56
+ for event in stream.events:
57
+ if event["type"] == "item.completed":
58
+ print(event["item"])
59
+ elif event["type"] == "turn.completed":
60
+ print(event["usage"])
61
+ ```
62
+
63
+ ## Structured output
64
+
65
+ ```python
66
+ from codex import Codex, TurnOptions
67
+
68
+ schema = {
69
+ "type": "object",
70
+ "properties": {"summary": {"type": "string"}},
71
+ "required": ["summary"],
72
+ "additionalProperties": False,
73
+ }
74
+
75
+ client = Codex()
76
+ thread = client.start_thread()
77
+ result = thread.run("Summarize repository status", TurnOptions(output_schema=schema))
78
+ print(result.final_response)
79
+ ```
80
+
81
+ ## Input with local images
82
+
83
+ ```python
84
+ from codex import Codex
85
+
86
+ client = Codex()
87
+ thread = client.start_thread()
88
+ result = thread.run(
89
+ [
90
+ {"type": "text", "text": "Describe these screenshots"},
91
+ {"type": "local_image", "path": "./ui.png"},
92
+ {"type": "local_image", "path": "./diagram.jpg"},
93
+ ]
94
+ )
95
+ ```
96
+
97
+ ## Resume a thread
98
+
99
+ ```python
100
+ from codex import Codex
101
+
102
+ client = Codex()
103
+ thread = client.resume_thread("thread_123")
104
+ thread.run("Continue from previous context")
105
+ ```
106
+
107
+ ## Options
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`
112
+
113
+ ## Bundled binary behavior
114
+
115
+ By default, the SDK resolves the bundled binary at:
116
+
117
+ `codex/vendor/<target-triple>/codex/{codex|codex.exe}`
118
+
119
+ If the bundled binary is not present (for example in a source checkout), the SDK falls back to
120
+ `codex` on `PATH`.
121
+
122
+ You can always override with `CodexOptions(codex_path_override=...)`.
123
+
124
+ ## Development
125
+
126
+ ```bash
127
+ make lint
128
+ make test
129
+ ```
130
+
131
+ If you want to test vendored-binary behavior locally, fetch binaries into `codex/vendor`:
132
+
133
+ ```bash
134
+ python scripts/fetch_codex_binary.py --target-triple x86_64-unknown-linux-musl
135
+ ```
136
+
@@ -0,0 +1,19 @@
1
+ codex\__init__.py,sha256=rno8Yk5ORFZtbAFKIOWtCOwV6EbWpLs3ZNQcgUzzmtM,1647
2
+ codex\_binary.py,sha256=oCX2rNA_AZH1T7zClwe98uSGRGo4v-jhPoVRsC9qsTA,1633
3
+ codex\codex.py,sha256=UUOaRmo7YbaDqA4muxqqtUuOZ5ITy2NeMQmIGhlQwGg,859
4
+ codex\errors.py,sha256=xY4Yiz69OO0HMVysqSm_GG-yZHTyINB_O3-3314bZIQ,424
5
+ codex\events.py,sha256=2mJ_8TM5VSXNkiPG6-Q7HE09r-fcCD_MonPUdKLp43g,1242
6
+ codex\exec.py,sha256=mCumau0vHQFHJ2E3UmvL8Yq7FNa3ly4ysnF1NreBhIw,4209
7
+ codex\items.py,sha256=bEz_7H2p6eD1DoGdCwh81HCDExUfaXJ-x_mGF6bDNm8,1698
8
+ codex\options.py,sha256=26W-J5ZDx35n2A-PrKWvgSupv9zIQGr2Uj-SyeFiTcM,757
9
+ codex\output_schema_file.py,sha256=EWuUChz7aXtjLGuBQo14BzJogf9bffoXjUYjKMiunyk,1068
10
+ codex\py.typed,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
11
+ codex\thread.py,sha256=RWItAf9zgc-ag_PWzoIDVT-P7sdARWlHE-qCK-HXLwY,6303
12
+ codex\vendor\.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ codex\vendor\aarch64-pc-windows-msvc\codex\codex.exe,sha256=nMLg6_LimKG14gOtEIFi32AEeZ-bPd1P9pMNuHPsli4,68397032
14
+ codex_native\__init__.py,sha256=F6wnaifw2lA804o89FM946g2tbrunIvG3xy1PWR_9DE,131
15
+ codex_native\codex_native.pyd,sha256=ztpeMe0lMDgAdHVpG9fB8KprMDL-AQAG5ichvfJKF4s,166400
16
+ codex_python-1.0.0.dist-info\METADATA,sha256=wCN6WIZAXeFXlEUp9I-LCxoW5kGxnmSj9Y2iEp9Ii-8,3386
17
+ codex_python-1.0.0.dist-info\WHEEL,sha256=FDCxeh2p7wU8Ho3i9fQc9LZ-W3b0At4U5rtXgMygv9s,96
18
+ codex_python-1.0.0.dist-info\licenses\LICENSE,sha256=1jaLCCF7Ws0ixvNF8sUq2FgCo_cvvLL8xIucPAFdhK8,1088
19
+ codex_python-1.0.0.dist-info\RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.11.5)
3
+ Root-Is-Purelib: false
4
+ Tag: cp312-abi3-win_arm64
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 gersmann
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+