From d3eb2296e72cb8f0f5f53e9fc527fd8c312fd51e Mon Sep 17 00:00:00 2001 From: Mason Daugherty Date: Fri, 26 Jun 2026 03:14:47 -0400 Subject: [PATCH] fix(core): avoid `dict` shadowing in language models (#38480) Fixes #37835 --- When Pydantic collects fields for a `BaseLanguageModel` subclass that defines a `dict()` method, inherited annotations can resolve `dict` against the subclass namespace instead of the builtin. With Pydantic 2.14.0a1 this caused `BaseLanguageModel.metadata: dict[str, Any] | None` to fail during rebuild/import with `'function' object is not subscriptable`. This qualifies the inherited `metadata` field annotation as `builtins.dict[...]`, matching the existing pattern in chat models, and documents why the runtime import cannot move behind `TYPE_CHECKING`. It also adds a regression test that rebuilds a `BaseLanguageModel` subclass with a `dict()` method so core catches this failure before partner packages hit it at import time. Related to #37924, which hardens `_create_subset_model_v2`; this PR fixes the `BaseLanguageModel` class-construction failure directly. --- .../langchain_core/language_models/base.py | 3 +- .../language_models/chat_models.py | 2 +- .../language_models/test_imports.py | 45 ++++++++++++++++++- 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/libs/core/langchain_core/language_models/base.py b/libs/core/langchain_core/language_models/base.py index a39979d14c5..44eb7aeee5a 100644 --- a/libs/core/langchain_core/language_models/base.py +++ b/libs/core/langchain_core/language_models/base.py @@ -2,6 +2,7 @@ from __future__ import annotations +import builtins # noqa: TC003 # runtime-evaluated; subclass `dict()` shadows the builtin import warnings from abc import ABC, abstractmethod from collections.abc import Callable, Mapping, Sequence @@ -191,7 +192,7 @@ class BaseLanguageModel( tags: list[str] | None = Field(default=None, exclude=True) """Tags to add to the run trace.""" - metadata: dict[str, Any] | None = Field(default=None, exclude=True) + metadata: builtins.dict[str, Any] | None = Field(default=None, exclude=True) """Metadata to add to the run trace.""" custom_get_token_ids: Callable[[str], list[int]] | None = Field( diff --git a/libs/core/langchain_core/language_models/chat_models.py b/libs/core/langchain_core/language_models/chat_models.py index 01737308ebe..3ed4b9455cf 100644 --- a/libs/core/langchain_core/language_models/chat_models.py +++ b/libs/core/langchain_core/language_models/chat_models.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio -import builtins # noqa: TC003 +import builtins # noqa: TC003 # runtime-evaluated; subclass `dict()` shadows the builtin import contextlib import inspect import json diff --git a/libs/core/tests/unit_tests/language_models/test_imports.py b/libs/core/tests/unit_tests/language_models/test_imports.py index d99f37f99d9..cfd2cc18fb3 100644 --- a/libs/core/tests/unit_tests/language_models/test_imports.py +++ b/libs/core/tests/unit_tests/language_models/test_imports.py @@ -1,4 +1,9 @@ -from langchain_core.language_models import __all__ +from typing import Any + +from langchain_core.callbacks import Callbacks +from langchain_core.language_models import BaseLanguageModel, __all__ +from langchain_core.outputs import LLMResult +from langchain_core.prompt_values import PromptValue EXPECTED_ALL = [ "BaseLanguageModel", @@ -26,3 +31,41 @@ EXPECTED_ALL = [ def test_all_imports() -> None: assert set(__all__) == set(EXPECTED_ALL) + + +def test_pydantic_rebuild_handles_subclass_dict_method_shadowing_builtin() -> None: + """Regression for Pydantic field collection with subclasses that define `dict()`. + + Pydantic 2.14.0a1 evaluates inherited field annotations during subclass + rebuilds. If `BaseLanguageModel.metadata` uses a plain `dict[...]` + annotation, the subclass `dict()` method can shadow the builtin and make + annotation evaluation fail with `'function' object is not subscriptable`. + """ + + class DictMethodLanguageModel(BaseLanguageModel[str]): + name: str = "test" + + def generate_prompt( + self, + prompts: list[PromptValue], + stop: list[str] | None = None, + callbacks: Callbacks = None, + **kwargs: Any, + ) -> LLMResult: + raise NotImplementedError + + async def agenerate_prompt( + self, + prompts: list[PromptValue], + stop: list[str] | None = None, + callbacks: Callbacks = None, + **kwargs: Any, + ) -> LLMResult: + raise NotImplementedError + + def dict(self, **_kwargs: Any) -> dict[str, Any]: + return {} + + DictMethodLanguageModel.model_rebuild(force=True) + + assert "metadata" in DictMethodLanguageModel.model_fields