Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion .github/workflows/sdk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
pull_request: {}

jobs:
sdks:
typescript-sdk:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
Expand Down Expand Up @@ -41,3 +41,40 @@ jobs:

- name: Test SDK packages
run: pnpm -r --filter ./sdk/typescript run test

python-sdk:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout repository
uses: actions/checkout@v5

- uses: dtolnay/[email protected]

- name: Build codex
run: cargo build --bin codex
working-directory: codex-rs

- name: Install uv
uses: astral-sh/setup-uv@v6
with:
python-version: "3.12"
enable-cache: true
cache-dependency-glob: |
sdk/python/pyproject.toml
sdk/python/uv.lock

- name: Install Python runtime
run: uv python install 3.12

- name: Sync Python dependencies
run: uv sync --project sdk/python --extra dev

- name: Lint Python SDK
run: uv run --project sdk/python ruff check

- name: Type-check Python SDK
run: uv run --project sdk/python mypy --config-file sdk/python/pyproject.toml sdk/python/src/codex

- name: Test Python SDK
run: uv run --project sdk/python pytest
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ yarn-error.log*
# env
.env*
!.env.example
**/.venv/
**/.ruff_cache/
sdk/python/src/codex/vendor/**
!sdk/python/src/codex/vendor/
!sdk/python/src/codex/vendor/README.md

# package
*.tgz
Expand Down
6 changes: 3 additions & 3 deletions scripts/readme_toc.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ def check_or_fix(readme_path: Path, fix: bool) -> int:
lines = content.splitlines()
# locate ToC markers
try:
begin_idx = next(i for i, l in enumerate(lines) if l.strip() == BEGIN_TOC)
end_idx = next(i for i, l in enumerate(lines) if l.strip() == END_TOC)
begin_idx = next(i for i, line in enumerate(lines) if line.strip() == BEGIN_TOC)
end_idx = next(i for i, line in enumerate(lines) if line.strip() == END_TOC)
except StopIteration:
# No ToC markers found; treat as a no-op so repos without a ToC don't fail CI
print(
Expand All @@ -86,7 +86,7 @@ def check_or_fix(readme_path: Path, fix: bool) -> int:
return 0
# extract current ToC list items
current_block = lines[begin_idx + 1 : end_idx]
current = [l for l in current_block if l.lstrip().startswith("- [")]
current = [line for line in current_block if line.lstrip().startswith("- [")]
# generate expected ToC
expected = generate_toc_lines(content)
if current == expected:
Expand Down
1 change: 1 addition & 0 deletions sdk/python/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.13
103 changes: 103 additions & 0 deletions sdk/python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Codex Python SDK

Embed the Codex agent in Python workflows. This SDK shells out to the bundled `codex` CLI, streams
structured events, and provides strongly-typed helpers for synchronous and streaming turns.

## Status

- Target Python 3.12+.
- API and packaging are pre-alpha; expect breaking changes.
- Binaries are bundled under `codex/vendor` for supported triples.

## Quickstart

```python
from codex import Codex

client = Codex()
thread = client.start_thread()
turn = thread.run("Summarize the latest CI failure.")

print(turn.final_response)
for item in turn.items:
print(item)
```

## Streaming

```python
from codex import Codex

client = Codex()
thread = client.start_thread()

stream = thread.run_streamed("Implement the fix.")
for event in stream:
print(event)
```

## Structured Output

```python
from codex import Codex, TurnOptions

schema = {
"type": "object",
"properties": {
"summary": {"type": "string"},
"status": {"type": "string", "enum": ["ok", "action_required"]},
},
"required": ["summary", "status"],
"additionalProperties": False,
}

thread = Codex().start_thread()
turn = thread.run("Summarize repository status", TurnOptions(output_schema=schema))
print(turn.final_response)
```

### Structured output with Pydantic (optional)

If you use [Pydantic](https://docs.pydantic.dev/latest/) v2, you can pass a model class or instance directly. The SDK converts it to JSON Schema automatically:

```python
from pydantic import BaseModel
from codex import Codex, TurnOptions


class StatusReport(BaseModel):
summary: str
status: str


thread = Codex().start_thread()
turn = thread.run(
"Summarize repository status",
TurnOptions(output_schema=StatusReport),
)
print(turn.final_response)
```

## Development

- Install dependencies with `uv sync --extra dev`.
- Run formatting and linting: `uv run ruff check .` and `uv run ruff format .`.
- Type-check with `uv run mypy --config-file pyproject.toml src/codex`.
- Tests via `uv run pytest`.

### Bundling native binaries

The SDK shells out to the Rust `codex` executable. For local testing we point at
`codex-rs/target/debug/codex`, but release builds should bundle the official
artifacts in `src/codex/vendor/` just like the TypeScript SDK. Use the helper
script to fetch prebuilt binaries from the Rust release workflow:

```bash
uv run python sdk/python/scripts/install_native_deps.py --clean --workflow-url <workflow-url>
```

Omit `--workflow-url` to use the default pinned run. After bundling, build the
wheel/sdist with `uv build` (or `python -m build`). The `vendor/` directory is
ignored by git aside from its README, so remember to run the script before
cutting a release.

25 changes: 25 additions & 0 deletions sdk/python/examples/basic_streaming.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from __future__ import annotations

from codex import Codex, ItemCompletedEvent, TurnCompletedEvent


def main() -> None:
client = Codex()
thread = client.start_thread()

stream = thread.run_streamed("Summarize repository health")
for event in stream:
match event:
case ItemCompletedEvent(item=item):
print(f"item[{item.type}]: {item}")
case TurnCompletedEvent(usage=usage):
print(
"usage: input=%s cached=%s output=%s"
% (usage.input_tokens, usage.cached_input_tokens, usage.output_tokens)
)
case _:
print(event)


if __name__ == "__main__":
main()
24 changes: 24 additions & 0 deletions sdk/python/examples/structured_output.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from __future__ import annotations

from codex import Codex, TurnOptions


SCHEMA = {
"type": "object",
"properties": {
"summary": {"type": "string"},
"status": {"type": "string", "enum": ["ok", "action_required"]},
},
"required": ["summary", "status"],
"additionalProperties": False,
}


def main() -> None:
thread = Codex().start_thread()
turn = thread.run("Summarize repository status", TurnOptions(output_schema=SCHEMA))
print(turn.final_response)


if __name__ == "__main__":
main()
49 changes: 49 additions & 0 deletions sdk/python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "openai-codex-sdk"
version = "0.0.1a0"
description = "Python SDK for Codex APIs."
readme = "README.md"
requires-python = ">=3.12"
license = { text = "Apache-2.0" }
authors = [{ name = "OpenAI" }]
keywords = ["codex", "sdk", "agents", "cli"]
classifiers = [
"Development Status :: 2 - Pre-Alpha",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.12",
"Typing :: Typed",
]
dependencies = []

[project.optional-dependencies]
dev = ["pytest>=8.4", "mypy>=1.18", "ruff>=0.5", "pydantic>=2.7"]

[project.urls]
Homepage = "https://github.com/openai/codex"
Repository = "https://github.com/openai/codex/tree/main/sdk/python"

[tool.hatch.metadata]
allow-direct-references = true

[tool.hatch.build]
packages = ["src/codex"]
include = ["examples", "src/codex/vendor", "src/codex/py.typed"]

[tool.pytest.ini_options]
pythonpath = ["src"]
filterwarnings = ["error"]

[tool.ruff]
line-length = 100
target-version = "py312"

[tool.mypy]
python_version = "3.12"
files = "src/codex"
strict = true
79 changes: 79 additions & 0 deletions sdk/python/scripts/install_native_deps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""Install Codex native binaries for the Python SDK."""

from __future__ import annotations

import argparse
import shutil
import subprocess
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parents[2]
INSTALL_NATIVE_DEPS = REPO_ROOT / "codex-cli" / "scripts" / "install_native_deps.py"
PYTHON_SDK_ROOT = REPO_ROOT / "sdk" / "python"
PACKAGE_ROOT = PYTHON_SDK_ROOT / "src" / "codex"
VENDOR_DIR = PACKAGE_ROOT / "vendor"


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--workflow-url",
help=(
"GitHub Actions workflow URL containing the prebuilt Codex binaries. "
"If omitted, the default from install_native_deps.py is used."
),
)
parser.add_argument(
"--component",
dest="components",
action="append",
default=["codex"],
choices=("codex", "rg", "codex-responses-api-proxy"),
help="Native component(s) to install (default: codex).",
)
parser.add_argument(
"--clean",
action="store_true",
help="Remove the existing vendor directory before installing binaries.",
)
return parser.parse_args()


def ensure_install_script() -> None:
if not INSTALL_NATIVE_DEPS.exists():
raise FileNotFoundError(f"install_native_deps.py not found at {INSTALL_NATIVE_DEPS}")


def run_install(workflow_url: str | None, components: list[str]) -> None:
cmd = [str(INSTALL_NATIVE_DEPS)]

if workflow_url:
cmd.extend(["--workflow-url", workflow_url])

for component in components:
cmd.extend(["--component", component])

cmd.append(str(PACKAGE_ROOT))

subprocess.run(cmd, check=True, cwd=REPO_ROOT)


def clean_vendor() -> None:
if VENDOR_DIR.exists():
shutil.rmtree(VENDOR_DIR)


def main() -> int:
args = parse_args()
ensure_install_script()

if args.clean:
clean_vendor()

run_install(args.workflow_url, args.components)
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading