fix(types): unquote resolvable forward refs so Message builds on python 3.10/3.11

Quoted refs inside TypedDicts embedded in pydantic models are resolved
against the embedding model's module namespace, so ModelResponse() raised
PydanticUserError: `Message` is not fully defined on python < 3.12.

Fixes #36384

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Devin AI 2026-08-10 09:12:06 +00:00
parent f6b9518ddb
commit 9fa5a872d3
2 changed files with 45 additions and 3 deletions

View file

@ -534,7 +534,7 @@ class ChatCompletionReasoningItem(TypedDict, total=False):
type: Required[Literal["reasoning"]]
id: str
encrypted_content: str | None
summary: list["ChatCompletionReasoningSummaryTextBlock"]
summary: list[ChatCompletionReasoningSummaryTextBlock]
class WebSearchOptionsUserLocationApproximate(TypedDict, total=False):
@ -1135,7 +1135,7 @@ class ResponsesAPIOptionalRequestParams(TypedDict, total=False):
background: bool | None
stream: bool | None
temperature: float | None
text: Optional["ResponseText"]
text: ResponseText | None
tool_choice: ToolChoice | None
tools: list[ALL_RESPONSES_API_TOOL_PARAMS] | None
top_p: float | None
@ -1243,7 +1243,7 @@ class ResponsesAPIResponse(BaseLiteLLMOpenAIResponseObject):
previous_response_id: str | None = None
reasoning: dict[str, Any] | None = None
status: str | None = None
text: Union["ResponseText", dict[str, Any]] | None = None
text: ResponseText | dict[str, Any] | None = None
truncation: Literal["auto", "disabled"] | None = None
usage: ResponseAPIUsage | None = None
user: str | None = None

View file

@ -453,3 +453,45 @@ def test_openai_file_object_accepts_pending_status():
status="pending",
)
assert file_obj.status == "pending"
def test_typed_dicts_have_no_resolvable_quoted_forward_refs():
"""A quoted ref inside a TypedDict is resolved against the namespace of the
pydantic model embedding it, not this module, so it breaks model building on
python < 3.12 (issue #36384)."""
from typing import ForwardRef, get_args
from typing_extensions import is_typeddict
import litellm.types.llms.openai as openai_types
def string_refs(annotation: object) -> tuple[str, ...]:
return tuple(
ref
for arg in get_args(annotation)
for ref in (
(arg,)
if isinstance(arg, str)
else (arg.__forward_arg__,)
if isinstance(arg, ForwardRef)
else string_refs(arg)
)
)
stale = tuple(
f"{cls.__name__}.{field} -> {ref!r}"
for cls in vars(openai_types).values()
if is_typeddict(cls) and cls.__module__ == openai_types.__name__
for field, annotation in cls.__annotations__.items()
for ref in string_refs(annotation)
if ref != cls.__name__ and getattr(openai_types, ref, None) is not None
)
assert not stale, f"unquote these forward refs: {stale}"
def test_model_response_constructs_with_reasoning_items_annotation():
from litellm.types.utils import ModelResponse
ModelResponse().choices[0].message.reasoning_items = [
{"type": "reasoning", "id": "rs_1", "summary": [{"type": "summary_text", "text": "hi"}]}
]