Skip to content

Commit d3eb229

Browse files
authored
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.
1 parent 20ba43d commit d3eb229

3 files changed

Lines changed: 47 additions & 3 deletions

File tree

libs/core/langchain_core/language_models/base.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import builtins # noqa: TC003 # runtime-evaluated; subclass `dict()` shadows the builtin
56
import warnings
67
from abc import ABC, abstractmethod
78
from collections.abc import Callable, Mapping, Sequence
@@ -191,7 +192,7 @@ class BaseLanguageModel(
191192
tags: list[str] | None = Field(default=None, exclude=True)
192193
"""Tags to add to the run trace."""
193194

194-
metadata: dict[str, Any] | None = Field(default=None, exclude=True)
195+
metadata: builtins.dict[str, Any] | None = Field(default=None, exclude=True)
195196
"""Metadata to add to the run trace."""
196197

197198
custom_get_token_ids: Callable[[str], list[int]] | None = Field(

libs/core/langchain_core/language_models/chat_models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44

55
import asyncio
6-
import builtins # noqa: TC003
6+
import builtins # noqa: TC003 # runtime-evaluated; subclass `dict()` shadows the builtin
77
import contextlib
88
import inspect
99
import json

libs/core/tests/unit_tests/language_models/test_imports.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
from langchain_core.language_models import __all__
1+
from typing import Any
2+
3+
from langchain_core.callbacks import Callbacks
4+
from langchain_core.language_models import BaseLanguageModel, __all__
5+
from langchain_core.outputs import LLMResult
6+
from langchain_core.prompt_values import PromptValue
27

38
EXPECTED_ALL = [
49
"BaseLanguageModel",
@@ -26,3 +31,41 @@
2631

2732
def test_all_imports() -> None:
2833
assert set(__all__) == set(EXPECTED_ALL)
34+
35+
36+
def test_pydantic_rebuild_handles_subclass_dict_method_shadowing_builtin() -> None:
37+
"""Regression for Pydantic field collection with subclasses that define `dict()`.
38+
39+
Pydantic 2.14.0a1 evaluates inherited field annotations during subclass
40+
rebuilds. If `BaseLanguageModel.metadata` uses a plain `dict[...]`
41+
annotation, the subclass `dict()` method can shadow the builtin and make
42+
annotation evaluation fail with `'function' object is not subscriptable`.
43+
"""
44+
45+
class DictMethodLanguageModel(BaseLanguageModel[str]):
46+
name: str = "test"
47+
48+
def generate_prompt(
49+
self,
50+
prompts: list[PromptValue],
51+
stop: list[str] | None = None,
52+
callbacks: Callbacks = None,
53+
**kwargs: Any,
54+
) -> LLMResult:
55+
raise NotImplementedError
56+
57+
async def agenerate_prompt(
58+
self,
59+
prompts: list[PromptValue],
60+
stop: list[str] | None = None,
61+
callbacks: Callbacks = None,
62+
**kwargs: Any,
63+
) -> LLMResult:
64+
raise NotImplementedError
65+
66+
def dict(self, **_kwargs: Any) -> dict[str, Any]:
67+
return {}
68+
69+
DictMethodLanguageModel.model_rebuild(force=True)
70+
71+
assert "metadata" in DictMethodLanguageModel.model_fields

0 commit comments

Comments
 (0)