Expose Python SDK history selection and per-turn options (#44084)

## Why

Python callers need control over response history loading and a way to override the service tier for one turn. These options also need runtime compatibility checks to prevent older CLIs from silently ignoring them.

## What changed

- Add `include_turns` to sync and async thread resume/fork methods. Omission preserves server defaults; `False` skips response history loading without changing model context.
- Add `turn_service_tier` and `source` to sync and async `run()` and `turn()`, and generate both methods together to keep their options aligned.
- Require CLI `0.151.0` or newer when sending the new options, with lazy schema checks for unversioned local builds.
- Pin the bundled runtime dependency to `0.153.4` and reject unsupported runtime versions during SDK packaging.

## Testing

Add coverage for option forwarding, history flag omission and inversion, runtime version checks, cached schema probing, and packaging compatibility. Extend app-server and installed SDK smoke tests to exercise the new options.

GitOrigin-RevId: 4bcc9cff687b0651e67852e7c080df7fadac6d76
This commit is contained in:
Ahmed Ibrahim
2026-09-09 06:40:56 +00:00
committed by copyberry
parent 7b9e7d99bd
commit 8afccec87a
14 changed files with 667 additions and 108 deletions
+45
View File
@@ -14,7 +14,9 @@ from typing import Callable, Iterator, TypeVar
from pydantic import BaseModel
from ._goal import _GoalOperationState
from ._initialize_metadata import _split_user_agent
from ._message_router import MessageRouter
from ._runtime_requirements import CheckoutCapabilities, require_runtime_version
from ._version import __version__ as SDK_VERSION
from .errors import CodexError, InvalidRequestError, TransportClosedError
from .generated.notification_registry import NOTIFICATION_MODELS
@@ -227,6 +229,8 @@ class CodexClient:
self._stderr_lines: deque[str] = deque(maxlen=400)
self._stderr_thread: threading.Thread | None = None
self._reader_thread: threading.Thread | None = None
self._runtime_version: str | None = None
self._checkout_capabilities: CheckoutCapabilities | None = None
def __enter__(self) -> "CodexClient":
self.start()
@@ -256,6 +260,11 @@ class CodexClient:
env.update(self.config.env)
_prepend_path_dirs(env, path_dirs)
if self.config.launch_args_override is None:
self._checkout_capabilities = CheckoutCapabilities(
command=tuple(args[:-2]), cwd=self.config.cwd, env=env.copy()
)
self._proc = subprocess.Popen(
args,
stdin=subprocess.PIPE,
@@ -272,6 +281,8 @@ class CodexClient:
self._start_reader_thread()
def close(self) -> None:
self._runtime_version = None
self._checkout_capabilities = None
if self._proc is None:
return
proc = self._proc
@@ -291,6 +302,7 @@ class CodexClient:
self._reader_thread.join(timeout=0.5)
def initialize(self) -> InitializeResponse:
self._runtime_version = None
result = self.request(
"initialize",
{
@@ -305,6 +317,10 @@ class CodexClient:
},
response_model=InitializeResponse,
)
version = result.serverInfo.version if result.serverInfo is not None else None
if not version or not version.strip():
_, version = _split_user_agent(result.userAgent or "")
self._runtime_version = version.split()[0] if version and version.strip() else None
self.notify("initialized", None)
return result
@@ -315,6 +331,35 @@ class CodexClient:
*,
response_model: type[ModelT],
) -> ModelT:
runtime_fields = {
"turn/start": ("turnTrigger", "serviceTierForTurn"),
"thread/resume": ("excludeTurns",),
"thread/fork": ("excludeTurns",),
}
supplied_fields = [
field
for field in runtime_fields.get(method, ())
if (params or {}).get(field) is not None
]
if supplied_fields:
try:
if self._runtime_version == "0.0.0":
if self._checkout_capabilities is None:
raise ValueError(
"Cannot verify an unversioned CLI with a custom launch command"
)
supported = self._checkout_capabilities.fields[method]
if missing := set(supplied_fields) - supported:
raise ValueError(
f"The checkout does not support {', '.join(sorted(missing))}"
)
else:
require_runtime_version(self._runtime_version)
except ValueError as exc:
raise CodexError(
f"{method} with {', '.join(supplied_fields)}: {exc}. "
"Configure CodexConfig.codex_bin with a supported CLI."
) from exc
result = self._request_raw(method, params)
if not isinstance(result, dict):
raise CodexError(f"{method} response must be a JSON object")