1"""Test chat model integration."""23from __future__ import annotations45import copy6import os7import warnings8from collections.abc import Callable9from typing import Any, Literal, cast10from unittest.mock import MagicMock, patch1112import anthropic13import pytest14from anthropic.types import Message, TextBlock, Usage15from blockbuster import blockbuster_ctx16from langchain_core.exceptions import ContextOverflowError17from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage18from langchain_core.runnables import RunnableBinding19from langchain_core.tools import BaseTool, tool20from langchain_core.tracers.base import BaseTracer21from langchain_core.tracers.schemas import Run22from pydantic import BaseModel, Field, SecretStr, ValidationError23from pytest import CaptureFixture, MonkeyPatch2425from langchain_anthropic import ChatAnthropic26from langchain_anthropic.chat_models import (27 _create_usage_metadata,28 _format_image,29 _format_messages,30 _is_builtin_tool,31 _merge_messages,32 _thinking_in_params,33 convert_to_anthropic_tool,34)3536os.environ["ANTHROPIC_API_KEY"] = "foo"3738MODEL_NAME = "claude-sonnet-4-5-20250929"394041def test_initialization() -> None:42 """Test chat model initialization."""43 for model in [44 ChatAnthropic(model_name=MODEL_NAME, api_key="xyz", timeout=2), # type: ignore[arg-type, call-arg]45 ChatAnthropic( # type: ignore[call-arg, call-arg, call-arg]46 model=MODEL_NAME,47 anthropic_api_key="xyz",48 default_request_timeout=2,49 base_url="https://api.anthropic.com",50 ),51 ]:52 assert model.model == MODEL_NAME53 assert cast("SecretStr", model.anthropic_api_key).get_secret_value() == "xyz"54 assert model.default_request_timeout == 2.055 assert model.anthropic_api_url == "https://api.anthropic.com"565758def test_user_agent_header_in_client_params() -> None:59 """Test that _client_params includes a User-Agent header."""60 llm = ChatAnthropic(model=MODEL_NAME, api_key="test-key") # type: ignore[arg-type]61 params = llm._client_params62 assert "default_headers" in params63 assert "User-Agent" in params["default_headers"]64 assert params["default_headers"]["User-Agent"].startswith("langchain-anthropic/")656667@pytest.mark.parametrize("async_api", [True, False])68def test_streaming_attribute_should_stream(async_api: bool) -> None: # noqa: FBT00169 llm = ChatAnthropic(model=MODEL_NAME, streaming=True)70 assert llm._should_stream(async_api=async_api)717273def test_anthropic_client_caching() -> None:74 """Test that the OpenAI client is cached."""75 llm1 = ChatAnthropic(model=MODEL_NAME)76 llm2 = ChatAnthropic(model=MODEL_NAME)77 assert llm1._client._client is llm2._client._client7879 llm3 = ChatAnthropic(model=MODEL_NAME, base_url="foo")80 assert llm1._client._client is not llm3._client._client8182 llm4 = ChatAnthropic(model=MODEL_NAME, timeout=None)83 assert llm1._client._client is llm4._client._client8485 llm5 = ChatAnthropic(model=MODEL_NAME, timeout=3)86 assert llm1._client._client is not llm5._client._client878889def test_anthropic_proxy_support() -> None:90 """Test that both sync and async clients support proxy configuration."""91 proxy_url = "http://proxy.example.com:8080"9293 # Test sync client with proxy94 llm_sync = ChatAnthropic(model=MODEL_NAME, anthropic_proxy=proxy_url)95 sync_client = llm_sync._client96 assert sync_client is not None9798 # Test async client with proxy - this should not raise TypeError99 async_client = llm_sync._async_client100 assert async_client is not None101102 # Test that clients with different proxy settings are not cached together103 llm_no_proxy = ChatAnthropic(model=MODEL_NAME)104 llm_with_proxy = ChatAnthropic(model=MODEL_NAME, anthropic_proxy=proxy_url)105106 # Different proxy settings should result in different cached clients107 assert llm_no_proxy._client._client is not llm_with_proxy._client._client108109110def test_anthropic_proxy_from_environment() -> None:111 """Test that proxy can be set from ANTHROPIC_PROXY environment variable."""112 proxy_url = "http://env-proxy.example.com:8080"113114 # Test with environment variable set115 with patch.dict(os.environ, {"ANTHROPIC_PROXY": proxy_url}):116 llm = ChatAnthropic(model=MODEL_NAME)117 assert llm.anthropic_proxy == proxy_url118119 # Should be able to create clients successfully120 sync_client = llm._client121 async_client = llm._async_client122 assert sync_client is not None123 assert async_client is not None124125 # Test that explicit parameter overrides environment variable126 with patch.dict(os.environ, {"ANTHROPIC_PROXY": "http://env-proxy.com"}):127 explicit_proxy = "http://explicit-proxy.com"128 llm = ChatAnthropic(model=MODEL_NAME, anthropic_proxy=explicit_proxy)129 assert llm.anthropic_proxy == explicit_proxy130131132def test_set_default_max_tokens() -> None:133 """Test the set_default_max_tokens function."""134 # Test claude-sonnet-4-5 models135 llm = ChatAnthropic(model="claude-sonnet-4-5-20250929", anthropic_api_key="test")136 assert llm.max_tokens == 64000137138 # Test claude-opus-4 models139 llm = ChatAnthropic(model="claude-opus-4-20250514", anthropic_api_key="test")140 assert llm.max_tokens == 32000141142 # Test claude-sonnet-4 models143 llm = ChatAnthropic(model="claude-sonnet-4-20250514", anthropic_api_key="test")144 assert llm.max_tokens == 64000145146 # Test claude-3-7-sonnet models147 llm = ChatAnthropic(model="claude-3-7-sonnet-20250219", anthropic_api_key="test")148 assert llm.max_tokens == 64000149150 # Test claude-3-5-haiku models151 llm = ChatAnthropic(model="claude-3-5-haiku-20241022", anthropic_api_key="test")152 assert llm.max_tokens == 8192153154 # Test claude-3-haiku models (should default to 4096)155 llm = ChatAnthropic(model="claude-3-haiku-20240307", anthropic_api_key="test")156 assert llm.max_tokens == 4096157158 # Test that existing max_tokens values are preserved159 llm = ChatAnthropic(model=MODEL_NAME, max_tokens=2048, anthropic_api_key="test")160 assert llm.max_tokens == 2048161162 # Test that explicitly set max_tokens values are preserved163 llm = ChatAnthropic(model=MODEL_NAME, max_tokens=4096, anthropic_api_key="test")164 assert llm.max_tokens == 4096165166167@pytest.mark.requires("anthropic")168def test_anthropic_model_name_param() -> None:169 llm = ChatAnthropic(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]170 assert llm.model == MODEL_NAME171172173@pytest.mark.requires("anthropic")174def test_anthropic_model_param() -> None:175 llm = ChatAnthropic(model=MODEL_NAME) # type: ignore[call-arg]176 assert llm.model == MODEL_NAME177178179@pytest.mark.requires("anthropic")180def test_anthropic_model_kwargs() -> None:181 llm = ChatAnthropic(model_name=MODEL_NAME, model_kwargs={"foo": "bar"}) # type: ignore[call-arg, call-arg]182 assert llm.model_kwargs == {"foo": "bar"}183184185@pytest.mark.requires("anthropic")186def test_anthropic_fields_in_model_kwargs() -> None:187 """Test that for backwards compatibility fields can be passed in as model_kwargs."""188 llm = ChatAnthropic(model=MODEL_NAME, model_kwargs={"max_tokens_to_sample": 5}) # type: ignore[call-arg]189 assert llm.max_tokens == 5190 llm = ChatAnthropic(model=MODEL_NAME, model_kwargs={"max_tokens": 5}) # type: ignore[call-arg]191 assert llm.max_tokens == 5192193194@pytest.mark.requires("anthropic")195def test_anthropic_incorrect_field() -> None:196 with pytest.warns(match="not default parameter"):197 llm = ChatAnthropic(model=MODEL_NAME, foo="bar") # type: ignore[call-arg, call-arg]198 assert llm.model_kwargs == {"foo": "bar"}199200201@pytest.mark.requires("anthropic")202def test_anthropic_initialization() -> None:203 """Test anthropic initialization."""204 # Verify that chat anthropic can be initialized using a secret key provided205 # as a parameter rather than an environment variable.206 ChatAnthropic(model=MODEL_NAME, anthropic_api_key="test") # type: ignore[call-arg, call-arg]207208209def test__format_output() -> None:210 anthropic_msg = Message(211 id="foo",212 content=[TextBlock(type="text", text="bar")],213 model="baz",214 role="assistant",215 stop_reason=None,216 stop_sequence=None,217 usage=Usage(input_tokens=2, output_tokens=1),218 type="message",219 )220 expected = AIMessage( # type: ignore[misc]221 "bar",222 usage_metadata={223 "input_tokens": 2,224 "output_tokens": 1,225 "total_tokens": 3,226 "input_token_details": {},227 },228 response_metadata={"model_provider": "anthropic"},229 )230 llm = ChatAnthropic(model=MODEL_NAME, anthropic_api_key="test") # type: ignore[call-arg, call-arg]231 actual = llm._format_output(anthropic_msg)232 assert actual.generations[0].message == expected233234235def test__format_output_cached() -> None:236 anthropic_msg = Message(237 id="foo",238 content=[TextBlock(type="text", text="bar")],239 model="baz",240 role="assistant",241 stop_reason=None,242 stop_sequence=None,243 usage=Usage(244 input_tokens=2,245 output_tokens=1,246 cache_creation_input_tokens=3,247 cache_read_input_tokens=4,248 ),249 type="message",250 )251 expected = AIMessage( # type: ignore[misc]252 "bar",253 usage_metadata={254 "input_tokens": 9,255 "output_tokens": 1,256 "total_tokens": 10,257 "input_token_details": {"cache_creation": 3, "cache_read": 4},258 },259 response_metadata={"model_provider": "anthropic"},260 )261262 llm = ChatAnthropic(model=MODEL_NAME, anthropic_api_key="test") # type: ignore[call-arg, call-arg]263 actual = llm._format_output(anthropic_msg)264 assert actual.generations[0].message == expected265266267def test__merge_messages() -> None:268 messages = [269 SystemMessage("foo"), # type: ignore[misc]270 HumanMessage("bar"), # type: ignore[misc]271 AIMessage( # type: ignore[misc]272 [273 {"text": "baz", "type": "text"},274 {275 "tool_input": {"a": "b"},276 "type": "tool_use",277 "id": "1",278 "text": None,279 "name": "buz",280 },281 {"text": "baz", "type": "text"},282 {283 "tool_input": {"a": "c"},284 "type": "tool_use",285 "id": "2",286 "text": None,287 "name": "blah",288 },289 {290 "tool_input": {"a": "c"},291 "type": "tool_use",292 "id": "3",293 "text": None,294 "name": "blah",295 },296 ],297 ),298 ToolMessage("buz output", tool_call_id="1", status="error"), # type: ignore[misc]299 ToolMessage(300 content=[301 {302 "type": "image",303 "source": {304 "type": "base64",305 "media_type": "image/jpeg",306 "data": "fake_image_data",307 },308 },309 ],310 tool_call_id="2",311 ), # type: ignore[misc]312 ToolMessage([], tool_call_id="3"), # type: ignore[misc]313 HumanMessage("next thing"), # type: ignore[misc]314 ]315 expected = [316 SystemMessage("foo"), # type: ignore[misc]317 HumanMessage("bar"), # type: ignore[misc]318 AIMessage( # type: ignore[misc]319 [320 {"text": "baz", "type": "text"},321 {322 "tool_input": {"a": "b"},323 "type": "tool_use",324 "id": "1",325 "text": None,326 "name": "buz",327 },328 {"text": "baz", "type": "text"},329 {330 "tool_input": {"a": "c"},331 "type": "tool_use",332 "id": "2",333 "text": None,334 "name": "blah",335 },336 {337 "tool_input": {"a": "c"},338 "type": "tool_use",339 "id": "3",340 "text": None,341 "name": "blah",342 },343 ],344 ),345 HumanMessage( # type: ignore[misc]346 [347 {348 "type": "tool_result",349 "content": "buz output",350 "tool_use_id": "1",351 "is_error": True,352 },353 {354 "type": "tool_result",355 "content": [356 {357 "type": "image",358 "source": {359 "type": "base64",360 "media_type": "image/jpeg",361 "data": "fake_image_data",362 },363 },364 ],365 "tool_use_id": "2",366 "is_error": False,367 },368 {369 "type": "tool_result",370 "content": [],371 "tool_use_id": "3",372 "is_error": False,373 },374 {"type": "text", "text": "next thing"},375 ],376 ),377 ]378 actual = _merge_messages(messages)379 assert expected == actual380381 # Test tool message case382 messages = [383 ToolMessage("buz output", tool_call_id="1"), # type: ignore[misc]384 ToolMessage( # type: ignore[misc]385 content=[386 {"type": "tool_result", "content": "blah output", "tool_use_id": "2"},387 ],388 tool_call_id="2",389 ),390 ]391 expected = [392 HumanMessage( # type: ignore[misc]393 [394 {395 "type": "tool_result",396 "content": "buz output",397 "tool_use_id": "1",398 "is_error": False,399 },400 {"type": "tool_result", "content": "blah output", "tool_use_id": "2"},401 ],402 ),403 ]404 actual = _merge_messages(messages)405 assert expected == actual406407408def test__merge_messages_mutation() -> None:409 original_messages = [410 HumanMessage([{"type": "text", "text": "bar"}]), # type: ignore[misc]411 HumanMessage("next thing"), # type: ignore[misc]412 ]413 messages = [414 HumanMessage([{"type": "text", "text": "bar"}]), # type: ignore[misc]415 HumanMessage("next thing"), # type: ignore[misc]416 ]417 expected = [418 HumanMessage( # type: ignore[misc]419 [{"type": "text", "text": "bar"}, {"type": "text", "text": "next thing"}],420 ),421 ]422 actual = _merge_messages(messages)423 assert expected == actual424 assert messages == original_messages425426427def test__merge_messages_tool_message_cache_control() -> None:428 """Test that cache_control is hoisted from content blocks to tool_result level."""429 # Test with cache_control in content block430 messages = [431 ToolMessage(432 content=[433 {434 "type": "text",435 "text": "tool output",436 "cache_control": {"type": "ephemeral"},437 }438 ],439 tool_call_id="1",440 )441 ]442 original_messages = [copy.deepcopy(m) for m in messages]443 expected = [444 HumanMessage(445 [446 {447 "type": "tool_result",448 "content": [{"type": "text", "text": "tool output"}],449 "tool_use_id": "1",450 "is_error": False,451 "cache_control": {"type": "ephemeral"},452 }453 ]454 )455 ]456 actual = _merge_messages(messages)457 assert expected == actual458 # Verify no mutation459 assert messages == original_messages460461 # Test with multiple content blocks, cache_control on last one462 messages = [463 ToolMessage(464 content=[465 {"type": "text", "text": "first output"},466 {467 "type": "text",468 "text": "second output",469 "cache_control": {"type": "ephemeral"},470 },471 ],472 tool_call_id="2",473 )474 ]475 expected = [476 HumanMessage(477 [478 {479 "type": "tool_result",480 "content": [481 {"type": "text", "text": "first output"},482 {"type": "text", "text": "second output"},483 ],484 "tool_use_id": "2",485 "is_error": False,486 "cache_control": {"type": "ephemeral"},487 }488 ]489 )490 ]491 actual = _merge_messages(messages)492 assert expected == actual493494 # Test without cache_control495 messages = [ToolMessage(content="simple output", tool_call_id="3")]496 expected = [497 HumanMessage(498 [499 {500 "type": "tool_result",501 "content": "simple output",502 "tool_use_id": "3",503 "is_error": False,504 }505 ]506 )507 ]508 actual = _merge_messages(messages)509 assert expected == actual510511512def test__format_image() -> None:513 url = "dummyimage.com/600x400/000/fff"514 with pytest.raises(ValueError):515 _format_image(url)516517518@pytest.fixture519def pydantic() -> type[BaseModel]:520 class dummy_function(BaseModel): # noqa: N801521 """Dummy function."""522523 arg1: int = Field(..., description="foo")524 arg2: Literal["bar", "baz"] = Field(..., description="one of 'bar', 'baz'")525526 return dummy_function527528529@pytest.fixture530def function() -> Callable:531 def dummy_function(arg1: int, arg2: Literal["bar", "baz"]) -> None:532 """Dummy function.533534 Args:535 arg1: foo536 arg2: one of 'bar', 'baz'537538 """539540 return dummy_function541542543@pytest.fixture544def dummy_tool() -> BaseTool:545 class Schema(BaseModel):546 arg1: int = Field(..., description="foo")547 arg2: Literal["bar", "baz"] = Field(..., description="one of 'bar', 'baz'")548549 class DummyFunction(BaseTool): # type: ignore[override]550 args_schema: type[BaseModel] = Schema551 name: str = "dummy_function"552 description: str = "Dummy function."553554 def _run(self, *args: Any, **kwargs: Any) -> Any:555 pass556557 return DummyFunction()558559560@pytest.fixture561def json_schema() -> dict:562 return {563 "title": "dummy_function",564 "description": "Dummy function.",565 "type": "object",566 "properties": {567 "arg1": {"description": "foo", "type": "integer"},568 "arg2": {569 "description": "one of 'bar', 'baz'",570 "enum": ["bar", "baz"],571 "type": "string",572 },573 },574 "required": ["arg1", "arg2"],575 }576577578@pytest.fixture579def openai_function() -> dict:580 return {581 "name": "dummy_function",582 "description": "Dummy function.",583 "parameters": {584 "type": "object",585 "properties": {586 "arg1": {"description": "foo", "type": "integer"},587 "arg2": {588 "description": "one of 'bar', 'baz'",589 "enum": ["bar", "baz"],590 "type": "string",591 },592 },593 "required": ["arg1", "arg2"],594 },595 }596597598def test_convert_to_anthropic_tool(599 pydantic: type[BaseModel],600 function: Callable,601 dummy_tool: BaseTool,602 json_schema: dict,603 openai_function: dict,604) -> None:605 expected = {606 "name": "dummy_function",607 "description": "Dummy function.",608 "input_schema": {609 "type": "object",610 "properties": {611 "arg1": {"description": "foo", "type": "integer"},612 "arg2": {613 "description": "one of 'bar', 'baz'",614 "enum": ["bar", "baz"],615 "type": "string",616 },617 },618 "required": ["arg1", "arg2"],619 },620 }621622 for fn in (pydantic, function, dummy_tool, json_schema, expected, openai_function):623 actual = convert_to_anthropic_tool(fn)624 assert actual == expected625626627def test__format_messages_with_tool_calls() -> None:628 system = SystemMessage("fuzz") # type: ignore[misc]629 human = HumanMessage("foo") # type: ignore[misc]630 ai = AIMessage(631 "", # with empty string632 tool_calls=[{"name": "bar", "id": "1", "args": {"baz": "buzz"}}],633 )634 ai2 = AIMessage(635 [], # with empty list636 tool_calls=[{"name": "bar", "id": "2", "args": {"baz": "buzz"}}],637 )638 tool = ToolMessage(639 "blurb",640 tool_call_id="1",641 )642 tool_image_url = ToolMessage(643 [{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,...."}}],644 tool_call_id="2",645 )646 tool_image = ToolMessage(647 [648 {649 "type": "image",650 "source": {651 "data": "....",652 "type": "base64",653 "media_type": "image/jpeg",654 },655 },656 ],657 tool_call_id="3",658 )659 messages = [system, human, ai, tool, ai2, tool_image_url, tool_image]660 expected = (661 "fuzz",662 [663 {"role": "user", "content": "foo"},664 {665 "role": "assistant",666 "content": [667 {668 "type": "tool_use",669 "name": "bar",670 "id": "1",671 "input": {"baz": "buzz"},672 },673 ],674 },675 {676 "role": "user",677 "content": [678 {679 "type": "tool_result",680 "content": "blurb",681 "tool_use_id": "1",682 "is_error": False,683 },684 ],685 },686 {687 "role": "assistant",688 "content": [689 {690 "type": "tool_use",691 "name": "bar",692 "id": "2",693 "input": {"baz": "buzz"},694 },695 ],696 },697 {698 "role": "user",699 "content": [700 {701 "type": "tool_result",702 "content": [703 {704 "type": "image",705 "source": {706 "data": "....",707 "type": "base64",708 "media_type": "image/jpeg",709 },710 },711 ],712 "tool_use_id": "2",713 "is_error": False,714 },715 {716 "type": "tool_result",717 "content": [718 {719 "type": "image",720 "source": {721 "data": "....",722 "type": "base64",723 "media_type": "image/jpeg",724 },725 },726 ],727 "tool_use_id": "3",728 "is_error": False,729 },730 ],731 },732 ],733 )734 actual = _format_messages(messages)735 assert expected == actual736737 # Check handling of empty AIMessage738 empty_contents: list[str | list[str | dict]] = ["", []]739 for empty_content in empty_contents:740 ## Permit message in final position741 _, anthropic_messages = _format_messages([human, AIMessage(empty_content)])742 expected_messages = [743 {"role": "user", "content": "foo"},744 {"role": "assistant", "content": empty_content},745 ]746 assert expected_messages == anthropic_messages747748 ## Remove message otherwise749 _, anthropic_messages = _format_messages(750 [human, AIMessage(empty_content), human]751 )752 expected_messages = [753 {"role": "user", "content": "foo"},754 {"role": "user", "content": "foo"},755 ]756 assert expected_messages == anthropic_messages757758 actual = _format_messages(759 [system, human, ai, tool, AIMessage(empty_content), human]760 )761 assert actual[0] == "fuzz"762 assert [message["role"] for message in actual[1]] == [763 "user",764 "assistant",765 "user",766 "user",767 ]768769770def test__format_tool_use_block() -> None:771 # Test we correctly format tool_use blocks when there is no corresponding tool_call.772 message = AIMessage(773 [774 {775 "type": "tool_use",776 "name": "foo_1",777 "id": "1",778 "input": {"bar_1": "baz_1"},779 },780 {781 "type": "tool_use",782 "name": "foo_2",783 "id": "2",784 "input": {},785 "partial_json": '{"bar_2": "baz_2"}',786 "index": 1,787 },788 ]789 )790 result = _format_messages([message])791 expected = {792 "role": "assistant",793 "content": [794 {795 "type": "tool_use",796 "name": "foo_1",797 "id": "1",798 "input": {"bar_1": "baz_1"},799 },800 {801 "type": "tool_use",802 "name": "foo_2",803 "id": "2",804 "input": {"bar_2": "baz_2"},805 },806 ],807 }808 assert result == (None, [expected])809810811def test__format_messages_with_str_content_and_tool_calls() -> None:812 system = SystemMessage("fuzz") # type: ignore[misc]813 human = HumanMessage("foo") # type: ignore[misc]814 # If content and tool_calls are specified and content is a string, then both are815 # included with content first.816 ai = AIMessage( # type: ignore[misc]817 "thought",818 tool_calls=[{"name": "bar", "id": "1", "args": {"baz": "buzz"}}],819 )820 tool = ToolMessage("blurb", tool_call_id="1") # type: ignore[misc]821 messages = [system, human, ai, tool]822 expected = (823 "fuzz",824 [825 {"role": "user", "content": "foo"},826 {827 "role": "assistant",828 "content": [829 {"type": "text", "text": "thought"},830 {831 "type": "tool_use",832 "name": "bar",833 "id": "1",834 "input": {"baz": "buzz"},835 },836 ],837 },838 {839 "role": "user",840 "content": [841 {842 "type": "tool_result",843 "content": "blurb",844 "tool_use_id": "1",845 "is_error": False,846 },847 ],848 },849 ],850 )851 actual = _format_messages(messages)852 assert expected == actual853854855def test__format_messages_with_list_content_and_tool_calls() -> None:856 system = SystemMessage("fuzz") # type: ignore[misc]857 human = HumanMessage("foo") # type: ignore[misc]858 ai = AIMessage( # type: ignore[misc]859 [{"type": "text", "text": "thought"}],860 tool_calls=[{"name": "bar", "id": "1", "args": {"baz": "buzz"}}],861 )862 tool = ToolMessage( # type: ignore[misc]863 "blurb",864 tool_call_id="1",865 )866 messages = [system, human, ai, tool]867 expected = (868 "fuzz",869 [870 {"role": "user", "content": "foo"},871 {872 "role": "assistant",873 "content": [874 {"type": "text", "text": "thought"},875 {876 "type": "tool_use",877 "name": "bar",878 "id": "1",879 "input": {"baz": "buzz"},880 },881 ],882 },883 {884 "role": "user",885 "content": [886 {887 "type": "tool_result",888 "content": "blurb",889 "tool_use_id": "1",890 "is_error": False,891 },892 ],893 },894 ],895 )896 actual = _format_messages(messages)897 assert expected == actual898899900def test__format_messages_with_tool_use_blocks_and_tool_calls() -> None:901 """Show that tool_calls are preferred to tool_use blocks when both have same id."""902 system = SystemMessage("fuzz") # type: ignore[misc]903 human = HumanMessage("foo") # type: ignore[misc]904 # NOTE: tool_use block in contents and tool_calls have different arguments.905 ai = AIMessage( # type: ignore[misc]906 [907 {"type": "text", "text": "thought"},908 {909 "type": "tool_use",910 "name": "bar",911 "id": "1",912 "input": {"baz": "NOT_BUZZ"},913 },914 ],915 tool_calls=[{"name": "bar", "id": "1", "args": {"baz": "BUZZ"}}],916 )917 tool = ToolMessage("blurb", tool_call_id="1") # type: ignore[misc]918 messages = [system, human, ai, tool]919 expected = (920 "fuzz",921 [922 {"role": "user", "content": "foo"},923 {924 "role": "assistant",925 "content": [926 {"type": "text", "text": "thought"},927 {928 "type": "tool_use",929 "name": "bar",930 "id": "1",931 "input": {"baz": "BUZZ"}, # tool_calls value preferred.932 },933 ],934 },935 {936 "role": "user",937 "content": [938 {939 "type": "tool_result",940 "content": "blurb",941 "tool_use_id": "1",942 "is_error": False,943 },944 ],945 },946 ],947 )948 actual = _format_messages(messages)949 assert expected == actual950951952def test__format_messages_with_cache_control() -> None:953 messages = [954 SystemMessage(955 [956 {"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}},957 ],958 ),959 HumanMessage(960 [961 {"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}},962 {963 "type": "text",964 "text": "foo",965 },966 ],967 ),968 ]969 expected_system = [970 {"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}},971 ]972 expected_messages = [973 {974 "role": "user",975 "content": [976 {"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}},977 {"type": "text", "text": "foo"},978 ],979 },980 ]981 actual_system, actual_messages = _format_messages(messages)982 assert expected_system == actual_system983 assert expected_messages == actual_messages984985 # Test standard multi-modal format (v0)986 messages = [987 HumanMessage(988 [989 {990 "type": "text",991 "text": "Summarize this document:",992 },993 {994 "type": "file",995 "source_type": "base64",996 "mime_type": "application/pdf",997 "data": "<base64 data>",998 "cache_control": {"type": "ephemeral"},999 },1000 ],1001 ),1002 ]1003 actual_system, actual_messages = _format_messages(messages)1004 assert actual_system is None1005 expected_messages = [1006 {1007 "role": "user",1008 "content": [1009 {1010 "type": "text",1011 "text": "Summarize this document:",1012 },1013 {1014 "type": "document",1015 "source": {1016 "type": "base64",1017 "media_type": "application/pdf",1018 "data": "<base64 data>",1019 },1020 "cache_control": {"type": "ephemeral"},1021 },1022 ],1023 },1024 ]1025 assert actual_messages == expected_messages10261027 # Test standard multi-modal format (v1)1028 messages = [1029 HumanMessage(1030 [1031 {1032 "type": "text",1033 "text": "Summarize this document:",1034 },1035 {1036 "type": "file",1037 "mime_type": "application/pdf",1038 "base64": "<base64 data>",1039 "extras": {"cache_control": {"type": "ephemeral"}},1040 },1041 ],1042 ),1043 ]1044 actual_system, actual_messages = _format_messages(messages)1045 assert actual_system is None1046 expected_messages = [1047 {1048 "role": "user",1049 "content": [1050 {1051 "type": "text",1052 "text": "Summarize this document:",1053 },1054 {1055 "type": "document",1056 "source": {1057 "type": "base64",1058 "media_type": "application/pdf",1059 "data": "<base64 data>",1060 },1061 "cache_control": {"type": "ephemeral"},1062 },1063 ],1064 },1065 ]1066 assert actual_messages == expected_messages10671068 # Test standard multi-modal format (v1, unpacked extras)1069 messages = [1070 HumanMessage(1071 [1072 {1073 "type": "text",1074 "text": "Summarize this document:",1075 },1076 {1077 "type": "file",1078 "mime_type": "application/pdf",1079 "base64": "<base64 data>",1080 "cache_control": {"type": "ephemeral"},1081 },1082 ],1083 ),1084 ]1085 actual_system, actual_messages = _format_messages(messages)1086 assert actual_system is None1087 expected_messages = [1088 {1089 "role": "user",1090 "content": [1091 {1092 "type": "text",1093 "text": "Summarize this document:",1094 },1095 {1096 "type": "document",1097 "source": {1098 "type": "base64",1099 "media_type": "application/pdf",1100 "data": "<base64 data>",1101 },1102 "cache_control": {"type": "ephemeral"},1103 },1104 ],1105 },1106 ]1107 assert actual_messages == expected_messages11081109 # Also test file inputs1110 ## Images1111 for block in [1112 # v11113 {1114 "type": "image",1115 "file_id": "abc123",1116 },1117 # v01118 {1119 "type": "image",1120 "source_type": "id",1121 "id": "abc123",1122 },1123 ]:1124 messages = [1125 HumanMessage(1126 [1127 {1128 "type": "text",1129 "text": "Summarize this image:",1130 },1131 block,1132 ],1133 ),1134 ]1135 actual_system, actual_messages = _format_messages(messages)1136 assert actual_system is None1137 expected_messages = [1138 {1139 "role": "user",1140 "content": [1141 {1142 "type": "text",1143 "text": "Summarize this image:",1144 },1145 {1146 "type": "image",1147 "source": {1148 "type": "file",1149 "file_id": "abc123",1150 },1151 },1152 ],1153 },1154 ]1155 assert actual_messages == expected_messages11561157 ## Documents1158 for block in [1159 # v11160 {1161 "type": "file",1162 "file_id": "abc123",1163 },1164 # v01165 {1166 "type": "file",1167 "source_type": "id",1168 "id": "abc123",1169 },1170 ]:1171 messages = [1172 HumanMessage(1173 [1174 {1175 "type": "text",1176 "text": "Summarize this document:",1177 },1178 block,1179 ],1180 ),1181 ]1182 actual_system, actual_messages = _format_messages(messages)1183 assert actual_system is None1184 expected_messages = [1185 {1186 "role": "user",1187 "content": [1188 {1189 "type": "text",1190 "text": "Summarize this document:",1191 },1192 {1193 "type": "document",1194 "source": {1195 "type": "file",1196 "file_id": "abc123",1197 },1198 },1199 ],1200 },1201 ]1202 assert actual_messages == expected_messages120312041205def test__format_messages_with_citations() -> None:1206 input_messages = [1207 HumanMessage(1208 content=[1209 {1210 "type": "file",1211 "source_type": "text",1212 "text": "The grass is green. The sky is blue.",1213 "mime_type": "text/plain",1214 "citations": {"enabled": True},1215 },1216 {"type": "text", "text": "What color is the grass and sky?"},1217 ],1218 ),1219 ]1220 expected_messages = [1221 {1222 "role": "user",1223 "content": [1224 {1225 "type": "document",1226 "source": {1227 "type": "text",1228 "media_type": "text/plain",1229 "data": "The grass is green. The sky is blue.",1230 },1231 "citations": {"enabled": True},1232 },1233 {"type": "text", "text": "What color is the grass and sky?"},1234 ],1235 },1236 ]1237 actual_system, actual_messages = _format_messages(input_messages)1238 assert actual_system is None1239 assert actual_messages == expected_messages124012411242def test__format_messages_openai_image_format() -> None:1243 message = HumanMessage(1244 content=[1245 {1246 "type": "text",1247 "text": "Can you highlight the differences between these two images?",1248 },1249 {1250 "type": "image_url",1251 "image_url": {"url": "data:image/jpeg;base64,<base64 data>"},1252 },1253 {1254 "type": "image_url",1255 "image_url": {"url": "https://<image url>"},1256 },1257 ],1258 )1259 actual_system, actual_messages = _format_messages([message])1260 assert actual_system is None1261 expected_messages = [1262 {1263 "role": "user",1264 "content": [1265 {1266 "type": "text",1267 "text": (1268 "Can you highlight the differences between these two images?"1269 ),1270 },1271 {1272 "type": "image",1273 "source": {1274 "type": "base64",1275 "media_type": "image/jpeg",1276 "data": "<base64 data>",1277 },1278 },1279 {1280 "type": "image",1281 "source": {1282 "type": "url",1283 "url": "https://<image url>",1284 },1285 },1286 ],1287 },1288 ]1289 assert actual_messages == expected_messages129012911292def test__format_messages_with_multiple_system() -> None:1293 messages = [1294 HumanMessage("baz"),1295 SystemMessage("bar"),1296 SystemMessage("baz"),1297 SystemMessage(1298 [1299 {"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}},1300 ],1301 ),1302 ]1303 expected_system = [1304 {"type": "text", "text": "bar"},1305 {"type": "text", "text": "baz"},1306 {"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}},1307 ]1308 expected_messages = [{"role": "user", "content": "baz"}]1309 actual_system, actual_messages = _format_messages(messages)1310 assert expected_system == actual_system1311 assert expected_messages == actual_messages131213131314def test_anthropic_api_key_is_secret_string() -> None:1315 """Test that the API key is stored as a SecretStr."""1316 chat_model = ChatAnthropic( # type: ignore[call-arg, call-arg]1317 model=MODEL_NAME,1318 anthropic_api_key="secret-api-key",1319 )1320 assert isinstance(chat_model.anthropic_api_key, SecretStr)132113221323def test_anthropic_api_key_masked_when_passed_from_env(1324 monkeypatch: MonkeyPatch,1325 capsys: CaptureFixture,1326) -> None:1327 """Test that the API key is masked when passed from an environment variable."""1328 monkeypatch.setenv("ANTHROPIC_API_KEY ", "secret-api-key")1329 chat_model = ChatAnthropic( # type: ignore[call-arg]1330 model=MODEL_NAME,1331 )1332 print(chat_model.anthropic_api_key, end="") # noqa: T2011333 captured = capsys.readouterr()13341335 assert captured.out == "**********"133613371338def test_anthropic_api_key_masked_when_passed_via_constructor(1339 capsys: CaptureFixture,1340) -> None:1341 """Test that the API key is masked when passed via the constructor."""1342 chat_model = ChatAnthropic( # type: ignore[call-arg, call-arg]1343 model=MODEL_NAME,1344 anthropic_api_key="secret-api-key",1345 )1346 print(chat_model.anthropic_api_key, end="") # noqa: T2011347 captured = capsys.readouterr()13481349 assert captured.out == "**********"135013511352def test_anthropic_uses_actual_secret_value_from_secretstr() -> None:1353 """Test that the actual secret value is correctly retrieved."""1354 chat_model = ChatAnthropic( # type: ignore[call-arg, call-arg]1355 model=MODEL_NAME,1356 anthropic_api_key="secret-api-key",1357 )1358 assert (1359 cast("SecretStr", chat_model.anthropic_api_key).get_secret_value()1360 == "secret-api-key"1361 )136213631364class GetWeather(BaseModel):1365 """Get the current weather in a given location."""13661367 location: str = Field(..., description="The city and state, e.g. San Francisco, CA")136813691370def test_anthropic_bind_tools_tool_choice() -> None:1371 chat_model = ChatAnthropic( # type: ignore[call-arg, call-arg]1372 model=MODEL_NAME,1373 anthropic_api_key="secret-api-key",1374 )1375 chat_model_with_tools = chat_model.bind_tools(1376 [GetWeather],1377 tool_choice={"type": "tool", "name": "GetWeather"},1378 )1379 assert cast("RunnableBinding", chat_model_with_tools).kwargs["tool_choice"] == {1380 "type": "tool",1381 "name": "GetWeather",1382 }1383 chat_model_with_tools = chat_model.bind_tools(1384 [GetWeather],1385 tool_choice="GetWeather",1386 )1387 assert cast("RunnableBinding", chat_model_with_tools).kwargs["tool_choice"] == {1388 "type": "tool",1389 "name": "GetWeather",1390 }1391 chat_model_with_tools = chat_model.bind_tools([GetWeather], tool_choice="auto")1392 assert cast("RunnableBinding", chat_model_with_tools).kwargs["tool_choice"] == {1393 "type": "auto",1394 }1395 chat_model_with_tools = chat_model.bind_tools([GetWeather], tool_choice="any")1396 assert cast("RunnableBinding", chat_model_with_tools).kwargs["tool_choice"] == {1397 "type": "any",1398 }139914001401def test_fine_grained_tool_streaming_beta() -> None:1402 """Test that fine-grained tool streaming beta can be enabled."""1403 # Test with betas parameter at initialization1404 model = ChatAnthropic(1405 model=MODEL_NAME, betas=["fine-grained-tool-streaming-2025-05-14"]1406 )14071408 # Create a simple tool1409 def get_weather(city: str) -> str:1410 """Get the weather for a city."""1411 return f"Weather in {city}"14121413 model_with_tools = model.bind_tools([get_weather])1414 payload = model_with_tools._get_request_payload( # type: ignore[attr-defined]1415 "What's the weather in SF?",1416 stream=True,1417 **model_with_tools.kwargs, # type: ignore[attr-defined]1418 )14191420 # Verify beta header is in payload1421 assert "fine-grained-tool-streaming-2025-05-14" in payload["betas"]1422 assert payload["stream"] is True14231424 # Test combining with other betas1425 model = ChatAnthropic(1426 model=MODEL_NAME,1427 betas=["context-1m-2025-08-07", "fine-grained-tool-streaming-2025-05-14"],1428 )1429 model_with_tools = model.bind_tools([get_weather])1430 payload = model_with_tools._get_request_payload( # type: ignore[attr-defined]1431 "What's the weather?",1432 stream=True,1433 **model_with_tools.kwargs, # type: ignore[attr-defined]1434 )1435 assert set(payload["betas"]) == {1436 "context-1m-2025-08-07",1437 "fine-grained-tool-streaming-2025-05-14",1438 }14391440 # Test that _create routes to beta client when betas are present1441 model = ChatAnthropic(1442 model=MODEL_NAME, betas=["fine-grained-tool-streaming-2025-05-14"]1443 )1444 payload = {"betas": ["fine-grained-tool-streaming-2025-05-14"], "stream": True}14451446 with patch.object(model._client.beta.messages, "create") as mock_beta_create:1447 model._create(payload)1448 mock_beta_create.assert_called_once_with(**payload)144914501451def test_optional_description() -> None:1452 llm = ChatAnthropic(model=MODEL_NAME)14531454 class SampleModel(BaseModel):1455 sample_field: str14561457 _ = llm.with_structured_output(SampleModel.model_json_schema())145814591460def test_get_num_tokens_from_messages_passes_kwargs() -> None:1461 """Test that get_num_tokens_from_messages passes kwargs to the model."""1462 llm = ChatAnthropic(model=MODEL_NAME)14631464 with patch.object(anthropic, "Client") as _client:1465 llm.get_num_tokens_from_messages([HumanMessage("foo")], foo="bar")14661467 assert _client.return_value.messages.count_tokens.call_args.kwargs["foo"] == "bar"14681469 llm = ChatAnthropic(1470 model=MODEL_NAME,1471 betas=["context-management-2025-06-27"],1472 context_management={"edits": [{"type": "clear_tool_uses_20250919"}]},1473 )1474 with patch.object(anthropic, "Client") as _client:1475 llm.get_num_tokens_from_messages([HumanMessage("foo")])14761477 call_args = _client.return_value.beta.messages.count_tokens.call_args.kwargs1478 assert call_args["betas"] == ["context-management-2025-06-27"]1479 assert call_args["context_management"] == {1480 "edits": [{"type": "clear_tool_uses_20250919"}]1481 }148214831484def test_usage_metadata_standardization() -> None:1485 class UsageModel(BaseModel):1486 input_tokens: int = 101487 output_tokens: int = 51488 cache_read_input_tokens: int = 31489 cache_creation_input_tokens: int = 214901491 # Happy path1492 usage = UsageModel()1493 result = _create_usage_metadata(usage)1494 assert result["input_tokens"] == 15 # 10 + 3 + 21495 assert result["output_tokens"] == 51496 assert result["total_tokens"] == 201497 assert result.get("input_token_details") == {"cache_read": 3, "cache_creation": 2}14981499 # Null input and output tokens1500 class UsageModelNulls(BaseModel):1501 input_tokens: int | None = None1502 output_tokens: int | None = None1503 cache_read_input_tokens: int | None = None1504 cache_creation_input_tokens: int | None = None15051506 usage_nulls = UsageModelNulls()1507 result = _create_usage_metadata(usage_nulls)1508 assert result["input_tokens"] == 01509 assert result["output_tokens"] == 01510 assert result["total_tokens"] == 015111512 # Test missing fields1513 class UsageModelMissing(BaseModel):1514 pass15151516 usage_missing = UsageModelMissing()1517 result = _create_usage_metadata(usage_missing)1518 assert result["input_tokens"] == 01519 assert result["output_tokens"] == 01520 assert result["total_tokens"] == 0152115221523def test_usage_metadata_cache_creation_ttl() -> None:1524 """Test _create_usage_metadata with granular cache_creation TTL fields."""15251526 # Case 1: cache_creation with specific ephemeral TTL tokens (BaseModel)1527 class CacheCreation(BaseModel):1528 ephemeral_5m_input_tokens: int = 1001529 ephemeral_1h_input_tokens: int = 5015301531 class UsageWithCacheCreation(BaseModel):1532 input_tokens: int = 2001533 output_tokens: int = 301534 cache_read_input_tokens: int = 101535 cache_creation_input_tokens: int = 1501536 cache_creation: CacheCreation = CacheCreation()15371538 result = _create_usage_metadata(UsageWithCacheCreation())1539 # input_tokens = 200 (base) + 10 (cache_read) + 150 (specific: 100+50)1540 assert result["input_tokens"] == 3601541 assert result["output_tokens"] == 301542 assert result["total_tokens"] == 3901543 details = dict(result.get("input_token_details") or {})1544 assert details["cache_read"] == 101545 # cache_creation should be suppressed to avoid double counting1546 assert details["cache_creation"] == 01547 assert details["ephemeral_5m_input_tokens"] == 1001548 assert details["ephemeral_1h_input_tokens"] == 5015491550 # Case 2: cache_creation as a dict1551 class UsageWithCacheCreationDict(BaseModel):1552 input_tokens: int = 2001553 output_tokens: int = 301554 cache_read_input_tokens: int = 101555 cache_creation_input_tokens: int = 1501556 cache_creation: dict = {1557 "ephemeral_5m_input_tokens": 80,1558 "ephemeral_1h_input_tokens": 70,1559 }15601561 result = _create_usage_metadata(UsageWithCacheCreationDict())1562 assert result["input_tokens"] == 200 + 10 + 80 + 701563 details = dict(result.get("input_token_details") or {})1564 assert details["cache_creation"] == 01565 assert details["ephemeral_5m_input_tokens"] == 801566 assert details["ephemeral_1h_input_tokens"] == 7015671568 # Case 3: cache_creation exists but specific keys are zero — falls back to1569 # generic cache_creation_input_tokens1570 class CacheCreationZero(BaseModel):1571 ephemeral_5m_input_tokens: int = 01572 ephemeral_1h_input_tokens: int = 015731574 class UsageWithCacheCreationZero(BaseModel):1575 input_tokens: int = 2001576 output_tokens: int = 301577 cache_read_input_tokens: int = 101578 cache_creation_input_tokens: int = 501579 cache_creation: CacheCreationZero = CacheCreationZero()15801581 result = _create_usage_metadata(UsageWithCacheCreationZero())1582 # specific_cache_creation_tokens = 0, so falls back to cache_creation_input_tokens1583 # input_tokens = 200 + 10 + 50 = 2601584 assert result["input_tokens"] == 2601585 assert result["output_tokens"] == 301586 assert result["total_tokens"] == 2901587 details = dict(result.get("input_token_details") or {})1588 assert details["cache_read"] == 101589 assert details["cache_creation"] == 5015901591 # Case 4: cache_creation exists but specific keys are missing from the dict1592 class CacheCreationEmpty(BaseModel):1593 pass15941595 class UsageWithCacheCreationEmpty(BaseModel):1596 input_tokens: int = 1001597 output_tokens: int = 201598 cache_read_input_tokens: int = 51599 cache_creation_input_tokens: int = 151600 cache_creation: CacheCreationEmpty = CacheCreationEmpty()16011602 result = _create_usage_metadata(UsageWithCacheCreationEmpty())1603 # specific_cache_creation_tokens = 0, falls back to cache_creation_input_tokens1604 assert result["input_tokens"] == 100 + 5 + 151605 assert result["output_tokens"] == 201606 assert result["total_tokens"] == 1401607 details = dict(result.get("input_token_details") or {})1608 assert details["cache_creation"] == 1516091610 # Case 5: only one ephemeral key is non-zero1611 class CacheCreationPartial(BaseModel):1612 ephemeral_5m_input_tokens: int = 01613 ephemeral_1h_input_tokens: int = 7516141615 class UsageWithPartialCache(BaseModel):1616 input_tokens: int = 1001617 output_tokens: int = 101618 cache_read_input_tokens: int = 01619 cache_creation_input_tokens: int = 751620 cache_creation: CacheCreationPartial = CacheCreationPartial()16211622 result = _create_usage_metadata(UsageWithPartialCache())1623 # specific_cache_creation_tokens = 75 > 0, so generic cache_creation is suppressed1624 assert result["input_tokens"] == 100 + 0 + 751625 assert result["output_tokens"] == 101626 assert result["total_tokens"] == 1851627 details = dict(result.get("input_token_details") or {})1628 assert details["cache_creation"] == 01629 assert details["ephemeral_1h_input_tokens"] == 751630 # ephemeral_5m_input_tokens is 0 — still included since 0 is not None1631 assert details["ephemeral_5m_input_tokens"] == 016321633 # Case 6: no cache_creation field at all (the pre-existing path)1634 class UsageNoCacheCreation(BaseModel):1635 input_tokens: int = 501636 output_tokens: int = 251637 cache_read_input_tokens: int = 51638 cache_creation_input_tokens: int = 1016391640 result = _create_usage_metadata(UsageNoCacheCreation())1641 assert result["input_tokens"] == 50 + 5 + 101642 assert result["output_tokens"] == 251643 assert result["total_tokens"] == 901644 details = dict(result.get("input_token_details") or {})1645 assert details["cache_read"] == 51646 assert details["cache_creation"] == 10164716481649class FakeTracer(BaseTracer):1650 """Fake tracer to capture inputs to `chat_model_start`."""16511652 def __init__(self) -> None:1653 super().__init__()1654 self.chat_model_start_inputs: list = []16551656 def _persist_run(self, run: Run) -> None:1657 """Persist a run."""16581659 def on_chat_model_start(self, *args: Any, **kwargs: Any) -> Run:1660 self.chat_model_start_inputs.append({"args": args, "kwargs": kwargs})1661 return super().on_chat_model_start(*args, **kwargs)166216631664def test_mcp_tracing() -> None:1665 # Test we exclude sensitive information from traces1666 mcp_servers = [1667 {1668 "type": "url",1669 "url": "https://mcp.deepwiki.com/mcp",1670 "name": "deepwiki",1671 "authorization_token": "PLACEHOLDER",1672 },1673 ]16741675 llm = ChatAnthropic(1676 model=MODEL_NAME,1677 betas=["mcp-client-2025-04-04"],1678 mcp_servers=mcp_servers,1679 )16801681 tracer = FakeTracer()1682 mock_client = MagicMock()16831684 def mock_create(*args: Any, **kwargs: Any) -> Message:1685 return Message(1686 id="foo",1687 content=[TextBlock(type="text", text="bar")],1688 model="baz",1689 role="assistant",1690 stop_reason=None,1691 stop_sequence=None,1692 usage=Usage(input_tokens=2, output_tokens=1),1693 type="message",1694 )16951696 mock_client.messages.create = mock_create1697 input_message = HumanMessage("Test query")1698 with patch.object(llm, "_client", mock_client):1699 _ = llm.invoke([input_message], config={"callbacks": [tracer]})17001701 # Test headers are not traced1702 assert len(tracer.chat_model_start_inputs) == 11703 assert "PLACEHOLDER" not in str(tracer.chat_model_start_inputs)17041705 # Test headers are correctly propagated to request1706 payload = llm._get_request_payload([input_message])1707 assert payload["mcp_servers"][0]["authorization_token"] == "PLACEHOLDER" # noqa: S105170817091710def test_cache_control_kwarg() -> None:1711 llm = ChatAnthropic(model=MODEL_NAME)17121713 messages = [HumanMessage("foo"), AIMessage("bar"), HumanMessage("baz")]1714 payload = llm._get_request_payload(messages)1715 assert "cache_control" not in payload17161717 payload = llm._get_request_payload(messages, cache_control={"type": "ephemeral"})1718 assert payload["cache_control"] == {"type": "ephemeral"}1719 assert payload["messages"] == [1720 {"role": "user", "content": "foo"},1721 {"role": "assistant", "content": "bar"},1722 {"role": "user", "content": "baz"},1723 ]172417251726class _BedrockLikeAnthropic(ChatAnthropic):1727 """Stand-in for `ChatAnthropicBedrock` for `_llm_type`-based gating tests.17281729 Vertex is not modeled here: `langchain-google-vertexai`'s1730 `ChatAnthropicVertex` does not subclass `ChatAnthropic` and ships its own1731 `_get_request_payload`, so it never reaches the gate under test.1732 """17331734 @property1735 def _llm_type(self) -> str:1736 return "anthropic-bedrock-chat"173717381739def test_cache_control_kwarg_bedrock_injects_into_blocks() -> None:1740 """Non-direct subclasses must place `cache_control` inside the last block.17411742 Transports like Bedrock reject the top-level `cache_control` field, so1743 the kwarg has to be expanded into a nested breakpoint to remain effective.1744 """1745 llm = _BedrockLikeAnthropic(model=MODEL_NAME)17461747 messages = [HumanMessage("foo"), AIMessage("bar"), HumanMessage("baz")]1748 payload = llm._get_request_payload(messages, cache_control={"type": "ephemeral"})17491750 assert "cache_control" not in payload1751 last_message = payload["messages"][-1]1752 assert last_message["content"] == [1753 {"type": "text", "text": "baz", "cache_control": {"type": "ephemeral"}}1754 ]175517561757def test_cache_control_kwarg_bedrock_with_list_content() -> None:1758 """`cache_control` lands on the last block when content is already a list."""1759 llm = _BedrockLikeAnthropic(model=MODEL_NAME)17601761 messages = [HumanMessage([{"type": "text", "text": "foo"}])]1762 payload = llm._get_request_payload(1763 messages, cache_control={"type": "ephemeral", "ttl": "1h"}1764 )17651766 assert "cache_control" not in payload1767 last_block = payload["messages"][-1]["content"][-1]1768 assert last_block["cache_control"] == {"type": "ephemeral", "ttl": "1h"}176917701771def test_cache_control_kwarg_bedrock_skips_code_execution_blocks() -> None:1772 """`cache_control` must skip `code_execution`-related blocks.17731774 Anthropic rejects breakpoints applied to those blocks, so the injector1775 walks backwards until it finds an eligible block.1776 """1777 llm = _BedrockLikeAnthropic(model=MODEL_NAME)17781779 ai_message = AIMessage(1780 content=[1781 {"type": "text", "text": "earlier text"},1782 {1783 "type": "tool_use",1784 "id": "toolu_code_exec_1",1785 "name": "get_weather",1786 "input": {"location": "NYC"},1787 "caller": {1788 "type": "code_execution_20250825",1789 "tool_id": "srvtoolu_abc",1790 },1791 },1792 ]1793 )17941795 payload = llm._get_request_payload(1796 [HumanMessage("hi"), ai_message],1797 cache_control={"type": "ephemeral"},1798 )17991800 last_content = payload["messages"][-1]["content"]1801 assert last_content[0]["cache_control"] == {"type": "ephemeral"}1802 assert "cache_control" not in last_content[1]180318041805def test_cache_control_kwarg_bedrock_walks_back_to_earlier_message() -> None:1806 """When the last message has no eligible blocks, walk back to a prior one.18071808 Pins the contract that `reversed(formatted_messages)` is intentional: a1809 refactor that only inspects the last message would silently regress.1810 """1811 llm = _BedrockLikeAnthropic(model=MODEL_NAME)18121813 ai_message = AIMessage(1814 content=[1815 {1816 "type": "tool_use",1817 "id": "toolu_code_exec_1",1818 "name": "noop",1819 "input": {},1820 "caller": {1821 "type": "code_execution_20250825",1822 "tool_id": "srvtoolu_abc",1823 },1824 }1825 ]1826 )18271828 payload = llm._get_request_payload(1829 [HumanMessage("earlier"), ai_message],1830 cache_control={"type": "ephemeral"},1831 )18321833 first_message_content = payload["messages"][0]["content"]1834 assert first_message_content == [1835 {"type": "text", "text": "earlier", "cache_control": {"type": "ephemeral"}}1836 ]1837 last_message_content = payload["messages"][-1]["content"]1838 assert all("cache_control" not in block for block in last_message_content)183918401841def test_cache_control_kwarg_bedrock_no_eligible_block_warns() -> None:1842 """When every candidate is `code_execution`-related, warn and drop the kwarg.18431844 Pins the silent-drop contract: payload remains valid for Anthropic, but1845 the caller is told their cache request was skipped.1846 """1847 llm = _BedrockLikeAnthropic(model=MODEL_NAME)18481849 ai_message = AIMessage(1850 content=[1851 {1852 "type": "tool_use",1853 "id": "toolu_code_exec_1",1854 "name": "noop",1855 "input": {},1856 "caller": {1857 "type": "code_execution_20250825",1858 "tool_id": "srvtoolu_abc",1859 },1860 }1861 ]1862 )18631864 with pytest.warns(UserWarning, match="cache_control.*dropped"):1865 payload = llm._get_request_payload(1866 [ai_message],1867 cache_control={"type": "ephemeral"},1868 )18691870 assert "cache_control" not in payload1871 only_block = payload["messages"][-1]["content"][0]1872 assert "cache_control" not in only_block187318741875def test_cache_control_absent_kwarg_bedrock_is_noop() -> None:1876 """Without a `cache_control` kwarg, the Bedrock branch must not mutate."""1877 llm = _BedrockLikeAnthropic(model=MODEL_NAME)18781879 messages = [HumanMessage("foo"), AIMessage("bar"), HumanMessage("baz")]1880 payload = llm._get_request_payload(messages)18811882 assert "cache_control" not in payload1883 for message in payload["messages"]:1884 content = message["content"]1885 if isinstance(content, list):1886 for block in content:1887 assert "cache_control" not in block188818891890def test_cache_control_kwarg_unknown_subclass_injects_into_blocks() -> None:1891 """Any subclass that overrides `_llm_type` is treated as non-direct.18921893 The gate is allowlist-shaped on `"anthropic-chat"`, so a future subclass1894 routing through a new transport is safe by default rather than silently1895 sending an unsupported top-level field.1896 """18971898 class _FutureTransportAnthropic(ChatAnthropic):1899 @property1900 def _llm_type(self) -> str:1901 return "anthropic-some-future-transport"19021903 llm = _FutureTransportAnthropic(model=MODEL_NAME)1904 payload = llm._get_request_payload(1905 [HumanMessage("hello")],1906 cache_control={"type": "ephemeral"},1907 )19081909 assert "cache_control" not in payload1910 assert payload["messages"][-1]["content"] == [1911 {"type": "text", "text": "hello", "cache_control": {"type": "ephemeral"}}1912 ]191319141915@pytest.mark.parametrize(1916 ("llm_type", "expected"),1917 [1918 ("anthropic-chat", True),1919 ("anthropic-bedrock-chat", False),1920 ("anthropic-chat-vertexai", False),1921 ("", False),1922 ("ANTHROPIC-CHAT", False),1923 (None, False),1924 (object(), False),1925 ],1926)1927def test_is_direct_anthropic_llm_type(llm_type: object, expected: bool) -> None: # noqa: FBT0011928 """Predicate is exact-match and tolerates non-string inputs."""1929 from langchain_anthropic.chat_models import _is_direct_anthropic_llm_type19301931 assert _is_direct_anthropic_llm_type(llm_type) is expected193219331934def test_context_management_in_payload() -> None:1935 llm = ChatAnthropic(1936 model=MODEL_NAME, # type: ignore[call-arg]1937 betas=["context-management-2025-06-27"],1938 context_management={"edits": [{"type": "clear_tool_uses_20250919"}]},1939 )1940 llm_with_tools = llm.bind_tools(1941 [{"type": "web_search_20250305", "name": "web_search"}]1942 )1943 input_message = HumanMessage("Search for recent developments in AI")1944 payload = llm_with_tools._get_request_payload([input_message]) # type: ignore[attr-defined]1945 assert payload["context_management"] == {1946 "edits": [{"type": "clear_tool_uses_20250919"}]1947 }194819491950def test_inference_geo_in_payload() -> None:1951 llm = ChatAnthropic(model=MODEL_NAME, inference_geo="us")1952 input_message = HumanMessage("Hello, world!")1953 payload = llm._get_request_payload([input_message])1954 assert payload["inference_geo"] == "us"195519561957def test_anthropic_model_params() -> None:1958 llm = ChatAnthropic(model=MODEL_NAME)19591960 ls_params = llm._get_ls_params()1961 assert ls_params == {1962 "ls_provider": "anthropic",1963 "ls_model_type": "chat",1964 "ls_model_name": MODEL_NAME,1965 "ls_max_tokens": 64000,1966 "ls_temperature": None,1967 }19681969 ls_params = llm._get_ls_params(model=MODEL_NAME)1970 assert ls_params.get("ls_model_name") == MODEL_NAME197119721973def test_streaming_cache_token_reporting() -> None:1974 """Test that cache tokens are properly reported in streaming events."""1975 from unittest.mock import MagicMock19761977 from anthropic.types import MessageDeltaUsage19781979 # Create a mock message_start event1980 mock_message = MagicMock()1981 mock_message.model = MODEL_NAME1982 mock_message.usage.input_tokens = 1001983 mock_message.usage.output_tokens = 01984 mock_message.usage.cache_read_input_tokens = 251985 mock_message.usage.cache_creation_input_tokens = 1019861987 message_start_event = MagicMock()1988 message_start_event.type = "message_start"1989 message_start_event.message = mock_message19901991 # Create a mock message_delta event with complete usage info1992 mock_delta_usage = MessageDeltaUsage(1993 output_tokens=50,1994 input_tokens=100,1995 cache_read_input_tokens=25,1996 cache_creation_input_tokens=10,1997 )19981999 mock_delta = MagicMock()2000 mock_delta.stop_reason = "end_turn"
Findings
✓ No findings reported for this file.