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