mirror of
https://github.com/openai/codex.git
synced 2026-10-21 05:22:21 +00:00
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:
@@ -67,8 +67,8 @@ Properties/methods:
|
||||
- `logout() -> None`
|
||||
- `thread_start(*, approval_mode=ApprovalMode.auto_review, base_instructions=None, config=None, cwd=None, developer_instructions=None, ephemeral=None, model=None, model_provider=None, personality=None, sandbox: Sandbox | None = None) -> Thread`
|
||||
- `thread_list(*, archived=None, cursor=None, cwd=None, limit=None, model_providers=None, sort_key=None, source_kinds=None) -> ThreadListResponse`
|
||||
- `thread_resume(thread_id: str, *, approval_mode=ApprovalMode.auto_review, base_instructions=None, config=None, cwd=None, developer_instructions=None, model=None, model_provider=None, personality=None, sandbox: Sandbox | None = None) -> Thread`
|
||||
- `thread_fork(thread_id: str, *, approval_mode=ApprovalMode.auto_review, base_instructions=None, config=None, cwd=None, developer_instructions=None, model=None, model_provider=None, sandbox: Sandbox | None = None) -> Thread`
|
||||
- `thread_resume(thread_id: str, *, approval_mode=None, base_instructions=None, config=None, cwd=None, developer_instructions=None, include_turns: bool | None = None, model=None, model_provider=None, personality=None, sandbox: Sandbox | None = None, service_tier=None) -> Thread`
|
||||
- `thread_fork(thread_id: str, *, approval_mode=None, base_instructions=None, config=None, cwd=None, developer_instructions=None, ephemeral=None, include_turns: bool | None = None, model=None, model_provider=None, sandbox: Sandbox | None = None, service_tier=None) -> Thread`
|
||||
- `thread_archive(thread_id: str) -> ThreadArchiveResponse`
|
||||
- `thread_unarchive(thread_id: str) -> Thread`
|
||||
- `models(*, include_hidden: bool = False) -> ModelListResponse`
|
||||
@@ -80,6 +80,13 @@ with Codex() as codex:
|
||||
...
|
||||
```
|
||||
|
||||
`thread_resume(...)` and `thread_fork(...)` accept `include_turns` to control
|
||||
whether the server loads turn history into its response. `False` skips that
|
||||
work; `True` requests it. Omitting the option, or passing `None`, preserves the
|
||||
server's default behavior. This does not remove history from the model's
|
||||
context. Both methods return a thread handle; use `thread.read(include_turns=True)`
|
||||
to retrieve its history.
|
||||
|
||||
## AsyncCodex (async parity)
|
||||
|
||||
```python
|
||||
@@ -107,8 +114,8 @@ Properties/methods:
|
||||
- `logout() -> Awaitable[None]`
|
||||
- `thread_start(*, approval_mode=ApprovalMode.auto_review, base_instructions=None, config=None, cwd=None, developer_instructions=None, ephemeral=None, model=None, model_provider=None, personality=None, sandbox: Sandbox | None = None) -> Awaitable[AsyncThread]`
|
||||
- `thread_list(*, archived=None, cursor=None, cwd=None, limit=None, model_providers=None, sort_key=None, source_kinds=None) -> Awaitable[ThreadListResponse]`
|
||||
- `thread_resume(thread_id: str, *, approval_mode=ApprovalMode.auto_review, base_instructions=None, config=None, cwd=None, developer_instructions=None, model=None, model_provider=None, personality=None, sandbox: Sandbox | None = None) -> Awaitable[AsyncThread]`
|
||||
- `thread_fork(thread_id: str, *, approval_mode=ApprovalMode.auto_review, base_instructions=None, config=None, cwd=None, developer_instructions=None, ephemeral=None, model=None, model_provider=None, sandbox: Sandbox | None = None) -> Awaitable[AsyncThread]`
|
||||
- `thread_resume(thread_id: str, *, approval_mode=None, base_instructions=None, config=None, cwd=None, developer_instructions=None, include_turns: bool | None = None, model=None, model_provider=None, personality=None, sandbox: Sandbox | None = None, service_tier=None) -> Awaitable[AsyncThread]`
|
||||
- `thread_fork(thread_id: str, *, approval_mode=None, base_instructions=None, config=None, cwd=None, developer_instructions=None, ephemeral=None, include_turns: bool | None = None, model=None, model_provider=None, sandbox: Sandbox | None = None, service_tier=None) -> Awaitable[AsyncThread]`
|
||||
- `thread_archive(thread_id: str) -> Awaitable[ThreadArchiveResponse]`
|
||||
- `thread_unarchive(thread_id: str) -> Awaitable[AsyncThread]`
|
||||
- `models(*, include_hidden: bool = False) -> Awaitable[ModelListResponse]`
|
||||
@@ -150,23 +157,23 @@ attempt. API-key login completes synchronously and does not return a handle.
|
||||
|
||||
### Thread
|
||||
|
||||
- `run(input: str | Input, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox: Sandbox | None = None, service_tier=None, summary=None) -> TurnResult`
|
||||
- `turn(input: str | Input, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox: Sandbox | None = None, service_tier=None, summary=None) -> TurnHandle`
|
||||
- `run(input: RunInput, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox: Sandbox | None = None, service_tier=None, source=None, summary=None, turn_service_tier=None) -> TurnResult`
|
||||
- `turn(input: RunInput, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox: Sandbox | None = None, service_tier=None, source=None, summary=None, turn_service_tier=None) -> TurnHandle`
|
||||
- `read(*, include_turns: bool = False) -> ThreadReadResponse`
|
||||
- `set_name(name: str) -> ThreadSetNameResponse`
|
||||
- `compact() -> ThreadCompactStartResponse`
|
||||
|
||||
### AsyncThread
|
||||
|
||||
- `run(input: str | Input, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox: Sandbox | None = None, service_tier=None, summary=None) -> Awaitable[TurnResult]`
|
||||
- `turn(input: str | Input, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox: Sandbox | None = None, service_tier=None, summary=None) -> Awaitable[AsyncTurnHandle]`
|
||||
- `run(input: RunInput, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox: Sandbox | None = None, service_tier=None, source=None, summary=None, turn_service_tier=None) -> Awaitable[TurnResult]`
|
||||
- `turn(input: RunInput, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox: Sandbox | None = None, service_tier=None, source=None, summary=None, turn_service_tier=None) -> Awaitable[AsyncTurnHandle]`
|
||||
- `read(*, include_turns: bool = False) -> Awaitable[ThreadReadResponse]`
|
||||
- `set_name(name: str) -> Awaitable[ThreadSetNameResponse]`
|
||||
- `compact() -> Awaitable[ThreadCompactStartResponse]`
|
||||
|
||||
`run(...)` is the common-case convenience path. It accepts plain strings, starts
|
||||
the turn, consumes notifications until completion, and returns a small result
|
||||
object with:
|
||||
`run(...)` is the common-case convenience path. It accepts the same input and
|
||||
options as `turn(...)`, consumes notifications until completion, and returns a
|
||||
small result object with:
|
||||
|
||||
- `id: str`
|
||||
- `status: TurnStatus`
|
||||
@@ -184,6 +191,24 @@ phase-less assistant message item.
|
||||
Use `turn(...)` when you need low-level turn control (`stream()`, `steer()`,
|
||||
`interrupt()`) before collecting the turn result.
|
||||
|
||||
### Turn options
|
||||
|
||||
These options have the same behavior on sync and async `run(...)` and `turn(...)`:
|
||||
|
||||
| Option | Behavior |
|
||||
| --- | --- |
|
||||
| `service_tier: str | None = None` | Sets the thread's service tier for this and subsequent turns. |
|
||||
| `turn_service_tier: str | None = None` | Overrides the tier for a newly started turn only. `None` inherits the thread setting; `"default"` selects standard speed. Does not change the thread default and is ignored when input joins an active turn. |
|
||||
| `source: str | None = None` | Labels the caller that initiated a new turn, such as `"review_ui"`. This is metadata; it does not schedule work or grant authority. Ignored when input joins an active turn. |
|
||||
|
||||
`turn_service_tier`, `source`, and explicit `include_turns`
|
||||
on resume/fork require Codex CLI 0.151.0 or newer. The SDK raises `CodexError`
|
||||
before sending these options to an older runtime, which would otherwise ignore
|
||||
them. Published SDK releases install a matching runtime automatically; when
|
||||
using `CodexConfig.codex_bin`, choose a compatible executable. Unversioned local
|
||||
builds are checked lazily against their experimental schema before these options
|
||||
are sent. A custom `launch_args_override` must report a supported version.
|
||||
|
||||
## Sandbox
|
||||
|
||||
Use `sandbox=` consistently on thread lifecycle methods and turns:
|
||||
|
||||
@@ -37,6 +37,22 @@ Choose `run()` for most apps. Choose `stream()` for progress UIs, custom timeout
|
||||
|
||||
If your app is not already async, stay with `Codex`.
|
||||
|
||||
## Does `include_turns=False` remove the conversation's context?
|
||||
|
||||
No. On `thread_resume(...)` and `thread_fork(...)`, it only skips loading turn
|
||||
history into the server's response. Omitting it preserves the server's
|
||||
existing default. Retrieve saved history with `thread.read(include_turns=True)`.
|
||||
|
||||
## How do I change the service tier for just one turn?
|
||||
|
||||
Pass `turn_service_tier=` to `thread.run(...)` or `thread.turn(...)`.
|
||||
`None` inherits the thread setting, and `"default"` selects standard speed.
|
||||
The override applies only when starting a new turn. Use `service_tier=` when
|
||||
you want to change the thread's setting for subsequent turns too.
|
||||
|
||||
`source=` on those methods only labels what initiated the turn. It does not
|
||||
schedule work or grant authority, and it is ignored when joining an active turn.
|
||||
|
||||
## How do I log in?
|
||||
|
||||
- `login_api_key(...)` authenticates immediately with an API key.
|
||||
|
||||
@@ -16,7 +16,7 @@ classifiers = [
|
||||
"Intended Audience :: Developers",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
]
|
||||
dependencies = ["pydantic>=2.12", "openai-codex-cli-bin==0.147.0"]
|
||||
dependencies = ["pydantic>=2.12", "packaging>=26.2", "openai-codex-cli-bin==0.153.4"]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/openai/codex"
|
||||
@@ -68,10 +68,10 @@ combine-as-imports = true
|
||||
|
||||
[tool.uv]
|
||||
exclude-newer = "7 days"
|
||||
exclude-newer-package = { openai-codex-cli-bin = "2026-08-19T00:00:00Z" }
|
||||
exclude-newer-package = { openai-codex-cli-bin = "2026-09-09T06:06:45Z" }
|
||||
index-strategy = "first-index"
|
||||
|
||||
[tool.uv.pip]
|
||||
exclude-newer = "7 days"
|
||||
exclude-newer-package = { openai-codex-cli-bin = "2026-08-19T00:00:00Z" }
|
||||
exclude-newer-package = { openai-codex-cli-bin = "2026-09-09T06:06:45Z" }
|
||||
index-strategy = "first-index"
|
||||
|
||||
@@ -5,6 +5,7 @@ import importlib.util
|
||||
import json
|
||||
import platform
|
||||
import re
|
||||
import runpy
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -175,6 +176,11 @@ def stage_python_sdk_package(
|
||||
)
|
||||
if len(runtime_versions) != 1:
|
||||
raise RuntimeError("Expected exactly one pinned Codex runtime dependency")
|
||||
requirements = runpy.run_path(sdk_root() / "src/openai_codex/_runtime_requirements.py")
|
||||
try:
|
||||
requirements["require_runtime_version"](runtime_versions[0])
|
||||
except ValueError as exc:
|
||||
raise RuntimeError(f"Cannot package the Python SDK: {exc}") from exc
|
||||
pyproject_path.write_text(pyproject_text)
|
||||
return staging_dir
|
||||
|
||||
@@ -834,7 +840,10 @@ FIELD_ANNOTATION_OVERRIDES: dict[str, str] = {
|
||||
}
|
||||
|
||||
PUBLIC_FIELD_NAMES = {
|
||||
"exclude_turns": "include_turns",
|
||||
"sandbox_policy": "sandbox",
|
||||
"service_tier_for_turn": "turn_service_tier",
|
||||
"turn_trigger": "source",
|
||||
}
|
||||
|
||||
# Adding a protocol field must not silently add a public SDK parameter. These
|
||||
@@ -873,6 +882,7 @@ PUBLIC_METHOD_FIELDS = {
|
||||
"config",
|
||||
"cwd",
|
||||
"developer_instructions",
|
||||
"exclude_turns",
|
||||
"model",
|
||||
"model_provider",
|
||||
"personality",
|
||||
@@ -885,6 +895,7 @@ PUBLIC_METHOD_FIELDS = {
|
||||
"cwd",
|
||||
"developer_instructions",
|
||||
"ephemeral",
|
||||
"exclude_turns",
|
||||
"model",
|
||||
"model_provider",
|
||||
"sandbox",
|
||||
@@ -899,7 +910,9 @@ PUBLIC_METHOD_FIELDS = {
|
||||
"personality",
|
||||
"sandbox_policy",
|
||||
"service_tier",
|
||||
"service_tier_for_turn",
|
||||
"summary",
|
||||
"turn_trigger",
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1033,6 +1046,8 @@ def _model_arg_lines(fields: list[PublicFieldSpec], *, indent: str = "
|
||||
arg = "_sandbox_mode(sandbox)"
|
||||
elif field.wire_name == "sandbox_policy":
|
||||
arg = "_sandbox_policy(sandbox)"
|
||||
elif field.wire_name == "exclude_turns":
|
||||
arg = "None if include_turns is None else not include_turns"
|
||||
lines.append(f"{indent}{field.wire_name}={arg},")
|
||||
return lines
|
||||
|
||||
@@ -1088,7 +1103,11 @@ def _render_codex_block(
|
||||
*_approval_mode_override_signature_lines(),
|
||||
*_kw_signature_lines(resume_fields),
|
||||
" ) -> Thread:",
|
||||
' """Resume an existing conversation thread by ID."""',
|
||||
' """Resume an existing conversation thread by ID.',
|
||||
"",
|
||||
" include_turns controls the runtime response history, not model context.",
|
||||
" Omit it to preserve the runtime default. Use thread.read() for history.",
|
||||
' """',
|
||||
_approval_mode_assignment_line("_approval_mode_override_settings"),
|
||||
" params = ThreadResumeParams(",
|
||||
" thread_id=thread_id,",
|
||||
@@ -1105,7 +1124,11 @@ def _render_codex_block(
|
||||
*_approval_mode_override_signature_lines(),
|
||||
*_kw_signature_lines(fork_fields),
|
||||
" ) -> Thread:",
|
||||
' """Create a new thread from an existing thread."""',
|
||||
' """Create a new thread from an existing thread.',
|
||||
"",
|
||||
" include_turns controls the runtime response history, not model context.",
|
||||
" Omit it to preserve the runtime default. Use thread.read() for history.",
|
||||
' """',
|
||||
_approval_mode_assignment_line("_approval_mode_override_settings"),
|
||||
" params = ThreadForkParams(",
|
||||
" thread_id=thread_id,",
|
||||
@@ -1169,7 +1192,11 @@ def _render_async_codex_block(
|
||||
*_approval_mode_override_signature_lines(),
|
||||
*_kw_signature_lines(resume_fields),
|
||||
" ) -> AsyncThread:",
|
||||
' """Resume an existing conversation thread by ID."""',
|
||||
' """Resume an existing conversation thread by ID.',
|
||||
"",
|
||||
" include_turns controls the runtime response history, not model context.",
|
||||
" Omit it to preserve the runtime default. Use thread.read() for history.",
|
||||
' """',
|
||||
" await self._ensure_initialized()",
|
||||
_approval_mode_assignment_line("_approval_mode_override_settings"),
|
||||
" params = ThreadResumeParams(",
|
||||
@@ -1187,7 +1214,11 @@ def _render_async_codex_block(
|
||||
*_approval_mode_override_signature_lines(),
|
||||
*_kw_signature_lines(fork_fields),
|
||||
" ) -> AsyncThread:",
|
||||
' """Create a new thread from an existing thread."""',
|
||||
' """Create a new thread from an existing thread.',
|
||||
"",
|
||||
" include_turns controls the runtime response history, not model context.",
|
||||
" Omit it to preserve the runtime default. Use thread.read() for history.",
|
||||
' """',
|
||||
" await self._ensure_initialized()",
|
||||
_approval_mode_assignment_line("_approval_mode_override_settings"),
|
||||
" params = ThreadForkParams(",
|
||||
@@ -1212,19 +1243,46 @@ def _render_async_codex_block(
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _render_thread_block(
|
||||
turn_fields: list[PublicFieldSpec],
|
||||
) -> str:
|
||||
def _render_thread_block(turn_fields: list[PublicFieldSpec], *, is_async: bool = False) -> str:
|
||||
async_prefix = "async " if is_async else ""
|
||||
await_prefix = "await " if is_async else ""
|
||||
client = "self._codex._client" if is_async else "self._client"
|
||||
handle_type = "AsyncTurnHandle" if is_async else "TurnHandle"
|
||||
handle_owner = "self._codex" if is_async else "self._client"
|
||||
lines = [
|
||||
" def turn(",
|
||||
f" {async_prefix}def run(",
|
||||
" self,",
|
||||
" input: RunInput,",
|
||||
" *,",
|
||||
*_approval_mode_override_signature_lines(),
|
||||
*_kw_signature_lines(turn_fields),
|
||||
" ) -> TurnHandle:",
|
||||
' """Start a turn and return a handle for streaming or control."""',
|
||||
" ) -> TurnResult:",
|
||||
' """Run a complete turn and collect its final result.',
|
||||
"",
|
||||
" Accepts the same input and options as turn().",
|
||||
' """',
|
||||
f" turn = {await_prefix}self.turn(",
|
||||
" input,",
|
||||
" approval_mode=approval_mode,",
|
||||
*[f" {field.py_name}={field.py_name}," for field in turn_fields],
|
||||
" )",
|
||||
f" return {await_prefix}turn.run()",
|
||||
"",
|
||||
f" {async_prefix}def turn(",
|
||||
" self,",
|
||||
" input: RunInput,",
|
||||
" *,",
|
||||
*_approval_mode_override_signature_lines(),
|
||||
*_kw_signature_lines(turn_fields),
|
||||
f" ) -> {handle_type}:",
|
||||
' """Start a turn and return a handle for streaming or control.',
|
||||
"",
|
||||
" turn_service_tier applies only to this new turn; service_tier updates",
|
||||
" the thread default. source labels what initiated a new turn and grants",
|
||||
" no authority. Both turn_service_tier and source are ignored when joining.",
|
||||
' """',
|
||||
" wire_input = _to_wire_input(_normalize_run_input(input))",
|
||||
*([" await self._codex._ensure_initialized()"] if is_async else []),
|
||||
_approval_mode_assignment_line("_approval_mode_override_settings"),
|
||||
" params = TurnStartParams(",
|
||||
" thread_id=self.id,",
|
||||
@@ -1232,39 +1290,8 @@ def _render_thread_block(
|
||||
*_approval_mode_model_arg_lines(),
|
||||
*_model_arg_lines(turn_fields),
|
||||
" )",
|
||||
" turn = self._client.turn_start(self.id, wire_input, params=params)",
|
||||
" return TurnHandle(self._client, self.id, turn.turn.id)",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _render_async_thread_block(
|
||||
turn_fields: list[PublicFieldSpec],
|
||||
) -> str:
|
||||
lines = [
|
||||
" async def turn(",
|
||||
" self,",
|
||||
" input: RunInput,",
|
||||
" *,",
|
||||
*_approval_mode_override_signature_lines(),
|
||||
*_kw_signature_lines(turn_fields),
|
||||
" ) -> AsyncTurnHandle:",
|
||||
' """Start a turn and return a handle for streaming or control."""',
|
||||
" await self._codex._ensure_initialized()",
|
||||
" wire_input = _to_wire_input(_normalize_run_input(input))",
|
||||
_approval_mode_assignment_line("_approval_mode_override_settings"),
|
||||
" params = TurnStartParams(",
|
||||
" thread_id=self.id,",
|
||||
" input=wire_input,",
|
||||
*_approval_mode_model_arg_lines(),
|
||||
*_model_arg_lines(turn_fields),
|
||||
" )",
|
||||
" turn = await self._codex._client.turn_start(",
|
||||
" self.id,",
|
||||
" wire_input,",
|
||||
" params=params,",
|
||||
" )",
|
||||
" return AsyncTurnHandle(self._codex, self.id, turn.turn.id)",
|
||||
f" turn = {await_prefix}{client}.turn_start(self.id, wire_input, params=params)",
|
||||
f" return {handle_type}({handle_owner}, self.id, turn.turn.id)",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
@@ -1315,7 +1342,7 @@ def generate_public_api_flat_methods() -> None:
|
||||
source = _replace_generated_block(
|
||||
source,
|
||||
"AsyncThread.flat_methods",
|
||||
_render_async_thread_block(turn_start_fields),
|
||||
_render_thread_block(turn_start_fields, is_async=True),
|
||||
)
|
||||
public_api_path.write_text(source)
|
||||
run_python_module("ruff", ["format", str(public_api_path)], cwd=sdk_root())
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Runtime version and checkout-schema requirements for newer SDK options."""
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from packaging.version import InvalidVersion, Version
|
||||
|
||||
MINIMUM_RUNTIME_VERSION = "0.151.0"
|
||||
|
||||
|
||||
def require_runtime_version(version: str | None) -> None:
|
||||
"""Reject unknown or unsupported versions, including prereleases at the minimum."""
|
||||
# CLI alpha hotfixes use 0.154.0-alpha.1.2; PEP 440 spells that a1.post2.
|
||||
normalized = re.sub(r"-alpha\.(\d+)\.(\d+)$", r"a\1.post\2", version or "")
|
||||
try:
|
||||
if Version(normalized) >= Version(MINIMUM_RUNTIME_VERSION):
|
||||
return
|
||||
except InvalidVersion:
|
||||
pass
|
||||
raise ValueError(
|
||||
f"Codex CLI {MINIMUM_RUNTIME_VERSION} or newer is required; "
|
||||
f"reported version is {version or 'unknown'!r}"
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CheckoutCapabilities:
|
||||
"""Lazily inspect the same executable and configuration as a running checkout."""
|
||||
|
||||
command: tuple[str, ...]
|
||||
cwd: str | None
|
||||
env: dict[str, str]
|
||||
|
||||
@cached_property
|
||||
def fields(self) -> dict[str, frozenset[str]]:
|
||||
try:
|
||||
with TemporaryDirectory(prefix="codex-sdk-schema-") as directory:
|
||||
subprocess.run(
|
||||
[*self.command, "generate-json-schema", "--experimental", "--out", directory],
|
||||
cwd=self.cwd,
|
||||
env=self.env,
|
||||
capture_output=True,
|
||||
check=True,
|
||||
timeout=30,
|
||||
)
|
||||
result = {}
|
||||
for method, name in (
|
||||
("turn/start", "TurnStartParams"),
|
||||
("thread/resume", "ThreadResumeParams"),
|
||||
("thread/fork", "ThreadForkParams"),
|
||||
):
|
||||
schema = json.loads((Path(directory) / "v2" / f"{name}.json").read_text())
|
||||
properties = schema.get("properties") if isinstance(schema, dict) else None
|
||||
if not isinstance(properties, dict):
|
||||
raise ValueError(f"Missing properties in {name} schema")
|
||||
result[method] = frozenset(properties)
|
||||
return result
|
||||
except (OSError, subprocess.SubprocessError, ValueError) as exc:
|
||||
raise ValueError("Could not inspect the unversioned CLI's experimental schema") from exc
|
||||
@@ -209,13 +209,18 @@ class Codex:
|
||||
config: JsonObject | None = None,
|
||||
cwd: str | None = None,
|
||||
developer_instructions: str | None = None,
|
||||
include_turns: bool | None = None,
|
||||
model: str | None = None,
|
||||
model_provider: str | None = None,
|
||||
personality: Personality | None = None,
|
||||
sandbox: Sandbox | None = None,
|
||||
service_tier: str | None = None,
|
||||
) -> Thread:
|
||||
"""Resume an existing conversation thread by ID."""
|
||||
"""Resume an existing conversation thread by ID.
|
||||
|
||||
include_turns controls the runtime response history, not model context.
|
||||
Omit it to preserve the runtime default. Use thread.read() for history.
|
||||
"""
|
||||
approval_policy, approvals_reviewer = _approval_mode_override_settings(approval_mode)
|
||||
params = ThreadResumeParams(
|
||||
thread_id=thread_id,
|
||||
@@ -225,6 +230,7 @@ class Codex:
|
||||
config=config,
|
||||
cwd=cwd,
|
||||
developer_instructions=developer_instructions,
|
||||
exclude_turns=None if include_turns is None else not include_turns,
|
||||
model=model,
|
||||
model_provider=model_provider,
|
||||
personality=personality,
|
||||
@@ -244,13 +250,18 @@ class Codex:
|
||||
cwd: str | None = None,
|
||||
developer_instructions: str | None = None,
|
||||
ephemeral: bool | None = None,
|
||||
include_turns: bool | None = None,
|
||||
model: str | None = None,
|
||||
model_provider: str | None = None,
|
||||
sandbox: Sandbox | None = None,
|
||||
service_tier: str | None = None,
|
||||
thread_source: ThreadSource | None = None,
|
||||
) -> Thread:
|
||||
"""Create a new thread from an existing thread."""
|
||||
"""Create a new thread from an existing thread.
|
||||
|
||||
include_turns controls the runtime response history, not model context.
|
||||
Omit it to preserve the runtime default. Use thread.read() for history.
|
||||
"""
|
||||
approval_policy, approvals_reviewer = _approval_mode_override_settings(approval_mode)
|
||||
params = ThreadForkParams(
|
||||
thread_id=thread_id,
|
||||
@@ -261,6 +272,7 @@ class Codex:
|
||||
cwd=cwd,
|
||||
developer_instructions=developer_instructions,
|
||||
ephemeral=ephemeral,
|
||||
exclude_turns=None if include_turns is None else not include_turns,
|
||||
model=model,
|
||||
model_provider=model_provider,
|
||||
sandbox=_sandbox_mode(sandbox),
|
||||
@@ -453,13 +465,18 @@ class AsyncCodex:
|
||||
config: JsonObject | None = None,
|
||||
cwd: str | None = None,
|
||||
developer_instructions: str | None = None,
|
||||
include_turns: bool | None = None,
|
||||
model: str | None = None,
|
||||
model_provider: str | None = None,
|
||||
personality: Personality | None = None,
|
||||
sandbox: Sandbox | None = None,
|
||||
service_tier: str | None = None,
|
||||
) -> AsyncThread:
|
||||
"""Resume an existing conversation thread by ID."""
|
||||
"""Resume an existing conversation thread by ID.
|
||||
|
||||
include_turns controls the runtime response history, not model context.
|
||||
Omit it to preserve the runtime default. Use thread.read() for history.
|
||||
"""
|
||||
await self._ensure_initialized()
|
||||
approval_policy, approvals_reviewer = _approval_mode_override_settings(approval_mode)
|
||||
params = ThreadResumeParams(
|
||||
@@ -470,6 +487,7 @@ class AsyncCodex:
|
||||
config=config,
|
||||
cwd=cwd,
|
||||
developer_instructions=developer_instructions,
|
||||
exclude_turns=None if include_turns is None else not include_turns,
|
||||
model=model,
|
||||
model_provider=model_provider,
|
||||
personality=personality,
|
||||
@@ -489,13 +507,18 @@ class AsyncCodex:
|
||||
cwd: str | None = None,
|
||||
developer_instructions: str | None = None,
|
||||
ephemeral: bool | None = None,
|
||||
include_turns: bool | None = None,
|
||||
model: str | None = None,
|
||||
model_provider: str | None = None,
|
||||
sandbox: Sandbox | None = None,
|
||||
service_tier: str | None = None,
|
||||
thread_source: ThreadSource | None = None,
|
||||
) -> AsyncThread:
|
||||
"""Create a new thread from an existing thread."""
|
||||
"""Create a new thread from an existing thread.
|
||||
|
||||
include_turns controls the runtime response history, not model context.
|
||||
Omit it to preserve the runtime default. Use thread.read() for history.
|
||||
"""
|
||||
await self._ensure_initialized()
|
||||
approval_policy, approvals_reviewer = _approval_mode_override_settings(approval_mode)
|
||||
params = ThreadForkParams(
|
||||
@@ -507,6 +530,7 @@ class AsyncCodex:
|
||||
cwd=cwd,
|
||||
developer_instructions=developer_instructions,
|
||||
ephemeral=ephemeral,
|
||||
exclude_turns=None if include_turns is None else not include_turns,
|
||||
model=model,
|
||||
model_provider=model_provider,
|
||||
sandbox=_sandbox_mode(sandbox),
|
||||
@@ -541,6 +565,7 @@ class Thread:
|
||||
_client: CodexClient
|
||||
id: str
|
||||
|
||||
# BEGIN GENERATED: Thread.flat_methods
|
||||
def run(
|
||||
self,
|
||||
input: RunInput,
|
||||
@@ -553,9 +578,14 @@ class Thread:
|
||||
personality: Personality | None = None,
|
||||
sandbox: Sandbox | None = None,
|
||||
service_tier: str | None = None,
|
||||
source: str | None = None,
|
||||
summary: ReasoningSummary | None = None,
|
||||
turn_service_tier: str | None = None,
|
||||
) -> TurnResult:
|
||||
"""Run a complete turn and collect its final result."""
|
||||
"""Run a complete turn and collect its final result.
|
||||
|
||||
Accepts the same input and options as turn().
|
||||
"""
|
||||
turn = self.turn(
|
||||
input,
|
||||
approval_mode=approval_mode,
|
||||
@@ -566,15 +596,12 @@ class Thread:
|
||||
personality=personality,
|
||||
sandbox=sandbox,
|
||||
service_tier=service_tier,
|
||||
source=source,
|
||||
summary=summary,
|
||||
turn_service_tier=turn_service_tier,
|
||||
)
|
||||
stream = turn.stream()
|
||||
try:
|
||||
return _collect_turn_result(stream, turn_id=turn.id)
|
||||
finally:
|
||||
stream.close()
|
||||
return turn.run()
|
||||
|
||||
# BEGIN GENERATED: Thread.flat_methods
|
||||
def turn(
|
||||
self,
|
||||
input: RunInput,
|
||||
@@ -587,9 +614,16 @@ class Thread:
|
||||
personality: Personality | None = None,
|
||||
sandbox: Sandbox | None = None,
|
||||
service_tier: str | None = None,
|
||||
source: str | None = None,
|
||||
summary: ReasoningSummary | None = None,
|
||||
turn_service_tier: str | None = None,
|
||||
) -> TurnHandle:
|
||||
"""Start a turn and return a handle for streaming or control."""
|
||||
"""Start a turn and return a handle for streaming or control.
|
||||
|
||||
turn_service_tier applies only to this new turn; service_tier updates
|
||||
the thread default. source labels what initiated a new turn and grants
|
||||
no authority. Both turn_service_tier and source are ignored when joining.
|
||||
"""
|
||||
wire_input = _to_wire_input(_normalize_run_input(input))
|
||||
approval_policy, approvals_reviewer = _approval_mode_override_settings(approval_mode)
|
||||
params = TurnStartParams(
|
||||
@@ -604,7 +638,9 @@ class Thread:
|
||||
personality=personality,
|
||||
sandbox_policy=_sandbox_policy(sandbox),
|
||||
service_tier=service_tier,
|
||||
turn_trigger=source,
|
||||
summary=summary,
|
||||
service_tier_for_turn=turn_service_tier,
|
||||
)
|
||||
turn = self._client.turn_start(self.id, wire_input, params=params)
|
||||
return TurnHandle(self._client, self.id, turn.turn.id)
|
||||
@@ -629,6 +665,7 @@ class AsyncThread:
|
||||
_codex: AsyncCodex
|
||||
id: str
|
||||
|
||||
# BEGIN GENERATED: AsyncThread.flat_methods
|
||||
async def run(
|
||||
self,
|
||||
input: RunInput,
|
||||
@@ -641,9 +678,14 @@ class AsyncThread:
|
||||
personality: Personality | None = None,
|
||||
sandbox: Sandbox | None = None,
|
||||
service_tier: str | None = None,
|
||||
source: str | None = None,
|
||||
summary: ReasoningSummary | None = None,
|
||||
turn_service_tier: str | None = None,
|
||||
) -> TurnResult:
|
||||
"""Run a complete turn asynchronously and collect its final result."""
|
||||
"""Run a complete turn and collect its final result.
|
||||
|
||||
Accepts the same input and options as turn().
|
||||
"""
|
||||
turn = await self.turn(
|
||||
input,
|
||||
approval_mode=approval_mode,
|
||||
@@ -654,15 +696,12 @@ class AsyncThread:
|
||||
personality=personality,
|
||||
sandbox=sandbox,
|
||||
service_tier=service_tier,
|
||||
source=source,
|
||||
summary=summary,
|
||||
turn_service_tier=turn_service_tier,
|
||||
)
|
||||
stream = turn.stream()
|
||||
try:
|
||||
return await _collect_async_turn_result(stream, turn_id=turn.id)
|
||||
finally:
|
||||
await stream.aclose()
|
||||
return await turn.run()
|
||||
|
||||
# BEGIN GENERATED: AsyncThread.flat_methods
|
||||
async def turn(
|
||||
self,
|
||||
input: RunInput,
|
||||
@@ -675,11 +714,18 @@ class AsyncThread:
|
||||
personality: Personality | None = None,
|
||||
sandbox: Sandbox | None = None,
|
||||
service_tier: str | None = None,
|
||||
source: str | None = None,
|
||||
summary: ReasoningSummary | None = None,
|
||||
turn_service_tier: str | None = None,
|
||||
) -> AsyncTurnHandle:
|
||||
"""Start a turn and return a handle for streaming or control."""
|
||||
await self._codex._ensure_initialized()
|
||||
"""Start a turn and return a handle for streaming or control.
|
||||
|
||||
turn_service_tier applies only to this new turn; service_tier updates
|
||||
the thread default. source labels what initiated a new turn and grants
|
||||
no authority. Both turn_service_tier and source are ignored when joining.
|
||||
"""
|
||||
wire_input = _to_wire_input(_normalize_run_input(input))
|
||||
await self._codex._ensure_initialized()
|
||||
approval_policy, approvals_reviewer = _approval_mode_override_settings(approval_mode)
|
||||
params = TurnStartParams(
|
||||
thread_id=self.id,
|
||||
@@ -693,13 +739,11 @@ class AsyncThread:
|
||||
personality=personality,
|
||||
sandbox_policy=_sandbox_policy(sandbox),
|
||||
service_tier=service_tier,
|
||||
turn_trigger=source,
|
||||
summary=summary,
|
||||
service_tier_for_turn=turn_service_tier,
|
||||
)
|
||||
turn = await self._codex._client.turn_start(
|
||||
self.id,
|
||||
wire_input,
|
||||
params=params,
|
||||
)
|
||||
turn = await self._codex._client.turn_start(self.id, wire_input, params=params)
|
||||
return AsyncTurnHandle(self._codex, self.id, turn.turn.id)
|
||||
|
||||
# END GENERATED: AsyncThread.flat_methods
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -21,7 +21,12 @@ def main() -> None:
|
||||
harness.responses.enqueue_assistant_message("Installed SDK works")
|
||||
config = replace(harness.app_server_config(), codex_bin=None)
|
||||
with Codex(config=config) as codex:
|
||||
result = codex.thread_start().run("Check the installed SDK")
|
||||
thread = codex.thread_start()
|
||||
result = thread.run(
|
||||
"Check the installed SDK", turn_service_tier="default", source="automation"
|
||||
)
|
||||
codex.thread_resume(thread.id, include_turns=False)
|
||||
codex.thread_fork(thread.id, include_turns=True)
|
||||
assert result.final_response == "Installed SDK works"
|
||||
assert harness.responses.single_request().message_input_texts("user")[-1:] == [
|
||||
"Check the installed SDK"
|
||||
|
||||
@@ -52,6 +52,20 @@ def test_sync_thread_run_uses_mock_responses(
|
||||
}
|
||||
|
||||
|
||||
def test_checkout_supports_new_options_and_history_selection(tmp_path) -> None:
|
||||
with AppServerHarness(tmp_path) as harness:
|
||||
harness.responses.enqueue_assistant_message("Options supported")
|
||||
with Codex(config=harness.app_server_config()) as codex:
|
||||
thread = codex.thread_start()
|
||||
result = thread.run("hello", turn_service_tier="default", source="automation")
|
||||
resumed = codex.thread_resume(thread.id, include_turns=False)
|
||||
forked = codex.thread_fork(thread.id, include_turns=True)
|
||||
assert result.final_response == "Options supported"
|
||||
assert resumed.id == thread.id
|
||||
assert forked.id != thread.id
|
||||
assert harness.responses.single_request().message_input_texts("user")[-1:] == ["hello"]
|
||||
|
||||
|
||||
def test_run_params_and_usage_cross_app_server_boundary(tmp_path) -> None:
|
||||
"""Thread.run should pass overrides and collect app-server token usage."""
|
||||
with AppServerHarness(tmp_path) as harness:
|
||||
|
||||
@@ -637,7 +637,7 @@ def test_runtime_setup_reads_independent_runtime_pin_and_release_tags() -> None:
|
||||
} == {
|
||||
"package_name": "openai-codex-cli-bin",
|
||||
"sdk_template_version": "0.0.0-dev",
|
||||
"runtime_pin": "0.147.0",
|
||||
"runtime_pin": "0.153.4",
|
||||
"normalized_release_version": "0.116.0a1",
|
||||
"normalized_alpha_hotfix_version": "0.116.0a1.post2",
|
||||
"release_tag": "rust-v0.116.0-alpha.1",
|
||||
@@ -955,17 +955,24 @@ def test_stage_sdk_release_packages_reviewed_artifacts(
|
||||
assert not any((staged / "src" / "openai_codex").glob("bin/**"))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("source_runtime", ["0.147.0", "0.153.0"])
|
||||
@pytest.mark.parametrize("sdk_version", ["0.154.0", "0.2.0b1"])
|
||||
def test_built_sdk_uses_explicit_release_versions(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, sdk_release_source: Path, sdk_version: str
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
sdk_release_source: Path,
|
||||
sdk_version: str,
|
||||
source_runtime: str,
|
||||
) -> None:
|
||||
script = _load_update_script_module()
|
||||
monkeypatch.setattr(script, "sdk_root", lambda: sdk_release_source)
|
||||
source_project = (sdk_release_source / "pyproject.toml").read_bytes()
|
||||
project_path = sdk_release_source / "pyproject.toml"
|
||||
project_path.write_text(project_path.read_text().replace("==0.153.0", f"=={source_runtime}"))
|
||||
source_project = project_path.read_bytes()
|
||||
expected_dependencies = {
|
||||
*tomllib.loads(source_project.decode())["project"]["dependencies"],
|
||||
"openai-codex-cli-bin==0.154.0",
|
||||
} - {"openai-codex-cli-bin==0.153.0"}
|
||||
} - {f"openai-codex-cli-bin=={source_runtime}"}
|
||||
reviewed_files = {
|
||||
path: (sdk_release_source / path).read_bytes()
|
||||
for path in (
|
||||
@@ -1058,11 +1065,52 @@ def test_sdk_release_matches_stable_runtime(
|
||||
"runtime_version": "0.153.0",
|
||||
"sdk_dependencies": [
|
||||
"pydantic>=2.12",
|
||||
"packaging>=26.2",
|
||||
"openai-codex-cli-bin==0.153.0",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("runtime_version", ["0.149.0", "0.151.0a1", "0.0.0", "unknown"])
|
||||
def test_sdk_release_rejects_unsupported_runtime_even_for_beta(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
sdk_release_source: Path,
|
||||
runtime_version: str,
|
||||
) -> None:
|
||||
script = _load_update_script_module()
|
||||
monkeypatch.setattr(script, "sdk_root", lambda: sdk_release_source)
|
||||
project = sdk_release_source / "pyproject.toml"
|
||||
project.write_text(project.read_text().replace("==0.153.0", f"=={runtime_version}"))
|
||||
|
||||
with pytest.raises(RuntimeError, match=r"Cannot package.*Codex CLI 0\.151\.0 or newer"):
|
||||
script.stage_python_sdk_package(tmp_path / "sdk-stage", "0.1.0b1")
|
||||
|
||||
|
||||
def test_sdk_runtime_override_is_checked_after_stamping(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, sdk_release_source: Path
|
||||
) -> None:
|
||||
script = _load_update_script_module()
|
||||
monkeypatch.setattr(script, "sdk_root", lambda: sdk_release_source)
|
||||
with pytest.raises(RuntimeError, match=r"Cannot package.*Codex CLI 0\.151\.0 or newer"):
|
||||
script.stage_python_sdk_package(tmp_path / "sdk-stage", "0.1.0b1", "0.149.0")
|
||||
|
||||
|
||||
def test_sdk_beta_can_use_a_supported_runtime(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, sdk_release_source: Path
|
||||
) -> None:
|
||||
script = _load_update_script_module()
|
||||
monkeypatch.setattr(script, "sdk_root", lambda: sdk_release_source)
|
||||
|
||||
staged = script.stage_python_sdk_package(tmp_path / "sdk-stage", "0.1.0b1")
|
||||
|
||||
project = tomllib.loads((staged / "pyproject.toml").read_text())["project"]
|
||||
assert (project["version"], project["dependencies"]) == (
|
||||
"0.1.0b1",
|
||||
["pydantic>=2.12", "packaging>=26.2", "openai-codex-cli-bin==0.153.0"],
|
||||
)
|
||||
|
||||
|
||||
def test_stage_runtime_stages_package_without_type_generation(tmp_path: Path) -> None:
|
||||
script = _load_update_script_module()
|
||||
package_archive = _write_fake_codex_package_archive(tmp_path, script)
|
||||
@@ -1255,7 +1303,7 @@ def test_sdk_beta_can_pin_an_independent_runtime(tmp_path: Path) -> None:
|
||||
project = tomllib.loads((staged / "pyproject.toml").read_text())["project"]
|
||||
assert (project["version"], project["dependencies"]) == (
|
||||
"0.1.0b1",
|
||||
["pydantic>=2.12", "openai-codex-cli-bin==0.153.0"],
|
||||
["pydantic>=2.12", "packaging>=26.2", "openai-codex-cli-bin==0.153.0"],
|
||||
)
|
||||
|
||||
|
||||
@@ -1297,6 +1345,7 @@ def test_sdk_release_matches_runtime(
|
||||
"runtime_version": package_version,
|
||||
"sdk_dependencies": [
|
||||
"pydantic>=2.12",
|
||||
"packaging>=26.2",
|
||||
f"openai-codex-cli-bin=={package_version}",
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import get_type_hints
|
||||
|
||||
import pytest
|
||||
|
||||
from openai_codex._runtime_requirements import CheckoutCapabilities
|
||||
from openai_codex.client import CodexClient, _params_dict
|
||||
from openai_codex.errors import CodexError
|
||||
from openai_codex.generated.notification_registry import notification_turn_id
|
||||
from openai_codex.generated.v2_all import (
|
||||
AbsolutePathBuf,
|
||||
@@ -30,7 +33,7 @@ from openai_codex.generated.v2_all import (
|
||||
TurnStartParams,
|
||||
WarningNotification,
|
||||
)
|
||||
from openai_codex.models import Notification, UnknownNotification
|
||||
from openai_codex.models import InitializeResponse, JsonObject, Notification, UnknownNotification
|
||||
from openai_codex.types import ThreadSource
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
@@ -61,6 +64,160 @@ def test_approval_review_paths_preserve_existing_wrappers(model, fields) -> None
|
||||
assert isinstance(action.cwd, AbsolutePathBuf)
|
||||
|
||||
|
||||
def _initialized_client(
|
||||
monkeypatch: pytest.MonkeyPatch, metadata: JsonObject
|
||||
) -> tuple[CodexClient, list[tuple[str, JsonObject | None]]]:
|
||||
client = CodexClient()
|
||||
requests: list[tuple[str, JsonObject | None]] = []
|
||||
|
||||
def request_raw(method: str, params: JsonObject | None) -> JsonObject:
|
||||
requests.append((method, params))
|
||||
return metadata if method == "initialize" else {}
|
||||
|
||||
monkeypatch.setattr(client, "_request_raw", request_raw)
|
||||
monkeypatch.setattr(client, "notify", lambda *_args: None)
|
||||
client.initialize()
|
||||
requests.clear()
|
||||
return client, requests
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("method", "params"),
|
||||
[
|
||||
("turn/start", {"input": [], "turnTrigger": "automation"}),
|
||||
("turn/start", {"input": [], "serviceTierForTurn": "default"}),
|
||||
("thread/resume", {"threadId": "thread-1", "excludeTurns": False}),
|
||||
("thread/fork", {"threadId": "thread-1", "excludeTurns": True}),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("version", ["0.147.0", "0.149.0", "0.151.0-alpha.6", "unknown", ""])
|
||||
def test_new_options_reject_unsupported_runtime_before_sending(
|
||||
monkeypatch: pytest.MonkeyPatch, method: str, params: JsonObject, version: str
|
||||
) -> None:
|
||||
client, requests = _initialized_client(monkeypatch, {"userAgent": f"codex-cli/{version}"})
|
||||
|
||||
with pytest.raises(CodexError, match=r"Codex CLI 0\.151\.0 or newer"):
|
||||
client.request(method, params, response_model=InitializeResponse)
|
||||
|
||||
assert requests == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"metadata",
|
||||
[
|
||||
{"userAgent": "codex-cli/0.151.0 (Linux)"},
|
||||
{"userAgent": "codex-cli 0.153.0"},
|
||||
{"userAgent": "codex-cli/0.154.0-alpha.1"},
|
||||
{"userAgent": "codex-cli/0.154.0-alpha.1.2"},
|
||||
{"userAgent": "codex-cli/0.151.0.post1"},
|
||||
{"userAgent": "unknown", "serverInfo": {"name": "codex", "version": "0.153.0"}},
|
||||
],
|
||||
)
|
||||
def test_new_options_accept_supported_runtime_metadata(
|
||||
monkeypatch: pytest.MonkeyPatch, metadata: JsonObject
|
||||
) -> None:
|
||||
client, requests = _initialized_client(monkeypatch, metadata)
|
||||
params = {"input": [], "turnTrigger": "automation"}
|
||||
|
||||
client.request("turn/start", params, response_model=InitializeResponse)
|
||||
|
||||
assert requests == [("turn/start", params)]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("supports_options", [True, False])
|
||||
def test_unversioned_checkout_probes_and_caches_its_own_schema(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, supports_options: bool
|
||||
) -> None:
|
||||
client, requests = _initialized_client(monkeypatch, {"userAgent": "codex-cli/0.0.0"})
|
||||
command = ("checkout-codex", "--config", "key=value", "app-server")
|
||||
client._checkout_capabilities = CheckoutCapabilities(
|
||||
command, str(tmp_path), {"CUSTOM": "value"}
|
||||
)
|
||||
probes = []
|
||||
|
||||
def generate_schema(args, **kwargs):
|
||||
probes.append((args[:-1], kwargs))
|
||||
output = Path(args[-1]) / "v2"
|
||||
output.mkdir()
|
||||
for name, fields in (
|
||||
("TurnStartParams", ["turnTrigger", "serviceTierForTurn"]),
|
||||
("ThreadResumeParams", ["excludeTurns"]),
|
||||
("ThreadForkParams", ["excludeTurns"]),
|
||||
):
|
||||
(output / f"{name}.json").write_text(
|
||||
json.dumps(
|
||||
{"properties": {field: {} for field in fields} if supports_options else {}}
|
||||
)
|
||||
)
|
||||
|
||||
monkeypatch.setattr("openai_codex._runtime_requirements.subprocess.run", generate_schema)
|
||||
for method, params in (
|
||||
("turn/start", {"input": [], "turnTrigger": "automation"}),
|
||||
("thread/resume", {"threadId": "thread-1", "excludeTurns": False}),
|
||||
):
|
||||
if supports_options:
|
||||
client.request(method, params, response_model=InitializeResponse)
|
||||
else:
|
||||
with pytest.raises(CodexError, match="checkout does not support"):
|
||||
client.request(method, params, response_model=InitializeResponse)
|
||||
assert len(requests) == (2 if supports_options else 0)
|
||||
assert probes == [
|
||||
(
|
||||
[*command, "generate-json-schema", "--experimental", "--out"],
|
||||
{
|
||||
"cwd": str(tmp_path),
|
||||
"env": {"CUSTOM": "value"},
|
||||
"capture_output": True,
|
||||
"check": True,
|
||||
"timeout": 30,
|
||||
},
|
||||
)
|
||||
]
|
||||
client.close()
|
||||
assert client._checkout_capabilities is None
|
||||
|
||||
|
||||
def test_unversioned_custom_launch_requires_verifiable_capabilities(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
client, requests = _initialized_client(monkeypatch, {"userAgent": "codex-cli/0.0.0"})
|
||||
with pytest.raises(CodexError, match="Cannot verify an unversioned CLI"):
|
||||
client.request(
|
||||
"turn/start", {"turnTrigger": "automation"}, response_model=InitializeResponse
|
||||
)
|
||||
assert requests == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("metadata", [{}, {"userAgent": "codex-cli/0.147.0"}])
|
||||
def test_ordinary_requests_keep_working_on_old_or_unknown_runtime(
|
||||
monkeypatch: pytest.MonkeyPatch, metadata: JsonObject
|
||||
) -> None:
|
||||
client, requests = _initialized_client(monkeypatch, metadata)
|
||||
params = {"input": [{"type": "text", "text": "Hello"}], "serviceTier": "default"}
|
||||
|
||||
client.request("turn/start", params, response_model=InitializeResponse)
|
||||
client.request("thread/resume", {"threadId": "thread-1"}, response_model=InitializeResponse)
|
||||
client.request("thread/fork", {"threadId": "thread-1"}, response_model=InitializeResponse)
|
||||
|
||||
assert requests == [
|
||||
("turn/start", params),
|
||||
("thread/resume", {"threadId": "thread-1"}),
|
||||
("thread/fork", {"threadId": "thread-1"}),
|
||||
]
|
||||
|
||||
|
||||
def test_new_options_require_fresh_initialize_metadata_after_close(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
client, requests = _initialized_client(monkeypatch, {"userAgent": "codex-cli/0.153.0"})
|
||||
client.close()
|
||||
|
||||
with pytest.raises(CodexError, match="reported version is 'unknown'"):
|
||||
client.request("thread/resume", {"excludeTurns": True}, response_model=InitializeResponse)
|
||||
|
||||
assert requests == []
|
||||
|
||||
|
||||
def test_generated_params_models_are_snake_case_and_dump_by_alias() -> None:
|
||||
params = ThreadListParams(search_term="needle", limit=5)
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -13,8 +15,9 @@ from openai_codex.api import (
|
||||
Codex,
|
||||
Sandbox,
|
||||
)
|
||||
from openai_codex.generated.v2_all import TurnStartParams
|
||||
from openai_codex.models import InitializeResponse
|
||||
from openai_codex.client import _params_dict
|
||||
from openai_codex.generated.v2_all import TurnCompletedNotification, TurnStartParams
|
||||
from openai_codex.models import InitializeResponse, Notification
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
@@ -128,6 +131,97 @@ def test_async_codex_initializes_only_once_under_concurrency() -> None:
|
||||
asyncio.run(scenario())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("api_type", [Codex, AsyncCodex])
|
||||
@pytest.mark.parametrize(
|
||||
("options", "expected"),
|
||||
[
|
||||
({}, {}),
|
||||
({"include_turns": None}, {}),
|
||||
({"include_turns": True}, {"excludeTurns": False}),
|
||||
({"include_turns": False}, {"excludeTurns": True}),
|
||||
],
|
||||
)
|
||||
def test_include_turns_preserves_omission_and_inverts_explicit_values(
|
||||
api_type, options, expected
|
||||
) -> None:
|
||||
async def scenario() -> None:
|
||||
async_api = api_type is AsyncCodex
|
||||
rpc = AsyncMock if async_api else Mock
|
||||
thread_response = SimpleNamespace(thread=SimpleNamespace(id="thread-2"))
|
||||
client = SimpleNamespace(
|
||||
thread_resume=rpc(return_value=thread_response),
|
||||
thread_fork=rpc(return_value=thread_response),
|
||||
)
|
||||
codex = api_type.__new__(api_type)
|
||||
codex._client = client
|
||||
codex._initialized = True
|
||||
|
||||
for method in ("thread_resume", "thread_fork"):
|
||||
thread = getattr(codex, method)("thread-1", **options)
|
||||
if async_api:
|
||||
thread = await thread
|
||||
assert thread.id == "thread-2"
|
||||
assert _params_dict(getattr(client, method).call_args.args[1]) == {
|
||||
"threadId": "thread-1",
|
||||
**expected,
|
||||
}
|
||||
|
||||
asyncio.run(scenario())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("api_type", [Codex, AsyncCodex])
|
||||
@pytest.mark.parametrize("method", ["run", "turn"])
|
||||
def test_turn_inputs_and_options_reach_the_client(api_type, method) -> None:
|
||||
"""Turn options reach the client through both sync and async entry points."""
|
||||
|
||||
async def scenario() -> None:
|
||||
async_api = api_type is AsyncCodex
|
||||
rpc = AsyncMock if async_api else Mock
|
||||
completed = Notification(
|
||||
method="turn/completed",
|
||||
payload=TurnCompletedNotification.model_validate(
|
||||
{
|
||||
"threadId": "thread-1",
|
||||
"turn": {"id": "turn-1", "items": [], "status": "completed"},
|
||||
}
|
||||
),
|
||||
)
|
||||
client = SimpleNamespace(
|
||||
turn_start=rpc(return_value=SimpleNamespace(turn=SimpleNamespace(id="turn-1"))),
|
||||
register_turn_notifications=Mock(),
|
||||
unregister_turn_notifications=Mock(),
|
||||
next_turn_notification=rpc(return_value=completed),
|
||||
)
|
||||
codex = api_type.__new__(api_type)
|
||||
codex._client = client
|
||||
codex._initialized = True
|
||||
thread = (
|
||||
public_api_module.AsyncThread(codex, "thread-1")
|
||||
if async_api
|
||||
else public_api_module.Thread(client, "thread-1")
|
||||
)
|
||||
input = "Continue."
|
||||
turn = getattr(thread, method)(
|
||||
input,
|
||||
service_tier="priority",
|
||||
turn_service_tier="default",
|
||||
source="automation",
|
||||
)
|
||||
if async_api:
|
||||
turn = await turn
|
||||
assert turn.id == "turn-1"
|
||||
expected_input = [{"type": "text", "text": "Continue.", "text_elements": []}]
|
||||
assert _params_dict(client.turn_start.call_args.kwargs["params"]) == {
|
||||
"threadId": "thread-1",
|
||||
"input": expected_input,
|
||||
"serviceTier": "priority",
|
||||
"serviceTierForTurn": "default",
|
||||
"turnTrigger": "automation",
|
||||
}
|
||||
|
||||
asyncio.run(scenario())
|
||||
|
||||
|
||||
def _approval_mode_turn_params(approval_mode: ApprovalMode) -> TurnStartParams:
|
||||
"""Build real generated turn params from one public approval mode."""
|
||||
approval_policy, approvals_reviewer = public_api_module._approval_mode_settings(approval_mode)
|
||||
|
||||
@@ -5,8 +5,6 @@ import inspect
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import tomllib
|
||||
|
||||
import openai_codex
|
||||
import openai_codex.types as public_types
|
||||
from openai_codex import (
|
||||
@@ -24,6 +22,11 @@ from openai_codex import (
|
||||
from openai_codex._initialize_metadata import validate_initialize_metadata
|
||||
from openai_codex.types import InitializeResponse
|
||||
|
||||
try:
|
||||
import tomllib
|
||||
except ModuleNotFoundError:
|
||||
import tomli as tomllib
|
||||
|
||||
EXPECTED_ROOT_EXPORTS = [
|
||||
"__version__",
|
||||
"CodexConfig",
|
||||
@@ -176,13 +179,15 @@ def test_turn_input_methods_accept_string_shortcut() -> None:
|
||||
Thread.turn,
|
||||
AsyncThread.run,
|
||||
AsyncThread.turn,
|
||||
TurnHandle.steer,
|
||||
AsyncTurnHandle.steer,
|
||||
]
|
||||
|
||||
assert {fn: inspect.signature(fn).parameters["input"].annotation for fn in funcs} == (
|
||||
dict.fromkeys(funcs, "RunInput")
|
||||
)
|
||||
assert {
|
||||
fn: inspect.signature(fn).parameters["input"].annotation
|
||||
for fn in (TurnHandle.steer, AsyncTurnHandle.steer)
|
||||
} == dict.fromkeys((TurnHandle.steer, AsyncTurnHandle.steer), "RunInput")
|
||||
|
||||
|
||||
def test_root_exports_approval_mode() -> None:
|
||||
@@ -351,6 +356,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
|
||||
"config",
|
||||
"cwd",
|
||||
"developer_instructions",
|
||||
"include_turns",
|
||||
"model",
|
||||
"model_provider",
|
||||
"personality",
|
||||
@@ -364,6 +370,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
|
||||
"cwd",
|
||||
"developer_instructions",
|
||||
"ephemeral",
|
||||
"include_turns",
|
||||
"model",
|
||||
"model_provider",
|
||||
"sandbox",
|
||||
@@ -379,7 +386,9 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
|
||||
"personality",
|
||||
"sandbox",
|
||||
"service_tier",
|
||||
"source",
|
||||
"summary",
|
||||
"turn_service_tier",
|
||||
],
|
||||
Thread.run: [
|
||||
"approval_mode",
|
||||
@@ -390,7 +399,9 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
|
||||
"personality",
|
||||
"sandbox",
|
||||
"service_tier",
|
||||
"source",
|
||||
"summary",
|
||||
"turn_service_tier",
|
||||
],
|
||||
AsyncCodex.thread_start: [
|
||||
"approval_mode",
|
||||
@@ -427,6 +438,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
|
||||
"config",
|
||||
"cwd",
|
||||
"developer_instructions",
|
||||
"include_turns",
|
||||
"model",
|
||||
"model_provider",
|
||||
"personality",
|
||||
@@ -440,6 +452,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
|
||||
"cwd",
|
||||
"developer_instructions",
|
||||
"ephemeral",
|
||||
"include_turns",
|
||||
"model",
|
||||
"model_provider",
|
||||
"sandbox",
|
||||
@@ -455,7 +468,9 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
|
||||
"personality",
|
||||
"sandbox",
|
||||
"service_tier",
|
||||
"source",
|
||||
"summary",
|
||||
"turn_service_tier",
|
||||
],
|
||||
AsyncThread.run: [
|
||||
"approval_mode",
|
||||
@@ -466,7 +481,9 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
|
||||
"personality",
|
||||
"sandbox",
|
||||
"service_tier",
|
||||
"source",
|
||||
"summary",
|
||||
"turn_service_tier",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
Generated
+13
-11
@@ -7,7 +7,7 @@ exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for
|
||||
exclude-newer-span = "P7D"
|
||||
|
||||
[options.exclude-newer-package]
|
||||
openai-codex-cli-bin = "2026-08-19T00:00:00Z"
|
||||
openai-codex-cli-bin = "2026-09-09T06:06:45Z"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
@@ -286,6 +286,7 @@ version = "0.0.0.dev0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "openai-codex-cli-bin" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pydantic" },
|
||||
]
|
||||
|
||||
@@ -306,7 +307,8 @@ test = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "openai-codex-cli-bin", specifier = "==0.147.0" },
|
||||
{ name = "openai-codex-cli-bin", specifier = "==0.153.4" },
|
||||
{ name = "packaging", specifier = ">=26.2" },
|
||||
{ name = "pydantic", specifier = ">=2.12" },
|
||||
]
|
||||
|
||||
@@ -325,17 +327,17 @@ test = [
|
||||
|
||||
[[package]]
|
||||
name = "openai-codex-cli-bin"
|
||||
version = "0.147.0"
|
||||
version = "0.153.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/46/85/3302e5265f35941a24f614573e1a27e6f218d7fa71e4dd3b1353d1726c5a/openai_codex_cli_bin-0.147.0-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:19c3a72a0eac6706bb5023088ab7eb31d8cfc06bf7860250b5c5b90f4505489b", size = 116948927, upload-time = "2026-08-18T06:05:53.479Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/0c/6474ef2f854ecf217550d06667a8ac988b5656c448b680e5b5ece5ae5152/openai_codex_cli_bin-0.147.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b851943fffc48aa7c5c130b6a34be09964833d2785546eda96d749427c6e24f2", size = 107573930, upload-time = "2026-08-18T06:05:57.701Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/4b/0efce457a301271301b9229b36b3f53ca94d5f4db1d605c86e0da9833565/openai_codex_cli_bin-0.147.0-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:aab3e27ce07cd7bc7a78708efa40375b28e89f586851c495ef55aa6cb6806d11", size = 111220520, upload-time = "2026-08-18T06:06:01.636Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/e4/5e4fb0f61ca90c2bb42a8823e08fe05957c54591d1e161c56a93d27d565d/openai_codex_cli_bin-0.147.0-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:cb3907d633cda87c4b68b47ff4979e6054c02a08c41b15aa2e9a689558a61074", size = 119921665, upload-time = "2026-08-18T06:06:05.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/83/a52e32fc63bde10996e38cb4e1175c402c578ee7c7ca79b96abee5219127/openai_codex_cli_bin-0.147.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:ea0bfdee98164fc7d589eea5623aff06c8f8f2ba3f9bc550297429db45ea68f7", size = 111220518, upload-time = "2026-08-18T06:06:09.538Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/b6/c159da0a1ec136fde6d2742ec05347e311f000dfe19024ed7d708cf60aa5/openai_codex_cli_bin-0.147.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:a9be23f46326494b7ebf4bd98cbe985542f5ad076355f3b0fb636a984df56816", size = 119921665, upload-time = "2026-08-18T06:06:15.061Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/38/1bb47e08b9c523b673358e4f5597fc99699e855ec63bd02fdc157f1679c9/openai_codex_cli_bin-0.147.0-py3-none-win_amd64.whl", hash = "sha256:1a403ff803ae27e078189a0fd24687f32d43c46f152e50cdbe3eddc9e302f697", size = 127586208, upload-time = "2026-08-18T06:06:19.395Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/1e/a44a8da140ef080aebac6da6517e8fe6ac1aad49436c8b1f9b87e735b813/openai_codex_cli_bin-0.147.0-py3-none-win_arm64.whl", hash = "sha256:be0c8b9e34b067151d0964a24e9f4b8b48ea516448a08ef2636f357ebdc877b7", size = 117863410, upload-time = "2026-08-18T06:06:23.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/16/b28109968c0912956a52f41eb022605c6eaba2d4793b5e9f4b9479c954e2/openai_codex_cli_bin-0.153.4-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:637bdf17d6387e9ae12e1094af6aa22dabc2ba55c100ee0310178e79427b2e69", size = 121719355, upload-time = "2026-09-09T06:06:11.682Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/69/7dd4cb9135ac893985c92ecc03dc3df8408917d469fb3df6650af2fde328/openai_codex_cli_bin-0.153.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:08c80523fd3674e7d6f36bccc69557f9bac257cbd7ad937309dad0457e1a7845", size = 111925786, upload-time = "2026-09-09T06:06:16.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/e7/57e5dce0b13c0643cd20f308d9e727a44d542bca38f958f038204f959687/openai_codex_cli_bin-0.153.4-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:f92f0aff384258f5bac3519cd9ad2639be08239004452d0e4b00725e5fe62bba", size = 117599105, upload-time = "2026-09-09T06:06:22.08Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/b3/938bfc0cfdaa06881d904b1c9a3151c1252e278d00fe1afcbb954cee65c4/openai_codex_cli_bin-0.153.4-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:584ecdbd81b01b3002ecbdddfa009dc8d7bfdbed9c208a0fa8c131a2e337bba6", size = 127000985, upload-time = "2026-09-09T06:06:26.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/e8/9743befc32fa292d3682075c15e056044652dae1bab9581a9e9ee0cd352b/openai_codex_cli_bin-0.153.4-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:fef1cc54d7408da08f9bfa6179c7fad4231591c29f76eb339d0a59e540b3e718", size = 117599103, upload-time = "2026-09-09T06:06:30.967Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/d2/ded14722031d8dda9c796ab8d39f8a29cbae27d28bf2b1a0b12c2e429c6f/openai_codex_cli_bin-0.153.4-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:0f97697f39fd9fd32fbf9caa29df8ff0a6cfe33755f4f007bc97ee4cb88a7743", size = 127000983, upload-time = "2026-09-09T06:06:35.565Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/b4/afec4ffe8a3e3f2b5fa7ef6d892b830f63ee8aaa5c222b23652329bc3e4e/openai_codex_cli_bin-0.153.4-py3-none-win_amd64.whl", hash = "sha256:9b52da6531617a363a165c246aded59a1a61e82fac623ab97e095cc37a5038c5", size = 136934929, upload-time = "2026-09-09T06:06:40.046Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/d3/e8de747460d8d2934dce84867ac8227d4d2d4b31f111e881076f97525a72/openai_codex_cli_bin-0.153.4-py3-none-win_arm64.whl", hash = "sha256:23240c7041a8ef5f22297c3fead21da0143dcec2a186d9b64f386f52b396da47", size = 126478600, upload-time = "2026-09-09T06:06:44.431Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user