libs/partners/anthropic/tests/unit_tests/test_chat_models.py PYTHON 3,212 lines View on github.com → Search inside
File is large — showing lines 1–2,000 of 3,212.
1"""Test chat model integration."""23from __future__ import annotations45import copy6import os7import warnings8from collections.abc import Callable9from typing import Any, Literal, cast10from unittest.mock import MagicMock, patch1112import anthropic13import pytest14from anthropic.types import Message, TextBlock, Usage15from blockbuster import blockbuster_ctx16from langchain_core.exceptions import ContextOverflowError17from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage18from langchain_core.runnables import RunnableBinding19from langchain_core.tools import BaseTool, tool20from langchain_core.tracers.base import BaseTracer21from langchain_core.tracers.schemas import Run22from pydantic import BaseModel, Field, SecretStr, ValidationError23from pytest import CaptureFixture, MonkeyPatch2425from langchain_anthropic import ChatAnthropic26from langchain_anthropic.chat_models import (27    _create_usage_metadata,28    _format_image,29    _format_messages,30    _is_builtin_tool,31    _merge_messages,32    _thinking_in_params,33    convert_to_anthropic_tool,34)3536os.environ["ANTHROPIC_API_KEY"] = "foo"3738MODEL_NAME = "claude-sonnet-4-5-20250929"394041def test_initialization() -> None:42    """Test chat model initialization."""43    for model in [44        ChatAnthropic(model_name=MODEL_NAME, api_key="xyz", timeout=2),  # type: ignore[arg-type, call-arg]45        ChatAnthropic(  # type: ignore[call-arg, call-arg, call-arg]46            model=MODEL_NAME,47            anthropic_api_key="xyz",48            default_request_timeout=2,49            base_url="https://api.anthropic.com",50        ),51    ]:52        assert model.model == MODEL_NAME53        assert cast("SecretStr", model.anthropic_api_key).get_secret_value() == "xyz"54        assert model.default_request_timeout == 2.055        assert model.anthropic_api_url == "https://api.anthropic.com"565758def test_user_agent_header_in_client_params() -> None:59    """Test that _client_params includes a User-Agent header."""60    llm = ChatAnthropic(model=MODEL_NAME, api_key="test-key")  # type: ignore[arg-type]61    params = llm._client_params62    assert "default_headers" in params63    assert "User-Agent" in params["default_headers"]64    assert params["default_headers"]["User-Agent"].startswith("langchain-anthropic/")656667@pytest.mark.parametrize("async_api", [True, False])68def test_streaming_attribute_should_stream(async_api: bool) -> None:  # noqa: FBT00169    llm = ChatAnthropic(model=MODEL_NAME, streaming=True)70    assert llm._should_stream(async_api=async_api)717273def test_anthropic_client_caching() -> None:74    """Test that the OpenAI client is cached."""75    llm1 = ChatAnthropic(model=MODEL_NAME)76    llm2 = ChatAnthropic(model=MODEL_NAME)77    assert llm1._client._client is llm2._client._client7879    llm3 = ChatAnthropic(model=MODEL_NAME, base_url="foo")80    assert llm1._client._client is not llm3._client._client8182    llm4 = ChatAnthropic(model=MODEL_NAME, timeout=None)83    assert llm1._client._client is llm4._client._client8485    llm5 = ChatAnthropic(model=MODEL_NAME, timeout=3)86    assert llm1._client._client is not llm5._client._client878889def test_anthropic_proxy_support() -> None:90    """Test that both sync and async clients support proxy configuration."""91    proxy_url = "http://proxy.example.com:8080"9293    # Test sync client with proxy94    llm_sync = ChatAnthropic(model=MODEL_NAME, anthropic_proxy=proxy_url)95    sync_client = llm_sync._client96    assert sync_client is not None9798    # Test async client with proxy - this should not raise TypeError99    async_client = llm_sync._async_client100    assert async_client is not None101102    # Test that clients with different proxy settings are not cached together103    llm_no_proxy = ChatAnthropic(model=MODEL_NAME)104    llm_with_proxy = ChatAnthropic(model=MODEL_NAME, anthropic_proxy=proxy_url)105106    # Different proxy settings should result in different cached clients107    assert llm_no_proxy._client._client is not llm_with_proxy._client._client108109110def test_anthropic_proxy_from_environment() -> None:111    """Test that proxy can be set from ANTHROPIC_PROXY environment variable."""112    proxy_url = "http://env-proxy.example.com:8080"113114    # Test with environment variable set115    with patch.dict(os.environ, {"ANTHROPIC_PROXY": proxy_url}):116        llm = ChatAnthropic(model=MODEL_NAME)117        assert llm.anthropic_proxy == proxy_url118119        # Should be able to create clients successfully120        sync_client = llm._client121        async_client = llm._async_client122        assert sync_client is not None123        assert async_client is not None124125    # Test that explicit parameter overrides environment variable126    with patch.dict(os.environ, {"ANTHROPIC_PROXY": "http://env-proxy.com"}):127        explicit_proxy = "http://explicit-proxy.com"128        llm = ChatAnthropic(model=MODEL_NAME, anthropic_proxy=explicit_proxy)129        assert llm.anthropic_proxy == explicit_proxy130131132def test_set_default_max_tokens() -> None:133    """Test the set_default_max_tokens function."""134    # Test claude-sonnet-4-5 models135    llm = ChatAnthropic(model="claude-sonnet-4-5-20250929", anthropic_api_key="test")136    assert llm.max_tokens == 64000137138    # Test claude-opus-4 models139    llm = ChatAnthropic(model="claude-opus-4-20250514", anthropic_api_key="test")140    assert llm.max_tokens == 32000141142    # Test claude-sonnet-4 models143    llm = ChatAnthropic(model="claude-sonnet-4-20250514", anthropic_api_key="test")144    assert llm.max_tokens == 64000145146    # Test claude-3-7-sonnet models147    llm = ChatAnthropic(model="claude-3-7-sonnet-20250219", anthropic_api_key="test")148    assert llm.max_tokens == 64000149150    # Test claude-3-5-haiku models151    llm = ChatAnthropic(model="claude-3-5-haiku-20241022", anthropic_api_key="test")152    assert llm.max_tokens == 8192153154    # Test claude-3-haiku models (should default to 4096)155    llm = ChatAnthropic(model="claude-3-haiku-20240307", anthropic_api_key="test")156    assert llm.max_tokens == 4096157158    # Test that existing max_tokens values are preserved159    llm = ChatAnthropic(model=MODEL_NAME, max_tokens=2048, anthropic_api_key="test")160    assert llm.max_tokens == 2048161162    # Test that explicitly set max_tokens values are preserved163    llm = ChatAnthropic(model=MODEL_NAME, max_tokens=4096, anthropic_api_key="test")164    assert llm.max_tokens == 4096165166167@pytest.mark.requires("anthropic")168def test_anthropic_model_name_param() -> None:169    llm = ChatAnthropic(model_name=MODEL_NAME)  # type: ignore[call-arg, call-arg]170    assert llm.model == MODEL_NAME171172173@pytest.mark.requires("anthropic")174def test_anthropic_model_param() -> None:175    llm = ChatAnthropic(model=MODEL_NAME)  # type: ignore[call-arg]176    assert llm.model == MODEL_NAME177178179@pytest.mark.requires("anthropic")180def test_anthropic_model_kwargs() -> None:181    llm = ChatAnthropic(model_name=MODEL_NAME, model_kwargs={"foo": "bar"})  # type: ignore[call-arg, call-arg]182    assert llm.model_kwargs == {"foo": "bar"}183184185@pytest.mark.requires("anthropic")186def test_anthropic_fields_in_model_kwargs() -> None:187    """Test that for backwards compatibility fields can be passed in as model_kwargs."""188    llm = ChatAnthropic(model=MODEL_NAME, model_kwargs={"max_tokens_to_sample": 5})  # type: ignore[call-arg]189    assert llm.max_tokens == 5190    llm = ChatAnthropic(model=MODEL_NAME, model_kwargs={"max_tokens": 5})  # type: ignore[call-arg]191    assert llm.max_tokens == 5192193194@pytest.mark.requires("anthropic")195def test_anthropic_incorrect_field() -> None:196    with pytest.warns(match="not default parameter"):197        llm = ChatAnthropic(model=MODEL_NAME, foo="bar")  # type: ignore[call-arg, call-arg]198    assert llm.model_kwargs == {"foo": "bar"}199200201@pytest.mark.requires("anthropic")202def test_anthropic_initialization() -> None:203    """Test anthropic initialization."""204    # Verify that chat anthropic can be initialized using a secret key provided205    # as a parameter rather than an environment variable.206    ChatAnthropic(model=MODEL_NAME, anthropic_api_key="test")  # type: ignore[call-arg, call-arg]207208209def test__format_output() -> None:210    anthropic_msg = Message(211        id="foo",212        content=[TextBlock(type="text", text="bar")],213        model="baz",214        role="assistant",215        stop_reason=None,216        stop_sequence=None,217        usage=Usage(input_tokens=2, output_tokens=1),218        type="message",219    )220    expected = AIMessage(  # type: ignore[misc]221        "bar",222        usage_metadata={223            "input_tokens": 2,224            "output_tokens": 1,225            "total_tokens": 3,226            "input_token_details": {},227        },228        response_metadata={"model_provider": "anthropic"},229    )230    llm = ChatAnthropic(model=MODEL_NAME, anthropic_api_key="test")  # type: ignore[call-arg, call-arg]231    actual = llm._format_output(anthropic_msg)232    assert actual.generations[0].message == expected233234235def test__format_output_cached() -> None:236    anthropic_msg = Message(237        id="foo",238        content=[TextBlock(type="text", text="bar")],239        model="baz",240        role="assistant",241        stop_reason=None,242        stop_sequence=None,243        usage=Usage(244            input_tokens=2,245            output_tokens=1,246            cache_creation_input_tokens=3,247            cache_read_input_tokens=4,248        ),249        type="message",250    )251    expected = AIMessage(  # type: ignore[misc]252        "bar",253        usage_metadata={254            "input_tokens": 9,255            "output_tokens": 1,256            "total_tokens": 10,257            "input_token_details": {"cache_creation": 3, "cache_read": 4},258        },259        response_metadata={"model_provider": "anthropic"},260    )261262    llm = ChatAnthropic(model=MODEL_NAME, anthropic_api_key="test")  # type: ignore[call-arg, call-arg]263    actual = llm._format_output(anthropic_msg)264    assert actual.generations[0].message == expected265266267def test__merge_messages() -> None:268    messages = [269        SystemMessage("foo"),  # type: ignore[misc]270        HumanMessage("bar"),  # type: ignore[misc]271        AIMessage(  # type: ignore[misc]272            [273                {"text": "baz", "type": "text"},274                {275                    "tool_input": {"a": "b"},276                    "type": "tool_use",277                    "id": "1",278                    "text": None,279                    "name": "buz",280                },281                {"text": "baz", "type": "text"},282                {283                    "tool_input": {"a": "c"},284                    "type": "tool_use",285                    "id": "2",286                    "text": None,287                    "name": "blah",288                },289                {290                    "tool_input": {"a": "c"},291                    "type": "tool_use",292                    "id": "3",293                    "text": None,294                    "name": "blah",295                },296            ],297        ),298        ToolMessage("buz output", tool_call_id="1", status="error"),  # type: ignore[misc]299        ToolMessage(300            content=[301                {302                    "type": "image",303                    "source": {304                        "type": "base64",305                        "media_type": "image/jpeg",306                        "data": "fake_image_data",307                    },308                },309            ],310            tool_call_id="2",311        ),  # type: ignore[misc]312        ToolMessage([], tool_call_id="3"),  # type: ignore[misc]313        HumanMessage("next thing"),  # type: ignore[misc]314    ]315    expected = [316        SystemMessage("foo"),  # type: ignore[misc]317        HumanMessage("bar"),  # type: ignore[misc]318        AIMessage(  # type: ignore[misc]319            [320                {"text": "baz", "type": "text"},321                {322                    "tool_input": {"a": "b"},323                    "type": "tool_use",324                    "id": "1",325                    "text": None,326                    "name": "buz",327                },328                {"text": "baz", "type": "text"},329                {330                    "tool_input": {"a": "c"},331                    "type": "tool_use",332                    "id": "2",333                    "text": None,334                    "name": "blah",335                },336                {337                    "tool_input": {"a": "c"},338                    "type": "tool_use",339                    "id": "3",340                    "text": None,341                    "name": "blah",342                },343            ],344        ),345        HumanMessage(  # type: ignore[misc]346            [347                {348                    "type": "tool_result",349                    "content": "buz output",350                    "tool_use_id": "1",351                    "is_error": True,352                },353                {354                    "type": "tool_result",355                    "content": [356                        {357                            "type": "image",358                            "source": {359                                "type": "base64",360                                "media_type": "image/jpeg",361                                "data": "fake_image_data",362                            },363                        },364                    ],365                    "tool_use_id": "2",366                    "is_error": False,367                },368                {369                    "type": "tool_result",370                    "content": [],371                    "tool_use_id": "3",372                    "is_error": False,373                },374                {"type": "text", "text": "next thing"},375            ],376        ),377    ]378    actual = _merge_messages(messages)379    assert expected == actual380381    # Test tool message case382    messages = [383        ToolMessage("buz output", tool_call_id="1"),  # type: ignore[misc]384        ToolMessage(  # type: ignore[misc]385            content=[386                {"type": "tool_result", "content": "blah output", "tool_use_id": "2"},387            ],388            tool_call_id="2",389        ),390    ]391    expected = [392        HumanMessage(  # type: ignore[misc]393            [394                {395                    "type": "tool_result",396                    "content": "buz output",397                    "tool_use_id": "1",398                    "is_error": False,399                },400                {"type": "tool_result", "content": "blah output", "tool_use_id": "2"},401            ],402        ),403    ]404    actual = _merge_messages(messages)405    assert expected == actual406407408def test__merge_messages_mutation() -> None:409    original_messages = [410        HumanMessage([{"type": "text", "text": "bar"}]),  # type: ignore[misc]411        HumanMessage("next thing"),  # type: ignore[misc]412    ]413    messages = [414        HumanMessage([{"type": "text", "text": "bar"}]),  # type: ignore[misc]415        HumanMessage("next thing"),  # type: ignore[misc]416    ]417    expected = [418        HumanMessage(  # type: ignore[misc]419            [{"type": "text", "text": "bar"}, {"type": "text", "text": "next thing"}],420        ),421    ]422    actual = _merge_messages(messages)423    assert expected == actual424    assert messages == original_messages425426427def test__merge_messages_tool_message_cache_control() -> None:428    """Test that cache_control is hoisted from content blocks to tool_result level."""429    # Test with cache_control in content block430    messages = [431        ToolMessage(432            content=[433                {434                    "type": "text",435                    "text": "tool output",436                    "cache_control": {"type": "ephemeral"},437                }438            ],439            tool_call_id="1",440        )441    ]442    original_messages = [copy.deepcopy(m) for m in messages]443    expected = [444        HumanMessage(445            [446                {447                    "type": "tool_result",448                    "content": [{"type": "text", "text": "tool output"}],449                    "tool_use_id": "1",450                    "is_error": False,451                    "cache_control": {"type": "ephemeral"},452                }453            ]454        )455    ]456    actual = _merge_messages(messages)457    assert expected == actual458    # Verify no mutation459    assert messages == original_messages460461    # Test with multiple content blocks, cache_control on last one462    messages = [463        ToolMessage(464            content=[465                {"type": "text", "text": "first output"},466                {467                    "type": "text",468                    "text": "second output",469                    "cache_control": {"type": "ephemeral"},470                },471            ],472            tool_call_id="2",473        )474    ]475    expected = [476        HumanMessage(477            [478                {479                    "type": "tool_result",480                    "content": [481                        {"type": "text", "text": "first output"},482                        {"type": "text", "text": "second output"},483                    ],484                    "tool_use_id": "2",485                    "is_error": False,486                    "cache_control": {"type": "ephemeral"},487                }488            ]489        )490    ]491    actual = _merge_messages(messages)492    assert expected == actual493494    # Test without cache_control495    messages = [ToolMessage(content="simple output", tool_call_id="3")]496    expected = [497        HumanMessage(498            [499                {500                    "type": "tool_result",501                    "content": "simple output",502                    "tool_use_id": "3",503                    "is_error": False,504                }505            ]506        )507    ]508    actual = _merge_messages(messages)509    assert expected == actual510511512def test__format_image() -> None:513    url = "dummyimage.com/600x400/000/fff"514    with pytest.raises(ValueError):515        _format_image(url)516517518@pytest.fixture519def pydantic() -> type[BaseModel]:520    class dummy_function(BaseModel):  # noqa: N801521        """Dummy function."""522523        arg1: int = Field(..., description="foo")524        arg2: Literal["bar", "baz"] = Field(..., description="one of 'bar', 'baz'")525526    return dummy_function527528529@pytest.fixture530def function() -> Callable:531    def dummy_function(arg1: int, arg2: Literal["bar", "baz"]) -> None:532        """Dummy function.533534        Args:535            arg1: foo536            arg2: one of 'bar', 'baz'537538        """539540    return dummy_function541542543@pytest.fixture544def dummy_tool() -> BaseTool:545    class Schema(BaseModel):546        arg1: int = Field(..., description="foo")547        arg2: Literal["bar", "baz"] = Field(..., description="one of 'bar', 'baz'")548549    class DummyFunction(BaseTool):  # type: ignore[override]550        args_schema: type[BaseModel] = Schema551        name: str = "dummy_function"552        description: str = "Dummy function."553554        def _run(self, *args: Any, **kwargs: Any) -> Any:555            pass556557    return DummyFunction()558559560@pytest.fixture561def json_schema() -> dict:562    return {563        "title": "dummy_function",564        "description": "Dummy function.",565        "type": "object",566        "properties": {567            "arg1": {"description": "foo", "type": "integer"},568            "arg2": {569                "description": "one of 'bar', 'baz'",570                "enum": ["bar", "baz"],571                "type": "string",572            },573        },574        "required": ["arg1", "arg2"],575    }576577578@pytest.fixture579def openai_function() -> dict:580    return {581        "name": "dummy_function",582        "description": "Dummy function.",583        "parameters": {584            "type": "object",585            "properties": {586                "arg1": {"description": "foo", "type": "integer"},587                "arg2": {588                    "description": "one of 'bar', 'baz'",589                    "enum": ["bar", "baz"],590                    "type": "string",591                },592            },593            "required": ["arg1", "arg2"],594        },595    }596597598def test_convert_to_anthropic_tool(599    pydantic: type[BaseModel],600    function: Callable,601    dummy_tool: BaseTool,602    json_schema: dict,603    openai_function: dict,604) -> None:605    expected = {606        "name": "dummy_function",607        "description": "Dummy function.",608        "input_schema": {609            "type": "object",610            "properties": {611                "arg1": {"description": "foo", "type": "integer"},612                "arg2": {613                    "description": "one of 'bar', 'baz'",614                    "enum": ["bar", "baz"],615                    "type": "string",616                },617            },618            "required": ["arg1", "arg2"],619        },620    }621622    for fn in (pydantic, function, dummy_tool, json_schema, expected, openai_function):623        actual = convert_to_anthropic_tool(fn)624        assert actual == expected625626627def test__format_messages_with_tool_calls() -> None:628    system = SystemMessage("fuzz")  # type: ignore[misc]629    human = HumanMessage("foo")  # type: ignore[misc]630    ai = AIMessage(631        "",  # with empty string632        tool_calls=[{"name": "bar", "id": "1", "args": {"baz": "buzz"}}],633    )634    ai2 = AIMessage(635        [],  # with empty list636        tool_calls=[{"name": "bar", "id": "2", "args": {"baz": "buzz"}}],637    )638    tool = ToolMessage(639        "blurb",640        tool_call_id="1",641    )642    tool_image_url = ToolMessage(643        [{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,...."}}],644        tool_call_id="2",645    )646    tool_image = ToolMessage(647        [648            {649                "type": "image",650                "source": {651                    "data": "....",652                    "type": "base64",653                    "media_type": "image/jpeg",654                },655            },656        ],657        tool_call_id="3",658    )659    messages = [system, human, ai, tool, ai2, tool_image_url, tool_image]660    expected = (661        "fuzz",662        [663            {"role": "user", "content": "foo"},664            {665                "role": "assistant",666                "content": [667                    {668                        "type": "tool_use",669                        "name": "bar",670                        "id": "1",671                        "input": {"baz": "buzz"},672                    },673                ],674            },675            {676                "role": "user",677                "content": [678                    {679                        "type": "tool_result",680                        "content": "blurb",681                        "tool_use_id": "1",682                        "is_error": False,683                    },684                ],685            },686            {687                "role": "assistant",688                "content": [689                    {690                        "type": "tool_use",691                        "name": "bar",692                        "id": "2",693                        "input": {"baz": "buzz"},694                    },695                ],696            },697            {698                "role": "user",699                "content": [700                    {701                        "type": "tool_result",702                        "content": [703                            {704                                "type": "image",705                                "source": {706                                    "data": "....",707                                    "type": "base64",708                                    "media_type": "image/jpeg",709                                },710                            },711                        ],712                        "tool_use_id": "2",713                        "is_error": False,714                    },715                    {716                        "type": "tool_result",717                        "content": [718                            {719                                "type": "image",720                                "source": {721                                    "data": "....",722                                    "type": "base64",723                                    "media_type": "image/jpeg",724                                },725                            },726                        ],727                        "tool_use_id": "3",728                        "is_error": False,729                    },730                ],731            },732        ],733    )734    actual = _format_messages(messages)735    assert expected == actual736737    # Check handling of empty AIMessage738    empty_contents: list[str | list[str | dict]] = ["", []]739    for empty_content in empty_contents:740        ## Permit message in final position741        _, anthropic_messages = _format_messages([human, AIMessage(empty_content)])742        expected_messages = [743            {"role": "user", "content": "foo"},744            {"role": "assistant", "content": empty_content},745        ]746        assert expected_messages == anthropic_messages747748        ## Remove message otherwise749        _, anthropic_messages = _format_messages(750            [human, AIMessage(empty_content), human]751        )752        expected_messages = [753            {"role": "user", "content": "foo"},754            {"role": "user", "content": "foo"},755        ]756        assert expected_messages == anthropic_messages757758        actual = _format_messages(759            [system, human, ai, tool, AIMessage(empty_content), human]760        )761        assert actual[0] == "fuzz"762        assert [message["role"] for message in actual[1]] == [763            "user",764            "assistant",765            "user",766            "user",767        ]768769770def test__format_tool_use_block() -> None:771    # Test we correctly format tool_use blocks when there is no corresponding tool_call.772    message = AIMessage(773        [774            {775                "type": "tool_use",776                "name": "foo_1",777                "id": "1",778                "input": {"bar_1": "baz_1"},779            },780            {781                "type": "tool_use",782                "name": "foo_2",783                "id": "2",784                "input": {},785                "partial_json": '{"bar_2": "baz_2"}',786                "index": 1,787            },788        ]789    )790    result = _format_messages([message])791    expected = {792        "role": "assistant",793        "content": [794            {795                "type": "tool_use",796                "name": "foo_1",797                "id": "1",798                "input": {"bar_1": "baz_1"},799            },800            {801                "type": "tool_use",802                "name": "foo_2",803                "id": "2",804                "input": {"bar_2": "baz_2"},805            },806        ],807    }808    assert result == (None, [expected])809810811def test__format_messages_with_str_content_and_tool_calls() -> None:812    system = SystemMessage("fuzz")  # type: ignore[misc]813    human = HumanMessage("foo")  # type: ignore[misc]814    # If content and tool_calls are specified and content is a string, then both are815    # included with content first.816    ai = AIMessage(  # type: ignore[misc]817        "thought",818        tool_calls=[{"name": "bar", "id": "1", "args": {"baz": "buzz"}}],819    )820    tool = ToolMessage("blurb", tool_call_id="1")  # type: ignore[misc]821    messages = [system, human, ai, tool]822    expected = (823        "fuzz",824        [825            {"role": "user", "content": "foo"},826            {827                "role": "assistant",828                "content": [829                    {"type": "text", "text": "thought"},830                    {831                        "type": "tool_use",832                        "name": "bar",833                        "id": "1",834                        "input": {"baz": "buzz"},835                    },836                ],837            },838            {839                "role": "user",840                "content": [841                    {842                        "type": "tool_result",843                        "content": "blurb",844                        "tool_use_id": "1",845                        "is_error": False,846                    },847                ],848            },849        ],850    )851    actual = _format_messages(messages)852    assert expected == actual853854855def test__format_messages_with_list_content_and_tool_calls() -> None:856    system = SystemMessage("fuzz")  # type: ignore[misc]857    human = HumanMessage("foo")  # type: ignore[misc]858    ai = AIMessage(  # type: ignore[misc]859        [{"type": "text", "text": "thought"}],860        tool_calls=[{"name": "bar", "id": "1", "args": {"baz": "buzz"}}],861    )862    tool = ToolMessage(  # type: ignore[misc]863        "blurb",864        tool_call_id="1",865    )866    messages = [system, human, ai, tool]867    expected = (868        "fuzz",869        [870            {"role": "user", "content": "foo"},871            {872                "role": "assistant",873                "content": [874                    {"type": "text", "text": "thought"},875                    {876                        "type": "tool_use",877                        "name": "bar",878                        "id": "1",879                        "input": {"baz": "buzz"},880                    },881                ],882            },883            {884                "role": "user",885                "content": [886                    {887                        "type": "tool_result",888                        "content": "blurb",889                        "tool_use_id": "1",890                        "is_error": False,891                    },892                ],893            },894        ],895    )896    actual = _format_messages(messages)897    assert expected == actual898899900def test__format_messages_with_tool_use_blocks_and_tool_calls() -> None:901    """Show that tool_calls are preferred to tool_use blocks when both have same id."""902    system = SystemMessage("fuzz")  # type: ignore[misc]903    human = HumanMessage("foo")  # type: ignore[misc]904    # NOTE: tool_use block in contents and tool_calls have different arguments.905    ai = AIMessage(  # type: ignore[misc]906        [907            {"type": "text", "text": "thought"},908            {909                "type": "tool_use",910                "name": "bar",911                "id": "1",912                "input": {"baz": "NOT_BUZZ"},913            },914        ],915        tool_calls=[{"name": "bar", "id": "1", "args": {"baz": "BUZZ"}}],916    )917    tool = ToolMessage("blurb", tool_call_id="1")  # type: ignore[misc]918    messages = [system, human, ai, tool]919    expected = (920        "fuzz",921        [922            {"role": "user", "content": "foo"},923            {924                "role": "assistant",925                "content": [926                    {"type": "text", "text": "thought"},927                    {928                        "type": "tool_use",929                        "name": "bar",930                        "id": "1",931                        "input": {"baz": "BUZZ"},  # tool_calls value preferred.932                    },933                ],934            },935            {936                "role": "user",937                "content": [938                    {939                        "type": "tool_result",940                        "content": "blurb",941                        "tool_use_id": "1",942                        "is_error": False,943                    },944                ],945            },946        ],947    )948    actual = _format_messages(messages)949    assert expected == actual950951952def test__format_messages_with_cache_control() -> None:953    messages = [954        SystemMessage(955            [956                {"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}},957            ],958        ),959        HumanMessage(960            [961                {"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}},962                {963                    "type": "text",964                    "text": "foo",965                },966            ],967        ),968    ]969    expected_system = [970        {"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}},971    ]972    expected_messages = [973        {974            "role": "user",975            "content": [976                {"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}},977                {"type": "text", "text": "foo"},978            ],979        },980    ]981    actual_system, actual_messages = _format_messages(messages)982    assert expected_system == actual_system983    assert expected_messages == actual_messages984985    # Test standard multi-modal format (v0)986    messages = [987        HumanMessage(988            [989                {990                    "type": "text",991                    "text": "Summarize this document:",992                },993                {994                    "type": "file",995                    "source_type": "base64",996                    "mime_type": "application/pdf",997                    "data": "<base64 data>",998                    "cache_control": {"type": "ephemeral"},999                },1000            ],1001        ),1002    ]1003    actual_system, actual_messages = _format_messages(messages)1004    assert actual_system is None1005    expected_messages = [1006        {1007            "role": "user",1008            "content": [1009                {1010                    "type": "text",1011                    "text": "Summarize this document:",1012                },1013                {1014                    "type": "document",1015                    "source": {1016                        "type": "base64",1017                        "media_type": "application/pdf",1018                        "data": "<base64 data>",1019                    },1020                    "cache_control": {"type": "ephemeral"},1021                },1022            ],1023        },1024    ]1025    assert actual_messages == expected_messages10261027    # Test standard multi-modal format (v1)1028    messages = [1029        HumanMessage(1030            [1031                {1032                    "type": "text",1033                    "text": "Summarize this document:",1034                },1035                {1036                    "type": "file",1037                    "mime_type": "application/pdf",1038                    "base64": "<base64 data>",1039                    "extras": {"cache_control": {"type": "ephemeral"}},1040                },1041            ],1042        ),1043    ]1044    actual_system, actual_messages = _format_messages(messages)1045    assert actual_system is None1046    expected_messages = [1047        {1048            "role": "user",1049            "content": [1050                {1051                    "type": "text",1052                    "text": "Summarize this document:",1053                },1054                {1055                    "type": "document",1056                    "source": {1057                        "type": "base64",1058                        "media_type": "application/pdf",1059                        "data": "<base64 data>",1060                    },1061                    "cache_control": {"type": "ephemeral"},1062                },1063            ],1064        },1065    ]1066    assert actual_messages == expected_messages10671068    # Test standard multi-modal format (v1, unpacked extras)1069    messages = [1070        HumanMessage(1071            [1072                {1073                    "type": "text",1074                    "text": "Summarize this document:",1075                },1076                {1077                    "type": "file",1078                    "mime_type": "application/pdf",1079                    "base64": "<base64 data>",1080                    "cache_control": {"type": "ephemeral"},1081                },1082            ],1083        ),1084    ]1085    actual_system, actual_messages = _format_messages(messages)1086    assert actual_system is None1087    expected_messages = [1088        {1089            "role": "user",1090            "content": [1091                {1092                    "type": "text",1093                    "text": "Summarize this document:",1094                },1095                {1096                    "type": "document",1097                    "source": {1098                        "type": "base64",1099                        "media_type": "application/pdf",1100                        "data": "<base64 data>",1101                    },1102                    "cache_control": {"type": "ephemeral"},1103                },1104            ],1105        },1106    ]1107    assert actual_messages == expected_messages11081109    # Also test file inputs1110    ## Images1111    for block in [1112        # v11113        {1114            "type": "image",1115            "file_id": "abc123",1116        },1117        # v01118        {1119            "type": "image",1120            "source_type": "id",1121            "id": "abc123",1122        },1123    ]:1124        messages = [1125            HumanMessage(1126                [1127                    {1128                        "type": "text",1129                        "text": "Summarize this image:",1130                    },1131                    block,1132                ],1133            ),1134        ]1135        actual_system, actual_messages = _format_messages(messages)1136        assert actual_system is None1137        expected_messages = [1138            {1139                "role": "user",1140                "content": [1141                    {1142                        "type": "text",1143                        "text": "Summarize this image:",1144                    },1145                    {1146                        "type": "image",1147                        "source": {1148                            "type": "file",1149                            "file_id": "abc123",1150                        },1151                    },1152                ],1153            },1154        ]1155        assert actual_messages == expected_messages11561157    ## Documents1158    for block in [1159        # v11160        {1161            "type": "file",1162            "file_id": "abc123",1163        },1164        # v01165        {1166            "type": "file",1167            "source_type": "id",1168            "id": "abc123",1169        },1170    ]:1171        messages = [1172            HumanMessage(1173                [1174                    {1175                        "type": "text",1176                        "text": "Summarize this document:",1177                    },1178                    block,1179                ],1180            ),1181        ]1182        actual_system, actual_messages = _format_messages(messages)1183        assert actual_system is None1184        expected_messages = [1185            {1186                "role": "user",1187                "content": [1188                    {1189                        "type": "text",1190                        "text": "Summarize this document:",1191                    },1192                    {1193                        "type": "document",1194                        "source": {1195                            "type": "file",1196                            "file_id": "abc123",1197                        },1198                    },1199                ],1200            },1201        ]1202        assert actual_messages == expected_messages120312041205def test__format_messages_with_citations() -> None:1206    input_messages = [1207        HumanMessage(1208            content=[1209                {1210                    "type": "file",1211                    "source_type": "text",1212                    "text": "The grass is green. The sky is blue.",1213                    "mime_type": "text/plain",1214                    "citations": {"enabled": True},1215                },1216                {"type": "text", "text": "What color is the grass and sky?"},1217            ],1218        ),1219    ]1220    expected_messages = [1221        {1222            "role": "user",1223            "content": [1224                {1225                    "type": "document",1226                    "source": {1227                        "type": "text",1228                        "media_type": "text/plain",1229                        "data": "The grass is green. The sky is blue.",1230                    },1231                    "citations": {"enabled": True},1232                },1233                {"type": "text", "text": "What color is the grass and sky?"},1234            ],1235        },1236    ]1237    actual_system, actual_messages = _format_messages(input_messages)1238    assert actual_system is None1239    assert actual_messages == expected_messages124012411242def test__format_messages_openai_image_format() -> None:1243    message = HumanMessage(1244        content=[1245            {1246                "type": "text",1247                "text": "Can you highlight the differences between these two images?",1248            },1249            {1250                "type": "image_url",1251                "image_url": {"url": "data:image/jpeg;base64,<base64 data>"},1252            },1253            {1254                "type": "image_url",1255                "image_url": {"url": "https://<image url>"},1256            },1257        ],1258    )1259    actual_system, actual_messages = _format_messages([message])1260    assert actual_system is None1261    expected_messages = [1262        {1263            "role": "user",1264            "content": [1265                {1266                    "type": "text",1267                    "text": (1268                        "Can you highlight the differences between these two images?"1269                    ),1270                },1271                {1272                    "type": "image",1273                    "source": {1274                        "type": "base64",1275                        "media_type": "image/jpeg",1276                        "data": "<base64 data>",1277                    },1278                },1279                {1280                    "type": "image",1281                    "source": {1282                        "type": "url",1283                        "url": "https://<image url>",1284                    },1285                },1286            ],1287        },1288    ]1289    assert actual_messages == expected_messages129012911292def test__format_messages_with_multiple_system() -> None:1293    messages = [1294        HumanMessage("baz"),1295        SystemMessage("bar"),1296        SystemMessage("baz"),1297        SystemMessage(1298            [1299                {"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}},1300            ],1301        ),1302    ]1303    expected_system = [1304        {"type": "text", "text": "bar"},1305        {"type": "text", "text": "baz"},1306        {"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}},1307    ]1308    expected_messages = [{"role": "user", "content": "baz"}]1309    actual_system, actual_messages = _format_messages(messages)1310    assert expected_system == actual_system1311    assert expected_messages == actual_messages131213131314def test_anthropic_api_key_is_secret_string() -> None:1315    """Test that the API key is stored as a SecretStr."""1316    chat_model = ChatAnthropic(  # type: ignore[call-arg, call-arg]1317        model=MODEL_NAME,1318        anthropic_api_key="secret-api-key",1319    )1320    assert isinstance(chat_model.anthropic_api_key, SecretStr)132113221323def test_anthropic_api_key_masked_when_passed_from_env(1324    monkeypatch: MonkeyPatch,1325    capsys: CaptureFixture,1326) -> None:1327    """Test that the API key is masked when passed from an environment variable."""1328    monkeypatch.setenv("ANTHROPIC_API_KEY ", "secret-api-key")1329    chat_model = ChatAnthropic(  # type: ignore[call-arg]1330        model=MODEL_NAME,1331    )1332    print(chat_model.anthropic_api_key, end="")  # noqa: T2011333    captured = capsys.readouterr()13341335    assert captured.out == "**********"133613371338def test_anthropic_api_key_masked_when_passed_via_constructor(1339    capsys: CaptureFixture,1340) -> None:1341    """Test that the API key is masked when passed via the constructor."""1342    chat_model = ChatAnthropic(  # type: ignore[call-arg, call-arg]1343        model=MODEL_NAME,1344        anthropic_api_key="secret-api-key",1345    )1346    print(chat_model.anthropic_api_key, end="")  # noqa: T2011347    captured = capsys.readouterr()13481349    assert captured.out == "**********"135013511352def test_anthropic_uses_actual_secret_value_from_secretstr() -> None:1353    """Test that the actual secret value is correctly retrieved."""1354    chat_model = ChatAnthropic(  # type: ignore[call-arg, call-arg]1355        model=MODEL_NAME,1356        anthropic_api_key="secret-api-key",1357    )1358    assert (1359        cast("SecretStr", chat_model.anthropic_api_key).get_secret_value()1360        == "secret-api-key"1361    )136213631364class GetWeather(BaseModel):1365    """Get the current weather in a given location."""13661367    location: str = Field(..., description="The city and state, e.g. San Francisco, CA")136813691370def test_anthropic_bind_tools_tool_choice() -> None:1371    chat_model = ChatAnthropic(  # type: ignore[call-arg, call-arg]1372        model=MODEL_NAME,1373        anthropic_api_key="secret-api-key",1374    )1375    chat_model_with_tools = chat_model.bind_tools(1376        [GetWeather],1377        tool_choice={"type": "tool", "name": "GetWeather"},1378    )1379    assert cast("RunnableBinding", chat_model_with_tools).kwargs["tool_choice"] == {1380        "type": "tool",1381        "name": "GetWeather",1382    }1383    chat_model_with_tools = chat_model.bind_tools(1384        [GetWeather],1385        tool_choice="GetWeather",1386    )1387    assert cast("RunnableBinding", chat_model_with_tools).kwargs["tool_choice"] == {1388        "type": "tool",1389        "name": "GetWeather",1390    }1391    chat_model_with_tools = chat_model.bind_tools([GetWeather], tool_choice="auto")1392    assert cast("RunnableBinding", chat_model_with_tools).kwargs["tool_choice"] == {1393        "type": "auto",1394    }1395    chat_model_with_tools = chat_model.bind_tools([GetWeather], tool_choice="any")1396    assert cast("RunnableBinding", chat_model_with_tools).kwargs["tool_choice"] == {1397        "type": "any",1398    }139914001401def test_fine_grained_tool_streaming_beta() -> None:1402    """Test that fine-grained tool streaming beta can be enabled."""1403    # Test with betas parameter at initialization1404    model = ChatAnthropic(1405        model=MODEL_NAME, betas=["fine-grained-tool-streaming-2025-05-14"]1406    )14071408    # Create a simple tool1409    def get_weather(city: str) -> str:1410        """Get the weather for a city."""1411        return f"Weather in {city}"14121413    model_with_tools = model.bind_tools([get_weather])1414    payload = model_with_tools._get_request_payload(  # type: ignore[attr-defined]1415        "What's the weather in SF?",1416        stream=True,1417        **model_with_tools.kwargs,  # type: ignore[attr-defined]1418    )14191420    # Verify beta header is in payload1421    assert "fine-grained-tool-streaming-2025-05-14" in payload["betas"]1422    assert payload["stream"] is True14231424    # Test combining with other betas1425    model = ChatAnthropic(1426        model=MODEL_NAME,1427        betas=["context-1m-2025-08-07", "fine-grained-tool-streaming-2025-05-14"],1428    )1429    model_with_tools = model.bind_tools([get_weather])1430    payload = model_with_tools._get_request_payload(  # type: ignore[attr-defined]1431        "What's the weather?",1432        stream=True,1433        **model_with_tools.kwargs,  # type: ignore[attr-defined]1434    )1435    assert set(payload["betas"]) == {1436        "context-1m-2025-08-07",1437        "fine-grained-tool-streaming-2025-05-14",1438    }14391440    # Test that _create routes to beta client when betas are present1441    model = ChatAnthropic(1442        model=MODEL_NAME, betas=["fine-grained-tool-streaming-2025-05-14"]1443    )1444    payload = {"betas": ["fine-grained-tool-streaming-2025-05-14"], "stream": True}14451446    with patch.object(model._client.beta.messages, "create") as mock_beta_create:1447        model._create(payload)1448        mock_beta_create.assert_called_once_with(**payload)144914501451def test_optional_description() -> None:1452    llm = ChatAnthropic(model=MODEL_NAME)14531454    class SampleModel(BaseModel):1455        sample_field: str14561457    _ = llm.with_structured_output(SampleModel.model_json_schema())145814591460def test_get_num_tokens_from_messages_passes_kwargs() -> None:1461    """Test that get_num_tokens_from_messages passes kwargs to the model."""1462    llm = ChatAnthropic(model=MODEL_NAME)14631464    with patch.object(anthropic, "Client") as _client:1465        llm.get_num_tokens_from_messages([HumanMessage("foo")], foo="bar")14661467    assert _client.return_value.messages.count_tokens.call_args.kwargs["foo"] == "bar"14681469    llm = ChatAnthropic(1470        model=MODEL_NAME,1471        betas=["context-management-2025-06-27"],1472        context_management={"edits": [{"type": "clear_tool_uses_20250919"}]},1473    )1474    with patch.object(anthropic, "Client") as _client:1475        llm.get_num_tokens_from_messages([HumanMessage("foo")])14761477    call_args = _client.return_value.beta.messages.count_tokens.call_args.kwargs1478    assert call_args["betas"] == ["context-management-2025-06-27"]1479    assert call_args["context_management"] == {1480        "edits": [{"type": "clear_tool_uses_20250919"}]1481    }148214831484def test_usage_metadata_standardization() -> None:1485    class UsageModel(BaseModel):1486        input_tokens: int = 101487        output_tokens: int = 51488        cache_read_input_tokens: int = 31489        cache_creation_input_tokens: int = 214901491    # Happy path1492    usage = UsageModel()1493    result = _create_usage_metadata(usage)1494    assert result["input_tokens"] == 15  # 10 + 3 + 21495    assert result["output_tokens"] == 51496    assert result["total_tokens"] == 201497    assert result.get("input_token_details") == {"cache_read": 3, "cache_creation": 2}14981499    # Null input and output tokens1500    class UsageModelNulls(BaseModel):1501        input_tokens: int | None = None1502        output_tokens: int | None = None1503        cache_read_input_tokens: int | None = None1504        cache_creation_input_tokens: int | None = None15051506    usage_nulls = UsageModelNulls()1507    result = _create_usage_metadata(usage_nulls)1508    assert result["input_tokens"] == 01509    assert result["output_tokens"] == 01510    assert result["total_tokens"] == 015111512    # Test missing fields1513    class UsageModelMissing(BaseModel):1514        pass15151516    usage_missing = UsageModelMissing()1517    result = _create_usage_metadata(usage_missing)1518    assert result["input_tokens"] == 01519    assert result["output_tokens"] == 01520    assert result["total_tokens"] == 0152115221523def test_usage_metadata_cache_creation_ttl() -> None:1524    """Test _create_usage_metadata with granular cache_creation TTL fields."""15251526    # Case 1: cache_creation with specific ephemeral TTL tokens (BaseModel)1527    class CacheCreation(BaseModel):1528        ephemeral_5m_input_tokens: int = 1001529        ephemeral_1h_input_tokens: int = 5015301531    class UsageWithCacheCreation(BaseModel):1532        input_tokens: int = 2001533        output_tokens: int = 301534        cache_read_input_tokens: int = 101535        cache_creation_input_tokens: int = 1501536        cache_creation: CacheCreation = CacheCreation()15371538    result = _create_usage_metadata(UsageWithCacheCreation())1539    # input_tokens = 200 (base) + 10 (cache_read) + 150 (specific: 100+50)1540    assert result["input_tokens"] == 3601541    assert result["output_tokens"] == 301542    assert result["total_tokens"] == 3901543    details = dict(result.get("input_token_details") or {})1544    assert details["cache_read"] == 101545    # cache_creation should be suppressed to avoid double counting1546    assert details["cache_creation"] == 01547    assert details["ephemeral_5m_input_tokens"] == 1001548    assert details["ephemeral_1h_input_tokens"] == 5015491550    # Case 2: cache_creation as a dict1551    class UsageWithCacheCreationDict(BaseModel):1552        input_tokens: int = 2001553        output_tokens: int = 301554        cache_read_input_tokens: int = 101555        cache_creation_input_tokens: int = 1501556        cache_creation: dict = {1557            "ephemeral_5m_input_tokens": 80,1558            "ephemeral_1h_input_tokens": 70,1559        }15601561    result = _create_usage_metadata(UsageWithCacheCreationDict())1562    assert result["input_tokens"] == 200 + 10 + 80 + 701563    details = dict(result.get("input_token_details") or {})1564    assert details["cache_creation"] == 01565    assert details["ephemeral_5m_input_tokens"] == 801566    assert details["ephemeral_1h_input_tokens"] == 7015671568    # Case 3: cache_creation exists but specific keys are zero  falls back to1569    # generic cache_creation_input_tokens1570    class CacheCreationZero(BaseModel):1571        ephemeral_5m_input_tokens: int = 01572        ephemeral_1h_input_tokens: int = 015731574    class UsageWithCacheCreationZero(BaseModel):1575        input_tokens: int = 2001576        output_tokens: int = 301577        cache_read_input_tokens: int = 101578        cache_creation_input_tokens: int = 501579        cache_creation: CacheCreationZero = CacheCreationZero()15801581    result = _create_usage_metadata(UsageWithCacheCreationZero())1582    # specific_cache_creation_tokens = 0, so falls back to cache_creation_input_tokens1583    # input_tokens = 200 + 10 + 50 = 2601584    assert result["input_tokens"] == 2601585    assert result["output_tokens"] == 301586    assert result["total_tokens"] == 2901587    details = dict(result.get("input_token_details") or {})1588    assert details["cache_read"] == 101589    assert details["cache_creation"] == 5015901591    # Case 4: cache_creation exists but specific keys are missing from the dict1592    class CacheCreationEmpty(BaseModel):1593        pass15941595    class UsageWithCacheCreationEmpty(BaseModel):1596        input_tokens: int = 1001597        output_tokens: int = 201598        cache_read_input_tokens: int = 51599        cache_creation_input_tokens: int = 151600        cache_creation: CacheCreationEmpty = CacheCreationEmpty()16011602    result = _create_usage_metadata(UsageWithCacheCreationEmpty())1603    # specific_cache_creation_tokens = 0, falls back to cache_creation_input_tokens1604    assert result["input_tokens"] == 100 + 5 + 151605    assert result["output_tokens"] == 201606    assert result["total_tokens"] == 1401607    details = dict(result.get("input_token_details") or {})1608    assert details["cache_creation"] == 1516091610    # Case 5: only one ephemeral key is non-zero1611    class CacheCreationPartial(BaseModel):1612        ephemeral_5m_input_tokens: int = 01613        ephemeral_1h_input_tokens: int = 7516141615    class UsageWithPartialCache(BaseModel):1616        input_tokens: int = 1001617        output_tokens: int = 101618        cache_read_input_tokens: int = 01619        cache_creation_input_tokens: int = 751620        cache_creation: CacheCreationPartial = CacheCreationPartial()16211622    result = _create_usage_metadata(UsageWithPartialCache())1623    # specific_cache_creation_tokens = 75 > 0, so generic cache_creation is suppressed1624    assert result["input_tokens"] == 100 + 0 + 751625    assert result["output_tokens"] == 101626    assert result["total_tokens"] == 1851627    details = dict(result.get("input_token_details") or {})1628    assert details["cache_creation"] == 01629    assert details["ephemeral_1h_input_tokens"] == 751630    # ephemeral_5m_input_tokens is 0  still included since 0 is not None1631    assert details["ephemeral_5m_input_tokens"] == 016321633    # Case 6: no cache_creation field at all (the pre-existing path)1634    class UsageNoCacheCreation(BaseModel):1635        input_tokens: int = 501636        output_tokens: int = 251637        cache_read_input_tokens: int = 51638        cache_creation_input_tokens: int = 1016391640    result = _create_usage_metadata(UsageNoCacheCreation())1641    assert result["input_tokens"] == 50 + 5 + 101642    assert result["output_tokens"] == 251643    assert result["total_tokens"] == 901644    details = dict(result.get("input_token_details") or {})1645    assert details["cache_read"] == 51646    assert details["cache_creation"] == 10164716481649class FakeTracer(BaseTracer):1650    """Fake tracer to capture inputs to `chat_model_start`."""16511652    def __init__(self) -> None:1653        super().__init__()1654        self.chat_model_start_inputs: list = []16551656    def _persist_run(self, run: Run) -> None:1657        """Persist a run."""16581659    def on_chat_model_start(self, *args: Any, **kwargs: Any) -> Run:1660        self.chat_model_start_inputs.append({"args": args, "kwargs": kwargs})1661        return super().on_chat_model_start(*args, **kwargs)166216631664def test_mcp_tracing() -> None:1665    # Test we exclude sensitive information from traces1666    mcp_servers = [1667        {1668            "type": "url",1669            "url": "https://mcp.deepwiki.com/mcp",1670            "name": "deepwiki",1671            "authorization_token": "PLACEHOLDER",1672        },1673    ]16741675    llm = ChatAnthropic(1676        model=MODEL_NAME,1677        betas=["mcp-client-2025-04-04"],1678        mcp_servers=mcp_servers,1679    )16801681    tracer = FakeTracer()1682    mock_client = MagicMock()16831684    def mock_create(*args: Any, **kwargs: Any) -> Message:1685        return Message(1686            id="foo",1687            content=[TextBlock(type="text", text="bar")],1688            model="baz",1689            role="assistant",1690            stop_reason=None,1691            stop_sequence=None,1692            usage=Usage(input_tokens=2, output_tokens=1),1693            type="message",1694        )16951696    mock_client.messages.create = mock_create1697    input_message = HumanMessage("Test query")1698    with patch.object(llm, "_client", mock_client):1699        _ = llm.invoke([input_message], config={"callbacks": [tracer]})17001701    # Test headers are not traced1702    assert len(tracer.chat_model_start_inputs) == 11703    assert "PLACEHOLDER" not in str(tracer.chat_model_start_inputs)17041705    # Test headers are correctly propagated to request1706    payload = llm._get_request_payload([input_message])1707    assert payload["mcp_servers"][0]["authorization_token"] == "PLACEHOLDER"  # noqa: S105170817091710def test_cache_control_kwarg() -> None:1711    llm = ChatAnthropic(model=MODEL_NAME)17121713    messages = [HumanMessage("foo"), AIMessage("bar"), HumanMessage("baz")]1714    payload = llm._get_request_payload(messages)1715    assert "cache_control" not in payload17161717    payload = llm._get_request_payload(messages, cache_control={"type": "ephemeral"})1718    assert payload["cache_control"] == {"type": "ephemeral"}1719    assert payload["messages"] == [1720        {"role": "user", "content": "foo"},1721        {"role": "assistant", "content": "bar"},1722        {"role": "user", "content": "baz"},1723    ]172417251726class _BedrockLikeAnthropic(ChatAnthropic):1727    """Stand-in for `ChatAnthropicBedrock` for `_llm_type`-based gating tests.17281729    Vertex is not modeled here: `langchain-google-vertexai`'s1730    `ChatAnthropicVertex` does not subclass `ChatAnthropic` and ships its own1731    `_get_request_payload`, so it never reaches the gate under test.1732    """17331734    @property1735    def _llm_type(self) -> str:1736        return "anthropic-bedrock-chat"173717381739def test_cache_control_kwarg_bedrock_injects_into_blocks() -> None:1740    """Non-direct subclasses must place `cache_control` inside the last block.17411742    Transports like Bedrock reject the top-level `cache_control` field, so1743    the kwarg has to be expanded into a nested breakpoint to remain effective.1744    """1745    llm = _BedrockLikeAnthropic(model=MODEL_NAME)17461747    messages = [HumanMessage("foo"), AIMessage("bar"), HumanMessage("baz")]1748    payload = llm._get_request_payload(messages, cache_control={"type": "ephemeral"})17491750    assert "cache_control" not in payload1751    last_message = payload["messages"][-1]1752    assert last_message["content"] == [1753        {"type": "text", "text": "baz", "cache_control": {"type": "ephemeral"}}1754    ]175517561757def test_cache_control_kwarg_bedrock_with_list_content() -> None:1758    """`cache_control` lands on the last block when content is already a list."""1759    llm = _BedrockLikeAnthropic(model=MODEL_NAME)17601761    messages = [HumanMessage([{"type": "text", "text": "foo"}])]1762    payload = llm._get_request_payload(1763        messages, cache_control={"type": "ephemeral", "ttl": "1h"}1764    )17651766    assert "cache_control" not in payload1767    last_block = payload["messages"][-1]["content"][-1]1768    assert last_block["cache_control"] == {"type": "ephemeral", "ttl": "1h"}176917701771def test_cache_control_kwarg_bedrock_skips_code_execution_blocks() -> None:1772    """`cache_control` must skip `code_execution`-related blocks.17731774    Anthropic rejects breakpoints applied to those blocks, so the injector1775    walks backwards until it finds an eligible block.1776    """1777    llm = _BedrockLikeAnthropic(model=MODEL_NAME)17781779    ai_message = AIMessage(1780        content=[1781            {"type": "text", "text": "earlier text"},1782            {1783                "type": "tool_use",1784                "id": "toolu_code_exec_1",1785                "name": "get_weather",1786                "input": {"location": "NYC"},1787                "caller": {1788                    "type": "code_execution_20250825",1789                    "tool_id": "srvtoolu_abc",1790                },1791            },1792        ]1793    )17941795    payload = llm._get_request_payload(1796        [HumanMessage("hi"), ai_message],1797        cache_control={"type": "ephemeral"},1798    )17991800    last_content = payload["messages"][-1]["content"]1801    assert last_content[0]["cache_control"] == {"type": "ephemeral"}1802    assert "cache_control" not in last_content[1]180318041805def test_cache_control_kwarg_bedrock_walks_back_to_earlier_message() -> None:1806    """When the last message has no eligible blocks, walk back to a prior one.18071808    Pins the contract that `reversed(formatted_messages)` is intentional: a1809    refactor that only inspects the last message would silently regress.1810    """1811    llm = _BedrockLikeAnthropic(model=MODEL_NAME)18121813    ai_message = AIMessage(1814        content=[1815            {1816                "type": "tool_use",1817                "id": "toolu_code_exec_1",1818                "name": "noop",1819                "input": {},1820                "caller": {1821                    "type": "code_execution_20250825",1822                    "tool_id": "srvtoolu_abc",1823                },1824            }1825        ]1826    )18271828    payload = llm._get_request_payload(1829        [HumanMessage("earlier"), ai_message],1830        cache_control={"type": "ephemeral"},1831    )18321833    first_message_content = payload["messages"][0]["content"]1834    assert first_message_content == [1835        {"type": "text", "text": "earlier", "cache_control": {"type": "ephemeral"}}1836    ]1837    last_message_content = payload["messages"][-1]["content"]1838    assert all("cache_control" not in block for block in last_message_content)183918401841def test_cache_control_kwarg_bedrock_no_eligible_block_warns() -> None:1842    """When every candidate is `code_execution`-related, warn and drop the kwarg.18431844    Pins the silent-drop contract: payload remains valid for Anthropic, but1845    the caller is told their cache request was skipped.1846    """1847    llm = _BedrockLikeAnthropic(model=MODEL_NAME)18481849    ai_message = AIMessage(1850        content=[1851            {1852                "type": "tool_use",1853                "id": "toolu_code_exec_1",1854                "name": "noop",1855                "input": {},1856                "caller": {1857                    "type": "code_execution_20250825",1858                    "tool_id": "srvtoolu_abc",1859                },1860            }1861        ]1862    )18631864    with pytest.warns(UserWarning, match="cache_control.*dropped"):1865        payload = llm._get_request_payload(1866            [ai_message],1867            cache_control={"type": "ephemeral"},1868        )18691870    assert "cache_control" not in payload1871    only_block = payload["messages"][-1]["content"][0]1872    assert "cache_control" not in only_block187318741875def test_cache_control_absent_kwarg_bedrock_is_noop() -> None:1876    """Without a `cache_control` kwarg, the Bedrock branch must not mutate."""1877    llm = _BedrockLikeAnthropic(model=MODEL_NAME)18781879    messages = [HumanMessage("foo"), AIMessage("bar"), HumanMessage("baz")]1880    payload = llm._get_request_payload(messages)18811882    assert "cache_control" not in payload1883    for message in payload["messages"]:1884        content = message["content"]1885        if isinstance(content, list):1886            for block in content:1887                assert "cache_control" not in block188818891890def test_cache_control_kwarg_unknown_subclass_injects_into_blocks() -> None:1891    """Any subclass that overrides `_llm_type` is treated as non-direct.18921893    The gate is allowlist-shaped on `"anthropic-chat"`, so a future subclass1894    routing through a new transport is safe by default rather than silently1895    sending an unsupported top-level field.1896    """18971898    class _FutureTransportAnthropic(ChatAnthropic):1899        @property1900        def _llm_type(self) -> str:1901            return "anthropic-some-future-transport"19021903    llm = _FutureTransportAnthropic(model=MODEL_NAME)1904    payload = llm._get_request_payload(1905        [HumanMessage("hello")],1906        cache_control={"type": "ephemeral"},1907    )19081909    assert "cache_control" not in payload1910    assert payload["messages"][-1]["content"] == [1911        {"type": "text", "text": "hello", "cache_control": {"type": "ephemeral"}}1912    ]191319141915@pytest.mark.parametrize(1916    ("llm_type", "expected"),1917    [1918        ("anthropic-chat", True),1919        ("anthropic-bedrock-chat", False),1920        ("anthropic-chat-vertexai", False),1921        ("", False),1922        ("ANTHROPIC-CHAT", False),1923        (None, False),1924        (object(), False),1925    ],1926)1927def test_is_direct_anthropic_llm_type(llm_type: object, expected: bool) -> None:  # noqa: FBT0011928    """Predicate is exact-match and tolerates non-string inputs."""1929    from langchain_anthropic.chat_models import _is_direct_anthropic_llm_type19301931    assert _is_direct_anthropic_llm_type(llm_type) is expected193219331934def test_context_management_in_payload() -> None:1935    llm = ChatAnthropic(1936        model=MODEL_NAME,  # type: ignore[call-arg]1937        betas=["context-management-2025-06-27"],1938        context_management={"edits": [{"type": "clear_tool_uses_20250919"}]},1939    )1940    llm_with_tools = llm.bind_tools(1941        [{"type": "web_search_20250305", "name": "web_search"}]1942    )1943    input_message = HumanMessage("Search for recent developments in AI")1944    payload = llm_with_tools._get_request_payload([input_message])  # type: ignore[attr-defined]1945    assert payload["context_management"] == {1946        "edits": [{"type": "clear_tool_uses_20250919"}]1947    }194819491950def test_inference_geo_in_payload() -> None:1951    llm = ChatAnthropic(model=MODEL_NAME, inference_geo="us")1952    input_message = HumanMessage("Hello, world!")1953    payload = llm._get_request_payload([input_message])1954    assert payload["inference_geo"] == "us"195519561957def test_anthropic_model_params() -> None:1958    llm = ChatAnthropic(model=MODEL_NAME)19591960    ls_params = llm._get_ls_params()1961    assert ls_params == {1962        "ls_provider": "anthropic",1963        "ls_model_type": "chat",1964        "ls_model_name": MODEL_NAME,1965        "ls_max_tokens": 64000,1966        "ls_temperature": None,1967    }19681969    ls_params = llm._get_ls_params(model=MODEL_NAME)1970    assert ls_params.get("ls_model_name") == MODEL_NAME197119721973def test_streaming_cache_token_reporting() -> None:1974    """Test that cache tokens are properly reported in streaming events."""1975    from unittest.mock import MagicMock19761977    from anthropic.types import MessageDeltaUsage19781979    # Create a mock message_start event1980    mock_message = MagicMock()1981    mock_message.model = MODEL_NAME1982    mock_message.usage.input_tokens = 1001983    mock_message.usage.output_tokens = 01984    mock_message.usage.cache_read_input_tokens = 251985    mock_message.usage.cache_creation_input_tokens = 1019861987    message_start_event = MagicMock()1988    message_start_event.type = "message_start"1989    message_start_event.message = mock_message19901991    # Create a mock message_delta event with complete usage info1992    mock_delta_usage = MessageDeltaUsage(1993        output_tokens=50,1994        input_tokens=100,1995        cache_read_input_tokens=25,1996        cache_creation_input_tokens=10,1997    )19981999    mock_delta = MagicMock()2000    mock_delta.stop_reason = "end_turn"

Findings

✓ No findings reported for this file.

Get this view in your editor

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