From 9fa5a872d3bdd1b00b65d1cf72bdfc34edd11b1b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 09:12:06 +0000 Subject: [PATCH] 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> --- litellm/types/llms/openai.py | 6 +-- .../types/llms/test_types_llms_openai.py | 42 +++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/litellm/types/llms/openai.py b/litellm/types/llms/openai.py index da0592e6bb2..afbc88c6cbc 100644 --- a/litellm/types/llms/openai.py +++ b/litellm/types/llms/openai.py @@ -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 diff --git a/tests/test_litellm/types/llms/test_types_llms_openai.py b/tests/test_litellm/types/llms/test_types_llms_openai.py index 569743269a5..7ed3ad8cfdc 100644 --- a/tests/test_litellm/types/llms/test_types_llms_openai.py +++ b/tests/test_litellm/types/llms/test_types_llms_openai.py @@ -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"}]} + ]