mirror of
https://github.com/openai/codex.git
synced 2026-05-28 23:10:20 +00:00
[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:
41
.github/workflows/python-sdk-release.yml
vendored
41
.github/workflows/python-sdk-release.yml
vendored
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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, ...] = ()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user