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.
This commit is contained in:
Mason Daugherty
2026-06-26 03:14:47 -04:00
committed by GitHub
parent 20ba43df9c
commit d3eb2296e7
3 changed files with 47 additions and 3 deletions

View File

@@ -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(

View File

@@ -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

View File

@@ -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