mirror of
https://github.com/openai/codex.git
synced 2026-05-01 20:02:05 +03:00
python-sdk: expose canonical app-server types (2026-03-16)
Remove the SDK alias/result layers so the wrapper surface returns canonical generated app-server models directly. - delete public type alias modules and regenerate v2_all.py against current schema - return InitializeResponse from metadata and generated Turn from run() - update docs, examples, notebook, and tests to use canonical generated models and repo-only text extraction helpers Validation: - PYTHONPATH=sdk/python/src python3 -m pytest sdk/python/tests - GH_TOKEN="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" RUN_REAL_CODEX_TESTS=1 PYTHONPATH=sdk/python/src python3 -m pytest sdk/python/tests -rs Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -117,7 +117,7 @@ def test_python_codegen_schema_annotation_adds_stable_variant_titles() -> None:
|
||||
]
|
||||
assert ask_for_approval_titles == [
|
||||
"AskForApprovalValue",
|
||||
"RejectAskForApproval",
|
||||
"GranularAskForApproval",
|
||||
]
|
||||
|
||||
reasoning_summary_titles = [
|
||||
|
||||
@@ -10,12 +10,11 @@ import codex_app_server.public_api as public_api_module
|
||||
from codex_app_server.client import AppServerClient
|
||||
from codex_app_server.generated.v2_all import (
|
||||
AgentMessageDeltaNotification,
|
||||
RawResponseItemCompletedNotification,
|
||||
ThreadTokenUsageUpdatedNotification,
|
||||
TurnCompletedNotification,
|
||||
TurnStatus,
|
||||
)
|
||||
from codex_app_server.models import InitializeResponse, Notification
|
||||
from codex_app_server.public_api import AsyncCodex, AsyncTurn, Codex, Turn
|
||||
from codex_app_server.public_types import TurnStatus
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
@@ -39,60 +38,6 @@ def _delta_notification(
|
||||
)
|
||||
|
||||
|
||||
def _raw_response_notification(
|
||||
*,
|
||||
thread_id: str = "thread-1",
|
||||
turn_id: str = "turn-1",
|
||||
text: str = "raw-text",
|
||||
) -> Notification:
|
||||
return Notification(
|
||||
method="rawResponseItem/completed",
|
||||
payload=RawResponseItemCompletedNotification.model_validate(
|
||||
{
|
||||
"item": {
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"content": [{"type": "output_text", "text": text}],
|
||||
},
|
||||
"threadId": thread_id,
|
||||
"turnId": turn_id,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _usage_notification(
|
||||
*,
|
||||
thread_id: str = "thread-1",
|
||||
turn_id: str = "turn-1",
|
||||
) -> Notification:
|
||||
return Notification(
|
||||
method="thread/tokenUsage/updated",
|
||||
payload=ThreadTokenUsageUpdatedNotification.model_validate(
|
||||
{
|
||||
"threadId": thread_id,
|
||||
"turnId": turn_id,
|
||||
"tokenUsage": {
|
||||
"last": {
|
||||
"cachedInputTokens": 0,
|
||||
"inputTokens": 1,
|
||||
"outputTokens": 2,
|
||||
"reasoningOutputTokens": 0,
|
||||
"totalTokens": 3,
|
||||
},
|
||||
"total": {
|
||||
"cachedInputTokens": 0,
|
||||
"inputTokens": 1,
|
||||
"outputTokens": 2,
|
||||
"reasoningOutputTokens": 0,
|
||||
"totalTokens": 3,
|
||||
},
|
||||
},
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _completed_notification(
|
||||
*,
|
||||
thread_id: str = "thread-1",
|
||||
@@ -101,7 +46,7 @@ def _completed_notification(
|
||||
) -> Notification:
|
||||
return Notification(
|
||||
method="turn/completed",
|
||||
payload=public_api_module.TurnCompletedNotificationPayload.model_validate(
|
||||
payload=TurnCompletedNotification.model_validate(
|
||||
{
|
||||
"threadId": thread_id,
|
||||
"turn": {
|
||||
@@ -259,12 +204,10 @@ def test_async_turn_stream_rejects_second_active_consumer() -> None:
|
||||
asyncio.run(scenario())
|
||||
|
||||
|
||||
def test_turn_run_falls_back_to_completed_raw_response_text() -> None:
|
||||
def test_turn_run_returns_completed_turn_payload() -> None:
|
||||
client = AppServerClient()
|
||||
notifications: deque[Notification] = deque(
|
||||
[
|
||||
_raw_response_notification(text="hello from raw response"),
|
||||
_usage_notification(),
|
||||
_completed_notification(),
|
||||
]
|
||||
)
|
||||
@@ -272,8 +215,9 @@ def test_turn_run_falls_back_to_completed_raw_response_text() -> None:
|
||||
|
||||
result = Turn(client, "thread-1", "turn-1").run()
|
||||
|
||||
assert result.id == "turn-1"
|
||||
assert result.status == TurnStatus.completed
|
||||
assert result.text == "hello from raw response"
|
||||
assert result.items == []
|
||||
|
||||
|
||||
def test_retry_examples_compare_status_with_enum() -> None:
|
||||
|
||||
@@ -196,15 +196,15 @@ def test_lifecycle_methods_are_codex_scoped() -> None:
|
||||
|
||||
|
||||
def test_initialize_metadata_parses_user_agent_shape() -> None:
|
||||
parsed = Codex._parse_initialize(InitializeResponse.model_validate({"userAgent": "codex-cli/1.2.3"}))
|
||||
assert parsed.user_agent == "codex-cli/1.2.3"
|
||||
assert parsed.server_name == "codex-cli"
|
||||
assert parsed.server_version == "1.2.3"
|
||||
payload = InitializeResponse.model_validate({"userAgent": "codex-cli/1.2.3"})
|
||||
parsed = Codex._validate_initialize(payload)
|
||||
assert parsed is payload
|
||||
assert parsed.userAgent == "codex-cli/1.2.3"
|
||||
|
||||
|
||||
def test_initialize_metadata_requires_non_empty_information() -> None:
|
||||
try:
|
||||
Codex._parse_initialize(InitializeResponse.model_validate({}))
|
||||
Codex._validate_initialize(InitializeResponse.model_validate({}))
|
||||
except RuntimeError as exc:
|
||||
assert "missing required metadata" in str(exc)
|
||||
else:
|
||||
|
||||
@@ -182,10 +182,11 @@ def test_real_initialize_and_model_list(runtime_env: PreparedRuntimeEnv) -> None
|
||||
|
||||
with Codex() as codex:
|
||||
models = codex.models(include_hidden=True)
|
||||
server = codex.metadata.serverInfo
|
||||
print(json.dumps({
|
||||
"user_agent": codex.metadata.user_agent,
|
||||
"server_name": codex.metadata.server_name,
|
||||
"server_version": codex.metadata.server_version,
|
||||
"user_agent": codex.metadata.userAgent,
|
||||
"server_name": None if server is None else server.name,
|
||||
"server_version": None if server is None else server.version,
|
||||
"model_count": len(models.data),
|
||||
}))
|
||||
"""
|
||||
@@ -193,8 +194,10 @@ def test_real_initialize_and_model_list(runtime_env: PreparedRuntimeEnv) -> None
|
||||
)
|
||||
|
||||
assert isinstance(data["user_agent"], str) and data["user_agent"].strip()
|
||||
assert isinstance(data["server_name"], str) and data["server_name"].strip()
|
||||
assert isinstance(data["server_version"], str) and data["server_version"].strip()
|
||||
if data["server_name"] is not None:
|
||||
assert isinstance(data["server_name"], str) and data["server_name"].strip()
|
||||
if data["server_version"] is not None:
|
||||
assert isinstance(data["server_version"], str) and data["server_version"].strip()
|
||||
assert isinstance(data["model_count"], int)
|
||||
|
||||
|
||||
@@ -212,13 +215,17 @@ def test_real_thread_and_turn_start_smoke(runtime_env: PreparedRuntimeEnv) -> No
|
||||
config={"model_reasoning_effort": "high"},
|
||||
)
|
||||
result = thread.turn(TextInput("hello")).run()
|
||||
persisted = thread.read(include_turns=True)
|
||||
persisted_turn = next(
|
||||
(turn for turn in persisted.thread.turns or [] if turn.id == result.id),
|
||||
None,
|
||||
)
|
||||
print(json.dumps({
|
||||
"thread_id": result.thread_id,
|
||||
"turn_id": result.turn_id,
|
||||
"items_count": len(result.items),
|
||||
"has_usage": result.usage is not None,
|
||||
"usage_thread_id": None if result.usage is None else result.usage.thread_id,
|
||||
"usage_turn_id": None if result.usage is None else result.usage.turn_id,
|
||||
"thread_id": thread.id,
|
||||
"turn_id": result.id,
|
||||
"status": result.status.value,
|
||||
"items_count": len(result.items or []),
|
||||
"persisted_items_count": 0 if persisted_turn is None else len(persisted_turn.items or []),
|
||||
}))
|
||||
"""
|
||||
),
|
||||
@@ -226,10 +233,9 @@ def test_real_thread_and_turn_start_smoke(runtime_env: PreparedRuntimeEnv) -> No
|
||||
|
||||
assert isinstance(data["thread_id"], str) and data["thread_id"].strip()
|
||||
assert isinstance(data["turn_id"], str) and data["turn_id"].strip()
|
||||
assert data["status"] == "completed"
|
||||
assert isinstance(data["items_count"], int)
|
||||
assert data["has_usage"] is True
|
||||
assert data["usage_thread_id"] == data["thread_id"]
|
||||
assert data["usage_turn_id"] == data["turn_id"]
|
||||
assert isinstance(data["persisted_items_count"], int)
|
||||
|
||||
|
||||
def test_real_async_thread_turn_usage_and_ids_smoke(
|
||||
@@ -250,13 +256,17 @@ def test_real_async_thread_turn_usage_and_ids_smoke(
|
||||
config={"model_reasoning_effort": "high"},
|
||||
)
|
||||
result = await (await thread.turn(TextInput("say ok"))).run()
|
||||
persisted = await thread.read(include_turns=True)
|
||||
persisted_turn = next(
|
||||
(turn for turn in persisted.thread.turns or [] if turn.id == result.id),
|
||||
None,
|
||||
)
|
||||
print(json.dumps({
|
||||
"thread_id": result.thread_id,
|
||||
"turn_id": result.turn_id,
|
||||
"items_count": len(result.items),
|
||||
"has_usage": result.usage is not None,
|
||||
"usage_thread_id": None if result.usage is None else result.usage.thread_id,
|
||||
"usage_turn_id": None if result.usage is None else result.usage.turn_id,
|
||||
"thread_id": thread.id,
|
||||
"turn_id": result.id,
|
||||
"status": result.status.value,
|
||||
"items_count": len(result.items or []),
|
||||
"persisted_items_count": 0 if persisted_turn is None else len(persisted_turn.items or []),
|
||||
}))
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -266,10 +276,9 @@ def test_real_async_thread_turn_usage_and_ids_smoke(
|
||||
|
||||
assert isinstance(data["thread_id"], str) and data["thread_id"].strip()
|
||||
assert isinstance(data["turn_id"], str) and data["turn_id"].strip()
|
||||
assert data["status"] == "completed"
|
||||
assert isinstance(data["items_count"], int)
|
||||
assert data["has_usage"] is True
|
||||
assert data["usage_thread_id"] == data["thread_id"]
|
||||
assert data["usage_turn_id"] == data["turn_id"]
|
||||
assert isinstance(data["persisted_items_count"], int)
|
||||
|
||||
|
||||
def test_notebook_bootstrap_resolves_sdk_and_runtime_from_unrelated_cwd(
|
||||
@@ -386,10 +395,10 @@ def test_real_examples_run_and_assert(
|
||||
|
||||
if folder == "01_quickstart_constructor":
|
||||
assert "Status:" in out and "Text:" in out
|
||||
assert "Server: None None" not in out
|
||||
assert "Server: unknown" not in out
|
||||
elif folder == "02_turn_run":
|
||||
assert "thread_id:" in out and "turn_id:" in out and "status:" in out
|
||||
assert "usage: None" not in out
|
||||
assert "persisted.items.count:" in out
|
||||
elif folder == "03_turn_stream_events":
|
||||
assert "turn/completed" in out
|
||||
elif folder == "04_models_and_metadata":
|
||||
@@ -409,7 +418,6 @@ def test_real_examples_run_and_assert(
|
||||
elif folder == "11_cli_mini_app":
|
||||
assert "Thread:" in out
|
||||
elif folder == "12_turn_params_kitchen_sink":
|
||||
assert "Status:" in out and "Usage:" in out
|
||||
assert "Status:" in out and "Items:" in out
|
||||
elif folder == "13_model_select_and_turn_params":
|
||||
assert "selected.model:" in out and "agent.message.params:" in out and "usage.params:" in out
|
||||
assert "usage.params: None" not in out
|
||||
assert "selected.model:" in out and "agent.message.params:" in out and "items.params:" in out
|
||||
|
||||
Reference in New Issue
Block a user