mirror of
https://github.com/openai/codex.git
synced 2026-10-23 18:08:45 +00:00
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:
committed by
copyberry
parent
cb2581dae8
commit
0eb98b4617
@@ -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 = [
|
||||
|
||||
@@ -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:
|
||||
|
||||
Generated
+13
-10
@@ -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" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user