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

## Why

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

## What changed

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

## Validation

- Ran `uv run --frozen --extra dev ruff check
scripts/update_sdk_artifacts.py src/openai_codex
tests/test_public_api_signatures.py
tests/test_artifact_workflow_and_binaries.py` before the final
README-only reductions and review-fix follow-ups.
- Built `openai_codex-0.1.0b1-py3-none-any.whl` and
`openai_codex-0.1.0b1.tar.gz` before the final README-only reductions
and review-fix follow-ups.
- Ran `python -m twine check --strict` on both built artifacts before
the final README-only reductions and review-fix follow-ups.
- Verified artifact metadata reports `Apache-2.0` without a duplicated
SDK-local license file.
- Verified `inspect.getdoc(...)` resolves documentation for the package,
`Codex`, `CodexConfig`, and key generated thread methods.
- Rebased the documentation/readiness change onto merged PR #24828
without changing the intended SDK or workflow file contents.
- Final verification is delegated to online CI for this PR.
This commit is contained in:
Ahmed Ibrahim
2026-05-27 18:29:05 -07:00
committed by GitHub
Unverified
parent 4d0c4cd058
commit eb1cc3824c
16 changed files with 365 additions and 239 deletions
+14
View File
@@ -1,3 +1,17 @@
"""Python SDK for running Codex workflows.
Start with :class:`Codex` for synchronous applications or
:class:`AsyncCodex` for async applications. Most programs create a thread and
run a turn::
from openai_codex import Codex, Sandbox
with Codex() as codex:
thread = codex.thread_start(sandbox=Sandbox.workspace_write)
result = thread.run("Describe this project.")
print(result.final_response)
"""
from ._version import __version__
from .api import (
ApprovalMode,
+10
View File
@@ -7,27 +7,37 @@ from .models import JsonObject
@dataclass(slots=True)
class TextInput:
"""Text supplied to a turn or steering request."""
text: str
@dataclass(slots=True)
class ImageInput:
"""Remote image URL supplied as turn input."""
url: str
@dataclass(slots=True)
class LocalImageInput:
"""Local image path supplied as turn input."""
path: str
@dataclass(slots=True)
class SkillInput:
"""Named skill reference supplied as turn input."""
name: str
path: str
@dataclass(slots=True)
class MentionInput:
"""Named resource mention supplied as turn input."""
name: str
path: str
+2
View File
@@ -20,6 +20,8 @@ from .models import Notification
@dataclass(slots=True)
class TurnResult:
"""Collected result returned after a turn completes."""
id: str
status: TurnStatus
error: TurnError | None
+38 -1
View File
@@ -73,7 +73,11 @@ from .models import InitializeResponse, JsonObject, Notification
class Codex:
"""Typed Python client for Codex workflows."""
"""Synchronous client for creating threads and running Codex turns.
The client starts its runtime connection during construction. Use it as a
context manager so resources are closed promptly.
"""
def __init__(self, config: CodexConfig | None = None) -> None:
self._client = CodexClient(config=config)
@@ -143,6 +147,7 @@ class Codex:
session_start_source: ThreadStartSource | None = None,
thread_source: ThreadSource | None = None,
) -> Thread:
"""Create a new Codex conversation thread."""
approval_policy, approvals_reviewer = _approval_mode_settings(approval_mode)
params = ThreadStartParams(
approval_policy=approval_policy,
@@ -178,6 +183,7 @@ class Codex:
source_kinds: list[ThreadSourceKind] | None = None,
use_state_db_only: bool | None = None,
) -> ThreadListResponse:
"""List saved conversation threads."""
params = ThreadListParams(
archived=archived,
cursor=cursor,
@@ -207,6 +213,7 @@ class Codex:
sandbox: Sandbox | None = None,
service_tier: str | None = None,
) -> Thread:
"""Resume an existing conversation thread by ID."""
approval_policy, approvals_reviewer = _approval_mode_override_settings(approval_mode)
params = ThreadResumeParams(
thread_id=thread_id,
@@ -241,6 +248,7 @@ class Codex:
service_tier: str | None = None,
thread_source: ThreadSource | None = None,
) -> Thread:
"""Create a new thread from an existing thread."""
approval_policy, approvals_reviewer = _approval_mode_override_settings(approval_mode)
params = ThreadForkParams(
thread_id=thread_id,
@@ -261,15 +269,18 @@ class Codex:
return Thread(self._client, forked.thread.id)
def thread_archive(self, thread_id: str) -> ThreadArchiveResponse:
"""Archive a stored conversation thread."""
return self._client.thread_archive(thread_id)
def thread_unarchive(self, thread_id: str) -> Thread:
"""Restore an archived conversation thread."""
unarchived = self._client.thread_unarchive(thread_id)
return Thread(self._client, unarchived.thread.id)
# END GENERATED: Codex.flat_methods
def models(self, *, include_hidden: bool = False) -> ModelListResponse:
"""List available models reported by Codex."""
return self._client.model_list(include_hidden=include_hidden)
@@ -376,6 +387,7 @@ class AsyncCodex:
session_start_source: ThreadStartSource | None = None,
thread_source: ThreadSource | None = None,
) -> AsyncThread:
"""Create a new Codex conversation thread."""
await self._ensure_initialized()
approval_policy, approvals_reviewer = _approval_mode_settings(approval_mode)
params = ThreadStartParams(
@@ -412,6 +424,7 @@ class AsyncCodex:
source_kinds: list[ThreadSourceKind] | None = None,
use_state_db_only: bool | None = None,
) -> ThreadListResponse:
"""List saved conversation threads."""
await self._ensure_initialized()
params = ThreadListParams(
archived=archived,
@@ -442,6 +455,7 @@ class AsyncCodex:
sandbox: Sandbox | None = None,
service_tier: str | None = None,
) -> AsyncThread:
"""Resume an existing conversation thread by ID."""
await self._ensure_initialized()
approval_policy, approvals_reviewer = _approval_mode_override_settings(approval_mode)
params = ThreadResumeParams(
@@ -477,6 +491,7 @@ class AsyncCodex:
service_tier: str | None = None,
thread_source: ThreadSource | None = None,
) -> AsyncThread:
"""Create a new thread from an existing thread."""
await self._ensure_initialized()
approval_policy, approvals_reviewer = _approval_mode_override_settings(approval_mode)
params = ThreadForkParams(
@@ -498,10 +513,12 @@ class AsyncCodex:
return AsyncThread(self, forked.thread.id)
async def thread_archive(self, thread_id: str) -> ThreadArchiveResponse:
"""Archive a stored conversation thread."""
await self._ensure_initialized()
return await self._client.thread_archive(thread_id)
async def thread_unarchive(self, thread_id: str) -> AsyncThread:
"""Restore an archived conversation thread."""
await self._ensure_initialized()
unarchived = await self._client.thread_unarchive(thread_id)
return AsyncThread(self, unarchived.thread.id)
@@ -515,6 +532,8 @@ class AsyncCodex:
@dataclass(slots=True)
class Thread:
"""Synchronous conversation thread used to run one or more turns."""
_client: CodexClient
id: str
@@ -532,6 +551,7 @@ class Thread:
service_tier: str | None = None,
summary: ReasoningSummary | None = None,
) -> TurnResult:
"""Run a complete turn and collect its final result."""
turn = self.turn(
input,
approval_mode=approval_mode,
@@ -565,6 +585,7 @@ class Thread:
service_tier: str | None = None,
summary: ReasoningSummary | None = None,
) -> TurnHandle:
"""Start a turn and return a handle for streaming or control."""
wire_input = _to_wire_input(_normalize_run_input(input))
approval_policy, approvals_reviewer = _approval_mode_override_settings(approval_mode)
params = TurnStartParams(
@@ -587,6 +608,7 @@ class Thread:
# END GENERATED: Thread.flat_methods
def read(self, *, include_turns: bool = False) -> ThreadReadResponse:
"""Read this thread, optionally including its turn history."""
return self._client.thread_read(self.id, include_turns=include_turns)
def set_name(self, name: str) -> ThreadSetNameResponse:
@@ -598,6 +620,8 @@ class Thread:
@dataclass(slots=True)
class AsyncThread:
"""Asynchronous conversation thread used to run one or more turns."""
_codex: AsyncCodex
id: str
@@ -615,6 +639,7 @@ class AsyncThread:
service_tier: str | None = None,
summary: ReasoningSummary | None = None,
) -> TurnResult:
"""Run a complete turn asynchronously and collect its final result."""
turn = await self.turn(
input,
approval_mode=approval_mode,
@@ -648,6 +673,7 @@ class AsyncThread:
service_tier: str | None = None,
summary: ReasoningSummary | None = None,
) -> AsyncTurnHandle:
"""Start a turn and return a handle for streaming or control."""
await self._codex._ensure_initialized()
wire_input = _to_wire_input(_normalize_run_input(input))
approval_policy, approvals_reviewer = _approval_mode_override_settings(approval_mode)
@@ -675,6 +701,7 @@ class AsyncThread:
# END GENERATED: AsyncThread.flat_methods
async def read(self, *, include_turns: bool = False) -> ThreadReadResponse:
"""Read this thread, optionally including its turn history."""
await self._codex._ensure_initialized()
return await self._codex._client.thread_read(self.id, include_turns=include_turns)
@@ -689,11 +716,14 @@ class AsyncThread:
@dataclass(slots=True)
class TurnHandle:
"""Control and consume a synchronous turn after it has started."""
_client: CodexClient
thread_id: str
id: str
def steer(self, input: RunInput) -> TurnSteerResponse:
"""Send additional input to this active turn."""
return self._client.turn_steer(
self.thread_id,
self.id,
@@ -701,6 +731,7 @@ class TurnHandle:
)
def interrupt(self) -> TurnInterruptResponse:
"""Request interruption of this active turn."""
return self._client.turn_interrupt(self.thread_id, self.id)
def stream(self) -> Iterator[Notification]:
@@ -720,6 +751,7 @@ class TurnHandle:
self._client.unregister_turn_notifications(self.id)
def run(self) -> TurnResult:
"""Consume the turn stream and return its completed result."""
stream = self.stream()
try:
return _collect_turn_result(stream, turn_id=self.id)
@@ -729,11 +761,14 @@ class TurnHandle:
@dataclass(slots=True)
class AsyncTurnHandle:
"""Control and consume an asynchronous turn after it has started."""
_codex: AsyncCodex
thread_id: str
id: str
async def steer(self, input: RunInput) -> TurnSteerResponse:
"""Send additional input to this active turn."""
await self._codex._ensure_initialized()
return await self._codex._client.turn_steer(
self.thread_id,
@@ -742,6 +777,7 @@ class AsyncTurnHandle:
)
async def interrupt(self) -> TurnInterruptResponse:
"""Request interruption of this active turn."""
await self._codex._ensure_initialized()
return await self._codex._client.turn_interrupt(self.thread_id, self.id)
@@ -763,6 +799,7 @@ class AsyncTurnHandle:
self._codex._client.unregister_turn_notifications(self.id)
async def run(self) -> TurnResult:
"""Consume the turn stream and return its completed result."""
stream = self.stream()
try:
return await _collect_async_turn_result(stream, turn_id=self.id)
+6
View File
@@ -172,6 +172,12 @@ def _resolve_codex_bin(config: "CodexConfig") -> Path:
@dataclass(slots=True)
class CodexConfig:
"""Configuration for launching and identifying the local Codex runtime.
Most callers can use ``Codex()`` without configuration. Set ``codex_bin``
only when intentionally using a specific local Codex executable.
"""
codex_bin: str | None = None
launch_args_override: tuple[str, ...] | None = None
config_overrides: tuple[str, ...] = ()
+5 -5
View File
@@ -26,23 +26,23 @@ class CodexRpcError(JsonRpcError):
class ParseError(CodexRpcError):
pass
"""Raised when a request or response cannot be parsed."""
class InvalidRequestError(CodexRpcError):
pass
"""Raised when the runtime rejects the request shape."""
class MethodNotFoundError(CodexRpcError):
pass
"""Raised when the requested operation is unavailable."""
class InvalidParamsError(CodexRpcError):
pass
"""Raised when an operation receives invalid parameters."""
class InternalRpcError(CodexRpcError):
pass
"""Raised when the runtime reports an internal RPC failure."""
class ServerBusyError(CodexRpcError):