[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

@@ -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`