Skip to content

Commit ae53ee6

Browse files
dmchoiboiclaude
andauthored
fix: upgrade datamodel-code-generator to fix RootModel serialization bug (#745)
* fix: upgrade datamodel-code-generator to fix RootModel serialization bug Upgrades datamodel-code-generator from >=0.25.8 to >=0.47.0 to fix a bug where RootModel fields with default values were generated with raw primitive defaults instead of proper RootModel instances. This caused "'str' object has no attribute 'root'" errors during FastAPI response serialization for fields like: - service_tier: ServiceTier (3 occurrences) - reasoning_effort: ReasoningEffort (5 occurrences) - search_context_size: WebSearchContextSize (1 occurrence) The fix (datamodel-code-generator PR #2714) generates proper default_factory: ```python # Before (broken): service_tier: Annotated[Optional[ServiceTier], Field()] = "auto" # After (fixed): service_tier: Annotated[ServiceTier | None, Field(default_factory=lambda: ServiceTier('auto'))] ``` Fixes serialization error introduced in commit 0cbb58d. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: add constr export to pydantic_types module The new version of datamodel-code-generator now imports constr for constrained string types. Add this export to the custom pydantic_types module. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: add type ignore for model field override in ChatCompletionV2Request The newer datamodel-codegen changed CreateChatCompletionRequest.model from str to ModelIdsShared (a RootModel). The override is still compatible at runtime since ModelIdsShared wraps str. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: sort imports in pydantic_types.py Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: add default value for InputMessage.type in OpenAPI spec The type field was optional without a default, which caused datamodel-codegen to generate `Literal["message"] | None = None`. This broke Pydantic's discriminated union since discriminator fields need concrete Literal types. Adding `default: message` to the spec generates proper code with a non-None default value. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: remove discriminator='type' to fix duplicate discriminator values The OpenAPI spec has InputMessage and OutputMessage both with type='message', which causes Pydantic's discriminated union to fail with "Value 'message' mapped to multiple choices". Removing the discriminator makes Pydantic try each model in the union until one validates. Also reverted the InputMessage.type default change since it wasn't needed. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: remove all discriminator='type' patterns Handle all variations of the discriminator pattern in sed. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: remove accidentally added worktree * fix: remove content/refusal from required fields in ChatCompletionResponseMessage The OpenAPI spec marked these as required but the actual API doesn't always return them (refusal is only present when model refuses). This caused validation errors when deserializing responses. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: targeted discriminator removal with Python post-processor Replace blanket sed-based discriminator removal with a targeted Python post-processing script that only removes discriminator='type' from unions with conflicting type values: - Item: InputMessage + OutputMessage (both type='message') - InputItem: EasyInputMessage + InputMessage + OutputMessage (all type='message') Other unions (ToolModel, AnnotationModel, ItemResource, RealtimeClientEvent, RealtimeServerEvent, ResponseStreamEvent, etc.) retain their discriminators for better Pydantic validation performance. Also fix .black.toml to use force-exclude instead of exclude, which is required when files are passed explicitly (as pre-commit does). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: add type ignore for model field override The model field is overridden from ModelIdsShared to str to support a broader set of model IDs than the OpenAI enum. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d44b1ee commit ae53ee6

12 files changed

Lines changed: 212 additions & 79 deletions

File tree

.black.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
line-length = 100
44
target-version = ['py310']
55
include = '\.pyi?$'
6-
exclude = '''
6+
force-exclude = '''
77
(
88
/(
99
\.eggs # exclude a few common directories in the
@@ -18,6 +18,7 @@ exclude = '''
1818
| dist
1919
| alembic
2020
| gen
21+
| scripts
2122
)/
2223
)
2324
'''
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit af7145c086ffeeb1c42f5a00566b53dc5dfa1fb5

clients/python/llmengine/data_types/chat_completion.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515

1616
class ChatCompletionV2Request(CreateChatCompletionRequest, VLLMChatCompletionAdditionalParams):
17-
model: str = Field(
17+
model: str = Field( # type: ignore[assignment] # ModelIdsShared is a RootModel wrapping str
1818
description="ID of the model to use.",
1919
examples=["mixtral-8x7b-instruct"],
2020
)

clients/python/llmengine/data_types/gen/openai.py

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
# mypy: ignore-errors
22
# generated by datamodel-codegen:
33
# filename: openai-spec.yaml
4-
# timestamp: 2025-12-19T20:33:10+00:00
4+
# timestamp: 2026-01-10T00:37:57+00:00
55

66
from __future__ import annotations
77

8-
from typing import Annotated, Any, Dict, List, Literal, Optional, Union
8+
from typing import Any, Dict, List, Literal, Optional, Union
99

1010
import pydantic
1111

@@ -14,6 +14,7 @@
1414
from pydantic.v1 import AnyUrl, BaseModel, Extra, Field # noqa: F401
1515
else:
1616
from pydantic import AnyUrl, BaseModel, Extra, Field # type: ignore # noqa: F401
17+
from typing_extensions import Annotated
1718

1819

1920
class AddUploadPartRequest(BaseModel):
@@ -932,10 +933,10 @@ class Audio1(BaseModel):
932933

933934

934935
class ChatCompletionResponseMessage(BaseModel):
935-
content: Annotated[Optional[str], Field(description="The contents of the message.")]
936+
content: Annotated[Optional[str], Field(description="The contents of the message.")] = None
936937
refusal: Annotated[
937938
Optional[str], Field(description="The refusal message generated by the model.")
938-
]
939+
] = None
939940
tool_calls: Optional[ChatCompletionMessageToolCalls] = None
940941
annotations: Annotated[
941942
Optional[List[Annotation]],
@@ -3593,7 +3594,7 @@ class MessageStreamEvent3(BaseModel):
35933594

35943595

35953596
class Metadata(BaseModel):
3596-
__root__: Dict[str, str]
3597+
__root__: Optional[Dict[str, str]] = None
35973598

35983599

35993600
class Model(BaseModel):
@@ -6703,7 +6704,7 @@ class VectorStoreFileAttributes1(BaseModel):
67036704

67046705

67056706
class VectorStoreFileAttributes(BaseModel):
6706-
__root__: Dict[str, Union[VectorStoreFileAttributes1, float, bool]]
6707+
__root__: Optional[Dict[str, Union[VectorStoreFileAttributes1, float, bool]]] = None
67076708

67086709

67096710
class FileCounts(BaseModel):
@@ -7728,7 +7729,7 @@ class WebSearchOptions(BaseModel):
77287729
Optional[UserLocation],
77297730
Field(description="Approximate location parameters for the search.\n"),
77307731
] = None
7731-
search_context_size: Annotated[Optional[WebSearchContextSize], Field()] = "medium"
7732+
search_context_size: Optional[WebSearchContextSize] = None
77327733

77337734

77347735
class Audio2(BaseModel):
@@ -7761,7 +7762,7 @@ class CreateChatCompletionResponse(BaseModel):
77617762
),
77627763
]
77637764
model: Annotated[str, Field(description="The model used for the chat completion.")]
7764-
service_tier: Annotated[Optional[ServiceTier], Field()] = "auto"
7765+
service_tier: Optional[ServiceTier] = None
77657766
system_fingerprint: Annotated[
77667767
Optional[str],
77677768
Field(
@@ -7795,7 +7796,7 @@ class CreateChatCompletionStreamResponse(BaseModel):
77957796
),
77967797
]
77977798
model: Annotated[str, Field(description="The model to generate the completion.")]
7798-
service_tier: Annotated[Optional[ServiceTier], Field()] = "auto"
7799+
service_tier: Optional[ServiceTier] = None
77997800
system_fingerprint: Annotated[
78007801
Optional[str],
78017802
Field(
@@ -8157,7 +8158,7 @@ class Config:
81578158
description="An optional text to guide the model's style or continue a previous audio segment. The [prompt](/docs/guides/speech-to-text#prompting) should match the audio language.\n"
81588159
),
81598160
] = None
8160-
response_format: Annotated[Optional[AudioResponseFormat], Field()] = "json"
8161+
response_format: Optional[AudioResponseFormat] = None
81618162
temperature: Annotated[
81628163
float,
81638164
Field(
@@ -8358,7 +8359,7 @@ class EvalResponsesSource(BaseModel):
83588359
Field(
83598360
description="Optional reasoning effort parameter. This is a query parameter used to select responses."
83608361
),
8361-
] = "medium"
8362+
] = None
83628363
temperature: Annotated[
83638364
Optional[float],
83648365
Field(
@@ -8806,7 +8807,7 @@ class ModelResponseProperties(BaseModel):
88068807
example="user-1234",
88078808
),
88088809
] = None
8809-
service_tier: Annotated[Optional[ServiceTier], Field()] = "auto"
8810+
service_tier: Optional[ServiceTier] = None
88108811

88118812

88128813
class OutputContent(BaseModel):
@@ -9302,7 +9303,7 @@ class RealtimeSessionCreateResponse(BaseModel):
93029303

93039304

93049305
class Reasoning(BaseModel):
9305-
effort: Annotated[Optional[ReasoningEffort], Field()] = "medium"
9306+
effort: Optional[ReasoningEffort] = None
93069307
summary: Annotated[
93079308
Optional[Literal["auto", "concise", "detailed"]],
93089309
Field(
@@ -9912,7 +9913,7 @@ class Config:
99129913
max_length=256000,
99139914
),
99149915
] = None
9915-
reasoning_effort: Annotated[Optional[ReasoningEffort], Field()] = "medium"
9916+
reasoning_effort: Optional[ReasoningEffort] = None
99169917
tools: Annotated[
99179918
List[Union[AssistantToolsCode, AssistantToolsFileSearch, AssistantToolsFunction]],
99189919
Field(
@@ -10108,7 +10109,7 @@ class Config:
1010810109
example="gpt-4o",
1010910110
),
1011010111
] = None
10111-
reasoning_effort: Annotated[Optional[ReasoningEffort], Field()] = "medium"
10112+
reasoning_effort: Optional[ReasoningEffort] = None
1011210113
instructions: Annotated[
1011310114
Optional[str],
1011410115
Field(
@@ -10173,7 +10174,7 @@ class Config:
1017310174
] = None
1017410175
truncation_strategy: Optional[TruncationObject] = None
1017510176
tool_choice: Optional[AssistantsApiToolChoiceOption] = None
10176-
parallel_tool_calls: Annotated[Optional[ParallelToolCalls], Field()] = True
10177+
parallel_tool_calls: Optional[ParallelToolCalls] = None
1017710178
response_format: Optional[AssistantsApiResponseFormatOption] = None
1017810179

1017910180

@@ -10293,7 +10294,7 @@ class Config:
1029310294
] = None
1029410295
truncation_strategy: Optional[TruncationObject] = None
1029510296
tool_choice: Optional[AssistantsApiToolChoiceOption] = None
10296-
parallel_tool_calls: Annotated[Optional[ParallelToolCalls], Field()] = True
10297+
parallel_tool_calls: Optional[ParallelToolCalls] = None
1029710298
response_format: Optional[AssistantsApiResponseFormatOption] = None
1029810299

1029910300

@@ -10394,7 +10395,7 @@ class FineTuneChatRequestInput(BaseModel):
1039410395
Optional[List[ChatCompletionTool]],
1039510396
Field(description="A list of tools the model may generate JSON inputs for."),
1039610397
] = None
10397-
parallel_tool_calls: Annotated[Optional[ParallelToolCalls], Field()] = True
10398+
parallel_tool_calls: Optional[ParallelToolCalls] = None
1039810399
functions: Annotated[
1039910400
Optional[List[ChatCompletionFunctions]],
1040010401
Field(
@@ -10424,7 +10425,7 @@ class Input5(BaseModel):
1042410425
Optional[List[ChatCompletionTool]],
1042510426
Field(description="A list of tools the model may generate JSON inputs for."),
1042610427
] = None
10427-
parallel_tool_calls: Annotated[Optional[ParallelToolCalls], Field()] = True
10428+
parallel_tool_calls: Optional[ParallelToolCalls] = None
1042810429

1042910430

1043010431
class FineTunePreferenceRequestInput(BaseModel):
@@ -10536,7 +10537,7 @@ class Config:
1053610537
description="ID of the model to use. You can use the [List models](/docs/api-reference/models/list) API to see all of your available models, or see our [Model overview](/docs/models) for descriptions of them.\n"
1053710538
),
1053810539
] = None
10539-
reasoning_effort: Annotated[Optional[ReasoningEffort], Field()] = "medium"
10540+
reasoning_effort: Optional[ReasoningEffort] = None
1054010541
name: Annotated[
1054110542
Optional[str],
1054210543
Field(
@@ -11276,13 +11277,13 @@ class CreateChatCompletionRequest(CreateModelResponseProperties):
1127611277
),
1127711278
]
1127811279
model: Annotated[
11279-
str,
11280+
ModelIdsShared,
1128011281
Field(
1128111282
description="Model ID used to generate the response, like `gpt-4o` or `o3`. OpenAI\noffers a wide range of models with different capabilities, performance\ncharacteristics, and price points. Refer to the [model guide](/docs/models)\nto browse and compare available models.\n"
1128211283
),
1128311284
]
1128411285
modalities: Optional[ResponseModalities] = None
11285-
reasoning_effort: Annotated[Optional[ReasoningEffort], Field()] = "medium"
11286+
reasoning_effort: Optional[ReasoningEffort] = None
1128611287
max_completion_tokens: Annotated[
1128711288
Optional[int],
1128811289
Field(
@@ -11394,7 +11395,7 @@ class CreateChatCompletionRequest(CreateModelResponseProperties):
1139411395
),
1139511396
] = None
1139611397
tool_choice: Optional[ChatCompletionToolChoiceOption] = None
11397-
parallel_tool_calls: Annotated[Optional[ParallelToolCalls], Field()] = True
11398+
parallel_tool_calls: Optional[ParallelToolCalls] = None
1139811399
function_call: Annotated[
1139911400
Optional[Union[Literal["none", "auto"], ChatCompletionFunctionCallOption]],
1140011401
Field(

model-engine/model_engine_server/common/dtos/llms/chat_completion.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717

1818
class ChatCompletionV2Request(CreateChatCompletionRequest, VLLMChatCompletionAdditionalParams):
19-
model: Annotated[
19+
model: Annotated[ # type: ignore[assignment]
2020
str,
2121
Field(
2222
description="ID of the model to use.",

model-engine/model_engine_server/common/pydantic_types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from pydantic import AnyUrl as PyAnyUrl
55
from pydantic import AnyWebsocketUrl as PyAnyWebsocketUrl
66
from pydantic import BaseModel as PydanticBaseModel
7+
from pydantic import constr # noqa: F401
78
from pydantic import model_validator # noqa: F401
89
from pydantic import ConfigDict, Field # noqa: F401
910
from pydantic import FileUrl as PyFileUrl

0 commit comments

Comments
 (0)