diff --git a/.github/workflows/_integration_test.yml b/.github/workflows/_integration_test.yml
index 2a4f3aab48c..e64a4a62dff 100644
--- a/.github/workflows/_integration_test.yml
+++ b/.github/workflows/_integration_test.yml
@@ -41,6 +41,8 @@ jobs:
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_FILES_API_IMAGE_ID: ${{ secrets.ANTHROPIC_FILES_API_IMAGE_ID }}
+ ANTHROPIC_FILES_API_PDF_ID: ${{ secrets.ANTHROPIC_FILES_API_PDF_ID }}
AZURE_OPENAI_API_VERSION: ${{ secrets.AZURE_OPENAI_API_VERSION }}
AZURE_OPENAI_API_BASE: ${{ secrets.AZURE_OPENAI_API_BASE }}
AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }}
diff --git a/.github/workflows/_release.yml b/.github/workflows/_release.yml
index 6f186379153..2c0cd53716f 100644
--- a/.github/workflows/_release.yml
+++ b/.github/workflows/_release.yml
@@ -344,6 +344,8 @@ jobs:
fail-fast: false # Continue testing other partners if one fails
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_FILES_API_IMAGE_ID: ${{ secrets.ANTHROPIC_FILES_API_IMAGE_ID }}
+ ANTHROPIC_FILES_API_PDF_ID: ${{ secrets.ANTHROPIC_FILES_API_PDF_ID }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
AZURE_OPENAI_API_VERSION: ${{ secrets.AZURE_OPENAI_API_VERSION }}
AZURE_OPENAI_API_BASE: ${{ secrets.AZURE_OPENAI_API_BASE }}
diff --git a/.github/workflows/scheduled_test.yml b/.github/workflows/scheduled_test.yml
index 0cc622f85a7..1b5b094818b 100644
--- a/.github/workflows/scheduled_test.yml
+++ b/.github/workflows/scheduled_test.yml
@@ -127,6 +127,8 @@ jobs:
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_FILES_API_IMAGE_ID: ${{ secrets.ANTHROPIC_FILES_API_IMAGE_ID }}
+ ANTHROPIC_FILES_API_PDF_ID: ${{ secrets.ANTHROPIC_FILES_API_PDF_ID }}
AZURE_OPENAI_API_VERSION: ${{ secrets.AZURE_OPENAI_API_VERSION }}
AZURE_OPENAI_API_BASE: ${{ secrets.AZURE_OPENAI_API_BASE }}
AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }}
diff --git a/docs/docs/integrations/chat/anthropic.ipynb b/docs/docs/integrations/chat/anthropic.ipynb
index b49379aee6a..cfe88b99ff1 100644
--- a/docs/docs/integrations/chat/anthropic.ipynb
+++ b/docs/docs/integrations/chat/anthropic.ipynb
@@ -325,6 +325,102 @@
"ai_msg.tool_calls"
]
},
+ {
+ "cell_type": "markdown",
+ "id": "535a16e4-cd5a-479f-b315-37c816ec4387",
+ "metadata": {},
+ "source": [
+ "## Multimodal\n",
+ "\n",
+ "Claude supports image and PDF inputs as content blocks, both in Anthropic's native format (see docs for [vision](https://docs.anthropic.com/en/docs/build-with-claude/vision#base64-encoded-image-example) and [PDF support](https://docs.anthropic.com/en/docs/build-with-claude/pdf-support)) as well as LangChain's [standard format](/docs/how_to/multimodal_inputs/).\n",
+ "\n",
+ "### Files API\n",
+ "\n",
+ "Claude also supports interactions with files through its managed [Files API](https://docs.anthropic.com/en/docs/build-with-claude/files). See examples below.\n",
+ "\n",
+ "The Files API can also be used to upload files to a container for use with Claude's built-in code-execution tools. See the [code execution](#code-execution) section below, for details.\n",
+ "\n",
+ "\n",
+ "Images
\n",
+ "\n",
+ "```python\n",
+ "# Upload image\n",
+ "\n",
+ "import anthropic\n",
+ "\n",
+ "client = anthropic.Anthropic()\n",
+ "file = client.beta.files.upload(\n",
+ " # Supports image/jpeg, image/png, image/gif, image/webp\n",
+ " file=(\"image.png\", open(\"/path/to/image.png\", \"rb\"), \"image/png\"),\n",
+ ")\n",
+ "image_file_id = file.id\n",
+ "\n",
+ "\n",
+ "# Run inference\n",
+ "from langchain_anthropic import ChatAnthropic\n",
+ "\n",
+ "llm = ChatAnthropic(\n",
+ " model=\"claude-sonnet-4-20250514\",\n",
+ " betas=[\"files-api-2025-04-14\"],\n",
+ ")\n",
+ "\n",
+ "input_message = {\n",
+ " \"role\": \"user\",\n",
+ " \"content\": [\n",
+ " {\n",
+ " \"type\": \"text\",\n",
+ " \"text\": \"Describe this image.\",\n",
+ " },\n",
+ " {\n",
+ " \"type\": \"image\",\n",
+ " \"source\": {\n",
+ " \"type\": \"file\",\n",
+ " \"file_id\": image_file_id,\n",
+ " },\n",
+ " },\n",
+ " ],\n",
+ "}\n",
+ "llm.invoke([input_message])\n",
+ "```\n",
+ "\n",
+ " \n",
+ "\n",
+ "\n",
+ "PDFs
\n",
+ "\n",
+ "```python\n",
+ "# Upload document\n",
+ "\n",
+ "import anthropic\n",
+ "\n",
+ "client = anthropic.Anthropic()\n",
+ "file = client.beta.files.upload(\n",
+ " file=(\"document.pdf\", open(\"/path/to/document.pdf\", \"rb\"), \"application/pdf\"),\n",
+ ")\n",
+ "pdf_file_id = file.id\n",
+ "\n",
+ "\n",
+ "# Run inference\n",
+ "from langchain_anthropic import ChatAnthropic\n",
+ "\n",
+ "llm = ChatAnthropic(\n",
+ " model=\"claude-sonnet-4-20250514\",\n",
+ " betas=[\"files-api-2025-04-14\"],\n",
+ ")\n",
+ "\n",
+ "input_message = {\n",
+ " \"role\": \"user\",\n",
+ " \"content\": [\n",
+ " {\"type\": \"text\", \"text\": \"Describe this document.\"},\n",
+ " {\"type\": \"document\", \"source\": {\"type\": \"file\", \"file_id\": pdf_file_id}}\n",
+ " ],\n",
+ "}\n",
+ "llm.invoke([input_message])\n",
+ "```\n",
+ "\n",
+ " "
+ ]
+ },
{
"cell_type": "markdown",
"id": "6e36d25c-f358-49e5-aefa-b99fbd3fec6b",
@@ -454,6 +550,27 @@
"print(f\"\\nSecond:\\n{usage_2}\")"
]
},
+ {
+ "cell_type": "markdown",
+ "id": "9678656f-1ec4-4bf1-bf62-bbd49eb5c4e7",
+ "metadata": {},
+ "source": [
+ ":::tip Extended caching\n",
+ "\n",
+ " The cache lifetime is 5 minutes by default. If this is too short, you can apply one hour caching by enabling the `\"extended-cache-ttl-2025-04-11\"` beta header:\n",
+ "\n",
+ " ```python\n",
+ " llm = ChatAnthropic(\n",
+ " model=\"claude-3-7-sonnet-20250219\",\n",
+ " # highlight-next-line\n",
+ " betas=[\"extended-cache-ttl-2025-04-11\"],\n",
+ " )\n",
+ " ```\n",
+ " and specifying `\"cache_control\": {\"type\": \"ephemeral\", \"ttl\": \"1h\"}`.\n",
+ "\n",
+ ":::"
+ ]
+ },
{
"cell_type": "markdown",
"id": "141ce9c5-012d-4502-9d61-4a413b5d959a",
@@ -953,6 +1070,159 @@
"response = llm_with_tools.invoke(\"How do I update a web app to TypeScript 5.5?\")"
]
},
+ {
+ "cell_type": "markdown",
+ "id": "1478cdc6-2e52-4870-80f9-b4ddf88f2db2",
+ "metadata": {},
+ "source": [
+ "### Code execution\n",
+ "\n",
+ "Claude can use a [code execution tool](https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/code-execution-tool) to execute Python code in a sandboxed environment.\n",
+ "\n",
+ ":::info Code execution is supported since ``langchain-anthropic>=0.3.14``\n",
+ "\n",
+ ":::"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "2ce13632-a2da-439f-a429-f66481501630",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from langchain_anthropic import ChatAnthropic\n",
+ "\n",
+ "llm = ChatAnthropic(\n",
+ " model=\"claude-sonnet-4-20250514\",\n",
+ " betas=[\"code-execution-2025-05-22\"],\n",
+ ")\n",
+ "\n",
+ "tool = {\"type\": \"code_execution_20250522\", \"name\": \"code_execution\"}\n",
+ "llm_with_tools = llm.bind_tools([tool])\n",
+ "\n",
+ "response = llm_with_tools.invoke(\n",
+ " \"Calculate the mean and standard deviation of \" \"[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]\"\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "24076f91-3a3d-4e53-9618-429888197061",
+ "metadata": {},
+ "source": [
+ "\n",
+ "Use with Files API
\n",
+ "\n",
+ "Using the Files API, Claude can write code to access files for data analysis and other purposes. See example below:\n",
+ "\n",
+ "```python\n",
+ "# Upload file\n",
+ "\n",
+ "import anthropic\n",
+ "\n",
+ "client = anthropic.Anthropic()\n",
+ "file = client.beta.files.upload(\n",
+ " file=open(\"/path/to/sample_data.csv\", \"rb\")\n",
+ ")\n",
+ "file_id = file.id\n",
+ "\n",
+ "\n",
+ "# Run inference\n",
+ "from langchain_anthropic import ChatAnthropic\n",
+ "\n",
+ "llm = ChatAnthropic(\n",
+ " model=\"claude-sonnet-4-20250514\",\n",
+ " betas=[\"code-execution-2025-05-22\"],\n",
+ ")\n",
+ "\n",
+ "tool = {\"type\": \"code_execution_20250522\", \"name\": \"code_execution\"}\n",
+ "llm_with_tools = llm.bind_tools([tool])\n",
+ "\n",
+ "input_message = {\n",
+ " \"role\": \"user\",\n",
+ " \"content\": [\n",
+ " {\n",
+ " \"type\": \"text\",\n",
+ " \"text\": \"Please plot these data and tell me what you see.\",\n",
+ " },\n",
+ " {\n",
+ " \"type\": \"container_upload\",\n",
+ " \"file_id\": file_id,\n",
+ " },\n",
+ " ]\n",
+ "}\n",
+ "llm_with_tools.invoke([input_message])\n",
+ "```\n",
+ "\n",
+ "Note that Claude may generate files as part of its code execution. You can access these files using the Files API:\n",
+ "```python\n",
+ "# Take all file outputs for demonstration purposes\n",
+ "file_ids = []\n",
+ "for block in response.content:\n",
+ " if block[\"type\"] == \"code_execution_tool_result\":\n",
+ " file_ids.extend(\n",
+ " content[\"file_id\"]\n",
+ " for content in block.get(\"content\", {}).get(\"content\", [])\n",
+ " if \"file_id\" in content\n",
+ " )\n",
+ "\n",
+ "for i, file_id in enumerate(file_ids):\n",
+ " file_content = client.beta.files.download(file_id)\n",
+ " file_content.write_to_file(f\"/path/to/file_{i}.png\")\n",
+ "```\n",
+ "\n",
+ " "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "040f381a-1768-479a-9a5e-aa2d7d77e0d5",
+ "metadata": {},
+ "source": [
+ "### Remote MCP\n",
+ "\n",
+ "Claude can use a [MCP connector tool](https://docs.anthropic.com/en/docs/agents-and-tools/mcp-connector) for model-generated calls to remote MCP servers.\n",
+ "\n",
+ ":::info Remote MCP is supported since ``langchain-anthropic>=0.3.14``\n",
+ "\n",
+ ":::"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "22fc4a89-e6d8-4615-96cb-2e117349aebf",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from langchain_anthropic import ChatAnthropic\n",
+ "\n",
+ "mcp_servers = [\n",
+ " {\n",
+ " \"type\": \"url\",\n",
+ " \"url\": \"https://mcp.deepwiki.com/mcp\",\n",
+ " \"name\": \"deepwiki\",\n",
+ " \"tool_configuration\": { # optional configuration\n",
+ " \"enabled\": True,\n",
+ " \"allowed_tools\": [\"ask_question\"],\n",
+ " },\n",
+ " \"authorization_token\": \"PLACEHOLDER\", # optional authorization\n",
+ " }\n",
+ "]\n",
+ "\n",
+ "llm = ChatAnthropic(\n",
+ " model=\"claude-sonnet-4-20250514\",\n",
+ " betas=[\"mcp-client-2025-04-04\"],\n",
+ " mcp_servers=mcp_servers,\n",
+ ")\n",
+ "\n",
+ "response = llm.invoke(\n",
+ " \"What transport protocols does the 2025-03-26 version of the MCP \"\n",
+ " \"spec (modelcontextprotocol/modelcontextprotocol) support?\"\n",
+ ")"
+ ]
+ },
{
"cell_type": "markdown",
"id": "2fd5d545-a40d-42b1-ad0c-0a79e2536c9b",
diff --git a/libs/core/langchain_core/language_models/chat_models.py b/libs/core/langchain_core/language_models/chat_models.py
index a0e975271f6..1070a8877e4 100644
--- a/libs/core/langchain_core/language_models/chat_models.py
+++ b/libs/core/langchain_core/language_models/chat_models.py
@@ -129,6 +129,7 @@ def _format_for_tracing(messages: list[BaseMessage]) -> list[BaseMessage]:
isinstance(block, dict)
and block.get("type") == "image"
and is_data_content_block(block)
+ and block.get("source_type") != "id"
):
if message_to_trace is message:
message_to_trace = message.model_copy()
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 9c24390de48..e90b7d6dca0 100644
--- a/libs/langchain/tests/unit_tests/chat_models/test_base.py
+++ b/libs/langchain/tests/unit_tests/chat_models/test_base.py
@@ -193,6 +193,7 @@ def test_configurable_with_default() -> None:
"name": None,
"disable_streaming": False,
"model": "claude-3-sonnet-20240229",
+ "mcp_servers": None,
"max_tokens": 1024,
"temperature": None,
"thinking": None,
@@ -203,6 +204,7 @@ def test_configurable_with_default() -> None:
"stop_sequences": None,
"anthropic_api_url": "https://api.anthropic.com",
"anthropic_api_key": SecretStr("bar"),
+ "betas": None,
"default_headers": None,
"model_kwargs": {},
"streaming": False,
diff --git a/libs/partners/anthropic/langchain_anthropic/chat_models.py b/libs/partners/anthropic/langchain_anthropic/chat_models.py
index a0313988604..3bc75804f08 100644
--- a/libs/partners/anthropic/langchain_anthropic/chat_models.py
+++ b/libs/partners/anthropic/langchain_anthropic/chat_models.py
@@ -1,4 +1,5 @@
import copy
+import json
import re
import warnings
from collections.abc import AsyncIterator, Iterator, Mapping, Sequence
@@ -100,6 +101,7 @@ def _is_builtin_tool(tool: Any) -> bool:
"computer_",
"bash_",
"web_search_",
+ "code_execution_",
]
return any(tool_type.startswith(prefix) for prefix in _builtin_tool_prefixes)
@@ -219,6 +221,14 @@ def _format_data_content_block(block: dict) -> dict:
"data": block["data"],
},
}
+ elif block["source_type"] == "id":
+ formatted_block = {
+ "type": "image",
+ "source": {
+ "type": "file",
+ "file_id": block["id"],
+ },
+ }
else:
raise ValueError(
"Anthropic only supports 'url' and 'base64' source_type for image "
@@ -252,6 +262,14 @@ def _format_data_content_block(block: dict) -> dict:
"data": block["text"],
},
}
+ elif block["source_type"] == "id":
+ formatted_block = {
+ "type": "document",
+ "source": {
+ "type": "file",
+ "file_id": block["id"],
+ },
+ }
else:
raise ValueError(f"Block of type {block['type']} is not supported.")
@@ -340,6 +358,29 @@ def _format_messages(
else:
block.pop("text", None)
content.append(block)
+ elif block["type"] in ("server_tool_use", "mcp_tool_use"):
+ formatted_block = {
+ k: v
+ for k, v in block.items()
+ if k
+ in (
+ "type",
+ "id",
+ "input",
+ "name",
+ "server_name", # for mcp_tool_use
+ "cache_control",
+ )
+ }
+ # Attempt to parse streamed output
+ if block.get("input") == {} and "partial_json" in block:
+ try:
+ input_ = json.loads(block["partial_json"])
+ if input_:
+ formatted_block["input"] = input_
+ except json.JSONDecodeError:
+ pass
+ content.append(formatted_block)
elif block["type"] == "text":
text = block.get("text", "")
# Only add non-empty strings for now as empty ones are not
@@ -375,6 +416,25 @@ def _format_messages(
[HumanMessage(block["content"])]
)[1][0]["content"]
content.append({**block, **{"content": tool_content}})
+ elif block["type"] in (
+ "code_execution_tool_result",
+ "mcp_tool_result",
+ "web_search_tool_result",
+ ):
+ content.append(
+ {
+ k: v
+ for k, v in block.items()
+ if k
+ in (
+ "type",
+ "content",
+ "tool_use_id",
+ "is_error", # for mcp_tool_result
+ "cache_control",
+ )
+ }
+ )
else:
content.append(block)
else:
@@ -472,20 +532,21 @@ class ChatAnthropic(BaseChatModel):
**NOTE**: Any param which is not explicitly supported will be passed directly to the
``anthropic.Anthropic.messages.create(...)`` API every time to the model is
invoked. For example:
- .. code-block:: python
- from langchain_anthropic import ChatAnthropic
- import anthropic
+ .. code-block:: python
- ChatAnthropic(..., extra_headers={}).invoke(...)
+ from langchain_anthropic import ChatAnthropic
+ import anthropic
- # results in underlying API call of:
+ ChatAnthropic(..., extra_headers={}).invoke(...)
- anthropic.Anthropic(..).messages.create(..., extra_headers={})
+ # results in underlying API call of:
- # which is also equivalent to:
+ anthropic.Anthropic(..).messages.create(..., extra_headers={})
- ChatAnthropic(...).invoke(..., extra_headers={})
+ # which is also equivalent to:
+
+ ChatAnthropic(...).invoke(..., extra_headers={})
Invoke:
.. code-block:: python
@@ -645,6 +706,35 @@ class ChatAnthropic(BaseChatModel):
"After examining both images carefully, I can see that they are actually identical."
+ .. dropdown:: Files API
+
+ You can also pass in files that are managed through Anthropic's
+ `Files API `_:
+
+ .. code-block:: python
+
+ from langchain_anthropic import ChatAnthropic
+
+ llm = ChatAnthropic(
+ model="claude-sonnet-4-20250514",
+ betas=["files-api-2025-04-14"],
+ )
+ input_message = {
+ "role": "user",
+ "content": [
+ {
+ "type": "text",
+ "text": "Describe this document.",
+ },
+ {
+ "type": "image",
+ "source_type": "id",
+ "id": "file_abc123...",
+ },
+ ],
+ }
+ llm.invoke([input_message])
+
PDF input:
See `multimodal guides `_
for more detail.
@@ -681,6 +771,35 @@ class ChatAnthropic(BaseChatModel):
"This appears to be a simple document..."
+ .. dropdown:: Files API
+
+ You can also pass in files that are managed through Anthropic's
+ `Files API `_:
+
+ .. code-block:: python
+
+ from langchain_anthropic import ChatAnthropic
+
+ llm = ChatAnthropic(
+ model="claude-sonnet-4-20250514",
+ betas=["files-api-2025-04-14"],
+ )
+ input_message = {
+ "role": "user",
+ "content": [
+ {
+ "type": "text",
+ "text": "Describe this document.",
+ },
+ {
+ "type": "file",
+ "source_type": "id",
+ "id": "file_abc123...",
+ },
+ ],
+ }
+ llm.invoke([input_message])
+
Extended thinking:
Claude 3.7 Sonnet supports an
`extended thinking `_
@@ -797,7 +916,7 @@ class ChatAnthropic(BaseChatModel):
or by setting ``stream_usage=False`` when initializing ChatAnthropic.
Prompt caching:
- See LangChain `docs `_
+ See LangChain `docs `_
for more detail.
.. code-block:: python
@@ -834,6 +953,24 @@ class ChatAnthropic(BaseChatModel):
{'cache_read': 0, 'cache_creation': 1458}
+ .. dropdown:: Extended caching
+
+ The cache lifetime is 5 minutes by default. If this is too short, you can
+ apply one hour caching by enabling the ``"extended-cache-ttl-2025-04-11"``
+ beta header:
+
+ .. code-block:: python
+
+ llm = ChatAnthropic(
+ model="claude-3-7-sonnet-20250219",
+ betas=["extended-cache-ttl-2025-04-11"],
+ )
+
+ and specifying ``"cache_control": {"type": "ephemeral", "ttl": "1h"}``.
+
+ See `Claude documentation `_
+ for detail.
+
Token-efficient tool use (beta):
See LangChain `docs `_
for more detail.
@@ -875,7 +1012,7 @@ class ChatAnthropic(BaseChatModel):
See LangChain `docs `_
for more detail.
- Web search:
+ .. dropdown:: Web search
.. code-block:: python
@@ -890,7 +1027,53 @@ class ChatAnthropic(BaseChatModel):
"How do I update a web app to TypeScript 5.5?"
)
- Text editor:
+ .. dropdown:: Code execution
+
+ .. code-block:: python
+
+ llm = ChatAnthropic(
+ model="claude-sonnet-4-20250514",
+ betas=["code-execution-2025-05-22"],
+ )
+
+ tool = {"type": "code_execution_20250522", "name": "code_execution"}
+ llm_with_tools = llm.bind_tools([tool])
+
+ response = llm_with_tools.invoke(
+ "Calculate the mean and standard deviation of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"
+ )
+
+ .. dropdown:: Remote MCP
+
+ .. code-block:: python
+
+ 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
+ }
+ ]
+
+ llm = ChatAnthropic(
+ model="claude-sonnet-4-20250514",
+ betas=["mcp-client-2025-04-04"],
+ mcp_servers=mcp_servers,
+ )
+
+ response = llm.invoke(
+ "What transport protocols does the 2025-03-26 version of the MCP "
+ "spec (modelcontextprotocol/modelcontextprotocol) support?"
+ )
+
+ .. dropdown:: Text editor
.. code-block:: python
@@ -986,6 +1169,13 @@ class ChatAnthropic(BaseChatModel):
default_headers: Optional[Mapping[str, str]] = None
"""Headers to pass to the Anthropic clients, will be used for every API call."""
+ betas: Optional[list[str]] = None
+ """List of beta features to enable. If specified, invocations will be routed
+ through client.beta.messages.create.
+
+ Example: ``betas=["mcp-client-2025-04-04"]``
+ """
+
model_kwargs: dict[str, Any] = Field(default_factory=dict)
streaming: bool = False
@@ -1000,6 +1190,13 @@ class ChatAnthropic(BaseChatModel):
"""Parameters for Claude reasoning,
e.g., ``{"type": "enabled", "budget_tokens": 10_000}``"""
+ mcp_servers: Optional[list[dict[str, Any]]] = None
+ """List of MCP servers to use for the request.
+
+ Example: ``mcp_servers=[{"type": "url", "url": "https://mcp.example.com/mcp",
+ "name": "example-mcp"}]``
+ """
+
@property
def _llm_type(self) -> str:
"""Return type of chat model."""
@@ -1007,7 +1204,10 @@ class ChatAnthropic(BaseChatModel):
@property
def lc_secrets(self) -> dict[str, str]:
- return {"anthropic_api_key": "ANTHROPIC_API_KEY"}
+ return {
+ "anthropic_api_key": "ANTHROPIC_API_KEY",
+ "mcp_servers": "ANTHROPIC_MCP_SERVERS",
+ }
@classmethod
def is_lc_serializable(cls) -> bool:
@@ -1099,6 +1299,8 @@ class ChatAnthropic(BaseChatModel):
"top_k": self.top_k,
"top_p": self.top_p,
"stop_sequences": stop or self.stop_sequences,
+ "betas": self.betas,
+ "mcp_servers": self.mcp_servers,
"system": system,
**self.model_kwargs,
**kwargs,
@@ -1107,6 +1309,18 @@ class ChatAnthropic(BaseChatModel):
payload["thinking"] = self.thinking
return {k: v for k, v in payload.items() if v is not None}
+ def _create(self, payload: dict) -> Any:
+ if "betas" in payload:
+ return self._client.beta.messages.create(**payload)
+ else:
+ return self._client.messages.create(**payload)
+
+ async def _acreate(self, payload: dict) -> Any:
+ if "betas" in payload:
+ return await self._async_client.beta.messages.create(**payload)
+ else:
+ return await self._async_client.messages.create(**payload)
+
def _stream(
self,
messages: list[BaseMessage],
@@ -1121,17 +1335,19 @@ class ChatAnthropic(BaseChatModel):
kwargs["stream"] = True
payload = self._get_request_payload(messages, stop=stop, **kwargs)
try:
- stream = self._client.messages.create(**payload)
+ stream = self._create(payload)
coerce_content_to_string = (
not _tools_in_params(payload)
and not _documents_in_params(payload)
and not _thinking_in_params(payload)
)
+ block_start_event = None
for event in stream:
- msg = _make_message_chunk_from_anthropic_event(
+ msg, block_start_event = _make_message_chunk_from_anthropic_event(
event,
stream_usage=stream_usage,
coerce_content_to_string=coerce_content_to_string,
+ block_start_event=block_start_event,
)
if msg is not None:
chunk = ChatGenerationChunk(message=msg)
@@ -1155,17 +1371,19 @@ class ChatAnthropic(BaseChatModel):
kwargs["stream"] = True
payload = self._get_request_payload(messages, stop=stop, **kwargs)
try:
- stream = await self._async_client.messages.create(**payload)
+ stream = await self._acreate(payload)
coerce_content_to_string = (
not _tools_in_params(payload)
and not _documents_in_params(payload)
and not _thinking_in_params(payload)
)
+ block_start_event = None
async for event in stream:
- msg = _make_message_chunk_from_anthropic_event(
+ msg, block_start_event = _make_message_chunk_from_anthropic_event(
event,
stream_usage=stream_usage,
coerce_content_to_string=coerce_content_to_string,
+ block_start_event=block_start_event,
)
if msg is not None:
chunk = ChatGenerationChunk(message=msg)
@@ -1234,7 +1452,7 @@ class ChatAnthropic(BaseChatModel):
return generate_from_stream(stream_iter)
payload = self._get_request_payload(messages, stop=stop, **kwargs)
try:
- data = self._client.messages.create(**payload)
+ data = self._create(payload)
except anthropic.BadRequestError as e:
_handle_anthropic_bad_request(e)
return self._format_output(data, **kwargs)
@@ -1253,7 +1471,7 @@ class ChatAnthropic(BaseChatModel):
return await agenerate_from_stream(stream_iter)
payload = self._get_request_payload(messages, stop=stop, **kwargs)
try:
- data = await self._async_client.messages.create(**payload)
+ data = await self._acreate(payload)
except anthropic.BadRequestError as e:
_handle_anthropic_bad_request(e)
return self._format_output(data, **kwargs)
@@ -1722,8 +1940,10 @@ def convert_to_anthropic_tool(
def _tools_in_params(params: dict) -> bool:
- return "tools" in params or (
- "extra_body" in params and params["extra_body"].get("tools")
+ return (
+ "tools" in params
+ or ("extra_body" in params and params["extra_body"].get("tools"))
+ or "mcp_servers" in params
)
@@ -1772,7 +1992,8 @@ def _make_message_chunk_from_anthropic_event(
*,
stream_usage: bool = True,
coerce_content_to_string: bool,
-) -> Optional[AIMessageChunk]:
+ block_start_event: Optional[anthropic.types.RawMessageStreamEvent] = None,
+) -> tuple[Optional[AIMessageChunk], Optional[anthropic.types.RawMessageStreamEvent]]:
"""Convert Anthropic event to AIMessageChunk.
Note that not all events will result in a message chunk. In these cases
@@ -1800,7 +2021,17 @@ def _make_message_chunk_from_anthropic_event(
elif (
event.type == "content_block_start"
and event.content_block is not None
- and event.content_block.type in ("tool_use", "document", "redacted_thinking")
+ and event.content_block.type
+ in (
+ "tool_use",
+ "code_execution_tool_result",
+ "document",
+ "redacted_thinking",
+ "mcp_tool_use",
+ "mcp_tool_result",
+ "server_tool_use",
+ "web_search_tool_result",
+ )
):
if coerce_content_to_string:
warnings.warn("Received unexpected tool content block.")
@@ -1820,6 +2051,7 @@ def _make_message_chunk_from_anthropic_event(
content=[content_block],
tool_call_chunks=tool_call_chunks, # type: ignore
)
+ block_start_event = event
elif event.type == "content_block_delta":
if event.delta.type in ("text_delta", "citations_delta"):
if coerce_content_to_string and hasattr(event.delta, "text"):
@@ -1849,16 +2081,23 @@ def _make_message_chunk_from_anthropic_event(
elif event.delta.type == "input_json_delta":
content_block = event.delta.model_dump()
content_block["index"] = event.index
- content_block["type"] = "tool_use"
- tool_call_chunk = create_tool_call_chunk(
- index=event.index,
- id=None,
- name=None,
- args=event.delta.partial_json,
- )
+ if (
+ (block_start_event is not None)
+ and hasattr(block_start_event, "content_block")
+ and (block_start_event.content_block.type == "tool_use")
+ ):
+ tool_call_chunk = create_tool_call_chunk(
+ index=event.index,
+ id=None,
+ name=None,
+ args=event.delta.partial_json,
+ )
+ tool_call_chunks = [tool_call_chunk]
+ else:
+ tool_call_chunks = []
message_chunk = AIMessageChunk(
content=[content_block],
- tool_call_chunks=[tool_call_chunk], # type: ignore
+ tool_call_chunks=tool_call_chunks, # type: ignore
)
elif event.type == "message_delta" and stream_usage:
usage_metadata = UsageMetadata(
@@ -1877,7 +2116,7 @@ def _make_message_chunk_from_anthropic_event(
else:
pass
- return message_chunk
+ return message_chunk, block_start_event
@deprecated(since="0.1.0", removal="1.0.0", alternative="ChatAnthropic")
diff --git a/libs/partners/anthropic/pyproject.toml b/libs/partners/anthropic/pyproject.toml
index 2177a569b26..02255079587 100644
--- a/libs/partners/anthropic/pyproject.toml
+++ b/libs/partners/anthropic/pyproject.toml
@@ -7,7 +7,7 @@ authors = []
license = { text = "MIT" }
requires-python = ">=3.9"
dependencies = [
- "anthropic<1,>=0.51.0",
+ "anthropic<1,>=0.52.0",
"langchain-core<1.0.0,>=0.3.59",
"pydantic<3.0.0,>=2.7.4",
]
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 a2563cc480d..1fe50cf91d7 100644
--- a/libs/partners/anthropic/tests/integration_tests/test_chat_models.py
+++ b/libs/partners/anthropic/tests/integration_tests/test_chat_models.py
@@ -1,6 +1,7 @@
"""Test ChatAnthropic chat model."""
import json
+import os
from base64 import b64encode
from typing import Optional
@@ -863,3 +864,206 @@ def test_image_tool_calling() -> None:
]
llm = ChatAnthropic(model="claude-3-5-sonnet-latest")
llm.bind_tools([color_picker]).invoke(messages)
+
+
+# TODO: set up VCR
+def test_web_search() -> None:
+ pytest.skip()
+ llm = ChatAnthropic(model="claude-3-5-sonnet-latest")
+
+ tool = {"type": "web_search_20250305", "name": "web_search", "max_uses": 1}
+ llm_with_tools = llm.bind_tools([tool])
+
+ input_message = {
+ "role": "user",
+ "content": [
+ {
+ "type": "text",
+ "text": "How do I update a web app to TypeScript 5.5?",
+ }
+ ],
+ }
+ response = llm_with_tools.invoke([input_message])
+ block_types = {block["type"] for block in response.content}
+ assert block_types == {"text", "server_tool_use", "web_search_tool_result"}
+
+ # Test streaming
+ full: Optional[BaseMessageChunk] = None
+ for chunk in llm_with_tools.stream([input_message]):
+ assert isinstance(chunk, AIMessageChunk)
+ full = chunk if full is None else full + chunk
+ assert isinstance(full, AIMessageChunk)
+ assert isinstance(full.content, list)
+ block_types = {block["type"] for block in full.content} # type: ignore[index]
+ assert block_types == {"text", "server_tool_use", "web_search_tool_result"}
+
+ # Test we can pass back in
+ next_message = {
+ "role": "user",
+ "content": "Please repeat the last search, but focus on sources from 2024.",
+ }
+ _ = llm_with_tools.invoke(
+ [input_message, full, next_message],
+ )
+
+
+def test_code_execution() -> None:
+ pytest.skip()
+ llm = ChatAnthropic(
+ model="claude-sonnet-4-20250514",
+ betas=["code-execution-2025-05-22"],
+ )
+
+ tool = {"type": "code_execution_20250522", "name": "code_execution"}
+ llm_with_tools = llm.bind_tools([tool])
+
+ input_message = {
+ "role": "user",
+ "content": [
+ {
+ "type": "text",
+ "text": (
+ "Calculate the mean and standard deviation of "
+ "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"
+ ),
+ }
+ ],
+ }
+ response = llm_with_tools.invoke([input_message])
+ block_types = {block["type"] for block in response.content}
+ assert block_types == {"text", "server_tool_use", "code_execution_tool_result"}
+
+ # Test streaming
+ full: Optional[BaseMessageChunk] = None
+ for chunk in llm_with_tools.stream([input_message]):
+ assert isinstance(chunk, AIMessageChunk)
+ full = chunk if full is None else full + chunk
+ assert isinstance(full, AIMessageChunk)
+ assert isinstance(full.content, list)
+ block_types = {block["type"] for block in full.content} # type: ignore[index]
+ assert block_types == {"text", "server_tool_use", "code_execution_tool_result"}
+
+ # Test we can pass back in
+ next_message = {
+ "role": "user",
+ "content": "Please add more comments to the code.",
+ }
+ _ = llm_with_tools.invoke(
+ [input_message, full, next_message],
+ )
+
+
+def test_remote_mcp() -> None:
+ pytest.skip()
+ mcp_servers = [
+ {
+ "type": "url",
+ "url": "https://mcp.deepwiki.com/mcp",
+ "name": "deepwiki",
+ "tool_configuration": {"enabled": True, "allowed_tools": ["ask_question"]},
+ "authorization_token": "PLACEHOLDER",
+ }
+ ]
+
+ llm = ChatAnthropic(
+ model="claude-sonnet-4-20250514",
+ betas=["mcp-client-2025-04-04"],
+ mcp_servers=mcp_servers,
+ )
+
+ input_message = {
+ "role": "user",
+ "content": [
+ {
+ "type": "text",
+ "text": (
+ "What transport protocols does the 2025-03-26 version of the MCP "
+ "spec (modelcontextprotocol/modelcontextprotocol) support?"
+ ),
+ }
+ ],
+ }
+ response = llm.invoke([input_message])
+ block_types = {block["type"] for block in response.content}
+ assert block_types == {"text", "mcp_tool_use", "mcp_tool_result"}
+
+ # Test streaming
+ full: Optional[BaseMessageChunk] = None
+ for chunk in llm.stream([input_message]):
+ assert isinstance(chunk, AIMessageChunk)
+ full = chunk if full is None else full + chunk
+ assert isinstance(full, AIMessageChunk)
+ assert isinstance(full.content, list)
+ block_types = {block["type"] for block in full.content}
+ assert block_types == {"text", "mcp_tool_use", "mcp_tool_result"}
+
+ # Test we can pass back in
+ next_message = {
+ "role": "user",
+ "content": "Please query the same tool again, but add 'please' to your query.",
+ }
+ _ = llm.invoke(
+ [input_message, full, next_message],
+ )
+
+
+@pytest.mark.parametrize("block_format", ["anthropic", "standard"])
+def test_files_api_image(block_format: str) -> None:
+ image_file_id = os.getenv("ANTHROPIC_FILES_API_IMAGE_ID")
+ if not image_file_id:
+ pytest.skip()
+ llm = ChatAnthropic(
+ model="claude-sonnet-4-20250514",
+ betas=["files-api-2025-04-14"],
+ )
+ if block_format == "anthropic":
+ block = {
+ "type": "image",
+ "source": {
+ "type": "file",
+ "file_id": image_file_id,
+ },
+ }
+ else:
+ # standard block format
+ block = {
+ "type": "image",
+ "source_type": "id",
+ "id": image_file_id,
+ }
+ input_message = {
+ "role": "user",
+ "content": [
+ {"type": "text", "text": "Describe this image."},
+ block,
+ ],
+ }
+ _ = llm.invoke([input_message])
+
+
+@pytest.mark.parametrize("block_format", ["anthropic", "standard"])
+def test_files_api_pdf(block_format: str) -> None:
+ pdf_file_id = os.getenv("ANTHROPIC_FILES_API_PDF_ID")
+ if not pdf_file_id:
+ pytest.skip()
+ llm = ChatAnthropic(
+ model="claude-sonnet-4-20250514",
+ betas=["files-api-2025-04-14"],
+ )
+ if block_format == "anthropic":
+ block = {"type": "document", "source": {"type": "file", "file_id": pdf_file_id}}
+ else:
+ # standard block format
+ block = {
+ "type": "file",
+ "source_type": "id",
+ "id": pdf_file_id,
+ }
+ input_message = {
+ "role": "user",
+ "content": [
+ {"type": "text", "text": "Describe this document."},
+ block,
+ ],
+ }
+ _ = llm.invoke([input_message])
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 1c18fc08d94..2d418b6bddd 100644
--- a/libs/partners/anthropic/tests/unit_tests/test_chat_models.py
+++ b/libs/partners/anthropic/tests/unit_tests/test_chat_models.py
@@ -2,7 +2,7 @@
import os
from typing import Any, Callable, Literal, Optional, cast
-from unittest.mock import patch
+from unittest.mock import MagicMock, patch
import anthropic
import pytest
@@ -10,6 +10,8 @@ from anthropic.types import Message, TextBlock, Usage
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage
from langchain_core.runnables import RunnableBinding
from langchain_core.tools import BaseTool
+from langchain_core.tracers.base import BaseTracer
+from langchain_core.tracers.schemas import Run
from pydantic import BaseModel, Field, SecretStr
from pytest import CaptureFixture, MonkeyPatch
@@ -994,3 +996,63 @@ def test_usage_metadata_standardization() -> None:
assert result["input_tokens"] == 0
assert result["output_tokens"] == 0
assert result["total_tokens"] == 0
+
+
+class FakeTracer(BaseTracer):
+ def __init__(self) -> None:
+ super().__init__()
+ self.chat_model_start_inputs: list = []
+
+ def _persist_run(self, run: Run) -> None:
+ """Persist a run."""
+ pass
+
+ def on_chat_model_start(self, *args: Any, **kwargs: Any) -> Run:
+ self.chat_model_start_inputs.append({"args": args, "kwargs": kwargs})
+ return super().on_chat_model_start(*args, **kwargs)
+
+
+def test_mcp_tracing() -> None:
+ # Test we exclude sensitive information from traces
+ mcp_servers = [
+ {
+ "type": "url",
+ "url": "https://mcp.deepwiki.com/mcp",
+ "name": "deepwiki",
+ "authorization_token": "PLACEHOLDER",
+ }
+ ]
+
+ llm = ChatAnthropic(
+ model="claude-sonnet-4-20250514",
+ betas=["mcp-client-2025-04-04"],
+ mcp_servers=mcp_servers,
+ )
+
+ tracer = FakeTracer()
+ mock_client = MagicMock()
+
+ def mock_create(*args: Any, **kwargs: Any) -> Message:
+ return Message(
+ id="foo",
+ content=[TextBlock(type="text", text="bar")],
+ model="baz",
+ role="assistant",
+ stop_reason=None,
+ stop_sequence=None,
+ usage=Usage(input_tokens=2, output_tokens=1),
+ type="message",
+ )
+
+ mock_client.messages.create = mock_create
+ input_message = HumanMessage("Test query")
+ with patch.object(llm, "_client", mock_client):
+ _ = llm.invoke([input_message], config={"callbacks": [tracer]})
+
+ # Test headers are not traced
+ assert len(tracer.chat_model_start_inputs) == 1
+ assert "PLACEHOLDER" not in str(tracer.chat_model_start_inputs)
+
+ # Test headers are correctly propagated to request
+ payload = llm._get_request_payload([input_message])
+ assert payload["mcp_servers"][0]["authorization_token"] == "PLACEHOLDER"
diff --git a/libs/partners/anthropic/uv.lock b/libs/partners/anthropic/uv.lock
index 4b18119da96..df8c8056ebf 100644
--- a/libs/partners/anthropic/uv.lock
+++ b/libs/partners/anthropic/uv.lock
@@ -18,7 +18,7 @@ wheels = [
[[package]]
name = "anthropic"
-version = "0.51.0"
+version = "0.52.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -29,9 +29,9 @@ dependencies = [
{ name = "sniffio" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/63/4a/96f99a61ae299f9e5aa3e765d7342d95ab2e2ba5b69a3ffedb00ef779651/anthropic-0.51.0.tar.gz", hash = "sha256:6f824451277992af079554430d5b2c8ff5bc059cc2c968cdc3f06824437da201", size = 219063 }
+sdist = { url = "https://files.pythonhosted.org/packages/57/fd/8a9332f5baf352c272494a9d359863a53385a208954c1a7251a524071930/anthropic-0.52.0.tar.gz", hash = "sha256:f06bc924d7eb85f8a43fe587b875ff58b410d60251b7dc5f1387b322a35bd67b", size = 229372 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/8c/6e/9637122c5f007103bd5a259f4250bd8f1533dd2473227670fd10a1457b62/anthropic-0.51.0-py3-none-any.whl", hash = "sha256:b8b47d482c9aa1f81b923555cebb687c2730309a20d01be554730c8302e0f62a", size = 263957 },
+ { url = "https://files.pythonhosted.org/packages/a0/43/172c0031654908bbac2a87d356fff4de1b4947a9b14b9658540b69416417/anthropic-0.52.0-py3-none-any.whl", hash = "sha256:c026daa164f0e3bde36ce9cbdd27f5f1419fff03306be1e138726f42e6a7810f", size = 286076 },
]
[[package]]
@@ -451,7 +451,7 @@ typing = [
[package.metadata]
requires-dist = [
- { name = "anthropic", specifier = ">=0.51.0,<1" },
+ { name = "anthropic", specifier = ">=0.52.0,<1" },
{ name = "langchain-core", editable = "../../core" },
{ name = "pydantic", specifier = ">=2.7.4,<3.0.0" },
]
@@ -486,7 +486,7 @@ typing = [
[[package]]
name = "langchain-core"
-version = "0.3.59"
+version = "0.3.61"
source = { editable = "../../core" }
dependencies = [
{ name = "jsonpatch" },
@@ -501,10 +501,9 @@ dependencies = [
[package.metadata]
requires-dist = [
{ name = "jsonpatch", specifier = ">=1.33,<2.0" },
- { name = "langsmith", specifier = ">=0.1.125,<0.4" },
+ { name = "langsmith", specifier = ">=0.1.126,<0.4" },
{ name = "packaging", specifier = ">=23.2,<25" },
- { name = "pydantic", marker = "python_full_version < '3.12.4'", specifier = ">=2.5.2,<3.0.0" },
- { name = "pydantic", marker = "python_full_version >= '3.12.4'", specifier = ">=2.7.4,<3.0.0" },
+ { name = "pydantic", specifier = ">=2.7.4" },
{ name = "pyyaml", specifier = ">=5.3" },
{ name = "tenacity", specifier = ">=8.1.0,!=8.4.0,<10.0.0" },
{ name = "typing-extensions", specifier = ">=4.7" },