Files
codex/sdk/python/docs/api-reference.md
Ahmed Ibrahim ddea03ad04 Start Python SDK turn subscriptions at their attachment point (#44400)
## What changed

Make turn event delivery depend on when each consumer attaches:

- Handles returned by `thread.turn(...)` receive events from when the request is sent, including events received before the response arrives.
- Other handles and explicit low-level registrations receive only events from when they join. Remove replay of completed items, token usage, and terminal events; use `thread.read(include_turns=True)` for earlier history.
- Retain unread events for active subscribers and pending starts, release unused turn state, and close the returned subscription when an async start is cancelled.

Preserve the existing low-level subscription when repeated `turn_start(...)` calls return the same turn.

## Testing

Update coverage for late joins, request-time buffering, completion or transport failure before a start returns, async cancellation cleanup, and repeated low-level starts. Adjust external-message integration tests to allow handles to collect different items while receiving the same final response.

GitOrigin-RevId: 3b7f1268df1193dee88385006ae9510101086c24
2026-09-10 03:40:55 +00:00

395 lines
15 KiB
Markdown

# OpenAI Codex Python SDK - API Reference
Public surface of `openai_codex` for Codex workflows.
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
```python
from openai_codex import (
Codex,
AsyncCodex,
CodexConfig,
ApprovalMode,
Sandbox,
ChatgptLoginHandle,
DeviceCodeLoginHandle,
AsyncChatgptLoginHandle,
AsyncDeviceCodeLoginHandle,
Thread,
AsyncThread,
TurnHandle,
AsyncTurnHandle,
TurnResult,
Input,
InputItem,
RunInput,
TextInput,
ImageInput,
LocalImageInput,
SkillInput,
MentionInput,
ExternalMessage,
)
from openai_codex.types import (
Account,
AccountLoginCompletedNotification,
CancelLoginAccountResponse,
CancelLoginAccountStatus,
GetAccountResponse,
InitializeResponse,
ThreadItem,
ThreadTokenUsage,
TurnError,
TurnStatus,
)
```
- Version: `openai_codex.__version__`
- Requires Python >= 3.10
- Public Codex protocol value and event types live in `openai_codex.types`
## Codex (sync)
```python
Codex(config: CodexConfig | None = None)
```
Properties/methods:
- `metadata -> InitializeResponse`
- `close() -> None`
- `login_api_key(api_key: str) -> None`
- `login_chatgpt() -> ChatgptLoginHandle`
- `login_chatgpt_device_code() -> DeviceCodeLoginHandle`
- `account(*, refresh_token: bool = False) -> GetAccountResponse`
- `logout() -> None`
- `thread_start(*, approval_mode=ApprovalMode.auto_review, base_instructions=None, config=None, cwd=None, developer_instructions=None, ephemeral=None, model=None, model_provider=None, personality=None, sandbox: Sandbox | None = None) -> Thread`
- `thread_list(*, archived=None, cursor=None, cwd=None, limit=None, model_providers=None, sort_key=None, source_kinds=None) -> ThreadListResponse`
- `thread_resume(thread_id: str, *, approval_mode=None, base_instructions=None, config=None, cwd=None, developer_instructions=None, include_turns: bool | None = None, model=None, model_provider=None, personality=None, sandbox: Sandbox | None = None, service_tier=None) -> Thread`
- `thread_fork(thread_id: str, *, approval_mode=None, base_instructions=None, config=None, cwd=None, developer_instructions=None, ephemeral=None, include_turns: bool | None = None, model=None, model_provider=None, sandbox: Sandbox | None = None, service_tier=None) -> Thread`
- `thread_archive(thread_id: str) -> ThreadArchiveResponse`
- `thread_unarchive(thread_id: str) -> Thread`
- `models(*, include_hidden: bool = False) -> ModelListResponse`
Context manager:
```python
with Codex() as codex:
...
```
`thread_resume(...)` and `thread_fork(...)` accept `include_turns` to control
whether the server loads turn history into its response. `False` skips that
work; `True` requests it. Omitting the option, or passing `None`, preserves the
server's default behavior. This does not remove history from the model's
context. Both methods return a thread handle; use `thread.read(include_turns=True)`
to retrieve its history.
## AsyncCodex (async parity)
```python
AsyncCodex(config: CodexConfig | None = None)
```
Preferred usage:
```python
async with AsyncCodex() as codex:
...
```
`AsyncCodex` initializes lazily. Context entry is the standard path because it
ensures startup and shutdown are paired explicitly.
Properties/methods:
- `metadata -> InitializeResponse`
- `close() -> Awaitable[None]`
- `login_api_key(api_key: str) -> Awaitable[None]`
- `login_chatgpt() -> Awaitable[AsyncChatgptLoginHandle]`
- `login_chatgpt_device_code() -> Awaitable[AsyncDeviceCodeLoginHandle]`
- `account(*, refresh_token: bool = False) -> Awaitable[GetAccountResponse]`
- `logout() -> Awaitable[None]`
- `thread_start(*, approval_mode=ApprovalMode.auto_review, base_instructions=None, config=None, cwd=None, developer_instructions=None, ephemeral=None, model=None, model_provider=None, personality=None, sandbox: Sandbox | None = None) -> Awaitable[AsyncThread]`
- `thread_list(*, archived=None, cursor=None, cwd=None, limit=None, model_providers=None, sort_key=None, source_kinds=None) -> Awaitable[ThreadListResponse]`
- `thread_resume(thread_id: str, *, approval_mode=None, base_instructions=None, config=None, cwd=None, developer_instructions=None, include_turns: bool | None = None, model=None, model_provider=None, personality=None, sandbox: Sandbox | None = None, service_tier=None) -> Awaitable[AsyncThread]`
- `thread_fork(thread_id: str, *, approval_mode=None, base_instructions=None, config=None, cwd=None, developer_instructions=None, ephemeral=None, include_turns: bool | None = None, model=None, model_provider=None, sandbox: Sandbox | None = None, service_tier=None) -> Awaitable[AsyncThread]`
- `thread_archive(thread_id: str) -> Awaitable[ThreadArchiveResponse]`
- `thread_unarchive(thread_id: str) -> Awaitable[AsyncThread]`
- `models(*, include_hidden: bool = False) -> Awaitable[ModelListResponse]`
Async context manager:
```python
async with AsyncCodex() as codex:
...
```
## Login handles
### ChatgptLoginHandle / AsyncChatgptLoginHandle
- `login_id: str`
- `auth_url: str`
- `wait() -> AccountLoginCompletedNotification`
- `cancel() -> CancelLoginAccountResponse`
Async handle methods return awaitables.
### DeviceCodeLoginHandle / AsyncDeviceCodeLoginHandle
- `login_id: str`
- `verification_url: str`
- `user_code: str`
- `wait() -> AccountLoginCompletedNotification`
- `cancel() -> CancelLoginAccountResponse`
Async handle methods return awaitables.
`wait()` consumes only the completion notification for its matching login
attempt. API-key login completes synchronously and does not return a handle.
## Thread / AsyncThread
`Thread` and `AsyncThread` share the same shape and intent.
### Thread
- `run(input: RunInput, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox: Sandbox | None = None, service_tier=None, source=None, summary=None, turn_service_tier=None) -> TurnResult`
- `turn(input: RunInput, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox: Sandbox | None = None, service_tier=None, source=None, summary=None, turn_service_tier=None) -> TurnHandle`
- `read(*, include_turns: bool = False) -> ThreadReadResponse`
- `set_name(name: str) -> ThreadSetNameResponse`
- `compact() -> ThreadCompactStartResponse`
### AsyncThread
- `run(input: RunInput, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox: Sandbox | None = None, service_tier=None, source=None, summary=None, turn_service_tier=None) -> Awaitable[TurnResult]`
- `turn(input: RunInput, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox: Sandbox | None = None, service_tier=None, source=None, summary=None, turn_service_tier=None) -> Awaitable[AsyncTurnHandle]`
- `read(*, include_turns: bool = False) -> Awaitable[ThreadReadResponse]`
- `set_name(name: str) -> Awaitable[ThreadSetNameResponse]`
- `compact() -> Awaitable[ThreadCompactStartResponse]`
`run(...)` is the common-case convenience path. It accepts the same input and
options as `turn(...)`, consumes notifications until completion, and returns a
small result object with:
- `id: str`
- `status: TurnStatus`
- `error: TurnError | None`
- `started_at: int | None`
- `completed_at: int | None`
- `duration_ms: int | None`
- `final_response: str | None`
- `items: list[ThreadItem]`
- `usage: ThreadTokenUsage | None`
`final_response` is `None` when the turn finishes without a final-answer or
phase-less assistant message item.
Use `turn(...)` when you need low-level turn control (`stream()`, `steer()`,
`interrupt()`) before collecting the turn result.
### Turn options
These options have the same behavior on sync and async `run(...)` and `turn(...)`:
| Option | Behavior |
| --- | --- |
| `service_tier: str | None = None` | Sets the thread's service tier for this and subsequent turns. |
| `turn_service_tier: str | None = None` | Overrides the tier for a newly started turn only. `None` inherits the thread setting; `"default"` selects standard speed. Does not change the thread default and is ignored when input joins an active turn. |
| `source: str | None = None` | Labels the caller that initiated a new turn, such as `"review_ui"`. This is metadata; it does not schedule work or grant authority. Ignored when input joins an active turn. |
`ExternalMessage`, `turn_service_tier`, `source`, and explicit `include_turns`
on resume/fork require Codex CLI 0.151.0 or newer. The SDK raises `CodexError`
before sending these options to an older runtime, which would otherwise ignore
them. Published SDK releases install a matching runtime automatically; when
using `CodexConfig.codex_bin`, choose a compatible executable. Unversioned local
builds are checked lazily against their experimental schema before these options
are sent. A custom `launch_args_override` must report a supported version.
## Sandbox
Use `sandbox=` consistently on thread lifecycle methods and turns:
```python
from openai_codex import Codex, Sandbox
with Codex() as codex:
thread = codex.thread_start(sandbox=Sandbox.workspace_write)
result = thread.run("Review the diff only.", sandbox=Sandbox.read_only)
```
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.
## TurnHandle / AsyncTurnHandle
A `thread.turn(...)` handle receives events from when the call sends its request.
Other handles start when they join; use `thread.read(include_turns=True)` for earlier history.
### TurnHandle
- `steer(input: str | Input) -> TurnSteerResponse`
- `interrupt() -> TurnInterruptResponse`
- `stream() -> Iterator[Notification]`
- `run() -> TurnResult`
Behavior notes:
- `stream()` and `run()` consume only notifications for their own turn ID
- one `Codex` instance can stream multiple active turns concurrently
### AsyncTurnHandle
- `steer(input: str | Input) -> Awaitable[TurnSteerResponse]`
- `interrupt() -> Awaitable[TurnInterruptResponse]`
- `stream() -> AsyncIterator[Notification]`
- `run() -> Awaitable[TurnResult]`
Behavior notes:
- `stream()` and `run()` consume only notifications for their own turn ID
- one `AsyncCodex` instance can stream multiple active turns concurrently
## Inputs
```python
@dataclass class TextInput: text: str
@dataclass class ImageInput: url: str
@dataclass class LocalImageInput: path: str
@dataclass class SkillInput: name: str; path: str
@dataclass class MentionInput: name: str; path: str
InputItem = TextInput | ImageInput | LocalImageInput | SkillInput | MentionInput
Input = list[InputItem] | InputItem
RunInput = Input | str | ExternalMessage
```
Use `ImageInput` with a base64-encoded `data:image/...` URL. HTTP and HTTPS image URLs are
deprecated; download remote images and pass their local paths with `LocalImageInput` instead.
Use a plain `str` as shorthand for `TextInput(...)` anywhere a turn input is accepted:
`thread.run("...")`, `thread.turn("...")`, and `turn.steer("...")`.
### ExternalMessage
`ExternalMessage` supplies **untrusted content** from another agent, tool, or
application. Content reaches the model with tool-level authority, below user
and developer instructions. It does not establish user authorization or
approval. Keep the thread's sandbox and approval policies appropriate for the
work the user has authorized.
```python
from openai_codex import ExternalMessage
message = ExternalMessage(
tool_name="notifications",
namespace="slack",
content="Deployment notification: the staging checks failed.",
)
result = thread.run(message)
```
| Field | Meaning |
| --- | --- |
| `tool_name: str` | Required, nonempty name of the tool or application delivering the message. |
| `content` | Required text, or a sequence of structured content dictionaries or generated `FunctionCallOutputContentItem` models. Structured image content requires inline data URLs. |
| `namespace: str | None = None` | Optional namespace for the tool name. |
Pass one `ExternalMessage` as the complete input to `run(...)` or `turn(...)`.
It starts a turn when the thread is idle or joins an active regular turn. It
appears in saved history and item notifications as a `functionCallOutput`
item, retaining tool authority. No preceding tool call or call ID is required.
Tool names and namespaces identify the source; they are not proof of its
identity or permission to act.
When a message joins an active turn, both handles can stream or collect the
result independently. A joining handle receives previously completed items and
the latest usage, followed by live notifications. Consumed transient events such
as token deltas are discarded. Both handles collect the complete result, and
closing one stream leaves the other active.
The async calls use the same object:
```python
result = await async_thread.run(message)
```
Use `await async_thread.turn(message)` to collect a handle for streaming and
interruption. An `ExternalMessage` cannot be mixed into a user-input list.
`TurnHandle.steer(...)` accepts user input; deliver an external message to an
active turn through `thread.turn(message)`.
See the [external message examples](../examples/16_external_message) for a user
request followed by an external notification.
## Public Types
The SDK wrappers return and accept public Codex protocol models wherever possible:
```python
from openai_codex.types import (
Account,
AccountLoginCompletedNotification,
CancelLoginAccountResponse,
CancelLoginAccountStatus,
GetAccountResponse,
ThreadReadResponse,
Turn,
TurnStatus,
)
```
### Notifications and generated models
Known notifications have typed `Notification.payload` values, including
authentication recovery, thread queue/project changes, thread reversion, and
realtime item updates. The `Notification.payload` type covers every registered
event. Unknown methods and payloads that fail validation still produce
`UnknownNotification`, with the raw data in
`.params`. When an event gains a typed payload, read its named fields instead
of `.params`.
Returned models include the current CLI's thread metadata, richer turn errors,
and `functionCallOutput` history items. Code that imports generated
`HookMetadata` directly must access the handler through `.root`, inspect its
`handler_type`, and then read the fields for that handler. For example, only a
`"command"` handler has a `command` field. This reflects the app-server's
separate command, MCP tool, prompt, and agent hook variants.
## Retry + errors
```python
from openai_codex import (
retry_on_overload,
JsonRpcError,
MethodNotFoundError,
InvalidParamsError,
ServerBusyError,
is_retryable_error,
)
```
- `retry_on_overload(...)` retries transient overload errors with exponential backoff + jitter.
- `is_retryable_error(exc)` checks if an exception is transient/overload-like.
## Example
```python
from openai_codex import Codex
with Codex() as codex:
thread = codex.thread_start(model="gpt-5.4", config={"model_reasoning_effort": "high"})
result = thread.run("Say hello in one sentence.")
print(result.final_response)
```