libs/partners/anthropic/tests/integration_tests/test_chat_models.py PYTHON 2,838 lines View on github.com → Search inside
File is large — showing lines 1–2,000 of 2,838.
1"""Test ChatAnthropic chat model."""23from __future__ import annotations45import asyncio6import json7import os8from base64 import b64encode9from typing import TYPE_CHECKING, Any, Literal, cast1011import anthropic12import httpx13import pytest14import requests15from langchain.agents import create_agent16from langchain.agents.structured_output import ProviderStrategy17from langchain_core.callbacks import CallbackManager18from langchain_core.exceptions import OutputParserException19from langchain_core.messages import (20    AIMessage,21    AIMessageChunk,22    BaseMessage,23    BaseMessageChunk,24    HumanMessage,25    SystemMessage,26    ToolMessage,27)28from langchain_core.outputs import ChatGeneration, LLMResult29from langchain_core.prompts import ChatPromptTemplate30from langchain_core.tools import tool3132if TYPE_CHECKING:33    from collections.abc import Awaitable3435    from langchain_core.language_models.chat_model_stream import (36        AsyncChatModelStream,37        ChatModelStream,38    )39from langchain_tests.utils.stream_lifecycle import assert_valid_event_stream40from pydantic import BaseModel, Field41from typing_extensions import TypedDict4243from langchain_anthropic import ChatAnthropic44from langchain_anthropic._compat import _convert_from_v1_to_anthropic45from tests.unit_tests._utils import FakeCallbackHandler4647MODEL_NAME = "claude-haiku-4-5-20251001"484950def test_stream() -> None:51    """Test streaming tokens from Anthropic."""52    llm = ChatAnthropic(model_name=MODEL_NAME)  # type: ignore[call-arg, call-arg]5354    full: BaseMessageChunk | None = None55    chunks_with_input_token_counts = 056    chunks_with_output_token_counts = 057    chunks_with_model_name = 058    for token in llm.stream("I'm Pickle Rick"):59        assert isinstance(token.content, str)60        full = cast("BaseMessageChunk", token) if full is None else full + token61        assert isinstance(token, AIMessageChunk)62        if token.usage_metadata is not None:63            if token.usage_metadata.get("input_tokens"):64                chunks_with_input_token_counts += 165            if token.usage_metadata.get("output_tokens"):66                chunks_with_output_token_counts += 167        chunks_with_model_name += int("model_name" in token.response_metadata)68    if chunks_with_input_token_counts != 1 or chunks_with_output_token_counts != 1:69        msg = (70            "Expected exactly one chunk with input or output token counts. "71            "AIMessageChunk aggregation adds counts. Check that "72            "this is behaving properly."73        )74        raise AssertionError(75            msg,76        )77    assert chunks_with_model_name == 178    # check token usage is populated79    assert isinstance(full, AIMessageChunk)80    assert len(full.content_blocks) == 181    assert full.content_blocks[0]["type"] == "text"82    assert full.content_blocks[0]["text"]83    assert full.usage_metadata is not None84    assert full.usage_metadata["input_tokens"] > 085    assert full.usage_metadata["output_tokens"] > 086    assert full.usage_metadata["total_tokens"] > 087    assert (88        full.usage_metadata["input_tokens"] + full.usage_metadata["output_tokens"]89        == full.usage_metadata["total_tokens"]90    )91    assert "stop_reason" in full.response_metadata92    assert "stop_sequence" in full.response_metadata93    assert "model_name" in full.response_metadata949596async def test_astream() -> None:97    """Test streaming tokens from Anthropic."""98    llm = ChatAnthropic(model_name=MODEL_NAME)  # type: ignore[call-arg, call-arg]99100    full: BaseMessageChunk | None = None101    chunks_with_input_token_counts = 0102    chunks_with_output_token_counts = 0103    async for token in llm.astream("I'm Pickle Rick"):104        assert isinstance(token.content, str)105        full = cast("BaseMessageChunk", token) if full is None else full + token106        assert isinstance(token, AIMessageChunk)107        if token.usage_metadata is not None:108            if token.usage_metadata.get("input_tokens"):109                chunks_with_input_token_counts += 1110            if token.usage_metadata.get("output_tokens"):111                chunks_with_output_token_counts += 1112    if chunks_with_input_token_counts != 1 or chunks_with_output_token_counts != 1:113        msg = (114            "Expected exactly one chunk with input or output token counts. "115            "AIMessageChunk aggregation adds counts. Check that "116            "this is behaving properly."117        )118        raise AssertionError(119            msg,120        )121    # check token usage is populated122    assert isinstance(full, AIMessageChunk)123    assert len(full.content_blocks) == 1124    assert full.content_blocks[0]["type"] == "text"125    assert full.content_blocks[0]["text"]126    assert full.usage_metadata is not None127    assert full.usage_metadata["input_tokens"] > 0128    assert full.usage_metadata["output_tokens"] > 0129    assert full.usage_metadata["total_tokens"] > 0130    assert (131        full.usage_metadata["input_tokens"] + full.usage_metadata["output_tokens"]132        == full.usage_metadata["total_tokens"]133    )134    assert "stop_reason" in full.response_metadata135    assert "stop_sequence" in full.response_metadata136137    # Check expected raw API output138    async_client = llm._async_client139    params: dict = {140        "model": MODEL_NAME,141        "max_tokens": 1024,142        "messages": [{"role": "user", "content": "hi"}],143        "extra_body": {"temperature": 0.0},144    }145    stream = await async_client.messages.create(**params, stream=True)146    async for event in stream:147        if event.type == "message_start":148            assert event.message.usage.input_tokens > 1149            # Different models may report different initial output token counts150            # in the message_start event. Ensure it's a positive value.151            assert event.message.usage.output_tokens >= 1152        elif event.type == "message_delta":153            assert event.usage.output_tokens >= 1154        else:155            pass156157158async def test_stream_usage() -> None:159    """Test usage metadata can be excluded."""160    model = ChatAnthropic(model_name=MODEL_NAME, stream_usage=False)  # type: ignore[call-arg]161    async for token in model.astream("hi"):162        assert isinstance(token, AIMessageChunk)163        assert token.usage_metadata is None164165166async def test_stream_usage_override() -> None:167    # check we override with kwarg168    model = ChatAnthropic(model_name=MODEL_NAME)  # type: ignore[call-arg]169    assert model.stream_usage170    async for token in model.astream("hi", stream_usage=False):171        assert isinstance(token, AIMessageChunk)172        assert token.usage_metadata is None173174175async def test_abatch() -> None:176    """Test streaming tokens."""177    llm = ChatAnthropic(model_name=MODEL_NAME)  # type: ignore[call-arg, call-arg]178179    result = await llm.abatch(["I'm Pickle Rick", "I'm not Pickle Rick"])180    for token in result:181        assert isinstance(token.content, str)182183184async def test_abatch_tags() -> None:185    """Test batch tokens."""186    llm = ChatAnthropic(model_name=MODEL_NAME)  # type: ignore[call-arg, call-arg]187188    result = await llm.abatch(189        ["I'm Pickle Rick", "I'm not Pickle Rick"],190        config={"tags": ["foo"]},191    )192    for token in result:193        assert isinstance(token.content, str)194195196async def test_async_tool_use() -> None:197    llm = ChatAnthropic(198        model=MODEL_NAME,  # type: ignore[call-arg]199    )200201    llm_with_tools = llm.bind_tools(202        [203            {204                "name": "get_weather",205                "description": "Get weather report for a city",206                "input_schema": {207                    "type": "object",208                    "properties": {"location": {"type": "string"}},209                },210            },211        ],212    )213    response = await llm_with_tools.ainvoke("what's the weather in san francisco, ca")214    assert isinstance(response, AIMessage)215    assert isinstance(response.content, list)216    assert isinstance(response.tool_calls, list)217    assert len(response.tool_calls) == 1218    tool_call = response.tool_calls[0]219    assert tool_call["name"] == "get_weather"220    assert isinstance(tool_call["args"], dict)221    assert "location" in tool_call["args"]222223    # Test streaming224    first = True225    chunks: list[BaseMessage | BaseMessageChunk] = []226    async for chunk in llm_with_tools.astream(227        "what's the weather in san francisco, ca",228    ):229        chunks = [*chunks, chunk]230        if first:231            gathered = chunk232            first = False233        else:234            gathered = gathered + chunk  # type: ignore[assignment]235    assert len(chunks) > 1236    assert isinstance(gathered, AIMessageChunk)237    assert isinstance(gathered.tool_call_chunks, list)238    assert len(gathered.tool_call_chunks) == 1239    tool_call_chunk = gathered.tool_call_chunks[0]240    assert tool_call_chunk["name"] == "get_weather"241    assert isinstance(tool_call_chunk["args"], str)242    assert "location" in json.loads(tool_call_chunk["args"])243244245def test_batch() -> None:246    """Test batch tokens."""247    llm = ChatAnthropic(model_name=MODEL_NAME)  # type: ignore[call-arg, call-arg]248249    result = llm.batch(["I'm Pickle Rick", "I'm not Pickle Rick"])250    for token in result:251        assert isinstance(token.content, str)252253254async def test_ainvoke() -> None:255    """Test invoke tokens."""256    llm = ChatAnthropic(model_name=MODEL_NAME)  # type: ignore[call-arg, call-arg]257258    result = await llm.ainvoke("I'm Pickle Rick", config={"tags": ["foo"]})259    assert isinstance(result.content, str)260    assert "model_name" in result.response_metadata261262263def test_invoke() -> None:264    """Test invoke tokens."""265    llm = ChatAnthropic(model_name=MODEL_NAME)  # type: ignore[call-arg, call-arg]266267    result = llm.invoke("I'm Pickle Rick", config={"tags": ["foo"]})268    assert isinstance(result.content, str)269270271def test_system_invoke() -> None:272    """Test invoke tokens with a system message."""273    llm = ChatAnthropic(model_name=MODEL_NAME)  # type: ignore[call-arg, call-arg]274275    prompt = ChatPromptTemplate.from_messages(276        [277            (278                "system",279                "You are an expert cartographer. If asked, you are a cartographer. "280                "STAY IN CHARACTER",281            ),282            ("human", "Are you a mathematician?"),283        ],284    )285286    chain = prompt | llm287288    result = chain.invoke({})289    assert isinstance(result.content, str)290291292def test_handle_empty_aimessage() -> None:293    # Anthropic can generate empty AIMessages, which are not valid unless in the last294    # message in a sequence.295    llm = ChatAnthropic(model=MODEL_NAME)296    messages = [297        HumanMessage("Hello"),298        AIMessage([]),299        HumanMessage("My name is Bob."),300    ]301    _ = llm.invoke(messages)302303    # Test tool call sequence304    llm_with_tools = llm.bind_tools(305        [306            {307                "name": "get_weather",308                "description": "Get weather report for a city",309                "input_schema": {310                    "type": "object",311                    "properties": {"location": {"type": "string"}},312                },313            },314        ],315    )316    _ = llm_with_tools.invoke(317        [318            HumanMessage("What's the weather in Boston?"),319            AIMessage(320                content=[],321                tool_calls=[322                    {323                        "name": "get_weather",324                        "args": {"location": "Boston"},325                        "id": "toolu_01V6d6W32QGGSmQm4BT98EKk",326                        "type": "tool_call",327                    },328                ],329            ),330            ToolMessage(331                content="It's sunny.", tool_call_id="toolu_01V6d6W32QGGSmQm4BT98EKk"332            ),333            AIMessage([]),334            HumanMessage("Thanks!"),335        ]336    )337338339def test_anthropic_call() -> None:340    """Test valid call to anthropic."""341    chat = ChatAnthropic(model=MODEL_NAME)  # type: ignore[call-arg]342    message = HumanMessage(content="Hello")343    response = chat.invoke([message])344    assert isinstance(response, AIMessage)345    assert isinstance(response.content, str)346347348def test_anthropic_generate() -> None:349    """Test generate method of anthropic."""350    chat = ChatAnthropic(model=MODEL_NAME)  # type: ignore[call-arg]351    chat_messages: list[list[BaseMessage]] = [352        [HumanMessage(content="How many toes do dogs have?")],353    ]354    messages_copy = [messages.copy() for messages in chat_messages]355    result: LLMResult = chat.generate(chat_messages)356    assert isinstance(result, LLMResult)357    for response in result.generations[0]:358        assert isinstance(response, ChatGeneration)359        assert isinstance(response.text, str)360        assert response.text == response.message.content361    assert chat_messages == messages_copy362363364def test_anthropic_streaming() -> None:365    """Test streaming tokens from anthropic."""366    chat = ChatAnthropic(model=MODEL_NAME)  # type: ignore[call-arg]367    message = HumanMessage(content="Hello")368    response = chat.stream([message])369    for token in response:370        assert isinstance(token, AIMessageChunk)371        assert isinstance(token.content, str)372373374def test_anthropic_streaming_callback() -> None:375    """Test that streaming correctly invokes on_llm_new_token callback."""376    callback_handler = FakeCallbackHandler()377    callback_manager = CallbackManager([callback_handler])378    chat = ChatAnthropic(379        model=MODEL_NAME,  # type: ignore[call-arg]380        callbacks=callback_manager,381        verbose=True,382    )383    message = HumanMessage(content="Write me a sentence with 10 words.")384    for token in chat.stream([message]):385        assert isinstance(token, AIMessageChunk)386        assert isinstance(token.content, str)387    assert callback_handler.llm_streams > 1388389390async def test_anthropic_async_streaming_callback() -> None:391    """Test that streaming correctly invokes on_llm_new_token callback."""392    callback_handler = FakeCallbackHandler()393    callback_manager = CallbackManager([callback_handler])394    chat = ChatAnthropic(395        model=MODEL_NAME,  # type: ignore[call-arg]396        callbacks=callback_manager,397        verbose=True,398    )399    chat_messages: list[BaseMessage] = [400        HumanMessage(content="How many toes do dogs have?"),401    ]402    async for token in chat.astream(chat_messages):403        assert isinstance(token, AIMessageChunk)404        assert isinstance(token.content, str)405    assert callback_handler.llm_streams > 1406407408def test_anthropic_multimodal() -> None:409    """Test that multimodal inputs are handled correctly."""410    chat = ChatAnthropic(model=MODEL_NAME)  # type: ignore[call-arg]411    messages: list[BaseMessage] = [412        HumanMessage(413            content=[414                {415                    "type": "image_url",416                    "image_url": {417                        # langchain logo418                        "url": "data:image/jpeg;base64,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",  # noqa: E501419                    },420                },421                {"type": "text", "text": "What is this a logo for?"},422            ],423        ),424    ]425    response = chat.invoke(messages)426    assert isinstance(response, AIMessage)427    assert isinstance(response.content, str)428    num_tokens = chat.get_num_tokens_from_messages(messages)429    assert num_tokens > 0430431432def test_streaming() -> None:433    """Test streaming tokens from Anthropic."""434    callback_handler = FakeCallbackHandler()435    callback_manager = CallbackManager([callback_handler])436437    llm = ChatAnthropic(  # type: ignore[call-arg, call-arg]438        model_name=MODEL_NAME,439        streaming=True,440        callbacks=callback_manager,441    )442443    response = llm.generate([[HumanMessage(content="I'm Pickle Rick")]])444    assert callback_handler.llm_streams > 0445    assert isinstance(response, LLMResult)446447448async def test_astreaming() -> None:449    """Test streaming tokens from Anthropic."""450    callback_handler = FakeCallbackHandler()451    callback_manager = CallbackManager([callback_handler])452453    llm = ChatAnthropic(  # type: ignore[call-arg, call-arg]454        model_name=MODEL_NAME,455        streaming=True,456        callbacks=callback_manager,457    )458459    response = await llm.agenerate([[HumanMessage(content="I'm Pickle Rick")]])460    assert callback_handler.llm_streams > 0461    assert isinstance(response, LLMResult)462463464def test_tool_use() -> None:465    llm = ChatAnthropic(466        model="claude-sonnet-4-5-20250929",  # type: ignore[call-arg]467        temperature=0,468    )469    tool_definition = {470        "name": "get_weather",471        "description": "Get weather report for a city",472        "input_schema": {473            "type": "object",474            "properties": {"location": {"type": "string"}},475        },476    }477    llm_with_tools = llm.bind_tools([tool_definition])478    query = "how are you? what's the weather in san francisco, ca"479    response = llm_with_tools.invoke(query)480    assert isinstance(response, AIMessage)481    assert isinstance(response.content, list)482    assert isinstance(response.tool_calls, list)483    assert len(response.tool_calls) == 1484    tool_call = response.tool_calls[0]485    assert tool_call["name"] == "get_weather"486    assert isinstance(tool_call["args"], dict)487    assert "location" in tool_call["args"]488489    content_blocks = response.content_blocks490    assert len(content_blocks) == 2491    assert content_blocks[0]["type"] == "text"492    assert content_blocks[0]["text"]493    assert content_blocks[1]["type"] == "tool_call"494    assert content_blocks[1]["name"] == "get_weather"495    assert content_blocks[1]["args"] == tool_call["args"]496497    # Test streaming498    llm = ChatAnthropic(model="claude-sonnet-4-5-20250929")  # type: ignore[call-arg]499    llm_with_tools = llm.bind_tools([tool_definition])500    first = True501    chunks: list[BaseMessage | BaseMessageChunk] = []502    for chunk in llm_with_tools.stream(query):503        chunks = [*chunks, chunk]504        if first:505            gathered = chunk506            first = False507        else:508            gathered = gathered + chunk  # type: ignore[assignment]509        for block in chunk.content_blocks:510            assert block["type"] in ("text", "tool_call_chunk")511    assert len(chunks) > 1512    assert isinstance(gathered.content, list)513    assert len(gathered.content) == 2514    tool_use_block = None515    for content_block in gathered.content:516        assert isinstance(content_block, dict)517        if content_block["type"] == "tool_use":518            tool_use_block = content_block519            break520    assert tool_use_block is not None521    assert tool_use_block["name"] == "get_weather"522    assert "location" in json.loads(tool_use_block["partial_json"])523    assert isinstance(gathered, AIMessageChunk)524    assert isinstance(gathered.tool_calls, list)525    assert len(gathered.tool_calls) == 1526    tool_call = gathered.tool_calls[0]527    assert tool_call["name"] == "get_weather"528    assert isinstance(tool_call["args"], dict)529    assert "location" in tool_call["args"]530    assert tool_call["id"] is not None531532    content_blocks = gathered.content_blocks533    assert len(content_blocks) == 2534    assert content_blocks[0]["type"] == "text"535    assert content_blocks[0]["text"]536    assert content_blocks[1]["type"] == "tool_call"537    assert content_blocks[1]["name"] == "get_weather"538    assert content_blocks[1]["args"]539540    # Test passing response back to model541    stream = llm_with_tools.stream(542        [543            query,544            gathered,545            ToolMessage(content="sunny and warm", tool_call_id=tool_call["id"]),546        ],547    )548    chunks = []549    first = True550    for chunk in stream:551        chunks = [*chunks, chunk]552        if first:553            gathered = chunk554            first = False555        else:556            gathered = gathered + chunk  # type: ignore[assignment]557    assert len(chunks) > 1558559560def test_builtin_tools_text_editor() -> None:561    llm = ChatAnthropic(model="claude-sonnet-4-5-20250929")  # type: ignore[call-arg]562    tool = {"type": "text_editor_20250728", "name": "str_replace_based_edit_tool"}563    llm_with_tools = llm.bind_tools([tool])564    response = llm_with_tools.invoke(565        "There's a syntax error in my primes.py file. Can you help me fix it?",566    )567    assert isinstance(response, AIMessage)568    assert response.tool_calls569570    content_blocks = response.content_blocks571    assert len(content_blocks) == 2572    assert content_blocks[0]["type"] == "text"573    assert content_blocks[0]["text"]574    assert content_blocks[1]["type"] == "tool_call"575    assert content_blocks[1]["name"] == "str_replace_based_edit_tool"576577578def test_builtin_tools_computer_use() -> None:579    """Test computer use tool integration.580581    Beta header should be automatically appended based on tool type.582583    This test only verifies tool call generation.584    """585    llm = ChatAnthropic(586        model="claude-sonnet-4-5-20250929",  # type: ignore[call-arg]587    )588    tool = {589        "type": "computer_20250124",590        "name": "computer",591        "display_width_px": 1024,592        "display_height_px": 768,593        "display_number": 1,594    }595    llm_with_tools = llm.bind_tools([tool])596    response = llm_with_tools.invoke(597        "Can you take a screenshot to see what's on the screen?",598    )599    assert isinstance(response, AIMessage)600    assert response.tool_calls601602    content_blocks = response.content_blocks603    assert len(content_blocks) >= 2604    assert content_blocks[0]["type"] == "text"605    assert content_blocks[0]["text"]606607    # Check that we have a tool_call for computer use608    tool_call_blocks = [b for b in content_blocks if b["type"] == "tool_call"]609    assert len(tool_call_blocks) >= 1610    assert tool_call_blocks[0]["name"] == "computer"611612    # Verify tool call has expected action (screenshot in this case)613    tool_call = response.tool_calls[0]614    assert tool_call["name"] == "computer"615    assert "action" in tool_call["args"]616    assert tool_call["args"]["action"] == "screenshot"617618619class GenerateUsername(BaseModel):620    """Get a username based on someone's name and hair color."""621622    name: str623    hair_color: str624625626def test_disable_parallel_tool_calling() -> None:627    llm = ChatAnthropic(model=MODEL_NAME)  # type: ignore[call-arg]628    llm_with_tools = llm.bind_tools([GenerateUsername], parallel_tool_calls=False)629    result = llm_with_tools.invoke(630        "Use the GenerateUsername tool to generate user names for:\n\n"631        "Sally with green hair\n"632        "Bob with blue hair",633    )634    assert isinstance(result, AIMessage)635    assert len(result.tool_calls) == 1636637638def test_anthropic_with_empty_text_block() -> None:639    """Anthropic SDK can return an empty text block."""640641    @tool642    def type_letter(letter: str) -> str:643        """Type the given letter."""644        return "OK"645646    model = ChatAnthropic(model=MODEL_NAME, temperature=0).bind_tools(  # type: ignore[call-arg]647        [type_letter],648    )649650    messages = [651        SystemMessage(652            content="Repeat the given string using the provided tools. Do not write "653            "anything else or provide any explanations. For example, "654            "if the string is 'abc', you must print the "655            "letters 'a', 'b', and 'c' one at a time and in that order. ",656        ),657        HumanMessage(content="dog"),658        AIMessage(659            content=[660                {"text": "", "type": "text"},661                {662                    "id": "toolu_01V6d6W32QGGSmQm4BT98EKk",663                    "input": {"letter": "d"},664                    "name": "type_letter",665                    "type": "tool_use",666                },667            ],668            tool_calls=[669                {670                    "name": "type_letter",671                    "args": {"letter": "d"},672                    "id": "toolu_01V6d6W32QGGSmQm4BT98EKk",673                    "type": "tool_call",674                },675            ],676        ),677        ToolMessage(content="OK", tool_call_id="toolu_01V6d6W32QGGSmQm4BT98EKk"),678    ]679680    model.invoke(messages)681682683def test_with_structured_output() -> None:684    llm = ChatAnthropic(685        model=MODEL_NAME,  # type: ignore[call-arg]686    )687688    structured_llm = llm.with_structured_output(689        {690            "name": "get_weather",691            "description": "Get weather report for a city",692            "input_schema": {693                "type": "object",694                "properties": {"location": {"type": "string"}},695            },696        },697    )698    response = structured_llm.invoke("what's the weather in san francisco, ca")699    assert isinstance(response, dict)700    assert response["location"]701702703class Person(BaseModel):704    """Person data."""705706    name: str707    age: int708    nicknames: list[str] | None709710711class PersonDict(TypedDict):712    """Person data as a TypedDict."""713714    name: str715    age: int716    nicknames: list[str] | None717718719@pytest.mark.parametrize("schema", [Person, Person.model_json_schema(), PersonDict])720def test_response_format(schema: dict | type) -> None:721    model = ChatAnthropic(722        model="claude-sonnet-4-5",  # type: ignore[call-arg]723    )724    query = "Chester (a.k.a. Chet) is 100 years old."725726    response = model.invoke(query, response_format=schema)727    parsed = json.loads(response.text)728    if isinstance(schema, type) and issubclass(schema, BaseModel):729        schema.model_validate(parsed)730    else:731        assert isinstance(parsed, dict)732        assert parsed["name"]733        assert parsed["age"]734735736@pytest.mark.vcr737def test_response_format_in_agent() -> None:738    class Weather(BaseModel):739        temperature: float740        units: str741742    # no tools743    agent = create_agent(744        "anthropic:claude-sonnet-4-5", response_format=ProviderStrategy(Weather)745    )746    result = agent.invoke({"messages": [{"role": "user", "content": "75 degrees F."}]})747    assert len(result["messages"]) == 2748    parsed = json.loads(result["messages"][-1].text)749    assert Weather(**parsed) == result["structured_response"]750751    # with tools752    def get_weather(location: str) -> str:753        """Get the weather at a location."""754        return "75 degrees Fahrenheit."755756    agent = create_agent(757        "anthropic:claude-sonnet-4-5",758        tools=[get_weather],759        response_format=ProviderStrategy(Weather),760    )761    result = agent.invoke(762        {"messages": [{"role": "user", "content": "What's the weather in SF?"}]},763    )764    assert len(result["messages"]) == 4765    assert result["messages"][1].tool_calls766    parsed = json.loads(result["messages"][-1].text)767    assert Weather(**parsed) == result["structured_response"]768769770@pytest.mark.vcr771def test_strict_tool_use() -> None:772    model = ChatAnthropic(773        model="claude-sonnet-4-5",  # type: ignore[call-arg]774    )775776    def get_weather(location: str, unit: Literal["C", "F"]) -> str:777        """Get the weather at a location."""778        return "75 degrees Fahrenheit."779780    model_with_tools = model.bind_tools([get_weather], strict=True)781782    response = model_with_tools.invoke("What's the weather in Boston, in Celsius?")783    assert response.tool_calls784785786def test_get_num_tokens_from_messages() -> None:787    llm = ChatAnthropic(model=MODEL_NAME)  # type: ignore[call-arg]788789    # Test simple case790    messages = [791        SystemMessage(content="You are a scientist"),792        HumanMessage(content="Hello, Claude"),793    ]794    num_tokens = llm.get_num_tokens_from_messages(messages)795    assert num_tokens > 0796797    # Test tool use798    @tool(parse_docstring=True)799    def get_weather(location: str) -> str:800        """Get the current weather in a given location.801802        Args:803            location: The city and state, e.g. San Francisco, CA804805        """806        return "Sunny"807808    messages = [809        HumanMessage(content="What's the weather like in San Francisco?"),810    ]811    num_tokens = llm.get_num_tokens_from_messages(messages, tools=[get_weather])812    assert num_tokens > 0813814    messages = [815        HumanMessage(content="What's the weather like in San Francisco?"),816        AIMessage(817            content=[818                {"text": "Let's see.", "type": "text"},819                {820                    "id": "toolu_01V6d6W32QGGSmQm4BT98EKk",821                    "input": {"location": "SF"},822                    "name": "get_weather",823                    "type": "tool_use",824                },825            ],826            tool_calls=[827                {828                    "name": "get_weather",829                    "args": {"location": "SF"},830                    "id": "toolu_01V6d6W32QGGSmQm4BT98EKk",831                    "type": "tool_call",832                },833            ],834        ),835        ToolMessage(content="Sunny", tool_call_id="toolu_01V6d6W32QGGSmQm4BT98EKk"),836    ]837    num_tokens = llm.get_num_tokens_from_messages(messages, tools=[get_weather])838    assert num_tokens > 0839840841class GetWeather(BaseModel):842    """Get the current weather in a given location."""843844    location: str = Field(..., description="The city and state, e.g. San Francisco, CA")845846847@pytest.mark.parametrize("tool_choice", ["GetWeather", "auto", "any"])848def test_anthropic_bind_tools_tool_choice(tool_choice: str) -> None:849    chat_model = ChatAnthropic(850        model=MODEL_NAME,  # type: ignore[call-arg]851    )852    chat_model_with_tools = chat_model.bind_tools([GetWeather], tool_choice=tool_choice)853    response = chat_model_with_tools.invoke("what's the weather in ny and la")854    assert isinstance(response, AIMessage)855856857def test_pdf_document_input() -> None:858    url = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"859    data = b64encode(requests.get(url, timeout=10).content).decode()860861    result = ChatAnthropic(model=MODEL_NAME).invoke(  # type: ignore[call-arg]862        [863            HumanMessage(864                [865                    "summarize this document",866                    {867                        "type": "document",868                        "source": {869                            "type": "base64",870                            "data": data,871                            "media_type": "application/pdf",872                        },873                    },874                ],875            ),876        ],877    )878    assert isinstance(result, AIMessage)879    assert isinstance(result.content, str)880    assert len(result.content) > 0881882883@pytest.mark.default_cassette("test_agent_loop.yaml.gz")884@pytest.mark.vcr885@pytest.mark.parametrize("output_version", ["v0", "v1"])886def test_agent_loop(output_version: Literal["v0", "v1"]) -> None:887    @tool888    def get_weather(location: str) -> str:889        """Get the weather for a location."""890        return "It's sunny."891892    llm = ChatAnthropic(model=MODEL_NAME, output_version=output_version)  # type: ignore[call-arg]893    llm_with_tools = llm.bind_tools([get_weather])894    input_message = HumanMessage("What is the weather in San Francisco, CA?")895    tool_call_message = llm_with_tools.invoke([input_message])896    assert isinstance(tool_call_message, AIMessage)897    tool_calls = tool_call_message.tool_calls898    assert len(tool_calls) == 1899    tool_call = tool_calls[0]900    tool_message = get_weather.invoke(tool_call)901    assert isinstance(tool_message, ToolMessage)902    response = llm_with_tools.invoke(903        [904            input_message,905            tool_call_message,906            tool_message,907        ]908    )909    assert isinstance(response, AIMessage)910911912@pytest.mark.default_cassette("test_agent_loop_streaming.yaml.gz")913@pytest.mark.vcr914@pytest.mark.parametrize(915    ("output_version", "use_v2_stream"),916    [917        ("v0", False),918        ("v1", False),919        ("v1", True),920    ],921)922def test_agent_loop_streaming(923    output_version: Literal["v0", "v1"], *, use_v2_stream: bool924) -> None:925    @tool926    def get_weather(location: str) -> str:927        """Get the weather for a location."""928        return "It's sunny."929930    llm = ChatAnthropic(931        model=MODEL_NAME,932        streaming=True,933        output_version=output_version,  # type: ignore[call-arg]934    )935    llm_with_tools = llm.bind_tools([get_weather])936    input_message = HumanMessage("What is the weather in San Francisco, CA?")937    if use_v2_stream:938        tool_call_message = cast(939            "ChatModelStream",940            llm_with_tools.stream_events([input_message], version="v3"),941        ).output942    else:943        tool_call_message = llm_with_tools.invoke([input_message])944    assert isinstance(tool_call_message, AIMessage)945946    tool_calls = tool_call_message.tool_calls947    assert len(tool_calls) == 1948    tool_call = tool_calls[0]949    tool_message = get_weather.invoke(tool_call)950    assert isinstance(tool_message, ToolMessage)951    if use_v2_stream:952        response = cast(953            "ChatModelStream",954            llm_with_tools.stream_events(955                [input_message, tool_call_message, tool_message],956                version="v3",957            ),958        ).output959    else:960        response = llm_with_tools.invoke(961            [962                input_message,963                tool_call_message,964                tool_message,965            ]966        )967    assert isinstance(response, AIMessage)968969970@pytest.mark.default_cassette("test_agent_loop_streaming.yaml.gz")971@pytest.mark.vcr972async def test_agent_loop_streaming_astream_events_v3_v1() -> None:973    """Async multi-turn through `astream_events(version="v3")`.974975    Mirrors `test_agent_loop_streaming` for `output_version="v1"` but976    exercises `AsyncChatModelStream` end-to-end.977    """978979    @tool980    def get_weather(location: str) -> str:981        """Get the weather for a location."""982        return "It's sunny."983984    llm = ChatAnthropic(985        model=MODEL_NAME,986        streaming=True,987        output_version="v1",  # type: ignore[call-arg]988    )989    llm_with_tools = llm.bind_tools([get_weather])990    input_message = HumanMessage("What is the weather in San Francisco, CA?")991    tool_call_message = await (992        await cast(993            "Awaitable[AsyncChatModelStream]",994            llm_with_tools.astream_events([input_message], version="v3"),995        )996    )997    assert isinstance(tool_call_message, AIMessage)998    tool_calls = tool_call_message.tool_calls999    assert len(tool_calls) == 11000    tool_call = tool_calls[0]1001    tool_message = get_weather.invoke(tool_call)1002    assert isinstance(tool_message, ToolMessage)1003    response = await (1004        await cast(1005            "Awaitable[AsyncChatModelStream]",1006            llm_with_tools.astream_events(1007                [input_message, tool_call_message, tool_message],1008                version="v3",1009            ),1010        )1011    )1012    assert isinstance(response, AIMessage)101310141015@pytest.mark.default_cassette("test_citations.yaml.gz")1016@pytest.mark.vcr1017@pytest.mark.parametrize(1018    ("output_version", "use_v2_stream"),1019    [1020        ("v0", False),1021        ("v1", False),1022        ("v1", True),1023    ],1024)1025def test_citations(output_version: Literal["v0", "v1"], *, use_v2_stream: bool) -> None:1026    llm = ChatAnthropic(model=MODEL_NAME, output_version=output_version)  # type: ignore[call-arg]1027    messages = [1028        {1029            "role": "user",1030            "content": [1031                {1032                    "type": "document",1033                    "source": {1034                        "type": "content",1035                        "content": [1036                            {"type": "text", "text": "The grass is green"},1037                            {"type": "text", "text": "The sky is blue"},1038                        ],1039                    },1040                    "citations": {"enabled": True},1041                },1042                {"type": "text", "text": "What color is the grass and sky?"},1043            ],1044        },1045    ]1046    response = llm.invoke(messages)1047    assert isinstance(response, AIMessage)1048    assert isinstance(response.content, list)1049    if output_version == "v1":1050        assert any("annotations" in block for block in response.content)1051    else:1052        assert any("citations" in block for block in response.content)10531054    # Test streaming1055    full: BaseMessage1056    if use_v2_stream:1057        full = llm.stream_events(messages, version="v3").output1058    else:1059        aggregated: BaseMessageChunk | None = None1060        for chunk in llm.stream(messages):1061            aggregated = (1062                cast("BaseMessageChunk", chunk)1063                if aggregated is None1064                else aggregated + chunk1065            )1066        assert isinstance(aggregated, AIMessageChunk)1067        full = aggregated1068    assert isinstance(full.content, list)1069    assert not any("citation" in block for block in full.content)1070    if output_version == "v1":1071        assert any("annotations" in block for block in full.content)1072    else:1073        assert any("citations" in block for block in full.content)10741075    # Test pass back in1076    next_message = {1077        "role": "user",1078        "content": "Can you comment on the citations you just made?",1079    }1080    _ = llm.invoke([*messages, full, next_message])108110821083@pytest.mark.vcr1084def test_thinking() -> None:1085    llm = ChatAnthropic(1086        model="claude-sonnet-4-5-20250929",  # type: ignore[call-arg]1087        max_tokens=5_000,  # type: ignore[call-arg]1088        thinking={"type": "enabled", "budget_tokens": 2_000},1089    )10901091    input_message = {"role": "user", "content": "Hello"}1092    response = llm.invoke([input_message])1093    assert any("thinking" in block for block in response.content)1094    for block in response.content:1095        assert isinstance(block, dict)1096        if block["type"] == "thinking":1097            assert set(block.keys()) == {"type", "thinking", "signature"}1098            assert block["thinking"]1099            assert isinstance(block["thinking"], str)1100            assert block["signature"]1101            assert isinstance(block["signature"], str)11021103    # Test streaming1104    full: BaseMessageChunk | None = None1105    for chunk in llm.stream([input_message]):1106        full = cast("BaseMessageChunk", chunk) if full is None else full + chunk1107    assert isinstance(full, AIMessageChunk)1108    assert isinstance(full.content, list)1109    assert any("thinking" in block for block in full.content)1110    for block in full.content:1111        assert isinstance(block, dict)1112        if block["type"] == "thinking":1113            assert set(block.keys()) == {"type", "thinking", "signature", "index"}1114            assert block["thinking"]1115            assert isinstance(block["thinking"], str)1116            assert block["signature"]1117            assert isinstance(block["signature"], str)11181119    # Test pass back in1120    next_message = {"role": "user", "content": "How are you?"}1121    _ = llm.invoke([input_message, full, next_message])112211231124@pytest.mark.default_cassette("test_thinking.yaml.gz")1125@pytest.mark.vcr1126@pytest.mark.parametrize("use_v2_stream", [False, True])1127def test_thinking_v1(*, use_v2_stream: bool) -> None:1128    llm = ChatAnthropic(1129        model="claude-sonnet-4-5-20250929",  # type: ignore[call-arg]1130        max_tokens=5_000,  # type: ignore[call-arg]1131        thinking={"type": "enabled", "budget_tokens": 2_000},1132        output_version="v1",1133    )11341135    input_message = {"role": "user", "content": "Hello"}1136    response = llm.invoke([input_message])1137    assert any("reasoning" in block for block in response.content)1138    for block in response.content:1139        assert isinstance(block, dict)1140        if block["type"] == "reasoning":1141            assert set(block.keys()) == {"type", "reasoning", "extras"}1142            assert block["reasoning"]1143            assert isinstance(block["reasoning"], str)1144            signature = block["extras"]["signature"]1145            assert signature1146            assert isinstance(signature, str)11471148    # Test streaming1149    full: BaseMessage1150    if use_v2_stream:1151        full = llm.stream_events([input_message], version="v3").output1152    else:1153        aggregated: BaseMessageChunk | None = None1154        for chunk in llm.stream([input_message]):1155            aggregated = (1156                cast(BaseMessageChunk, chunk)1157                if aggregated is None1158                else aggregated + chunk1159            )1160        assert isinstance(aggregated, AIMessageChunk)1161        full = aggregated1162    assert isinstance(full.content, list)1163    assert any("reasoning" in block for block in full.content)1164    for block in full.content:1165        assert isinstance(block, dict)1166        if block["type"] == "reasoning":1167            assert set(block.keys()) == {"type", "reasoning", "extras", "index"}1168            assert block["reasoning"]1169            assert isinstance(block["reasoning"], str)1170            signature = block["extras"]["signature"]1171            assert signature1172            assert isinstance(signature, str)11731174    # Test pass back in1175    next_message = {"role": "user", "content": "How are you?"}1176    _ = llm.invoke([input_message, full, next_message])117711781179@pytest.mark.default_cassette("test_redacted_thinking.yaml.gz")1180@pytest.mark.vcr1181@pytest.mark.parametrize("output_version", ["v0", "v1"])1182def test_redacted_thinking(output_version: Literal["v0", "v1"]) -> None:1183    llm = ChatAnthropic(1184        # It appears that Sonnet 4.5 either: isn't returning redacted thinking blocks,1185        # or the magic string is broken? Retry later once 3-7 finally removed1186        model="claude-3-7-sonnet-latest",  # type: ignore[call-arg]1187        max_tokens=5_000,  # type: ignore[call-arg]1188        thinking={"type": "enabled", "budget_tokens": 2_000},1189        output_version=output_version,1190    )1191    query = "ANTHROPIC_MAGIC_STRING_TRIGGER_REDACTED_THINKING_46C9A13E193C177646C7398A98432ECCCE4C1253D5E2D82641AC0E52CC2876CB"  # noqa: E5011192    input_message = {"role": "user", "content": query}11931194    response = llm.invoke([input_message])1195    value = None1196    for block in response.content:1197        assert isinstance(block, dict)1198        if block["type"] == "redacted_thinking":1199            value = block1200        elif (1201            block["type"] == "non_standard"1202            and block["value"]["type"] == "redacted_thinking"1203        ):1204            value = block["value"]1205        else:1206            pass1207        if value:1208            assert set(value.keys()) == {"type", "data"}1209            assert value["data"]1210            assert isinstance(value["data"], str)1211    assert value is not None12121213    # Test streaming1214    full: BaseMessageChunk | None = None1215    for chunk in llm.stream([input_message]):1216        full = cast("BaseMessageChunk", chunk) if full is None else full + chunk1217    assert isinstance(full, AIMessageChunk)1218    assert isinstance(full.content, list)1219    value = None1220    for block in full.content:1221        assert isinstance(block, dict)1222        if block["type"] == "redacted_thinking":1223            value = block1224            assert set(value.keys()) == {"type", "data", "index"}1225            assert "index" in block1226        elif (1227            block["type"] == "non_standard"1228            and block["value"]["type"] == "redacted_thinking"1229        ):1230            value = block["value"]1231            assert isinstance(value, dict)1232            assert set(value.keys()) == {"type", "data"}1233            assert "index" in block1234        else:1235            pass1236        if value:1237            assert value["data"]1238            assert isinstance(value["data"], str)1239    assert value is not None12401241    # Test pass back in1242    next_message = {"role": "user", "content": "What?"}1243    _ = llm.invoke([input_message, full, next_message])124412451246def test_structured_output_thinking_enabled() -> None:1247    llm = ChatAnthropic(1248        model="claude-sonnet-4-5-20250929",  # type: ignore[call-arg]1249        max_tokens=5_000,  # type: ignore[call-arg]1250        thinking={"type": "enabled", "budget_tokens": 2_000},1251    )1252    with pytest.warns(match="structured output"):1253        structured_llm = llm.with_structured_output(GenerateUsername)1254    query = "Generate a username for Sally with green hair"1255    response = structured_llm.invoke(query)1256    assert isinstance(response, GenerateUsername)12571258    with pytest.raises(OutputParserException):1259        structured_llm.invoke("Hello")12601261    # Test streaming1262    for chunk in structured_llm.stream(query):1263        assert isinstance(chunk, GenerateUsername)126412651266@pytest.mark.retry(count=3, delay=1)1267def test_structured_output_thinking_force_tool_use() -> None:1268    # Structured output currently relies on forced tool use, which is not supported1269    # when `thinking` is enabled. When this test fails, it means that the feature1270    # is supported and the workarounds in `with_structured_output` should be removed.1271    # Use the client resolved off `ChatAnthropic` so requests honor any1272    # configured base URL / credentials (e.g. the LangSmith gateway).1273    client = ChatAnthropic(model="claude-sonnet-4-5-20250929")._client  # type: ignore[call-arg]1274    with pytest.raises(anthropic.BadRequestError):1275        _ = client.messages.create(1276            model="claude-sonnet-4-5-20250929",1277            max_tokens=5_000,1278            thinking={"type": "enabled", "budget_tokens": 2_000},1279            tool_choice={"type": "tool", "name": "get_weather"},1280            tools=[1281                {1282                    "name": "get_weather",1283                    "description": "Get the weather at a location.",1284                    "input_schema": {1285                        "type": "object",1286                        "properties": {1287                            "location": {"type": "string"},1288                        },1289                        "required": ["location"],1290                    },1291                }1292            ],1293            messages=[1294                {1295                    "role": "user",1296                    "content": "What's the weather in San Francisco?",1297                }1298            ],1299        )130013011302def test_effort_parameter() -> None:1303    """Test that effort parameter can be passed without errors.13041305    Only Opus 4.5 supports currently.1306    """1307    llm = ChatAnthropic(1308        model="claude-opus-4-5-20251101",1309        effort="medium",1310        max_tokens=100,1311    )13121313    result = llm.invoke("Say hello in one sentence")13141315    # Verify we got a response1316    assert isinstance(result.content, str)1317    assert len(result.content) > 013181319    # Verify response metadata is present1320    assert "model_name" in result.response_metadata1321    assert result.usage_metadata is not None1322    assert result.usage_metadata["input_tokens"] > 01323    assert result.usage_metadata["output_tokens"] > 0132413251326def test_reasoning_effort_parameter() -> None:1327    """Test that the standard `reasoning_effort` parameter is accepted by the API."""1328    llm = ChatAnthropic(1329        model="claude-opus-4-5-20251101",1330        reasoning_effort="medium",1331        max_tokens=100,1332    )13331334    result = llm.invoke("Say hello in one sentence")13351336    assert isinstance(result.content, str)1337    assert len(result.content) > 01338    assert "model_name" in result.response_metadata1339    assert result.usage_metadata is not None1340    assert result.usage_metadata["input_tokens"] > 01341    assert result.usage_metadata["output_tokens"] > 0134213431344def test_reasoning_effort_call_time_kwarg() -> None:1345    """Test that `reasoning_effort` is accepted as a call-time kwarg."""1346    llm = ChatAnthropic(model="claude-opus-4-5-20251101", max_tokens=100)13471348    result = llm.invoke("Say hello in one sentence", reasoning_effort="low")13491350    assert isinstance(result.content, str)1351    assert len(result.content) > 01352    assert result.usage_metadata is not None135313541355def test_reasoning_effort_defaults_adaptive_thinking() -> None:1356    """`reasoning_effort` defaults `thinking` to adaptive on models that support it.13571358    Regression test for a model (Opus 4.7+, Opus 5, Sonnet 5) actually accepting the1359    resulting `{"type": "adaptive", "display": "summarized"}` thinking config,1360    not just that the payload is well-formed locally.1361    """1362    llm = ChatAnthropic(1363        model="claude-opus-5",1364        reasoning_effort="high",1365        max_tokens=2_000,1366    )13671368    # A genuine multi-step problem: adaptive thinking is model-decided, and a1369    # trivial prompt (e.g. "what is 3+4") may not surface a visible `thinking`1370    # block even when accepted, which would make this test flaky.1371    result = llm.invoke(1372        "A farmer has 17 sheep. All but 9 die. Then he buys triple the number "1373        "of remaining sheep, then sells half (rounding down). How many sheep "1374        "does he have now? Show your reasoning step by step."1375    )13761377    assert isinstance(result.content, list)1378    assert any(1379        isinstance(block, dict) and block.get("type") == "thinking"1380        for block in result.content1381    )1382    assert result.usage_metadata is not None138313841385def test_image_tool_calling() -> None:1386    """Test tool calling with image inputs."""13871388    class color_picker(BaseModel):  # noqa: N8011389        """Input your fav color and get a random fact about it."""13901391        fav_color: str13921393    human_content: list[dict] = [1394        {1395            "type": "text",1396            "text": "what's your favorite color in this image",1397        },1398    ]1399    image_url = "https://raw.githubusercontent.com/langchain-ai/docs/4d11d08b6b0e210bd456943f7a22febbd168b543/src/images/agentic-rag-output.png"1400    image_data = b64encode(httpx.get(image_url, timeout=10.0).content).decode("utf-8")1401    human_content.append(1402        {1403            "type": "image",1404            "source": {1405                "type": "base64",1406                "media_type": "image/png",1407                "data": image_data,1408            },1409        },1410    )1411    messages = [1412        SystemMessage("you're a good assistant"),1413        HumanMessage(human_content),  # type: ignore[arg-type]1414        AIMessage(1415            [1416                {"type": "text", "text": "Hmm let me think about that"},1417                {1418                    "type": "tool_use",1419                    "input": {"fav_color": "purple"},1420                    "id": "foo",1421                    "name": "color_picker",1422                },1423            ],1424        ),1425        HumanMessage(1426            [1427                {1428                    "type": "tool_result",1429                    "tool_use_id": "foo",1430                    "content": [1431                        {1432                            "type": "text",1433                            "text": "purple is a great pick! that's my sister's favorite color",  # noqa: E5011434                        },1435                    ],1436                    "is_error": False,1437                },1438                {"type": "text", "text": "what's my sister's favorite color"},1439            ],1440        ),1441    ]1442    llm = ChatAnthropic(model=MODEL_NAME)  # type: ignore[call-arg]1443    _ = llm.bind_tools([color_picker]).invoke(messages)144414451446@pytest.mark.default_cassette("test_web_search.yaml.gz")1447@pytest.mark.vcr1448@pytest.mark.parametrize("output_version", ["v0", "v1"])1449def test_web_search(output_version: Literal["v0", "v1"]) -> None:1450    llm = ChatAnthropic(1451        model=MODEL_NAME,  # type: ignore[call-arg]1452        max_tokens=1024,1453        output_version=output_version,1454    )14551456    tool = {"type": "web_search_20250305", "name": "web_search", "max_uses": 1}1457    llm_with_tools = llm.bind_tools([tool])14581459    input_message = {1460        "role": "user",1461        "content": [1462            {1463                "type": "text",1464                "text": "How do I update a web app to TypeScript 5.5?",1465            },1466        ],1467    }1468    response = llm_with_tools.invoke([input_message])1469    assert all(isinstance(block, dict) for block in response.content)1470    block_types = {block["type"] for block in response.content}  # type: ignore[index]1471    if output_version == "v0":1472        assert block_types == {"text", "server_tool_use", "web_search_tool_result"}1473    else:1474        assert block_types == {"text", "server_tool_call", "server_tool_result"}14751476    # Test streaming1477    full: BaseMessageChunk | None = None1478    for chunk in llm_with_tools.stream([input_message]):1479        assert isinstance(chunk, AIMessageChunk)1480        full = chunk if full is None else full + chunk14811482    assert isinstance(full, AIMessageChunk)1483    assert isinstance(full.content, list)1484    block_types = {block["type"] for block in full.content}  # type: ignore[index]1485    if output_version == "v0":1486        assert block_types == {"text", "server_tool_use", "web_search_tool_result"}1487    else:1488        assert block_types == {"text", "server_tool_call", "server_tool_result"}14891490    # Test we can pass back in1491    next_message = {1492        "role": "user",1493        "content": "Please repeat the last search, but focus on sources from 2024.",1494    }1495    _ = llm_with_tools.invoke(1496        [input_message, full, next_message],1497    )149814991500@pytest.mark.vcr1501def test_web_fetch() -> None:1502    """Note: this is a beta feature.15031504    TODO: Update to remove beta once it's generally available.1505    """1506    llm = ChatAnthropic(1507        model=MODEL_NAME,  # type: ignore[call-arg]1508        max_tokens=1024,1509        betas=["web-fetch-2025-09-10"],1510    )1511    tool = {"type": "web_fetch_20250910", "name": "web_fetch", "max_uses": 1}1512    llm_with_tools = llm.bind_tools([tool])15131514    input_message = {1515        "role": "user",1516        "content": [1517            {1518                "type": "text",1519                "text": "Fetch the content at https://docs.langchain.com and analyze",1520            },1521        ],1522    }1523    response = llm_with_tools.invoke([input_message])1524    assert all(isinstance(block, dict) for block in response.content)1525    block_types = {1526        block["type"] for block in response.content if isinstance(block, dict)1527    }15281529    # A successful fetch call should include:1530    # 1. text response from the model (e.g. "I'll fetch that for you")1531    # 2. server_tool_use block indicating the tool was called (using tool "web_fetch")1532    # 3. web_fetch_tool_result block with the results of said fetch1533    assert block_types == {"text", "server_tool_use", "web_fetch_tool_result"}15341535    # Verify web fetch result structure1536    web_fetch_results = [1537        block1538        for block in response.content1539        if isinstance(block, dict) and block.get("type") == "web_fetch_tool_result"1540    ]1541    assert len(web_fetch_results) == 1  # Since max_uses=11542    fetch_result = web_fetch_results[0]1543    assert "content" in fetch_result1544    assert "url" in fetch_result["content"]1545    assert "retrieved_at" in fetch_result["content"]15461547    # Fetch with citations enabled1548    tool_with_citations = tool.copy()1549    tool_with_citations["citations"] = {"enabled": True}1550    llm_with_citations = llm.bind_tools([tool_with_citations])15511552    citation_message = {1553        "role": "user",1554        "content": (1555            "Fetch https://docs.langchain.com and provide specific quotes with "1556            "citations"1557        ),1558    }1559    citation_response = llm_with_citations.invoke([citation_message])15601561    citation_results = [1562        block1563        for block in citation_response.content1564        if isinstance(block, dict) and block.get("type") == "web_fetch_tool_result"1565    ]1566    assert len(citation_results) == 1  # Since max_uses=11567    citation_result = citation_results[0]1568    assert citation_result["content"]["content"]["citations"]["enabled"]1569    text_blocks = [1570        block1571        for block in citation_response.content1572        if isinstance(block, dict) and block.get("type") == "text"1573    ]15741575    # Check that the response contains actual citations in the content1576    has_citations = False1577    for block in text_blocks:1578        citations = block.get("citations", [])1579        for citation in citations:1580            if citation.get("type") and citation.get("start_char_index"):1581                has_citations = True1582                break1583    assert has_citations, (1584        "Expected inline citation tags in response when citations are enabled for "1585        "web fetch"1586    )15871588    # Max content tokens param1589    tool_with_limit = tool.copy()1590    tool_with_limit["max_content_tokens"] = 10001591    llm_with_limit = llm.bind_tools([tool_with_limit])15921593    limit_response = llm_with_limit.invoke([input_message])1594    # Response should still work even with content limits1595    assert any(1596        block["type"] == "web_fetch_tool_result"1597        for block in limit_response.content1598        if isinstance(block, dict)1599    )16001601    # Domains filtering (note: only one can be set at a time)1602    tool_with_allowed_domains = tool.copy()1603    tool_with_allowed_domains["allowed_domains"] = ["docs.langchain.com"]1604    llm_with_allowed = llm.bind_tools([tool_with_allowed_domains])16051606    allowed_response = llm_with_allowed.invoke([input_message])1607    assert any(1608        block["type"] == "web_fetch_tool_result"1609        for block in allowed_response.content1610        if isinstance(block, dict)1611    )16121613    # Test that a disallowed domain doesn't work1614    tool_with_disallowed_domains = tool.copy()1615    tool_with_disallowed_domains["allowed_domains"] = [1616        "example.com"1617    ]  # Not docs.langchain.com1618    llm_with_disallowed = llm.bind_tools([tool_with_disallowed_domains])16191620    disallowed_response = llm_with_disallowed.invoke([input_message])16211622    # We should get an error result since the domain (docs.langchain.com) is not allowed1623    disallowed_results = [1624        block1625        for block in disallowed_response.content1626        if isinstance(block, dict) and block.get("type") == "web_fetch_tool_result"1627    ]1628    if disallowed_results:1629        disallowed_result = disallowed_results[0]1630        if disallowed_result.get("content", {}).get("type") == "web_fetch_tool_error":1631            assert disallowed_result["content"]["error_code"] in [1632                "invalid_url",1633                "fetch_failed",1634            ]16351636    # Blocked domains filtering1637    tool_with_blocked_domains = tool.copy()1638    tool_with_blocked_domains["blocked_domains"] = ["example.com"]1639    llm_with_blocked = llm.bind_tools([tool_with_blocked_domains])16401641    blocked_response = llm_with_blocked.invoke([input_message])1642    assert any(1643        block["type"] == "web_fetch_tool_result"1644        for block in blocked_response.content1645        if isinstance(block, dict)1646    )16471648    # Test fetching from a blocked domain fails1649    blocked_domain_message = {1650        "role": "user",1651        "content": "Fetch https://example.com and analyze",1652    }1653    tool_with_blocked_example = tool.copy()1654    tool_with_blocked_example["blocked_domains"] = ["example.com"]1655    llm_with_blocked_example = llm.bind_tools([tool_with_blocked_example])16561657    blocked_domain_response = llm_with_blocked_example.invoke([blocked_domain_message])16581659    # Should get an error when trying to access a blocked domain1660    blocked_domain_results = [1661        block1662        for block in blocked_domain_response.content1663        if isinstance(block, dict) and block.get("type") == "web_fetch_tool_result"1664    ]1665    if blocked_domain_results:1666        blocked_result = blocked_domain_results[0]1667        if blocked_result.get("content", {}).get("type") == "web_fetch_tool_error":1668            assert blocked_result["content"]["error_code"] in [1669                "invalid_url",1670                "fetch_failed",1671            ]16721673    # Max uses parameter - test exceeding the limit1674    multi_fetch_message = {1675        "role": "user",1676        "content": (1677            "Fetch https://docs.langchain.com and then try to fetch "1678            "https://langchain.com"1679        ),1680    }1681    max_uses_response = llm_with_tools.invoke([multi_fetch_message])16821683    # Should contain at least one fetch result and potentially an error for the second1684    fetch_results = [1685        block1686        for block in max_uses_response.content1687        if isinstance(block, dict) and block.get("type") == "web_fetch_tool_result"1688    ]  # type: ignore[index]1689    assert len(fetch_results) >= 11690    error_results = [1691        r1692        for r in fetch_results1693        if r.get("content", {}).get("type") == "web_fetch_tool_error"1694    ]1695    if error_results:1696        assert any(1697            r["content"]["error_code"] == "max_uses_exceeded" for r in error_results1698        )16991700    # Streaming1701    full: BaseMessageChunk | None = None1702    for chunk in llm_with_tools.stream([input_message]):1703        assert isinstance(chunk, AIMessageChunk)1704        full = chunk if full is None else full + chunk1705    assert isinstance(full, AIMessageChunk)1706    assert isinstance(full.content, list)1707    block_types = {block["type"] for block in full.content if isinstance(block, dict)}1708    assert block_types == {"text", "server_tool_use", "web_fetch_tool_result"}17091710    # Test that URLs from context can be used in follow-up1711    next_message = {1712        "role": "user",1713        "content": "What does the site you just fetched say about models?",1714    }1715    follow_up_response = llm_with_tools.invoke(1716        [input_message, full, next_message],1717    )1718    # Should work without issues since URL was already in context1719    assert isinstance(follow_up_response.content, (list, str))17201721    # Error handling - test with an invalid URL format1722    error_message = {1723        "role": "user",1724        "content": "Try to fetch this invalid URL: not-a-valid-url",1725    }1726    error_response = llm_with_tools.invoke([error_message])17271728    # Should handle the error gracefully1729    assert isinstance(error_response.content, (list, str))17301731    # PDF document fetching1732    pdf_message = {1733        "role": "user",1734        "content": (1735            "Fetch this PDF: "1736            "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf "1737            "and summarize its content",1738        ),1739    }1740    pdf_response = llm_with_tools.invoke([pdf_message])17411742    assert any(1743        block["type"] == "web_fetch_tool_result"1744        for block in pdf_response.content1745        if isinstance(block, dict)1746    )17471748    # Verify PDF content structure (should have base64 data for PDFs)1749    pdf_results = [1750        block1751        for block in pdf_response.content1752        if isinstance(block, dict) and block.get("type") == "web_fetch_tool_result"1753    ]1754    if pdf_results:1755        pdf_result = pdf_results[0]1756        content = pdf_result.get("content", {})1757        if content.get("content", {}).get("source", {}).get("type") == "base64":1758            assert content["content"]["source"]["media_type"] == "application/pdf"1759            assert "data" in content["content"]["source"]176017611762@pytest.mark.default_cassette("test_web_fetch_v1.yaml.gz")1763@pytest.mark.vcr1764@pytest.mark.parametrize("output_version", ["v0", "v1"])1765def test_web_fetch_v1(output_version: Literal["v0", "v1"]) -> None:1766    """Test that http calls are unchanged between v0 and v1."""1767    llm = ChatAnthropic(1768        model=MODEL_NAME,  # type: ignore[call-arg]1769        betas=["web-fetch-2025-09-10"],1770        output_version=output_version,1771    )17721773    if output_version == "v0":1774        call_key = "server_tool_use"1775        result_key = "web_fetch_tool_result"1776    else:1777        # v11778        call_key = "server_tool_call"1779        result_key = "server_tool_result"17801781    tool = {1782        "type": "web_fetch_20250910",1783        "name": "web_fetch",1784        "max_uses": 1,1785        "citations": {"enabled": True},1786    }1787    llm_with_tools = llm.bind_tools([tool])17881789    input_message = {1790        "role": "user",1791        "content": [1792            {1793                "type": "text",1794                "text": "Fetch the content at https://docs.langchain.com and analyze",1795            },1796        ],1797    }1798    response = llm_with_tools.invoke([input_message])1799    assert all(isinstance(block, dict) for block in response.content)1800    block_types = {block["type"] for block in response.content}  # type: ignore[index]1801    assert block_types == {"text", call_key, result_key}18021803    # Test streaming1804    full: BaseMessageChunk | None = None1805    for chunk in llm_with_tools.stream([input_message]):1806        assert isinstance(chunk, AIMessageChunk)1807        full = chunk if full is None else full + chunk18081809    assert isinstance(full, AIMessageChunk)1810    assert isinstance(full.content, list)1811    block_types = {block["type"] for block in full.content}  # type: ignore[index]1812    assert block_types == {"text", call_key, result_key}18131814    # Test we can pass back in1815    next_message = {1816        "role": "user",1817        "content": "What does the site you just fetched say about models?",1818    }1819    _ = llm_with_tools.invoke(1820        [input_message, full, next_message],1821    )182218231824@pytest.mark.default_cassette("test_code_execution_old.yaml.gz")1825@pytest.mark.vcr1826@pytest.mark.parametrize("output_version", ["v0", "v1"])1827def test_code_execution_old(output_version: Literal["v0", "v1"]) -> None:1828    """Note: this tests the `code_execution_20250522` tool, which is now legacy.18291830    See the `test_code_execution` test below to test the current1831    `code_execution_20250825` tool.18321833    Migration guide: https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#upgrade-to-latest-tool-version1834    """1835    llm = ChatAnthropic(1836        model=MODEL_NAME,  # type: ignore[call-arg]1837        betas=["code-execution-2025-05-22"],1838        output_version=output_version,1839    )18401841    tool = {"type": "code_execution_20250522", "name": "code_execution"}1842    llm_with_tools = llm.bind_tools([tool])18431844    input_message = {1845        "role": "user",1846        "content": [1847            {1848                "type": "text",1849                "text": (1850                    "Calculate the mean and standard deviation of "1851                    "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"1852                ),1853            },1854        ],1855    }1856    response = llm_with_tools.invoke([input_message])1857    assert all(isinstance(block, dict) for block in response.content)1858    block_types = {block["type"] for block in response.content}  # type: ignore[index]1859    if output_version == "v0":1860        assert block_types == {"text", "server_tool_use", "code_execution_tool_result"}1861    else:1862        assert block_types == {"text", "server_tool_call", "server_tool_result"}18631864    # Test streaming1865    full: BaseMessageChunk | None = None1866    for chunk in llm_with_tools.stream([input_message]):1867        assert isinstance(chunk, AIMessageChunk)1868        full = chunk if full is None else full + chunk1869    assert isinstance(full, AIMessageChunk)1870    assert isinstance(full.content, list)1871    block_types = {block["type"] for block in full.content}  # type: ignore[index]1872    if output_version == "v0":1873        assert block_types == {"text", "server_tool_use", "code_execution_tool_result"}1874    else:1875        assert block_types == {"text", "server_tool_call", "server_tool_result"}18761877    # Test we can pass back in1878    next_message = {1879        "role": "user",1880        "content": "Please add more comments to the code.",1881    }1882    _ = llm_with_tools.invoke(1883        [input_message, full, next_message],1884    )188518861887def _collect_file_ids(content: Any) -> list[str]:1888    """Recursively collect `file_id` values from response content."""1889    if isinstance(content, dict):1890        found = [content["file_id"]] if "file_id" in content else []1891        return found + [fid for v in content.values() for fid in _collect_file_ids(v)]1892    if isinstance(content, list):1893        return [fid for item in content for fid in _collect_file_ids(item)]1894    return []189518961897@pytest.mark.default_cassette("test_code_execution.yaml.gz")1898@pytest.mark.vcr1899@pytest.mark.parametrize("output_version", ["v0", "v1"])1900def test_code_execution(output_version: Literal["v0", "v1"]) -> None:1901    llm = ChatAnthropic(1902        model=MODEL_NAME,  # type: ignore[call-arg]1903        output_version=output_version,1904    )19051906    tool = {"type": "code_execution_20250825", "name": "code_execution"}1907    llm_with_tools = llm.bind_tools([tool])19081909    input_message = {1910        "role": "user",1911        "content": [1912            {1913                "type": "text",1914                "text": (1915                    "Calculate the mean and standard deviation of "1916                    "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"1917                ),1918            },1919        ],1920    }1921    response = llm_with_tools.invoke([input_message])1922    assert all(isinstance(block, dict) for block in response.content)1923    block_types = {block["type"] for block in response.content}  # type: ignore[index]1924    if output_version == "v0":1925        assert block_types == {1926            "text",1927            "server_tool_use",1928            "bash_code_execution_tool_result",1929        }1930    else:1931        assert block_types == {"text", "server_tool_call", "server_tool_result"}19321933    # Test streaming1934    full: BaseMessageChunk | None = None1935    for chunk in llm_with_tools.stream([input_message]):1936        assert isinstance(chunk, AIMessageChunk)1937        full = chunk if full is None else full + chunk1938    assert isinstance(full, AIMessageChunk)1939    assert isinstance(full.content, list)1940    block_types = {block["type"] for block in full.content}  # type: ignore[index]1941    if output_version == "v0":1942        assert block_types == {1943            "text",1944            "server_tool_use",1945            "bash_code_execution_tool_result",1946        }1947    else:1948        assert block_types == {"text", "server_tool_call", "server_tool_result"}19491950    # Test we can pass back in1951    next_message = {1952        "role": "user",1953        "content": "Please add more comments to the code.",1954    }1955    _ = llm_with_tools.invoke(1956        [input_message, full, next_message],1957    )195819591960@pytest.mark.default_cassette("test_skills.yaml.gz")1961@pytest.mark.vcr1962@pytest.mark.parametrize("output_version", ["v0", "v1"])1963def test_skills(output_version: Literal["v0", "v1"]) -> None:1964    """Load an Anthropic skill into the code execution container."""1965    skills = [{"type": "anthropic", "skill_id": "xlsx"}]1966    code_execution = {"type": "code_execution_20250825", "name": "code_execution"}1967    llm = ChatAnthropic(1968        model=MODEL_NAME,  # type: ignore[call-arg]1969        container={"skills": skills},1970        reuse_last_container=True,1971        output_version=output_version,1972    )1973    llm_with_tools = llm.bind_tools([code_execution])19741975    input_message = {1976        "role": "user",1977        "content": "Create an xlsx file with a single cell containing the number 42.",1978    }19791980    # Stream the first turn. `.output` blocks until the stream finishes and1981    # returns the aggregated message.1982    # `stream_events` is typed as `Iterator[Any]` on a bound model; the v31983    # protocol returns a `ChatModelStream`.1984    stream = cast("Any", llm_with_tools.stream_events([input_message], version="v3"))1985    first_response = stream.output19861987    # The skill ran in the container and wrote a file.1988    container_id = first_response.response_metadata["container"]["id"]1989    assert container_id1990    assert _collect_file_ids(first_response.content)19911992    # `reuse_last_container` supplies the container ID on the next turn without1993    # dropping the skills.1994    messages: list = [1995        input_message,1996        first_response,1997        {"role": "user", "content": "Now change the cell to 43."},1998    ]1999    payload = llm._get_request_payload(messages, tools=[code_execution])2000    assert payload["container"] == {"id": container_id, "skills": skills}

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.