libs/partners/anthropic/tests/integration_tests/test_chat_models.py PYTHON 2,868 lines View on github.com → Search inside
File is large — showing lines 1–2,000 of 2,868.
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_calls784785786@pytest.mark.vcr787def test_system_tool_addition() -> None:788    model = ChatAnthropic(model="claude-opus-5-5")  # type: ignore[call-arg]789    response = model.invoke(790        [791            HumanMessage("What time is it?"),792            SystemMessage(793                [794                    {795                        "type": "tool_addition",796                        "tool": {797                            "type": "tool_definition",798                            "definition": {799                                "name": "get_time",800                                "description": "Get the current time.",801                                "input_schema": {802                                    "type": "object",803                                    "properties": {},804                                },805                            },806                        },807                    }808                ]809            ),810        ]811    )812    assert isinstance(response, AIMessage)813    assert response.tool_calls[0]["name"] == "get_time"814815816def test_get_num_tokens_from_messages() -> None:817    llm = ChatAnthropic(model=MODEL_NAME)  # type: ignore[call-arg]818819    # Test simple case820    messages = [821        SystemMessage(content="You are a scientist"),822        HumanMessage(content="Hello, Claude"),823    ]824    num_tokens = llm.get_num_tokens_from_messages(messages)825    assert num_tokens > 0826827    # Test tool use828    @tool(parse_docstring=True)829    def get_weather(location: str) -> str:830        """Get the current weather in a given location.831832        Args:833            location: The city and state, e.g. San Francisco, CA834835        """836        return "Sunny"837838    messages = [839        HumanMessage(content="What's the weather like in San Francisco?"),840    ]841    num_tokens = llm.get_num_tokens_from_messages(messages, tools=[get_weather])842    assert num_tokens > 0843844    messages = [845        HumanMessage(content="What's the weather like in San Francisco?"),846        AIMessage(847            content=[848                {"text": "Let's see.", "type": "text"},849                {850                    "id": "toolu_01V6d6W32QGGSmQm4BT98EKk",851                    "input": {"location": "SF"},852                    "name": "get_weather",853                    "type": "tool_use",854                },855            ],856            tool_calls=[857                {858                    "name": "get_weather",859                    "args": {"location": "SF"},860                    "id": "toolu_01V6d6W32QGGSmQm4BT98EKk",861                    "type": "tool_call",862                },863            ],864        ),865        ToolMessage(content="Sunny", tool_call_id="toolu_01V6d6W32QGGSmQm4BT98EKk"),866    ]867    num_tokens = llm.get_num_tokens_from_messages(messages, tools=[get_weather])868    assert num_tokens > 0869870871class GetWeather(BaseModel):872    """Get the current weather in a given location."""873874    location: str = Field(..., description="The city and state, e.g. San Francisco, CA")875876877@pytest.mark.parametrize("tool_choice", ["GetWeather", "auto", "any"])878def test_anthropic_bind_tools_tool_choice(tool_choice: str) -> None:879    chat_model = ChatAnthropic(880        model=MODEL_NAME,  # type: ignore[call-arg]881    )882    chat_model_with_tools = chat_model.bind_tools([GetWeather], tool_choice=tool_choice)883    response = chat_model_with_tools.invoke("what's the weather in ny and la")884    assert isinstance(response, AIMessage)885886887def test_pdf_document_input() -> None:888    url = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"889    data = b64encode(requests.get(url, timeout=10).content).decode()890891    result = ChatAnthropic(model=MODEL_NAME).invoke(  # type: ignore[call-arg]892        [893            HumanMessage(894                [895                    "summarize this document",896                    {897                        "type": "document",898                        "source": {899                            "type": "base64",900                            "data": data,901                            "media_type": "application/pdf",902                        },903                    },904                ],905            ),906        ],907    )908    assert isinstance(result, AIMessage)909    assert isinstance(result.content, str)910    assert len(result.content) > 0911912913@pytest.mark.default_cassette("test_agent_loop.yaml.gz")914@pytest.mark.vcr915@pytest.mark.parametrize("output_version", ["v0", "v1"])916def test_agent_loop(output_version: Literal["v0", "v1"]) -> None:917    @tool918    def get_weather(location: str) -> str:919        """Get the weather for a location."""920        return "It's sunny."921922    llm = ChatAnthropic(model=MODEL_NAME, output_version=output_version)  # type: ignore[call-arg]923    llm_with_tools = llm.bind_tools([get_weather])924    input_message = HumanMessage("What is the weather in San Francisco, CA?")925    tool_call_message = llm_with_tools.invoke([input_message])926    assert isinstance(tool_call_message, AIMessage)927    tool_calls = tool_call_message.tool_calls928    assert len(tool_calls) == 1929    tool_call = tool_calls[0]930    tool_message = get_weather.invoke(tool_call)931    assert isinstance(tool_message, ToolMessage)932    response = llm_with_tools.invoke(933        [934            input_message,935            tool_call_message,936            tool_message,937        ]938    )939    assert isinstance(response, AIMessage)940941942@pytest.mark.default_cassette("test_agent_loop_streaming.yaml.gz")943@pytest.mark.vcr944@pytest.mark.parametrize(945    ("output_version", "use_v2_stream"),946    [947        ("v0", False),948        ("v1", False),949        ("v1", True),950    ],951)952def test_agent_loop_streaming(953    output_version: Literal["v0", "v1"], *, use_v2_stream: bool954) -> None:955    @tool956    def get_weather(location: str) -> str:957        """Get the weather for a location."""958        return "It's sunny."959960    llm = ChatAnthropic(961        model=MODEL_NAME,962        streaming=True,963        output_version=output_version,  # type: ignore[call-arg]964    )965    llm_with_tools = llm.bind_tools([get_weather])966    input_message = HumanMessage("What is the weather in San Francisco, CA?")967    if use_v2_stream:968        tool_call_message = cast(969            "ChatModelStream",970            llm_with_tools.stream_events([input_message], version="v3"),971        ).output972    else:973        tool_call_message = llm_with_tools.invoke([input_message])974    assert isinstance(tool_call_message, AIMessage)975976    tool_calls = tool_call_message.tool_calls977    assert len(tool_calls) == 1978    tool_call = tool_calls[0]979    tool_message = get_weather.invoke(tool_call)980    assert isinstance(tool_message, ToolMessage)981    if use_v2_stream:982        response = cast(983            "ChatModelStream",984            llm_with_tools.stream_events(985                [input_message, tool_call_message, tool_message],986                version="v3",987            ),988        ).output989    else:990        response = llm_with_tools.invoke(991            [992                input_message,993                tool_call_message,994                tool_message,995            ]996        )997    assert isinstance(response, AIMessage)9989991000@pytest.mark.default_cassette("test_agent_loop_streaming.yaml.gz")1001@pytest.mark.vcr1002async def test_agent_loop_streaming_astream_events_v3_v1() -> None:1003    """Async multi-turn through `astream_events(version="v3")`.10041005    Mirrors `test_agent_loop_streaming` for `output_version="v1"` but1006    exercises `AsyncChatModelStream` end-to-end.1007    """10081009    @tool1010    def get_weather(location: str) -> str:1011        """Get the weather for a location."""1012        return "It's sunny."10131014    llm = ChatAnthropic(1015        model=MODEL_NAME,1016        streaming=True,1017        output_version="v1",  # type: ignore[call-arg]1018    )1019    llm_with_tools = llm.bind_tools([get_weather])1020    input_message = HumanMessage("What is the weather in San Francisco, CA?")1021    tool_call_message = await (1022        await cast(1023            "Awaitable[AsyncChatModelStream]",1024            llm_with_tools.astream_events([input_message], version="v3"),1025        )1026    )1027    assert isinstance(tool_call_message, AIMessage)1028    tool_calls = tool_call_message.tool_calls1029    assert len(tool_calls) == 11030    tool_call = tool_calls[0]1031    tool_message = get_weather.invoke(tool_call)1032    assert isinstance(tool_message, ToolMessage)1033    response = await (1034        await cast(1035            "Awaitable[AsyncChatModelStream]",1036            llm_with_tools.astream_events(1037                [input_message, tool_call_message, tool_message],1038                version="v3",1039            ),1040        )1041    )1042    assert isinstance(response, AIMessage)104310441045@pytest.mark.default_cassette("test_citations.yaml.gz")1046@pytest.mark.vcr1047@pytest.mark.parametrize(1048    ("output_version", "use_v2_stream"),1049    [1050        ("v0", False),1051        ("v1", False),1052        ("v1", True),1053    ],1054)1055def test_citations(output_version: Literal["v0", "v1"], *, use_v2_stream: bool) -> None:1056    llm = ChatAnthropic(model=MODEL_NAME, output_version=output_version)  # type: ignore[call-arg]1057    messages = [1058        {1059            "role": "user",1060            "content": [1061                {1062                    "type": "document",1063                    "source": {1064                        "type": "content",1065                        "content": [1066                            {"type": "text", "text": "The grass is green"},1067                            {"type": "text", "text": "The sky is blue"},1068                        ],1069                    },1070                    "citations": {"enabled": True},1071                },1072                {"type": "text", "text": "What color is the grass and sky?"},1073            ],1074        },1075    ]1076    response = llm.invoke(messages)1077    assert isinstance(response, AIMessage)1078    assert isinstance(response.content, list)1079    if output_version == "v1":1080        assert any("annotations" in block for block in response.content)1081    else:1082        assert any("citations" in block for block in response.content)10831084    # Test streaming1085    full: BaseMessage1086    if use_v2_stream:1087        full = llm.stream_events(messages, version="v3").output1088    else:1089        aggregated: BaseMessageChunk | None = None1090        for chunk in llm.stream(messages):1091            aggregated = (1092                cast("BaseMessageChunk", chunk)1093                if aggregated is None1094                else aggregated + chunk1095            )1096        assert isinstance(aggregated, AIMessageChunk)1097        full = aggregated1098    assert isinstance(full.content, list)1099    assert not any("citation" in block for block in full.content)1100    if output_version == "v1":1101        assert any("annotations" in block for block in full.content)1102    else:1103        assert any("citations" in block for block in full.content)11041105    # Test pass back in1106    next_message = {1107        "role": "user",1108        "content": "Can you comment on the citations you just made?",1109    }1110    _ = llm.invoke([*messages, full, next_message])111111121113@pytest.mark.vcr1114def test_thinking() -> None:1115    llm = ChatAnthropic(1116        model="claude-sonnet-4-5-20250929",  # type: ignore[call-arg]1117        max_tokens=5_000,  # type: ignore[call-arg]1118        thinking={"type": "enabled", "budget_tokens": 2_000},1119    )11201121    input_message = {"role": "user", "content": "Hello"}1122    response = llm.invoke([input_message])1123    assert any("thinking" in block for block in response.content)1124    for block in response.content:1125        assert isinstance(block, dict)1126        if block["type"] == "thinking":1127            assert set(block.keys()) == {"type", "thinking", "signature"}1128            assert block["thinking"]1129            assert isinstance(block["thinking"], str)1130            assert block["signature"]1131            assert isinstance(block["signature"], str)11321133    # Test streaming1134    full: BaseMessageChunk | None = None1135    for chunk in llm.stream([input_message]):1136        full = cast("BaseMessageChunk", chunk) if full is None else full + chunk1137    assert isinstance(full, AIMessageChunk)1138    assert isinstance(full.content, list)1139    assert any("thinking" in block for block in full.content)1140    for block in full.content:1141        assert isinstance(block, dict)1142        if block["type"] == "thinking":1143            assert set(block.keys()) == {"type", "thinking", "signature", "index"}1144            assert block["thinking"]1145            assert isinstance(block["thinking"], str)1146            assert block["signature"]1147            assert isinstance(block["signature"], str)11481149    # Test pass back in1150    next_message = {"role": "user", "content": "How are you?"}1151    _ = llm.invoke([input_message, full, next_message])115211531154@pytest.mark.default_cassette("test_thinking.yaml.gz")1155@pytest.mark.vcr1156@pytest.mark.parametrize("use_v2_stream", [False, True])1157def test_thinking_v1(*, use_v2_stream: bool) -> None:1158    llm = ChatAnthropic(1159        model="claude-sonnet-4-5-20250929",  # type: ignore[call-arg]1160        max_tokens=5_000,  # type: ignore[call-arg]1161        thinking={"type": "enabled", "budget_tokens": 2_000},1162        output_version="v1",1163    )11641165    input_message = {"role": "user", "content": "Hello"}1166    response = llm.invoke([input_message])1167    assert any("reasoning" in block for block in response.content)1168    for block in response.content:1169        assert isinstance(block, dict)1170        if block["type"] == "reasoning":1171            assert set(block.keys()) == {"type", "reasoning", "extras"}1172            assert block["reasoning"]1173            assert isinstance(block["reasoning"], str)1174            signature = block["extras"]["signature"]1175            assert signature1176            assert isinstance(signature, str)11771178    # Test streaming1179    full: BaseMessage1180    if use_v2_stream:1181        full = llm.stream_events([input_message], version="v3").output1182    else:1183        aggregated: BaseMessageChunk | None = None1184        for chunk in llm.stream([input_message]):1185            aggregated = (1186                cast(BaseMessageChunk, chunk)1187                if aggregated is None1188                else aggregated + chunk1189            )1190        assert isinstance(aggregated, AIMessageChunk)1191        full = aggregated1192    assert isinstance(full.content, list)1193    assert any("reasoning" in block for block in full.content)1194    for block in full.content:1195        assert isinstance(block, dict)1196        if block["type"] == "reasoning":1197            assert set(block.keys()) == {"type", "reasoning", "extras", "index"}1198            assert block["reasoning"]1199            assert isinstance(block["reasoning"], str)1200            signature = block["extras"]["signature"]1201            assert signature1202            assert isinstance(signature, str)12031204    # Test pass back in1205    next_message = {"role": "user", "content": "How are you?"}1206    _ = llm.invoke([input_message, full, next_message])120712081209@pytest.mark.default_cassette("test_redacted_thinking.yaml.gz")1210@pytest.mark.vcr1211@pytest.mark.parametrize("output_version", ["v0", "v1"])1212def test_redacted_thinking(output_version: Literal["v0", "v1"]) -> None:1213    llm = ChatAnthropic(1214        # It appears that Sonnet 4.5 either: isn't returning redacted thinking blocks,1215        # or the magic string is broken? Retry later once 3-7 finally removed1216        model="claude-3-7-sonnet-latest",  # type: ignore[call-arg]1217        max_tokens=5_000,  # type: ignore[call-arg]1218        thinking={"type": "enabled", "budget_tokens": 2_000},1219        output_version=output_version,1220    )1221    query = "ANTHROPIC_MAGIC_STRING_TRIGGER_REDACTED_THINKING_46C9A13E193C177646C7398A98432ECCCE4C1253D5E2D82641AC0E52CC2876CB"  # noqa: E5011222    input_message = {"role": "user", "content": query}12231224    response = llm.invoke([input_message])1225    value = None1226    for block in response.content:1227        assert isinstance(block, dict)1228        if block["type"] == "redacted_thinking":1229            value = block1230        elif (1231            block["type"] == "non_standard"1232            and block["value"]["type"] == "redacted_thinking"1233        ):1234            value = block["value"]1235        else:1236            pass1237        if value:1238            assert set(value.keys()) == {"type", "data"}1239            assert value["data"]1240            assert isinstance(value["data"], str)1241    assert value is not None12421243    # Test streaming1244    full: BaseMessageChunk | None = None1245    for chunk in llm.stream([input_message]):1246        full = cast("BaseMessageChunk", chunk) if full is None else full + chunk1247    assert isinstance(full, AIMessageChunk)1248    assert isinstance(full.content, list)1249    value = None1250    for block in full.content:1251        assert isinstance(block, dict)1252        if block["type"] == "redacted_thinking":1253            value = block1254            assert set(value.keys()) == {"type", "data", "index"}1255            assert "index" in block1256        elif (1257            block["type"] == "non_standard"1258            and block["value"]["type"] == "redacted_thinking"1259        ):1260            value = block["value"]1261            assert isinstance(value, dict)1262            assert set(value.keys()) == {"type", "data"}1263            assert "index" in block1264        else:1265            pass1266        if value:1267            assert value["data"]1268            assert isinstance(value["data"], str)1269    assert value is not None12701271    # Test pass back in1272    next_message = {"role": "user", "content": "What?"}1273    _ = llm.invoke([input_message, full, next_message])127412751276def test_structured_output_thinking_enabled() -> None:1277    llm = ChatAnthropic(1278        model="claude-sonnet-4-5-20250929",  # type: ignore[call-arg]1279        max_tokens=5_000,  # type: ignore[call-arg]1280        thinking={"type": "enabled", "budget_tokens": 2_000},1281    )1282    with pytest.warns(match="structured output"):1283        structured_llm = llm.with_structured_output(GenerateUsername)1284    query = "Generate a username for Sally with green hair"1285    response = structured_llm.invoke(query)1286    assert isinstance(response, GenerateUsername)12871288    with pytest.raises(OutputParserException):1289        structured_llm.invoke("Hello")12901291    # Test streaming1292    for chunk in structured_llm.stream(query):1293        assert isinstance(chunk, GenerateUsername)129412951296@pytest.mark.retry(count=3, delay=1)1297def test_structured_output_thinking_force_tool_use() -> None:1298    # Structured output currently relies on forced tool use, which is not supported1299    # when `thinking` is enabled. When this test fails, it means that the feature1300    # is supported and the workarounds in `with_structured_output` should be removed.1301    # Use the client resolved off `ChatAnthropic` so requests honor any1302    # configured base URL / credentials (e.g. the LangSmith gateway).1303    client = ChatAnthropic(model="claude-sonnet-4-5-20250929")._client  # type: ignore[call-arg]1304    with pytest.raises(anthropic.BadRequestError):1305        _ = client.messages.create(1306            model="claude-sonnet-4-5-20250929",1307            max_tokens=5_000,1308            thinking={"type": "enabled", "budget_tokens": 2_000},1309            tool_choice={"type": "tool", "name": "get_weather"},1310            tools=[1311                {1312                    "name": "get_weather",1313                    "description": "Get the weather at a location.",1314                    "input_schema": {1315                        "type": "object",1316                        "properties": {1317                            "location": {"type": "string"},1318                        },1319                        "required": ["location"],1320                    },1321                }1322            ],1323            messages=[1324                {1325                    "role": "user",1326                    "content": "What's the weather in San Francisco?",1327                }1328            ],1329        )133013311332def test_effort_parameter() -> None:1333    """Test that effort parameter can be passed without errors.13341335    Only Opus 4.5 supports currently.1336    """1337    llm = ChatAnthropic(1338        model="claude-opus-4-5-20251101",1339        effort="medium",1340        max_tokens=100,1341    )13421343    result = llm.invoke("Say hello in one sentence")13441345    # Verify we got a response1346    assert isinstance(result.content, str)1347    assert len(result.content) > 013481349    # Verify response metadata is present1350    assert "model_name" in result.response_metadata1351    assert result.usage_metadata is not None1352    assert result.usage_metadata["input_tokens"] > 01353    assert result.usage_metadata["output_tokens"] > 0135413551356def test_reasoning_effort_parameter() -> None:1357    """Test that the standard `reasoning_effort` parameter is accepted by the API."""1358    llm = ChatAnthropic(1359        model="claude-opus-4-5-20251101",1360        reasoning_effort="medium",1361        max_tokens=100,1362    )13631364    result = llm.invoke("Say hello in one sentence")13651366    assert isinstance(result.content, str)1367    assert len(result.content) > 01368    assert "model_name" in result.response_metadata1369    assert result.usage_metadata is not None1370    assert result.usage_metadata["input_tokens"] > 01371    assert result.usage_metadata["output_tokens"] > 0137213731374def test_reasoning_effort_call_time_kwarg() -> None:1375    """Test that `reasoning_effort` is accepted as a call-time kwarg."""1376    llm = ChatAnthropic(model="claude-opus-4-5-20251101", max_tokens=100)13771378    result = llm.invoke("Say hello in one sentence", reasoning_effort="low")13791380    assert isinstance(result.content, str)1381    assert len(result.content) > 01382    assert result.usage_metadata is not None138313841385def test_reasoning_effort_defaults_adaptive_thinking() -> None:1386    """`reasoning_effort` defaults `thinking` to adaptive on models that support it.13871388    Regression test for a model (Opus 4.7+, Opus 5, Sonnet 5) actually accepting the1389    resulting `{"type": "adaptive", "display": "summarized"}` thinking config,1390    not just that the payload is well-formed locally.1391    """1392    llm = ChatAnthropic(1393        model="claude-opus-5",1394        reasoning_effort="high",1395        max_tokens=2_000,1396    )13971398    # A genuine multi-step problem: adaptive thinking is model-decided, and a1399    # trivial prompt (e.g. "what is 3+4") may not surface a visible `thinking`1400    # block even when accepted, which would make this test flaky.1401    result = llm.invoke(1402        "A farmer has 17 sheep. All but 9 die. Then he buys triple the number "1403        "of remaining sheep, then sells half (rounding down). How many sheep "1404        "does he have now? Show your reasoning step by step."1405    )14061407    assert isinstance(result.content, list)1408    assert any(1409        isinstance(block, dict) and block.get("type") == "thinking"1410        for block in result.content1411    )1412    assert result.usage_metadata is not None141314141415def test_image_tool_calling() -> None:1416    """Test tool calling with image inputs."""14171418    class color_picker(BaseModel):  # noqa: N8011419        """Input your fav color and get a random fact about it."""14201421        fav_color: str14221423    human_content: list[dict] = [1424        {1425            "type": "text",1426            "text": "what's your favorite color in this image",1427        },1428    ]1429    image_url = "https://raw.githubusercontent.com/langchain-ai/docs/4d11d08b6b0e210bd456943f7a22febbd168b543/src/images/agentic-rag-output.png"1430    image_data = b64encode(httpx.get(image_url, timeout=10.0).content).decode("utf-8")1431    human_content.append(1432        {1433            "type": "image",1434            "source": {1435                "type": "base64",1436                "media_type": "image/png",1437                "data": image_data,1438            },1439        },1440    )1441    messages = [1442        SystemMessage("you're a good assistant"),1443        HumanMessage(human_content),  # type: ignore[arg-type]1444        AIMessage(1445            [1446                {"type": "text", "text": "Hmm let me think about that"},1447                {1448                    "type": "tool_use",1449                    "input": {"fav_color": "purple"},1450                    "id": "foo",1451                    "name": "color_picker",1452                },1453            ],1454        ),1455        HumanMessage(1456            [1457                {1458                    "type": "tool_result",1459                    "tool_use_id": "foo",1460                    "content": [1461                        {1462                            "type": "text",1463                            "text": "purple is a great pick! that's my sister's favorite color",  # noqa: E5011464                        },1465                    ],1466                    "is_error": False,1467                },1468                {"type": "text", "text": "what's my sister's favorite color"},1469            ],1470        ),1471    ]1472    llm = ChatAnthropic(model=MODEL_NAME)  # type: ignore[call-arg]1473    _ = llm.bind_tools([color_picker]).invoke(messages)147414751476@pytest.mark.default_cassette("test_web_search.yaml.gz")1477@pytest.mark.vcr1478@pytest.mark.parametrize("output_version", ["v0", "v1"])1479def test_web_search(output_version: Literal["v0", "v1"]) -> None:1480    llm = ChatAnthropic(1481        model=MODEL_NAME,  # type: ignore[call-arg]1482        max_tokens=1024,1483        output_version=output_version,1484    )14851486    tool = {"type": "web_search_20250305", "name": "web_search", "max_uses": 1}1487    llm_with_tools = llm.bind_tools([tool])14881489    input_message = {1490        "role": "user",1491        "content": [1492            {1493                "type": "text",1494                "text": "How do I update a web app to TypeScript 5.5?",1495            },1496        ],1497    }1498    response = llm_with_tools.invoke([input_message])1499    assert all(isinstance(block, dict) for block in response.content)1500    block_types = {block["type"] for block in response.content}  # type: ignore[index]1501    if output_version == "v0":1502        assert block_types == {"text", "server_tool_use", "web_search_tool_result"}1503    else:1504        assert block_types == {"text", "server_tool_call", "server_tool_result"}15051506    # Test streaming1507    full: BaseMessageChunk | None = None1508    for chunk in llm_with_tools.stream([input_message]):1509        assert isinstance(chunk, AIMessageChunk)1510        full = chunk if full is None else full + chunk15111512    assert isinstance(full, AIMessageChunk)1513    assert isinstance(full.content, list)1514    block_types = {block["type"] for block in full.content}  # type: ignore[index]1515    if output_version == "v0":1516        assert block_types == {"text", "server_tool_use", "web_search_tool_result"}1517    else:1518        assert block_types == {"text", "server_tool_call", "server_tool_result"}15191520    # Test we can pass back in1521    next_message = {1522        "role": "user",1523        "content": "Please repeat the last search, but focus on sources from 2024.",1524    }1525    _ = llm_with_tools.invoke(1526        [input_message, full, next_message],1527    )152815291530@pytest.mark.vcr1531def test_web_fetch() -> None:1532    """Note: this is a beta feature.15331534    TODO: Update to remove beta once it's generally available.1535    """1536    llm = ChatAnthropic(1537        model=MODEL_NAME,  # type: ignore[call-arg]1538        max_tokens=1024,1539        betas=["web-fetch-2025-09-10"],1540    )1541    tool = {"type": "web_fetch_20250910", "name": "web_fetch", "max_uses": 1}1542    llm_with_tools = llm.bind_tools([tool])15431544    input_message = {1545        "role": "user",1546        "content": [1547            {1548                "type": "text",1549                "text": "Fetch the content at https://docs.langchain.com and analyze",1550            },1551        ],1552    }1553    response = llm_with_tools.invoke([input_message])1554    assert all(isinstance(block, dict) for block in response.content)1555    block_types = {1556        block["type"] for block in response.content if isinstance(block, dict)1557    }15581559    # A successful fetch call should include:1560    # 1. text response from the model (e.g. "I'll fetch that for you")1561    # 2. server_tool_use block indicating the tool was called (using tool "web_fetch")1562    # 3. web_fetch_tool_result block with the results of said fetch1563    assert block_types == {"text", "server_tool_use", "web_fetch_tool_result"}15641565    # Verify web fetch result structure1566    web_fetch_results = [1567        block1568        for block in response.content1569        if isinstance(block, dict) and block.get("type") == "web_fetch_tool_result"1570    ]1571    assert len(web_fetch_results) == 1  # Since max_uses=11572    fetch_result = web_fetch_results[0]1573    assert "content" in fetch_result1574    assert "url" in fetch_result["content"]1575    assert "retrieved_at" in fetch_result["content"]15761577    # Fetch with citations enabled1578    tool_with_citations = tool.copy()1579    tool_with_citations["citations"] = {"enabled": True}1580    llm_with_citations = llm.bind_tools([tool_with_citations])15811582    citation_message = {1583        "role": "user",1584        "content": (1585            "Fetch https://docs.langchain.com and provide specific quotes with "1586            "citations"1587        ),1588    }1589    citation_response = llm_with_citations.invoke([citation_message])15901591    citation_results = [1592        block1593        for block in citation_response.content1594        if isinstance(block, dict) and block.get("type") == "web_fetch_tool_result"1595    ]1596    assert len(citation_results) == 1  # Since max_uses=11597    citation_result = citation_results[0]1598    assert citation_result["content"]["content"]["citations"]["enabled"]1599    text_blocks = [1600        block1601        for block in citation_response.content1602        if isinstance(block, dict) and block.get("type") == "text"1603    ]16041605    # Check that the response contains actual citations in the content1606    has_citations = False1607    for block in text_blocks:1608        citations = block.get("citations", [])1609        for citation in citations:1610            if citation.get("type") and citation.get("start_char_index"):1611                has_citations = True1612                break1613    assert has_citations, (1614        "Expected inline citation tags in response when citations are enabled for "1615        "web fetch"1616    )16171618    # Max content tokens param1619    tool_with_limit = tool.copy()1620    tool_with_limit["max_content_tokens"] = 10001621    llm_with_limit = llm.bind_tools([tool_with_limit])16221623    limit_response = llm_with_limit.invoke([input_message])1624    # Response should still work even with content limits1625    assert any(1626        block["type"] == "web_fetch_tool_result"1627        for block in limit_response.content1628        if isinstance(block, dict)1629    )16301631    # Domains filtering (note: only one can be set at a time)1632    tool_with_allowed_domains = tool.copy()1633    tool_with_allowed_domains["allowed_domains"] = ["docs.langchain.com"]1634    llm_with_allowed = llm.bind_tools([tool_with_allowed_domains])16351636    allowed_response = llm_with_allowed.invoke([input_message])1637    assert any(1638        block["type"] == "web_fetch_tool_result"1639        for block in allowed_response.content1640        if isinstance(block, dict)1641    )16421643    # Test that a disallowed domain doesn't work1644    tool_with_disallowed_domains = tool.copy()1645    tool_with_disallowed_domains["allowed_domains"] = [1646        "example.com"1647    ]  # Not docs.langchain.com1648    llm_with_disallowed = llm.bind_tools([tool_with_disallowed_domains])16491650    disallowed_response = llm_with_disallowed.invoke([input_message])16511652    # We should get an error result since the domain (docs.langchain.com) is not allowed1653    disallowed_results = [1654        block1655        for block in disallowed_response.content1656        if isinstance(block, dict) and block.get("type") == "web_fetch_tool_result"1657    ]1658    if disallowed_results:1659        disallowed_result = disallowed_results[0]1660        if disallowed_result.get("content", {}).get("type") == "web_fetch_tool_error":1661            assert disallowed_result["content"]["error_code"] in [1662                "invalid_url",1663                "fetch_failed",1664            ]16651666    # Blocked domains filtering1667    tool_with_blocked_domains = tool.copy()1668    tool_with_blocked_domains["blocked_domains"] = ["example.com"]1669    llm_with_blocked = llm.bind_tools([tool_with_blocked_domains])16701671    blocked_response = llm_with_blocked.invoke([input_message])1672    assert any(1673        block["type"] == "web_fetch_tool_result"1674        for block in blocked_response.content1675        if isinstance(block, dict)1676    )16771678    # Test fetching from a blocked domain fails1679    blocked_domain_message = {1680        "role": "user",1681        "content": "Fetch https://example.com and analyze",1682    }1683    tool_with_blocked_example = tool.copy()1684    tool_with_blocked_example["blocked_domains"] = ["example.com"]1685    llm_with_blocked_example = llm.bind_tools([tool_with_blocked_example])16861687    blocked_domain_response = llm_with_blocked_example.invoke([blocked_domain_message])16881689    # Should get an error when trying to access a blocked domain1690    blocked_domain_results = [1691        block1692        for block in blocked_domain_response.content1693        if isinstance(block, dict) and block.get("type") == "web_fetch_tool_result"1694    ]1695    if blocked_domain_results:1696        blocked_result = blocked_domain_results[0]1697        if blocked_result.get("content", {}).get("type") == "web_fetch_tool_error":1698            assert blocked_result["content"]["error_code"] in [1699                "invalid_url",1700                "fetch_failed",1701            ]17021703    # Max uses parameter - test exceeding the limit1704    multi_fetch_message = {1705        "role": "user",1706        "content": (1707            "Fetch https://docs.langchain.com and then try to fetch "1708            "https://langchain.com"1709        ),1710    }1711    max_uses_response = llm_with_tools.invoke([multi_fetch_message])17121713    # Should contain at least one fetch result and potentially an error for the second1714    fetch_results = [1715        block1716        for block in max_uses_response.content1717        if isinstance(block, dict) and block.get("type") == "web_fetch_tool_result"1718    ]  # type: ignore[index]1719    assert len(fetch_results) >= 11720    error_results = [1721        r1722        for r in fetch_results1723        if r.get("content", {}).get("type") == "web_fetch_tool_error"1724    ]1725    if error_results:1726        assert any(1727            r["content"]["error_code"] == "max_uses_exceeded" for r in error_results1728        )17291730    # Streaming1731    full: BaseMessageChunk | None = None1732    for chunk in llm_with_tools.stream([input_message]):1733        assert isinstance(chunk, AIMessageChunk)1734        full = chunk if full is None else full + chunk1735    assert isinstance(full, AIMessageChunk)1736    assert isinstance(full.content, list)1737    block_types = {block["type"] for block in full.content if isinstance(block, dict)}1738    assert block_types == {"text", "server_tool_use", "web_fetch_tool_result"}17391740    # Test that URLs from context can be used in follow-up1741    next_message = {1742        "role": "user",1743        "content": "What does the site you just fetched say about models?",1744    }1745    follow_up_response = llm_with_tools.invoke(1746        [input_message, full, next_message],1747    )1748    # Should work without issues since URL was already in context1749    assert isinstance(follow_up_response.content, (list, str))17501751    # Error handling - test with an invalid URL format1752    error_message = {1753        "role": "user",1754        "content": "Try to fetch this invalid URL: not-a-valid-url",1755    }1756    error_response = llm_with_tools.invoke([error_message])17571758    # Should handle the error gracefully1759    assert isinstance(error_response.content, (list, str))17601761    # PDF document fetching1762    pdf_message = {1763        "role": "user",1764        "content": (1765            "Fetch this PDF: "1766            "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf "1767            "and summarize its content",1768        ),1769    }1770    pdf_response = llm_with_tools.invoke([pdf_message])17711772    assert any(1773        block["type"] == "web_fetch_tool_result"1774        for block in pdf_response.content1775        if isinstance(block, dict)1776    )17771778    # Verify PDF content structure (should have base64 data for PDFs)1779    pdf_results = [1780        block1781        for block in pdf_response.content1782        if isinstance(block, dict) and block.get("type") == "web_fetch_tool_result"1783    ]1784    if pdf_results:1785        pdf_result = pdf_results[0]1786        content = pdf_result.get("content", {})1787        if content.get("content", {}).get("source", {}).get("type") == "base64":1788            assert content["content"]["source"]["media_type"] == "application/pdf"1789            assert "data" in content["content"]["source"]179017911792@pytest.mark.default_cassette("test_web_fetch_v1.yaml.gz")1793@pytest.mark.vcr1794@pytest.mark.parametrize("output_version", ["v0", "v1"])1795def test_web_fetch_v1(output_version: Literal["v0", "v1"]) -> None:1796    """Test that http calls are unchanged between v0 and v1."""1797    llm = ChatAnthropic(1798        model=MODEL_NAME,  # type: ignore[call-arg]1799        betas=["web-fetch-2025-09-10"],1800        output_version=output_version,1801    )18021803    if output_version == "v0":1804        call_key = "server_tool_use"1805        result_key = "web_fetch_tool_result"1806    else:1807        # v11808        call_key = "server_tool_call"1809        result_key = "server_tool_result"18101811    tool = {1812        "type": "web_fetch_20250910",1813        "name": "web_fetch",1814        "max_uses": 1,1815        "citations": {"enabled": True},1816    }1817    llm_with_tools = llm.bind_tools([tool])18181819    input_message = {1820        "role": "user",1821        "content": [1822            {1823                "type": "text",1824                "text": "Fetch the content at https://docs.langchain.com and analyze",1825            },1826        ],1827    }1828    response = llm_with_tools.invoke([input_message])1829    assert all(isinstance(block, dict) for block in response.content)1830    block_types = {block["type"] for block in response.content}  # type: ignore[index]1831    assert block_types == {"text", call_key, result_key}18321833    # Test streaming1834    full: BaseMessageChunk | None = None1835    for chunk in llm_with_tools.stream([input_message]):1836        assert isinstance(chunk, AIMessageChunk)1837        full = chunk if full is None else full + chunk18381839    assert isinstance(full, AIMessageChunk)1840    assert isinstance(full.content, list)1841    block_types = {block["type"] for block in full.content}  # type: ignore[index]1842    assert block_types == {"text", call_key, result_key}18431844    # Test we can pass back in1845    next_message = {1846        "role": "user",1847        "content": "What does the site you just fetched say about models?",1848    }1849    _ = llm_with_tools.invoke(1850        [input_message, full, next_message],1851    )185218531854@pytest.mark.default_cassette("test_code_execution_old.yaml.gz")1855@pytest.mark.vcr1856@pytest.mark.parametrize("output_version", ["v0", "v1"])1857def test_code_execution_old(output_version: Literal["v0", "v1"]) -> None:1858    """Note: this tests the `code_execution_20250522` tool, which is now legacy.18591860    See the `test_code_execution` test below to test the current1861    `code_execution_20250825` tool.18621863    Migration guide: https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#upgrade-to-latest-tool-version1864    """1865    llm = ChatAnthropic(1866        model=MODEL_NAME,  # type: ignore[call-arg]1867        betas=["code-execution-2025-05-22"],1868        output_version=output_version,1869    )18701871    tool = {"type": "code_execution_20250522", "name": "code_execution"}1872    llm_with_tools = llm.bind_tools([tool])18731874    input_message = {1875        "role": "user",1876        "content": [1877            {1878                "type": "text",1879                "text": (1880                    "Calculate the mean and standard deviation of "1881                    "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"1882                ),1883            },1884        ],1885    }1886    response = llm_with_tools.invoke([input_message])1887    assert all(isinstance(block, dict) for block in response.content)1888    block_types = {block["type"] for block in response.content}  # type: ignore[index]1889    if output_version == "v0":1890        assert block_types == {"text", "server_tool_use", "code_execution_tool_result"}1891    else:1892        assert block_types == {"text", "server_tool_call", "server_tool_result"}18931894    # Test streaming1895    full: BaseMessageChunk | None = None1896    for chunk in llm_with_tools.stream([input_message]):1897        assert isinstance(chunk, AIMessageChunk)1898        full = chunk if full is None else full + chunk1899    assert isinstance(full, AIMessageChunk)1900    assert isinstance(full.content, list)1901    block_types = {block["type"] for block in full.content}  # type: ignore[index]1902    if output_version == "v0":1903        assert block_types == {"text", "server_tool_use", "code_execution_tool_result"}1904    else:1905        assert block_types == {"text", "server_tool_call", "server_tool_result"}19061907    # Test we can pass back in1908    next_message = {1909        "role": "user",1910        "content": "Please add more comments to the code.",1911    }1912    _ = llm_with_tools.invoke(1913        [input_message, full, next_message],1914    )191519161917def _collect_file_ids(content: Any) -> list[str]:1918    """Recursively collect `file_id` values from response content."""1919    if isinstance(content, dict):1920        found = [content["file_id"]] if "file_id" in content else []1921        return found + [fid for v in content.values() for fid in _collect_file_ids(v)]1922    if isinstance(content, list):1923        return [fid for item in content for fid in _collect_file_ids(item)]1924    return []192519261927@pytest.mark.default_cassette("test_code_execution.yaml.gz")1928@pytest.mark.vcr1929@pytest.mark.parametrize("output_version", ["v0", "v1"])1930def test_code_execution(output_version: Literal["v0", "v1"]) -> None:1931    llm = ChatAnthropic(1932        model=MODEL_NAME,  # type: ignore[call-arg]1933        output_version=output_version,1934    )19351936    tool = {"type": "code_execution_20250825", "name": "code_execution"}1937    llm_with_tools = llm.bind_tools([tool])19381939    input_message = {1940        "role": "user",1941        "content": [1942            {1943                "type": "text",1944                "text": (1945                    "Calculate the mean and standard deviation of "1946                    "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"1947                ),1948            },1949        ],1950    }1951    response = llm_with_tools.invoke([input_message])1952    assert all(isinstance(block, dict) for block in response.content)1953    block_types = {block["type"] for block in response.content}  # type: ignore[index]1954    if output_version == "v0":1955        assert block_types == {1956            "text",1957            "server_tool_use",1958            "bash_code_execution_tool_result",1959        }1960    else:1961        assert block_types == {"text", "server_tool_call", "server_tool_result"}19621963    # Test streaming1964    full: BaseMessageChunk | None = None1965    for chunk in llm_with_tools.stream([input_message]):1966        assert isinstance(chunk, AIMessageChunk)1967        full = chunk if full is None else full + chunk1968    assert isinstance(full, AIMessageChunk)1969    assert isinstance(full.content, list)1970    block_types = {block["type"] for block in full.content}  # type: ignore[index]1971    if output_version == "v0":1972        assert block_types == {1973            "text",1974            "server_tool_use",1975            "bash_code_execution_tool_result",1976        }1977    else:1978        assert block_types == {"text", "server_tool_call", "server_tool_result"}19791980    # Test we can pass back in1981    next_message = {1982        "role": "user",1983        "content": "Please add more comments to the code.",1984    }1985    _ = llm_with_tools.invoke(1986        [input_message, full, next_message],1987    )198819891990@pytest.mark.default_cassette("test_skills.yaml.gz")1991@pytest.mark.vcr1992@pytest.mark.parametrize("output_version", ["v0", "v1"])1993def test_skills(output_version: Literal["v0", "v1"]) -> None:1994    """Load an Anthropic skill into the code execution container."""1995    skills = [{"type": "anthropic", "skill_id": "xlsx"}]1996    code_execution = {"type": "code_execution_20250825", "name": "code_execution"}1997    llm = ChatAnthropic(1998        model=MODEL_NAME,  # type: ignore[call-arg]1999        container={"skills": skills},2000        reuse_last_container=True,

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.

Generated by searchcode.com · powered by code_get_file + code_get_findings