[codex] Rename Python SDK AppServerConfig to CodexConfig (#24800)

## Why

`AppServerConfig` is exported as part of the ergonomic Python SDK
surface and passed to `Codex(...)` and `AsyncCodex(...)`. That name
exposes the underlying app-server transport at the same layer where
users are configuring the Codex client. `CodexConfig` makes the common
callsite read naturally and names the object it configures.

## What changed

- Renamed the public configuration dataclass from `AppServerConfig` to
`CodexConfig`.
- Updated `Codex`, `AsyncCodex`, and the transport clients to accept
`CodexConfig`.
- Updated binary-resolution messages, package exports, docs, examples,
and related coverage to use the new public name.

## API impact

```python
from openai_codex import Codex, CodexConfig

with Codex(config=CodexConfig(codex_bin="/path/to/codex")) as codex:
    ...
```

Callers should now import and construct `CodexConfig`; `AppServerConfig`
is no longer part of the Python SDK surface.

## Validation

- `uv run --frozen --extra dev ruff check src/openai_codex scripts
examples tests`
- Tests are deferred to online CI for this PR.
This commit is contained in:
Ahmed Ibrahim
2026-05-27 16:10:15 -07:00
committed by GitHub
parent 090144e0ec
commit 0db49a7e6a
22 changed files with 127 additions and 126 deletions

View File

@@ -4,8 +4,8 @@ Experimental Python SDK for `codex app-server` JSON-RPC v2 over stdio, with a sm
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 app-servers camelCase wire format.
The package root exports the ergonomic client API; public app-server value and
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`.
## Install
@@ -17,7 +17,7 @@ source .venv/bin/activate
```
Published SDK builds pin an exact `openai-codex-cli-bin` runtime dependency
with the same version as the SDK. Pass `AppServerConfig(codex_bin=...)` only
with the same version as the SDK. Pass `CodexConfig(codex_bin=...)` only
when you intentionally want to run against a specific local app-server binary.
## Quickstart
@@ -26,7 +26,7 @@ when you intentionally want to run against a specific local app-server binary.
from openai_codex import Codex, Sandbox
with Codex() as codex:
# Call login_api_key(...) first when this app-server session is not
# 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.")
@@ -57,7 +57,7 @@ Available presets:
- `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, app-server uses its configured default. A sandbox
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.
@@ -87,7 +87,7 @@ with Codex() as codex:
Use `login_chatgpt_device_code()` for device-code auth, `handle.cancel()` to
stop an in-progress interactive login, and `logout()` to clear the active
app-server account session.
Codex account session.
## Docs map

View File

@@ -1,6 +1,6 @@
# OpenAI Codex SDK — API Reference
Public surface of `openai_codex` for app-server v2.
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.
Thread starts default to `ApprovalMode.auto_review`; turn starts accept an optional `approval_mode` override.
@@ -11,6 +11,7 @@ Thread starts default to `ApprovalMode.auto_review`; turn starts accept an optio
from openai_codex import (
Codex,
AsyncCodex,
CodexConfig,
ApprovalMode,
Sandbox,
ChatgptLoginHandle,
@@ -47,12 +48,12 @@ from openai_codex.types import (
- Version: `openai_codex.__version__`
- Requires Python >= 3.10
- Public app-server value and event types live in `openai_codex.types`
- Public Codex protocol value and event types live in `openai_codex.types`
## Codex (sync)
```python
Codex(config: AppServerConfig | None = None)
Codex(config: CodexConfig | None = None)
```
Properties/methods:
@@ -82,7 +83,7 @@ with Codex() as codex:
## AsyncCodex (async parity)
```python
AsyncCodex(config: AppServerConfig | None = None)
AsyncCodex(config: CodexConfig | None = None)
```
Preferred usage:
@@ -201,7 +202,7 @@ Presets:
- `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, app-server uses its configured default. A sandbox
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
@@ -249,7 +250,7 @@ Use a plain `str` as shorthand for `TextInput(...)` anywhere a turn input is acc
## Public Types
The SDK wrappers return and accept public app-server models wherever possible:
The SDK wrappers return and accept public Codex protocol models wherever possible:
```python
from openai_codex.types import (

View File

@@ -66,7 +66,7 @@ The presets are:
- `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, app-server uses its configured default. A turn
When `sandbox=` is omitted, Codex uses its configured default. A turn
sandbox override applies to that turn and subsequent turns.
## Why only `thread_start(...)` and `thread_resume(...)`?
@@ -86,7 +86,7 @@ Common causes:
- published runtime package (`openai-codex-cli-bin`) is not installed
- local `codex_bin` override points to a missing file
- app-server version older than the SDK schema
- installed Codex runtime version older than the SDK schema
## Why does a turn "hang"?

View File

@@ -99,7 +99,7 @@ Available presets:
- `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, app-server uses its configured default. A turn
When `sandbox=` is omitted, Codex uses its configured default. A turn
override also becomes the sandbox for subsequent turns on that thread.
## 5) Continue the same thread (multi-turn)
@@ -150,9 +150,9 @@ with Codex() as codex:
print(result.final_response)
```
## 8) Public app-server types
## 8) Public Codex protocol types
The convenience wrappers live at the package root. Public app-server value and
The convenience wrappers live at the package root. Public Codex protocol value and
event types live under:
```python

View File

@@ -47,11 +47,11 @@ def ensure_local_sdk_src() -> Path:
def runtime_config():
"""Return an example-friendly AppServerConfig for repo-source SDK usage."""
from openai_codex import AppServerConfig
"""Return an example-friendly CodexConfig for repo-source SDK usage."""
from openai_codex import CodexConfig
ensure_runtime_package_installed(sys.executable, _SDK_PYTHON_DIR)
return AppServerConfig()
return CodexConfig()
def _png_chunk(chunk_type: bytes, data: bytes) -> bytes:

View File

@@ -22,10 +22,10 @@ from .api import (
TurnHandle,
TurnResult,
)
from .client import AppServerConfig
from .client import CodexConfig
from .errors import (
AppServerError,
AppServerRpcError,
CodexError,
CodexRpcError,
InternalRpcError,
InvalidParamsError,
InvalidRequestError,
@@ -41,7 +41,7 @@ from .retry import retry_on_overload
__all__ = [
"__version__",
"AppServerConfig",
"CodexConfig",
"Codex",
"AsyncCodex",
"ApprovalMode",
@@ -64,10 +64,10 @@ __all__ = [
"SkillInput",
"MentionInput",
"retry_on_overload",
"AppServerError",
"CodexError",
"TransportClosedError",
"JsonRpcError",
"AppServerRpcError",
"CodexRpcError",
"ParseError",
"InvalidRequestError",
"MethodNotFoundError",

View File

@@ -3,8 +3,8 @@ from __future__ import annotations
from dataclasses import dataclass
from typing import Protocol
from .async_client import AsyncAppServerClient
from .client import AppServerClient
from .async_client import AsyncCodexClient
from .client import CodexClient
from .generated.v2_all import (
AccountLoginCompletedNotification,
CancelLoginAccountResponse,
@@ -19,14 +19,14 @@ from .generated.v2_all import (
class _AsyncLoginOwner(Protocol):
"""Subset of AsyncCodex needed by async login handles."""
_client: AsyncAppServerClient
_client: AsyncCodexClient
async def _ensure_initialized(self) -> None:
"""Ensure the owning SDK client has a live app-server connection."""
"""Ensure the owning SDK client has a live Codex connection."""
...
def start_chatgpt_login(client: AppServerClient) -> ChatgptLoginHandle:
def start_chatgpt_login(client: CodexClient) -> ChatgptLoginHandle:
"""Start browser ChatGPT login and return the handle for that attempt."""
response = client.account_login_start(
LoginAccountParams(
@@ -60,7 +60,7 @@ async def async_start_chatgpt_login(owner: _AsyncLoginOwner) -> AsyncChatgptLogi
)
def start_device_code_login(client: AppServerClient) -> DeviceCodeLoginHandle:
def start_device_code_login(client: CodexClient) -> DeviceCodeLoginHandle:
"""Start device-code ChatGPT login and return the handle for that attempt."""
response = client.account_login_start(
LoginAccountParams(
@@ -102,7 +102,7 @@ async def async_start_device_code_login(
class ChatgptLoginHandle:
"""Live browser-login attempt returned by `Codex.login_chatgpt()`."""
_client: AppServerClient
_client: CodexClient
login_id: str
auth_url: str
@@ -119,7 +119,7 @@ class ChatgptLoginHandle:
class DeviceCodeLoginHandle:
"""Live device-code login attempt returned by `Codex.login_chatgpt_device_code()`."""
_client: AppServerClient
_client: CodexClient
login_id: str
verification_url: str
user_code: str

View File

@@ -4,7 +4,7 @@ import queue
import threading
from collections import deque
from .errors import AppServerError, map_jsonrpc_error
from .errors import CodexError, map_jsonrpc_error
from .generated.notification_registry import notification_turn_id
from .generated.v2_all import AccountLoginCompletedNotification
from .models import JsonValue, Notification, UnknownNotification
@@ -136,7 +136,7 @@ class MessageRouter:
)
)
else:
waiter.put(AppServerError("Malformed JSON-RPC error response"))
waiter.put(CodexError("Malformed JSON-RPC error response"))
return
waiter.put(msg.get("result"))

View File

@@ -10,7 +10,7 @@ from .generated.v2_all import (
ThreadItem,
ThreadTokenUsage,
ThreadTokenUsageUpdatedNotification,
Turn as AppServerTurn,
Turn,
TurnCompletedNotification,
TurnError,
TurnStatus,
@@ -55,7 +55,7 @@ def _final_assistant_response_from_items(items: list[ThreadItem]) -> str | None:
return last_unknown_phase_response
def _raise_for_failed_turn(turn: AppServerTurn) -> None:
def _raise_for_failed_turn(turn: Turn) -> None:
if turn.status != TurnStatus.failed:
return
if turn.error is not None and turn.error.message:

View File

@@ -38,8 +38,8 @@ from ._run import (
_collect_turn_result,
)
from ._sandbox import Sandbox as Sandbox, _sandbox_mode, _sandbox_policy
from .async_client import AsyncAppServerClient
from .client import AppServerClient, AppServerConfig
from .async_client import AsyncCodexClient
from .client import CodexClient, CodexConfig
from .generated.v2_all import (
ApiKeyLoginAccountParams,
GetAccountParams,
@@ -73,10 +73,10 @@ from .models import InitializeResponse, JsonObject, Notification
class Codex:
"""Typed Python client for app-server v2 workflows."""
"""Typed Python client for Codex workflows."""
def __init__(self, config: AppServerConfig | None = None) -> None:
self._client = AppServerClient(config=config)
def __init__(self, config: CodexConfig | None = None) -> None:
self._client = CodexClient(config=config)
try:
self._client.start()
self._init = validate_initialize_metadata(self._client.initialize())
@@ -98,7 +98,7 @@ class Codex:
self._client.close()
def login_api_key(self, api_key: str) -> None:
"""Authenticate app-server with an API key."""
"""Authenticate Codex with an API key."""
self._client.account_login_start(
LoginAccountParams(
root=ApiKeyLoginAccountParams(
@@ -117,11 +117,11 @@ class Codex:
return start_device_code_login(self._client)
def account(self, *, refresh_token: bool = False) -> GetAccountResponse:
"""Read the current app-server account state."""
"""Read the current Codex account state."""
return self._client.account_read(GetAccountParams(refresh_token=refresh_token))
def logout(self) -> None:
"""Clear the current app-server account session."""
"""Clear the current Codex account session."""
self._client.account_logout()
# BEGIN GENERATED: Codex.flat_methods
@@ -281,8 +281,8 @@ class AsyncCodex:
or first awaited API use.
"""
def __init__(self, config: AppServerConfig | None = None) -> None:
self._client = AsyncAppServerClient(config=config)
def __init__(self, config: CodexConfig | None = None) -> None:
self._client = AsyncCodexClient(config=config)
self._init: InitializeResponse | None = None
self._initialized = False
self._init_lock = asyncio.Lock()
@@ -326,7 +326,7 @@ class AsyncCodex:
self._initialized = False
async def login_api_key(self, api_key: str) -> None:
"""Authenticate app-server with an API key."""
"""Authenticate Codex with an API key."""
await self._ensure_initialized()
await self._client.account_login_start(
LoginAccountParams(
@@ -348,12 +348,12 @@ class AsyncCodex:
return await async_start_device_code_login(self)
async def account(self, *, refresh_token: bool = False) -> GetAccountResponse:
"""Read the current app-server account state."""
"""Read the current Codex account state."""
await self._ensure_initialized()
return await self._client.account_read(GetAccountParams(refresh_token=refresh_token))
async def logout(self) -> None:
"""Clear the current app-server account session."""
"""Clear the current Codex account session."""
await self._ensure_initialized()
await self._client.account_logout()
@@ -515,7 +515,7 @@ class AsyncCodex:
@dataclass(slots=True)
class Thread:
_client: AppServerClient
_client: CodexClient
id: str
def run(
@@ -689,7 +689,7 @@ class AsyncThread:
@dataclass(slots=True)
class TurnHandle:
_client: AppServerClient
_client: CodexClient
thread_id: str
id: str

View File

@@ -6,7 +6,7 @@ from typing import AsyncIterator, Callable, ParamSpec, TypeVar
from pydantic import BaseModel
from .client import AppServerClient, AppServerConfig
from .client import CodexClient, CodexConfig
from .generated.v2_all import (
AccountLoginCompletedNotification,
AgentMessageDeltaNotification,
@@ -43,20 +43,20 @@ ParamsT = ParamSpec("ParamsT")
ReturnT = TypeVar("ReturnT")
class AsyncAppServerClient:
"""Async wrapper around AppServerClient using thread offloading."""
class AsyncCodexClient:
"""Async wrapper around CodexClient using thread offloading."""
def __init__(self, config: AppServerConfig | None = None) -> None:
def __init__(self, config: CodexConfig | None = None) -> None:
"""Create the wrapped sync client that owns the transport process."""
self._sync = AppServerClient(config=config)
self._sync = CodexClient(config=config)
async def __aenter__(self) -> "AsyncAppServerClient":
"""Start the app-server process when entering an async context."""
async def __aenter__(self) -> "AsyncCodexClient":
"""Start the Codex process when entering an async context."""
await self.start()
return self
async def __aexit__(self, _exc_type, _exc, _tb) -> None:
"""Close the app-server process when leaving an async context."""
"""Close the Codex process when leaving an async context."""
await self.close()
async def _call_sync(
@@ -88,7 +88,7 @@ class AsyncAppServerClient:
await self._call_sync(self._sync.close)
async def initialize(self) -> InitializeResponse:
"""Initialize the app-server session."""
"""Initialize the Codex session."""
return await self._call_sync(self._sync.initialize)
def register_turn_notifications(self, turn_id: str) -> None:

View File

@@ -12,7 +12,7 @@ from pydantic import BaseModel
from ._message_router import MessageRouter
from ._version import __version__ as SDK_VERSION
from .errors import AppServerError, TransportClosedError
from .errors import CodexError, TransportClosedError
from .generated.notification_registry import NOTIFICATION_MODELS
from .generated.v2_all import (
AccountLoginCompletedNotification,
@@ -94,7 +94,7 @@ def _installed_codex_path() -> Path:
except ImportError as exc:
raise FileNotFoundError(
"Unable to locate the pinned Codex runtime. Install the published SDK build "
f"with its {RUNTIME_PKG_NAME} dependency, or set AppServerConfig.codex_bin "
f"with its {RUNTIME_PKG_NAME} dependency, or set CodexConfig.codex_bin "
"explicitly."
) from exc
@@ -153,12 +153,12 @@ def _default_codex_bin_resolver_ops() -> CodexBinResolverOps:
)
def resolve_codex_bin(config: "AppServerConfig", ops: CodexBinResolverOps) -> Path:
def resolve_codex_bin(config: "CodexConfig", ops: CodexBinResolverOps) -> Path:
if config.codex_bin is not None:
codex_bin = Path(config.codex_bin)
if not ops.path_exists(codex_bin):
raise FileNotFoundError(
f"Codex binary not found at {codex_bin}. Set AppServerConfig.codex_bin "
f"Codex binary not found at {codex_bin}. Set CodexConfig.codex_bin "
"to a valid binary path."
)
return codex_bin
@@ -166,12 +166,12 @@ def resolve_codex_bin(config: "AppServerConfig", ops: CodexBinResolverOps) -> Pa
return ops.installed_codex_path()
def _resolve_codex_bin(config: "AppServerConfig") -> Path:
def _resolve_codex_bin(config: "CodexConfig") -> Path:
return resolve_codex_bin(config, _default_codex_bin_resolver_ops())
@dataclass(slots=True)
class AppServerConfig:
class CodexConfig:
codex_bin: str | None = None
launch_args_override: tuple[str, ...] | None = None
config_overrides: tuple[str, ...] = ()
@@ -183,15 +183,15 @@ class AppServerConfig:
experimental_api: bool = True
class AppServerClient:
class CodexClient:
"""Synchronous typed JSON-RPC client for `codex app-server` over stdio."""
def __init__(
self,
config: AppServerConfig | None = None,
config: CodexConfig | None = None,
approval_handler: ApprovalHandler | None = None,
) -> None:
self.config = config or AppServerConfig()
self.config = config or CodexConfig()
self._approval_handler = approval_handler or self._default_approval_handler
self._proc: subprocess.Popen[str] | None = None
self._lock = threading.Lock()
@@ -200,7 +200,7 @@ class AppServerClient:
self._stderr_thread: threading.Thread | None = None
self._reader_thread: threading.Thread | None = None
def __enter__(self) -> "AppServerClient":
def __enter__(self) -> "CodexClient":
self.start()
return self
@@ -289,7 +289,7 @@ class AppServerClient:
) -> ModelT:
result = self._request_raw(method, params)
if not isinstance(result, dict):
raise AppServerError(f"{method} response must be a JSON object")
raise CodexError(f"{method} response must be a JSON object")
return response_model.model_validate(result)
def _request_raw(self, method: str, params: JsonObject | None = None) -> JsonValue:
@@ -659,28 +659,28 @@ class AppServerClient:
def _write_message(self, payload: JsonObject) -> None:
if self._proc is None or self._proc.stdin is None:
raise TransportClosedError("app-server is not running")
raise TransportClosedError("Codex process is not running")
with self._lock:
self._proc.stdin.write(json.dumps(payload) + "\n")
self._proc.stdin.flush()
def _read_message(self) -> dict[str, JsonValue]:
if self._proc is None or self._proc.stdout is None:
raise TransportClosedError("app-server is not running")
raise TransportClosedError("Codex process is not running")
line = self._proc.stdout.readline()
if not line:
raise TransportClosedError(
f"app-server closed stdout. stderr_tail={self._stderr_tail()[:2000]}"
f"Codex process closed stdout. stderr_tail={self._stderr_tail()[:2000]}"
)
try:
message = json.loads(line)
except json.JSONDecodeError as exc:
raise AppServerError(f"Invalid JSON-RPC line: {line!r}") from exc
raise CodexError(f"Invalid JSON-RPC line: {line!r}") from exc
if not isinstance(message, dict):
raise AppServerError(f"Invalid JSON-RPC payload: {message!r}")
raise CodexError(f"Invalid JSON-RPC payload: {message!r}")
return message

View File

@@ -3,11 +3,11 @@ from __future__ import annotations
from typing import Any
class AppServerError(Exception):
class CodexError(Exception):
"""Base exception for SDK errors."""
class JsonRpcError(AppServerError):
class JsonRpcError(CodexError):
"""Raw JSON-RPC error wrapper from the server."""
def __init__(self, code: int, message: str, data: Any = None):
@@ -17,35 +17,35 @@ class JsonRpcError(AppServerError):
self.data = data
class TransportClosedError(AppServerError):
"""Raised when the app-server transport closes unexpectedly."""
class TransportClosedError(CodexError):
"""Raised when the Codex transport closes unexpectedly."""
class AppServerRpcError(JsonRpcError):
class CodexRpcError(JsonRpcError):
"""Base typed error for JSON-RPC failures."""
class ParseError(AppServerRpcError):
class ParseError(CodexRpcError):
pass
class InvalidRequestError(AppServerRpcError):
class InvalidRequestError(CodexRpcError):
pass
class MethodNotFoundError(AppServerRpcError):
class MethodNotFoundError(CodexRpcError):
pass
class InvalidParamsError(AppServerRpcError):
class InvalidParamsError(CodexRpcError):
pass
class InternalRpcError(AppServerRpcError):
class InternalRpcError(CodexRpcError):
pass
class ServerBusyError(AppServerRpcError):
class ServerBusyError(CodexRpcError):
"""Server is overloaded / unavailable and caller should retry."""
@@ -104,7 +104,7 @@ def map_jsonrpc_error(code: int, message: str, data: Any = None) -> JsonRpcError
return ServerBusyError(code, message, data)
if _contains_retry_limit_text(message):
return RetryLimitExceededError(code, message, data)
return AppServerRpcError(code, message, data)
return CodexRpcError(code, message, data)
return JsonRpcError(code, message, data)

View File

@@ -1,4 +1,4 @@
"""Public app-server model exports for type annotations and matching."""
"""Public Codex protocol model exports for type annotations and matching."""
from __future__ import annotations

View File

@@ -10,7 +10,7 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any
from openai_codex import AppServerConfig
from openai_codex import CodexConfig
Json = dict[str, Any]
@@ -225,9 +225,9 @@ class AppServerHarness:
shutil.rmtree(self.codex_home, ignore_errors=True)
shutil.rmtree(self.workspace, ignore_errors=True)
def app_server_config(self) -> AppServerConfig:
def app_server_config(self) -> CodexConfig:
"""Build SDK config for an isolated pinned-runtime app-server process."""
return AppServerConfig(
return CodexConfig(
cwd=str(self.workspace),
env={
"CODEX_HOME": str(self.codex_home),

View File

@@ -5,14 +5,14 @@ import json
from app_server_harness import AppServerHarness
from openai_codex import AppServerConfig, Codex
from openai_codex import Codex, CodexConfig
from openai_codex.generated.v2_all import (
ChatgptAuthTokensLoginAccountParams,
LoginAccountParams,
)
def _app_server_config(harness: AppServerHarness) -> AppServerConfig:
def _app_server_config(harness: AppServerHarness) -> CodexConfig:
"""Build an isolated login config without inheriting ambient API-key auth."""
config = harness.app_server_config()
config.env = {**(config.env or {}), "OPENAI_API_KEY": ""}

View File

@@ -114,7 +114,7 @@ def test_async_stream_routes_text_deltas_and_completion(tmp_path) -> None:
def test_low_level_sync_stream_text_uses_real_turn_routing(tmp_path) -> None:
"""AppServerClient.stream_text should stream through a real app-server turn."""
"""CodexClient.stream_text should stream through a real app-server turn."""
with AppServerHarness(tmp_path) as harness:
harness.responses.enqueue_sse(
streaming_response("low-sync-stream", "msg-low-sync-stream", ["fir", "st"])

View File

@@ -669,7 +669,7 @@ def test_default_runtime_is_resolved_from_installed_runtime_package(
path_exists=lambda path: path == fake_binary,
)
config = client_module.AppServerConfig()
config = client_module.CodexConfig()
assert config.codex_bin is None
assert client_module.resolve_codex_bin(config, ops) == fake_binary
@@ -717,7 +717,7 @@ def test_explicit_codex_bin_override_takes_priority(tmp_path: Path) -> None:
path_exists=lambda path: path == explicit_binary,
)
config = client_module.AppServerConfig(codex_bin=str(explicit_binary))
config = client_module.CodexConfig(codex_bin=str(explicit_binary))
assert client_module.resolve_codex_bin(config, ops) == explicit_binary
@@ -732,7 +732,7 @@ def test_missing_runtime_package_requires_explicit_codex_bin() -> None:
)
with pytest.raises(FileNotFoundError, match="missing packaged runtime"):
client_module.resolve_codex_bin(client_module.AppServerConfig(), ops)
client_module.resolve_codex_bin(client_module.CodexConfig(), ops)
def test_broken_runtime_package_does_not_fall_back() -> None:
@@ -746,6 +746,6 @@ def test_broken_runtime_package_does_not_fall_back() -> None:
)
with pytest.raises(FileNotFoundError) as exc_info:
client_module.resolve_codex_bin(client_module.AppServerConfig(), ops)
client_module.resolve_codex_bin(client_module.CodexConfig(), ops)
assert str(exc_info.value) == ("missing packaged binary")

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import asyncio
import time
from openai_codex.async_client import AsyncAppServerClient
from openai_codex.async_client import AsyncCodexClient
from openai_codex.generated.v2_all import (
TurnCompletedNotification,
)
@@ -15,7 +15,7 @@ def test_async_client_allows_concurrent_transport_calls() -> None:
async def scenario() -> int:
"""Run two blocking sync calls and report peak overlap."""
client = AsyncAppServerClient()
client = AsyncCodexClient()
active = 0
max_active = 0
@@ -40,7 +40,7 @@ def test_async_client_turn_notification_methods_delegate_to_sync_client() -> Non
async def scenario() -> tuple[list[tuple[str, str]], Notification, str]:
"""Record the sync-client calls made by async turn notification wrappers."""
client = AsyncAppServerClient()
client = AsyncCodexClient()
event = Notification(
method="unknown/direct",
payload=UnknownNotification(params={"turnId": "turn-1"}),

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from pathlib import Path
from openai_codex.client import AppServerClient, _params_dict
from openai_codex.client import CodexClient, _params_dict
from openai_codex.generated.notification_registry import notification_turn_id
from openai_codex.generated.v2_all import (
AgentMessageDeltaNotification,
@@ -63,7 +63,7 @@ def test_thread_resume_response_accepts_auto_review_reviewer() -> None:
def test_notifications_are_typed_with_canonical_v2_methods() -> None:
client = AppServerClient()
client = CodexClient()
event = client._coerce_notification(
"thread/tokenUsage/updated",
{
@@ -94,7 +94,7 @@ def test_notifications_are_typed_with_canonical_v2_methods() -> None:
def test_unknown_notifications_fall_back_to_unknown_payloads() -> None:
client = AppServerClient()
client = CodexClient()
event = client._coerce_notification(
"unknown/notification",
{
@@ -110,7 +110,7 @@ def test_unknown_notifications_fall_back_to_unknown_payloads() -> None:
def test_invalid_notification_payload_falls_back_to_unknown() -> None:
client = AppServerClient()
client = CodexClient()
event = client._coerce_notification("thread/tokenUsage/updated", {"threadId": "missing"})
assert event.method == "thread/tokenUsage/updated"
@@ -144,7 +144,7 @@ def test_generated_notification_turn_id_handles_known_payload_shapes() -> None:
def test_turn_notification_router_demuxes_registered_turns() -> None:
"""The router should deliver out-of-order turn events to the matching queues."""
client = AppServerClient()
client = CodexClient()
client.register_turn_notifications("turn-1")
client.register_turn_notifications("turn-2")
@@ -187,7 +187,7 @@ def test_turn_notification_router_demuxes_registered_turns() -> None:
def test_client_reader_routes_interleaved_turn_notifications_by_turn_id() -> None:
"""Reader-loop routing should preserve order within each interleaved turn stream."""
client = AppServerClient()
client = CodexClient()
client.register_turn_notifications("turn-1")
client.register_turn_notifications("turn-2")
@@ -266,7 +266,7 @@ def test_client_reader_routes_interleaved_turn_notifications_by_turn_id() -> Non
def test_turn_notification_router_buffers_events_before_registration() -> None:
"""Early turn events should be replayed once their TurnHandle registers."""
client = AppServerClient()
client = CodexClient()
client._router.route_notification(
client._coerce_notification(
"item/agentMessage/delta",
@@ -291,7 +291,7 @@ def test_turn_notification_router_buffers_events_before_registration() -> None:
def test_turn_notification_router_clears_unregistered_turn_when_completed() -> None:
"""A completed unregistered turn should not leave a pending queue behind."""
client = AppServerClient()
client = CodexClient()
client._router.route_notification(
client._coerce_notification(
"item/agentMessage/delta",
@@ -318,7 +318,7 @@ def test_turn_notification_router_clears_unregistered_turn_when_completed() -> N
def test_turn_notification_router_routes_unknown_turn_notifications() -> None:
"""Unknown notifications should still route when their raw params carry a turn id."""
client = AppServerClient()
client = CodexClient()
client.register_turn_notifications("turn-1")
client.register_turn_notifications("turn-2")

View File

@@ -52,7 +52,7 @@ def test_codex_init_failure_closes_client(monkeypatch: pytest.MonkeyPatch) -> No
self._closed = True
closed.append(True)
monkeypatch.setattr(public_api_module, "AppServerClient", FakeClient)
monkeypatch.setattr(public_api_module, "CodexClient", FakeClient)
with pytest.raises(RuntimeError, match="missing required metadata"):
Codex()

View File

@@ -11,11 +11,11 @@ import openai_codex
import openai_codex.types as public_types
from openai_codex import (
ApprovalMode,
AppServerConfig,
AsyncCodex,
AsyncThread,
AsyncTurnHandle,
Codex,
CodexConfig,
Sandbox,
Thread,
TurnHandle,
@@ -26,7 +26,7 @@ from openai_codex.types import InitializeResponse
EXPECTED_ROOT_EXPORTS = [
"__version__",
"AppServerConfig",
"CodexConfig",
"Codex",
"AsyncCodex",
"ApprovalMode",
@@ -49,10 +49,10 @@ EXPECTED_ROOT_EXPORTS = [
"SkillInput",
"MentionInput",
"retry_on_overload",
"AppServerError",
"CodexError",
"TransportClosedError",
"JsonRpcError",
"AppServerRpcError",
"CodexRpcError",
"ParseError",
"InvalidRequestError",
"MethodNotFoundError",
@@ -129,9 +129,9 @@ def _assert_no_any_annotations(fn: object) -> None:
raise AssertionError(f"{fn} has public return annotation typed as Any")
def test_root_exports_app_server_config() -> None:
def test_root_exports_codex_config() -> None:
"""The root package should expose the process configuration object."""
assert AppServerConfig.__name__ == "AppServerConfig"
assert CodexConfig.__name__ == "CodexConfig"
def test_root_exports_turn_result() -> None:
@@ -208,7 +208,7 @@ def test_package_and_default_client_versions_follow_project_version() -> None:
pyproject = tomllib.loads(pyproject_path.read_text())
assert openai_codex.__version__ == pyproject["project"]["version"]
assert AppServerConfig().client_version == openai_codex.__version__
assert CodexConfig().client_version == openai_codex.__version__
def test_package_includes_py_typed_marker() -> None:
@@ -224,16 +224,16 @@ def test_package_root_exports_only_public_api() -> None:
EXPECTED_ROOT_EXPORTS, True
)
assert {
"AppServerClient": hasattr(openai_codex, "AppServerClient"),
"AsyncAppServerClient": hasattr(openai_codex, "AsyncAppServerClient"),
"CodexClient": hasattr(openai_codex, "CodexClient"),
"AsyncCodexClient": hasattr(openai_codex, "AsyncCodexClient"),
"InitializeResponse": hasattr(openai_codex, "InitializeResponse"),
"ThreadStartParams": hasattr(openai_codex, "ThreadStartParams"),
"TurnStartParams": hasattr(openai_codex, "TurnStartParams"),
"TurnCompletedNotification": hasattr(openai_codex, "TurnCompletedNotification"),
"TurnStatus": hasattr(openai_codex, "TurnStatus"),
} == {
"AppServerClient": False,
"AsyncAppServerClient": False,
"CodexClient": False,
"AsyncCodexClient": False,
"InitializeResponse": False,
"ThreadStartParams": False,
"TurnStartParams": False,
@@ -252,7 +252,7 @@ def test_package_star_import_matches_public_api() -> None:
def test_types_module_exports_curated_public_types() -> None:
"""The public type module should be the supported place for app-server models."""
"""The public type module should expose Codex protocol models."""
assert public_types.__all__ == EXPECTED_TYPES_EXPORTS
assert {name: hasattr(public_types, name) for name in EXPECTED_TYPES_EXPORTS} == dict.fromkeys(
EXPECTED_TYPES_EXPORTS, True