From eb1cc3824ca69125113ebeeba6affe5946cf8072 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Wed, 27 May 2026 18:29:05 -0700 Subject: [PATCH] [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. --- .github/workflows/python-sdk-release.yml | 41 ++-- sdk/python/README.md | 155 ++++++--------- sdk/python/docs/api-reference.md | 4 +- sdk/python/docs/faq.md | 20 +- sdk/python/docs/getting-started.md | 188 +++++++++--------- sdk/python/examples/README.md | 36 ++-- sdk/python/pyproject.toml | 13 +- sdk/python/scripts/update_sdk_artifacts.py | 14 ++ sdk/python/src/openai_codex/__init__.py | 14 ++ sdk/python/src/openai_codex/_inputs.py | 10 + sdk/python/src/openai_codex/_run.py | 2 + sdk/python/src/openai_codex/api.py | 39 +++- sdk/python/src/openai_codex/client.py | 6 + sdk/python/src/openai_codex/errors.py | 10 +- .../test_artifact_workflow_and_binaries.py | 28 +++ .../tests/test_public_api_signatures.py | 24 +++ 16 files changed, 365 insertions(+), 239 deletions(-) diff --git a/.github/workflows/python-sdk-release.yml b/.github/workflows/python-sdk-release.yml index b831727e0a..ee0bc2ded6 100644 --- a/.github/workflows/python-sdk-release.yml +++ b/.github/workflows/python-sdk-release.yml @@ -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 diff --git a/sdk/python/README.md b/sdk/python/README.md index 457a20066a..38b472f33f 100644 --- a/sdk/python/README.md +++ b/sdk/python/README.md @@ -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). diff --git a/sdk/python/docs/api-reference.md b/sdk/python/docs/api-reference.md index f56234540b..f003a28511 100644 --- a/sdk/python/docs/api-reference.md +++ b/sdk/python/docs/api-reference.md @@ -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 diff --git a/sdk/python/docs/faq.md b/sdk/python/docs/faq.md index b0c496579b..e973292002 100644 --- a/sdk/python/docs/faq.md +++ b/sdk/python/docs/faq.md @@ -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 diff --git a/sdk/python/docs/getting-started.md b/sdk/python/docs/getting-started.md index 45271ed64d..ddb7432887 100644 --- a/sdk/python/docs/getting-started.md +++ b/sdk/python/docs/getting-started.md @@ -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) diff --git a/sdk/python/examples/README.md b/sdk/python/examples/README.md index eef3893721..8efeb75b14 100644 --- a/sdk/python/examples/README.md +++ b/sdk/python/examples/README.md @@ -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//sync.py python examples//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 diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml index 3b76e45526..6c16ae3026 100644 --- a/sdk/python/pyproject.toml +++ b/sdk/python/pyproject.toml @@ -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", ] diff --git a/sdk/python/scripts/update_sdk_artifacts.py b/sdk/python/scripts/update_sdk_artifacts.py index 077e44c46e..1742ced547 100755 --- a/sdk/python/scripts/update_sdk_artifacts.py +++ b/sdk/python/scripts/update_sdk_artifacts.py @@ -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"), diff --git a/sdk/python/src/openai_codex/__init__.py b/sdk/python/src/openai_codex/__init__.py index 3453388498..2829958dc1 100644 --- a/sdk/python/src/openai_codex/__init__.py +++ b/sdk/python/src/openai_codex/__init__.py @@ -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, diff --git a/sdk/python/src/openai_codex/_inputs.py b/sdk/python/src/openai_codex/_inputs.py index e3cd1c3969..a6e5e7b528 100644 --- a/sdk/python/src/openai_codex/_inputs.py +++ b/sdk/python/src/openai_codex/_inputs.py @@ -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 diff --git a/sdk/python/src/openai_codex/_run.py b/sdk/python/src/openai_codex/_run.py index f5c72d5109..8a66923704 100644 --- a/sdk/python/src/openai_codex/_run.py +++ b/sdk/python/src/openai_codex/_run.py @@ -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 diff --git a/sdk/python/src/openai_codex/api.py b/sdk/python/src/openai_codex/api.py index f27de49ebc..6fc9a8243d 100644 --- a/sdk/python/src/openai_codex/api.py +++ b/sdk/python/src/openai_codex/api.py @@ -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) diff --git a/sdk/python/src/openai_codex/client.py b/sdk/python/src/openai_codex/client.py index a97e12f658..9951d6d1f3 100644 --- a/sdk/python/src/openai_codex/client.py +++ b/sdk/python/src/openai_codex/client.py @@ -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, ...] = () diff --git a/sdk/python/src/openai_codex/errors.py b/sdk/python/src/openai_codex/errors.py index 63ff7d2d2a..db3e7238d3 100644 --- a/sdk/python/src/openai_codex/errors.py +++ b/sdk/python/src/openai_codex/errors.py @@ -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): diff --git a/sdk/python/tests/test_artifact_workflow_and_binaries.py b/sdk/python/tests/test_artifact_workflow_and_binaries.py index 0fb97b8e38..6608cc3a40 100644 --- a/sdk/python/tests/test_artifact_workflow_and_binaries.py +++ b/sdk/python/tests/test_artifact_workflow_and_binaries.py @@ -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: diff --git a/sdk/python/tests/test_public_api_signatures.py b/sdk/python/tests/test_public_api_signatures.py index 59e25dda24..c26ac90f6b 100644 --- a/sdk/python/tests/test_public_api_signatures.py +++ b/sdk/python/tests/test_public_api_signatures.py @@ -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")