[codex] Prepare Python SDK beta documentation and package metadata (#24836)

## Why

The initial public `openai-codex` beta should read and install like a
normal published Python package before a release tag is created. This
follows merged PR #24828, which establishes the independent SDK beta
release plumbing and exact runtime dependency.

## What changed

- Rewrote `sdk/python/README.md` as a compact PyPI-facing beta package
page: published installation, one quickstart, short login examples,
built-in help, and links to deeper guides.
- Updated the getting-started guide, API reference, FAQ, and examples
index to present the published beta consistently without repeating
onboarding in the package landing page or reference page.
- Made `pip install openai-codex` the primary install path while beta
releases are the only published SDK releases, with `--pre` documented
for opting into prereleases after a stable release exists.
- Added curated `help()` / `pydoc` docstrings across the public API and
generated public convenience methods through
`scripts/update_sdk_artifacts.py`.
- Declared the repository `Apache-2.0` license expression and
Documentation URL in package metadata, without introducing a duplicated
SDK-local license file.
- Kept the source distribution focused on installable package material
(`src/openai_codex`, `README.md`, and `pyproject.toml`); the repository
docs and runnable examples remain linked from the PyPI README.
- Built release artifacts in an Alpine container on the Ubuntu runner,
matching Python SDK CI and allowing type generation to install the
published `musllinux` runtime wheel.
- Added `twine check --strict` to the release workflow so malformed PyPI
metadata or rendered README content fails before publishing.
- Added focused SDK assertions for beta metadata, the exact runtime pin,
source distribution contents, and the built-in Python documentation
surface.

## Validation

- Ran `uv run --frozen --extra dev ruff check
scripts/update_sdk_artifacts.py src/openai_codex
tests/test_public_api_signatures.py
tests/test_artifact_workflow_and_binaries.py` before the final
README-only reductions and review-fix follow-ups.
- Built `openai_codex-0.1.0b1-py3-none-any.whl` and
`openai_codex-0.1.0b1.tar.gz` before the final README-only reductions
and review-fix follow-ups.
- Ran `python -m twine check --strict` on both built artifacts before
the final README-only reductions and review-fix follow-ups.
- Verified artifact metadata reports `Apache-2.0` without a duplicated
SDK-local license file.
- Verified `inspect.getdoc(...)` resolves documentation for the package,
`Codex`, `CodexConfig`, and key generated thread methods.
- Rebased the documentation/readiness change onto merged PR #24828
without changing the intended SDK or workflow file contents.
- Final verification is delegated to online CI for this PR.
This commit is contained in:
Ahmed Ibrahim
2026-05-27 18:29:05 -07:00
committed by GitHub
parent 4d0c4cd058
commit eb1cc3824c
16 changed files with 365 additions and 239 deletions

View File

@@ -28,9 +28,6 @@ jobs:
with:
python-version: "3.12"
- name: Install packaging tools
run: python -m pip install build uv==0.11.3
- name: Validate tag and build Python SDK package
shell: bash
run: |
@@ -50,17 +47,33 @@ jobs:
exit 1
fi
cd sdk/python
uv sync --extra dev --frozen
uv run --extra dev --frozen python scripts/update_sdk_artifacts.py \
stage-sdk "${RUNNER_TEMP}/openai-codex" \
--sdk-version "${sdk_version}"
python -m build \
--wheel \
--sdist \
--outdir "${GITHUB_WORKSPACE}/dist/python-sdk" \
"${RUNNER_TEMP}/openai-codex"
# The pinned runtime currently publishes a musllinux Linux wheel.
# Build in Alpine so release type generation installs that wheel.
docker run --rm \
--user "$(id -u):$(id -g)" \
-e HOME=/tmp/codex-python-sdk-home \
-e UV_LINK_MODE=copy \
-e SDK_VERSION="${sdk_version}" \
-e SDK_STAGE_DIR="${RUNNER_TEMP}/openai-codex" \
-e SDK_DIST_DIR="${GITHUB_WORKSPACE}/dist/python-sdk" \
-v "${GITHUB_WORKSPACE}:${GITHUB_WORKSPACE}" \
-v "${RUNNER_TEMP}:${RUNNER_TEMP}" \
-w "${GITHUB_WORKSPACE}/sdk/python" \
python:3.12-alpine \
sh -euxc '
python -m venv /tmp/release-tools
/tmp/release-tools/bin/python -m pip install build twine uv==0.11.3
/tmp/release-tools/bin/uv sync --extra dev --frozen
/tmp/release-tools/bin/uv run --extra dev --frozen python scripts/update_sdk_artifacts.py \
stage-sdk "${SDK_STAGE_DIR}" \
--sdk-version "${SDK_VERSION}"
/tmp/release-tools/bin/python -m build \
--wheel \
--sdist \
--outdir "${SDK_DIST_DIR}" \
"${SDK_STAGE_DIR}"
/tmp/release-tools/bin/python -m twine check --strict "${SDK_DIST_DIR}/"*
'
- name: Upload Python SDK package
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0

View File

@@ -1,132 +1,89 @@
# OpenAI Codex Python SDK (Experimental)
# OpenAI Codex Python SDK (Beta)
Experimental Python SDK for `codex app-server` JSON-RPC v2 over stdio, with a small default surface optimized for real scripts and apps.
Build Python applications that start Codex threads, run turns, stream progress,
and control workspace access.
The generated wire-model layer is sourced from the pinned `openai-codex-cli-bin`
runtime package and exposed as Pydantic models with snake_case Python fields
that serialize back to the protocol's camelCase wire format.
The package root exports the ergonomic client API; public Codex protocol value and
event types live in `openai_codex.types`.
> [!NOTE]
> `openai-codex` is in beta. Public APIs may change before `1.0`.
## Install
Install the SDK:
```bash
cd sdk/python
uv sync
source .venv/bin/activate
pip install openai-codex
```
Published SDK builds pin an exact compatible `openai-codex-cli-bin` runtime
dependency. Pass `CodexConfig(codex_bin=...)` only
when you intentionally want to run against a specific local app-server binary.
For reproducible environments, install this release exactly:
```bash
pip install openai-codex==0.1.0b1
```
The SDK requires Python `>=3.10` and installs its compatible Codex runtime
dependency automatically. While beta releases are the only published SDK
releases, the normal install command selects the latest beta. After a stable
release exists, use `pip install --pre openai-codex` to explicitly select a
newer prerelease.
## Quickstart
```python
from openai_codex import Codex, Sandbox
with Codex() as codex:
# Call login_api_key(...) first when this Codex session is not
# already authenticated.
thread = codex.thread_start(model="gpt-5", sandbox=Sandbox.workspace_write)
result = thread.run("Say hello in one sentence.")
print(result.final_response)
print(len(result.items))
```
`thread.run(...)` and `thread.turn(...).run()` return `TurnResult`. Its
`final_response` is `None` when the turn completes without a final-answer or
phase-less assistant message item.
## Sandbox
Use the same enum when creating a thread or changing its sandbox for a turn:
```python
from openai_codex import Codex, Sandbox
with Codex() as codex:
thread = codex.thread_start(sandbox=Sandbox.workspace_write)
thread.run("Make the requested change.")
review = thread.run("Review the diff only.", sandbox=Sandbox.read_only)
```
Available presets:
- `Sandbox.read_only`: read files without allowing writes.
- `Sandbox.workspace_write`: the normal default for projects with a recorded trust decision; read files and write inside the workspace and configured writable roots.
- `Sandbox.full_access`: run without filesystem access restrictions.
When `sandbox=` is omitted, Codex uses its configured default. A sandbox
passed to `run(...)` or `turn(...)` applies to that turn and subsequent turns
on the thread.
## Login
Use the auth helper that matches your app:
The SDK reuses your existing Codex authentication when one is already
available:
```python
from openai_codex import Codex
with Codex() as codex:
codex.login_api_key("sk-...")
account = codex.account()
print(account.account)
thread = codex.thread_start()
result = thread.run("Explain this repository in three bullets.")
print(result.final_response)
```
Interactive ChatGPT login returns a handle. Open the provided URL or device-code
page, then wait for the matching completion event:
`thread.run(...)` returns a `TurnResult` containing the final response,
collected items, and token usage.
## Authentication
Existing Codex authentication is reused automatically. To start ChatGPT
browser login explicitly:
```python
from openai_codex import Codex
with Codex() as codex:
login = codex.login_chatgpt()
print(login.auth_url)
completed = login.wait()
print(completed.success)
print(login.wait().success)
```
Use `login_chatgpt_device_code()` for device-code auth, `handle.cancel()` to
stop an in-progress interactive login, and `logout()` to clear the active
Codex account session.
For device-code login:
## Docs map
- Golden path tutorial: `docs/getting-started.md`
- API reference (signatures + behavior): `docs/api-reference.md`
- Common decisions and pitfalls: `docs/faq.md`
- Runnable examples index: `examples/README.md`
- Jupyter walkthrough notebook: `notebooks/sdk_walkthrough.ipynb`
## Examples
Start here:
```bash
cd sdk/python
python examples/01_quickstart_constructor/sync.py
python examples/01_quickstart_constructor/async.py
```python
with Codex() as codex:
login = codex.login_chatgpt_device_code()
print(login.verification_url, login.user_code)
login.wait()
```
## Runtime
For API-key login:
Published SDK builds are pinned to an exact `openai-codex-cli-bin` package
version, and that runtime package carries the platform-specific binary for the
target wheel. SDK beta releases are versioned independently of runtime releases.
```python
with Codex() as codex:
codex.login_api_key("sk-...")
```
## Compatibility and versioning
## Built-In Help
- Package: `openai-codex`
- Runtime package: `openai-codex-cli-bin`
- Python: `>=3.10`
- Target protocol: Codex `app-server` JSON-RPC v2
- Versioning rule: SDK releases pin one exact compatible Codex runtime version
Use Python's standard `help(openai_codex)`, `help(Codex)`, or
`python -m pydoc openai_codex` documentation tools.
## Notes
## Documentation
- `Codex()` is eager and performs startup + `initialize` in the constructor.
- Use context managers (`with Codex() as codex:`) to ensure shutdown.
- Plain strings are accepted anywhere a turn input is accepted; they are
shorthand for `TextInput(...)`.
- Prefer `thread.run("...")` for the common case. Use `thread.turn(...)` when
you need streaming, steering, or interrupt control.
- For transient overload, use `retry_on_overload` from the package root.
- [Getting started](https://github.com/openai/codex/blob/main/sdk/python/docs/getting-started.md)
- [API reference](https://github.com/openai/codex/blob/main/sdk/python/docs/api-reference.md)
- [FAQ](https://github.com/openai/codex/blob/main/sdk/python/docs/faq.md)
- [Examples](https://github.com/openai/codex/blob/main/sdk/python/examples/README.md)
The package is licensed under the
[repository Apache License 2.0](https://github.com/openai/codex/blob/main/LICENSE).

View File

@@ -1,8 +1,8 @@
# OpenAI Codex SDK — API Reference
# OpenAI Codex Python SDK (Beta) - API Reference
Public surface of `openai_codex` for Codex workflows.
This SDK surface is experimental. Turn streams are routed by turn ID so one client can consume multiple active turns concurrently.
This SDK is in beta. Public APIs may change before `1.0`. Turn streams are routed by turn ID so one client can consume multiple active turns concurrently.
Thread starts default to `ApprovalMode.auto_review`; turn starts accept an optional `approval_mode` override.
## Package Entry

View File

@@ -1,5 +1,18 @@
# FAQ
## Is the Python SDK stable?
`openai-codex` is a public beta. Install it with
`pip install openai-codex`; public APIs may change before `1.0`. While beta
releases are the only published SDK releases, pip selects the latest beta.
After a stable release exists, pass `--pre` to opt into newer prereleases.
## Why does the SDK install a runtime package?
The SDK and runtime packages are versioned independently. Each SDK release
pins one compatible runtime dependency, so `openai-codex==0.1.0b1` installs
`openai-codex-cli-bin==0.132.0` automatically.
## Thread vs turn
- A `Thread` is conversation state.
@@ -84,9 +97,9 @@ This avoids duplicate ways to do the same operation and keeps behavior explicit.
Common causes:
- published runtime package (`openai-codex-cli-bin`) is not installed
- installation is incomplete and the pinned `openai-codex-cli-bin` dependency is missing
- local `codex_bin` override points to a missing file
- installed Codex runtime version older than the SDK schema
- a custom local Codex executable does not support the SDK operation being used
## Why does a turn "hang"?
@@ -99,7 +112,8 @@ A turn is complete only when `turn/completed` arrives for that turn ID.
Use `retry_on_overload(...)` for transient overload failures (`ServerBusyError`).
Do not blindly retry all errors. For `InvalidParamsError` or `MethodNotFoundError`, fix inputs or update the runtime/schema version instead.
Do not blindly retry all errors. For `InvalidParamsError` or
`MethodNotFoundError`, fix the input or use the runtime pinned by the SDK.
## Common pitfalls

View File

@@ -1,68 +1,70 @@
# Getting Started
This is the fastest path from install to a multi-turn thread using the public SDK surface.
This guide gets a published OpenAI Codex Python SDK beta installation running
with a multi-turn thread.
The SDK is experimental, so the public API and runtime requirements may keep evolving before the first public release.
## 1. Install
## 1) Install
From repo root:
Install the SDK:
```bash
cd sdk/python
uv sync
source .venv/bin/activate
pip install openai-codex
```
For a reproducible install of this release:
```bash
pip install openai-codex==0.1.0b1
```
Requirements:
- Python `>=3.10`
- uv
- installed `openai-codex-cli-bin` runtime package, or an explicit `codex_bin` override
- An existing Codex account session, or one of the login flows below
## 2) Authenticate when needed
The SDK installs its compatible `openai-codex-cli-bin` runtime dependency
automatically. While beta releases are the only published SDK releases, this
normal install command selects the latest beta. After a stable release exists,
use `pip install --pre openai-codex` to opt into a newer prerelease.
Existing Codex auth state is reused automatically. To authenticate from the SDK,
use the flow that fits your app:
## 2. Authenticate When Needed
```python
from openai_codex import Codex, Sandbox
with Codex() as codex:
codex.login_api_key("sk-...")
account = codex.account()
print(account.account)
```
Interactive ChatGPT browser login returns a handle that carries the URL and the
matching completion event:
```python
with Codex() as codex:
login = codex.login_chatgpt()
print(login.auth_url)
completed = login.wait()
print(completed.success)
```
Device-code login works the same way with
`login_chatgpt_device_code()`, which exposes `verification_url`, `user_code`,
and `wait()`.
## 3) Run your first turn (sync)
Existing Codex authentication is reused automatically. For ChatGPT browser
login:
```python
from openai_codex import Codex
with Codex() as codex:
server = codex.metadata.serverInfo
print("Server:", None if server is None else server.name, None if server is None else server.version)
login = codex.login_chatgpt()
print(login.auth_url)
print(login.wait().success)
```
thread = codex.thread_start(
model="gpt-5.4",
config={"model_reasoning_effort": "high"},
sandbox=Sandbox.workspace_write,
)
For device-code login:
```python
with Codex() as codex:
login = codex.login_chatgpt_device_code()
print(login.verification_url, login.user_code)
print(login.wait().success)
```
For API-key login:
```python
with Codex() as codex:
codex.login_api_key("sk-...")
print(codex.account().account)
```
## 3. Run A Turn
```python
from openai_codex import Codex, Sandbox
with Codex() as codex:
thread = codex.thread_start(sandbox=Sandbox.workspace_write)
result = thread.run("Say hello in one sentence.")
print("Thread:", thread.id)
@@ -70,19 +72,15 @@ with Codex() as codex:
print("Items:", len(result.items))
```
What happened:
`Thread.run(...)` starts a turn, waits for completion, and returns
`TurnResult`. Plain strings are shorthand for `TextInput(...)`.
- `Codex()` started and initialized `codex app-server`.
- `thread_start(...)` created a thread.
- `thread.run("...")` started a turn, consumed events until completion, and returned `TurnResult` with turn metadata, final assistant response, collected items, and usage.
- `result.final_response` is `None` when no final-answer or phase-less assistant message item completes for the turn.
- plain strings are accepted anywhere a turn input is accepted; typed inputs are still available for multimodal and structured cases
- use `thread.turn(...)` when you need a `TurnHandle` for streaming, steering, or interrupting before collecting `TurnResult`
- one client can consume multiple active turns concurrently; turn streams are routed by turn ID
Use `Thread.turn(...)` when you need a `TurnHandle` for streaming, steering,
or interrupting an active turn.
## 4) Change sandbox access
## 4. Choose Sandbox Access
Use one enum for the initial sandbox and for later turn overrides:
Use one enum for the initial thread and later turn overrides:
```python
from openai_codex import Codex, Sandbox
@@ -96,40 +94,44 @@ with Codex() as codex:
Available presets:
- `Sandbox.read_only`: read files without allowing writes.
- `Sandbox.workspace_write`: the normal default for projects with a recorded trust decision; read files and write inside the workspace and configured writable roots.
- `Sandbox.workspace_write`: read files and write inside the workspace and
configured writable roots; this is the normal default for workspace work.
- `Sandbox.full_access`: run without filesystem access restrictions.
When `sandbox=` is omitted, Codex uses its configured default. A turn
override also becomes the sandbox for subsequent turns on that thread.
When `sandbox=` is omitted, Codex uses its configured default. A turn override
also applies to subsequent turns on that thread.
## 5) Continue the same thread (multi-turn)
## 5. Continue A Thread
```python
from openai_codex import Codex
with Codex() as codex:
thread = codex.thread_start(model="gpt-5.4", config={"model_reasoning_effort": "high"})
first = thread.run("Summarize Rust ownership in 2 bullets.")
second = thread.run("Now explain it to a Python developer.")
print("first:", first.final_response)
print("second:", second.final_response)
thread = codex.thread_start()
thread.run("Summarize Rust ownership in two bullets.")
result = thread.run("Now explain it to a Python developer.")
print(result.final_response)
```
## 6) Async parity
To resume a stored thread later:
Use `async with AsyncCodex()` as the normal async entrypoint. `AsyncCodex`
initializes lazily, and context entry makes startup/shutdown explicit.
```python
with Codex() as codex:
thread = codex.thread_resume("thr_123")
print(thread.run("Continue where we left off.").final_response)
```
## 6. Use The Async Client
```python
import asyncio
from openai_codex import AsyncCodex
from openai_codex import AsyncCodex, Sandbox
async def main() -> None:
async with AsyncCodex() as codex:
thread = await codex.thread_start(model="gpt-5.4", config={"model_reasoning_effort": "high"})
thread = await codex.thread_start(sandbox=Sandbox.workspace_write)
result = await thread.run("Continue where we left off.")
print(result.final_response)
@@ -137,30 +139,36 @@ async def main() -> None:
asyncio.run(main())
```
## 7) Resume an existing thread
## 7. Get Help
Python's built-in documentation tools cover the curated SDK surface:
```python
from openai_codex import Codex
import openai_codex
from openai_codex import Codex, CodexConfig
THREAD_ID = "thr_123" # replace with a real id
with Codex() as codex:
thread = codex.thread_resume(THREAD_ID)
result = thread.run("Continue where we left off.")
print(result.final_response)
help(openai_codex)
help(Codex)
help(CodexConfig)
```
## 8) Public Codex protocol types
The convenience wrappers live at the package root. Public Codex protocol value and
event types live under:
```python
from openai_codex.types import ThreadReadResponse, Turn, TurnStatus
```bash
python -m pydoc openai_codex
```
## 9) Next stops
## Developing From This Repository
- API surface and signatures: `docs/api-reference.md`
- Common decisions/pitfalls: `docs/faq.md`
- End-to-end runnable examples: `examples/README.md`
Contributors working from a checkout can install development dependencies from
the repository:
```bash
cd sdk/python
uv sync --extra dev
source .venv/bin/activate
```
## Next Stops
- [API reference](https://github.com/openai/codex/blob/main/sdk/python/docs/api-reference.md)
- [FAQ](https://github.com/openai/codex/blob/main/sdk/python/docs/faq.md)
- [Runnable examples](https://github.com/openai/codex/blob/main/sdk/python/examples/README.md)

View File

@@ -14,25 +14,30 @@ multimodal or structured input lists.
## Prerequisites
- Python `>=3.10`
- Install SDK dependencies for the same Python interpreter you will use to run examples
- Install the SDK for the same Python interpreter you will use to run examples
Recommended setup (from `sdk/python`):
Install the published beta:
```bash
uv sync
python -m pip install openai-codex
```
The SDK installs its pinned `openai-codex-cli-bin` runtime dependency.
The pinned runtime version comes from the SDK package dependency.
## Run From A Checkout
Contributors using these checked-in scripts should install development
dependencies from `sdk/python`:
```bash
uv sync --extra dev
source .venv/bin/activate
```
When running examples from this repo checkout, the SDK source uses the local
tree and does not bundle a runtime binary. The helper in `examples/_bootstrap.py`
uses the installed `openai-codex-cli-bin` runtime package.
If the pinned `openai-codex-cli-bin` runtime is not already installed, the bootstrap
will download the matching GitHub release artifact, stage a temporary local
`openai-codex-cli-bin` package, install it into your active interpreter, and clean up
the temporary files afterward.
The pinned runtime version comes from the SDK package dependency.
The examples bootstrap local SDK imports from `sdk/python/src`. If the pinned
runtime is not already installed, the bootstrap installs the matching runtime
package for the active interpreter and cleans up temporary files afterward.
## Run examples
@@ -43,10 +48,7 @@ python examples/<example-folder>/sync.py
python examples/<example-folder>/async.py
```
The examples bootstrap local imports from `sdk/python/src` automatically, so no
SDK wheel install is required. You only need the Python dependencies for your
active interpreter and an installed `openai-codex-cli-bin` runtime package (either
already present or automatically provisioned by the bootstrap).
The checked-in examples use the local SDK source tree automatically.
## Recommended first run

View File

@@ -1,20 +1,19 @@
[build-system]
requires = ["hatchling>=1.24.0"]
requires = ["hatchling>=1.27.0"]
build-backend = "hatchling.build"
[project]
name = "openai-codex"
version = "0.1.0b1"
description = "Python SDK for Codex app-server v2"
description = "Python SDK for Codex"
readme = "README.md"
requires-python = ">=3.10"
license = { text = "Apache-2.0" }
license = "Apache-2.0"
authors = [{ name = "OpenAI" }]
keywords = ["codex", "json-rpc", "sdk", "llm", "app-server"]
keywords = ["codex", "sdk", "llm", "ai", "agents"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
@@ -28,6 +27,7 @@ dependencies = ["pydantic>=2.12", "openai-codex-cli-bin==0.132.0"]
Homepage = "https://github.com/openai/codex"
Repository = "https://github.com/openai/codex"
Issues = "https://github.com/openai/codex/issues"
Documentation = "https://github.com/openai/codex/tree/main/sdk/python/docs"
[project.optional-dependencies]
dev = ["pytest>=8.0", "datamodel-code-generator==0.31.2", "ruff>=0.15.8"]
@@ -51,9 +51,6 @@ include = [
include = [
"src/openai_codex/**",
"README.md",
"CHANGELOG.md",
"CONTRIBUTING.md",
"RELEASE_CHECKLIST.md",
"pyproject.toml",
]

View File

@@ -918,6 +918,7 @@ def _render_codex_block(
*_approval_mode_start_signature_lines(),
*_kw_signature_lines(thread_start_fields),
" ) -> Thread:",
' """Create a new Codex conversation thread."""',
_approval_mode_assignment_line("_approval_mode_settings"),
" params = ThreadStartParams(",
*_approval_mode_model_arg_lines(),
@@ -931,6 +932,7 @@ def _render_codex_block(
" *,",
*_kw_signature_lines(thread_list_fields),
" ) -> ThreadListResponse:",
' """List saved conversation threads."""',
" params = ThreadListParams(",
*_model_arg_lines(thread_list_fields),
" )",
@@ -943,6 +945,7 @@ def _render_codex_block(
*_approval_mode_override_signature_lines(),
*_kw_signature_lines(resume_fields),
" ) -> Thread:",
' """Resume an existing conversation thread by ID."""',
_approval_mode_assignment_line("_approval_mode_override_settings"),
" params = ThreadResumeParams(",
" thread_id=thread_id,",
@@ -959,6 +962,7 @@ def _render_codex_block(
*_approval_mode_override_signature_lines(),
*_kw_signature_lines(fork_fields),
" ) -> Thread:",
' """Create a new thread from an existing thread."""',
_approval_mode_assignment_line("_approval_mode_override_settings"),
" params = ThreadForkParams(",
" thread_id=thread_id,",
@@ -969,9 +973,11 @@ def _render_codex_block(
" return Thread(self._client, forked.thread.id)",
"",
" def thread_archive(self, thread_id: str) -> ThreadArchiveResponse:",
' """Archive a stored conversation thread."""',
" return self._client.thread_archive(thread_id)",
"",
" def thread_unarchive(self, thread_id: str) -> Thread:",
' """Restore an archived conversation thread."""',
" unarchived = self._client.thread_unarchive(thread_id)",
" return Thread(self._client, unarchived.thread.id)",
]
@@ -991,6 +997,7 @@ def _render_async_codex_block(
*_approval_mode_start_signature_lines(),
*_kw_signature_lines(thread_start_fields),
" ) -> AsyncThread:",
' """Create a new Codex conversation thread."""',
" await self._ensure_initialized()",
_approval_mode_assignment_line("_approval_mode_settings"),
" params = ThreadStartParams(",
@@ -1005,6 +1012,7 @@ def _render_async_codex_block(
" *,",
*_kw_signature_lines(thread_list_fields),
" ) -> ThreadListResponse:",
' """List saved conversation threads."""',
" await self._ensure_initialized()",
" params = ThreadListParams(",
*_model_arg_lines(thread_list_fields),
@@ -1018,6 +1026,7 @@ def _render_async_codex_block(
*_approval_mode_override_signature_lines(),
*_kw_signature_lines(resume_fields),
" ) -> AsyncThread:",
' """Resume an existing conversation thread by ID."""',
" await self._ensure_initialized()",
_approval_mode_assignment_line("_approval_mode_override_settings"),
" params = ThreadResumeParams(",
@@ -1035,6 +1044,7 @@ def _render_async_codex_block(
*_approval_mode_override_signature_lines(),
*_kw_signature_lines(fork_fields),
" ) -> AsyncThread:",
' """Create a new thread from an existing thread."""',
" await self._ensure_initialized()",
_approval_mode_assignment_line("_approval_mode_override_settings"),
" params = ThreadForkParams(",
@@ -1046,10 +1056,12 @@ def _render_async_codex_block(
" return AsyncThread(self, forked.thread.id)",
"",
" async def thread_archive(self, thread_id: str) -> ThreadArchiveResponse:",
' """Archive a stored conversation thread."""',
" await self._ensure_initialized()",
" return await self._client.thread_archive(thread_id)",
"",
" async def thread_unarchive(self, thread_id: str) -> AsyncThread:",
' """Restore an archived conversation thread."""',
" await self._ensure_initialized()",
" unarchived = await self._client.thread_unarchive(thread_id)",
" return AsyncThread(self, unarchived.thread.id)",
@@ -1068,6 +1080,7 @@ def _render_thread_block(
*_approval_mode_override_signature_lines(),
*_kw_signature_lines(turn_fields),
" ) -> TurnHandle:",
' """Start a turn and return a handle for streaming or control."""',
" wire_input = _to_wire_input(_normalize_run_input(input))",
_approval_mode_assignment_line("_approval_mode_override_settings"),
" params = TurnStartParams(",
@@ -1093,6 +1106,7 @@ def _render_async_thread_block(
*_approval_mode_override_signature_lines(),
*_kw_signature_lines(turn_fields),
" ) -> AsyncTurnHandle:",
' """Start a turn and return a handle for streaming or control."""',
" await self._codex._ensure_initialized()",
" wire_input = _to_wire_input(_normalize_run_input(input))",
_approval_mode_assignment_line("_approval_mode_override_settings"),

View File

@@ -1,3 +1,17 @@
"""Python SDK for running Codex workflows.
Start with :class:`Codex` for synchronous applications or
:class:`AsyncCodex` for async applications. Most programs create a thread and
run a turn::
from openai_codex import Codex, Sandbox
with Codex() as codex:
thread = codex.thread_start(sandbox=Sandbox.workspace_write)
result = thread.run("Describe this project.")
print(result.final_response)
"""
from ._version import __version__
from .api import (
ApprovalMode,

View File

@@ -7,27 +7,37 @@ from .models import JsonObject
@dataclass(slots=True)
class TextInput:
"""Text supplied to a turn or steering request."""
text: str
@dataclass(slots=True)
class ImageInput:
"""Remote image URL supplied as turn input."""
url: str
@dataclass(slots=True)
class LocalImageInput:
"""Local image path supplied as turn input."""
path: str
@dataclass(slots=True)
class SkillInput:
"""Named skill reference supplied as turn input."""
name: str
path: str
@dataclass(slots=True)
class MentionInput:
"""Named resource mention supplied as turn input."""
name: str
path: str

View File

@@ -20,6 +20,8 @@ from .models import Notification
@dataclass(slots=True)
class TurnResult:
"""Collected result returned after a turn completes."""
id: str
status: TurnStatus
error: TurnError | None

View File

@@ -73,7 +73,11 @@ from .models import InitializeResponse, JsonObject, Notification
class Codex:
"""Typed Python client for Codex workflows."""
"""Synchronous client for creating threads and running Codex turns.
The client starts its runtime connection during construction. Use it as a
context manager so resources are closed promptly.
"""
def __init__(self, config: CodexConfig | None = None) -> None:
self._client = CodexClient(config=config)
@@ -143,6 +147,7 @@ class Codex:
session_start_source: ThreadStartSource | None = None,
thread_source: ThreadSource | None = None,
) -> Thread:
"""Create a new Codex conversation thread."""
approval_policy, approvals_reviewer = _approval_mode_settings(approval_mode)
params = ThreadStartParams(
approval_policy=approval_policy,
@@ -178,6 +183,7 @@ class Codex:
source_kinds: list[ThreadSourceKind] | None = None,
use_state_db_only: bool | None = None,
) -> ThreadListResponse:
"""List saved conversation threads."""
params = ThreadListParams(
archived=archived,
cursor=cursor,
@@ -207,6 +213,7 @@ class Codex:
sandbox: Sandbox | None = None,
service_tier: str | None = None,
) -> Thread:
"""Resume an existing conversation thread by ID."""
approval_policy, approvals_reviewer = _approval_mode_override_settings(approval_mode)
params = ThreadResumeParams(
thread_id=thread_id,
@@ -241,6 +248,7 @@ class Codex:
service_tier: str | None = None,
thread_source: ThreadSource | None = None,
) -> Thread:
"""Create a new thread from an existing thread."""
approval_policy, approvals_reviewer = _approval_mode_override_settings(approval_mode)
params = ThreadForkParams(
thread_id=thread_id,
@@ -261,15 +269,18 @@ class Codex:
return Thread(self._client, forked.thread.id)
def thread_archive(self, thread_id: str) -> ThreadArchiveResponse:
"""Archive a stored conversation thread."""
return self._client.thread_archive(thread_id)
def thread_unarchive(self, thread_id: str) -> Thread:
"""Restore an archived conversation thread."""
unarchived = self._client.thread_unarchive(thread_id)
return Thread(self._client, unarchived.thread.id)
# END GENERATED: Codex.flat_methods
def models(self, *, include_hidden: bool = False) -> ModelListResponse:
"""List available models reported by Codex."""
return self._client.model_list(include_hidden=include_hidden)
@@ -376,6 +387,7 @@ class AsyncCodex:
session_start_source: ThreadStartSource | None = None,
thread_source: ThreadSource | None = None,
) -> AsyncThread:
"""Create a new Codex conversation thread."""
await self._ensure_initialized()
approval_policy, approvals_reviewer = _approval_mode_settings(approval_mode)
params = ThreadStartParams(
@@ -412,6 +424,7 @@ class AsyncCodex:
source_kinds: list[ThreadSourceKind] | None = None,
use_state_db_only: bool | None = None,
) -> ThreadListResponse:
"""List saved conversation threads."""
await self._ensure_initialized()
params = ThreadListParams(
archived=archived,
@@ -442,6 +455,7 @@ class AsyncCodex:
sandbox: Sandbox | None = None,
service_tier: str | None = None,
) -> AsyncThread:
"""Resume an existing conversation thread by ID."""
await self._ensure_initialized()
approval_policy, approvals_reviewer = _approval_mode_override_settings(approval_mode)
params = ThreadResumeParams(
@@ -477,6 +491,7 @@ class AsyncCodex:
service_tier: str | None = None,
thread_source: ThreadSource | None = None,
) -> AsyncThread:
"""Create a new thread from an existing thread."""
await self._ensure_initialized()
approval_policy, approvals_reviewer = _approval_mode_override_settings(approval_mode)
params = ThreadForkParams(
@@ -498,10 +513,12 @@ class AsyncCodex:
return AsyncThread(self, forked.thread.id)
async def thread_archive(self, thread_id: str) -> ThreadArchiveResponse:
"""Archive a stored conversation thread."""
await self._ensure_initialized()
return await self._client.thread_archive(thread_id)
async def thread_unarchive(self, thread_id: str) -> AsyncThread:
"""Restore an archived conversation thread."""
await self._ensure_initialized()
unarchived = await self._client.thread_unarchive(thread_id)
return AsyncThread(self, unarchived.thread.id)
@@ -515,6 +532,8 @@ class AsyncCodex:
@dataclass(slots=True)
class Thread:
"""Synchronous conversation thread used to run one or more turns."""
_client: CodexClient
id: str
@@ -532,6 +551,7 @@ class Thread:
service_tier: str | None = None,
summary: ReasoningSummary | None = None,
) -> TurnResult:
"""Run a complete turn and collect its final result."""
turn = self.turn(
input,
approval_mode=approval_mode,
@@ -565,6 +585,7 @@ class Thread:
service_tier: str | None = None,
summary: ReasoningSummary | None = None,
) -> TurnHandle:
"""Start a turn and return a handle for streaming or control."""
wire_input = _to_wire_input(_normalize_run_input(input))
approval_policy, approvals_reviewer = _approval_mode_override_settings(approval_mode)
params = TurnStartParams(
@@ -587,6 +608,7 @@ class Thread:
# END GENERATED: Thread.flat_methods
def read(self, *, include_turns: bool = False) -> ThreadReadResponse:
"""Read this thread, optionally including its turn history."""
return self._client.thread_read(self.id, include_turns=include_turns)
def set_name(self, name: str) -> ThreadSetNameResponse:
@@ -598,6 +620,8 @@ class Thread:
@dataclass(slots=True)
class AsyncThread:
"""Asynchronous conversation thread used to run one or more turns."""
_codex: AsyncCodex
id: str
@@ -615,6 +639,7 @@ class AsyncThread:
service_tier: str | None = None,
summary: ReasoningSummary | None = None,
) -> TurnResult:
"""Run a complete turn asynchronously and collect its final result."""
turn = await self.turn(
input,
approval_mode=approval_mode,
@@ -648,6 +673,7 @@ class AsyncThread:
service_tier: str | None = None,
summary: ReasoningSummary | None = None,
) -> AsyncTurnHandle:
"""Start a turn and return a handle for streaming or control."""
await self._codex._ensure_initialized()
wire_input = _to_wire_input(_normalize_run_input(input))
approval_policy, approvals_reviewer = _approval_mode_override_settings(approval_mode)
@@ -675,6 +701,7 @@ class AsyncThread:
# END GENERATED: AsyncThread.flat_methods
async def read(self, *, include_turns: bool = False) -> ThreadReadResponse:
"""Read this thread, optionally including its turn history."""
await self._codex._ensure_initialized()
return await self._codex._client.thread_read(self.id, include_turns=include_turns)
@@ -689,11 +716,14 @@ class AsyncThread:
@dataclass(slots=True)
class TurnHandle:
"""Control and consume a synchronous turn after it has started."""
_client: CodexClient
thread_id: str
id: str
def steer(self, input: RunInput) -> TurnSteerResponse:
"""Send additional input to this active turn."""
return self._client.turn_steer(
self.thread_id,
self.id,
@@ -701,6 +731,7 @@ class TurnHandle:
)
def interrupt(self) -> TurnInterruptResponse:
"""Request interruption of this active turn."""
return self._client.turn_interrupt(self.thread_id, self.id)
def stream(self) -> Iterator[Notification]:
@@ -720,6 +751,7 @@ class TurnHandle:
self._client.unregister_turn_notifications(self.id)
def run(self) -> TurnResult:
"""Consume the turn stream and return its completed result."""
stream = self.stream()
try:
return _collect_turn_result(stream, turn_id=self.id)
@@ -729,11 +761,14 @@ class TurnHandle:
@dataclass(slots=True)
class AsyncTurnHandle:
"""Control and consume an asynchronous turn after it has started."""
_codex: AsyncCodex
thread_id: str
id: str
async def steer(self, input: RunInput) -> TurnSteerResponse:
"""Send additional input to this active turn."""
await self._codex._ensure_initialized()
return await self._codex._client.turn_steer(
self.thread_id,
@@ -742,6 +777,7 @@ class AsyncTurnHandle:
)
async def interrupt(self) -> TurnInterruptResponse:
"""Request interruption of this active turn."""
await self._codex._ensure_initialized()
return await self._codex._client.turn_interrupt(self.thread_id, self.id)
@@ -763,6 +799,7 @@ class AsyncTurnHandle:
self._codex._client.unregister_turn_notifications(self.id)
async def run(self) -> TurnResult:
"""Consume the turn stream and return its completed result."""
stream = self.stream()
try:
return await _collect_async_turn_result(stream, turn_id=self.id)

View File

@@ -172,6 +172,12 @@ def _resolve_codex_bin(config: "CodexConfig") -> Path:
@dataclass(slots=True)
class CodexConfig:
"""Configuration for launching and identifying the local Codex runtime.
Most callers can use ``Codex()`` without configuration. Set ``codex_bin``
only when intentionally using a specific local Codex executable.
"""
codex_bin: str | None = None
launch_args_override: tuple[str, ...] | None = None
config_overrides: tuple[str, ...] = ()

View File

@@ -26,23 +26,23 @@ class CodexRpcError(JsonRpcError):
class ParseError(CodexRpcError):
pass
"""Raised when a request or response cannot be parsed."""
class InvalidRequestError(CodexRpcError):
pass
"""Raised when the runtime rejects the request shape."""
class MethodNotFoundError(CodexRpcError):
pass
"""Raised when the requested operation is unavailable."""
class InvalidParamsError(CodexRpcError):
pass
"""Raised when an operation receives invalid parameters."""
class InternalRpcError(CodexRpcError):
pass
"""Raised when the runtime reports an internal RPC failure."""
class ServerBusyError(CodexRpcError):

View File

@@ -258,6 +258,34 @@ def test_source_sdk_package_pins_published_runtime() -> None:
}
def test_source_sdk_package_declares_beta_documentation_and_release_files() -> None:
"""Public package metadata should link beta docs and ship package metadata."""
pyproject = tomllib.loads((ROOT / "pyproject.toml").read_text())
readme = (ROOT / "README.md").read_text()
assert {
"description": pyproject["project"]["description"],
"is_beta": "Development Status :: 4 - Beta" in pyproject["project"]["classifiers"],
"license": pyproject["project"]["license"],
"documentation": pyproject["project"]["urls"]["Documentation"],
"sdist_include": pyproject["tool"]["hatch"]["build"]["targets"]["sdist"]["include"],
"readme_is_beta": "# OpenAI Codex Python SDK (Beta)" in readme,
"local_license_file": (ROOT / "LICENSE").exists(),
} == {
"description": "Python SDK for Codex",
"is_beta": True,
"license": "Apache-2.0",
"documentation": "https://github.com/openai/codex/tree/main/sdk/python/docs",
"sdist_include": [
"src/openai_codex/**",
"README.md",
"pyproject.toml",
],
"readme_is_beta": True,
"local_license_file": False,
}
def test_release_metadata_retries_without_invalid_auth(
monkeypatch: pytest.MonkeyPatch,
) -> None:

View File

@@ -211,6 +211,30 @@ def test_package_and_default_client_versions_follow_project_version() -> None:
assert CodexConfig().client_version == openai_codex.__version__
def test_curated_public_api_has_builtin_help_documentation() -> None:
"""The package's normal ``help()`` surface should explain common first-use APIs."""
documented = {
"module": openai_codex,
"Codex": Codex,
"AsyncCodex": AsyncCodex,
"CodexConfig": CodexConfig,
"Thread": Thread,
"AsyncThread": AsyncThread,
"TurnHandle": TurnHandle,
"AsyncTurnHandle": AsyncTurnHandle,
"TurnResult": TurnResult,
"Sandbox": Sandbox,
"thread_start": Codex.thread_start,
"thread_resume": Codex.thread_resume,
"thread_run": Thread.run,
"thread_turn": Thread.turn,
}
assert {name: inspect.getdoc(value) is not None for name, value in documented.items()} == (
dict.fromkeys(documented, True)
)
def test_package_includes_py_typed_marker() -> None:
"""The wheel should advertise that inline type information is available."""
marker = resources.files("openai_codex").joinpath("py.typed")