"""Test ChatAnthropic chat model.""" from __future__ import annotations import asyncio import json import os from base64 import b64encode from typing import Literal, cast import httpx import pytest import requests from anthropic import BadRequestError from langchain_core.callbacks import CallbackManager from langchain_core.exceptions import OutputParserException from langchain_core.messages import ( AIMessage, AIMessageChunk, BaseMessage, BaseMessageChunk, HumanMessage, SystemMessage, ToolMessage, ) from langchain_core.outputs import ChatGeneration, LLMResult from langchain_core.prompts import ChatPromptTemplate from langchain_core.tools import tool from pydantic import BaseModel, Field from langchain_anthropic import ChatAnthropic from langchain_anthropic._compat import _convert_from_v1_to_anthropic from tests.unit_tests._utils import FakeCallbackHandler MODEL_NAME = "claude-3-5-haiku-latest" IMAGE_MODEL_NAME = "claude-3-5-haiku-latest" def test_stream() -> None: """Test streaming tokens from Anthropic.""" llm = ChatAnthropic(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg] full: BaseMessageChunk | None = None chunks_with_input_token_counts = 0 chunks_with_output_token_counts = 0 chunks_with_model_name = 0 for token in llm.stream("I'm Pickle Rick"): assert isinstance(token.content, str) full = cast("BaseMessageChunk", token) if full is None else full + token assert isinstance(token, AIMessageChunk) if token.usage_metadata is not None: if token.usage_metadata.get("input_tokens"): chunks_with_input_token_counts += 1 if token.usage_metadata.get("output_tokens"): chunks_with_output_token_counts += 1 chunks_with_model_name += int("model_name" in token.response_metadata) if chunks_with_input_token_counts != 1 or chunks_with_output_token_counts != 1: msg = ( "Expected exactly one chunk with input or output token counts. " "AIMessageChunk aggregation adds counts. Check that " "this is behaving properly." ) raise AssertionError( msg, ) assert chunks_with_model_name == 1 # check token usage is populated assert isinstance(full, AIMessageChunk) assert len(full.content_blocks) == 1 assert full.content_blocks[0]["type"] == "text" assert full.content_blocks[0]["text"] assert full.usage_metadata is not None assert full.usage_metadata["input_tokens"] > 0 assert full.usage_metadata["output_tokens"] > 0 assert full.usage_metadata["total_tokens"] > 0 assert ( full.usage_metadata["input_tokens"] + full.usage_metadata["output_tokens"] == full.usage_metadata["total_tokens"] ) assert "stop_reason" in full.response_metadata assert "stop_sequence" in full.response_metadata assert "model_name" in full.response_metadata async def test_astream() -> None: """Test streaming tokens from Anthropic.""" llm = ChatAnthropic(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg] full: BaseMessageChunk | None = None chunks_with_input_token_counts = 0 chunks_with_output_token_counts = 0 async for token in llm.astream("I'm Pickle Rick"): assert isinstance(token.content, str) full = cast("BaseMessageChunk", token) if full is None else full + token assert isinstance(token, AIMessageChunk) if token.usage_metadata is not None: if token.usage_metadata.get("input_tokens"): chunks_with_input_token_counts += 1 if token.usage_metadata.get("output_tokens"): chunks_with_output_token_counts += 1 if chunks_with_input_token_counts != 1 or chunks_with_output_token_counts != 1: msg = ( "Expected exactly one chunk with input or output token counts. " "AIMessageChunk aggregation adds counts. Check that " "this is behaving properly." ) raise AssertionError( msg, ) # check token usage is populated assert isinstance(full, AIMessageChunk) assert len(full.content_blocks) == 1 assert full.content_blocks[0]["type"] == "text" assert full.content_blocks[0]["text"] assert full.usage_metadata is not None assert full.usage_metadata["input_tokens"] > 0 assert full.usage_metadata["output_tokens"] > 0 assert full.usage_metadata["total_tokens"] > 0 assert ( full.usage_metadata["input_tokens"] + full.usage_metadata["output_tokens"] == full.usage_metadata["total_tokens"] ) assert "stop_reason" in full.response_metadata assert "stop_sequence" in full.response_metadata # Check expected raw API output async_client = llm._async_client params: dict = { "model": MODEL_NAME, "max_tokens": 1024, "messages": [{"role": "user", "content": "hi"}], "temperature": 0.0, } stream = await async_client.messages.create(**params, stream=True) async for event in stream: if event.type == "message_start": assert event.message.usage.input_tokens > 1 # Different models may report different initial output token counts # in the message_start event. Ensure it's a positive value. assert event.message.usage.output_tokens >= 1 elif event.type == "message_delta": assert event.usage.output_tokens >= 1 else: pass async def test_stream_usage() -> None: """Test usage metadata can be excluded.""" model = ChatAnthropic(model_name=MODEL_NAME, stream_usage=False) # type: ignore[call-arg] async for token in model.astream("hi"): assert isinstance(token, AIMessageChunk) assert token.usage_metadata is None async def test_stream_usage_override() -> None: # check we override with kwarg model = ChatAnthropic(model_name=MODEL_NAME) # type: ignore[call-arg] assert model.stream_usage async for token in model.astream("hi", stream_usage=False): assert isinstance(token, AIMessageChunk) assert token.usage_metadata is None async def test_abatch() -> None: """Test streaming tokens from ChatAnthropic.""" llm = ChatAnthropic(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg] result = await llm.abatch(["I'm Pickle Rick", "I'm not Pickle Rick"]) for token in result: assert isinstance(token.content, str) async def test_abatch_tags() -> None: """Test batch tokens from ChatAnthropic.""" llm = ChatAnthropic(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg] result = await llm.abatch( ["I'm Pickle Rick", "I'm not Pickle Rick"], config={"tags": ["foo"]}, ) for token in result: assert isinstance(token.content, str) async def test_async_tool_use() -> None: llm = ChatAnthropic( model=MODEL_NAME, # type: ignore[call-arg] ) llm_with_tools = llm.bind_tools( [ { "name": "get_weather", "description": "Get weather report for a city", "input_schema": { "type": "object", "properties": {"location": {"type": "string"}}, }, }, ], ) response = await llm_with_tools.ainvoke("what's the weather in san francisco, ca") assert isinstance(response, AIMessage) assert isinstance(response.content, list) assert isinstance(response.tool_calls, list) assert len(response.tool_calls) == 1 tool_call = response.tool_calls[0] assert tool_call["name"] == "get_weather" assert isinstance(tool_call["args"], dict) assert "location" in tool_call["args"] # Test streaming first = True chunks: list[BaseMessage | BaseMessageChunk] = [] async for chunk in llm_with_tools.astream( "what's the weather in san francisco, ca", ): chunks = [*chunks, chunk] if first: gathered = chunk first = False else: gathered = gathered + chunk # type: ignore[assignment] assert len(chunks) > 1 assert isinstance(gathered, AIMessageChunk) assert isinstance(gathered.tool_call_chunks, list) assert len(gathered.tool_call_chunks) == 1 tool_call_chunk = gathered.tool_call_chunks[0] assert tool_call_chunk["name"] == "get_weather" assert isinstance(tool_call_chunk["args"], str) assert "location" in json.loads(tool_call_chunk["args"]) def test_batch() -> None: """Test batch tokens from ChatAnthropic.""" llm = ChatAnthropic(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg] result = llm.batch(["I'm Pickle Rick", "I'm not Pickle Rick"]) for token in result: assert isinstance(token.content, str) async def test_ainvoke() -> None: """Test invoke tokens from ChatAnthropic.""" llm = ChatAnthropic(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg] result = await llm.ainvoke("I'm Pickle Rick", config={"tags": ["foo"]}) assert isinstance(result.content, str) assert "model_name" in result.response_metadata def test_invoke() -> None: """Test invoke tokens from ChatAnthropic.""" llm = ChatAnthropic(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg] result = llm.invoke("I'm Pickle Rick", config={"tags": ["foo"]}) assert isinstance(result.content, str) def test_system_invoke() -> None: """Test invoke tokens with a system message.""" llm = ChatAnthropic(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg] prompt = ChatPromptTemplate.from_messages( [ ( "system", "You are an expert cartographer. If asked, you are a cartographer. " "STAY IN CHARACTER", ), ("human", "Are you a mathematician?"), ], ) chain = prompt | llm result = chain.invoke({}) assert isinstance(result.content, str) def test_handle_empty_aimessage() -> None: # Anthropic can generate empty AIMessages, which are not valid unless in the last # message in a sequence. llm = ChatAnthropic(model=MODEL_NAME) messages = [ HumanMessage("Hello"), AIMessage([]), HumanMessage("My name is Bob."), ] _ = llm.invoke(messages) # Test tool call sequence llm_with_tools = llm.bind_tools( [ { "name": "get_weather", "description": "Get weather report for a city", "input_schema": { "type": "object", "properties": {"location": {"type": "string"}}, }, }, ], ) _ = llm_with_tools.invoke( [ HumanMessage("What's the weather in Boston?"), AIMessage( content=[], tool_calls=[ { "name": "get_weather", "args": {"location": "Boston"}, "id": "toolu_01V6d6W32QGGSmQm4BT98EKk", "type": "tool_call", }, ], ), ToolMessage( content="It's sunny.", tool_call_id="toolu_01V6d6W32QGGSmQm4BT98EKk" ), AIMessage([]), HumanMessage("Thanks!"), ] ) def test_anthropic_call() -> None: """Test valid call to anthropic.""" chat = ChatAnthropic(model=MODEL_NAME) # type: ignore[call-arg] message = HumanMessage(content="Hello") response = chat.invoke([message]) assert isinstance(response, AIMessage) assert isinstance(response.content, str) def test_anthropic_generate() -> None: """Test generate method of anthropic.""" chat = ChatAnthropic(model=MODEL_NAME) # type: ignore[call-arg] chat_messages: list[list[BaseMessage]] = [ [HumanMessage(content="How many toes do dogs have?")], ] messages_copy = [messages.copy() for messages in chat_messages] result: LLMResult = chat.generate(chat_messages) assert isinstance(result, LLMResult) for response in result.generations[0]: assert isinstance(response, ChatGeneration) assert isinstance(response.text, str) assert response.text == response.message.content assert chat_messages == messages_copy def test_anthropic_streaming() -> None: """Test streaming tokens from anthropic.""" chat = ChatAnthropic(model=MODEL_NAME) # type: ignore[call-arg] message = HumanMessage(content="Hello") response = chat.stream([message]) for token in response: assert isinstance(token, AIMessageChunk) assert isinstance(token.content, str) def test_anthropic_streaming_callback() -> None: """Test that streaming correctly invokes on_llm_new_token callback.""" callback_handler = FakeCallbackHandler() callback_manager = CallbackManager([callback_handler]) chat = ChatAnthropic( model=MODEL_NAME, # type: ignore[call-arg] callbacks=callback_manager, verbose=True, ) message = HumanMessage(content="Write me a sentence with 10 words.") for token in chat.stream([message]): assert isinstance(token, AIMessageChunk) assert isinstance(token.content, str) assert callback_handler.llm_streams > 1 async def test_anthropic_async_streaming_callback() -> None: """Test that streaming correctly invokes on_llm_new_token callback.""" callback_handler = FakeCallbackHandler() callback_manager = CallbackManager([callback_handler]) chat = ChatAnthropic( model=MODEL_NAME, # type: ignore[call-arg] callbacks=callback_manager, verbose=True, ) chat_messages: list[BaseMessage] = [ HumanMessage(content="How many toes do dogs have?"), ] async for token in chat.astream(chat_messages): assert isinstance(token, AIMessageChunk) assert isinstance(token.content, str) assert callback_handler.llm_streams > 1 def test_anthropic_multimodal() -> None: """Test that multimodal inputs are handled correctly.""" chat = ChatAnthropic(model=IMAGE_MODEL_NAME) # type: ignore[call-arg] messages: list[BaseMessage] = [ HumanMessage( content=[ { "type": "image_url", "image_url": { # langchain logo "url": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAMCAggHCQgGCQgICAcICAgICAgICAYICAgHDAgHCAgICAgIBggICAgICAgICBYICAgICwkKCAgNDQoIDggICQgBAwQEBgUGCgYGCBALCg0QCg0NEA0KCg8LDQoKCgoLDgoQDQoLDQoKCg4NDQ0NDgsQDw0OCg4NDQ4NDQoJDg8OCP/AABEIALAAsAMBEQACEQEDEQH/xAAdAAEAAgEFAQAAAAAAAAAAAAAABwgJAQIEBQYD/8QANBAAAgIBAwIDBwQCAgIDAAAAAQIAAwQFERIIEwYhMQcUFyJVldQjQVGBcZEJMzJiFRYk/8QAGwEBAAMAAwEAAAAAAAAAAAAAAAQFBgEDBwL/xAA5EQACAQIDBQQJBAIBBQAAAAAAAQIDEQQhMQVBUWGREhRxgRMVIjJSU8HR8CNyobFCguEGJGKi4v/aAAwDAQACEQMRAD8ApfJplBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBAEAQBANl16qOTEKB6kkAD+z5Tkcj0On+z7Ub1FlOmanejeavj6dqV6kfsQ1OK4IP8AIM6pVYR1kuqJdLCV6qvCnJ/6v66nL+Ems/RNc+y63+BOvvFL411O/wBW4r5T6D4Saz9E1z7Lrf4Ed4pfGuo9W4r5T6D4Saz9E1z7Lrf4Ed4pfGuo9W4r5T6D4Saz9E1z7Lrf4Ed4pfGuo9W4r5T6D4Saz9E1z7Lrf4Ed4pfGuo9W4r5T6D4Saz9E1z7Lrf4Ed4pfGuo9W4r5T6D4Saz9E1z7Lrf4Ed4pfGuo9W4r5T6D4Saz9E1z7Lrf4Ed4pfGuo9W4r5T6HE1D2e6lQpsu0zU6EXzZ8jTtSoUD9yWuxUAA/kmdkasJaSXVHRVwlekrzpyX+r+mh56m9WHJSGU+hUgg/wBjynaRORvnAEAQBAEAQBAEAQCbennpVzfER95LHE0tX4tlsnJr2B2srw6yQLCpBQ3Me1W+4/VZLKlh4jFRo5ay4cPH7f0XWA2XUxft37MONs34ffRcy/Xsu6bdG0UK2Nh1tkAbHMyAt+Wx2HIi11/SDcQe3jrTXv6IJRVcRUqe88uC0Nxhdn0MMv0458XnJ+e7wVlyJPJkYsTSAIAgCAIAgCAIBqDAIx9qHTbo2tBmycOtcgjYZmOBRlqdjxJtQDuhdye3ette/qhkmliKlP3XlwehXYrZ9DEr9SOfFZS6rXwd1yKCdQ3Srm+HT7yGOXpbPxXLVOLUMTtXXmVgkVliQgvU9qx9h+kz11Ne4fFRrZaS4cfD7f2YfH7LqYT279qHHevH76PlvhKTClEAQBAEAQBAJp6WOn0+I80i7mumYnF8x1LIbSSe3iV2DYq13ElnQ8q6gdijWUuIeKxHoY5e89PuXWy8D3qp7S9iOvN/D9+XiZRNN06uiuvHqrSqmpFrqqrVUrrrUBUREUBVVVAAUAAATNNtu7PR4xUUoxVkskloktxyCZwfRj26jetHPtzrMXSM4Uabj7Vrfj10O2ZdsDbb3bqrCKEYmpeyED8Hs53LZVwvsPg4qN6kbt+OS8t5hdobYqOo44edorK6SzfmtFpz14H16f8Arkz6cmrD1e9crBvsFZy3ropvxC2yo7NTXXXbjhtuXcTmisz91hX2yr4KLjemrNbuPXeMDtuoqihiGnF/5ZJx55ZNceF76GQSUJuhAEAQBAEAhb239WWl+H391s7mXnbAnExu2WqUjdWyLHda6Qw2IXdrCCGFZX5pMo4WdXNZLiyoxm1KOFfZl7UuCtdeN2kvzcRB4d/5JMV7OOVpWRRSWAFmPk1ZTKN9uT1PRi+QHnsj2H12DHYGXLZzS9mV3zVvuVFL/qGDlapSaXFST6qyfS/3tb4M8a4up49WoYlyZGLcCUsTf1B2ZGVgHrsRgVNbqrIwIYAjaVc4Sg+zJWZqaVWFWCnB3T0/PodnqOnV312Y9taW02o1dtViq9dlbAq6OjAqyspIKkEEGfKbTuj7lFSTjJXTyaejXAxd9U/T6fDmYBTzbTMvm+G7FnNRBHcxLLDuWankCrueVlRG5dq7nOlwuI9NHP3lr9zzjamA7rU9n3Jacn8P25eBC0mFKIAgCAIBtdwASfQDc/4nIbsZXulr2ZDR9HwsYpxybqxmZe4Xl71cquyMR69hO3jg+fy0r5n1OWxNX0lRvdovBflz1DZuG7vh4xtZtXl+55vpp5EsyKWZ5X2seH783TdRwsZgmVk4OVRQzMUUXPRYle7gEoCxA5gEqDvsdp2U5KM03omv7I+Ig6lKUIuzaaXmigPtb6HNQ0bEytTGXjZeLiKlhWuu6rINPMLbY1bFqkXHQ908b7CyK+wUqFe+pY2FSSjZpvnl+MwmJ2JVw9OVTtqUYq+Sadt+WaVtd9+W+uLLv5HzB8j/AIlgZ8yRdGfUXXq2JXpGTZtquFUE+cnfMxU2Wu9CzEvaicEsG+/MdzYLbsmexmHdOXaS9l/w+H2PQ9kY9V6apyftxVtdUtJc3x58iykrjQCAIAgFdurzqbPh+lMHFKHVspC6FuLLh427Icp0O4d2ZWREb5WZLGbktJrssMJhvSu8vdX8vh9zP7X2i8LBRp27b46Rj8Vt73JebyVnCfSz0jNqh/8AsGsrZZRcxuoxrms7ua7HmcvLYkOaXJ5Ctjvkb8n/AE+K3TcVi+x+nS6rdyX33eJTbL2S636+JTaeaTveTf8AlLlwjv35ZFmfHnSnoWo47Yo0/FxLOBWnJw8ejHuobb5GVqkUOqnY9qwOjDyI9CKyGKqwd+03ybdjS19mYarHs+jSe5pJNdP6KudBPiTIwNYz/D1jA1WJk91AWKLqGJctDWVg+QFlfdQtsGcVY+//AFgSzx0VKmqi5dJK/wCeZm9iVJ0sRPDye6WWdu1BpXWeV78M8uGd/wCURuCJuqX2YjWNHzMYJyyaKzmYm3Hl71SrOqKW8h307mOT5fLc3mPUSsNV9HUT3aPwf5crNpYbvGHlG2azj+5Zrrp5mKFHBAI9CNx/iak8vTubpwBAEAQDtPCekLk5WHiON0yczFx3H8pbkVVMP7VyJ8zfZi3wTfRHdRh26kI8ZRXk5IzREf6mPPXTSAIB1/iPQa8yjIwrVD05NFuPYrAFWrsrat1YHyIKsRsf2nMXZpo+ZR7UXF77rqYW2xHrJqsHG2smu1T6rapKWKf8OCP6mxvfNHj1nH2XqsnfW6yOVpGr241teVRY9ORS4sqtrPF67B6Mp/2NiCGBIIYMQeGlJWaujsp1JU5KcHZrQyZdK/U3X4ipONdwq1fGQNkVL5JkVbhfe8cE/wDgWKq1e5NFjKD8ttLPm8ThnSd17r0+35qej7N2hHFQs8prVfVcv6J4kIuBAKtdWnV8uj89I090fVeP/wCi8hXq05CvIcg26PmMpDCpgVqUrZaCGqrussLhPSe3P3f7/wCOf4s9tTaXd16On77/APXn48EU58OYl+RremrrRyHbJzdPbI9+LvZZjW21vUlgs5FMe4OqmshVrrscca9jtcSaVKXotydrcVr58zH04znioLFXd3G/a17L08E3u5vJEveGeobX/Cuq2YmttbbjX3NflUu7ZC1VW2OTlaZZuzDHrIbbGXZOFbV9qmwfLElh6Venelqsl4rc+fP6FtT2hicHiHDEu8W7u+ii8lKObtHL3fH/AC1tn1AdReJ4exVvJW/MyEJwcVWG9x2G1zkb8MVNwTbt83kqhmYCVVDDyqytot7/ADeanG46GFh2nm37q4/8c/qVr/4/fZ9k5Obm+J7+Xa430V2soVcrNuuW3LtT+RQUNZKjj3L2QHlRYqWOPqJRVJcvJJWRnth4epKpLE1FqnZ8XJ3b8MuG/LQvdKQ2ZqB/qAYXfFmkLjZWZiINkxszKx0H8JVkW1KP6VAJsIPtRT4pPqjyKtDsVJx4SkvJSdjq59HSIAgCAdp4T1dcbKw8tzsmNmYuQ5/hKsiq1j/SoTPma7UWuKa6o7qM+xUhLhKL8lJXM0RP+pjz100gCAIBjA6x/Y9ZpGq35KofcdSssy8ewA8Vvcl8rHJ3OzrazXAeQNVq8d+3Zx0mDrKpTS3rLy3P6HnG18I6FdzS9mWa/c9V9fPkQTJxRnf+AfHeRpOXj6pjHa/GsDhd+K2p6W0WHY/p31lqidiVDchsyqR8VIKpFxlo/wAv5EjD15UKiqw1X8revMy++DfFtOo4uNqNDcsfKprvrJ8iFZQeLD1Dod0KnzVlI/aZKcXCTi9UerUqkasFOLumk14M8T1L+0uzRdHzdRp8skKlGO2wPC+6xKUt2PkezzN3E7g8NtjvO7D01UqKL03+CzIe0MQ8Ph5VI66Lxbsv7Ks9D3ThTqG/iXOBvSvJsGHTae4L8lWDXZ2QzMzXMt7MoWzzNyW2PzPaYWeNxDj+nDLLPw4dPsZ7Y+CVb/ua3tO7tfitZPzyS5XJS6zOlu3XAmrYSh9Rpq7N2OzKozMYF3RUZyEXIqZ325lVtVyrMOFUjYPEql7MtP6f2J+1tmvE2qU/fWWusfo1/P8AVWfbjruoWabpFGrl/wD5Wq/UOyMhO3mV6QFxaU98BCuzW5dNxW2wcraqeZawku1pQjFVJOn7uWmna1y8uhmMdUqOhSjiPfTlr73o0rXfi1k96V7nq/YP0n6lr99OdqgysfS6qqKw2QbK8rKx6kWrHxcdG2toxlrUA3lU+Q71c3ta+rpr4qFJONOzlnpom9/N8vpkTMBsyriZKeITUEla+rSyUbapLyvzeZkT0fR6saqvFprSmilFrqqrUJXXWo2VEUABVUDbYSgbbd3qbyMVFWSskcucH0ag/wCoBhd8WauuTlZmWh3TIzMrIQ/yluRbap/tXBmwguzFLgkuiPIq0+3UnLjKT8nJ2Orn0dIgCAIBtdAQQfQjY/4nIauZXulr2nDWNHw8kvyyaKxh5e/Hl71SqozsF8h307eQB5fLcvkPQZbE0vR1Gt2q8H+WPUNm4nvGHjK92spfuWT66+ZLMilmIAgHm/aL4ExtVxL9PyaVvptRtkb1WwA9uyths1dqNsRYhDKf39Z905uElKLszor0YVoOE1dP86mH7R/DORdi5OeKz2sI4iZZIKtU+Q11dPJSvl+rS1ZBIKsyDY7krrXJKSjxvbyzPKY0ZuMprSNlLim21p4rPh1t6fA9ieq34Ka1RhW5OA7XKbMcC6ypq7DU/doT9cLyBPNK7ECglmT0nW60FLsN2fPnnroSI4KvKl6aMLxz0zeTavbW3hfy3Wq/4+fbVQKbPDd9wW7vWZGnK2wW2l17l9FTehsS0W5PA/M62uV5CqzhV4+i7+kS5Px4/T8z02wcXHsvDyed24+DzaXg7u3PLLSderP2f3arombi0KXyEFWVVWBu1jU2pc1SD93sqWxAP3dlkHC1FCqm9NOuRd7ToOvhpwjrk14xadv4K7dEPU5gYOI2iZ+RXiql1l2Hk2fJjtVae5ZVbaSUrsW42WB7O2jpYqg8k+exxuGnKXbgr8eOWXmUGxtpUqdP0FV9m12m9Gm72/8AFp8dfEmb22dZmlaXjv7nk42pag4K0U49q3U1t5fqZV1LFErTfl2g4st/8VCjnZXDo4Oc37ScVvv9L/iLXG7Xo0IfpyU57kndeLa0X8vRcq59OnsAzPFWY3iTVmezBa3uMbQOWo2qdhSibcUwa+IrPEBSq9pB/wBjV2GIrxoR9HT1/r/6M/s7A1MbU7ziHeN75/5tbuUF/Oml28h0oDfCAIBE/VL7TRo+j5uSr8cm6s4eJtx5e9XKyK6hvJuwncyCPP5aW8j6GVhqXpKiW7V+C/LFZtLE93w8pXzeUf3PJdNfIxQIgAAHoBsP8TUnl6VjdOAIAgCAIBNPSx1BHw5mE3c20zL4JmIoZjUQT28uusblmp5EMiDlZUTsHaulDDxWH9NHL3lp9i62Xj+61Pa9yWvJ/F9+XgZRNN1Ku+uvIqsS2m1FsqtrZXrsrYBkdHUlWVlIIYEggzNNNOzPR4yUkpRd081bRp7zkTg+jUQCH9Q8FeJjnNdVrmImmPx/QfTKXuqAVOXa2ZeTO5tAe29hWq1bpeS8lKdLs2cH2v3Zfn5kVjpYr0t1VXY4djNaaZ+OumWpGh9j2vaVi6pp+NVpep4+ouxQXY9ZzMnKybbGy8rVbNsHENdKMdiot2Raa0pbtjud/pac5RlK6a4PJJaJasivD4inCcIdmSle11m3JttyeStn/RJ/sG8A6no2LgaTaultiY+MwuuxmzUyDlFue4rek1XGxmd3yWspLvuwoTnskevONSTkr58bafm7dxJuDpVaNONOXZsln2b6+evjv4I6jVejTRLMp9TqTLw8xrRkV24eVZT7vkcuZtorKvUjM25KMj1+Z2RdzOxYuoo9l2a5rVcOJGnsnDubqxTjLVOMmrPilnG/k1yJxrXYAbkkADkdtyf5OwA3Pr5AD+APSQi5K7e1zod0nVrnzanu07KtZnuOMK3x7rWO7WPjuNlsY7sWoenmzMzB2YtLCljZ012XmuevUoMVsWhXk5puEnra1m+Nnl0tffmeY8Df8dum49iXZmZkZ4Q79gImJjv/AALQj23Mv/qt6BvRuQJU9lTaE5K0Vb+X9iNQ2BRg71JOfKyUemb/AJ/gtXhYSVIlNaLXVWqpXWiqqIigBURVACqoAAUAAASrbvmzTpJKy0PtByIBx9R1KuiuzItsSqmpGsttsZUrrrUFnd3YhVVVBJYkAATlJt2R8ykopyk7JZtvRJbzF31T9QR8R5gNPNdMxOSYaMGQ2kkdzLsrOxVruICo45V1AbhGsuQaXC4f0Mc/eev2PONqY7vVT2fcjpzfxfbl4kLSYUogCAIAgCAIBNvTz1VZvh0+7FTl6Wz8mxGfi1DE72WYdhBFZYkuaGHasfc/os9lrQ8RhY1s9JcePj9/7LrAbUnhPYt2ocN68Pto+W+/fsv6ktG1oKuNmVrkEbnDyCKMtTsOQFTkd0LuB3KGtr39HMoquHqU/eWXFaG4wu0KGJX6cs+DykvJ6+KuuZJxEjFiaQBAEAQBAEAQBANQIBGHtR6ktG0UMuTmVtkAbjDxyt+Wx2PEGpG/SDcSO5kNTXv6uJJpYepV91ZcXoV2K2hQwy/UlnwWcn5bvF2XMoL1DdVWb4iPuwU4mlq/JcRX5NewO9dmZYABYVIDilR2q32P6rJXat7h8LGjnrLjw8Pv/Rh8ftSpi/Yt2YcL5vx+2i5kJSYUogCAIAgCAIAgCAbLqFYcWAZT6hgCD/R8pyOZ6HT/AGg6lQorp1PU6EXyVMfUdSoUD9gFpykAA/gCdUqUJaxXREuli69JWhUkv9n9Tl/FvWfreufetb/PnX3el8C6Hf6yxXzX1Hxb1n63rn3rW/z47vS+BdB6yxXzX1Hxb1n63rn3rW/z47vS+BdB6yxXzX1Hxb1n63rn3rW/z47vS+BdB6yxXzX1Hxb1n63rn3rW/wA+O70vgXQessV819R8W9Z+t65961v8+O70vgXQessV819R8W9Z+t65961v8+O70vgXQessV819R8W9Z+t65961v8+O70vgXQessV819Tiah7QdRvU13anqd6N5MmRqOpXqR+4K3ZTgg/wROyNKEdIrojoqYuvVVp1JP/Z/TU89TQqjioCgegAAA/oeU7SJzN84AgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgCAIAgH/9k=", # noqa: E501 }, }, {"type": "text", "text": "What is this a logo for?"}, ], ), ] response = chat.invoke(messages) assert isinstance(response, AIMessage) assert isinstance(response.content, str) num_tokens = chat.get_num_tokens_from_messages(messages) assert num_tokens > 0 def test_streaming() -> None: """Test streaming tokens from Anthropic.""" callback_handler = FakeCallbackHandler() callback_manager = CallbackManager([callback_handler]) llm = ChatAnthropic( # type: ignore[call-arg, call-arg] model_name=MODEL_NAME, streaming=True, callbacks=callback_manager, ) response = llm.generate([[HumanMessage(content="I'm Pickle Rick")]]) assert callback_handler.llm_streams > 0 assert isinstance(response, LLMResult) async def test_astreaming() -> None: """Test streaming tokens from Anthropic.""" callback_handler = FakeCallbackHandler() callback_manager = CallbackManager([callback_handler]) llm = ChatAnthropic( # type: ignore[call-arg, call-arg] model_name=MODEL_NAME, streaming=True, callbacks=callback_manager, ) response = await llm.agenerate([[HumanMessage(content="I'm Pickle Rick")]]) assert callback_handler.llm_streams > 0 assert isinstance(response, LLMResult) def test_tool_use() -> None: llm = ChatAnthropic( model="claude-3-7-sonnet-20250219", # type: ignore[call-arg] temperature=0, ) tool_definition = { "name": "get_weather", "description": "Get weather report for a city", "input_schema": { "type": "object", "properties": {"location": {"type": "string"}}, }, } llm_with_tools = llm.bind_tools([tool_definition]) query = "how are you? what's the weather in san francisco, ca" response = llm_with_tools.invoke(query) assert isinstance(response, AIMessage) assert isinstance(response.content, list) assert isinstance(response.tool_calls, list) assert len(response.tool_calls) == 1 tool_call = response.tool_calls[0] assert tool_call["name"] == "get_weather" assert isinstance(tool_call["args"], dict) assert "location" in tool_call["args"] content_blocks = response.content_blocks assert len(content_blocks) == 2 assert content_blocks[0]["type"] == "text" assert content_blocks[0]["text"] assert content_blocks[1]["type"] == "tool_call" assert content_blocks[1]["name"] == "get_weather" assert content_blocks[1]["args"] == tool_call["args"] # Test streaming llm = ChatAnthropic( model="claude-3-7-sonnet-20250219", # type: ignore[call-arg] temperature=0, # Add extra headers to also test token-efficient tools model_kwargs={ "extra_headers": {"anthropic-beta": "token-efficient-tools-2025-02-19"}, }, ) llm_with_tools = llm.bind_tools([tool_definition]) first = True chunks: list[BaseMessage | BaseMessageChunk] = [] for chunk in llm_with_tools.stream(query): chunks = [*chunks, chunk] if first: gathered = chunk first = False else: gathered = gathered + chunk # type: ignore[assignment] for block in chunk.content_blocks: assert block["type"] in ("text", "tool_call_chunk") assert len(chunks) > 1 assert isinstance(gathered.content, list) assert len(gathered.content) == 2 tool_use_block = None for content_block in gathered.content: assert isinstance(content_block, dict) if content_block["type"] == "tool_use": tool_use_block = content_block break assert tool_use_block is not None assert tool_use_block["name"] == "get_weather" assert "location" in json.loads(tool_use_block["partial_json"]) assert isinstance(gathered, AIMessageChunk) assert isinstance(gathered.tool_calls, list) assert len(gathered.tool_calls) == 1 tool_call = gathered.tool_calls[0] assert tool_call["name"] == "get_weather" assert isinstance(tool_call["args"], dict) assert "location" in tool_call["args"] assert tool_call["id"] is not None content_blocks = gathered.content_blocks assert len(content_blocks) == 2 assert content_blocks[0]["type"] == "text" assert content_blocks[0]["text"] assert content_blocks[1]["type"] == "tool_call" assert content_blocks[1]["name"] == "get_weather" assert content_blocks[1]["args"] # Testing token-efficient tools # https://docs.claude.com/en/docs/agents-and-tools/tool-use/token-efficient-tool-use assert gathered.usage_metadata assert response.usage_metadata assert ( gathered.usage_metadata["total_tokens"] < response.usage_metadata["total_tokens"] ) # Test passing response back to model stream = llm_with_tools.stream( [ query, gathered, ToolMessage(content="sunny and warm", tool_call_id=tool_call["id"]), ], ) chunks = [] first = True for chunk in stream: chunks = [*chunks, chunk] if first: gathered = chunk first = False else: gathered = gathered + chunk # type: ignore[assignment] assert len(chunks) > 1 def test_builtin_tools() -> None: llm = ChatAnthropic(model="claude-3-7-sonnet-20250219") # type: ignore[call-arg] tool = {"type": "text_editor_20250124", "name": "str_replace_editor"} llm_with_tools = llm.bind_tools([tool]) response = llm_with_tools.invoke( "There's a syntax error in my primes.py file. Can you help me fix it?", ) assert isinstance(response, AIMessage) assert response.tool_calls content_blocks = response.content_blocks assert len(content_blocks) == 2 assert content_blocks[0]["type"] == "text" assert content_blocks[0]["text"] assert content_blocks[1]["type"] == "tool_call" assert content_blocks[1]["name"] == "str_replace_editor" class GenerateUsername(BaseModel): """Get a username based on someone's name and hair color.""" name: str hair_color: str def test_disable_parallel_tool_calling() -> None: llm = ChatAnthropic(model="claude-sonnet-4-5-20250929") # type: ignore[call-arg] llm_with_tools = llm.bind_tools([GenerateUsername], parallel_tool_calls=False) result = llm_with_tools.invoke( "Use the GenerateUsername tool to generate user names for:\n\n" "Sally with green hair\n" "Bob with blue hair", ) assert isinstance(result, AIMessage) assert len(result.tool_calls) == 1 def test_anthropic_with_empty_text_block() -> None: """Anthropic SDK can return an empty text block.""" @tool def type_letter(letter: str) -> str: """Type the given letter.""" return "OK" model = ChatAnthropic(model="claude-3-opus-20240229", temperature=0).bind_tools( # type: ignore[call-arg] [type_letter], ) messages = [ SystemMessage( content="Repeat the given string using the provided tools. Do not write " "anything else or provide any explanations. For example, " "if the string is 'abc', you must print the " "letters 'a', 'b', and 'c' one at a time and in that order. ", ), HumanMessage(content="dog"), AIMessage( content=[ {"text": "", "type": "text"}, { "id": "toolu_01V6d6W32QGGSmQm4BT98EKk", "input": {"letter": "d"}, "name": "type_letter", "type": "tool_use", }, ], tool_calls=[ { "name": "type_letter", "args": {"letter": "d"}, "id": "toolu_01V6d6W32QGGSmQm4BT98EKk", "type": "tool_call", }, ], ), ToolMessage(content="OK", tool_call_id="toolu_01V6d6W32QGGSmQm4BT98EKk"), ] model.invoke(messages) def test_with_structured_output() -> None: llm = ChatAnthropic( model="claude-3-opus-20240229", # type: ignore[call-arg] ) structured_llm = llm.with_structured_output( { "name": "get_weather", "description": "Get weather report for a city", "input_schema": { "type": "object", "properties": {"location": {"type": "string"}}, }, }, ) response = structured_llm.invoke("what's the weather in san francisco, ca") assert isinstance(response, dict) assert response["location"] def test_get_num_tokens_from_messages() -> None: llm = ChatAnthropic(model="claude-sonnet-4-5-20250929") # type: ignore[call-arg] # Test simple case messages = [ SystemMessage(content="You are a scientist"), HumanMessage(content="Hello, Claude"), ] num_tokens = llm.get_num_tokens_from_messages(messages) assert num_tokens > 0 # Test tool use @tool(parse_docstring=True) def get_weather(location: str) -> str: """Get the current weather in a given location. Args: location: The city and state, e.g. San Francisco, CA """ return "Sunny" messages = [ HumanMessage(content="What's the weather like in San Francisco?"), ] num_tokens = llm.get_num_tokens_from_messages(messages, tools=[get_weather]) assert num_tokens > 0 messages = [ HumanMessage(content="What's the weather like in San Francisco?"), AIMessage( content=[ {"text": "Let's see.", "type": "text"}, { "id": "toolu_01V6d6W32QGGSmQm4BT98EKk", "input": {"location": "SF"}, "name": "get_weather", "type": "tool_use", }, ], tool_calls=[ { "name": "get_weather", "args": {"location": "SF"}, "id": "toolu_01V6d6W32QGGSmQm4BT98EKk", "type": "tool_call", }, ], ), ToolMessage(content="Sunny", tool_call_id="toolu_01V6d6W32QGGSmQm4BT98EKk"), ] num_tokens = llm.get_num_tokens_from_messages(messages, tools=[get_weather]) assert num_tokens > 0 class GetWeather(BaseModel): """Get the current weather in a given location.""" location: str = Field(..., description="The city and state, e.g. San Francisco, CA") @pytest.mark.parametrize("tool_choice", ["GetWeather", "auto", "any"]) def test_anthropic_bind_tools_tool_choice(tool_choice: str) -> None: chat_model = ChatAnthropic( model=MODEL_NAME, # type: ignore[call-arg] ) chat_model_with_tools = chat_model.bind_tools([GetWeather], tool_choice=tool_choice) response = chat_model_with_tools.invoke("what's the weather in ny and la") assert isinstance(response, AIMessage) def test_pdf_document_input() -> None: url = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf" data = b64encode(requests.get(url, timeout=10).content).decode() result = ChatAnthropic(model=IMAGE_MODEL_NAME).invoke( # type: ignore[call-arg] [ HumanMessage( [ "summarize this document", { "type": "document", "source": { "type": "base64", "data": data, "media_type": "application/pdf", }, }, ], ), ], ) assert isinstance(result, AIMessage) assert isinstance(result.content, str) assert len(result.content) > 0 @pytest.mark.default_cassette("test_agent_loop.yaml.gz") @pytest.mark.vcr @pytest.mark.parametrize("output_version", ["v0", "v1"]) def test_agent_loop(output_version: Literal["v0", "v1"]) -> None: @tool def get_weather(location: str) -> str: """Get the weather for a location.""" return "It's sunny." llm = ChatAnthropic(model="claude-3-5-haiku-latest", output_version=output_version) # type: ignore[call-arg] llm_with_tools = llm.bind_tools([get_weather]) input_message = HumanMessage("What is the weather in San Francisco, CA?") tool_call_message = llm_with_tools.invoke([input_message]) assert isinstance(tool_call_message, AIMessage) tool_calls = tool_call_message.tool_calls assert len(tool_calls) == 1 tool_call = tool_calls[0] tool_message = get_weather.invoke(tool_call) assert isinstance(tool_message, ToolMessage) response = llm_with_tools.invoke( [ input_message, tool_call_message, tool_message, ] ) assert isinstance(response, AIMessage) @pytest.mark.default_cassette("test_agent_loop_streaming.yaml.gz") @pytest.mark.vcr @pytest.mark.parametrize("output_version", ["v0", "v1"]) def test_agent_loop_streaming(output_version: Literal["v0", "v1"]) -> None: @tool def get_weather(location: str) -> str: """Get the weather for a location.""" return "It's sunny." llm = ChatAnthropic( model="claude-3-5-haiku-latest", streaming=True, output_version=output_version, # type: ignore[call-arg] ) llm_with_tools = llm.bind_tools([get_weather]) input_message = HumanMessage("What is the weather in San Francisco, CA?") tool_call_message = llm_with_tools.invoke([input_message]) assert isinstance(tool_call_message, AIMessage) tool_calls = tool_call_message.tool_calls assert len(tool_calls) == 1 tool_call = tool_calls[0] tool_message = get_weather.invoke(tool_call) assert isinstance(tool_message, ToolMessage) response = llm_with_tools.invoke( [ input_message, tool_call_message, tool_message, ] ) assert isinstance(response, AIMessage) @pytest.mark.default_cassette("test_citations.yaml.gz") @pytest.mark.vcr @pytest.mark.parametrize("output_version", ["v0", "v1"]) def test_citations(output_version: Literal["v0", "v1"]) -> None: llm = ChatAnthropic(model="claude-3-5-haiku-latest", output_version=output_version) # type: ignore[call-arg] messages = [ { "role": "user", "content": [ { "type": "document", "source": { "type": "content", "content": [ {"type": "text", "text": "The grass is green"}, {"type": "text", "text": "The sky is blue"}, ], }, "citations": {"enabled": True}, }, {"type": "text", "text": "What color is the grass and sky?"}, ], }, ] response = llm.invoke(messages) assert isinstance(response, AIMessage) assert isinstance(response.content, list) if output_version == "v1": assert any("annotations" in block for block in response.content) else: assert any("citations" in block for block in response.content) # Test streaming full: BaseMessageChunk | None = None for chunk in llm.stream(messages): full = cast("BaseMessageChunk", chunk) if full is None else full + chunk assert isinstance(full, AIMessageChunk) assert isinstance(full.content, list) assert not any("citation" in block for block in full.content) if output_version == "v1": assert any("annotations" in block for block in full.content) else: assert any("citations" in block for block in full.content) # Test pass back in next_message = { "role": "user", "content": "Can you comment on the citations you just made?", } _ = llm.invoke([*messages, full, next_message]) @pytest.mark.vcr def test_thinking() -> None: llm = ChatAnthropic( model="claude-3-7-sonnet-latest", # type: ignore[call-arg] max_tokens=5_000, # type: ignore[call-arg] thinking={"type": "enabled", "budget_tokens": 2_000}, ) input_message = {"role": "user", "content": "Hello"} response = llm.invoke([input_message]) assert any("thinking" in block for block in response.content) for block in response.content: assert isinstance(block, dict) if block["type"] == "thinking": assert set(block.keys()) == {"type", "thinking", "signature"} assert block["thinking"] assert isinstance(block["thinking"], str) assert block["signature"] assert isinstance(block["signature"], str) # Test streaming full: BaseMessageChunk | None = None for chunk in llm.stream([input_message]): full = cast("BaseMessageChunk", chunk) if full is None else full + chunk assert isinstance(full, AIMessageChunk) assert isinstance(full.content, list) assert any("thinking" in block for block in full.content) for block in full.content: assert isinstance(block, dict) if block["type"] == "thinking": assert set(block.keys()) == {"type", "thinking", "signature", "index"} assert block["thinking"] assert isinstance(block["thinking"], str) assert block["signature"] assert isinstance(block["signature"], str) # Test pass back in next_message = {"role": "user", "content": "How are you?"} _ = llm.invoke([input_message, full, next_message]) @pytest.mark.default_cassette("test_thinking.yaml.gz") @pytest.mark.vcr def test_thinking_v1() -> None: llm = ChatAnthropic( model="claude-3-7-sonnet-latest", # type: ignore[call-arg] max_tokens=5_000, # type: ignore[call-arg] thinking={"type": "enabled", "budget_tokens": 2_000}, output_version="v1", ) input_message = {"role": "user", "content": "Hello"} response = llm.invoke([input_message]) assert any("reasoning" in block for block in response.content) for block in response.content: assert isinstance(block, dict) if block["type"] == "reasoning": assert set(block.keys()) == {"type", "reasoning", "extras"} assert block["reasoning"] assert isinstance(block["reasoning"], str) signature = block["extras"]["signature"] assert signature assert isinstance(signature, str) # Test streaming full: BaseMessageChunk | None = None for chunk in llm.stream([input_message]): full = cast(BaseMessageChunk, chunk) if full is None else full + chunk assert isinstance(full, AIMessageChunk) assert isinstance(full.content, list) assert any("reasoning" in block for block in full.content) for block in full.content: assert isinstance(block, dict) if block["type"] == "reasoning": assert set(block.keys()) == {"type", "reasoning", "extras", "index"} assert block["reasoning"] assert isinstance(block["reasoning"], str) signature = block["extras"]["signature"] assert signature assert isinstance(signature, str) # Test pass back in next_message = {"role": "user", "content": "How are you?"} _ = llm.invoke([input_message, full, next_message]) @pytest.mark.default_cassette("test_redacted_thinking.yaml.gz") @pytest.mark.vcr @pytest.mark.parametrize("output_version", ["v0", "v1"]) def test_redacted_thinking(output_version: Literal["v0", "v1"]) -> None: llm = ChatAnthropic( model="claude-3-7-sonnet-latest", # type: ignore[call-arg] max_tokens=5_000, # type: ignore[call-arg] thinking={"type": "enabled", "budget_tokens": 2_000}, output_version=output_version, ) query = "ANTHROPIC_MAGIC_STRING_TRIGGER_REDACTED_THINKING_46C9A13E193C177646C7398A98432ECCCE4C1253D5E2D82641AC0E52CC2876CB" # noqa: E501 input_message = {"role": "user", "content": query} response = llm.invoke([input_message]) value = None for block in response.content: assert isinstance(block, dict) if block["type"] == "redacted_thinking": value = block elif ( block["type"] == "non_standard" and block["value"]["type"] == "redacted_thinking" ): value = block["value"] else: pass if value: assert set(value.keys()) == {"type", "data"} assert value["data"] assert isinstance(value["data"], str) assert value is not None # Test streaming full: BaseMessageChunk | None = None for chunk in llm.stream([input_message]): full = cast("BaseMessageChunk", chunk) if full is None else full + chunk assert isinstance(full, AIMessageChunk) assert isinstance(full.content, list) value = None for block in full.content: assert isinstance(block, dict) if block["type"] == "redacted_thinking": value = block assert set(value.keys()) == {"type", "data", "index"} assert "index" in block elif ( block["type"] == "non_standard" and block["value"]["type"] == "redacted_thinking" ): value = block["value"] assert isinstance(value, dict) assert set(value.keys()) == {"type", "data"} assert "index" in block else: pass if value: assert value["data"] assert isinstance(value["data"], str) assert value is not None # Test pass back in next_message = {"role": "user", "content": "What?"} _ = llm.invoke([input_message, full, next_message]) def test_structured_output_thinking_enabled() -> None: llm = ChatAnthropic( model="claude-3-7-sonnet-latest", # type: ignore[call-arg] max_tokens=5_000, # type: ignore[call-arg] thinking={"type": "enabled", "budget_tokens": 2_000}, ) with pytest.warns(match="structured output"): structured_llm = llm.with_structured_output(GenerateUsername) query = "Generate a username for Sally with green hair" response = structured_llm.invoke(query) assert isinstance(response, GenerateUsername) with pytest.raises(OutputParserException): structured_llm.invoke("Hello") # Test streaming for chunk in structured_llm.stream(query): assert isinstance(chunk, GenerateUsername) def test_structured_output_thinking_force_tool_use() -> None: # Structured output currently relies on forced tool use, which is not supported # when `thinking` is enabled. When this test fails, it means that the feature # is supported and the workarounds in `with_structured_output` should be removed. llm = ChatAnthropic( model="claude-3-7-sonnet-latest", # type: ignore[call-arg] max_tokens=5_000, # type: ignore[call-arg] thinking={"type": "enabled", "budget_tokens": 2_000}, ).bind_tools( [GenerateUsername], tool_choice="GenerateUsername", ) with pytest.raises(BadRequestError): llm.invoke("Generate a username for Sally with green hair") def test_image_tool_calling() -> None: """Test tool calling with image inputs.""" class color_picker(BaseModel): # noqa: N801 """Input your fav color and get a random fact about it.""" fav_color: str human_content: list[dict] = [ { "type": "text", "text": "what's your favorite color in this image", }, ] image_url = "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg" image_data = b64encode(httpx.get(image_url).content).decode("utf-8") human_content.append( { "type": "image", "source": { "type": "base64", "media_type": "image/jpeg", "data": image_data, }, }, ) messages = [ SystemMessage("you're a good assistant"), HumanMessage(human_content), # type: ignore[arg-type] AIMessage( [ {"type": "text", "text": "Hmm let me think about that"}, { "type": "tool_use", "input": {"fav_color": "green"}, "id": "foo", "name": "color_picker", }, ], ), HumanMessage( [ { "type": "tool_result", "tool_use_id": "foo", "content": [ { "type": "text", "text": "green is a great pick! that's my sister's favorite color", # noqa: E501 }, ], "is_error": False, }, {"type": "text", "text": "what's my sister's favorite color"}, ], ), ] llm = ChatAnthropic(model="claude-3-5-haiku-latest") # type: ignore[call-arg] llm.bind_tools([color_picker]).invoke(messages) @pytest.mark.default_cassette("test_web_search.yaml.gz") @pytest.mark.vcr @pytest.mark.parametrize("output_version", ["v0", "v1"]) def test_web_search(output_version: Literal["v0", "v1"]) -> None: llm = ChatAnthropic( model="claude-3-5-haiku-latest", # type: ignore[call-arg] max_tokens=1024, output_version=output_version, ) tool = {"type": "web_search_20250305", "name": "web_search", "max_uses": 1} llm_with_tools = llm.bind_tools([tool]) input_message = { "role": "user", "content": [ { "type": "text", "text": "How do I update a web app to TypeScript 5.5?", }, ], } response = llm_with_tools.invoke([input_message]) assert all(isinstance(block, dict) for block in response.content) block_types = {block["type"] for block in response.content} # type: ignore[index] if output_version == "v0": assert block_types == {"text", "server_tool_use", "web_search_tool_result"} else: assert block_types == {"text", "server_tool_call", "server_tool_result"} # Test streaming full: BaseMessageChunk | None = None for chunk in llm_with_tools.stream([input_message]): assert isinstance(chunk, AIMessageChunk) full = chunk if full is None else full + chunk assert isinstance(full, AIMessageChunk) assert isinstance(full.content, list) block_types = {block["type"] for block in full.content} # type: ignore[index] if output_version == "v0": assert block_types == {"text", "server_tool_use", "web_search_tool_result"} else: assert block_types == {"text", "server_tool_call", "server_tool_result"} # Test we can pass back in next_message = { "role": "user", "content": "Please repeat the last search, but focus on sources from 2024.", } _ = llm_with_tools.invoke( [input_message, full, next_message], ) @pytest.mark.default_cassette("test_web_fetch_v1.yaml.gz") @pytest.mark.vcr @pytest.mark.parametrize("output_version", ["v0", "v1"]) def test_web_fetch_v1(output_version: Literal["v0", "v1"]) -> None: """Test that http calls are unchanged between v0 and v1.""" llm = ChatAnthropic( model="claude-3-5-haiku-latest", # type: ignore[call-arg] betas=["web-fetch-2025-09-10"], output_version=output_version, ) if output_version == "v0": call_key = "server_tool_use" result_key = "web_fetch_tool_result" else: # v1 call_key = "server_tool_call" result_key = "server_tool_result" tool = { "type": "web_fetch_20250910", "name": "web_fetch", "max_uses": 1, "citations": {"enabled": True}, } llm_with_tools = llm.bind_tools([tool]) input_message = { "role": "user", "content": [ { "type": "text", "text": "Fetch the content at https://docs.langchain.com and analyze", }, ], } response = llm_with_tools.invoke([input_message]) assert all(isinstance(block, dict) for block in response.content) block_types = {block["type"] for block in response.content} # type: ignore[index] assert block_types == {"text", call_key, result_key} # Test streaming full: BaseMessageChunk | None = None for chunk in llm_with_tools.stream([input_message]): assert isinstance(chunk, AIMessageChunk) full = chunk if full is None else full + chunk assert isinstance(full, AIMessageChunk) assert isinstance(full.content, list) block_types = {block["type"] for block in full.content} # type: ignore[index] assert block_types == {"text", call_key, result_key} # Test we can pass back in next_message = { "role": "user", "content": "What does the site you just fetched say about models?", } _ = llm_with_tools.invoke( [input_message, full, next_message], ) @pytest.mark.vcr def test_web_fetch() -> None: """Note: this is a beta feature. TODO: Update to remove beta once it's generally available. """ llm = ChatAnthropic( model="claude-3-5-haiku-latest", # type: ignore[call-arg] max_tokens=1024, betas=["web-fetch-2025-09-10"], ) tool = {"type": "web_fetch_20250910", "name": "web_fetch", "max_uses": 1} llm_with_tools = llm.bind_tools([tool]) input_message = { "role": "user", "content": [ { "type": "text", "text": "Fetch the content at https://docs.langchain.com and analyze", }, ], } response = llm_with_tools.invoke([input_message]) assert all(isinstance(block, dict) for block in response.content) block_types = { block["type"] for block in response.content if isinstance(block, dict) } # A successful fetch call should include: # 1. text response from the model (e.g. "I'll fetch that for you") # 2. server_tool_use block indicating the tool was called (using tool "web_fetch") # 3. web_fetch_tool_result block with the results of said fetch assert block_types == {"text", "server_tool_use", "web_fetch_tool_result"} # Verify web fetch result structure web_fetch_results = [ block for block in response.content if isinstance(block, dict) and block.get("type") == "web_fetch_tool_result" ] assert len(web_fetch_results) == 1 # Since max_uses=1 fetch_result = web_fetch_results[0] assert "content" in fetch_result assert "url" in fetch_result["content"] assert "retrieved_at" in fetch_result["content"] # Fetch with citations enabled tool_with_citations = tool.copy() tool_with_citations["citations"] = {"enabled": True} llm_with_citations = llm.bind_tools([tool_with_citations]) citation_message = { "role": "user", "content": ( "Fetch https://docs.langchain.com and provide specific quotes with " "citations" ), } citation_response = llm_with_citations.invoke([citation_message]) citation_results = [ block for block in citation_response.content if isinstance(block, dict) and block.get("type") == "web_fetch_tool_result" ] assert len(citation_results) == 1 # Since max_uses=1 citation_result = citation_results[0] assert citation_result["content"]["content"]["citations"]["enabled"] text_blocks = [ block for block in citation_response.content if isinstance(block, dict) and block.get("type") == "text" ] # Check that the response contains actual citations in the content has_citations = False for block in text_blocks: citations = block.get("citations", []) for citation in citations: if citation.get("type") and citation.get("start_char_index"): has_citations = True break assert has_citations, ( "Expected inline citation tags in response when citations are enabled for " "web fetch" ) # Max content tokens param tool_with_limit = tool.copy() tool_with_limit["max_content_tokens"] = 1000 llm_with_limit = llm.bind_tools([tool_with_limit]) limit_response = llm_with_limit.invoke([input_message]) # Response should still work even with content limits assert any( block["type"] == "web_fetch_tool_result" for block in limit_response.content if isinstance(block, dict) ) # Domains filtering (note: only one can be set at a time) tool_with_allowed_domains = tool.copy() tool_with_allowed_domains["allowed_domains"] = ["docs.langchain.com"] llm_with_allowed = llm.bind_tools([tool_with_allowed_domains]) allowed_response = llm_with_allowed.invoke([input_message]) assert any( block["type"] == "web_fetch_tool_result" for block in allowed_response.content if isinstance(block, dict) ) # Test that a disallowed domain doesn't work tool_with_disallowed_domains = tool.copy() tool_with_disallowed_domains["allowed_domains"] = [ "example.com" ] # Not docs.langchain.com llm_with_disallowed = llm.bind_tools([tool_with_disallowed_domains]) disallowed_response = llm_with_disallowed.invoke([input_message]) # We should get an error result since the domain (docs.langchain.com) is not allowed disallowed_results = [ block for block in disallowed_response.content if isinstance(block, dict) and block.get("type") == "web_fetch_tool_result" ] if disallowed_results: disallowed_result = disallowed_results[0] if disallowed_result.get("content", {}).get("type") == "web_fetch_tool_error": assert disallowed_result["content"]["error_code"] in [ "invalid_url", "fetch_failed", ] # Blocked domains filtering tool_with_blocked_domains = tool.copy() tool_with_blocked_domains["blocked_domains"] = ["example.com"] llm_with_blocked = llm.bind_tools([tool_with_blocked_domains]) blocked_response = llm_with_blocked.invoke([input_message]) assert any( block["type"] == "web_fetch_tool_result" for block in blocked_response.content if isinstance(block, dict) ) # Test fetching from a blocked domain fails blocked_domain_message = { "role": "user", "content": "Fetch https://example.com and analyze", } tool_with_blocked_example = tool.copy() tool_with_blocked_example["blocked_domains"] = ["example.com"] llm_with_blocked_example = llm.bind_tools([tool_with_blocked_example]) blocked_domain_response = llm_with_blocked_example.invoke([blocked_domain_message]) # Should get an error when trying to access a blocked domain blocked_domain_results = [ block for block in blocked_domain_response.content if isinstance(block, dict) and block.get("type") == "web_fetch_tool_result" ] if blocked_domain_results: blocked_result = blocked_domain_results[0] if blocked_result.get("content", {}).get("type") == "web_fetch_tool_error": assert blocked_result["content"]["error_code"] in [ "invalid_url", "fetch_failed", ] # Max uses parameter - test exceeding the limit multi_fetch_message = { "role": "user", "content": ( "Fetch https://docs.langchain.com and then try to fetch " "https://langchain.com" ), } max_uses_response = llm_with_tools.invoke([multi_fetch_message]) # Should contain at least one fetch result and potentially an error for the second fetch_results = [ block for block in max_uses_response.content if isinstance(block, dict) and block.get("type") == "web_fetch_tool_result" ] # type: ignore[index] assert len(fetch_results) >= 1 error_results = [ r for r in fetch_results if r.get("content", {}).get("type") == "web_fetch_tool_error" ] if error_results: assert any( r["content"]["error_code"] == "max_uses_exceeded" for r in error_results ) # Streaming full: BaseMessageChunk | None = None for chunk in llm_with_tools.stream([input_message]): assert isinstance(chunk, AIMessageChunk) full = chunk if full is None else full + chunk assert isinstance(full, AIMessageChunk) assert isinstance(full.content, list) block_types = {block["type"] for block in full.content if isinstance(block, dict)} assert block_types == {"text", "server_tool_use", "web_fetch_tool_result"} # Test that URLs from context can be used in follow-up next_message = { "role": "user", "content": "What does the site you just fetched say about models?", } follow_up_response = llm_with_tools.invoke( [input_message, full, next_message], ) # Should work without issues since URL was already in context assert isinstance(follow_up_response.content, (list, str)) # Error handling - test with an invalid URL format error_message = { "role": "user", "content": "Try to fetch this invalid URL: not-a-valid-url", } error_response = llm_with_tools.invoke([error_message]) # Should handle the error gracefully assert isinstance(error_response.content, (list, str)) # PDF document fetching pdf_message = { "role": "user", "content": ( "Fetch this PDF: " "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf " "and summarize its content", ), } pdf_response = llm_with_tools.invoke([pdf_message]) assert any( block["type"] == "web_fetch_tool_result" for block in pdf_response.content if isinstance(block, dict) ) # Verify PDF content structure (should have base64 data for PDFs) pdf_results = [ block for block in pdf_response.content if isinstance(block, dict) and block.get("type") == "web_fetch_tool_result" ] if pdf_results: pdf_result = pdf_results[0] content = pdf_result.get("content", {}) if content.get("content", {}).get("source", {}).get("type") == "base64": assert content["content"]["source"]["media_type"] == "application/pdf" assert "data" in content["content"]["source"] @pytest.mark.vcr @pytest.mark.parametrize("output_version", ["v0", "v1"]) def test_code_execution(output_version: Literal["v0", "v1"]) -> None: """Note: this is a beta feature. TODO: Update to remove beta once generally available. """ llm = ChatAnthropic( model="claude-sonnet-4-20250514", # type: ignore[call-arg] betas=["code-execution-2025-05-22"], max_tokens=10_000, # type: ignore[call-arg] output_version=output_version, ) tool = {"type": "code_execution_20250522", "name": "code_execution"} llm_with_tools = llm.bind_tools([tool]) input_message = { "role": "user", "content": [ { "type": "text", "text": ( "Calculate the mean and standard deviation of " "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]" ), }, ], } response = llm_with_tools.invoke([input_message]) assert all(isinstance(block, dict) for block in response.content) block_types = {block["type"] for block in response.content} # type: ignore[index] if output_version == "v0": assert block_types == {"text", "server_tool_use", "code_execution_tool_result"} else: assert block_types == {"text", "server_tool_call", "server_tool_result"} # Test streaming full: BaseMessageChunk | None = None for chunk in llm_with_tools.stream([input_message]): assert isinstance(chunk, AIMessageChunk) full = chunk if full is None else full + chunk assert isinstance(full, AIMessageChunk) assert isinstance(full.content, list) block_types = {block["type"] for block in full.content} # type: ignore[index] if output_version == "v0": assert block_types == {"text", "server_tool_use", "code_execution_tool_result"} else: assert block_types == {"text", "server_tool_call", "server_tool_result"} # Test we can pass back in next_message = { "role": "user", "content": "Please add more comments to the code.", } _ = llm_with_tools.invoke( [input_message, full, next_message], ) @pytest.mark.default_cassette("test_remote_mcp.yaml.gz") @pytest.mark.vcr @pytest.mark.parametrize("output_version", ["v0", "v1"]) def test_remote_mcp(output_version: Literal["v0", "v1"]) -> None: """Note: this is a beta feature. TODO: Update to remove beta once generally available. """ mcp_servers = [ { "type": "url", "url": "https://mcp.deepwiki.com/mcp", "name": "deepwiki", "tool_configuration": {"enabled": True, "allowed_tools": ["ask_question"]}, "authorization_token": "PLACEHOLDER", }, ] llm = ChatAnthropic( model="claude-sonnet-4-20250514", # type: ignore[call-arg] betas=["mcp-client-2025-04-04"], mcp_servers=mcp_servers, max_tokens=10_000, # type: ignore[call-arg] output_version=output_version, ) input_message = { "role": "user", "content": [ { "type": "text", "text": ( "What transport protocols does the 2025-03-26 version of the MCP " "spec (modelcontextprotocol/modelcontextprotocol) support?" ), }, ], } response = llm.invoke([input_message]) assert all(isinstance(block, dict) for block in response.content) block_types = {block["type"] for block in response.content} # type: ignore[index] if output_version == "v0": assert block_types == {"text", "mcp_tool_use", "mcp_tool_result"} else: assert block_types == {"text", "server_tool_call", "server_tool_result"} # Test streaming full: BaseMessageChunk | None = None for chunk in llm.stream([input_message]): assert isinstance(chunk, AIMessageChunk) full = chunk if full is None else full + chunk assert isinstance(full, AIMessageChunk) assert isinstance(full.content, list) assert all(isinstance(block, dict) for block in full.content) block_types = {block["type"] for block in full.content} # type: ignore[index] if output_version == "v0": assert block_types == {"text", "mcp_tool_use", "mcp_tool_result"} else: assert block_types == {"text", "server_tool_call", "server_tool_result"} # Test we can pass back in next_message = { "role": "user", "content": "Please query the same tool again, but add 'please' to your query.", } _ = llm.invoke( [input_message, full, next_message], ) @pytest.mark.parametrize("block_format", ["anthropic", "standard"]) def test_files_api_image(block_format: str) -> None: """Note: this is a beta feature. TODO: Update to remove beta once generally available. """ image_file_id = os.getenv("ANTHROPIC_FILES_API_IMAGE_ID") if not image_file_id: pytest.skip() llm = ChatAnthropic( model="claude-sonnet-4-20250514", # type: ignore[call-arg] betas=["files-api-2025-04-14"], ) if block_format == "anthropic": block = { "type": "image", "source": { "type": "file", "file_id": image_file_id, }, } else: # standard block format block = { "type": "image", "file_id": image_file_id, } input_message = { "role": "user", "content": [ {"type": "text", "text": "Describe this image."}, block, ], } _ = llm.invoke([input_message]) @pytest.mark.parametrize("block_format", ["anthropic", "standard"]) def test_files_api_pdf(block_format: str) -> None: """Note: this is a beta feature. TODO: Update to remove beta once generally available. """ pdf_file_id = os.getenv("ANTHROPIC_FILES_API_PDF_ID") if not pdf_file_id: pytest.skip() llm = ChatAnthropic( model="claude-sonnet-4-20250514", # type: ignore[call-arg] betas=["files-api-2025-04-14"], ) if block_format == "anthropic": block = {"type": "document", "source": {"type": "file", "file_id": pdf_file_id}} else: # standard block format block = { "type": "file", "file_id": pdf_file_id, } input_message = { "role": "user", "content": [ {"type": "text", "text": "Describe this document."}, block, ], } _ = llm.invoke([input_message]) def test_search_result_tool_message() -> None: """Test that we can pass a search result tool message to the model.""" llm = ChatAnthropic( model="claude-3-5-haiku-latest", # type: ignore[call-arg] ) @tool def retrieval_tool(query: str) -> list[dict]: """Retrieve information from a knowledge base.""" return [ { "type": "search_result", "title": "Leave policy", "source": "HR Leave Policy 2025", "citations": {"enabled": True}, "content": [ { "type": "text", "text": ( "To request vacation days, submit a leave request form " "through the HR portal. Approval will be sent by email." ), }, ], }, ] tool_call = { "type": "tool_call", "name": "retrieval_tool", "args": {"query": "vacation days request process"}, "id": "toolu_abc123", } tool_message = retrieval_tool.invoke(tool_call) assert isinstance(tool_message, ToolMessage) assert isinstance(tool_message.content, list) messages = [ HumanMessage("How do I request vacation days?"), AIMessage( [{"type": "text", "text": "Let me look that up for you."}], tool_calls=[tool_call], ), tool_message, ] result = llm.invoke(messages) assert isinstance(result, AIMessage) assert isinstance(result.content, list) assert any("citations" in block for block in result.content) assert ( _convert_from_v1_to_anthropic(result.content_blocks, [], "anthropic") == result.content ) def test_search_result_top_level() -> None: llm = ChatAnthropic( model="claude-3-5-haiku-latest", # type: ignore[call-arg] ) input_message = HumanMessage( [ { "type": "search_result", "title": "Leave policy", "source": "HR Leave Policy 2025 - page 1", "citations": {"enabled": True}, "content": [ { "type": "text", "text": ( "To request vacation days, submit a leave request form " "through the HR portal. Approval will be sent by email." ), }, ], }, { "type": "search_result", "title": "Leave policy", "source": "HR Leave Policy 2025 - page 2", "citations": {"enabled": True}, "content": [ { "type": "text", "text": "Managers have 3 days to approve a request.", }, ], }, { "type": "text", "text": "How do I request vacation days?", }, ], ) result = llm.invoke([input_message]) assert isinstance(result, AIMessage) assert isinstance(result.content, list) assert any("citations" in block for block in result.content) assert ( _convert_from_v1_to_anthropic(result.content_blocks, [], "anthropic") == result.content ) def test_memory_tool() -> None: llm = ChatAnthropic( model="claude-sonnet-4-5", # type: ignore[call-arg] betas=["context-management-2025-06-27"], ) llm_with_tools = llm.bind_tools([{"type": "memory_20250818", "name": "memory"}]) response = llm_with_tools.invoke("What are my interests?") assert isinstance(response, AIMessage) assert response.tool_calls assert response.tool_calls[0]["name"] == "memory" @pytest.mark.vcr def test_context_management() -> None: # TODO: update example to trigger action llm = ChatAnthropic( model="claude-sonnet-4-5", # type: ignore[call-arg] betas=["context-management-2025-06-27"], context_management={ "edits": [ { "type": "clear_tool_uses_20250919", "trigger": {"type": "input_tokens", "value": 10}, "clear_at_least": {"type": "input_tokens", "value": 5}, } ] }, max_tokens=1024, # type: ignore[call-arg] ) llm_with_tools = llm.bind_tools( [{"type": "web_search_20250305", "name": "web_search"}] ) input_message = {"role": "user", "content": "Search for recent developments in AI"} response = llm_with_tools.invoke([input_message]) assert response.response_metadata.get("context_management") # Test streaming full: BaseMessageChunk | None = None for chunk in llm_with_tools.stream([input_message]): assert isinstance(chunk, AIMessageChunk) full = chunk if full is None else full + chunk assert isinstance(full, AIMessageChunk) assert full.response_metadata.get("context_management") def test_async_shared_client() -> None: llm = ChatAnthropic(model="claude-3-5-haiku-latest") # type: ignore[call-arg] _ = asyncio.run(llm.ainvoke("Hello")) _ = asyncio.run(llm.ainvoke("Hello"))