libs/core/tests/unit_tests/language_models/chat_models/test_base.py PYTHON 1,841 lines View on github.com → Search inside
1"""Test base chat model."""23import uuid4import warnings5from collections.abc import AsyncIterator, Iterator6from contextlib import contextmanager7from importlib.metadata import PackageNotFoundError8from typing import TYPE_CHECKING, Any, Literal, get_type_hints9from unittest.mock import patch1011import pytest12from langsmith.env import get_langchain_env_var_metadata13from pydantic import model_validator14from typing_extensions import Self, override1516from langchain_core._api import LangChainDeprecationWarning17from langchain_core.callbacks import (18    AsyncCallbackManagerForLLMRun,19    BaseCallbackHandler,20    CallbackManagerForLLMRun,21)22from langchain_core.language_models import (23    BaseChatModel,24    FakeListChatModel,25    ParrotFakeChatModel,26)27from langchain_core.language_models._utils import (28    _filter_invocation_params_for_tracing,29    _normalize_messages,30)31from langchain_core.language_models.base import _get_langchain_version32from langchain_core.language_models.chat_models import (33    SimpleChatModel,34    _generate_response_from_error,35)36from langchain_core.language_models.fake_chat_models import (37    FakeListChatModelError,38    GenericFakeChatModel,39)40from langchain_core.language_models.model_profile import ModelProfile41from langchain_core.messages import (42    AIMessage,43    AIMessageChunk,44    BaseMessage,45    HumanMessage,46    SystemMessage,47)48from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult49from langchain_core.outputs.llm_result import LLMResult50from langchain_core.tracers import LogStreamCallbackHandler51from langchain_core.tracers._streaming import _V2StreamingCallbackHandler52from langchain_core.tracers.base import BaseTracer53from langchain_core.tracers.context import collect_runs54from langchain_core.tracers.event_stream import _AstreamEventsCallbackHandler55from langchain_core.tracers.langchain import LangChainTracer56from langchain_core.tracers.schemas import Run57from langchain_core.version import VERSION58from tests.unit_tests.fake.callbacks import (59    BaseFakeCallbackHandler,60    FakeAsyncCallbackHandler,61    FakeCallbackHandler,62)63from tests.unit_tests.stubs import _any_id_ai_message, _any_id_ai_message_chunk6465if TYPE_CHECKING:66    from langchain_core.outputs.llm_result import LLMResult67    from langchain_core.runnables.config import RunnableConfig686970def _content_blocks_equal_ignore_id(71    actual: str | list[Any], expected: str | list[Any]72) -> bool:73    """Compare content blocks, ignoring auto-generated `id` fields.7475    Args:76        actual: Actual content from response (string or list of content blocks).77        expected: Expected content to compare against (string or list of blocks).7879    Returns:80        True if content matches (excluding `id` fields), `False` otherwise.8182    """83    if isinstance(actual, str) or isinstance(expected, str):84        return actual == expected8586    if len(actual) != len(expected):87        return False88    for actual_block, expected_block in zip(actual, expected, strict=False):89        actual_without_id = (90            {k: v for k, v in actual_block.items() if k != "id"}91            if isinstance(actual_block, dict) and "id" in actual_block92            else actual_block93        )9495        if actual_without_id != expected_block:96            return False9798    return True99100101def test_asdict_replaces_deprecated_dict() -> None:102    model = FakeListChatModel(responses=["foo"])103104    expected = {"responses": ["foo"], "_type": "fake-list-chat-model"}105    assert model.asdict() == expected106    with pytest.warns(LangChainDeprecationWarning, match="asdict"):107        assert model.dict() == expected108109110def test_base_chat_model_type_hints_resolve() -> None:111    assert get_type_hints(BaseChatModel.asdict)["return"] == dict[str, Any]112113114def test_invoke_preserves_deprecated_dict_override() -> None:115    """Invoking should preserve `dict()` overrides until `dict()` is removed."""116117    class CustomDictChatModel(FakeListChatModel):118        @override119        def dict(self, **kwargs: Any) -> dict[str, Any]:120            data = super().dict(**kwargs)121            data["custom_trace_param"] = "custom"122            return data123124    model = CustomDictChatModel(responses=["foo"])125    with warnings.catch_warnings():126        warnings.simplefilter("error", LangChainDeprecationWarning)127        with collect_runs() as cb:128            assert model.invoke("hello").content == "foo"129130    assert cb.traced_runs[0].extra is not None131    assert cb.traced_runs[0].extra["invocation_params"]["custom_trace_param"] == (132        "custom"133    )134135136@pytest.fixture137def messages() -> list[BaseMessage]:138    return [139        SystemMessage(content="You are a test user."),140        HumanMessage(content="Hello, I am a test user."),141    ]142143144@pytest.fixture145def messages_2() -> list[BaseMessage]:146    return [147        SystemMessage(content="You are a test user."),148        HumanMessage(content="Hello, I not a test user."),149    ]150151152def test_batch_size(messages: list[BaseMessage], messages_2: list[BaseMessage]) -> None:153    # The base endpoint doesn't support native batching,154    # so we expect batch_size to always be 1155    llm = FakeListChatModel(responses=[str(i) for i in range(100)])156    with collect_runs() as cb:157        llm.batch([messages, messages_2], {"callbacks": [cb]})158        assert len(cb.traced_runs) == 2159        assert all((r.extra or {}).get("batch_size") == 1 for r in cb.traced_runs)160    with collect_runs() as cb:161        llm.batch([messages], {"callbacks": [cb]})162        assert all((r.extra or {}).get("batch_size") == 1 for r in cb.traced_runs)163        assert len(cb.traced_runs) == 1164165    with collect_runs() as cb:166        llm.invoke(messages)167        assert len(cb.traced_runs) == 1168        assert (cb.traced_runs[0].extra or {}).get("batch_size") == 1169170    with collect_runs() as cb:171        list(llm.stream(messages))172        assert len(cb.traced_runs) == 1173        assert (cb.traced_runs[0].extra or {}).get("batch_size") == 1174175176async def test_async_batch_size(177    messages: list[BaseMessage], messages_2: list[BaseMessage]178) -> None:179    llm = FakeListChatModel(responses=[str(i) for i in range(100)])180    # The base endpoint doesn't support native batching,181    # so we expect batch_size to always be 1182    with collect_runs() as cb:183        await llm.abatch([messages, messages_2], {"callbacks": [cb]})184        assert all((r.extra or {}).get("batch_size") == 1 for r in cb.traced_runs)185        assert len(cb.traced_runs) == 2186    with collect_runs() as cb:187        await llm.abatch([messages], {"callbacks": [cb]})188        assert all((r.extra or {}).get("batch_size") == 1 for r in cb.traced_runs)189        assert len(cb.traced_runs) == 1190191    with collect_runs() as cb:192        await llm.ainvoke(messages)193        assert len(cb.traced_runs) == 1194        assert (cb.traced_runs[0].extra or {}).get("batch_size") == 1195196    with collect_runs() as cb:197        async for _ in llm.astream(messages):198            pass199        assert len(cb.traced_runs) == 1200        assert (cb.traced_runs[0].extra or {}).get("batch_size") == 1201202203@pytest.mark.xfail(reason="This test is failing due to a bug in the testing code")204async def test_stream_error_callback() -> None:205    message = "test"206207    def eval_response(callback: BaseFakeCallbackHandler, i: int) -> None:208        assert callback.errors == 1209        assert len(callback.errors_args) == 1210        llm_result: LLMResult = callback.errors_args[0]["kwargs"]["response"]211        if i == 0:212            assert llm_result.generations == []213        else:214            assert llm_result.generations[0][0].text == message[:i]215216    for i in range(len(message)):217        llm = FakeListChatModel(218            responses=[message],219            error_on_chunk_number=i,220        )221        cb_async = FakeAsyncCallbackHandler()222        llm_astream = llm.astream("Dummy message", config={"callbacks": [cb_async]})223        for _ in range(i):224            await anext(llm_astream)225        with pytest.raises(FakeListChatModelError):226            await anext(llm_astream)227        eval_response(cb_async, i)228229        cb_sync = FakeCallbackHandler()230        llm_stream = llm.stream("Dumy message", config={"callbacks": [cb_sync]})231        for _ in range(i):232            next(llm_stream)233        with pytest.raises(FakeListChatModelError):234            next(llm_stream)235        eval_response(cb_sync, i)236237238async def test_astream_fallback_to_ainvoke() -> None:239    """Test `astream()` uses appropriate implementation."""240241    class ModelWithGenerate(BaseChatModel):242        @override243        def _generate(244            self,245            messages: list[BaseMessage],246            stop: list[str] | None = None,247            run_manager: CallbackManagerForLLMRun | None = None,248            **kwargs: Any,249        ) -> ChatResult:250            """Top Level call."""251            message = AIMessage(content="hello")252            generation = ChatGeneration(message=message)253            return ChatResult(generations=[generation])254255        @property256        def _llm_type(self) -> str:257            return "fake-chat-model"258259    model = ModelWithGenerate()260    chunks = list(model.stream("anything"))261    # BaseChatModel.stream is typed to return Iterator[BaseMessageChunk].262    # When streaming is disabled, it returns Iterator[BaseMessage], so the type hint263    # is not strictly correct.264    # LangChain documents a pattern of adding BaseMessageChunks to accumulate a stream.265    # This may be better done with `reduce(operator.add, chunks)`.266    assert chunks == [_any_id_ai_message(content="hello")]267268    chunks = [chunk async for chunk in model.astream("anything")]269    assert chunks == [_any_id_ai_message(content="hello")]270271272async def test_astream_implementation_fallback_to_stream() -> None:273    """Test astream uses appropriate implementation."""274275    class ModelWithSyncStream(BaseChatModel):276        def _generate(277            self,278            messages: list[BaseMessage],279            stop: list[str] | None = None,280            run_manager: CallbackManagerForLLMRun | None = None,281            **kwargs: Any,282        ) -> ChatResult:283            """Top Level call."""284            raise NotImplementedError285286        @override287        def _stream(288            self,289            messages: list[BaseMessage],290            stop: list[str] | None = None,291            run_manager: CallbackManagerForLLMRun | None = None,292            **kwargs: Any,293        ) -> Iterator[ChatGenerationChunk]:294            """Stream the output of the model."""295            yield ChatGenerationChunk(message=AIMessageChunk(content="a"))296            yield ChatGenerationChunk(297                message=AIMessageChunk(content="b", chunk_position="last")298            )299300        @property301        def _llm_type(self) -> str:302            return "fake-chat-model"303304    model = ModelWithSyncStream()305    chunks = list(model.stream("anything"))306    assert chunks == [307        _any_id_ai_message_chunk(308            content="a",309        ),310        _any_id_ai_message_chunk(content="b", chunk_position="last"),311    ]312    assert len({chunk.id for chunk in chunks}) == 1313    assert type(model)._astream == BaseChatModel._astream314    astream_chunks = [chunk async for chunk in model.astream("anything")]315    assert astream_chunks == [316        _any_id_ai_message_chunk(317            content="a",318        ),319        _any_id_ai_message_chunk(content="b", chunk_position="last"),320    ]321    assert len({chunk.id for chunk in astream_chunks}) == 1322323324async def test_astream_implementation_uses_astream() -> None:325    """Test astream uses appropriate implementation."""326327    class ModelWithAsyncStream(BaseChatModel):328        def _generate(329            self,330            messages: list[BaseMessage],331            stop: list[str] | None = None,332            run_manager: CallbackManagerForLLMRun | None = None,333            **kwargs: Any,334        ) -> ChatResult:335            """Top Level call."""336            raise NotImplementedError337338        @override339        async def _astream(340            self,341            messages: list[BaseMessage],342            stop: list[str] | None = None,343            run_manager: CallbackManagerForLLMRun | None = None,  # type: ignore[override]344            **kwargs: Any,345        ) -> AsyncIterator[ChatGenerationChunk]:346            """Stream the output of the model."""347            yield ChatGenerationChunk(message=AIMessageChunk(content="a"))348            yield ChatGenerationChunk(349                message=AIMessageChunk(content="b", chunk_position="last")350            )351352        @property353        def _llm_type(self) -> str:354            return "fake-chat-model"355356    model = ModelWithAsyncStream()357    chunks = [chunk async for chunk in model.astream("anything")]358    assert chunks == [359        _any_id_ai_message_chunk(360            content="a",361        ),362        _any_id_ai_message_chunk(content="b", chunk_position="last"),363    ]364    assert len({chunk.id for chunk in chunks}) == 1365366367class FakeTracer(BaseTracer):368    def __init__(self) -> None:369        super().__init__()370        self.traced_run_ids: list[uuid.UUID] = []371372    def _persist_run(self, run: Run) -> None:373        """Persist a run."""374        self.traced_run_ids.append(run.id)375376377class LangChainTracerRunCollector:378    def __init__(self) -> None:379        self.tracer = LangChainTracer()380        self.runs: list[Run] = []381382    @contextmanager383    def tracing_callback(self) -> Iterator[LangChainTracer]:384        def collect_tracer_run(_: LangChainTracer, run: Run) -> None:385            self.runs.append(run)386387        with patch.object(LangChainTracer, "_persist_run", new=collect_tracer_run):388            yield self.tracer389390391def test_pass_run_id() -> None:392    llm = FakeListChatModel(responses=["a", "b", "c"])393    cb = FakeTracer()394    uid1 = uuid.uuid4()395    llm.invoke("Dummy message", {"callbacks": [cb], "run_id": uid1})396    assert cb.traced_run_ids == [uid1]397    uid2 = uuid.uuid4()398    list(llm.stream("Dummy message", {"callbacks": [cb], "run_id": uid2}))399    assert cb.traced_run_ids == [uid1, uid2]400    uid3 = uuid.uuid4()401    llm.batch([["Dummy message"]], {"callbacks": [cb], "run_id": uid3})402    assert cb.traced_run_ids == [uid1, uid2, uid3]403404405async def test_async_pass_run_id() -> None:406    llm = FakeListChatModel(responses=["a", "b", "c"])407    cb = FakeTracer()408    uid1 = uuid.uuid4()409    await llm.ainvoke("Dummy message", {"callbacks": [cb], "run_id": uid1})410    assert cb.traced_run_ids == [uid1]411    uid2 = uuid.uuid4()412    async for _ in llm.astream("Dummy message", {"callbacks": [cb], "run_id": uid2}):413        pass414    assert cb.traced_run_ids == [uid1, uid2]415416    uid3 = uuid.uuid4()417    await llm.abatch([["Dummy message"]], {"callbacks": [cb], "run_id": uid3})418    assert cb.traced_run_ids == [uid1, uid2, uid3]419420421class NoStreamingModel(BaseChatModel):422    @override423    def _generate(424        self,425        messages: list[BaseMessage],426        stop: list[str] | None = None,427        run_manager: CallbackManagerForLLMRun | None = None,428        **kwargs: Any,429    ) -> ChatResult:430        return ChatResult(generations=[ChatGeneration(message=AIMessage("invoke"))])431432    @property433    def _llm_type(self) -> str:434        return "model1"435436437class StreamingModel(NoStreamingModel):438    streaming: bool = False439440    @override441    def _stream(442        self,443        messages: list[BaseMessage],444        stop: list[str] | None = None,445        run_manager: CallbackManagerForLLMRun | None = None,446        **kwargs: Any,447    ) -> Iterator[ChatGenerationChunk]:448        yield ChatGenerationChunk(message=AIMessageChunk(content="stream"))449450451@pytest.mark.parametrize("disable_streaming", [True, False, "tool_calling"])452def test_disable_streaming(453    *,454    disable_streaming: bool | Literal["tool_calling"],455) -> None:456    model = StreamingModel(disable_streaming=disable_streaming)457    assert model.invoke([]).content == "invoke"458459    expected = "invoke" if disable_streaming is True else "stream"460    assert next(model.stream([])).content == expected461    assert (462        model.invoke([], config={"callbacks": [LogStreamCallbackHandler()]}).content463        == expected464    )465466    expected = "invoke" if disable_streaming in {"tool_calling", True} else "stream"467    assert next(model.stream([], tools=[{"type": "function"}])).content == expected468    assert (469        model.invoke(470            [], config={"callbacks": [LogStreamCallbackHandler()]}, tools=[{}]471        ).content472        == expected473    )474475476@pytest.mark.parametrize("disable_streaming", [True, False, "tool_calling"])477async def test_disable_streaming_async(478    *,479    disable_streaming: bool | Literal["tool_calling"],480) -> None:481    model = StreamingModel(disable_streaming=disable_streaming)482    assert (await model.ainvoke([])).content == "invoke"483484    expected = "invoke" if disable_streaming is True else "stream"485    async for c in model.astream([]):486        assert c.content == expected487        break488    assert (489        await model.ainvoke([], config={"callbacks": [_AstreamEventsCallbackHandler()]})490    ).content == expected491492    expected = "invoke" if disable_streaming in {"tool_calling", True} else "stream"493    async for c in model.astream([], tools=[{}]):494        assert c.content == expected495        break496    assert (497        await model.ainvoke(498            [], config={"callbacks": [_AstreamEventsCallbackHandler()]}, tools=[{}]499        )500    ).content == expected501502503async def test_streaming_attribute_overrides_streaming_callback() -> None:504    model = StreamingModel(streaming=False)505    assert (506        await model.ainvoke([], config={"callbacks": [_AstreamEventsCallbackHandler()]})507    ).content == "invoke"508509510class _FakeV2Handler(BaseCallbackHandler, _V2StreamingCallbackHandler):511    """Minimal v2 handler marker for routing tests; records nothing."""512513514async def test_streaming_attribute_overrides_v2_callback() -> None:515    """`self.streaming=False` must opt out of the v2 event path too.516517    `_should_use_protocol_streaming` shares the `_streaming_disabled` opt-outs with518    `_should_stream`, so an instance-level `streaming=False` takes519    precedence over an attached `_V2StreamingCallbackHandler`.520    """521    model = StreamingModel(streaming=False)522    assert (523        await model.ainvoke([], config={"callbacks": [_FakeV2Handler()]})524    ).content == "invoke"525    assert (526        model.invoke([], config={"callbacks": [_FakeV2Handler()]})527    ).content == "invoke"528529530@pytest.mark.parametrize("disable_streaming", [True, False, "tool_calling"])531def test_disable_streaming_no_streaming_model(532    *,533    disable_streaming: bool | Literal["tool_calling"],534) -> None:535    model = NoStreamingModel(disable_streaming=disable_streaming)536    assert model.invoke([]).content == "invoke"537    assert next(model.stream([])).content == "invoke"538    assert (539        model.invoke([], config={"callbacks": [LogStreamCallbackHandler()]}).content540        == "invoke"541    )542    assert next(model.stream([], tools=[{}])).content == "invoke"543544545@pytest.mark.parametrize("disable_streaming", [True, False, "tool_calling"])546async def test_disable_streaming_no_streaming_model_async(547    *,548    disable_streaming: bool | Literal["tool_calling"],549) -> None:550    model = NoStreamingModel(disable_streaming=disable_streaming)551    assert (await model.ainvoke([])).content == "invoke"552    async for c in model.astream([]):553        assert c.content == "invoke"554        break555    assert (556        await model.ainvoke([], config={"callbacks": [_AstreamEventsCallbackHandler()]})557    ).content == "invoke"558    async for c in model.astream([], tools=[{}]):559        assert c.content == "invoke"560        break561562563class FakeChatModelStartTracer(FakeTracer):564    def __init__(self) -> None:565        super().__init__()566        self.messages: list[list[list[BaseMessage]]] = []567568    def on_chat_model_start(self, *args: Any, **kwargs: Any) -> Run:569        _, messages = args570        self.messages.append(messages)571        return super().on_chat_model_start(572            *args,573            **kwargs,574        )575576577def test_trace_images_in_openai_format() -> None:578    """Test that images are traced in OpenAI Chat Completions format."""579    llm = ParrotFakeChatModel()580    messages = [581        {582            "role": "user",583            # v0 format584            "content": [585                {586                    "type": "image",587                    "source_type": "url",588                    "url": "https://example.com/image.png",589                }590            ],591        }592    ]593    tracer = FakeChatModelStartTracer()594    llm.invoke(messages, config={"callbacks": [tracer]})595    assert tracer.messages == [596        [597            [598                HumanMessage(599                    content=[600                        {601                            "type": "image_url",602                            "image_url": {"url": "https://example.com/image.png"},603                        }604                    ]605                )606            ]607        ]608    ]609610611def test_trace_pdfs() -> None:612    # For backward compat613    llm = ParrotFakeChatModel()614    messages = [615        {616            "role": "user",617            "content": [618                {619                    "type": "file",620                    "mime_type": "application/pdf",621                    "base64": "<base64 string>",622                }623            ],624        }625    ]626    tracer = FakeChatModelStartTracer()627628    with warnings.catch_warnings():629        warnings.simplefilter("error")630        llm.invoke(messages, config={"callbacks": [tracer]})631632    assert tracer.messages == [633        [634            [635                HumanMessage(636                    content=[637                        {638                            "type": "file",639                            "mime_type": "application/pdf",640                            "source_type": "base64",641                            "data": "<base64 string>",642                        }643                    ]644                )645            ]646        ]647    ]648649650def test_content_block_transformation_v0_to_v1_image() -> None:651    """Test that v0 format image content blocks are transformed to v1 format."""652    # Create a message with v0 format image content653    image_message = AIMessage(654        content=[655            {656                "type": "image",657                "source_type": "url",658                "url": "https://example.com/image.png",659            }660        ]661    )662663    llm = GenericFakeChatModel(messages=iter([image_message]), output_version="v1")664    response = llm.invoke("test")665666    # With v1 output_version, .content should be transformed667    # Check structure, ignoring auto-generated IDs668    assert len(response.content) == 1669    content_block = response.content[0]670    if isinstance(content_block, dict) and "id" in content_block:671        # Remove auto-generated id for comparison672        content_without_id = {k: v for k, v in content_block.items() if k != "id"}673        expected_content = {674            "type": "image",675            "url": "https://example.com/image.png",676        }677        assert content_without_id == expected_content678    else:679        assert content_block == {680            "type": "image",681            "url": "https://example.com/image.png",682        }683684685@pytest.mark.parametrize("output_version", ["v0", "v1"])686def test_trace_content_blocks_with_no_type_key(output_version: str) -> None:687    """Test behavior of content blocks that don't have a `type` key.688689    Only for blocks with one key, in which case, the name of the key is used as `type`.690691    """692    llm = ParrotFakeChatModel(output_version=output_version)693    messages = [694        {695            "role": "user",696            "content": [697                {698                    "type": "text",699                    "text": "Hello",700                },701                {702                    "cachePoint": {"type": "default"},703                },704            ],705        }706    ]707    tracer = FakeChatModelStartTracer()708    response = llm.invoke(messages, config={"callbacks": [tracer]})709    assert tracer.messages == [710        [711            [712                HumanMessage(713                    [714                        {715                            "type": "text",716                            "text": "Hello",717                        },718                        {719                            "type": "cachePoint",720                            "cachePoint": {"type": "default"},721                        },722                    ]723                )724            ]725        ]726    ]727728    if output_version == "v0":729        assert response.content == [730            {731                "type": "text",732                "text": "Hello",733            },734            {735                "cachePoint": {"type": "default"},736            },737        ]738    else:739        assert response.content == [740            {741                "type": "text",742                "text": "Hello",743            },744            {745                "type": "non_standard",746                "value": {747                    "cachePoint": {"type": "default"},748                },749            },750        ]751752    assert response.content_blocks == [753        {754            "type": "text",755            "text": "Hello",756        },757        {758            "type": "non_standard",759            "value": {760                "cachePoint": {"type": "default"},761            },762        },763    ]764765766def test_extend_support_to_openai_multimodal_formats() -> None:767    """Test normalizing OpenAI audio, image, and file inputs to v1."""768    # Audio and file only (chat model default)769    messages = HumanMessage(770        content=[771            {"type": "text", "text": "Hello"},772            {  # audio-base64773                "type": "input_audio",774                "input_audio": {775                    "format": "wav",776                    "data": "<base64 string>",777                },778            },779            {  # file-base64780                "type": "file",781                "file": {782                    "filename": "draconomicon.pdf",783                    "file_data": "data:application/pdf;base64,<base64 string>",784                },785            },786            {  # file-id787                "type": "file",788                "file": {"file_id": "<file id>"},789            },790        ]791    )792793    expected_content_messages = HumanMessage(794        content=[795            {"type": "text", "text": "Hello"},  # TextContentBlock796            {  # AudioContentBlock797                "type": "audio",798                "base64": "<base64 string>",799                "mime_type": "audio/wav",800            },801            {  # FileContentBlock802                "type": "file",803                "base64": "<base64 string>",804                "mime_type": "application/pdf",805                "extras": {"filename": "draconomicon.pdf"},806            },807            {  # ...808                "type": "file",809                "file_id": "<file id>",810            },811        ]812    )813814    normalized_content = _normalize_messages([messages])815816    # Check structure, ignoring auto-generated IDs817    assert len(normalized_content) == 1818    normalized_message = normalized_content[0]819    assert len(normalized_message.content) == len(expected_content_messages.content)820821    assert _content_blocks_equal_ignore_id(822        normalized_message.content, expected_content_messages.content823    )824825    messages = HumanMessage(826        content=[827            {"type": "text", "text": "Hello"},828            {  # image-url829                "type": "image_url",830                "image_url": {"url": "https://example.com/image.png"},831            },832            {  # image-base64833                "type": "image_url",834                "image_url": {"url": "data:image/jpeg;base64,/9j/4AAQSkZJRg..."},835            },836            {  # audio-base64837                "type": "input_audio",838                "input_audio": {839                    "format": "wav",840                    "data": "<base64 string>",841                },842            },843            {  # file-base64844                "type": "file",845                "file": {846                    "filename": "draconomicon.pdf",847                    "file_data": "data:application/pdf;base64,<base64 string>",848                },849            },850            {  # file-id851                "type": "file",852                "file": {"file_id": "<file id>"},853            },854        ]855    )856857    expected_content_messages = HumanMessage(858        content=[859            {"type": "text", "text": "Hello"},  # TextContentBlock860            {  # image-url passes through861                "type": "image_url",862                "image_url": {"url": "https://example.com/image.png"},863            },864            {  # image-url passes through with inline data865                "type": "image_url",866                "image_url": {"url": "data:image/jpeg;base64,/9j/4AAQSkZJRg..."},867            },868            {  # AudioContentBlock869                "type": "audio",870                "base64": "<base64 string>",871                "mime_type": "audio/wav",872            },873            {  # FileContentBlock874                "type": "file",875                "base64": "<base64 string>",876                "mime_type": "application/pdf",877                "extras": {"filename": "draconomicon.pdf"},878            },879            {  # ...880                "type": "file",881                "file_id": "<file id>",882            },883        ]884    )885886    normalized_content = _normalize_messages([messages])887888    # Check structure, ignoring auto-generated IDs889    assert len(normalized_content) == 1890    normalized_message = normalized_content[0]891    assert len(normalized_message.content) == len(expected_content_messages.content)892893    assert _content_blocks_equal_ignore_id(894        normalized_message.content, expected_content_messages.content895    )896897898def test_normalize_messages_edge_cases() -> None:899    # Test behavior of malformed/unrecognized content blocks900901    messages = [902        HumanMessage(903            content=[904                {905                    "type": "input_image",  # Responses API type; not handled906                    "image_url": "uri",907                },908                {909                    # Standard OpenAI Chat Completions type but malformed structure910                    "type": "input_audio",911                    "input_audio": "uri",  # Should be nested in `audio`912                },913                {914                    "type": "file",915                    "file": "uri",  # `file` should be a dict for Chat Completions916                },917                {918                    "type": "input_file",  # Responses API type; not handled919                    "file_data": "uri",920                    "filename": "file-name",921                },922            ]923        )924    ]925926    assert messages == _normalize_messages(messages)927928929def test_normalize_messages_v1_content_blocks_unchanged() -> None:930    """Test passing v1 content blocks to `_normalize_messages()` leaves unchanged."""931    input_messages = [932        HumanMessage(933            content=[934                {935                    "type": "text",936                    "text": "Hello world",937                },938                {939                    "type": "image",940                    "url": "https://example.com/image.png",941                    "mime_type": "image/png",942                },943                {944                    "type": "audio",945                    "base64": "base64encodedaudiodata",946                    "mime_type": "audio/wav",947                },948                {949                    "type": "file",950                    "id": "file_123",951                },952                {953                    "type": "reasoning",954                    "reasoning": "Let me think about this...",955                },956            ]957        )958    ]959960    result = _normalize_messages(input_messages)961962    # Verify the result is identical to the input (message should not be copied)963    assert len(result) == 1964    assert result[0] is input_messages[0]965    assert result[0].content == input_messages[0].content966967968def test_output_version_invoke(monkeypatch: Any) -> None:969    messages = [AIMessage("hello")]970971    llm = GenericFakeChatModel(messages=iter(messages), output_version="v1")972    response = llm.invoke("hello")973    assert response.content == [{"type": "text", "text": "hello"}]974    assert response.response_metadata["output_version"] == "v1"975976    llm = GenericFakeChatModel(messages=iter(messages))977    response = llm.invoke("hello")978    assert response.content == "hello"979980    monkeypatch.setenv("LC_OUTPUT_VERSION", "v1")981    llm = GenericFakeChatModel(messages=iter(messages))982    response = llm.invoke("hello")983    assert response.content == [{"type": "text", "text": "hello"}]984    assert response.response_metadata["output_version"] == "v1"985986987# -- v1 output version tests --988989990async def test_output_version_ainvoke(monkeypatch: Any) -> None:991    messages = [AIMessage("hello")]992993    # v0994    llm = GenericFakeChatModel(messages=iter(messages))995    response = await llm.ainvoke("hello")996    assert response.content == "hello"997998    # v1999    llm = GenericFakeChatModel(messages=iter(messages), output_version="v1")1000    response = await llm.ainvoke("hello")1001    assert response.content == [{"type": "text", "text": "hello"}]1002    assert response.response_metadata["output_version"] == "v1"10031004    # v1 from env var1005    monkeypatch.setenv("LC_OUTPUT_VERSION", "v1")1006    llm = GenericFakeChatModel(messages=iter(messages))1007    response = await llm.ainvoke("hello")1008    assert response.content == [{"type": "text", "text": "hello"}]1009    assert response.response_metadata["output_version"] == "v1"101010111012class _AnotherFakeChatModel(BaseChatModel):1013    responses: Iterator[AIMessage]1014    """Responses for _generate."""10151016    chunks: Iterator[AIMessageChunk]1017    """Responses for _stream."""10181019    @property1020    def _llm_type(self) -> str:1021        return "another-fake-chat-model"10221023    def _generate(1024        self,1025        *_args: Any,1026        **_kwargs: Any,1027    ) -> ChatResult:1028        return ChatResult(generations=[ChatGeneration(message=next(self.responses))])10291030    async def _agenerate(1031        self,1032        *_args: Any,1033        **_kwargs: Any,1034    ) -> ChatResult:1035        return ChatResult(generations=[ChatGeneration(message=next(self.responses))])10361037    def _stream(1038        self,1039        *_args: Any,1040        **_kwargs: Any,1041    ) -> Iterator[ChatGenerationChunk]:1042        for chunk in self.chunks:1043            yield ChatGenerationChunk(message=chunk)10441045    async def _astream(1046        self,1047        *_args: Any,1048        **_kwargs: Any,1049    ) -> AsyncIterator[ChatGenerationChunk]:1050        for chunk in self.chunks:1051            yield ChatGenerationChunk(message=chunk)105210531054def test_output_version_stream(monkeypatch: Any) -> None:1055    messages = [AIMessage("foo bar")]10561057    # v01058    llm = GenericFakeChatModel(messages=iter(messages))1059    full = None1060    for chunk in llm.stream("hello"):1061        assert isinstance(chunk, AIMessageChunk)1062        assert isinstance(chunk.content, str)1063        assert chunk.content1064        full = chunk if full is None else full + chunk1065    assert isinstance(full, AIMessageChunk)1066    assert full.content == "foo bar"10671068    # v11069    llm = GenericFakeChatModel(messages=iter(messages), output_version="v1")1070    full_v1: AIMessageChunk | None = None1071    for chunk in llm.stream("hello"):1072        assert isinstance(chunk, AIMessageChunk)1073        assert isinstance(chunk.content, list)1074        assert len(chunk.content) == 11075        block = chunk.content[0]1076        assert isinstance(block, dict)1077        assert block["type"] == "text"1078        assert block["text"]1079        full_v1 = chunk if full_v1 is None else full_v1 + chunk1080    assert isinstance(full_v1, AIMessageChunk)1081    assert full_v1.response_metadata["output_version"] == "v1"10821083    assert full_v1.content == [{"type": "text", "text": "foo bar", "index": 0}]10841085    # Test text blocks1086    llm_with_rich_content = _AnotherFakeChatModel(1087        responses=iter([]),1088        chunks=iter(1089            [1090                AIMessageChunk(content="foo "),1091                AIMessageChunk(content="bar"),1092            ]1093        ),1094        output_version="v1",1095    )1096    full_v1 = None1097    for chunk in llm_with_rich_content.stream("hello"):1098        full_v1 = chunk if full_v1 is None else full_v1 + chunk1099    assert isinstance(full_v1, AIMessageChunk)1100    assert full_v1.content_blocks == [{"type": "text", "text": "foo bar", "index": 0}]11011102    # Test content blocks of different types1103    chunks = [1104        AIMessageChunk(content="", additional_kwargs={"reasoning_content": "<rea"}),1105        AIMessageChunk(content="", additional_kwargs={"reasoning_content": "soning>"}),1106        AIMessageChunk(content="<some "),1107        AIMessageChunk(content="text>"),1108    ]1109    llm_with_rich_content = _AnotherFakeChatModel(1110        responses=iter([]),1111        chunks=iter(chunks),1112        output_version="v1",1113    )1114    full_v1 = None1115    for chunk in llm_with_rich_content.stream("hello"):1116        full_v1 = chunk if full_v1 is None else full_v1 + chunk1117    assert isinstance(full_v1, AIMessageChunk)1118    assert full_v1.content_blocks == [1119        {"type": "reasoning", "reasoning": "<reasoning>", "index": 0},1120        {"type": "text", "text": "<some text>", "index": 1},1121    ]11221123    # Test invoke with stream=True1124    llm_with_rich_content = _AnotherFakeChatModel(1125        responses=iter([]),1126        chunks=iter(chunks),1127        output_version="v1",1128    )1129    response_v1 = llm_with_rich_content.invoke("hello", stream=True)1130    assert response_v1.content_blocks == [1131        {"type": "reasoning", "reasoning": "<reasoning>", "index": 0},1132        {"type": "text", "text": "<some text>", "index": 1},1133    ]11341135    # v1 from env var1136    monkeypatch.setenv("LC_OUTPUT_VERSION", "v1")1137    llm = GenericFakeChatModel(messages=iter(messages))1138    full_env = None1139    for chunk in llm.stream("hello"):1140        assert isinstance(chunk, AIMessageChunk)1141        assert isinstance(chunk.content, list)1142        assert len(chunk.content) == 11143        block = chunk.content[0]1144        assert isinstance(block, dict)1145        assert block["type"] == "text"1146        assert block["text"]1147        full_env = chunk if full_env is None else full_env + chunk1148    assert isinstance(full_env, AIMessageChunk)1149    assert full_env.response_metadata["output_version"] == "v1"115011511152async def test_output_version_astream(monkeypatch: Any) -> None:1153    messages = [AIMessage("foo bar")]11541155    # v01156    llm = GenericFakeChatModel(messages=iter(messages))1157    full = None1158    async for chunk in llm.astream("hello"):1159        assert isinstance(chunk, AIMessageChunk)1160        assert isinstance(chunk.content, str)1161        assert chunk.content1162        full = chunk if full is None else full + chunk1163    assert isinstance(full, AIMessageChunk)1164    assert full.content == "foo bar"11651166    # v11167    llm = GenericFakeChatModel(messages=iter(messages), output_version="v1")1168    full_v1: AIMessageChunk | None = None1169    async for chunk in llm.astream("hello"):1170        assert isinstance(chunk, AIMessageChunk)1171        assert isinstance(chunk.content, list)1172        assert len(chunk.content) == 11173        block = chunk.content[0]1174        assert isinstance(block, dict)1175        assert block["type"] == "text"1176        assert block["text"]1177        full_v1 = chunk if full_v1 is None else full_v1 + chunk1178    assert isinstance(full_v1, AIMessageChunk)1179    assert full_v1.response_metadata["output_version"] == "v1"11801181    assert full_v1.content == [{"type": "text", "text": "foo bar", "index": 0}]11821183    # Test text blocks1184    llm_with_rich_content = _AnotherFakeChatModel(1185        responses=iter([]),1186        chunks=iter(1187            [1188                AIMessageChunk(content="foo "),1189                AIMessageChunk(content="bar"),1190            ]1191        ),1192        output_version="v1",1193    )1194    full_v1 = None1195    async for chunk in llm_with_rich_content.astream("hello"):1196        full_v1 = chunk if full_v1 is None else full_v1 + chunk1197    assert isinstance(full_v1, AIMessageChunk)1198    assert full_v1.content_blocks == [{"type": "text", "text": "foo bar", "index": 0}]11991200    # Test content blocks of different types1201    chunks = [1202        AIMessageChunk(content="", additional_kwargs={"reasoning_content": "<rea"}),1203        AIMessageChunk(content="", additional_kwargs={"reasoning_content": "soning>"}),1204        AIMessageChunk(content="<some "),1205        AIMessageChunk(content="text>"),1206    ]1207    llm_with_rich_content = _AnotherFakeChatModel(1208        responses=iter([]),1209        chunks=iter(chunks),1210        output_version="v1",1211    )1212    full_v1 = None1213    async for chunk in llm_with_rich_content.astream("hello"):1214        full_v1 = chunk if full_v1 is None else full_v1 + chunk1215    assert isinstance(full_v1, AIMessageChunk)1216    assert full_v1.content_blocks == [1217        {"type": "reasoning", "reasoning": "<reasoning>", "index": 0},1218        {"type": "text", "text": "<some text>", "index": 1},1219    ]12201221    # Test invoke with stream=True1222    llm_with_rich_content = _AnotherFakeChatModel(1223        responses=iter([]),1224        chunks=iter(chunks),1225        output_version="v1",1226    )1227    response_v1 = await llm_with_rich_content.ainvoke("hello", stream=True)1228    assert response_v1.content_blocks == [1229        {"type": "reasoning", "reasoning": "<reasoning>", "index": 0},1230        {"type": "text", "text": "<some text>", "index": 1},1231    ]12321233    # v1 from env var1234    monkeypatch.setenv("LC_OUTPUT_VERSION", "v1")1235    llm = GenericFakeChatModel(messages=iter(messages))1236    full_env = None1237    async for chunk in llm.astream("hello"):1238        assert isinstance(chunk, AIMessageChunk)1239        assert isinstance(chunk.content, list)1240        assert len(chunk.content) == 11241        block = chunk.content[0]1242        assert isinstance(block, dict)1243        assert block["type"] == "text"1244        assert block["text"]1245        full_env = chunk if full_env is None else full_env + chunk1246    assert isinstance(full_env, AIMessageChunk)1247    assert full_env.response_metadata["output_version"] == "v1"1248    assert messages == _normalize_messages(messages)124912501251def test_get_ls_params() -> None:1252    class LSParamsModel(BaseChatModel):1253        model: str = "foo"1254        temperature: float = 0.11255        max_tokens: int = 102412561257        def _generate(1258            self,1259            messages: list[BaseMessage],1260            stop: list[str] | None = None,1261            run_manager: CallbackManagerForLLMRun | None = None,1262            **kwargs: Any,1263        ) -> ChatResult:1264            raise NotImplementedError12651266        @override1267        def _stream(1268            self,1269            messages: list[BaseMessage],1270            stop: list[str] | None = None,1271            run_manager: CallbackManagerForLLMRun | None = None,1272            **kwargs: Any,1273        ) -> Iterator[ChatGenerationChunk]:1274            raise NotImplementedError12751276        @property1277        def _llm_type(self) -> str:1278            return "fake-chat-model"12791280    llm = LSParamsModel()12811282    # Test standard tracing params1283    ls_params = llm._get_ls_params()1284    assert ls_params == {1285        "ls_provider": "lsparamsmodel",1286        "ls_model_type": "chat",1287        "ls_model_name": "foo",1288        "ls_temperature": 0.1,1289        "ls_max_tokens": 1024,1290    }12911292    ls_params = llm._get_ls_params(model="bar")1293    assert ls_params["ls_model_name"] == "bar"12941295    ls_params = llm._get_ls_params(temperature=0.2)1296    assert ls_params["ls_temperature"] == 0.212971298    # Test integer temperature values (regression test for issue #35300)1299    ls_params = llm._get_ls_params(temperature=0)1300    assert ls_params["ls_temperature"] == 013011302    ls_params = llm._get_ls_params(temperature=1)1303    assert ls_params["ls_temperature"] == 113041305    ls_params = llm._get_ls_params(max_tokens=2048)1306    assert ls_params["ls_max_tokens"] == 204813071308    ls_params = llm._get_ls_params(stop=["stop"])1309    assert ls_params["ls_stop"] == ["stop"]131013111312class _VersionedFakeModel(FakeListChatModel):1313    """Fake model that adds a version via `_add_version`."""13141315    def model_post_init(self, _context: Any, /) -> None:1316        super().model_post_init(_context)1317        self._add_version("langchain-fake", "0.1.0")131813191320def test_user_lc_versions_metadata_survives_merge() -> None:1321    """User-provided `lc_versions` metadata should merge with model versions."""1322    llm = _VersionedFakeModel(responses=["hello"])1323    user_config: RunnableConfig = {"metadata": {"lc_versions": {"my-app": "2.0"}}}13241325    with collect_runs() as cb:1326        llm.invoke([HumanMessage(content="hi")], config=user_config)1327        assert len(cb.traced_runs) == 11328        run_metadata = cb.traced_runs[0].extra["metadata"]1329        assert "my-app" in run_metadata["lc_versions"]1330        assert run_metadata["lc_versions"]["my-app"] == "2.0"1331        assert "langchain-fake" in run_metadata["lc_versions"]1332        assert "langchain-core" in run_metadata["lc_versions"]133313341335def test_user_versions_metadata_remains_user_owned() -> None:1336    """User-owned `versions` metadata is not used for package version tracking."""1337    llm = _VersionedFakeModel(responses=["hello"])1338    user_config: RunnableConfig = {"metadata": {"versions": {"my-app": "2.0"}}}13391340    with collect_runs() as cb:1341        llm.invoke([HumanMessage(content="hi")], config=user_config)1342        assert len(cb.traced_runs) == 11343        run_metadata = cb.traced_runs[0].extra["metadata"]1344        assert run_metadata["versions"] == {"my-app": "2.0"}1345        assert "langchain-fake" not in run_metadata["versions"]1346        assert "langchain-core" not in run_metadata["versions"]1347        assert "langchain-fake" in run_metadata["lc_versions"]1348        assert "langchain-core" in run_metadata["lc_versions"]134913501351async def test_user_lc_versions_metadata_survives_merge_async() -> None:1352    """Async variant: user-provided `lc_versions` metadata merges with model's."""1353    llm = _VersionedFakeModel(responses=["hello"])1354    user_config: RunnableConfig = {"metadata": {"lc_versions": {"my-app": "2.0"}}}13551356    with collect_runs() as cb:1357        await llm.ainvoke([HumanMessage(content="hi")], config=user_config)1358        assert len(cb.traced_runs) == 11359        run_metadata = cb.traced_runs[0].extra["metadata"]1360        assert "my-app" in run_metadata["lc_versions"]1361        assert "langchain-fake" in run_metadata["lc_versions"]1362        assert "langchain-core" in run_metadata["lc_versions"]136313641365def test_user_lc_versions_metadata_survives_merge_stream() -> None:1366    """Stream variant: user-provided `lc_versions` metadata merges with model's."""1367    llm = _VersionedFakeModel(responses=["hello"])1368    user_config: RunnableConfig = {"metadata": {"lc_versions": {"my-app": "2.0"}}}13691370    with collect_runs() as cb:1371        for _ in llm.stream([HumanMessage(content="hi")], config=user_config):1372            pass1373        assert len(cb.traced_runs) == 11374        run_metadata = cb.traced_runs[0].extra["metadata"]1375        assert "my-app" in run_metadata["lc_versions"]1376        assert "langchain-fake" in run_metadata["lc_versions"]1377        assert "langchain-core" in run_metadata["lc_versions"]137813791380async def test_user_lc_versions_metadata_survives_merge_astream() -> None:1381    """Async stream variant: user-provided `lc_versions` metadata merges."""1382    llm = _VersionedFakeModel(responses=["hello"])1383    user_config: RunnableConfig = {"metadata": {"lc_versions": {"my-app": "2.0"}}}13841385    with collect_runs() as cb:1386        async for _ in llm.astream([HumanMessage(content="hi")], config=user_config):1387            pass1388        assert len(cb.traced_runs) == 11389        run_metadata = cb.traced_runs[0].extra["metadata"]1390        assert "my-app" in run_metadata["lc_versions"]1391        assert "langchain-fake" in run_metadata["lc_versions"]1392        assert "langchain-core" in run_metadata["lc_versions"]139313941395def test_add_version_with_none_metadata() -> None:1396    """Model constructed with metadata=None should still get `lc_versions`."""1397    llm = FakeListChatModel(responses=["x"], metadata=None)1398    assert llm.metadata is not None1399    assert "langchain-core" in llm.metadata["lc_versions"]140014011402def test_add_version_with_non_dict_lc_versions() -> None:1403    """Non-dict `lc_versions` value is replaced with a warning."""1404    with warnings.catch_warnings(record=True) as w:1405        warnings.simplefilter("always")1406        llm = FakeListChatModel(responses=["x"], metadata={"lc_versions": "garbage"})1407        assert any("expected a dict" in str(warning.message) for warning in w)1408    assert llm.metadata is not None1409    assert isinstance(llm.metadata["lc_versions"], dict)1410    assert "langchain-core" in llm.metadata["lc_versions"]141114121413def test_langchain_version_in_metadata(monkeypatch: pytest.MonkeyPatch) -> None:1414    """When `langchain` is installed, its version appears in metadata."""14151416    def _fake_version(pkg: str) -> str:1417        if pkg == "langchain":1418            return "1.2.13"1419        raise PackageNotFoundError(pkg)14201421    monkeypatch.setattr("importlib.metadata.version", _fake_version)1422    _get_langchain_version.cache_clear()1423    try:1424        llm = FakeListChatModel(responses=["x"])1425        assert llm.metadata is not None1426        assert llm.metadata["lc_versions"]["langchain"] == "1.2.13"1427        assert "langchain-core" in llm.metadata["lc_versions"]1428    finally:1429        _get_langchain_version.cache_clear()143014311432def test_langchain_version_missing_when_not_installed(1433    monkeypatch: pytest.MonkeyPatch,1434) -> None:1435    """When `langchain` is not installed, metadata.lc_versions has no entry.14361437    The lookup raises `PackageNotFoundError` (the real not-installed signal), which1438    `_get_langchain_version` must swallow without dropping the rest of the dict.1439    """14401441    def _raise_not_found(pkg: str) -> str:1442        raise PackageNotFoundError(pkg)14431444    monkeypatch.setattr("importlib.metadata.version", _raise_not_found)1445    _get_langchain_version.cache_clear()1446    try:1447        llm = FakeListChatModel(responses=["x"])1448        assert llm.metadata is not None1449        assert "langchain" not in llm.metadata["lc_versions"]1450        assert "langchain-core" in llm.metadata["lc_versions"]1451    finally:1452        _get_langchain_version.cache_clear()145314541455def test_version_validator_coexists_with_core_seed() -> None:1456    """A `model_validator(mode="after")` calling `_add_version` keeps the core seed.14571458    Mirrors the real partner pattern (a validator, not a `model_post_init`1459    override) and confirms the validator-added entry and the core-seeded1460    `langchain-core` entry accumulate rather than clobber one another.1461    """14621463    class _ValidatorVersionedModel(FakeListChatModel):1464        @model_validator(mode="after")1465        def _set_fake_version(self) -> Self:1466            self._add_version("langchain-fake", "0.1.0")1467            return self14681469    llm = _ValidatorVersionedModel(responses=["x"])1470    assert llm.metadata is not None1471    versions = llm.metadata["lc_versions"]1472    assert versions["langchain-core"] == VERSION1473    assert versions["langchain-fake"] == "0.1.0"147414751476def test_subclass_unique_validator_names_accumulate() -> None:1477    """Parent and child uniquely-named validators must both contribute entries.14781479    Regression guard for the documented Pydantic footgun: same-named1480    `model_validator` methods *replace* rather than chain across a class1481    hierarchy. As long as each layer uses a unique validator name, a subclass1482    must not silently drop its parent's version entry.1483    """14841485    class _ParentModel(FakeListChatModel):1486        @model_validator(mode="after")1487        def _set_parent_version(self) -> Self:1488            self._add_version("langchain-parent", "1.0.0")1489            return self14901491    class _ChildModel(_ParentModel):1492        @model_validator(mode="after")1493        def _set_child_version(self) -> Self:1494            self._add_version("langchain-child", "2.0.0")1495            return self14961497    llm = _ChildModel(responses=["x"])1498    assert llm.metadata is not None1499    versions = llm.metadata["lc_versions"]1500    assert versions["langchain-core"] == VERSION1501    assert versions["langchain-parent"] == "1.0.0"1502    assert versions["langchain-child"] == "2.0.0"150315041505def test_model_profiles() -> None:1506    model = GenericFakeChatModel(messages=iter([]))1507    assert model.profile is None15081509    model_with_profile = GenericFakeChatModel(1510        messages=iter([]), profile={"max_input_tokens": 100}1511    )1512    assert model_with_profile.profile == {"max_input_tokens": 100}151315141515def test_resolve_model_profile_hook_populates_profile() -> None:1516    """_resolve_model_profile is called when profile is None."""15171518    class ResolverModel(GenericFakeChatModel):1519        def _resolve_model_profile(self) -> ModelProfile | None:1520            return {"max_input_tokens": 500}15211522    model = ResolverModel(messages=iter([]))1523    assert model.profile == {"max_input_tokens": 500}152415251526def test_resolve_model_profile_hook_skipped_when_explicit() -> None:1527    """_resolve_model_profile is NOT called when profile is set explicitly."""15281529    class ResolverModel(GenericFakeChatModel):1530        def _resolve_model_profile(self) -> ModelProfile | None:1531            return {"max_input_tokens": 500}15321533    model = ResolverModel(messages=iter([]), profile={"max_input_tokens": 999})1534    assert model.profile is not None1535    assert model.profile["max_input_tokens"] == 999153615371538def test_resolve_model_profile_hook_exception_is_caught() -> None:1539    """Model is still usable if _resolve_model_profile raises."""15401541    class BrokenProfileModel(GenericFakeChatModel):1542        def _resolve_model_profile(self) -> ModelProfile | None:1543            msg = "profile file not found"1544            raise RuntimeError(msg)15451546    with warnings.catch_warnings(record=True):1547        warnings.simplefilter("always")1548        model = BrokenProfileModel(messages=iter([]))15491550    assert model.profile is None155115521553def test_check_profile_keys_runs_despite_partner_override() -> None:1554    """Verify _check_profile_keys fires even when _set_model_profile is overridden.15551556    Because _check_profile_keys has a distinct validator name from1557    _set_model_profile, a partner override of the latter does not suppress1558    the key-checking validator.1559    """15601561    class PartnerModel(GenericFakeChatModel):1562        """Simulates a partner that overrides _set_model_profile."""15631564        @model_validator(mode="after")1565        def _set_model_profile(self) -> Self:1566            if self.profile is None:1567                profile: dict[str, Any] = {1568                    "max_input_tokens": 100,1569                    "partner_only_field": True,1570                }1571                self.profile = profile  # type: ignore[assignment]1572            return self15731574    with warnings.catch_warnings(record=True) as w:1575        warnings.simplefilter("always")1576        model = PartnerModel(messages=iter([]))15771578    assert model.profile is not None1579    assert model.profile.get("partner_only_field") is True1580    profile_warnings = [x for x in w if "Unrecognized keys" in str(x.message)]1581    assert len(profile_warnings) == 11582    assert "partner_only_field" in str(profile_warnings[0].message)158315841585class MockResponse:1586    """Mock response for testing _generate_response_from_error."""15871588    def __init__(1589        self,1590        status_code: int = 400,1591        headers: dict[str, str] | None = None,1592        json_data: dict[str, Any] | None = None,1593        json_raises: type[Exception] | None = None,1594        text_raises: type[Exception] | None = None,1595    ):1596        self.status_code = status_code1597        self.headers = headers or {}1598        self._json_data = json_data1599        self._json_raises = json_raises1600        self._text_raises = text_raises16011602    def json(self) -> dict[str, Any]:1603        if self._json_raises:1604            msg = "JSON parsing failed"1605            raise self._json_raises(msg)1606        return self._json_data or {}16071608    @property1609    def text(self) -> str:1610        if self._text_raises:1611            msg = "Text access failed"1612            raise self._text_raises(msg)1613        return ""161416151616class MockAPIError(Exception):1617    """Mock API error with response attribute."""16181619    def __init__(self, message: str, response: MockResponse | None = None):1620        super().__init__(message)1621        self.message = message1622        if response is not None:1623            self.response = response162416251626def test_generate_response_from_error_with_valid_json() -> None:1627    """Test `_generate_response_from_error` with valid JSON response."""1628    response = MockResponse(1629        status_code=400,1630        headers={"content-type": "application/json"},1631        json_data={"error": {"message": "Bad request", "type": "invalid_request"}},1632    )1633    error = MockAPIError("API Error", response=response)16341635    generations = _generate_response_from_error(error)16361637    assert len(generations) == 11638    generation = generations[0]1639    assert isinstance(generation, ChatGeneration)1640    assert isinstance(generation.message, AIMessage)1641    assert generation.message.content == ""16421643    metadata = generation.message.response_metadata1644    assert metadata["body"] == {1645        "error": {"message": "Bad request", "type": "invalid_request"}1646    }1647    assert metadata["headers"] == {"content-type": "application/json"}1648    assert metadata["status_code"] == 400164916501651def test_generate_response_from_error_handles_streaming_response_failure() -> None:1652    # Simulates scenario where accessing response.json() or response.text1653    # raises ResponseNotRead on streaming responses1654    response = MockResponse(1655        status_code=400,1656        headers={"content-type": "application/json"},1657        json_raises=Exception,  # Simulates ResponseNotRead or similar1658        text_raises=Exception,1659    )1660    error = MockAPIError("API Error", response=response)16611662    # This should NOT raise an exception, but should handle it gracefully1663    generations = _generate_response_from_error(error)16641665    assert len(generations) == 11666    generation = generations[0]1667    metadata = generation.message.response_metadata16681669    # When both fail, body should be None instead of raising an exception1670    assert metadata["body"] is None1671    assert metadata["headers"] == {"content-type": "application/json"}1672    assert metadata["status_code"] == 400167316741675def test_filter_invocation_params_for_tracing() -> None:1676    """Test that large fields are filtered from invocation params for tracing."""1677    params = {1678        "temperature": 0.7,1679        "tools": [{"name": "test_tool"}],1680        "functions": [{"name": "test_function"}],1681        "messages": [{"role": "system", "content": "test"}],1682        "response_format": {"type": "json_object"},1683    }1684    filtered = _filter_invocation_params_for_tracing(params)16851686    # Should include temperature1687    assert "temperature" in filtered1688    assert filtered["temperature"] == 0.716891690    # Should exclude these large fields1691    assert "tools" not in filtered1692    assert "functions" not in filtered1693    assert "messages" not in filtered1694    assert "response_format" not in filtered169516961697class FakeChatModelWithInvocationParams(SimpleChatModel):1698    """Fake chat model with invocation params for testing tracing."""16991700    temperature: float = 0.717011702    @property1703    @override1704    def _llm_type(self) -> str:1705        return "fake-chat-model-with-invocation-params"17061707    @property1708    @override1709    def _identifying_params(self) -> dict[str, Any]:1710        return {1711            "temperature": self.temperature,1712            "tools": [{"name": "test_tool"}],1713            "functions": [{"name": "test_function"}],1714            "messages": [{"role": "system", "content": "test"}],1715            "response_format": {"type": "json_object"},1716        }17171718    @override1719    def _call(1720        self,1721        messages: list[BaseMessage],1722        stop: list[str] | None = None,1723        run_manager: CallbackManagerForLLMRun | None = None,1724        **kwargs: Any,1725    ) -> str:1726        return "test response"172717281729class FakeStreamingChatModelWithInvocationParams(FakeChatModelWithInvocationParams):1730    """Streaming counterpart for tracer metadata tests."""17311732    @override1733    def _stream(1734        self,1735        messages: list[BaseMessage],1736        stop: list[str] | None = None,1737        run_manager: CallbackManagerForLLMRun | None = None,1738        **kwargs: Any,1739    ) -> Iterator[ChatGenerationChunk]:1740        del messages, stop, run_manager, kwargs1741        yield ChatGenerationChunk(message=AIMessageChunk(content="test response"))17421743    @override1744    async def _astream(1745        self,1746        messages: list[BaseMessage],1747        stop: list[str] | None = None,1748        run_manager: AsyncCallbackManagerForLLMRun | None = None,1749        **kwargs: Any,1750    ) -> AsyncIterator[ChatGenerationChunk]:1751        del messages, stop, run_manager, kwargs1752        yield ChatGenerationChunk(message=AIMessageChunk(content="test response"))175317541755def test_invocation_params_passed_to_tracer_metadata() -> None:1756    """Test that invocation params are passed to tracer metadata."""1757    llm = FakeChatModelWithInvocationParams()1758    collector = LangChainTracerRunCollector()17591760    with collector.tracing_callback() as tracer:1761        llm.invoke([HumanMessage(content="Hello")], config={"callbacks": [tracer]})17621763    assert len(collector.runs) == 11764    run = collector.runs[0]17651766    # LangSmith injects environment-derived keys (e.g. `revision_id`,1767    # `LANGCHAIN_TESTS_USER_AGENT`, `LANGSMITH_*`) into run metadata. These vary1768    # by environment and are not the subject of this test, so strip them before1769    # the exact-equality comparison.1770    for env_key in get_langchain_env_var_metadata():1771        run.extra["metadata"].pop(env_key, None)17721773    assert run.extra == {1774        "batch_size": 1,1775        "invocation_params": {1776            "_type": "fake-chat-model-with-invocation-params",1777            "functions": [{"name": "test_function"}],1778            "messages": [{"content": "test", "role": "system"}],1779            "response_format": {"type": "json_object"},1780            "stop": None,1781            "temperature": 0.7,1782            "tools": [{"name": "test_tool"}],1783        },1784        "metadata": {1785            "_type": "fake-chat-model-with-invocation-params",1786            "ls_integration": "langchain_chat_model",1787            "ls_model_type": "chat",1788            "ls_provider": "fakechatmodelwithinvocationparams",1789            "ls_temperature": 0.7,1790            "stop": None,1791            "temperature": 0.7,1792            "lc_versions": {"langchain-core": VERSION},1793        },1794        "options": {"stop": None},1795        "runtime": run.extra["runtime"],1796    }1797    assert run.metadata == run.extra["metadata"]179817991800def test_stream_events_v3_invocation_params_passed_to_tracer_metadata() -> None:1801    """`stream_events(version="v3")` preserves filtered invocation params."""1802    llm = FakeStreamingChatModelWithInvocationParams()1803    collector = LangChainTracerRunCollector()18041805    with collector.tracing_callback() as tracer:1806        _ = llm.stream_events(1807            [HumanMessage(content="Hello")],1808            config={"callbacks": [tracer]},1809            stop=["done"],1810            version="v3",1811        ).output18121813    assert len(collector.runs) == 11814    metadata = collector.runs[0].extra["metadata"]18151816    assert metadata["_type"] == "fake-chat-model-with-invocation-params"1817    assert metadata["stop"] == ["done"]1818    assert metadata["temperature"] == 0.7181918201821async def test_astream_events_v3_invocation_params_passed_to_tracer_metadata() -> None:1822    """`astream_events(version="v3")` preserves filtered invocation params."""1823    llm = FakeStreamingChatModelWithInvocationParams()1824    collector = LangChainTracerRunCollector()18251826    with collector.tracing_callback() as tracer:1827        stream = await llm.astream_events(1828            [HumanMessage(content="Hello")],1829            config={"callbacks": [tracer]},1830            stop=["done"],1831            version="v3",1832        )1833        _ = await stream18341835    assert len(collector.runs) == 11836    metadata = collector.runs[0].extra["metadata"]18371838    assert metadata["_type"] == "fake-chat-model-with-invocation-params"1839    assert metadata["stop"] == ["done"]1840    assert metadata["temperature"] == 0.7

Code quality findings 71

Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(actual, str) or isinstance(expected, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(actual_block, dict) and "id" in actual_block
Ensure functions have docstrings for documentation
missing-docstring
def test_asdict_replaces_deprecated_dict() -> None:
Ensure functions have docstrings for documentation
missing-docstring
def test_base_chat_model_type_hints_resolve() -> None:
Ensure functions have docstrings for documentation
missing-docstring
def dict(self, **kwargs: Any) -> dict[str, Any]:
Ensure functions have docstrings for documentation
missing-docstring
def messages() -> list[BaseMessage]:
Ensure functions have docstrings for documentation
missing-docstring
def messages_2() -> list[BaseMessage]:
Ensure functions have docstrings for documentation
missing-docstring
def test_batch_size(messages: list[BaseMessage], messages_2: list[BaseMessage]) -> None:
Avoid unnecessary list conversions; use generators where possible
unnecessary-list
list(llm.stream(messages))
Ensure functions have docstrings for documentation
missing-docstring
async def test_async_batch_size(
Ensure functions have docstrings for documentation
missing-docstring
async def test_stream_error_callback() -> None:
Ensure functions have docstrings for documentation
missing-docstring
def eval_response(callback: BaseFakeCallbackHandler, i: int) -> None:
Avoid unnecessary list conversions; use generators where possible
unnecessary-list
chunks = list(model.stream("anything"))
Avoid unnecessary list conversions; use generators where possible
unnecessary-list
chunks = list(model.stream("anything"))
Use isinstance() for type checking instead of type()
type-check
assert type(model)._astream == BaseChatModel._astream
Ensure functions have docstrings for documentation
missing-docstring
def tracing_callback(self) -> Iterator[LangChainTracer]:
Ensure functions have docstrings for documentation
missing-docstring
def collect_tracer_run(_: LangChainTracer, run: Run) -> None:
Ensure functions have docstrings for documentation
missing-docstring
def test_pass_run_id() -> None:
Avoid unnecessary list conversions; use generators where possible
unnecessary-list
list(llm.stream("Dummy message", {"callbacks": [cb], "run_id": uid2}))
Ensure functions have docstrings for documentation
missing-docstring
async def test_async_pass_run_id() -> None:
Ensure functions have docstrings for documentation
missing-docstring
def test_disable_streaming(
Ensure functions have docstrings for documentation
missing-docstring
async def test_disable_streaming_async(
Ensure functions have docstrings for documentation
missing-docstring
async def test_streaming_attribute_overrides_streaming_callback() -> None:
Ensure functions have docstrings for documentation
missing-docstring
def test_disable_streaming_no_streaming_model(
Ensure functions have docstrings for documentation
missing-docstring
async def test_disable_streaming_no_streaming_model_async(
Ensure functions have docstrings for documentation
missing-docstring
def on_chat_model_start(self, *args: Any, **kwargs: Any) -> Run:
Ensure functions have docstrings for documentation
missing-docstring
def test_trace_pdfs() -> None:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(content_block, dict) and "id" in content_block:
Ensure functions have docstrings for documentation
missing-docstring
def test_normalize_messages_edge_cases() -> None:
Ensure functions have docstrings for documentation
missing-docstring
def test_output_version_invoke(monkeypatch: Any) -> None:
Ensure functions have docstrings for documentation
missing-docstring
async def test_output_version_ainvoke(monkeypatch: Any) -> None:
Ensure functions have docstrings for documentation
missing-docstring
def test_output_version_stream(monkeypatch: Any) -> None:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(chunk, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(chunk.content, str)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(full, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(chunk, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(chunk.content, list)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(block, dict)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(full_v1, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(full_v1, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(full_v1, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(chunk, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(chunk.content, list)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(block, dict)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(full_env, AIMessageChunk)
Ensure functions have docstrings for documentation
missing-docstring
async def test_output_version_astream(monkeypatch: Any) -> None:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(chunk, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(chunk.content, str)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(full, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(chunk, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(chunk.content, list)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(block, dict)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(full_v1, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(full_v1, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(full_v1, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(chunk, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(chunk.content, list)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(block, dict)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(full_env, AIMessageChunk)
Ensure functions have docstrings for documentation
missing-docstring
def test_get_ls_params() -> None:
Ensure functions have docstrings for documentation
missing-docstring
def model_post_init(self, _context: Any, /) -> None:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(llm.metadata["lc_versions"], dict)
Ensure functions have docstrings for documentation
missing-docstring
def test_langchain_version_missing_when_not_installed(
Ensure functions have docstrings for documentation
missing-docstring
def test_model_profiles() -> None:
Ensure functions have docstrings for documentation
missing-docstring
def json(self) -> dict[str, Any]:
Ensure functions have docstrings for documentation
missing-docstring
def text(self) -> str:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(generation, ChatGeneration)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(generation.message, AIMessage)
Ensure functions have docstrings for documentation
missing-docstring
def test_generate_response_from_error_handles_streaming_response_failure() -> None:
Avoid unless necessary; Python's garbage collector typically handles object deletion
unnecessary-del
del messages, stop, run_manager, kwargs
Avoid unless necessary; Python's garbage collector typically handles object deletion
unnecessary-del
del messages, stop, run_manager, kwargs

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.