From e7bffc5a20e92cbc64d6c16a1b257d0b2e4cd5df Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Sun, 17 May 2026 09:05:44 -0700 Subject: [PATCH] [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" --- sdk/python/README.md | 2 + sdk/python/docs/api-reference.md | 19 +++-- sdk/python/docs/getting-started.md | 1 + sdk/python/examples/02_turn_run/async.py | 4 +- sdk/python/examples/02_turn_run/sync.py | 4 +- .../examples/03_turn_stream_events/async.py | 4 +- .../examples/03_turn_stream_events/sync.py | 4 +- .../examples/05_existing_thread/async.py | 6 +- .../examples/05_existing_thread/sync.py | 6 +- .../06_thread_lifecycle_and_controls/async.py | 14 ++-- .../06_thread_lifecycle_and_controls/sync.py | 10 +-- sdk/python/examples/09_async_parity/sync.py | 4 +- .../10_error_handling_and_retry/async.py | 3 +- .../10_error_handling_and_retry/sync.py | 3 +- sdk/python/examples/11_cli_mini_app/async.py | 3 +- sdk/python/examples/11_cli_mini_app/sync.py | 3 +- .../12_turn_params_kitchen_sink/async.py | 3 +- .../12_turn_params_kitchen_sink/sync.py | 3 +- .../13_model_select_and_turn_params/async.py | 5 +- .../13_model_select_and_turn_params/sync.py | 5 +- sdk/python/examples/14_turn_controls/async.py | 10 +-- sdk/python/examples/14_turn_controls/sync.py | 12 +-- sdk/python/examples/README.md | 3 + sdk/python/notebooks/sdk_walkthrough.ipynb | 73 +++++++++++-------- sdk/python/scripts/update_sdk_artifacts.py | 8 +- sdk/python/src/openai_codex/__init__.py | 2 + sdk/python/src/openai_codex/api.py | 26 ++++--- sdk/python/tests/test_app_server_streaming.py | 22 ++++-- .../tests/test_app_server_turn_controls.py | 8 +- .../tests/test_public_api_signatures.py | 17 +++++ .../tests/test_real_app_server_integration.py | 18 ++--- 31 files changed, 171 insertions(+), 134 deletions(-) diff --git a/sdk/python/README.md b/sdk/python/README.md index 9107d15ba9..5b8e6b8178 100644 --- a/sdk/python/README.md +++ b/sdk/python/README.md @@ -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. diff --git a/sdk/python/docs/api-reference.md b/sdk/python/docs/api-reference.md index 4bee871f8c..db78ff5f1d 100644 --- a/sdk/python/docs/api-reference.md +++ b/sdk/python/docs/api-reference.md @@ -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: diff --git a/sdk/python/docs/getting-started.md b/sdk/python/docs/getting-started.md index 1df0026c68..b21db2a207 100644 --- a/sdk/python/docs/getting-started.md +++ b/sdk/python/docs/getting-started.md @@ -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 diff --git a/sdk/python/examples/02_turn_run/async.py b/sdk/python/examples/02_turn_run/async.py index debfad8d6e..c4071cde47 100644 --- a/sdk/python/examples/02_turn_run/async.py +++ b/sdk/python/examples/02_turn_run/async.py @@ -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) diff --git a/sdk/python/examples/02_turn_run/sync.py b/sdk/python/examples/02_turn_run/sync.py index c42bffa1f2..af22a220ba 100644 --- a/sdk/python/examples/02_turn_run/sync.py +++ b/sdk/python/examples/02_turn_run/sync.py @@ -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) diff --git a/sdk/python/examples/03_turn_stream_events/async.py b/sdk/python/examples/03_turn_stream_events/async.py index f0d3f1427d..e145b2222b 100644 --- a/sdk/python/examples/03_turn_stream_events/async.py +++ b/sdk/python/examples/03_turn_stream_events/async.py @@ -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 diff --git a/sdk/python/examples/03_turn_stream_events/sync.py b/sdk/python/examples/03_turn_stream_events/sync.py index 5a76bcb70f..f5a734dc38 100644 --- a/sdk/python/examples/03_turn_stream_events/sync.py +++ b/sdk/python/examples/03_turn_stream_events/sync.py @@ -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 diff --git a/sdk/python/examples/05_existing_thread/async.py b/sdk/python/examples/05_existing_thread/async.py index df3946ac87..e1f4db7105 100644 --- a/sdk/python/examples/05_existing_thread/async.py +++ b/sdk/python/examples/05_existing_thread/async.py @@ -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) diff --git a/sdk/python/examples/05_existing_thread/sync.py b/sdk/python/examples/05_existing_thread/sync.py index a0f0d37e27..7110ebac7f 100644 --- a/sdk/python/examples/05_existing_thread/sync.py +++ b/sdk/python/examples/05_existing_thread/sync.py @@ -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) diff --git a/sdk/python/examples/06_thread_lifecycle_and_controls/async.py b/sdk/python/examples/06_thread_lifecycle_and_controls/async.py index 24afbde6c2..7a786a1d79 100644 --- a/sdk/python/examples/06_thread_lifecycle_and_controls/async.py +++ b/sdk/python/examples/06_thread_lifecycle_and_controls/async.py @@ -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() diff --git a/sdk/python/examples/06_thread_lifecycle_and_controls/sync.py b/sdk/python/examples/06_thread_lifecycle_and_controls/sync.py index 0958002dd9..744b56ebb7 100644 --- a/sdk/python/examples/06_thread_lifecycle_and_controls/sync.py +++ b/sdk/python/examples/06_thread_lifecycle_and_controls/sync.py @@ -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() diff --git a/sdk/python/examples/09_async_parity/sync.py b/sdk/python/examples/09_async_parity/sync.py index 4b5d567159..a7ac8abbd7 100644 --- a/sdk/python/examples/09_async_parity/sync.py +++ b/sdk/python/examples/09_async_parity/sync.py @@ -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) diff --git a/sdk/python/examples/10_error_handling_and_retry/async.py b/sdk/python/examples/10_error_handling_and_retry/async.py index f1711bffc9..71f954544f 100644 --- a/sdk/python/examples/10_error_handling_and_retry/async.py +++ b/sdk/python/examples/10_error_handling_and_retry/async.py @@ -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 diff --git a/sdk/python/examples/10_error_handling_and_retry/sync.py b/sdk/python/examples/10_error_handling_and_retry/sync.py index c134063c80..9c0f9f30e5 100644 --- a/sdk/python/examples/10_error_handling_and_retry/sync.py +++ b/sdk/python/examples/10_error_handling_and_retry/sync.py @@ -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, diff --git a/sdk/python/examples/11_cli_mini_app/async.py b/sdk/python/examples/11_cli_mini_app/async.py index df9fc89b86..e2a695a924 100644 --- a/sdk/python/examples/11_cli_mini_app/async.py +++ b/sdk/python/examples/11_cli_mini_app/async.py @@ -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 diff --git a/sdk/python/examples/11_cli_mini_app/sync.py b/sdk/python/examples/11_cli_mini_app/sync.py index fd16f930dc..0bf906647c 100644 --- a/sdk/python/examples/11_cli_mini_app/sync.py +++ b/sdk/python/examples/11_cli_mini_app/sync.py @@ -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 diff --git a/sdk/python/examples/12_turn_params_kitchen_sink/async.py b/sdk/python/examples/12_turn_params_kitchen_sink/async.py index 258d674a88..f44ea229b0 100644 --- a/sdk/python/examples/12_turn_params_kitchen_sink/async.py +++ b/sdk/python/examples/12_turn_params_kitchen_sink/async.py @@ -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, diff --git a/sdk/python/examples/12_turn_params_kitchen_sink/sync.py b/sdk/python/examples/12_turn_params_kitchen_sink/sync.py index feced36f20..160a9bf187 100644 --- a/sdk/python/examples/12_turn_params_kitchen_sink/sync.py +++ b/sdk/python/examples/12_turn_params_kitchen_sink/sync.py @@ -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, diff --git a/sdk/python/examples/13_model_select_and_turn_params/async.py b/sdk/python/examples/13_model_select_and_turn_params/async.py index 31a0c9c243..144169c46c 100644 --- a/sdk/python/examples/13_model_select_and_turn_params/async.py +++ b/sdk/python/examples/13_model_select_and_turn_params/async.py @@ -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, diff --git a/sdk/python/examples/13_model_select_and_turn_params/sync.py b/sdk/python/examples/13_model_select_and_turn_params/sync.py index 9252f3913c..962665363f 100644 --- a/sdk/python/examples/13_model_select_and_turn_params/sync.py +++ b/sdk/python/examples/13_model_select_and_turn_params/sync.py @@ -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, diff --git a/sdk/python/examples/14_turn_controls/async.py b/sdk/python/examples/14_turn_controls/async.py index f044dadbbe..86e8def04f 100644 --- a/sdk/python/examples/14_turn_controls/async.py +++ b/sdk/python/examples/14_turn_controls/async.py @@ -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() diff --git a/sdk/python/examples/14_turn_controls/sync.py b/sdk/python/examples/14_turn_controls/sync.py index 6b6d046a43..c8f1a75e28 100644 --- a/sdk/python/examples/14_turn_controls/sync.py +++ b/sdk/python/examples/14_turn_controls/sync.py @@ -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 diff --git a/sdk/python/examples/README.md b/sdk/python/examples/README.md index bef1a16c0b..eef3893721 100644 --- a/sdk/python/examples/README.md +++ b/sdk/python/examples/README.md @@ -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` diff --git a/sdk/python/notebooks/sdk_walkthrough.ipynb b/sdk/python/notebooks/sdk_walkthrough.ipynb index 1eb80b3a71..bd313a02b1 100644 --- a/sdk/python/notebooks/sdk_walkthrough.ipynb +++ b/sdk/python/notebooks/sdk_walkthrough.ipynb @@ -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, diff --git a/sdk/python/scripts/update_sdk_artifacts.py b/sdk/python/scripts/update_sdk_artifacts.py index a40a4aa9c3..5cad14cf5e 100755 --- a/sdk/python/scripts/update_sdk_artifacts.py +++ b/sdk/python/scripts/update_sdk_artifacts.py @@ -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,", diff --git a/sdk/python/src/openai_codex/__init__.py b/sdk/python/src/openai_codex/__init__.py index b3db7e3175..3bc018a769 100644 --- a/sdk/python/src/openai_codex/__init__.py +++ b/sdk/python/src/openai_codex/__init__.py @@ -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", diff --git a/sdk/python/src/openai_codex/api.py b/sdk/python/src/openai_codex/api.py index 347f95eeae..f14a311789 100644 --- a/sdk/python/src/openai_codex/api.py +++ b/sdk/python/src/openai_codex/api.py @@ -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: diff --git a/sdk/python/tests/test_app_server_streaming.py b/sdk/python/tests/test_app_server_streaming.py index 02d089f2f1..3b79d9def3 100644 --- a/sdk/python/tests/test_app_server_streaming.py +++ b/sdk/python/tests/test_app_server_streaming.py @@ -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() diff --git a/sdk/python/tests/test_app_server_turn_controls.py b/sdk/python/tests/test_app_server_turn_controls.py index af02f6b74f..dd994b88f9 100644 --- a/sdk/python/tests/test_app_server_turn_controls.py +++ b/sdk/python/tests/test_app_server_turn_controls.py @@ -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() diff --git a/sdk/python/tests/test_public_api_signatures.py b/sdk/python/tests/test_public_api_signatures.py index c7a00d3897..9a1212dfdf 100644 --- a/sdk/python/tests/test_public_api_signatures.py +++ b/sdk/python/tests/test_public_api_signatures.py @@ -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] == [ diff --git a/sdk/python/tests/test_real_app_server_integration.py b/sdk/python/tests/test_real_app_server_integration.py index 08f07c986c..5c4ec83fd4 100644 --- a/sdk/python/tests/test_real_app_server_integration.py +++ b/sdk/python/tests/test_real_app_server_integration.py @@ -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})) """ ),