Skip to content

python-sdk: generated type foundation (all v2 schemas)#13953

Merged
shaqayeq-oai merged 10 commits intomainfrom
shaqayeq/python-sdk-types-foundation
Mar 10, 2026
Merged

python-sdk: generated type foundation (all v2 schemas)#13953
shaqayeq-oai merged 10 commits intomainfrom
shaqayeq/python-sdk-types-foundation

Conversation

@shaqayeq-oai
Copy link
Copy Markdown
Collaborator

Summary

Foundation PR only (base for PR #3).

This PR contains the SDK runtime foundation and generated artifacts:

  • pinned runtime binary in sdk/python/bin/ (codex or codex.exe by platform)
  • single maintenance script: sdk/python/scripts/update_sdk_artifacts.py
  • generated protocol/types artifacts under:
    • sdk/python/src/codex_app_server/generated/protocol_types.py
    • sdk/python/src/codex_app_server/generated/schema_types.py
    • sdk/python/src/codex_app_server/generated/v2_all/*
  • generation-contract test wiring (tests/test_contract_generation.py)

Release asset behavior

update_sdk_artifacts.py now:

  • selects latest release by channel (--channel stable|alpha)
  • resolves the correct asset for current OS/arch
  • extracts platform binary (codex on macOS/Linux, codex.exe on Windows)
  • keeps runtime on single pinned binary source in sdk/python/bin/

Scope boundary

Validation

  • Ran: python scripts/update_sdk_artifacts.py --channel stable
  • Regenerated and committed resulting generated artifacts
  • Local tests pass on branch
Description:
- pin datamodel-code-generator to an exact version and invoke it through the active interpreter
- make generate_types own the maintained generated surfaces and regenerate committed artifacts
- make sdk/python tests hermetic and regeneration checks idempotent
@etraut-openai etraut-openai added the oai PRs contributed by OpenAI employees label Mar 9, 2026
Copy link
Copy Markdown
Collaborator

@owenlin0 owenlin0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we run datamodel_code_generator against the json schema bundle at codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json instead of over the codex-rs/app-server-protocol/schema/json/v2/ directory? I think that should fix duplicate definitions (e.g. PlanType).

Also, I asked Codex to walk me through the app-server schema -> python pipeline, and it seems like we have these stages:

  1. Source To Raw Models: app-server json schema -> pydantic types
  2. Raw Models To Curated Python Shapes: a subset of manually curated types which outputs both TypedDict and Pydantic types
  3. Raw Models To Public API Signatures: generated Pydantic model -> ergonomic Python method signature

Can we simplify? Codex suggests this:

The simplest functional shape is:

  • one canonical generated wire-model layer
  • Pydantic only
  • no handwritten “curated schema shapes”
  • a thin handwritten client/public API on top
    That is the direction I’d recommend.

Use the bundled schema codex_app_server_protocol.v2.schemas.json as the canonical input, and generate a single Pydantic model namespace from it.

The Python-facing models should use snake_case field names, but serialize and validate against camelCase on the wire.

I think that should allow us to delete protocol_types.py and schema_types.py

("codex_app_server.generated.v2_all.TurnStartParams", "Personality"): "TurnPersonality",
("codex_app_server.generated.v2_all.TurnStartParams", "ReasoningEffort"): "TurnReasoningEffort",
("codex_app_server.generated.v2_all.TurnStartParams", "SandboxPolicy"): "TurnSandboxPolicy",
("codex_app_server.generated.v2_all.TurnStartParams", "ReasoningSummary"): "TurnReasoningSummary",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious why we need these type aliases, since things like ReasoningSummary is meant to be a shared type across a couple RPCs (same with the others here like Personality etc.)


class ContextCompactedNotification(BaseModel):
threadId: str
turnId: str
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to make things more pythonic, wonder if it's possible to produce these generated fields as snake_case, but send it over the wire to app-server as camelCase?

@@ -0,0 +1,77 @@
# Codex App Server Python SDK
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably worth adding (experimental) for now

source: dict[str, Any]
status: dict[str, Any]
turns: list[TurnObject]
updatedAt: int
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

snake_case would be so nice here


# Generated by scripts/update_sdk_artifacts.py

class ThreadObject(TypedDict):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious, why do we need a *Object suffix?

updatedAt: int

@dataclass(slots=True, kw_only=True)
class Thread:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the difference between ThreadDict vs. Thread (defined here) vs. ThreadObject (defined in protocol_types.py)? Do we need all those types? Can we simplify?


class AccountLoginCompletedNotification(BaseModel):
error: Optional[str] = None
loginId: Optional[str] = None
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think str | None = None is more idiomatic these days for optional types

chatgptAuthTokens = "chatgptAuthTokens"


class AuthMode(RootModel[Union[AuthMode1, AuthMode2, AuthMode3]]):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious why AuthMode is generated this way vs. PlanType below (which seems better and is less verbose)

unlimited: bool


class PlanType(Enum):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like shared enums are not generated once and imported (i.e. we define PlanType multiple times across various files) - possible to fix?

Copy link
Copy Markdown
Collaborator

@owenlin0 owenlin0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

much cleaner, thanks! just some non-blocking comments

}
return self.request("turn/start", payload, response_model=TurnStartResponse)

def turn_text(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: turn_text seems unnecessary?

active_thread_id = started.thread.id
return self.run_text_turn(active_thread_id, text)

def ask(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need ask and ask_result? These seem a bit weird

if notification.method in target_methods:
return out

def run_text_turn(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the use case of run_text_turn? do we need it?

class TextTurnResult:
thread_id: str
turn_id: str
deltas: list[AgentMessageDeltaNotification]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similarly, do we need TextTurnResult? wondering if we can remove.

but if you really do want it, instead of storing deltas could we store a list of agent messages instead? it'd feel a little less weird. (but still unclear of the use case of this)

def test_codex_event_notifications_are_typed() -> None:
client = AppServerClient()
event = client._coerce_notification(
"codex/event/turn_aborted",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI we are dropping codex/event/* from app-server API very soon. No need to invest in any support for these

@shaqayeq-oai shaqayeq-oai merged commit 6baeec6 into main Mar 10, 2026
15 checks passed
@shaqayeq-oai shaqayeq-oai deleted the shaqayeq/python-sdk-types-foundation branch March 10, 2026 08:00
@github-actions github-actions bot locked and limited conversation to collaborators Mar 10, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

oai PRs contributed by OpenAI employees

3 participants