From 754227899742eeafe15d05e52729eb5f2bcae737 Mon Sep 17 00:00:00 2001 From: Mason Daugherty Date: Wed, 10 Dec 2025 09:37:14 -0500 Subject: [PATCH] feat(core,anthropic): `extras` on `BaseTool` (#34120) --- .../messages/block_translators/anthropic.py | 11 + libs/core/langchain_core/tools/base.py | 18 + libs/core/langchain_core/tools/convert.py | 16 + .../block_translators/test_anthropic.py | 22 + .../tests/unit_tests/chat_models/test_base.py | 1 + .../chat_models/test_chat_models.py | 1 + .../anthropic/langchain_anthropic/_compat.py | 17 +- .../langchain_anthropic/chat_models.py | 977 ++++++++++-------- .../test_programmatic_tool_use.yaml.gz | Bin 0 -> 2564 bytes ...st_programmatic_tool_use_streaming.yaml.gz | Bin 0 -> 3544 bytes .../tests/cassettes/test_tool_search.yaml.gz | Bin 0 -> 2961 bytes .../integration_tests/test_chat_models.py | 264 ++++- .../tests/unit_tests/test_chat_models.py | 134 +++ libs/partners/anthropic/uv.lock | 4 +- 14 files changed, 971 insertions(+), 494 deletions(-) create mode 100644 libs/partners/anthropic/tests/cassettes/test_programmatic_tool_use.yaml.gz create mode 100644 libs/partners/anthropic/tests/cassettes/test_programmatic_tool_use_streaming.yaml.gz create mode 100644 libs/partners/anthropic/tests/cassettes/test_tool_search.yaml.gz diff --git a/libs/core/langchain_core/messages/block_translators/anthropic.py b/libs/core/langchain_core/messages/block_translators/anthropic.py index c5178be45b9..d3a297e2023 100644 --- a/libs/core/langchain_core/messages/block_translators/anthropic.py +++ b/libs/core/langchain_core/messages/block_translators/anthropic.py @@ -248,8 +248,14 @@ def _convert_to_v1_from_anthropic(message: AIMessage) -> list[types.ContentBlock tool_call_chunk: types.ToolCallChunk = ( message.tool_call_chunks[0].copy() # type: ignore[assignment] ) + if "caller" in block: + if "extras" not in tool_call_chunk: + tool_call_chunk["extras"] = {} + tool_call_chunk["extras"]["caller"] = block["caller"] + if "type" not in tool_call_chunk: tool_call_chunk["type"] = "tool_call_chunk" + yield tool_call_chunk else: tool_call_block: types.ToolCall | None = None @@ -282,6 +288,11 @@ def _convert_to_v1_from_anthropic(message: AIMessage) -> list[types.ContentBlock } if "index" in block: tool_call_block["index"] = block["index"] + if "caller" in block: + if "extras" not in tool_call_block: + tool_call_block["extras"] = {} + tool_call_block["extras"]["caller"] = block["caller"] + yield tool_call_block elif block_type == "input_json_delta" and isinstance( diff --git a/libs/core/langchain_core/tools/base.py b/libs/core/langchain_core/tools/base.py index 23cf6f9865b..ad770fea628 100644 --- a/libs/core/langchain_core/tools/base.py +++ b/libs/core/langchain_core/tools/base.py @@ -496,6 +496,24 @@ class ChildTool(BaseTool): two-tuple corresponding to the `(content, artifact)` of a `ToolMessage`. """ + extras: dict[str, Any] | None = None + """Optional provider-specific extra fields for the tool. + + This is used to pass provider-specific configuration that doesn't fit into + standard tool fields. + + Example: + Anthropic-specific fields like [`cache_control`](https://docs.langchain.com/oss/python/integrations/chat/anthropic#prompt-caching), + [`defer_loading`](https://docs.langchain.com/oss/python/integrations/chat/anthropic#tool-search), + or `input_examples`. + + ```python + @tool(extras={"defer_loading": True, "cache_control": {"type": "ephemeral"}}) + def my_tool(x: str) -> str: + return x + ``` + """ + def __init__(self, **kwargs: Any) -> None: """Initialize the tool. diff --git a/libs/core/langchain_core/tools/convert.py b/libs/core/langchain_core/tools/convert.py index 1dabed5e002..be66424c723 100644 --- a/libs/core/langchain_core/tools/convert.py +++ b/libs/core/langchain_core/tools/convert.py @@ -23,6 +23,7 @@ def tool( response_format: Literal["content", "content_and_artifact"] = "content", parse_docstring: bool = False, error_on_invalid_docstring: bool = True, + extras: dict[str, Any] | None = None, ) -> Callable[[Callable | Runnable], BaseTool]: ... @@ -38,6 +39,7 @@ def tool( response_format: Literal["content", "content_and_artifact"] = "content", parse_docstring: bool = False, error_on_invalid_docstring: bool = True, + extras: dict[str, Any] | None = None, ) -> BaseTool: ... @@ -52,6 +54,7 @@ def tool( response_format: Literal["content", "content_and_artifact"] = "content", parse_docstring: bool = False, error_on_invalid_docstring: bool = True, + extras: dict[str, Any] | None = None, ) -> BaseTool: ... @@ -66,6 +69,7 @@ def tool( response_format: Literal["content", "content_and_artifact"] = "content", parse_docstring: bool = False, error_on_invalid_docstring: bool = True, + extras: dict[str, Any] | None = None, ) -> Callable[[Callable | Runnable], BaseTool]: ... @@ -80,6 +84,7 @@ def tool( response_format: Literal["content", "content_and_artifact"] = "content", parse_docstring: bool = False, error_on_invalid_docstring: bool = True, + extras: dict[str, Any] | None = None, ) -> BaseTool | Callable[[Callable | Runnable], BaseTool]: """Convert Python functions and `Runnables` to LangChain tools. @@ -130,6 +135,15 @@ def tool( parse parameter descriptions from Google Style function docstrings. error_on_invalid_docstring: If `parse_docstring` is provided, configure whether to raise `ValueError` on invalid Google Style docstrings. + extras: Optional provider-specific extra fields for the tool. + + Used to pass configuration that doesn't fit into standard tool fields. + Chat models should process known extras when constructing model payloads. + + !!! example + + For example, Anthropic-specific fields like `cache_control`, + `defer_loading`, or `input_examples`. Raises: ValueError: If too many positional arguments are provided (e.g. violating the @@ -292,6 +306,7 @@ def tool( response_format=response_format, parse_docstring=parse_docstring, error_on_invalid_docstring=error_on_invalid_docstring, + extras=extras, ) # If someone doesn't want a schema applied, we must treat it as # a simple string->string function @@ -308,6 +323,7 @@ def tool( return_direct=return_direct, coroutine=coroutine, response_format=response_format, + extras=extras, ) return _tool_factory diff --git a/libs/core/tests/unit_tests/messages/block_translators/test_anthropic.py b/libs/core/tests/unit_tests/messages/block_translators/test_anthropic.py index bae653b1b4e..1b0ec035865 100644 --- a/libs/core/tests/unit_tests/messages/block_translators/test_anthropic.py +++ b/libs/core/tests/unit_tests/messages/block_translators/test_anthropic.py @@ -13,6 +13,16 @@ def test_convert_to_v1_from_anthropic() -> None: "name": "get_weather", "input": {"location": "San Francisco"}, }, + { + "type": "tool_use", + "id": "abc_234", + "name": "get_weather_programmatic", + "input": {"location": "Boston"}, + "caller": { + "type": "code_execution_20250825", + "tool_id": "srvtoolu_abc234", + }, + }, { "type": "text", "text": "It's sunny.", @@ -88,6 +98,18 @@ def test_convert_to_v1_from_anthropic() -> None: "name": "get_weather", "args": {"location": "San Francisco"}, }, + { + "type": "tool_call", + "id": "abc_234", + "name": "get_weather_programmatic", + "args": {"location": "Boston"}, + "extras": { + "caller": { + "type": "code_execution_20250825", + "tool_id": "srvtoolu_abc234", + } + }, + }, { "type": "text", "text": "It's sunny.", diff --git a/libs/langchain/tests/unit_tests/chat_models/test_base.py b/libs/langchain/tests/unit_tests/chat_models/test_base.py index 4b1a4147e46..cd91955f29b 100644 --- a/libs/langchain/tests/unit_tests/chat_models/test_base.py +++ b/libs/langchain/tests/unit_tests/chat_models/test_base.py @@ -269,6 +269,7 @@ def test_configurable_with_default() -> None: "betas": None, "default_headers": None, "model_kwargs": {}, + "reuse_last_container": None, "streaming": False, "stream_usage": True, "output_version": None, diff --git a/libs/langchain_v1/tests/unit_tests/chat_models/test_chat_models.py b/libs/langchain_v1/tests/unit_tests/chat_models/test_chat_models.py index 18a96fff2ee..c4a95c73d44 100644 --- a/libs/langchain_v1/tests/unit_tests/chat_models/test_chat_models.py +++ b/libs/langchain_v1/tests/unit_tests/chat_models/test_chat_models.py @@ -269,6 +269,7 @@ def test_configurable_with_default() -> None: "betas": None, "default_headers": None, "model_kwargs": {}, + "reuse_last_container": None, "streaming": False, "stream_usage": True, "output_version": None, diff --git a/libs/partners/anthropic/langchain_anthropic/_compat.py b/libs/partners/anthropic/langchain_anthropic/_compat.py index d16f5c77274..36495cd6632 100644 --- a/libs/partners/anthropic/langchain_anthropic/_compat.py +++ b/libs/partners/anthropic/langchain_anthropic/_compat.py @@ -113,14 +113,15 @@ def _convert_from_v1_to_anthropic( new_content.append(new_block) elif block["type"] == "tool_call": - new_content.append( - { - "type": "tool_use", - "name": block.get("name", ""), - "input": block.get("args", {}), - "id": block.get("id", ""), - } - ) + tool_use_block = { + "type": "tool_use", + "name": block.get("name", ""), + "input": block.get("args", {}), + "id": block.get("id", ""), + } + if "caller" in block.get("extras", {}): + tool_use_block["caller"] = block["extras"]["caller"] + new_content.append(tool_use_block) elif block["type"] == "tool_call_chunk": if isinstance(block["args"], str): diff --git a/libs/partners/anthropic/langchain_anthropic/chat_models.py b/libs/partners/anthropic/langchain_anthropic/chat_models.py index 4b2e313b381..15c1563d47c 100644 --- a/libs/partners/anthropic/langchain_anthropic/chat_models.py +++ b/libs/partners/anthropic/langchain_anthropic/chat_models.py @@ -3,6 +3,7 @@ from __future__ import annotations import copy +import datetime import json import re import warnings @@ -105,6 +106,12 @@ class AnthropicTool(TypedDict): cache_control: NotRequired[dict[str, str]] + defer_loading: NotRequired[bool] + + input_examples: NotRequired[list[dict[str, Any]]] + + allowed_callers: NotRequired[list[str]] + # Some tool types require specific beta headers to be enabled # Mapping of tool type patterns to required beta headers @@ -119,6 +126,14 @@ _TOOL_TYPE_TO_BETA: dict[str, str] = { "tool_search_tool_bm25_20251119": "advanced-tool-use-2025-11-20", } +# Allowlist of valid Anthropic-specific extra fields +_ANTHROPIC_EXTRA_FIELDS: set[str] = { + "allowed_callers", + "cache_control", + "defer_loading", + "input_examples", +} + def _is_builtin_tool(tool: Any) -> bool: """Check if a tool is a built-in Anthropic tool. @@ -420,9 +435,11 @@ def _format_messages( elif block["type"] == "tool_use": # If a tool_call with the same id as a tool_use content block # exists, the tool_call is preferred. - if isinstance(message, AIMessage) and block["id"] in [ - tc["id"] for tc in message.tool_calls - ]: + if ( + isinstance(message, AIMessage) + and (block["id"] in [tc["id"] for tc in message.tool_calls]) + and "caller" not in block # take caller from content + ): overlapping = [ tc for tc in message.tool_calls @@ -443,14 +460,15 @@ def _format_messages( args = {} else: args = {} - content.append( - _AnthropicToolUse( - type="tool_use", - name=block["name"], - input=args, - id=block["id"], - ) + tool_use_block = _AnthropicToolUse( + type="tool_use", + name=block["name"], + input=args, + id=block["id"], ) + if "caller" in block: + tool_use_block["caller"] = block["caller"] + content.append(tool_use_block) elif block["type"] in ("server_tool_use", "mcp_tool_use"): formatted_block = { k: v @@ -655,8 +673,6 @@ class ChatAnthropic(BaseChatModel): * [`base_url`][langchain_anthropic.chat_models.ChatAnthropic.anthropic_api_url]: Base URL for API requests. Only specify if using a proxy or service emulator. - See full list of supported init args and their descriptions below. - ???+ example "Instantiate" ```python @@ -674,10 +690,11 @@ class ChatAnthropic(BaseChatModel): ) ``` - ???+ note + ???+ note "Unsupported params" Any param which is not explicitly supported will be passed directly to - `Anthropic.messages.create(...)` each time to the model is invoked. + [`Anthropic.messages.create(...)`](https://platform.claude.com/docs/en/api/python/messages/create) + each time to the model is invoked. !!! example @@ -794,7 +811,13 @@ class ChatAnthropic(BaseChatModel): ) ``` - ???+ example "Tool calling" + ???+ example "Token counting" + + You can count tokens in messages before sending them to the model using the + [`get_num_tokens_from_messages()`][langchain_anthropic.chat_models.ChatAnthropic.get_num_tokens_from_messages] + method, which uses Anthropic's official token counting API. + + ???+ example "Tools" ```python hl_lines="16" from pydantic import BaseModel, Field @@ -853,210 +876,243 @@ class ChatAnthropic(BaseChatModel): See [`ChatAnthropic.bind_tools()`][langchain_anthropic.chat_models.ChatAnthropic.bind_tools] for more info. - ???+ example "Token-efficient tool use (beta)" + ???+ example "Token-efficient tool use" - See LangChain [docs](https://docs.langchain.com/oss/python/integrations/chat/anthropic#token-efficient-tool-use) - for more detail. + See LangChain [docs](https://docs.langchain.com/oss/python/integrations/chat/anthropic#token-efficient-tool-use) + for more detail. - ```python hl_lines="9" - from langchain_anthropic import ChatAnthropic - from langchain_core.tools import tool + ```python hl_lines="9" + from langchain_anthropic import ChatAnthropic + from langchain_core.tools import tool - model = ChatAnthropic( - model="claude-sonnet-4-5-20250929", - temperature=0, - model_kwargs={ - "extra_headers": { - "anthropic-beta": "token-efficient-tools-2025-02-19" + model = ChatAnthropic( + model="claude-sonnet-4-5-20250929", + temperature=0, + model_kwargs={ + "extra_headers": { + "anthropic-beta": "token-efficient-tools-2025-02-19" + } } - } - ) + ) - @tool - def get_weather(location: str) -> str: - \"\"\"Get the weather at a location.\"\"\" - return "It's sunny." + @tool + def get_weather(location: str) -> str: + \"\"\"Get the weather at a location.\"\"\" + return "It's sunny." - model_with_tools = model.bind_tools([get_weather]) - response = model_with_tools.invoke( - "What's the weather in San Francisco?" - ) - print(response.tool_calls) - print(f'Total tokens: {response.usage_metadata["total_tokens"]}') - ``` + model_with_tools = model.bind_tools([get_weather]) + response = model_with_tools.invoke( + "What's the weather in San Francisco?" + ) + print(response.tool_calls) + print(f'Total tokens: {response.usage_metadata["total_tokens"]}') + ``` - ```txt - [{'name': 'get_weather', 'args': {'location': 'San Francisco'}, 'id': 'toolu_01HLjQMSb1nWmgevQUtEyz17', 'type': 'tool_call'}] - Total tokens: 408 - ``` + ```txt + [{'name': 'get_weather', 'args': {'location': 'San Francisco'}, 'id': 'toolu_01HLjQMSb1nWmgevQUtEyz17', 'type': 'tool_call'}] + Total tokens: 408 + ``` - ???+ example "Fine-grained tool streaming" + ???+ example "Fine-grained tool streaming" - Fine-grained tool streaming enables faster streaming of tool parameters - without buffering or JSON validation, reducing latency when receiving large tool - parameters. + Fine-grained tool streaming enables faster streaming of tool parameters + without buffering or JSON validation, reducing latency when receiving large tool + parameters. For more details, see the + [LangChain docs](https://docs.langchain.com/oss/python/integrations/chat/anthropic#fine-grained-tool-streaming). - More info available in the [Claude docs](https://platform.claude.com/docs/en/agents-and-tools/tool-use/fine-grained-tool-streaming) + ```python hl_lines="5" + from langchain_anthropic import ChatAnthropic - ```python hl_lines="5" - from langchain_anthropic import ChatAnthropic + model = ChatAnthropic( + model="claude-3-5-sonnet-20241022", + betas=["fine-grained-tool-streaming-2025-05-14"] + ) - model = ChatAnthropic( - model="claude-3-5-sonnet-20241022", - betas=["fine-grained-tool-streaming-2025-05-14"] - ) + def write_document(title: str, content: str) -> str: + \"\"\"Write a document with the given title and content.\"\"\" + return f"Document '{title}' written" - def write_document(title: str, content: str) -> str: - \"\"\"Write a document with the given title and content.\"\"\" - return f"Document '{title}' written" + model_with_tools = model.bind_tools([write_document]) - model_with_tools = model.bind_tools([write_document]) + # Stream tool calls with reduced latency + for chunk in model_with_tools.stream( + "Write a document about the benefits of streaming APIs" + ): + print(chunk) + ``` - # Stream tool calls with reduced latency - for chunk in model_with_tools.stream( - "Write a document about the benefits of streaming APIs" - ): - print(chunk) - ``` + !!! note - !!! note + This is a beta feature that may return invalid or partial JSON inputs. - This is a beta feature that may return invalid or partial JSON inputs. - - Implement appropriate error handling for incomplete JSON, especially - when `max_tokens` is reached. + Implement appropriate error handling for incomplete JSON, especially + when `max_tokens` is reached. ???+ example "Image input" - See the [multimodal guide](https://docs.langchain.com/oss/python/langchain/models#multimodal) + See the [LangChain docs](https://docs.langchain.com/oss/python/integrations/chat/anthropic#multimodal) for more detail. - ```python - import base64 + ??? example "URL" - import httpx - from langchain_anthropic import ChatAnthropic - from langchain_core.messages import HumanMessage + ```python + from langchain_anthropic import ChatAnthropic + from langchain_core.messages import HumanMessage - 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 = base64.b64encode(httpx.get(image_url).content).decode("utf-8") + model = ChatAnthropic(model="claude-sonnet-4-5-20250929") - model = ChatAnthropic(model="claude-sonnet-4-5-20250929") - message = HumanMessage( - content=[ - { - "type": "text", - "text": "Can you highlight the differences between these two images?", - }, - { - "type": "image", - "base64": image_data, - "mime_type": "image/jpeg", - }, - { - "type": "image", - "url": image_url, - }, - ], - ) - ai_msg = model.invoke([message]) - ai_msg.content - ``` + message = HumanMessage( + content=[ + {"type": "text", "text": "Describe the image at the URL."}, + { + "type": "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", + }, + ] + ) + response = model.invoke([message]) + ``` - ```python - "After examining both images carefully, I can see that they are actually identical." - ``` + ??? example "Base64 encoded" - ??? example "Upload with Files API" + ```python + import base64 + import httpx + from langchain_anthropic import ChatAnthropic + from langchain_core.messages import HumanMessage + + model = ChatAnthropic(model="claude-sonnet-4-5-20250929") + + image_url = "https://picsum.photos/id/237/200/300" + image_data = base64.b64encode(httpx.get(image_url, follow_redirects=True).content).decode("utf-8") + + message = HumanMessage( + content=[ + {"type": "text", "text": "Describe the image."}, + { + "type": "image", + "base64": image_data, + "mime_type": "image/jpeg", + }, + ] + ) + response = model.invoke([message]) + ``` + + ??? example "Files API" You can also pass in files that are managed through Anthropic's [Files API](https://platform.claude.com/docs/en/build-with-claude/files): ```python + import anthropic from langchain_anthropic import ChatAnthropic + from langchain_core.messages import HumanMessage + + client = anthropic.Anthropic() + file = client.beta.files.upload( + file=("image.png", open("/path/to/image.png", "rb"), "image/png"), + ) model = ChatAnthropic( model="claude-sonnet-4-5-20250929", betas=["files-api-2025-04-14"], ) - input_message = { - "role": "user", - "content": [ - { - "type": "text", - "text": "Describe this document.", - }, + + message = HumanMessage( + content=[ + {"type": "text", "text": "Describe this image."}, { "type": "image", - "id": "file_abc123...", + "file_id": file.id, }, - ], - } - model.invoke([input_message]) + ] + ) + response = model.invoke([message]) ``` ???+ example "PDF input" - See the [multimodal guide](https://docs.langchain.com/oss/python/langchain/models#multimodal) + See the [LangChain docs](https://docs.langchain.com/oss/python/integrations/chat/anthropic#multimodal) for more detail. - ```python - from base64 import b64encode - from langchain_anthropic import ChatAnthropic - from langchain_core.messages import HumanMessage - import requests + ??? example "URL" - url = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf" - data = b64encode(requests.get(url).content).decode() + ```python + from langchain_anthropic import ChatAnthropic + from langchain_core.messages import HumanMessage - model = ChatAnthropic(model="claude-sonnet-4-5-20250929") - ai_msg = model.invoke( - [ - HumanMessage( - [ - "Summarize this document.", - { - "type": "file", - "mime_type": "application/pdf", - "base64": data, - }, - ] - ) - ] - ) - ai_msg.content - ``` + model = ChatAnthropic(model="claude-sonnet-4-5-20250929") - ```python - "This appears to be a simple document..." - ``` + message = HumanMessage( + content=[ + {"type": "text", "text": "Summarize this document."}, + { + "type": "file", + "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf", + "mime_type": "application/pdf", + }, + ] + ) + response = model.invoke([message]) + ``` - ??? example "Upload with Files API" + ??? example "Base64 encoded" + + ```python + import base64 + import httpx + from langchain_anthropic import ChatAnthropic + from langchain_core.messages import HumanMessage + + model = ChatAnthropic(model="claude-sonnet-4-5-20250929") + + pdf_url = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf" + pdf_data = base64.b64encode(httpx.get(pdf_url).content).decode("utf-8") + + message = HumanMessage( + content=[ + {"type": "text", "text": "Summarize this document."}, + { + "type": "file", + "base64": pdf_data, + "mime_type": "application/pdf", + }, + ] + ) + response = model.invoke([message]) + ``` + + ??? example "Files API" You can also pass in files that are managed through Anthropic's [Files API](https://platform.claude.com/docs/en/build-with-claude/files): ```python + import anthropic from langchain_anthropic import ChatAnthropic + from langchain_core.messages import HumanMessage + + client = anthropic.Anthropic() + file = client.beta.files.upload( + file=("document.pdf", open("/path/to/document.pdf", "rb"), "application/pdf"), + ) model = ChatAnthropic( model="claude-sonnet-4-5-20250929", betas=["files-api-2025-04-14"], ) - input_message = { - "role": "user", - "content": [ - { - "type": "text", - "text": "Describe this document.", - }, + + message = HumanMessage( + content=[ + {"type": "text", "text": "Summarize this document."}, { "type": "file", - "id": "file_abc123...", + "file_id": file.id, }, - ], - } - model.invoke([input_message]) + ] + ) + response = model.invoke([message]) ``` ???+ example "Extended thinking" @@ -1064,13 +1120,16 @@ class ChatAnthropic(BaseChatModel): Certain [Claude models](https://platform.claude.com/docs/en/build-with-claude/extended-thinking#supported-models) support an [extended thinking](https://platform.claude.com/docs/en/build-with-claude/extended-thinking) feature, which will output the step-by-step reasoning process that led to its - final answer. + final answer. See the [LangChain docs](https://docs.langchain.com/oss/python/integrations/chat/anthropic#extended-thinking) + for more detail. - To use it, specify the `thinking` parameter when initializing `ChatAnthropic`. + !!! warning "Differences in thinking across model versions" - It can also be passed in as a kwarg during invocation. + The Claude Messages API handles thinking differently across Claude Sonnet + 3.7 and Claude 4 models. - **You will need to specify a token budget** to use this feature. + Refer to the [Claude docs](https://platform.claude.com/docs/en/build-with-claude/extended-thinking#differences-in-thinking-across-model-versions) + for more info. !!! example @@ -1098,18 +1157,12 @@ class ChatAnthropic(BaseChatModel): ] ``` - !!! warning "Differences in thinking across model versions" - - The Claude Messages API handles thinking differently across Claude Sonnet - 3.7 and Claude 4 models. - - Refer to the [Claude docs](https://platform.claude.com/docs/en/build-with-claude/extended-thinking#differences-in-thinking-across-model-versions) - for more info. - ???+ example "Effort" Certain Claude models support an [effort](https://platform.claude.com/docs/en/build-with-claude/effort) - feature, which will control how many tokens Claude uses when responding. + feature, which will control how many tokens Claude uses when responding. See the + [LangChain docs](https://docs.langchain.com/oss/python/integrations/chat/anthropic#effort) + for more detail. !!! example @@ -1126,18 +1179,11 @@ class ChatAnthropic(BaseChatModel): print(response.content) ``` - See the [Claude docs](https://platform.claude.com/docs/en/build-with-claude/effort) - for more detail on when to use different effort levels. - ???+ example "Prompt caching" Prompt caching reduces processing time and costs for repetitive tasks or prompts - with consistent elements - - !!! note - Only certain models support prompt caching. - See the [Claude documentation](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#supported-models) - for a full list. + with consistent elements. See the [LangChain docs](https://docs.langchain.com/oss/python/integrations/chat/anthropic#prompt-caching) + for more detail. ```python hl_lines="16" from langchain_anthropic import ChatAnthropic @@ -1236,6 +1282,23 @@ class ChatAnthropic(BaseChatModel): See [Claude documentation](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#1-hour-cache-duration-beta) for detail. + ???+ example "Response metadata" + + ```python + ai_msg = model.invoke(messages) + ai_msg.response_metadata + ``` + + ```python + { + "id": "msg_013xU6FHEGEq76aP4RgFerVT", + "model": "claude-sonnet-4-5-20250929", + "stop_reason": "end_turn", + "stop_sequence": None, + "usage": {"input_tokens": 25, "output_tokens": 11}, + } + ``` + ???+ example "Token usage metadata" ```python @@ -1275,6 +1338,9 @@ class ChatAnthropic(BaseChatModel): with `#!json "citations": {"enabled": True}` included in the query, Claude may generate citations in its response. + See the [LangChain docs](https://docs.langchain.com/oss/python/integrations/chat/anthropic#citations) + for more detail. + ```python hl_lines="9-19" from langchain_anthropic import ChatAnthropic @@ -1342,10 +1408,9 @@ class ChatAnthropic(BaseChatModel): ???+ example "Context management" Anthropic supports a context editing feature that will automatically manage the - model's context window (e.g., by clearing tool results). - - See [Anthropic documentation](https://platform.claude.com/docs/en/build-with-claude/context-editing) - for details and configuration options. + model's context window (e.g., by clearing tool results). See the + [LangChain docs](https://docs.langchain.com/oss/python/integrations/chat/anthropic#context-management) + for more detail. ```python hl_lines="5-6" from langchain_anthropic import ChatAnthropic @@ -1359,27 +1424,12 @@ class ChatAnthropic(BaseChatModel): response = model_with_tools.invoke("Search for recent developments in AI") ``` - ???+ example "Response metadata" - - ```python - ai_msg = model.invoke(messages) - ai_msg.response_metadata - ``` - - ```python - { - "id": "msg_013xU6FHEGEq76aP4RgFerVT", - "model": "claude-sonnet-4-5-20250929", - "stop_reason": "end_turn", - "stop_sequence": None, - "usage": {"input_tokens": 25, "output_tokens": 11}, - } - ``` - - ???+ example "Extended context windows (beta)" + ???+ example "Extended context window" Claude Sonnet 4 supports a 1-million token context window, available in beta for - organizations in usage tier 4 and organizations with custom rate limits. + organizations in usage tier 4 and organizations with custom rate limits. See + the [LangChain docs](https://docs.langchain.com/oss/python/integrations/chat/anthropic#extended-context-window) + for more detail. ```python hl_lines="5" from langchain_anthropic import ChatAnthropic @@ -1408,11 +1458,11 @@ class ChatAnthropic(BaseChatModel): response = model.invoke(messages) ``` - See [Claude documentation](https://platform.claude.com/docs/en/build-with-claude/context-windows#1m-token-context-window) - for detail. - ???+ example "Structured output" + See [`ChatAnthropic.with_structured_output()`][langchain_anthropic.chat_models.ChatAnthropic.with_structured_output] + for more info, including strict output validation. + ```python hl_lines="13" from typing import Optional from pydantic import BaseModel, Field @@ -1438,72 +1488,39 @@ class ChatAnthropic(BaseChatModel): ) ``` - See [`ChatAnthropic.with_structured_output()`][langchain_anthropic.chat_models.ChatAnthropic.with_structured_output] - for more info. - - !!! note "Native structured output" - - Anthropic supports a native structured output feature that guarantees - responses adhere to a given schema. - - See [`ChatAnthropic.with_structured_output()`][langchain_anthropic.chat_models.ChatAnthropic.with_structured_output] - for more info. - ???+ example "Built-in tools" See LangChain [docs](https://docs.langchain.com/oss/python/integrations/chat/anthropic#built-in-tools) for more detail. - ??? example "Web search" + ??? example "Bash tool" - ```python hl_lines="5-9" + Claude supports a [bash tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool) + that allows it to execute shell commands in a persistent bash session. See + the LangChain [docs](https://docs.langchain.com/oss/python/integrations/chat/anthropic#bash-tool) + for more detail. + + ```python from langchain_anthropic import ChatAnthropic - model = ChatAnthropic(model="claude-3-5-haiku-20241022") + model = ChatAnthropic(model="claude-sonnet-4-5-20250929") - tool = { - "type": "web_search_20250305", - "name": "web_search", - "max_uses": 3, + bash_tool = { + "type": "bash_20250124", + "name": "bash", } - model_with_tools = model.bind_tools([tool]) - response = model_with_tools.invoke("How do I update a web app to TypeScript 5.5?") + model_with_bash = model.bind_tools([bash_tool]) + response = model_with_bash.invoke("List all Python files in the current directory") ``` - See the [Claude docs](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool) - for more info. - - ??? example "Web fetch (beta)" - - ```python hl_lines="7-11" - from langchain_anthropic import ChatAnthropic - - model = ChatAnthropic( - model="claude-3-5-haiku-20241022", - ) - - tool = { - "type": "web_fetch_20250910", - "name": "web_fetch", - "max_uses": 3, - } - model_with_tools = model.bind_tools([tool]) - - response = model_with_tools.invoke("Please analyze the content at https://example.com/article") - ``` - - !!! note "Automatic beta header" - - The required `web-fetch-2025-09-10` beta header is automatically - appended to the request when using the `web_fetch_20250910` tool type. - You don't need to manually specify it in the `betas` parameter. - - See the [Claude docs](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-fetch-tool) - for more info. - ??? example "Code execution" + Claude supports a [code execution tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool) + that allows it to execute code snippets in a secure, sandboxed environment. See the + LangChain [docs](https://docs.langchain.com/oss/python/integrations/chat/anthropic#code-execution) + for more detail. + ```python hl_lines="3-6" model = ChatAnthropic(model="claude-sonnet-4-5-20250929") @@ -1520,73 +1537,94 @@ class ChatAnthropic(BaseChatModel): !!! note "Automatic beta header" - The required `code-execution-2025-05-22` beta header is automatically - appended to the request when using the `code_execution_20250522` tool - type. You don't need to manually specify it in the `betas` parameter. + The required `code-execution-2025-05-22` or `code-execution-2025-08-25` + beta header is automatically appended to the request when using the + `code_execution_20250522` or `code_execution_20250825` tool type, + respectively. You don't need to manually specify it in the `betas` + parameter. - See the [Claude docs](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool) - for more info. + ??? example "Computer use" - ??? example "Memory tool" + Claude supports [computer use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/computer-use-tool) + capabilities, allowing it to interact with desktop environments through + screenshots, mouse control, and keyboard input. See the LangChain + [docs](https://docs.langchain.com/oss/python/integrations/chat/anthropic#computer-use) + for more detail. - ```python hl_lines="5-8" + ```python from langchain_anthropic import ChatAnthropic model = ChatAnthropic(model="claude-sonnet-4-5-20250929") - tool = { - "type": "memory_20250818", - "name": "memory", + computer_tool = { + "type": "computer_20250124", + "name": "computer", + "display_width_px": 1024, + "display_height_px": 768, + "display_number": 1, } - model_with_tools = model.bind_tools([tool]) - response = model_with_tools.invoke("What are my interests?") + model_with_computer = model.bind_tools([computer_tool]) + response = model_with_computer.invoke("Take a screenshot to see what's on the screen") + + # response.tool_calls contains the action Claude wants to perform + # You must execute this action in your environment and pass the result back ``` !!! note "Automatic beta header" - The required `context-management-2025-06-27` beta header is automatically - appended to the request when using the `memory_20250818` tool type. - You don't need to manually specify it in the `betas` parameter. - - See the [Claude docs](https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool) - for more info. + The required beta header is automatically appended based on the tool + version. For `computer_20250124` and `computer_20251124`, the respective + `computer-use-2025-01-24` and `computer-use-2025-11-24` beta header is + added automatically. ??? example "Remote MCP" - ```python hl_lines="3-14 18-19" + Claude can use a [MCP connector tool](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector) + for model-generated calls to remote MCP servers. See the LangChain + [docs](https://docs.langchain.com/oss/python/integrations/chat/anthropic#remote-mcp) + for more detail. + + ```python hl_lines="3-14 18 23" from langchain_anthropic import ChatAnthropic mcp_servers = [ { "type": "url", - "url": "https://mcp.deepwiki.com/mcp", - "name": "deepwiki", - "tool_configuration": { # optional configuration - "enabled": True, - "allowed_tools": ["ask_question"], - }, - "authorization_token": "PLACEHOLDER", # optional authorization + "url": "https://docs.langchain.com/mcp", + "name": "LangChain Docs", + # "tool_configuration": { # optional configuration + # "enabled": True, + # "allowed_tools": ["ask_question"], + # }, + # "authorization_token": "PLACEHOLDER", # optional authorization } ] model = ChatAnthropic( model="claude-sonnet-4-5-20250929", - betas=["mcp-client-2025-04-04"], # Enable MCP client beta - mcp_servers=mcp_servers, # Pass in MCP server configurations + mcp_servers=mcp_servers, ) response = model.invoke( - "What transport protocols does the 2025-03-26 version of the MCP " - "spec (modelcontextprotocol/modelcontextprotocol) support?" + "What are LangChain content blocks?", + tools=[{"type": "mcp_toolset", "mcp_server_name": "LangChain Docs"}], ) ``` - See the [Claude docs](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector) - for more info. + !!! note "Automatic beta header" + + The required `mcp-client-2025-11-20` beta header is automatically + appended to the request when using `mcp_servers`. You don't need to + manually specify it in the `betas` parameter. ??? example "Text editor" + Claude supports a [text editor tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/text-editor-tool) + that allows it to read and modify files in a code repository. See the + LangChain [docs](https://docs.langchain.com/oss/python/integrations/chat/anthropic#text-editor) + for more detail. + ```python hl_lines="5-8" from langchain_anthropic import ChatAnthropic @@ -1616,60 +1654,117 @@ class ChatAnthropic(BaseChatModel): 'type': 'tool_call'}] ``` - See the [Claude docs](https://platform.claude.com/docs/en/agents-and-tools/tool-use/text-editor-tool) + ??? example "Web fetch" + + Claude can use a [web fetching tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-fetch-tool) + to retrieve full content from specified web pages and PDF documents and + ground its responses with citations. See the LangChain + [docs](https://docs.langchain.com/oss/python/integrations/chat/anthropic#web-fetch) + for more detail. + + ```python hl_lines="5-9" + from langchain_anthropic import ChatAnthropic + + model = ChatAnthropic(model="claude-haiku-4-5-20251001") + + tool = { + "type": "web_fetch_20250910", + "name": "web_fetch", + "max_uses": 3, + } + model_with_tools = model.bind_tools([tool]) + + response = model_with_tools.invoke("Please analyze the content at https://docs.langchain.com/") + ``` + + !!! note "Automatic beta header" + + The required `web-fetch-2025-09-10` beta header is automatically + appended to the request when using the `web_fetch_20250910` tool type. + You don't need to manually specify it in the `betas` parameter. + + ??? example "Web search" + + Claude can use a [web search tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool) + to run searches and ground its responses with citations. See the LangChain + [docs](https://docs.langchain.com/oss/python/integrations/chat/anthropic#web-search) + for more detail. + + ```python hl_lines="5-9" + from langchain_anthropic import ChatAnthropic + + model = ChatAnthropic(model="claude-sonnet-4-5-20250929") + + tool = { + "type": "web_search_20250305", + "name": "web_search", + "max_uses": 3, + } + model_with_tools = model.bind_tools([tool]) + + response = model_with_tools.invoke("How do I update a web app to TypeScript 5.5?") + ``` + + ??? example "Memory tool" + + ```python hl_lines="5-8" + from langchain_anthropic import ChatAnthropic + + model = ChatAnthropic(model="claude-sonnet-4-5-20250929") + + tool = { + "type": "memory_20250818", + "name": "memory", + } + model_with_tools = model.bind_tools([tool]) + + response = model_with_tools.invoke("What are my interests?") + ``` + + !!! note "Automatic beta header" + + The required `context-management-2025-06-27` beta header is automatically + appended to the request when using the `memory_20250818` tool type. + You don't need to manually specify it in the `betas` parameter. + + See the [Claude docs](https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool) for more info. ??? example "Tool search" Tool search enables Claude to dynamically discover and load tools on-demand - instead of loading all tool definitions upfront. See the + instead of loading all tool definitions upfront. Use the `extras` parameter to + specify `defer_loading` on LangChain tools. + + See the [LangChain docs](https://docs.langchain.com/oss/python/integrations/chat/anthropic#tool-search) for more detail. - ```python hl_lines="8-11 26 36" + ```python hl_lines="4 10" from langchain_anthropic import ChatAnthropic + from langchain_core.tools import tool - model = ChatAnthropic( - model="claude-sonnet-4-5-20250929", - ) + @tool(extras={"defer_loading": True}) + def get_weather(location: str, unit: str = "fahrenheit") -> str: + \"\"\"Get the current weather for a location.\"\"\" + return f"Weather in {location}: Sunny, 72°{unit[0].upper()}" - tools = [ + @tool(extras={"defer_loading": True}) + def search_files(query: str) -> str: + \"\"\"Search through files in the workspace.\"\"\" + return f"Found 3 files matching '{query}'" + + model = ChatAnthropic(model="claude-sonnet-4-5-20250929") + + model_with_tools = model.bind_tools([ { "type": "tool_search_tool_regex_20251119", "name": "tool_search_tool_regex", }, - { - "name": "get_weather", - "description": "Get the current weather for a location", - "input_schema": { - "type": "object", - "properties": { - "location": {"type": "string", "description": "City name"}, - "unit": { - "type": "string", - "enum": ["celsius", "fahrenheit"], - }, - }, - "required": ["location"], - }, - "defer_loading": True, # Tool is loaded on-demand - }, - { - "name": "search_files", - "description": "Search through files in the workspace", - "input_schema": { - "type": "object", - "properties": { - "query": {"type": "string"}, - }, - "required": ["query"], - }, - "defer_loading": True, # Tool is loaded on-demand - }, - ..., - ] + get_weather, + search_files, + ]) - model_with_tools = model.bind_tools(tools) response = model_with_tools.invoke("What's the weather in San Francisco?") ``` @@ -1696,7 +1791,14 @@ class ChatAnthropic(BaseChatModel): """Model name to use.""" max_tokens: int | None = Field(default=None, alias="max_tokens_to_sample") - """Denotes the number of tokens to predict per generation.""" + """Denotes the number of tokens to predict per generation. + + If not specified, this is set dynamically using the model's `max_output_tokens` + from its model profile. + + See docs on [model profiles](https://docs.langchain.com/oss/python/langchain/models#model-profiles) + for more information. + """ temperature: float | None = None """A non-negative float that tunes the degree of randomness in generation.""" @@ -1815,6 +1917,16 @@ class ChatAnthropic(BaseChatModel): [context management](https://platform.claude.com/docs/en/build-with-claude/context-editing). """ + reuse_last_container: bool | None = None + """Automatically reuse container from most recent response (code execution). + + When using the built-in + [code execution tool](https://docs.langchain.com/oss/python/integrations/chat/anthropic#code-execution), + model responses will include container metadata. Set `reuse_last_container=True` + to automatically reuse the container from the most recent response for subsequent + invocations. + """ + @property def _llm_type(self) -> str: """Return type of chat model.""" @@ -2076,6 +2188,19 @@ class ChatAnthropic(BaseChatModel): else: payload["betas"] = ["structured-outputs-2025-11-13"] + if self.reuse_last_container: + # Check for most recent AIMessage with container set in response_metadata + # and set as a top-level param on the request + for message in reversed(messages): + if ( + isinstance(message, AIMessage) + and (container := message.response_metadata.get("container")) + and isinstance(container, dict) + and (container_id := container.get("id")) + ): + payload["container"] = container_id + break + # Check if any tools have strict mode enabled if "tools" in payload and isinstance(payload["tools"], list): has_strict_tool = any( @@ -2094,18 +2219,33 @@ class ChatAnthropic(BaseChatModel): else: payload["betas"] = ["structured-outputs-2025-11-13"] - # Auto-append required betas for specific tool types + # Auto-append required betas for specific tool types and input_examples + has_input_examples = False for tool in payload["tools"]: - if isinstance(tool, dict) and "type" in tool: - tool_type = tool["type"] - if tool_type in _TOOL_TYPE_TO_BETA: + if isinstance(tool, dict): + tool_type = tool.get("type") + if tool_type and tool_type in _TOOL_TYPE_TO_BETA: required_beta = _TOOL_TYPE_TO_BETA[tool_type] if payload["betas"]: - # Append to existing betas if not already present if required_beta not in payload["betas"]: - payload["betas"] = [*payload["betas"], required_beta] + payload["betas"] = [ + *payload["betas"], + required_beta, + ] else: payload["betas"] = [required_beta] + # Check for input_examples + if tool.get("input_examples"): + has_input_examples = True + + # Auto-append header for input_examples + if has_input_examples: + required_beta = "advanced-tool-use-2025-11-20" + if payload["betas"]: + if required_beta not in payload["betas"]: + payload["betas"] = [*payload["betas"], required_beta] + else: + payload["betas"] = [required_beta] # Auto-append required beta for mcp_servers if payload.get("mcp_servers"): @@ -2225,6 +2365,14 @@ class ChatAnthropic(BaseChatModel): llm_output = { k: v for k, v in data_dict.items() if k not in ("content", "role", "type") } + if ( + (container := llm_output.get("container")) + and isinstance(container, dict) + and (expires_at := container.get("expires_at")) + and isinstance(expires_at, datetime.datetime) + ): + # TODO: dump all `data` with `mode="json"` + llm_output["container"]["expires_at"] = expires_at.isoformat() response_metadata = {"model_provider": "anthropic"} if "model" in llm_output and "model_name" not in llm_output: llm_output["model_name"] = llm_output["model"] @@ -2316,13 +2464,13 @@ class ChatAnthropic(BaseChatModel): strict: bool | None = None, **kwargs: Any, ) -> Runnable[LanguageModelInput, AIMessage]: - r"""Bind tool-like objects to this chat model. + r"""Bind tool-like objects to `ChatAnthropic`. Args: tools: A list of tool definitions to bind to this chat model. Supports Anthropic format tool schemas and any tool definition handled - by `langchain_core.utils.function_calling.convert_to_openai_tool`. + by [`convert_to_openai_tool`][langchain_core.utils.function_calling.convert_to_openai_tool]. tool_choice: Which tool to require the model to call. Options are: - Name of the tool as a string or as dict `{"type": "tool", "name": "<>"}`: calls corresponding tool @@ -2335,7 +2483,7 @@ class ChatAnthropic(BaseChatModel): !!! version-added "Added in `langchain-anthropic` 0.3.2" strict: If `True`, Claude's schema adherence is applied to tool calls. - See the [Claude docs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs#when-to-use-json-outputs-vs-strict-tool-use). + See the [docs](https://docs.langchain.com/oss/python/integrations/chat/anthropic#strict-tool-use) for more info. kwargs: Any additional parameters are passed directly to `bind`. ???+ example @@ -2547,60 +2695,6 @@ class ChatAnthropic(BaseChatModel): ) ``` - ??? example "Computer use tool" - - Claude supports computer use capabilities, allowing it to interact with - desktop environments through screenshots, mouse control, and keyboard input. - - !!! warning "Execution environment required" - - LangChain handles the API integration, but **you must provide**: - - - A sandboxed computing environment (Docker, VM, etc.) - - A virtual display (e.g., Xvfb) - - Code to execute tool calls (screenshot, clicks, typing) - - An agent loop to pass results back to Claude - - Anthropic provides a [reference implementation](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo). - - !!! note - - Computer use requires: - - - Claude Opus 4.5, Claude 4, or Claude Sonnet 3.7 - - A sandboxed computing environment with virtual display - - See the [Claude docs](https://platform.claude.com/docs/en/agents-and-tools/tool-use/computer-use-tool) - for setup instructions, model capability, and best practices. - - ```python - from langchain_anthropic import ChatAnthropic - - model = ChatAnthropic(model="claude-sonnet-4-5-20250929") - - # LangChain handles the API call and tool binding - computer_tool = { - "type": "computer_20250124", - "name": "computer", - "display_width_px": 1024, - "display_height_px": 768, - "display_number": 1, - } - - model_with_computer = model.bind_tools([computer_tool]) - response = model_with_computer.invoke("Take a screenshot to see what's on the screen") - - # response.tool_calls contains the action Claude wants to perform - # You must execute this action in your environment and pass the result back - ``` - - !!! note "Automatic beta header" - - The required beta header is automatically appended based on the tool - version. For `computer_20250124` and `computer_20251124`, the respective - `computer-use-2025-01-24` and `computer-use-2025-11-24` beta header is - added automatically. - ??? example "Strict tool use" Strict tool use guarantees that tool names and arguments are validated @@ -2613,7 +2707,8 @@ class ChatAnthropic(BaseChatModel): - Claude Sonnet 4.5 or Opus 4.1 - `langchain-anthropic>=1.1.0` - To enable strict tool use, specify `strict=True` when calling `bind_tools`. + To enable strict tool use, specify `strict=True` when calling + [`bind_tools`][langchain_anthropic.chat_models.ChatAnthropic.bind_tools]. ```python hl_lines="11" from langchain_anthropic import ChatAnthropic @@ -2685,6 +2780,9 @@ class ChatAnthropic(BaseChatModel): ) -> Runnable[LanguageModelInput, dict | BaseModel]: """Model wrapper that returns outputs formatted to match the given schema. + See the [LangChain docs](https://docs.langchain.com/oss/python/integrations/chat/anthropic#structured-output) + for more details and examples. + Args: schema: The output schema. Can be passed in as: @@ -2740,6 +2838,41 @@ class ChatAnthropic(BaseChatModel): depends on the `schema` as described above. - `'parsing_error'`: `BaseException | None` + ???+ example "Native structured output with `method='json_schema'`" + + Anthropic supports a native structured output feature that guarantees + responses adhere to a given schema. + + !!! note + + Native structured output requires: + + - Claude Sonnet 4.5 or Opus 4.1 + - `langchain-anthropic>=1.1.0` + + To enable native structured output, specify `method="json_schema"` when + calling `with_structured_output`. (Under the hood, LangChain will + append the required `structured-outputs-2025-11-13` beta header) + + ```python hl_lines="13" + from langchain_anthropic import ChatAnthropic + from pydantic import BaseModel, Field + + model = ChatAnthropic(model="claude-sonnet-4-5") + + class Movie(BaseModel): + \"\"\"A movie with details.\"\"\" + title: str = Field(..., description="The title of the movie") + year: int = Field(..., description="The year the movie was released") + director: str = Field(..., description="The director of the movie") + rating: float = Field(..., description="The movie's rating out of 10") + + model_with_structure = model.with_structured_output(Movie, method="json_schema") + response = model_with_structure.invoke("Provide details about the movie Inception") + print(response) + # -> Movie(title="Inception", year=2010, director="Christopher Nolan", rating=8.8) + ``` + ??? example "Pydantic schema (`include_raw=False`)" ```python @@ -2815,41 +2948,6 @@ class ChatAnthropic(BaseChatModel): # 'justification': 'Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume and density of the two substances differ.' # } ``` - - ??? example "Native structured output with `method='json_schema'`" - - Anthropic supports a native structured output feature that guarantees - responses adhere to a given schema. - - !!! note - - Native structured output requires: - - - Claude Sonnet 4.5 or Opus 4.1 - - `langchain-anthropic>=1.1.0` - - To enable native structured output, specify `method="json_schema"` when - calling `with_structured_output`. (Under the hood, LangChain will - append the required `structured-outputs-2025-11-13` beta header) - - ```python hl_lines="13" - from langchain_anthropic import ChatAnthropic - from pydantic import BaseModel, Field - - model = ChatAnthropic(model="claude-sonnet-4-5") - - class Movie(BaseModel): - \"\"\"A movie with details.\"\"\" - title: str = Field(..., description="The title of the movie") - year: int = Field(..., description="The year the movie was released") - director: str = Field(..., description="The director of the movie") - rating: float = Field(..., description="The movie's rating out of 10") - - model_with_structure = model.with_structured_output(Movie, method="json_schema") - response = model_with_structure.invoke("Provide details about the movie Inception") - print(response) - # -> Movie(title="Inception", year=2010, director="Christopher Nolan", rating=8.8) - ``` """ # noqa: E501 if method == "json_mode": warning_message = ( @@ -2927,6 +3025,8 @@ class ChatAnthropic(BaseChatModel): ) -> int: """Count tokens in a sequence of input messages. + This uses Anthropic's official [token counting API](https://platform.claude.com/docs/en/build-with-claude/token-counting). + Args: messages: The message inputs to tokenize. tools: If provided, sequence of `dict`, `BaseModel`, function, or `BaseTool` @@ -2980,12 +3080,7 @@ class ChatAnthropic(BaseChatModel): ```txt 403 ``` - - !!! warning "Behavior changed in `langchain-anthropic` 0.3.0" - - Uses Anthropic's [token counting API](https://platform.claude.com/docs/en/build-with-claude/token-counting) to count tokens in messages. - - """ # noqa: D214,E501 + """ # noqa: D214 formatted_system, formatted_messages = _format_messages(messages) if isinstance(formatted_system, str): kwargs["system"] = formatted_system @@ -3044,6 +3139,16 @@ def convert_to_anthropic_tool( anthropic_formatted["description"] = oai_formatted["description"] if "strict" in oai_formatted and isinstance(strict, bool): anthropic_formatted["strict"] = oai_formatted["strict"] + # Select params from tool.extras + if ( + isinstance(tool, BaseTool) + and hasattr(tool, "extras") + and isinstance(tool.extras, dict) + ): + for key, value in tool.extras.items(): + if key in _ANTHROPIC_EXTRA_FIELDS: + # all are populated top-level + anthropic_formatted[key] = value # type: ignore[literal-required] return anthropic_formatted @@ -3077,6 +3182,7 @@ class _AnthropicToolUse(TypedDict): name: str input: dict id: str + caller: NotRequired[dict[str, Any]] def _lc_tool_calls_to_anthropic_tool_use_blocks( @@ -3173,11 +3279,19 @@ def _make_message_chunk_from_anthropic_event( content_block = event.content_block.model_dump() content_block["index"] = event.index if event.content_block.type == "tool_use": + if ( + parsed_args := getattr(event.content_block, "input", None) + ) and isinstance(parsed_args, dict): + # In some cases parsed args are represented in start event, with no + # following input_json_delta events + args = json.dumps(parsed_args) + else: + args = "" tool_call_chunk = create_tool_call_chunk( index=event.index, id=event.content_block.id, name=event.content_block.name, - args="", + args=args, ) tool_call_chunks = [tool_call_chunk] else: @@ -3249,6 +3363,9 @@ def _make_message_chunk_from_anthropic_event( } if context_management := getattr(event, "context_management", None): response_metadata["context_management"] = context_management.model_dump() + message_delta = getattr(event, "delta", None) + if message_delta and (container := getattr(message_delta, "container", None)): + response_metadata["container"] = container.model_dump(mode="json") message_chunk = AIMessageChunk( content="" if coerce_content_to_string else [], usage_metadata=usage_metadata, diff --git a/libs/partners/anthropic/tests/cassettes/test_programmatic_tool_use.yaml.gz b/libs/partners/anthropic/tests/cassettes/test_programmatic_tool_use.yaml.gz new file mode 100644 index 0000000000000000000000000000000000000000..98c32b29bed4af3bf9114926c33176d2773f224f GIT binary patch literal 2564 zcmV+f3j6gRiwFR{Q#fe?|LvJuZ{o-jfZz95WS>@=BX(>9vqPlOQGnRk3C?1meR;FH z4K}ndi*3MYfBdO#+k_BumCou&XCg(yrnj4Y{ramH z#_p!P`umqF`14CGx2<47l-|ZMy9afZNc!+*Hy-Rl&qN0-$+2UN!v~uGp~l+QQ|94A zsEJrYr5f%+Tj~`=K?ND8-?&1RlEk(qF)sUptIU@~LIF_%io<^Dh5doo)&)=Md{twS z+z+(elML#?U^UV++d^w+DjqzkTR7Gv=L+?KAhMaj!mT%|Es45;XL3uxI%DxTiJGYq zE$|2lR$E`W3d-%N`VG9?;i$Lc3LQ%mTyRZYrJ!iPwIvyzdj)@vjV`WSJdlx=dJzs6 zmXLw#6k9085n6+s*No!|`C!U~j)S*$lv2Ar2^Z5{=qTu5x3v_Hj1*jF-;cn(C9_x^ z5o%jT;#leu!nWiJ?P)*rBpqC$TQBT{5U0RX_FHgsZNYo^OmWOYk+B6>JE%u}1+9q1 zCg?84tD91v-E_vQdB_!7-~z-z@U1?D6@GINOjy{acu|%sWNs{w*n@|_cZt7&4CA4| z*iZB7bL;B2FJFw?;mu%nJN)wHw-bSuALE3C@$xmvI1e^S8se7c=kB`%w_rz@#IMNW zB#wQYzbTaaao(bSyv&1l2{K+#-YJ;6cn$8qWA#o)ZTx&wws2Cs5w?>NAR0#g$D}`K z6&sQiFIyD(d61Cnt3My6Gk7dE;q_IJ=V^BR^(!p!@5fyJ9VgM(t@t$ptq`2--)s~7 z$Tx-mKUm3maVb4%Ulj~XKe(1I*k)H~6mx}*cC(#9HAfxxSzWJVPuVSPDWJBLJ*$bX z*;!7a-T}NSRK?lCW^5#-9uHUYi0>yV7H9=M6gKu_MQ1!6~ z@4?-!7mb%t%JE_M#WuY5g(`ZPAhxWYo4_N z-fxbrf~5zEP-!tMr2T?eI;~gyD8pkc_r$6wN>BTTBe7n5Tv7I2>cxGLtv)C@X>=?b zYIiG3t6t*_`0BFRMeA_0M+%vEQa&`I-d|%g*i6tw^aOE^ktTX21Aw|ihI|6Z(Ozar zRYp`6ejqKLq*$bHVU-t-1-aTr92%TP%ghRVveMr#XE2Jv}1Ak;K6AP02>s~vT__e^2@xG0T_C!Wn6vA?Z)q@XLFlgO{z0i$d*9>^Fu3;OlK-umwnZfG8v)UQJyP&Ez z$|HR8)r{l}W&}5l1-ysPsN|t2A^3pFmV`Qvsf$&C_+E}7-NwUZsZTXh7l-|6PQ5XR zO?brUXR>wq(pWP8_=$NxG4Chl{j-?2@v}6EGyn1I&S%T=4ck!X^jGoUYlmrej=rDO zjcTTLt0Kz#N{%JM`UN8EdDmZ)eywt5YJ0=f2mV^Obwyt3QIJ1vh$6*pJ#ea)|26@h zG2j`xvGNbSRbVQnQ<8auK0Cp$+!p|}R#|iC(usPdI-*-L?SyBQ@BsZyg8xNBDmA2} z6fsrjN-$uMIw(+xs{Z;qhfjpxMl6@5RKHH(Ny7?-aXPw(U&qE=bmHZXwTLjcrh zj+I`?x&dy}!j|av6yQ|FpibD&ZIc#vgY`E{=>YWIETKgmnpm29K^+Q9+aiH$O_pb* zmH6WBr1Nz##B~XfS%W(~$3ohutyN-n=Mhg5HVadKhxP?nb+qt42F415#ws?PUvV_; z>hun!Rte6oT}!KOL#)f-_;fj5M!j0)bK-mZI_UXrPZF|&#W6486=3Z-JbvKDi?LUC(GcIs0e1&sU)d%QAmY}oA^gu(pQ)`?p7Nk$^%+D`3ACkN0VmvfP)u{ z7pGVWg;raKIB6@FEsq6&4rRBRUhFLKyDc~_VuL(H+Y1m7ry2cG-DKWNnq&a zm%o_$8)&Z=znX2_I9mW1-8Q6nNy=&&iI?PAYvx`Scx1z5vvlL|;4(K_A$dc3118fj z3iDPNrv(sRvi4)ZANJ~VLpOc|knoZt?--u>@1GeE>lHodl$fQTeiWvNEX>#K*me+(r1u4IfRF_D{E+YScPNFM4#Dxr1v zpbJU&zX6gLE@+y2tA=QfKPB-(iWbcBM^ZW9*x{{>pzpi!{Pr-n9NUk`aJrI5C=ON_ z^{1#!5E+h{r9ggN!C}@t{qbmZ>|5t2@LqFW$A%w>Vr=tZ zs_rPk8G%?@VM#Smu5QjRm&kM0;y12YL)$RryjJcYYUA<6^~Pm` z%{2xeSo`BQG9fu!`0UndTiY#_R0WwqM8@?+ME1t|XKO`q^zuvNNe|5A<>}X7b;s8> z$Q1Mtw**RWK7ZzX z+Wgw(W4$?Xb=b^#LZryzTAk8Iz1iNX@;Jr>g)T*-FBz>#qDpMctwPH1MHe;$Ato{s z?u*ol9NlAMb&3d?uE{bjSPKlKTxwE#Z_1MeCTKK;K$U142!TkQL~nIm!=Hp$OdkmK znUx|>Lf{hrZ4TT$ChQB8;kjEi;hDu081lpeCTL17a5jcBs&1BHQNGz@9L*C~W|wDx z^R+4qD!%k^}ND{OC>TnU~u@QU>#GEYPIs7gr z_$U&7h0auNiqEPf&?$%LEAF4ku*cEE-ZBxUyt$YjK>SJDNHtp7^l?4+;A;_8QV83jFZs1u*?pkOGmPetW85( z0-+0w4h=1I8fz=LxF(7<$9JtLYG@n7zIADo_{NQEUG1XI%1eXj#)Y9~tU>%6v{&A> zvEohAFoNxsV=L`|rH*IaxB5=5(3W6(o+HnS?ZA9_`g=5;!JpfW^YUcJaTvXP{tOEK zJ-6lGjllc7qd$8<6%Hr*7jlBX;?36jAB^r)1RqcI=D~%JI*KeDHCdn(sZ9~WS!7D7 zqX@2{80zpx!&h-+H^o2q!@1I)(Bf$JCJaY!N@B?3N|-;lrvr06r^gP&(opw1bk3ob z#Xs~_x!(?TxwA9;@e(*_>z)wm^kECnZKLIGm87#)C1t5gEO5UJNA6+Ebzhg_sdm@EqIq6O~O7>E?knkA9ZITLYHPTe(nhCe(I)08{xv4gSuN{M|p(AdtG@rs>^ zGhP-3Tw*r)S!xn#5=+zaWCy0RsI!Qa;B~X0EV+}~UXxj?BI8|JYa+~E zpNFeVJ6J)yA-Cgy;r)oCW;Ad4)8 z@t!p+xh1T?qhbYUfD5NuHaunzMhtc)9zWV$ST^P(d>FVkoP`WF=!193VhEgpErvSU zhY9AJU8?LYJH-oKr8RaCtW*2qQE9>X4l)Sn@B=uvCE}PHm@c5ZK$W>=N)TQs(@vx} z1v_=aXQUik2ebxau4Q-*zk{NN7#?1-#Z=2+kN~1k@Vwc8;e%U6qCc!n?JxtQ3z24i zBD0^!>?bn&?;^8}6@`HxSw#tszZS(Ck=ef<5(h2AN?> zWfk8#8DUJ2rgB{te3T5vQkIGUA+Vjj(5JZxC3vdzOsOq*K0KPeNm79!F_gJSMKWDn zz>>_SahKw?jkgImOKS9Eytby_v0u_6?#Y4;$aFwk4Vx)I*FJNb|44@p4 zLTaxNS@O=5KL}3B{}K#74p(amKCx0HA%L@6p)8h`y-W%ySq5@)4D3MUUcHvMw3Vjq zVy>X{t|7gzCF|h4<{FSiKxK&{9Tpi$L88x*sVpB#cI`TpwDgupp){J2l;M1|rnp57 zfgx&D0Fp61cLf}wWQtQgGAi8z!}7)`X)ZBF?RJ>%GIUK3z)LS!=4d9vGc1D}92%Y3 z+H1ImbZC#9&PgkwO#ye5+~iSa+(2@t6v*RQD`uUW`Vt>)q4~)|!6`mEZM;ANRb9hn zPB3d~pGJ#s=3J(z698v_G&jZB`p}#8?EcVaY2_%6MsvHRvcep3QZ$T&9VGpQD%$`8 z#1tUF`;26I zCUzH8VfNp?aDy`0(}Rf@z-a{Q(!$4^xEVpJl$m=S18OCac2nCi&>=g{J*eHF6 z&<;lg&jl&{EXKim5uwg<_aRad@5v!%#h4%}9VjK$=oUykXXa0ye<=sNN-q!L2?6JjvEs5%NzMI)6@!i5{v*%SDPGaXj8A+61$|MXFHu|m&<JmTbEvLps?D8AD<r9ON=Y@bBa~Mv-){dCp1m%BZTUDiyKIJB7O> zRgod@67G^zr9?qGaX2iT2?@8vf)Duea!9x(78Dx47826N5D9EQaB$<6e@{CESes2Y zG)u|!M^z8Egz}1#TZV(d&IKuQlHW%)NQxr3eQVw!J*j|zLHc0GUCyZ>xCd24xV%%i zOH$>gev@#KenTd$p5?=6t(;^?y!#OAmF`_?dGFJzFHa{ zTnZz38^5M3EvKlgEsw70k-Ss20AHZ=R%c-}#e*q{Tcuezq}QhP{+{%L!P(kqK8!$0 zsCIK<)8{Un^^EO_zUfV=P@70=YPvSN#@n722dY5M(p{r$=vspgYR(?zCQBjv!=uwF zbr?F-ek6_$qgikq_JTx6p7+9Wd>GDLjq_>L?4|MH6iF9x^Km#ZOh&VDlurhlLW1oR z-3Mus>2|3f-^E5qx&~mz=mLSQvh{NZ&SPkEaQb|mpz+A*C67D=r(+MZDLQgXfa-u> zX-F+-S|SS+=3+oI7px&Sy1cJ`YI~LG6tcO@xPlM&|GI2r@~XVl4dc3+VOx!E;K!T5 ztzS`$fX}?fGu;`Q>DDdiX>t9FskPZzo7z{ijpj!SYtwimbMLxUa$^$cK^!$^TK$4z z(j9D;n(rJnBu2wAudLnx%dX=&al`S$ZQO{1wdF?*{OGZ*8_Uxi|C}H%wlzZ`7i zhqco{BE07U&cV0k#xl@q6asr^EhkwMA5S3$1+f5e{dKRD#w{(<{R8u1!c<)3%Xb zQWYo_{a*#tLc)LZt~n4B5RTm2tsD-zzbDHyb6-2@ZqQ7iQ%$9Ht+G026_;n;7b~lC zR#6lllq%a)dY4pu#`jE5rgXP96?40MxMO-aZ7LFtXJjykY`K{@0Zfi>z0Rw5$a&^m z5luAy9XeB_LqN`Q^>(i9fzP!k|XMFZ@6tk~(quxF=Y)(FXuEwrEs{$vL1TCwu0waiBrWZ)IP-;@9 z%x|-(U5_dT=!UeI0vUYux?0JSc!`qjal9X8MJ=jk75vpSL0o|fs8C*$BDpS)cZ)P% zd5-Gs`deX0_?9a1sL4Puk>LpePMWd^=Qc?ZM5P&XStVsFAa;2Xk|OQ@EQWs;!#|7R SpT%&g82%56^W)%-Jpcf~y36kX literal 0 HcmV?d00001 diff --git a/libs/partners/anthropic/tests/cassettes/test_tool_search.yaml.gz b/libs/partners/anthropic/tests/cassettes/test_tool_search.yaml.gz new file mode 100644 index 0000000000000000000000000000000000000000..1381e57f6a99e366767daae255ce843d07208dc1 GIT binary patch literal 2961 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 HfhYg~UZ1F~ literal 0 HcmV?d00001 diff --git a/libs/partners/anthropic/tests/integration_tests/test_chat_models.py b/libs/partners/anthropic/tests/integration_tests/test_chat_models.py index ed6fa71c9ba..ee941fa24b2 100644 --- a/libs/partners/anthropic/tests/integration_tests/test_chat_models.py +++ b/libs/partners/anthropic/tests/integration_tests/test_chat_models.py @@ -2080,72 +2080,228 @@ def test_context_management() -> None: assert full.response_metadata.get("context_management") -def test_tool_search() -> None: - """Test tool search functionality with both regex and BM25 variants.""" - # Test with regex variant - llm = ChatAnthropic( - model="claude-opus-4-5-20251101", # type: ignore[call-arg] +@pytest.mark.default_cassette("test_tool_search.yaml.gz") +@pytest.mark.vcr +@pytest.mark.parametrize("output_version", ["v0", "v1"]) +def test_tool_search(output_version: str) -> None: + """Test tool search with LangChain tools using extras parameter.""" + + @tool(extras={"defer_loading": True}) + def get_weather(location: str, unit: str = "fahrenheit") -> str: + """Get the current weather for a location. + + Args: + location: City name + unit: Temperature unit (celsius or fahrenheit) + """ + return f"The weather in {location} is sunny and 72°{unit[0].upper()}" + + @tool(extras={"defer_loading": True}) + def search_files(query: str) -> str: + """Search through files in the workspace. + + Args: + query: Search query + """ + return f"Found 3 files matching '{query}'" + + model = ChatAnthropic( + model="claude-opus-4-5-20251101", output_version=output_version ) - # Define tools with defer_loading - tools = [ - { - "type": "tool_search_tool_regex_20251119", - "name": "tool_search_tool_regex", - }, - { - "name": "get_weather", - "description": "Get the current weather for a location", - "input_schema": { - "type": "object", - "properties": { - "location": {"type": "string", "description": "City name"}, - "unit": { - "type": "string", - "enum": ["celsius", "fahrenheit"], - "description": "Temperature unit", - }, - }, - "required": ["location"], + agent = create_agent( # type: ignore[var-annotated] + model, + tools=[ + { + "type": "tool_search_tool_regex_20251119", + "name": "tool_search_tool_regex", }, - "defer_loading": True, - }, - { - "name": "search_files", - "description": "Search through files in the workspace", - "input_schema": { - "type": "object", - "properties": { - "query": {"type": "string", "description": "Search query"}, - }, - "required": ["query"], - }, - "defer_loading": True, - }, - ] + get_weather, + search_files, + ], + ) - llm_with_tools = llm.bind_tools(tools) # type: ignore[arg-type] - - # Test with a query that should trigger tool search + # Test with actual API call input_message = { "role": "user", "content": "What's the weather in San Francisco?", } - response = llm_with_tools.invoke([input_message]) + result = agent.invoke({"messages": [input_message]}) + first_response = result["messages"][1] + content_types = [block["type"] for block in first_response.content] + if output_version == "v0": + assert content_types == [ + "text", + "server_tool_use", + "tool_search_tool_result", + "text", + "tool_use", + ] + else: + # v1 + assert content_types == [ + "text", + "server_tool_call", + "server_tool_result", + "text", + "tool_call", + ] - # Verify response contains expected block types - assert all(isinstance(block, (str, dict)) for block in response.content) + answer = result["messages"][-1] + assert not answer.tool_calls + assert answer.text - # Check for server_tool_use (tool search) and tool_result blocks - block_types = { - block["type"] - for block in response.content - if isinstance(block, dict) and "type" in block + +@pytest.mark.default_cassette("test_programmatic_tool_use.yaml.gz") +@pytest.mark.vcr +@pytest.mark.parametrize("output_version", ["v0", "v1"]) +def test_programmatic_tool_use(output_version: str) -> None: + """Test programmatic tool use. + + Implicitly checks that `allowed_callers` in tool extras works. + """ + + @tool(extras={"allowed_callers": ["code_execution_20250825"]}) + def get_weather(location: str) -> str: + """Get the weather at a location.""" + return "It's sunny." + + tools: list = [ + {"type": "code_execution_20250825", "name": "code_execution"}, + get_weather, + ] + + model = ChatAnthropic( + model="claude-sonnet-4-5", + betas=["advanced-tool-use-2025-11-20"], + reuse_last_container=True, + output_version=output_version, + ) + + agent = create_agent(model, tools=tools) # type: ignore[var-annotated] + + input_query = { + "role": "user", + "content": "What's the weather in Boston?", } - # Response should contain server_tool_use for tool search - # and potentially tool_result with tool_reference blocks - assert "server_tool_use" in block_types or "tool_use" in block_types + result = agent.invoke({"messages": [input_query]}) + assert len(result["messages"]) == 4 + tool_call_message = result["messages"][1] + response_message = result["messages"][-1] + + if output_version == "v0": + server_tool_use_block = next( + block + for block in tool_call_message.content + if block["type"] == "server_tool_use" + ) + assert server_tool_use_block + + tool_use_block = next( + block for block in tool_call_message.content if block["type"] == "tool_use" + ) + assert "caller" in tool_use_block + + code_execution_result = next( + block + for block in response_message.content + if block["type"] == "code_execution_tool_result" + ) + assert code_execution_result["content"]["return_code"] == 0 + else: + server_tool_call_block = next( + block + for block in tool_call_message.content + if block["type"] == "server_tool_call" + ) + assert server_tool_call_block + + tool_call_block = next( + block for block in tool_call_message.content if block["type"] == "tool_call" + ) + assert "caller" in tool_call_block["extras"] + + server_tool_result = next( + block + for block in response_message.content + if block["type"] == "server_tool_result" + ) + assert server_tool_result["output"]["return_code"] == 0 + + +@pytest.mark.default_cassette("test_programmatic_tool_use_streaming.yaml.gz") +@pytest.mark.vcr +@pytest.mark.parametrize("output_version", ["v0", "v1"]) +def test_programmatic_tool_use_streaming(output_version: str) -> None: + @tool(extras={"allowed_callers": ["code_execution_20250825"]}) + def get_weather(location: str) -> str: + """Get the weather at a location.""" + return "It's sunny." + + tools: list = [ + {"type": "code_execution_20250825", "name": "code_execution"}, + get_weather, + ] + + model = ChatAnthropic( + model="claude-sonnet-4-5", + betas=["advanced-tool-use-2025-11-20"], + reuse_last_container=True, + streaming=True, + output_version=output_version, + ) + + agent = create_agent(model, tools=tools) # type: ignore[var-annotated] + + input_query = { + "role": "user", + "content": "What's the weather in Boston?", + } + + result = agent.invoke({"messages": [input_query]}) + assert len(result["messages"]) == 4 + tool_call_message = result["messages"][1] + response_message = result["messages"][-1] + + if output_version == "v0": + server_tool_use_block = next( + block + for block in tool_call_message.content + if block["type"] == "server_tool_use" + ) + assert server_tool_use_block + + tool_use_block = next( + block for block in tool_call_message.content if block["type"] == "tool_use" + ) + assert "caller" in tool_use_block + + code_execution_result = next( + block + for block in response_message.content + if block["type"] == "code_execution_tool_result" + ) + assert code_execution_result["content"]["return_code"] == 0 + else: + server_tool_call_block = next( + block + for block in tool_call_message.content + if block["type"] == "server_tool_call" + ) + assert server_tool_call_block + + tool_call_block = next( + block for block in tool_call_message.content if block["type"] == "tool_call" + ) + assert "caller" in tool_call_block["extras"] + + server_tool_result = next( + block + for block in response_message.content + if block["type"] == "server_tool_result" + ) + assert server_tool_result["output"]["return_code"] == 0 def test_async_shared_client() -> None: diff --git a/libs/partners/anthropic/tests/unit_tests/test_chat_models.py b/libs/partners/anthropic/tests/unit_tests/test_chat_models.py index fee0fa177dd..8c5a155f06b 100644 --- a/libs/partners/anthropic/tests/unit_tests/test_chat_models.py +++ b/libs/partners/anthropic/tests/unit_tests/test_chat_models.py @@ -2228,3 +2228,137 @@ def test_output_config_without_effort() -> None: assert payload.get("betas") is None or "effort-2025-11-24" not in payload.get( "betas", [] ) + + +def test_extras_with_defer_loading() -> None: + """Test that extras with `defer_loading` are merged into tool definitions.""" + from langchain_core.tools import tool + + @tool(extras={"defer_loading": True}) + def get_weather(location: str) -> str: + """Get weather for a location.""" + return f"Weather in {location}" + + model = ChatAnthropic(model=MODEL_NAME) # type: ignore[call-arg] + model_with_tools = model.bind_tools([get_weather]) + + # Get the payload to check if defer_loading was merged + payload = model_with_tools._get_request_payload( # type: ignore[attr-defined] + "test", + **model_with_tools.kwargs, # type: ignore[attr-defined] + ) + + # Find the get_weather tool in the payload + weather_tool = None + for tool_def in payload["tools"]: + if isinstance(tool_def, dict) and tool_def.get("name") == "get_weather": + weather_tool = tool_def + break + + assert weather_tool is not None + assert weather_tool.get("defer_loading") is True + + +def test_extras_with_cache_control() -> None: + """Test that extras with `cache_control` are merged into tool definitions.""" + from langchain_core.tools import tool + + @tool(extras={"cache_control": {"type": "ephemeral"}}) + def search_files(query: str) -> str: + """Search files.""" + return f"Results for {query}" + + model = ChatAnthropic(model=MODEL_NAME) # type: ignore[call-arg] + model_with_tools = model.bind_tools([search_files]) + + payload = model_with_tools._get_request_payload( # type: ignore[attr-defined] + "test", + **model_with_tools.kwargs, # type: ignore[attr-defined] + ) + + search_tool = None + for tool_def in payload["tools"]: + if isinstance(tool_def, dict) and tool_def.get("name") == "search_files": + search_tool = tool_def + break + + assert search_tool is not None + assert search_tool.get("cache_control") == {"type": "ephemeral"} + + +def test_extras_with_input_examples() -> None: + """Test that extras with `input_examples` are merged into tool definitions.""" + from langchain_core.tools import tool + + @tool( + extras={ + "input_examples": [ + {"location": "San Francisco, CA", "unit": "fahrenheit"}, + {"location": "Tokyo, Japan", "unit": "celsius"}, + ] + } + ) + def get_weather(location: str, unit: str = "fahrenheit") -> str: + """Get weather for a location.""" + return f"Weather in {location}" + + model = ChatAnthropic(model=MODEL_NAME) # type: ignore[call-arg] + model_with_tools = model.bind_tools([get_weather]) + + payload = model_with_tools._get_request_payload( # type: ignore[attr-defined] + "test", + **model_with_tools.kwargs, # type: ignore[attr-defined] + ) + + weather_tool = None + for tool_def in payload["tools"]: + if isinstance(tool_def, dict) and tool_def.get("name") == "get_weather": + weather_tool = tool_def + break + + assert weather_tool is not None + assert "input_examples" in weather_tool + assert len(weather_tool["input_examples"]) == 2 + assert weather_tool["input_examples"][0] == { + "location": "San Francisco, CA", + "unit": "fahrenheit", + } + + # Beta header is required + assert "betas" in payload + assert "advanced-tool-use-2025-11-20" in payload["betas"] + + +def test_extras_with_multiple_fields() -> None: + """Test that multiple extra fields can be specified together.""" + from langchain_core.tools import tool + + @tool( + extras={ + "defer_loading": True, + "cache_control": {"type": "ephemeral"}, + "input_examples": [{"query": "python files"}], + } + ) + def search_code(query: str) -> str: + """Search code.""" + return f"Code for {query}" + + model = ChatAnthropic(model=MODEL_NAME) # type: ignore[call-arg] + model_with_tools = model.bind_tools([search_code]) + + payload = model_with_tools._get_request_payload( # type: ignore[attr-defined] + "test", + **model_with_tools.kwargs, # type: ignore[attr-defined] + ) + + tool_def = None + for t in payload["tools"]: + if isinstance(t, dict) and t.get("name") == "search_code": + tool_def = t + break + + assert tool_def is not None + assert tool_def.get("defer_loading") is True + assert tool_def.get("cache_control") == {"type": "ephemeral"} + assert "input_examples" in tool_def diff --git a/libs/partners/anthropic/uv.lock b/libs/partners/anthropic/uv.lock index bfe614c81aa..b4274b1ae5c 100644 --- a/libs/partners/anthropic/uv.lock +++ b/libs/partners/anthropic/uv.lock @@ -495,7 +495,7 @@ wheels = [ [[package]] name = "langchain" -version = "1.1.2" +version = "1.1.3" source = { editable = "../../langchain_v1" } dependencies = [ { name = "langchain-core" }, @@ -643,7 +643,7 @@ typing = [ [[package]] name = "langchain-core" -version = "1.1.1" +version = "1.1.2" source = { editable = "../../core" } dependencies = [ { name = "jsonpatch" },