codex-python 1.0.0__cp313-cp313t-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 +70 -0
- codex/_binary.py +42 -0
- codex/codex.py +22 -0
- codex/errors.py +17 -0
- codex/events.py +66 -0
- codex/exec.py +118 -0
- codex/items.py +84 -0
- codex/options.py +27 -0
- codex/output_schema_file.py +34 -0
- codex/py.typed +1 -0
- codex/thread.py +170 -0
- codex/vendor/.gitkeep +0 -0
- codex/vendor/aarch64-pc-windows-msvc/codex/codex.exe +0 -0
- codex_native/__init__.py +5 -0
- codex_native/codex_native.cp313t-win_arm64.pyd +0 -0
- codex_python-1.0.0.dist-info/METADATA +136 -0
- codex_python-1.0.0.dist-info/RECORD +19 -0
- codex_python-1.0.0.dist-info/WHEEL +4 -0
- codex_python-1.0.0.dist-info/licenses/LICENSE +22 -0
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
|
|
Binary file
|
codex_native/__init__.py
ADDED
|
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.cp313t-win_arm64.pyd,sha256=LOq-za25yVqXgkm_kOz3A7F6u9982q80GBj62GVh-eA,165888
|
|
16
|
+
codex_python-1.0.0.dist-info\METADATA,sha256=wCN6WIZAXeFXlEUp9I-LCxoW5kGxnmSj9Y2iEp9Ii-8,3386
|
|
17
|
+
codex_python-1.0.0.dist-info\WHEEL,sha256=HzMhMZ5m0Z89vUkS-V0VF24Ien1b8bNjE3scHQmaZDQ,98
|
|
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,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
|
+
|