mirror of
https://github.com/openai/codex.git
synced 2026-05-28 23:10:20 +00:00
[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:
@@ -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-server’s 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
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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"?
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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": ""}
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"}),
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user