## 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
15 KiB
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
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)
Codex(config: CodexConfig | None = None)
Properties/methods:
metadata -> InitializeResponseclose() -> Nonelogin_api_key(api_key: str) -> Nonelogin_chatgpt() -> ChatgptLoginHandlelogin_chatgpt_device_code() -> DeviceCodeLoginHandleaccount(*, refresh_token: bool = False) -> GetAccountResponselogout() -> Nonethread_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) -> Threadthread_list(*, archived=None, cursor=None, cwd=None, limit=None, model_providers=None, sort_key=None, source_kinds=None) -> ThreadListResponsethread_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) -> Threadthread_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) -> Threadthread_archive(thread_id: str) -> ThreadArchiveResponsethread_unarchive(thread_id: str) -> Threadmodels(*, include_hidden: bool = False) -> ModelListResponse
Context manager:
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)
AsyncCodex(config: CodexConfig | None = None)
Preferred usage:
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 -> InitializeResponseclose() -> 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:
async with AsyncCodex() as codex:
...
Login handles
ChatgptLoginHandle / AsyncChatgptLoginHandle
login_id: strauth_url: strwait() -> AccountLoginCompletedNotificationcancel() -> CancelLoginAccountResponse
Async handle methods return awaitables.
DeviceCodeLoginHandle / AsyncDeviceCodeLoginHandle
login_id: strverification_url: struser_code: strwait() -> AccountLoginCompletedNotificationcancel() -> 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) -> TurnResultturn(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) -> TurnHandleread(*, include_turns: bool = False) -> ThreadReadResponseset_name(name: str) -> ThreadSetNameResponsecompact() -> 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: strstatus: TurnStatuserror: TurnError | Nonestarted_at: int | Nonecompleted_at: int | Noneduration_ms: int | Nonefinal_response: str | Noneitems: 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` |
| `turn_service_tier: str | None = None` |
| `source: str | None = None` |
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:
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) -> TurnSteerResponseinterrupt() -> TurnInterruptResponsestream() -> Iterator[Notification]run() -> TurnResult
Behavior notes:
stream()andrun()consume only notifications for their own turn ID- one
Codexinstance 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()andrun()consume only notifications for their own turn ID- one
AsyncCodexinstance can stream multiple active turns concurrently
Inputs
@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.
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` |
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:
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 for a user request followed by an external notification.
Public Types
The SDK wrappers return and accept public Codex protocol models wherever possible:
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
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
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)