Upgrade Python SDK model generation to datamodel-code-generator 0.64.0 (#56272)

## What changed

- Regenerate protocol models with `datamodel-code-generator==0.64.0`, targeting Python 3.10.
- Preserve discriminator field titles without introducing extra `RootModel` wrappers for literal values or inline unions.
- Keep nullable project and section IDs optional for compatibility with older SDKs, and remove redundant required-nullable field patches.

## Testing

Extend schema annotation coverage to assert that `ReasoningSummary.model_validate("none").root` remains the string `"none"`.

GitOrigin-RevId: 386d07d7e2e368b07d4602b49be6e81a887685fe
This commit is contained in:
Adam Perry @ OpenAI
2026-10-22 21:40:58 +00:00
committed by copyberry
parent cb2581dae8
commit 0eb98b4617
5 changed files with 99 additions and 338 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ Documentation = "https://github.com/openai/codex/tree/main/sdk/python/docs"
format = ["ruff>=0.15.8"]
test = [
"pytest>=8.0",
"datamodel-code-generator==0.31.2",
"datamodel-code-generator==0.64.0",
{ include-group = "format" },
]
dev = [
+22 -40
View File
@@ -397,16 +397,6 @@ def _variant_collision_key(base: str, variant: dict[str, Any], generated_name: s
return "|".join(parts)
def _set_discriminator_titles(props: dict[str, Any], owner: str) -> None:
for key in DISCRIMINATOR_KEYS:
prop = props.get(key)
if not isinstance(prop, dict):
continue
if _string_literal(prop) is None or "title" in prop:
continue
prop["title"] = f"{owner}{_to_pascal_case(key)}"
def _annotate_variant_list(variants: list[Any], base: str | None) -> None:
seen = {
variant["title"]
@@ -438,12 +428,11 @@ def _annotate_variant_list(variants: list[Any], base: str | None) -> None:
seen.add(generated_name)
variant_name = generated_name
if isinstance(variant_name, str):
props = variant.get("properties")
if isinstance(props, dict):
_set_discriminator_titles(props, variant_name)
_annotate_schema(variant, base)
# A titled inline union becomes a separate RootModel. Keep its variants
# directly accessible on the enclosing SDK union.
if "oneOf" in variant or "anyOf" in variant or _string_literal(variant) is not None:
variant.pop("title", None)
def _annotate_schema(value: Any, base: str | None = None) -> None:
@@ -455,10 +444,15 @@ def _annotate_schema(value: Any, base: str | None = None) -> None:
if not isinstance(value, dict):
return
# The titles describe fields. Passing them to the generator as inline model
# names would change discriminator values from strings into RootModels.
owner = value.get("title")
props = value.get("properties")
if isinstance(owner, str) and isinstance(props, dict):
_set_discriminator_titles(props, owner)
for key in DISCRIMINATOR_KEYS:
prop = props.get(key)
if isinstance(prop, dict) and _string_literal(prop) is not None:
prop["x-title"] = prop.pop("title", f"{owner}{_to_pascal_case(key)}")
one_of = value.get("oneOf")
if isinstance(one_of, list):
@@ -535,6 +529,15 @@ def _normalized_schema_bundle_text(schema_dir: Path) -> str:
_preserve_guardian_approval_path_wrappers(schema)
definitions = schema.get("definitions", {})
if isinstance(definitions, dict):
# Older SDKs accept omission for these nullable project and section IDs.
for model, field in (
("Thread", "projectId"),
("ThreadProjectUpdatedNotification", "projectId"),
("ThreadSectionMoveParams", "sectionId"),
):
required = definitions.get(model, {}).get("required", [])
if field in required:
required.remove(field)
for name, definition in definitions.items():
if isinstance(definition, dict):
if name == "CodexErrorInfo":
@@ -572,11 +575,13 @@ def generate_v2_all(schema_dir: Path, *, format_output: bool = True) -> None:
"--output-model-type",
"pydantic_v2.BaseModel",
"--target-python-version",
"3.11",
"3.10",
"--use-standard-collections",
"--enum-field-as-literal",
"one",
"--field-constraints",
"--field-extra-keys-without-x-prefix",
"x-title",
"--use-default-kwarg",
"--snake-case-field",
"--allow-population-by-field-name",
@@ -592,8 +597,6 @@ def generate_v2_all(schema_dir: Path, *, format_output: bool = True) -> None:
)
_preserve_inline_image_class_names(out_path)
_add_legacy_image_generation_thread_item_alias(out_path)
_require_nullable_field(out_path, "ChatgptAccount", r"email: str \| None")
_require_nullable_field(out_path, "McpResourceReadTarget", r"link_id: Annotated\[[\s\S]*?\]")
_preserve_open_enum(out_path, "CodexErrorInfoValue", allow_empty=True)
_preserve_reasoning_effort_enum(out_path)
_preserve_thread_source_enum(out_path)
@@ -635,27 +638,6 @@ def _add_legacy_image_generation_thread_item_alias(out_path: Path) -> None:
out_path.write_text(source[:class_end] + alias + source[class_end:])
def _require_nullable_field(out_path: Path, class_name: str, field_pattern: str) -> None:
"""Preserve required-but-nullable fields that codegen gives a None default."""
source = out_path.read_text()
class_start = source.find(f"class {class_name}(BaseModel):")
if class_start == -1:
raise RuntimeError(f"Generated SDK is missing {class_name}")
class_end = source.find("\n\nclass ", class_start)
if class_end == -1:
class_end = len(source)
class_source, count = re.subn(
rf"(^ {field_pattern}) = None$",
r"\1",
source[class_start:class_end],
flags=re.MULTILINE,
)
if count != 1:
raise RuntimeError(f"Generated {class_name} field did not have the expected nullable shape")
out_path.write_text(source[:class_start] + class_source + source[class_end:])
def _preserve_reasoning_effort_enum(out_path: Path) -> None:
"""Keep the public effort constants while accepting future wire values."""
source = out_path.read_text()
File diff suppressed because it is too large Load Diff
@@ -500,13 +500,12 @@ def test_python_codegen_schema_annotation_adds_stable_variant_titles() -> None:
"GranularAskForApproval",
]
reasoning_summary_titles = [
variant.get("title") for variant in definitions["ReasoningSummary"]["oneOf"]
]
assert reasoning_summary_titles == [
"ReasoningSummaryValue",
"NoneReasoningSummary",
]
from openai_codex.generated.v2_all import ReasoningSummary
reasoning_values, disabled = definitions["ReasoningSummary"]["oneOf"]
assert reasoning_values["title"] == "ReasoningSummaryValue"
assert disabled["enum"] == ["none"]
assert ReasoningSummary.model_validate("none").root == "none"
def test_generate_v2_all_uses_titles_for_generated_names() -> None:
+13 -10
View File
@@ -1,6 +1,10 @@
version = 1
revision = 3
revision = 5
requires-python = ">=3.10"
resolution-markers = [
"python_full_version >= '3.14'",
"python_full_version < '3.14'",
]
[options]
exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values.
@@ -94,23 +98,22 @@ wheels = [
[[package]]
name = "datamodel-code-generator"
version = "0.31.2"
version = "0.64.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "argcomplete" },
{ name = "black" },
{ name = "black", marker = "sys_platform != 'emscripten'" },
{ name = "genson" },
{ name = "inflect" },
{ name = "isort" },
{ name = "isort", marker = "sys_platform != 'emscripten'" },
{ name = "jinja2" },
{ name = "packaging" },
{ name = "pydantic" },
{ name = "pyyaml" },
{ name = "tomli", marker = "python_full_version < '3.12'" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/07/40/26399980314bedb3a8ec6a31da646d190cd945192410ad3ab6eabe0a1c73/datamodel_code_generator-0.31.2.tar.gz", hash = "sha256:47887b8aa6fd69865e07e2893c1e76e34dae753b9a97f1020357af8337bc4cdb", size = 453381, upload-time = "2025-06-22T17:40:56.491Z" }
sdist = { url = "https://files.pythonhosted.org/packages/9c/d2/86c94a2836ed42231653a7ddaefa0a5bc23418167a876bba7376c96b3a35/datamodel_code_generator-0.64.0.tar.gz", hash = "sha256:9c592900a00b20e416494273c22435f5a9aef6ea8c7b9190747522a60497a1cb", size = 1316440, upload-time = "2026-06-14T17:24:50.528Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/82/9bda726b117967b8056315e51125633ec7a0d2f862c202aed01c3fde2e9f/datamodel_code_generator-0.31.2-py3-none-any.whl", hash = "sha256:78f200a9e673ee4d041e96e82a835273baf4ce15e446d46501c0433d82af3ef5", size = 119369, upload-time = "2025-06-22T17:40:54.615Z" },
{ url = "https://files.pythonhosted.org/packages/23/94/71338e2f0146ac10747a5537b3a1e45256e66b7c229869eb0ee787111b41/datamodel_code_generator-0.64.0-py3-none-any.whl", hash = "sha256:b7cd8bd41a312aa997aec6150670bad781847c5b674f17e4d70e78208a0fb990", size = 374698, upload-time = "2026-06-14T17:24:48.809Z" },
]
[[package]]
@@ -314,13 +317,13 @@ requires-dist = [
[package.metadata.requires-dev]
dev = [
{ name = "datamodel-code-generator", specifier = "==0.31.2" },
{ name = "datamodel-code-generator", specifier = "==0.64.0" },
{ name = "pytest", specifier = ">=8.0" },
{ name = "ruff", specifier = ">=0.15.8" },
]
format = [{ name = "ruff", specifier = ">=0.15.8" }]
test = [
{ name = "datamodel-code-generator", specifier = "==0.31.2" },
{ name = "datamodel-code-generator", specifier = "==0.64.0" },
{ name = "pytest", specifier = ">=8.0" },
{ name = "ruff", specifier = ">=0.15.8" },
]