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.