[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
Unverified
parent 090144e0ec
commit 0db49a7e6a
22 changed files with 127 additions and 126 deletions
+6 -6
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",
+8 -8
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
@@ -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"))
+2 -2
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:
+15 -15
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
+9 -9
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:
+16 -16
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
+12 -12
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)
+1 -1
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