[codex] Accept string input for Python turns (#23162)

## Summary
- Allow thread.turn and turn.steer, including async variants, to accept
RunInput so plain strings work alongside typed input objects.
- Export RunInput and update the SDK artifact generator so regenerated
turn methods keep the same signature and normalization.
- Update docs, examples, notebook cells, and tests to use string
shorthand for text-only turns while keeping typed inputs for multimodal
input.

## Validation
- uv run --extra dev ruff format .
- uv run --extra dev ruff check --output-format=github .
- python3 -m py_compile sdk/python/src/openai_codex/__init__.py
sdk/python/src/openai_codex/api.py
sdk/python/src/openai_codex/_inputs.py
sdk/python/scripts/update_sdk_artifacts.py
sdk/python/tests/test_public_api_signatures.py
sdk/python/tests/test_app_server_streaming.py
sdk/python/tests/test_app_server_turn_controls.py
sdk/python/tests/test_real_app_server_integration.py
- python3 -c "import json;
json.load(open('sdk/python/notebooks/sdk_walkthrough.ipynb'))"
- sdk/python/.venv/bin/python -c "import inspect, openai_codex; from
openai_codex import Thread, AsyncThread, TurnHandle, AsyncTurnHandle,
RunInput; funcs=[Thread.run, Thread.turn, AsyncThread.run,
AsyncThread.turn, TurnHandle.steer, AsyncTurnHandle.steer]; assert
all(inspect.signature(fn).parameters['input'].annotation == 'RunInput'
for fn in funcs); assert RunInput is openai_codex.RunInput"
This commit is contained in:
Ahmed Ibrahim
2026-05-17 09:05:44 -07:00
committed by GitHub
parent 0a83353ca3
commit e7bffc5a20
31 changed files with 171 additions and 134 deletions

View File

@@ -102,6 +102,8 @@ target wheel. The SDK package version and runtime package version must match.
- `Codex()` is eager and performs startup + `initialize` in the constructor.
- Use context managers (`with Codex() as codex:`) to ensure shutdown.
- Plain strings are accepted anywhere a turn input is accepted; they are
shorthand for `TextInput(...)`.
- Prefer `thread.run("...")` for the common case. Use `thread.turn(...)` when
you need streaming, steering, or interrupt control.
- For transient overload, use `retry_on_overload` from the package root.

View File

@@ -3,7 +3,7 @@
Public surface of `openai_codex` for app-server v2.
This SDK surface is experimental. Turn streams are routed by turn ID so one client can consume multiple active turns concurrently.
Thread and turn starts expose `approval_mode`. `ApprovalMode.auto_review` is the default; use `ApprovalMode.deny_all` to deny escalated permissions.
Thread starts default to `ApprovalMode.auto_review`; turn starts accept an optional `approval_mode` override.
## Package Entry
@@ -23,6 +23,7 @@ from openai_codex import (
TurnResult,
Input,
InputItem,
RunInput,
TextInput,
ImageInput,
LocalImageInput,
@@ -147,16 +148,16 @@ attempt. API-key login completes synchronously and does not return a handle.
### Thread
- `run(input: str | Input, *, approval_mode=ApprovalMode.auto_review, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox_policy=None, service_tier=None, summary=None) -> TurnResult`
- `turn(input: Input, *, approval_mode=ApprovalMode.auto_review, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox_policy=None, summary=None) -> TurnHandle`
- `run(input: str | Input, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox_policy=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_policy=None, service_tier=None, summary=None) -> TurnHandle`
- `read(*, include_turns: bool = False) -> ThreadReadResponse`
- `set_name(name: str) -> ThreadSetNameResponse`
- `compact() -> ThreadCompactStartResponse`
### AsyncThread
- `run(input: str | Input, *, approval_mode=ApprovalMode.auto_review, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox_policy=None, service_tier=None, summary=None) -> Awaitable[TurnResult]`
- `turn(input: Input, *, approval_mode=ApprovalMode.auto_review, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox_policy=None, summary=None) -> Awaitable[AsyncTurnHandle]`
- `run(input: str | Input, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox_policy=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_policy=None, service_tier=None, summary=None) -> Awaitable[AsyncTurnHandle]`
- `read(*, include_turns: bool = False) -> Awaitable[ThreadReadResponse]`
- `set_name(name: str) -> Awaitable[ThreadSetNameResponse]`
- `compact() -> Awaitable[ThreadCompactStartResponse]`
@@ -185,7 +186,7 @@ Use `turn(...)` when you need low-level turn control (`stream()`, `steer()`,
### TurnHandle
- `steer(input: Input) -> TurnSteerResponse`
- `steer(input: str | Input) -> TurnSteerResponse`
- `interrupt() -> TurnInterruptResponse`
- `stream() -> Iterator[Notification]`
- `run() -> TurnResult`
@@ -197,7 +198,7 @@ Behavior notes:
### AsyncTurnHandle
- `steer(input: Input) -> Awaitable[TurnSteerResponse]`
- `steer(input: str | Input) -> Awaitable[TurnSteerResponse]`
- `interrupt() -> Awaitable[TurnInterruptResponse]`
- `stream() -> AsyncIterator[Notification]`
- `run() -> Awaitable[TurnResult]`
@@ -218,8 +219,12 @@ Behavior notes:
InputItem = TextInput | ImageInput | LocalImageInput | SkillInput | MentionInput
Input = list[InputItem] | InputItem
RunInput = Input | str
```
Use a plain `str` as shorthand for `TextInput(...)` anywhere a turn input is accepted:
`thread.run("...")`, `thread.turn("...")`, and `turn.steer("...")`.
## Public Types
The SDK wrappers return and accept public app-server models wherever possible:

View File

@@ -72,6 +72,7 @@ What happened:
- `thread_start(...)` created a thread.
- `thread.run("...")` started a turn, consumed events until completion, and returned `TurnResult` with turn metadata, final assistant response, collected items, and usage.
- `result.final_response` is `None` when no final-answer or phase-less assistant message item completes for the turn.
- plain strings are accepted anywhere a turn input is accepted; typed inputs are still available for multimodal and structured cases
- use `thread.turn(...)` when you need a `TurnHandle` for streaming, steering, or interrupting before collecting `TurnResult`
- one client can consume multiple active turns concurrently; turn streams are routed by turn ID

View File

@@ -11,7 +11,7 @@ ensure_local_sdk_src()
import asyncio
from openai_codex import AsyncCodex, TextInput
from openai_codex import AsyncCodex
async def main() -> None:
@@ -19,7 +19,7 @@ async def main() -> None:
thread = await codex.thread_start(
model="gpt-5.4", config={"model_reasoning_effort": "high"}
)
turn = await thread.turn(TextInput("Give 3 bullets about SIMD."))
turn = await thread.turn("Give 3 bullets about SIMD.")
result = await turn.run()
print("thread_id:", thread.id)

View File

@@ -9,11 +9,11 @@ from _bootstrap import ensure_local_sdk_src, runtime_config
ensure_local_sdk_src()
from openai_codex import Codex, TextInput
from openai_codex import Codex
with Codex(config=runtime_config()) as codex:
thread = codex.thread_start(model="gpt-5.4", config={"model_reasoning_effort": "high"})
result = thread.turn(TextInput("Give 3 bullets about SIMD.")).run()
result = thread.turn("Give 3 bullets about SIMD.").run()
print("thread_id:", thread.id)
print("turn_id:", result.id)

View File

@@ -11,7 +11,7 @@ ensure_local_sdk_src()
import asyncio
from openai_codex import AsyncCodex, TextInput
from openai_codex import AsyncCodex
async def main() -> None:
@@ -19,7 +19,7 @@ async def main() -> None:
thread = await codex.thread_start(
model="gpt-5.4", config={"model_reasoning_effort": "high"}
)
turn = await thread.turn(TextInput("Explain SIMD in 3 short bullets."))
turn = await thread.turn("Explain SIMD in 3 short bullets.")
event_count = 0
saw_started = False

View File

@@ -9,11 +9,11 @@ from _bootstrap import ensure_local_sdk_src, runtime_config
ensure_local_sdk_src()
from openai_codex import Codex, TextInput
from openai_codex import Codex
with Codex(config=runtime_config()) as codex:
thread = codex.thread_start(model="gpt-5.4", config={"model_reasoning_effort": "high"})
turn = thread.turn(TextInput("Explain SIMD in 3 short bullets."))
turn = thread.turn("Explain SIMD in 3 short bullets.")
event_count = 0
saw_started = False

View File

@@ -11,7 +11,7 @@ ensure_local_sdk_src()
import asyncio
from openai_codex import AsyncCodex, TextInput
from openai_codex import AsyncCodex
async def main() -> None:
@@ -20,12 +20,12 @@ async def main() -> None:
model="gpt-5.4", config={"model_reasoning_effort": "high"}
)
first_turn = await original.turn(TextInput("Tell me one fact about Saturn."))
first_turn = await original.turn("Tell me one fact about Saturn.")
_ = await first_turn.run()
print("Created thread:", original.id)
resumed = await codex.thread_resume(original.id)
second_turn = await resumed.turn(TextInput("Continue with one more fact."))
second_turn = await resumed.turn("Continue with one more fact.")
second = await second_turn.run()
print(second.final_response)

View File

@@ -9,15 +9,15 @@ from _bootstrap import ensure_local_sdk_src, runtime_config
ensure_local_sdk_src()
from openai_codex import Codex, TextInput
from openai_codex import Codex
with Codex(config=runtime_config()) as codex:
# Create an initial thread and turn so we have a real thread to resume.
original = codex.thread_start(model="gpt-5.4", config={"model_reasoning_effort": "high"})
first = original.turn(TextInput("Tell me one fact about Saturn.")).run()
first = original.turn("Tell me one fact about Saturn.").run()
print("Created thread:", original.id)
# Resume the existing thread by ID.
resumed = codex.thread_resume(original.id)
second = resumed.turn(TextInput("Continue with one more fact.")).run()
second = resumed.turn("Continue with one more fact.").run()
print(second.final_response)

View File

@@ -11,7 +11,7 @@ ensure_local_sdk_src()
import asyncio
from openai_codex import AsyncCodex, TextInput
from openai_codex import AsyncCodex
async def main() -> None:
@@ -19,10 +19,8 @@ async def main() -> None:
thread = await codex.thread_start(
model="gpt-5.4", config={"model_reasoning_effort": "high"}
)
first = await (
await thread.turn(TextInput("One sentence about structured planning."))
).run()
second = await (await thread.turn(TextInput("Now restate it for a junior engineer."))).run()
first = await (await thread.turn("One sentence about structured planning.")).run()
second = await (await thread.turn("Now restate it for a junior engineer.")).run()
reopened = await codex.thread_resume(thread.id)
listing_active = await codex.thread_list(limit=20, archived=False)
@@ -38,13 +36,11 @@ async def main() -> None:
model="gpt-5.4",
config={"model_reasoning_effort": "high"},
)
resumed_result = await (
await resumed.turn(TextInput("Continue in one short sentence."))
).run()
resumed_result = await (await resumed.turn("Continue in one short sentence.")).run()
forked = await codex.thread_fork(unarchived.id, model="gpt-5.4")
forked_result = await (
await forked.turn(TextInput("Take a different angle in one short sentence."))
await forked.turn("Take a different angle in one short sentence.")
).run()
compact_result = await unarchived.compact()

View File

@@ -9,12 +9,12 @@ from _bootstrap import ensure_local_sdk_src, runtime_config
ensure_local_sdk_src()
from openai_codex import Codex, TextInput
from openai_codex import Codex
with Codex(config=runtime_config()) as codex:
thread = codex.thread_start(model="gpt-5.4", config={"model_reasoning_effort": "high"})
first = thread.turn(TextInput("One sentence about structured planning.")).run()
second = thread.turn(TextInput("Now restate it for a junior engineer.")).run()
first = thread.turn("One sentence about structured planning.").run()
second = thread.turn("Now restate it for a junior engineer.").run()
reopened = codex.thread_resume(thread.id)
listing_active = codex.thread_list(limit=20, archived=False)
@@ -30,10 +30,10 @@ with Codex(config=runtime_config()) as codex:
model="gpt-5.4",
config={"model_reasoning_effort": "high"},
)
resumed_result = resumed.turn(TextInput("Continue in one short sentence.")).run()
resumed_result = resumed.turn("Continue in one short sentence.").run()
forked = codex.thread_fork(unarchived.id, model="gpt-5.4")
forked_result = forked.turn(TextInput("Take a different angle in one short sentence.")).run()
forked_result = forked.turn("Take a different angle in one short sentence.").run()
compact_result = unarchived.compact()

View File

@@ -9,13 +9,13 @@ from _bootstrap import ensure_local_sdk_src, runtime_config, server_label
ensure_local_sdk_src()
from openai_codex import Codex, TextInput
from openai_codex import Codex
with Codex(config=runtime_config()) as codex:
print("Server:", server_label(codex.metadata))
thread = codex.thread_start(model="gpt-5.4", config={"model_reasoning_effort": "high"})
turn = thread.turn(TextInput("Say hello in one sentence."))
turn = thread.turn("Say hello in one sentence.")
result = turn.run()
print("Thread:", thread.id)

View File

@@ -18,7 +18,6 @@ from openai_codex import (
AsyncCodex,
JsonRpcError,
ServerBusyError,
TextInput,
is_retryable_error,
)
from openai_codex.types import TurnStatus
@@ -82,7 +81,7 @@ async def main() -> None:
def _run_turn(thread, prompt: str):
async def _inner():
turn = await thread.turn(TextInput(prompt))
turn = await thread.turn(prompt)
return await turn.run()
return _inner

View File

@@ -13,7 +13,6 @@ from openai_codex import (
Codex,
JsonRpcError,
ServerBusyError,
TextInput,
retry_on_overload,
)
from openai_codex.types import TurnStatus
@@ -23,7 +22,7 @@ with Codex(config=runtime_config()) as codex:
try:
result = retry_on_overload(
lambda: thread.turn(TextInput("Summarize retry best practices in 3 bullets.")).run(),
lambda: thread.turn("Summarize retry best practices in 3 bullets.").run(),
max_attempts=3,
initial_delay_s=0.25,
max_delay_s=2.0,

View File

@@ -13,7 +13,6 @@ import asyncio
from openai_codex import (
AsyncCodex,
TextInput,
)
from openai_codex.types import (
ThreadTokenUsageUpdatedNotification,
@@ -51,7 +50,7 @@ async def main() -> None:
if user_input in {"/exit", "/quit"}:
break
turn = await thread.turn(TextInput(user_input))
turn = await thread.turn(user_input)
usage = None
status = None
error = None

View File

@@ -11,7 +11,6 @@ ensure_local_sdk_src()
from openai_codex import (
Codex,
TextInput,
)
from openai_codex.types import (
ThreadTokenUsageUpdatedNotification,
@@ -46,7 +45,7 @@ with Codex(config=runtime_config()) as codex:
if user_input in {"/exit", "/quit"}:
break
turn = thread.turn(TextInput(user_input))
turn = thread.turn(user_input)
usage = None
status = None
error = None

View File

@@ -14,7 +14,6 @@ import asyncio
from openai_codex import (
AsyncCodex,
TextInput,
)
from openai_codex.types import (
Personality,
@@ -49,7 +48,7 @@ async def main() -> None:
)
turn = await thread.turn(
TextInput(PROMPT),
PROMPT,
output_schema=OUTPUT_SCHEMA,
personality=Personality.pragmatic,
summary=SUMMARY,

View File

@@ -12,7 +12,6 @@ ensure_local_sdk_src()
from openai_codex import (
Codex,
TextInput,
)
from openai_codex.types import (
Personality,
@@ -43,7 +42,7 @@ with Codex(config=runtime_config()) as codex:
thread = codex.thread_start(model="gpt-5.4", config={"model_reasoning_effort": "high"})
turn = thread.turn(
TextInput(PROMPT),
PROMPT,
output_schema=OUTPUT_SCHEMA,
personality=Personality.pragmatic,
summary=SUMMARY,

View File

@@ -13,7 +13,6 @@ import asyncio
from openai_codex import (
AsyncCodex,
TextInput,
)
from openai_codex.types import (
Personality,
@@ -91,7 +90,7 @@ async def main() -> None:
)
first_turn = await thread.turn(
TextInput("Give one short sentence about reliable production releases."),
"Give one short sentence about reliable production releases.",
model=selected_model.model,
effort=selected_effort,
)
@@ -101,7 +100,7 @@ async def main() -> None:
print("items:", len(first.items))
second_turn = await thread.turn(
TextInput("Return JSON for a safe feature-flag rollout plan."),
"Return JSON for a safe feature-flag rollout plan.",
cwd=str(Path.cwd()),
effort=selected_effort,
model=selected_model.model,

View File

@@ -11,7 +11,6 @@ ensure_local_sdk_src()
from openai_codex import (
Codex,
TextInput,
)
from openai_codex.types import (
Personality,
@@ -88,7 +87,7 @@ with Codex(config=runtime_config()) as codex:
)
first = thread.turn(
TextInput("Give one short sentence about reliable production releases."),
"Give one short sentence about reliable production releases.",
model=selected_model.model,
effort=selected_effort,
).run()
@@ -97,7 +96,7 @@ with Codex(config=runtime_config()) as codex:
print("items:", len(first.items))
second = thread.turn(
TextInput("Return JSON for a safe feature-flag rollout plan."),
"Return JSON for a safe feature-flag rollout plan.",
cwd=str(Path.cwd()),
effort=selected_effort,
model=selected_model.model,

View File

@@ -11,7 +11,7 @@ ensure_local_sdk_src()
import asyncio
from openai_codex import AsyncCodex, TextInput
from openai_codex import AsyncCodex
async def main() -> None:
@@ -19,10 +19,8 @@ async def main() -> None:
thread = await codex.thread_start(
model="gpt-5.4", config={"model_reasoning_effort": "high"}
)
steer_turn = await thread.turn(
TextInput("Count from 1 to 40 with commas, then one summary sentence.")
)
steer_result = await steer_turn.steer(TextInput("Keep it brief and stop after 10 numbers."))
steer_turn = await thread.turn("Count from 1 to 40 with commas, then one summary sentence.")
steer_result = await steer_turn.steer("Keep it brief and stop after 10 numbers.")
steer_event_count = 0
steer_completed_status = None
@@ -40,7 +38,7 @@ async def main() -> None:
steer_preview = "".join(steer_deltas).strip()
interrupt_turn = await thread.turn(
TextInput("Count from 1 to 200 with commas, then one summary sentence.")
"Count from 1 to 200 with commas, then one summary sentence."
)
interrupt_result = await interrupt_turn.interrupt()

View File

@@ -9,14 +9,12 @@ from _bootstrap import ensure_local_sdk_src, runtime_config
ensure_local_sdk_src()
from openai_codex import Codex, TextInput
from openai_codex import Codex
with Codex(config=runtime_config()) as codex:
thread = codex.thread_start(model="gpt-5.4", config={"model_reasoning_effort": "high"})
steer_turn = thread.turn(
TextInput("Count from 1 to 40 with commas, then one summary sentence.")
)
steer_result = steer_turn.steer(TextInput("Keep it brief and stop after 10 numbers."))
steer_turn = thread.turn("Count from 1 to 40 with commas, then one summary sentence.")
steer_result = steer_turn.steer("Keep it brief and stop after 10 numbers.")
steer_event_count = 0
steer_completed_status = None
@@ -33,9 +31,7 @@ with Codex(config=runtime_config()) as codex:
raise RuntimeError("stream ended without turn/completed")
steer_preview = "".join(steer_deltas).strip()
interrupt_turn = thread.turn(
TextInput("Count from 1 to 200 with commas, then one summary sentence.")
)
interrupt_turn = thread.turn("Count from 1 to 200 with commas, then one summary sentence.")
interrupt_result = interrupt_turn.interrupt()
interrupt_event_count = 0

View File

@@ -8,6 +8,9 @@ Each example folder contains runnable versions:
All examples intentionally use only public SDK exports from `openai_codex`
and `openai_codex.types`.
Examples use plain strings for text-only turns and typed input objects for
multimodal or structured input lists.
## Prerequisites
- Python `>=3.10`

View File

@@ -12,6 +12,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "1b6614a5",
"metadata": {},
"outputs": [],
"source": [
@@ -97,6 +98,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "137a6d64",
"metadata": {},
"outputs": [],
"source": [
@@ -115,6 +117,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "5fae892d",
"metadata": {},
"outputs": [],
"source": [
@@ -122,46 +125,42 @@
"with Codex() as codex:\n",
" # Open this URL and call `wait()` without canceling when completing login for real.\n",
" login = codex.login_chatgpt()\n",
" canceled = login.cancel()\n",
" print('Please complete login at:', login.auth_url)\n",
" completed = login.wait()\n",
" account = codex.account()\n",
"\n",
" print('login.id:', login.login_id)\n",
" print('login.auth_url:', login.auth_url)\n",
" print('login.cancel.status:', canceled.status)\n",
" print('login.completed.success:', completed.success)\n",
" print('account.requires_openai_auth:', account.requires_openai_auth)\n"
" print('account:', account.email)\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ebdc04d9",
"metadata": {},
"outputs": [],
"source": [
"# Cell 3: simple sync conversation\n",
"with Codex() as codex:\n",
" thread = codex.thread_start(model='gpt-5.4', config={'model_reasoning_effort': 'high'})\n",
" turn = thread.turn(TextInput('Explain gradient descent in 3 bullets.'))\n",
" result = turn.run()\n",
" print('server:', server_label(codex.metadata))\n",
" print('status:', result.status)\n",
" print(result.final_response)\n",
" print('items:', len(result.items))\n"
" result = thread.run('Explain gradient descent in 3 bullets.')\n",
" print(result.final_response)\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "bb4abb96",
"metadata": {},
"outputs": [],
"source": [
"# Cell 4: multi-turn continuity in same thread\n",
"with Codex() as codex:\n",
" thread = codex.thread_start(model='gpt-5.4', config={'model_reasoning_effort': 'high'})\n",
"\n",
" first = thread.turn(TextInput('Give a short summary of transformers.')).run()\n",
" second = thread.turn(TextInput('Now explain that to a high-school student.')).run()\n",
" first = thread.turn('Give a short summary of transformers.').run()\n",
" second = thread.turn('Now explain that to a high-school student.').run()\n",
" print('first status:', first.status)\n",
" print('second status:', second.status)\n",
" print('second text:', second.final_response)\n"
@@ -170,14 +169,15 @@
{
"cell_type": "code",
"execution_count": null,
"id": "8b0c80fd",
"metadata": {},
"outputs": [],
"source": [
"# Cell 5: full thread lifecycle and branching (sync)\n",
"with Codex() as codex:\n",
" thread = codex.thread_start(model='gpt-5.4', config={'model_reasoning_effort': 'high'})\n",
" first = thread.turn(TextInput('One sentence about structured planning.')).run()\n",
" second = thread.turn(TextInput('Now restate it for a junior engineer.')).run()\n",
" first = thread.turn('One sentence about structured planning.').run()\n",
" second = thread.turn('Now restate it for a junior engineer.').run()\n",
"\n",
" reopened = codex.thread_resume(thread.id)\n",
" listing_active = codex.thread_list(limit=20, archived=False)\n",
@@ -193,10 +193,10 @@
" model='gpt-5.4',\n",
" config={'model_reasoning_effort': 'high'},\n",
" )\n",
" resumed_result = resumed.turn(TextInput('Continue in one short sentence.')).run()\n",
" resumed_result = resumed.turn('Continue in one short sentence.').run()\n",
"\n",
" forked = codex.thread_fork(unarchived.id, model='gpt-5.4')\n",
" forked_result = forked.turn(TextInput('Take a different angle in one short sentence.')).run()\n",
" forked_result = forked.turn('Take a different angle in one short sentence.').run()\n",
"\n",
" compact_result = unarchived.compact()\n",
"\n",
@@ -214,6 +214,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "310db8c0",
"metadata": {},
"outputs": [],
"source": [
@@ -242,7 +243,7 @@
"with Codex() as codex:\n",
" thread = codex.thread_start(model='gpt-5.4', config={'model_reasoning_effort': 'high'})\n",
" turn = thread.turn(\n",
" TextInput('Propose a safe production feature-flag rollout. Return JSON matching the schema.'),\n",
" 'Propose a safe production feature-flag rollout. Return JSON matching the schema.',\n",
" cwd=str(Path.cwd()),\n",
" effort=ReasoningEffort.medium,\n",
" model='gpt-5.4',\n",
@@ -259,6 +260,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "7a33c97d",
"metadata": {},
"outputs": [],
"source": [
@@ -321,7 +323,7 @@
" thread = codex.thread_start(model=selected_model.model, config={'model_reasoning_effort': selected_effort.value})\n",
"\n",
" first = thread.turn(\n",
" TextInput('Give one short sentence about reliable production releases.'),\n",
" 'Give one short sentence about reliable production releases.',\n",
" model=selected_model.model,\n",
" effort=selected_effort,\n",
" ).run()\n",
@@ -329,7 +331,7 @@
" print('items:', len(first.items))\n",
"\n",
" second = thread.turn(\n",
" TextInput('Return JSON for a safe feature-flag rollout plan.'),\n",
" 'Return JSON for a safe feature-flag rollout plan.',\n",
" cwd=str(Path.cwd()),\n",
" effort=selected_effort,\n",
" model=selected_model.model,\n",
@@ -345,6 +347,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "e9aef26a",
"metadata": {},
"outputs": [],
"source": [
@@ -364,6 +367,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "a0cecc6c",
"metadata": {},
"outputs": [],
"source": [
@@ -382,6 +386,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "91afa2b8",
"metadata": {},
"outputs": [],
"source": [
@@ -390,7 +395,7 @@
" thread = codex.thread_start(model='gpt-5.4', config={'model_reasoning_effort': 'high'})\n",
"\n",
" result = retry_on_overload(\n",
" lambda: thread.turn(TextInput('List 5 failure modes in distributed systems.')).run(),\n",
" lambda: thread.turn('List 5 failure modes in distributed systems.').run(),\n",
" max_attempts=3,\n",
" initial_delay_s=0.25,\n",
" max_delay_s=2.0,\n",
@@ -402,6 +407,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "103be934",
"metadata": {},
"outputs": [],
"source": [
@@ -412,8 +418,8 @@
"async def async_lifecycle_demo():\n",
" async with AsyncCodex() as codex:\n",
" thread = await codex.thread_start(model='gpt-5.4', config={'model_reasoning_effort': 'high'})\n",
" first = await (await thread.turn(TextInput('One sentence about structured planning.'))).run()\n",
" second = await (await thread.turn(TextInput('Now restate it for a junior engineer.'))).run()\n",
" first = await (await thread.turn('One sentence about structured planning.')).run()\n",
" second = await (await thread.turn('Now restate it for a junior engineer.')).run()\n",
"\n",
" reopened = await codex.thread_resume(thread.id)\n",
" listing_active = await codex.thread_list(limit=20, archived=False)\n",
@@ -429,10 +435,10 @@
" model='gpt-5.4',\n",
" config={'model_reasoning_effort': 'high'},\n",
" )\n",
" resumed_result = await (await resumed.turn(TextInput('Continue in one short sentence.'))).run()\n",
" resumed_result = await (await resumed.turn('Continue in one short sentence.')).run()\n",
"\n",
" forked = await codex.thread_fork(unarchived.id, model='gpt-5.4')\n",
" forked_result = await (await forked.turn(TextInput('Take a different angle in one short sentence.'))).run()\n",
" forked_result = await (await forked.turn('Take a different angle in one short sentence.')).run()\n",
"\n",
" compact_result = await unarchived.compact()\n",
"\n",
@@ -453,6 +459,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "365aa10c",
"metadata": {},
"outputs": [],
"source": [
@@ -463,9 +470,9 @@
"async def async_stream_demo():\n",
" async with AsyncCodex() as codex:\n",
" thread = await codex.thread_start(model='gpt-5.4', config={'model_reasoning_effort': 'high'})\n",
" steer_turn = await thread.turn(TextInput('Count from 1 to 40 with commas, then one summary sentence.'))\n",
" steer_turn = await thread.turn('Count from 1 to 40 with commas, then one summary sentence.')\n",
"\n",
" steer_result = await steer_turn.steer(TextInput('Keep it brief and stop after 10 numbers.'))\n",
" steer_result = await steer_turn.steer('Keep it brief and stop after 10 numbers.')\n",
"\n",
" steer_event_count = 0\n",
" steer_completed_status = None\n",
@@ -482,7 +489,7 @@
" raise RuntimeError('stream ended without turn/completed')\n",
" steer_preview = ''.join(steer_deltas).strip()\n",
"\n",
" interrupt_turn = await thread.turn(TextInput('Count from 1 to 200 with commas, then one summary sentence.'))\n",
" interrupt_turn = await thread.turn('Count from 1 to 200 with commas, then one summary sentence.')\n",
" interrupt_result = await interrupt_turn.interrupt()\n",
"\n",
" interrupt_event_count = 0\n",
@@ -516,13 +523,21 @@
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"display_name": ".venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"version": "3.10+"
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.14.3"
}
},
"nbformat": 4,

View File

@@ -1040,12 +1040,12 @@ def _render_thread_block(
lines = [
" def turn(",
" self,",
" input: Input,",
" input: RunInput,",
" *,",
*_approval_mode_override_signature_lines(),
*_kw_signature_lines(turn_fields),
" ) -> TurnHandle:",
" wire_input = _to_wire_input(input)",
" wire_input = _to_wire_input(_normalize_run_input(input))",
_approval_mode_assignment_line("_approval_mode_override_settings"),
" params = TurnStartParams(",
" thread_id=self.id,",
@@ -1065,13 +1065,13 @@ def _render_async_thread_block(
lines = [
" async def turn(",
" self,",
" input: Input,",
" input: RunInput,",
" *,",
*_approval_mode_override_signature_lines(),
*_kw_signature_lines(turn_fields),
" ) -> AsyncTurnHandle:",
" await self._codex._ensure_initialized()",
" wire_input = _to_wire_input(input)",
" wire_input = _to_wire_input(_normalize_run_input(input))",
_approval_mode_assignment_line("_approval_mode_override_settings"),
" params = TurnStartParams(",
" thread_id=self.id,",

View File

@@ -14,6 +14,7 @@ from .api import (
InputItem,
LocalImageInput,
MentionInput,
RunInput,
SkillInput,
TextInput,
Thread,
@@ -54,6 +55,7 @@ __all__ = [
"TurnResult",
"Input",
"InputItem",
"RunInput",
"TextInput",
"ImageInput",
"LocalImageInput",

View File

@@ -12,7 +12,7 @@ from ._approval_mode import (
from ._initialize_metadata import validate_initialize_metadata
from ._inputs import (
ImageInput as ImageInput,
Input,
Input as Input,
InputItem as InputItem,
LocalImageInput as LocalImageInput,
MentionInput as MentionInput,
@@ -534,7 +534,7 @@ class Thread:
summary: ReasoningSummary | None = None,
) -> TurnResult:
turn = self.turn(
_normalize_run_input(input),
input,
approval_mode=approval_mode,
cwd=cwd,
effort=effort,
@@ -554,7 +554,7 @@ class Thread:
# BEGIN GENERATED: Thread.flat_methods
def turn(
self,
input: Input,
input: RunInput,
*,
approval_mode: ApprovalMode | None = None,
cwd: str | None = None,
@@ -566,7 +566,7 @@ class Thread:
service_tier: str | None = None,
summary: ReasoningSummary | None = None,
) -> TurnHandle:
wire_input = _to_wire_input(input)
wire_input = _to_wire_input(_normalize_run_input(input))
approval_policy, approvals_reviewer = _approval_mode_override_settings(approval_mode)
params = TurnStartParams(
thread_id=self.id,
@@ -617,7 +617,7 @@ class AsyncThread:
summary: ReasoningSummary | None = None,
) -> TurnResult:
turn = await self.turn(
_normalize_run_input(input),
input,
approval_mode=approval_mode,
cwd=cwd,
effort=effort,
@@ -637,7 +637,7 @@ class AsyncThread:
# BEGIN GENERATED: AsyncThread.flat_methods
async def turn(
self,
input: Input,
input: RunInput,
*,
approval_mode: ApprovalMode | None = None,
cwd: str | None = None,
@@ -650,7 +650,7 @@ class AsyncThread:
summary: ReasoningSummary | None = None,
) -> AsyncTurnHandle:
await self._codex._ensure_initialized()
wire_input = _to_wire_input(input)
wire_input = _to_wire_input(_normalize_run_input(input))
approval_policy, approvals_reviewer = _approval_mode_override_settings(approval_mode)
params = TurnStartParams(
thread_id=self.id,
@@ -694,8 +694,12 @@ class TurnHandle:
thread_id: str
id: str
def steer(self, input: Input) -> TurnSteerResponse:
return self._client.turn_steer(self.thread_id, self.id, _to_wire_input(input))
def steer(self, input: RunInput) -> TurnSteerResponse:
return self._client.turn_steer(
self.thread_id,
self.id,
_to_wire_input(_normalize_run_input(input)),
)
def interrupt(self) -> TurnInterruptResponse:
return self._client.turn_interrupt(self.thread_id, self.id)
@@ -730,12 +734,12 @@ class AsyncTurnHandle:
thread_id: str
id: str
async def steer(self, input: Input) -> TurnSteerResponse:
async def steer(self, input: RunInput) -> TurnSteerResponse:
await self._codex._ensure_initialized()
return await self._codex._client.turn_steer(
self.thread_id,
self.id,
_to_wire_input(input),
_to_wire_input(_normalize_run_input(input)),
)
async def interrupt(self) -> TurnInterruptResponse:

View File

@@ -11,7 +11,7 @@ from app_server_helpers import (
streaming_response,
)
from openai_codex import AsyncCodex, Codex, TextInput
from openai_codex import AsyncCodex, Codex
from openai_codex.generated.v2_all import (
AgentMessageDeltaNotification,
TurnCompletedNotification,
@@ -26,8 +26,9 @@ def test_sync_stream_routes_text_deltas_and_completion(tmp_path) -> None:
with Codex(config=harness.app_server_config()) as codex:
thread = codex.thread_start()
stream = thread.turn(TextInput("stream please")).stream()
stream = thread.turn("stream please").stream()
events = list(stream)
request = harness.responses.single_request()
assert {
"deltas": [
@@ -36,6 +37,7 @@ def test_sync_stream_routes_text_deltas_and_completion(tmp_path) -> None:
if isinstance(event.payload, AgentMessageDeltaNotification)
],
"agent_messages": agent_message_texts(events),
"request_user_texts": request.message_input_texts("user")[-1:],
"completed_statuses": [
event.payload.turn.status
for event in events
@@ -44,6 +46,7 @@ def test_sync_stream_routes_text_deltas_and_completion(tmp_path) -> None:
} == {
"deltas": ["he", "llo"],
"agent_messages": ["hello"],
"request_user_texts": ["stream please"],
"completed_statuses": [TurnStatus.completed],
}
@@ -55,7 +58,7 @@ def test_turn_run_returns_completed_turn(tmp_path) -> None:
with Codex(config=harness.app_server_config()) as codex:
thread = codex.thread_start()
turn = thread.turn(TextInput("complete this turn"))
turn = thread.turn("complete this turn")
completed = turn.run()
assert {
@@ -83,8 +86,9 @@ def test_async_stream_routes_text_deltas_and_completion(tmp_path) -> None:
async with AsyncCodex(config=harness.app_server_config()) as codex:
thread = await codex.thread_start()
turn = await thread.turn(TextInput("async stream please"))
turn = await thread.turn("async stream please")
events = [event async for event in turn.stream()]
request = harness.responses.single_request()
assert {
"deltas": [
@@ -93,6 +97,7 @@ def test_async_stream_routes_text_deltas_and_completion(tmp_path) -> None:
if isinstance(event.payload, AgentMessageDeltaNotification)
],
"agent_messages": agent_message_texts(events),
"request_user_texts": request.message_input_texts("user")[-1:],
"completed_statuses": [
event.payload.turn.status
for event in events
@@ -101,6 +106,7 @@ def test_async_stream_routes_text_deltas_and_completion(tmp_path) -> None:
} == {
"deltas": ["as", "ync"],
"agent_messages": ["async"],
"request_user_texts": ["async stream please"],
"completed_statuses": [TurnStatus.completed],
}
@@ -178,8 +184,8 @@ def test_interleaved_sync_turn_streams_route_by_turn_id(tmp_path) -> None:
with Codex(config=harness.app_server_config()) as codex:
first_thread = codex.thread_start()
second_thread = codex.thread_start()
first_turn = first_thread.turn(TextInput("first"))
second_turn = second_thread.turn(TextInput("second"))
first_turn = first_thread.turn("first")
second_turn = second_thread.turn("second")
first_stream = first_turn.stream()
second_stream = second_turn.stream()
@@ -231,8 +237,8 @@ def test_interleaved_async_turn_streams_route_by_turn_id(tmp_path) -> None:
async with AsyncCodex(config=harness.app_server_config()) as codex:
first_thread = await codex.thread_start()
second_thread = await codex.thread_start()
first_turn = await first_thread.turn(TextInput("async first"))
second_turn = await second_thread.turn(TextInput("async second"))
first_turn = await first_thread.turn("async first")
second_turn = await second_thread.turn("async second")
first_stream = first_turn.stream()
second_stream = second_turn.stream()

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from app_server_harness import AppServerHarness
from app_server_helpers import agent_message_texts, streaming_response
from openai_codex import Codex, TextInput
from openai_codex import Codex
from openai_codex.generated.v2_all import TurnStatus
@@ -21,9 +21,9 @@ def test_turn_steer_adds_follow_up_input(tmp_path) -> None:
with Codex(config=harness.app_server_config()) as codex:
thread = codex.thread_start()
turn = thread.turn(TextInput("Start a steerable turn."))
turn = thread.turn("Start a steerable turn.")
harness.responses.wait_for_requests(1)
steer = turn.steer(TextInput("Use this steering input."))
steer = turn.steer("Use this steering input.")
events = list(turn.stream())
requests = harness.responses.wait_for_requests(2)
@@ -61,7 +61,7 @@ def test_turn_interrupt_stops_active_turn_and_follow_up_runs(tmp_path) -> None:
with Codex(config=harness.app_server_config()) as codex:
thread = codex.thread_start()
interrupted_turn = thread.turn(TextInput("Start a long turn."))
interrupted_turn = thread.turn("Start a long turn.")
harness.responses.wait_for_requests(1)
interrupt_response = interrupted_turn.interrupt()
completed = interrupted_turn.run()

View File

@@ -40,6 +40,7 @@ EXPECTED_ROOT_EXPORTS = [
"TurnResult",
"Input",
"InputItem",
"RunInput",
"TextInput",
"ImageInput",
"LocalImageInput",
@@ -166,6 +167,22 @@ def test_turn_run_methods_return_turn_result() -> None:
)
def test_turn_input_methods_accept_string_shortcut() -> None:
"""Every public turn-input method should accept strings and typed inputs."""
funcs = [
Thread.run,
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")
)
def test_root_exports_approval_mode() -> None:
"""The root package should expose the high-level approval mode enum."""
assert [(mode.name, mode.value) for mode in ApprovalMode] == [

View File

@@ -234,14 +234,14 @@ def test_real_thread_and_turn_start_smoke(runtime_env: PreparedRuntimeEnv) -> No
textwrap.dedent(
"""
import json
from openai_codex import Codex, TextInput
from openai_codex import Codex
with Codex() as codex:
thread = codex.thread_start(
model="gpt-5.4",
config={"model_reasoning_effort": "high"},
)
result = thread.turn(TextInput("hello")).run()
result = thread.turn("hello").run()
print(json.dumps({
"thread_id": thread.id,
"turn_id": result.id,
@@ -331,7 +331,7 @@ def test_real_async_thread_turn_usage_and_ids_smoke(
"""
import asyncio
import json
from openai_codex import AsyncCodex, TextInput
from openai_codex import AsyncCodex
async def main():
async with AsyncCodex() as codex:
@@ -339,7 +339,7 @@ def test_real_async_thread_turn_usage_and_ids_smoke(
model="gpt-5.4",
config={"model_reasoning_effort": "high"},
)
result = await (await thread.turn(TextInput("say ok"))).run()
result = await (await thread.turn("say ok")).run()
print(json.dumps({
"thread_id": thread.id,
"turn_id": result.id,
@@ -458,14 +458,14 @@ def test_real_streaming_smoke_turn_completed(runtime_env: PreparedRuntimeEnv) ->
textwrap.dedent(
"""
import json
from openai_codex import Codex, TextInput
from openai_codex import Codex
with Codex() as codex:
thread = codex.thread_start(
model="gpt-5.4",
config={"model_reasoning_effort": "high"},
)
turn = thread.turn(TextInput("Reply with one short sentence."))
turn = thread.turn("Reply with one short sentence.")
saw_delta = False
saw_completed = False
for event in turn.stream():
@@ -491,16 +491,16 @@ def test_real_turn_interrupt_smoke(runtime_env: PreparedRuntimeEnv) -> None:
textwrap.dedent(
"""
import json
from openai_codex import Codex, TextInput
from openai_codex import Codex
with Codex() as codex:
thread = codex.thread_start(
model="gpt-5.4",
config={"model_reasoning_effort": "high"},
)
turn = thread.turn(TextInput("Count from 1 to 200 with commas."))
turn = thread.turn("Count from 1 to 200 with commas.")
turn.interrupt()
follow_up = thread.turn(TextInput("Say 'ok' only.")).run()
follow_up = thread.turn("Say 'ok' only.").run()
print(json.dumps({"status": follow_up.status.value}))
"""
),