Description
Confirm this is an issue with the Python library and not an underlying OpenAI API
- This is an issue with the Python library
Describe the bug
When response data is constructed into a model, if the data doesn't match a type defined in the union exactly, then the code attempts to build the correct model based on the discriminator value. During this process, the code extracts the field schema from each model in the union:
openai-python/src/openai/_models.py
Lines 672 to 687 in 9dea82f
In some cases, when accessing the __pydantic_core_schema__
of the model, it is found that a DefinitionsSchema is returned instead of a ModelSchema. This excludes the given type from consideration on L674-675, even though it is a valid type in the union and may have the matching discriminator value. The below code repros the issue and how it can lead to the wrong type being constructed.
To Reproduce
from typing import Any, Union, cast, List
from typing_extensions import Literal, Annotated
from openai._models import BaseModel, construct_type
from openai._utils import PropertyInfo
# this is a generalization of how Filters is defined on the FileSearchTool (https://github.com/openai/openai-python/blob/main/src/openai/types/responses/file_search_tool.py#L34)
class A(BaseModel):
type: Literal["a"]
data: bool
class B(BaseModel):
type: Literal["b"]
data: List[Union[A, object]]
class ModelA(BaseModel):
type: Literal["modelA"]
data: int
class ModelB(BaseModel):
type: Literal["modelB"]
required: str
data: Union[A, B]
ma = construct_type(value={"type": "modelA", "data": 1}, type_=ModelA)
assert isinstance(ma, ModelA)
schema_a = ma.__pydantic_core_schema__
assert schema_a["type"] == "model"
# ModelB builds a DefinitionsSchema instead of a ModelSchema
mb = construct_type(
value={
"type": "modelB",
"required": "foo",
"data": {"type": "a", "data": True},
},
type_=ModelB,
)
assert isinstance(mb, ModelB)
schema_b = mb.__pydantic_core_schema__
assert schema_b["type"] == "definitions"
model_schema = schema_b["schema"]
assert model_schema["type"] == "model"
# when constructing ModelA | ModelB, value data doesn't match ModelB exactly - missing `required`
m = construct_type(
value={"type": "modelB", "data": {"type": "a", "data": True}},
type_=cast(Any, Annotated[Union[ModelA, ModelB], PropertyInfo(discriminator="type")]),
)
# AssertionError: Expected ModelB but got ModelA
assert isinstance(m, ModelB), f"Expected ModelB but got {type(m).__name__}"
Code snippets
OS
WSL
Python version
3.12
Library version
1.66.3