feat(anthropic): auto append relevant beta headers (#34113)

This commit is contained in:
Mason Daugherty
2025-12-01 12:20:41 -05:00
committed by GitHub
parent 7a2952210e
commit b7091d391d
3 changed files with 392 additions and 37 deletions

View File

@@ -121,6 +121,16 @@ class AnthropicTool(TypedDict):
cache_control: NotRequired[dict[str, str]]
# Some tool types require specific beta headers to be enabled
# Mapping of tool type patterns to required beta headers
_TOOL_TYPE_TO_BETA: dict[str, str] = {
"web_fetch_20250910": "web-fetch-2025-09-10",
"code_execution_20250522": "code-execution-2025-05-22",
"code_execution_20250825": "code-execution-2025-08-25",
"memory_20250818": "context-management-2025-06-27",
}
def _is_builtin_tool(tool: Any) -> bool:
"""Check if a tool is a built-in Anthropic tool.
@@ -1393,12 +1403,11 @@ class ChatAnthropic(BaseChatModel):
??? example "Web fetch (beta)"
```python hl_lines="5 8-12"
```python hl_lines="7-11"
from langchain_anthropic import ChatAnthropic
model = ChatAnthropic(
model="claude-3-5-haiku-20241022",
betas=["web-fetch-2025-09-10"], # Enable web fetch beta
)
tool = {
@@ -1411,16 +1420,19 @@ class ChatAnthropic(BaseChatModel):
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"
```python hl_lines="3 6-9"
model = ChatAnthropic(
model="claude-sonnet-4-5-20250929",
betas=["code-execution-2025-05-22"], # Enable code execution beta
)
```python hl_lines="3-6"
model = ChatAnthropic(model="claude-sonnet-4-5-20250929")
tool = {
"type": "code_execution_20250522",
@@ -1433,18 +1445,21 @@ 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.
See the [Claude docs](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool)
for more info.
??? example "Memory tool"
```python hl_lines="5 8-11"
```python hl_lines="5-8"
from langchain_anthropic import ChatAnthropic
model = ChatAnthropic(
model="claude-sonnet-4-5-20250929",
betas=["context-management-2025-06-27"], # Enable context management beta
)
model = ChatAnthropic(model="claude-sonnet-4-5-20250929")
tool = {
"type": "memory_20250818",
@@ -1455,6 +1470,12 @@ class ChatAnthropic(BaseChatModel):
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.
@@ -1592,6 +1613,12 @@ class ChatAnthropic(BaseChatModel):
Example: `#!python betas=["mcp-client-2025-04-04"]`
"""
# Can also be passed in w/ model_kwargs, but having it as a param makes better devx
#
# Precedence order:
# 1. Call-time kwargs (e.g., llm.invoke(..., betas=[...]))
# 2. model_kwargs (e.g., ChatAnthropic(model_kwargs={"betas": [...]}))
# 3. Direct parameter (e.g., ChatAnthropic(betas=[...]))
model_kwargs: dict[str, Any] = Field(default_factory=dict)
@@ -1842,21 +1869,74 @@ class ChatAnthropic(BaseChatModel):
payload["thinking"] = self.thinking
if "response_format" in payload:
# response_format present when using agents.create_agent's ProviderStrategy
# ---
# ProviderStrategy converts to OpenAI-style format, which passes kwargs to
# ChatAnthropic, ending up in our payload
response_format = payload.pop("response_format")
if (
isinstance(response_format, dict)
and response_format.get("type") == "json_schema"
and "schema" in response_format.get("json_schema", {})
):
# compat with langchain.agents.create_agent response_format, which is
# an approximation of OpenAI format
response_format = cast(dict, response_format["json_schema"]["schema"])
# Convert OpenAI-style response_format to Anthropic's output_format
payload["output_format"] = _convert_to_anthropic_output_format(
response_format
)
if "output_format" in payload and not payload["betas"]:
payload["betas"] = ["structured-outputs-2025-11-13"]
if "output_format" in payload:
# Native structured output requires the structured outputs beta
if payload["betas"]:
if "structured-outputs-2025-11-13" not in payload["betas"]:
# Merge with existing betas
payload["betas"] = [
*payload["betas"],
"structured-outputs-2025-11-13",
]
else:
payload["betas"] = ["structured-outputs-2025-11-13"]
# Check if any tools have strict mode enabled
if "tools" in payload and isinstance(payload["tools"], list):
has_strict_tool = any(
isinstance(tool, dict) and tool.get("strict") is True
for tool in payload["tools"]
)
if has_strict_tool:
# Strict tool use requires the structured outputs beta
if payload["betas"]:
if "structured-outputs-2025-11-13" not in payload["betas"]:
# Merge with existing betas
payload["betas"] = [
*payload["betas"],
"structured-outputs-2025-11-13",
]
else:
payload["betas"] = ["structured-outputs-2025-11-13"]
# Auto-append required betas for specific tool types
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:
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]
else:
payload["betas"] = [required_beta]
# Auto-append required beta for mcp_servers
if payload.get("mcp_servers"):
required_beta = "mcp-client-2025-11-20"
if payload["betas"]:
# Append to existing betas if not already present
if required_beta not in payload["betas"]:
payload["betas"] = [*payload["betas"], required_beta]
else:
payload["betas"] = [required_beta]
return {k: v for k, v in payload.items() if v is not None}
@@ -2300,17 +2380,13 @@ class ChatAnthropic(BaseChatModel):
- Claude Sonnet 4.5 or Opus 4.1
- `langchain-anthropic>=1.1.0`
To enable strict tool use:
To enable strict tool use, specify `strict=True` when calling `bind_tools`.
1. Specify the `structured-outputs-2025-11-13` beta header
2. Specify `strict=True` when calling `bind_tools`
```python hl_lines="5 12"
```python hl_lines="11"
from langchain_anthropic import ChatAnthropic
model = ChatAnthropic(
model="claude-sonnet-4-5",
betas=["structured-outputs-2025-11-13"],
)
def get_weather(location: str) -> str:
@@ -2320,6 +2396,12 @@ class ChatAnthropic(BaseChatModel):
model_with_tools = model.bind_tools([get_weather], strict=True)
```
!!! note "Automatic beta header"
The required `structured-outputs-2025-11-13` beta header is
automatically appended to the request when using `strict=True`, so you
don't need to manually specify it in the `betas` parameter.
See LangChain [docs](https://docs.langchain.com/oss/python/integrations/chat/anthropic#strict-tool-use)
for more detail.
""" # noqa: E501
@@ -2513,19 +2595,15 @@ class ChatAnthropic(BaseChatModel):
- Claude Sonnet 4.5 or Opus 4.1
- `langchain-anthropic>=1.1.0`
To enable native structured output:
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)
1. Specify the `structured-outputs-2025-11-13` beta header
2. Specify `method="json_schema"` when calling `with_structured_output`
```python hl_lines="6 16"
```python hl_lines="13"
from langchain_anthropic import ChatAnthropic
from pydantic import BaseModel, Field
model = ChatAnthropic(
model="claude-sonnet-4-5",
betas=["structured-outputs-2025-11-13"],
)
model = ChatAnthropic(model="claude-sonnet-4-5")
class Movie(BaseModel):
\"\"\"A movie with details.\"\"\"
@@ -2713,8 +2791,7 @@ def convert_to_anthropic_tool(
!!! note
Requires Claude Sonnet 4.5 or Opus 4.1 and the
`structured-outputs-2025-11-13` beta header.
Requires Claude Sonnet 4.5 or Opus 4.1.
Returns:
An Anthropic tool definition dict.

View File

@@ -1398,7 +1398,7 @@ def test_mcp_tracing() -> None:
]
llm = ChatAnthropic(
model="claude-sonnet-4-5-20250929",
model=MODEL_NAME,
betas=["mcp-client-2025-04-04"],
mcp_servers=mcp_servers,
)
@@ -1586,7 +1586,7 @@ def test_streaming_cache_token_reporting() -> None:
def test_strict_tool_use() -> None:
model = ChatAnthropic(
model="claude-sonnet-4-5", # type: ignore[call-arg]
model=MODEL_NAME, # type: ignore[call-arg]
betas=["structured-outputs-2025-11-13"],
)
@@ -1600,6 +1600,284 @@ def test_strict_tool_use() -> None:
assert tool_definition["strict"] is True
def test_beta_merging_with_response_format() -> None:
"""Test that structured-outputs beta is merged with existing betas."""
class Person(BaseModel):
"""Person data."""
name: str
age: int
# Auto-inject structured-outputs beta with no others specified
model = ChatAnthropic(model=MODEL_NAME)
payload = model._get_request_payload(
"Test query",
response_format=Person.model_json_schema(),
)
assert payload["betas"] == ["structured-outputs-2025-11-13"]
# Merge structured-outputs beta if other betas are present
model = ChatAnthropic(
model=MODEL_NAME,
betas=["mcp-client-2025-04-04"],
)
payload = model._get_request_payload(
"Test query",
response_format=Person.model_json_schema(),
)
assert payload["betas"] == [
"mcp-client-2025-04-04",
"structured-outputs-2025-11-13",
]
# Structured-outputs beta already present - don't duplicate
model = ChatAnthropic(
model=MODEL_NAME,
betas=[
"mcp-client-2025-04-04",
"structured-outputs-2025-11-13",
],
)
payload = model._get_request_payload(
"Test query",
response_format=Person.model_json_schema(),
)
assert payload["betas"] == [
"mcp-client-2025-04-04",
"structured-outputs-2025-11-13",
]
# No response_format - betas should not be modified
model = ChatAnthropic(
model=MODEL_NAME,
betas=["mcp-client-2025-04-04"],
)
payload = model._get_request_payload("Test query")
assert payload["betas"] == ["mcp-client-2025-04-04"]
def test_beta_merging_with_strict_tool_use() -> None:
"""Test beta merging for strict tools."""
def get_weather(location: str) -> str:
"""Get the weather at a location."""
return "Sunny"
# Auto-inject structured-outputs beta with no others specified
model = ChatAnthropic(model=MODEL_NAME) # type: ignore[call-arg]
model_with_tools = model.bind_tools([get_weather], strict=True)
payload = model_with_tools._get_request_payload( # type: ignore[attr-defined]
"What's the weather?",
**model_with_tools.kwargs, # type: ignore[attr-defined]
)
assert payload["betas"] == ["structured-outputs-2025-11-13"]
# Merge structured-outputs beta if other betas are present
model = ChatAnthropic(
model=MODEL_NAME, # type: ignore[call-arg]
betas=["mcp-client-2025-04-04"],
)
model_with_tools = model.bind_tools([get_weather], strict=True)
payload = model_with_tools._get_request_payload( # type: ignore[attr-defined]
"What's the weather?",
**model_with_tools.kwargs, # type: ignore[attr-defined]
)
assert payload["betas"] == [
"mcp-client-2025-04-04",
"structured-outputs-2025-11-13",
]
# Structured-outputs beta already present - don't duplicate
model = ChatAnthropic(
model=MODEL_NAME, # type: ignore[call-arg]
betas=[
"mcp-client-2025-04-04",
"structured-outputs-2025-11-13",
],
)
model_with_tools = model.bind_tools([get_weather], strict=True)
payload = model_with_tools._get_request_payload( # type: ignore[attr-defined]
"What's the weather?",
**model_with_tools.kwargs, # type: ignore[attr-defined]
)
assert payload["betas"] == [
"mcp-client-2025-04-04",
"structured-outputs-2025-11-13",
]
# No strict tools - betas should not be modified
model = ChatAnthropic(
model=MODEL_NAME, # type: ignore[call-arg]
betas=["mcp-client-2025-04-04"],
)
model_with_tools = model.bind_tools([get_weather], strict=False)
payload = model_with_tools._get_request_payload( # type: ignore[attr-defined]
"What's the weather?",
**model_with_tools.kwargs, # type: ignore[attr-defined]
)
assert payload["betas"] == ["mcp-client-2025-04-04"]
def test_auto_append_betas_for_tool_types() -> None:
"""Test that betas are automatically appended based on tool types."""
# Test web_fetch_20250910 auto-appends web-fetch-2025-09-10
model = ChatAnthropic(model=MODEL_NAME) # type: ignore[call-arg]
tool = {"type": "web_fetch_20250910", "name": "web_fetch", "max_uses": 3}
model_with_tools = model.bind_tools([tool])
payload = model_with_tools._get_request_payload( # type: ignore[attr-defined]
"test",
**model_with_tools.kwargs, # type: ignore[attr-defined]
)
assert payload["betas"] == ["web-fetch-2025-09-10"]
# Test code_execution_20250522 auto-appends code-execution-2025-05-22
model = ChatAnthropic(model=MODEL_NAME) # type: ignore[call-arg]
tool = {"type": "code_execution_20250522", "name": "code_execution"}
model_with_tools = model.bind_tools([tool])
payload = model_with_tools._get_request_payload( # type: ignore[attr-defined]
"test",
**model_with_tools.kwargs, # type: ignore[attr-defined]
)
assert payload["betas"] == ["code-execution-2025-05-22"]
# Test memory_20250818 auto-appends context-management-2025-06-27
model = ChatAnthropic(model=MODEL_NAME) # type: ignore[call-arg]
tool = {"type": "memory_20250818", "name": "memory"}
model_with_tools = model.bind_tools([tool])
payload = model_with_tools._get_request_payload( # type: ignore[attr-defined]
"test",
**model_with_tools.kwargs, # type: ignore[attr-defined]
)
assert payload["betas"] == ["context-management-2025-06-27"]
# Test merging with existing betas
model = ChatAnthropic(
model=MODEL_NAME,
betas=["mcp-client-2025-04-04"], # type: ignore[call-arg]
)
tool = {"type": "web_fetch_20250910", "name": "web_fetch"}
model_with_tools = model.bind_tools([tool])
payload = model_with_tools._get_request_payload( # type: ignore[attr-defined]
"test",
**model_with_tools.kwargs, # type: ignore[attr-defined]
)
assert payload["betas"] == ["mcp-client-2025-04-04", "web-fetch-2025-09-10"]
# Test that it doesn't duplicate existing betas
model = ChatAnthropic(
model=MODEL_NAME,
betas=["web-fetch-2025-09-10"], # type: ignore[call-arg]
)
tool = {"type": "web_fetch_20250910", "name": "web_fetch"}
model_with_tools = model.bind_tools([tool])
payload = model_with_tools._get_request_payload( # type: ignore[attr-defined]
"test",
**model_with_tools.kwargs, # type: ignore[attr-defined]
)
assert payload["betas"] == ["web-fetch-2025-09-10"]
# Test multiple tools with different beta requirements
model = ChatAnthropic(model=MODEL_NAME) # type: ignore[call-arg]
tools = [
{"type": "web_fetch_20250910", "name": "web_fetch"},
{"type": "code_execution_20250522", "name": "code_execution"},
]
model_with_tools = model.bind_tools(tools)
payload = model_with_tools._get_request_payload( # type: ignore[attr-defined]
"test",
**model_with_tools.kwargs, # type: ignore[attr-defined]
)
assert set(payload["betas"]) == {
"web-fetch-2025-09-10",
"code-execution-2025-05-22",
}
def test_auto_append_betas_for_mcp_servers() -> None:
"""Test that `mcp-client-2025-11-20` beta is automatically appended
for `mcp_servers`."""
model = ChatAnthropic(model=MODEL_NAME) # type: ignore[call-arg]
mcp_servers = [
{
"type": "url",
"url": "https://mcp.example.com/mcp",
"name": "example",
}
]
payload = model._get_request_payload(
"Test query",
mcp_servers=mcp_servers, # type: ignore[arg-type]
)
assert payload["betas"] == ["mcp-client-2025-11-20"]
assert payload["mcp_servers"] == mcp_servers
# Test merging with existing betas
model = ChatAnthropic(
model=MODEL_NAME,
betas=["context-management-2025-06-27"],
)
payload = model._get_request_payload(
"Test query",
mcp_servers=mcp_servers, # type: ignore[arg-type]
)
assert payload["betas"] == [
"context-management-2025-06-27",
"mcp-client-2025-11-20",
]
# Test that it doesn't duplicate if beta already present
model = ChatAnthropic(
model=MODEL_NAME,
betas=["mcp-client-2025-11-20"],
)
payload = model._get_request_payload(
"Test query",
mcp_servers=mcp_servers, # type: ignore[arg-type]
)
assert payload["betas"] == ["mcp-client-2025-11-20"]
# Test with mcp_servers set on model initialization
model = ChatAnthropic(
model=MODEL_NAME,
mcp_servers=mcp_servers, # type: ignore[arg-type]
)
payload = model._get_request_payload("Test query")
assert payload["betas"] == ["mcp-client-2025-11-20"]
assert payload["mcp_servers"] == mcp_servers
# Test with existing betas and mcp_servers on model initialization
model = ChatAnthropic(
model=MODEL_NAME,
betas=["context-management-2025-06-27"],
mcp_servers=mcp_servers, # type: ignore[arg-type]
)
payload = model._get_request_payload("Test query")
assert payload["betas"] == [
"context-management-2025-06-27",
"mcp-client-2025-11-20",
]
# Test that beta is not appended when mcp_servers is None
model = ChatAnthropic(model=MODEL_NAME)
payload = model._get_request_payload("Test query")
assert "betas" not in payload or payload["betas"] is None
# Test combining mcp_servers with tool types that require betas
model = ChatAnthropic(model=MODEL_NAME)
tool = {"type": "web_fetch_20250910", "name": "web_fetch"}
model_with_tools = model.bind_tools([tool])
payload = model_with_tools._get_request_payload( # type: ignore[attr-defined]
"Test query",
mcp_servers=mcp_servers,
**model_with_tools.kwargs, # type: ignore[attr-defined]
)
assert set(payload["betas"]) == {
"web-fetch-2025-09-10",
"mcp-client-2025-11-20",
}
def test_profile() -> None:
model = ChatAnthropic(model="claude-sonnet-4-20250514")
assert model.profile

View File

@@ -1,5 +1,5 @@
version = 1
revision = 2
revision = 3
requires-python = ">=3.10.0, <4.0.0"
resolution-markers = [
"python_full_version >= '3.13' and platform_python_implementation == 'PyPy'",
@@ -495,7 +495,7 @@ wheels = [
[[package]]
name = "langchain"
version = "1.0.5"
version = "1.1.0"
source = { editable = "../../langchain_v1" }
dependencies = [
{ name = "langchain-core" },