mirror of
https://github.com/hwchase17/langchain.git
synced 2026-03-18 02:53:16 +00:00
feat(anthropic): delegate cache_control kwarg to anthropic top-level param (#35967)
This commit is contained in:
@@ -688,65 +688,6 @@ def _format_messages(
|
||||
return system, formatted_messages
|
||||
|
||||
|
||||
def _collect_code_execution_tool_ids(formatted_messages: list[dict]) -> set[str]:
|
||||
"""Collect tool_use IDs that were called by code_execution.
|
||||
|
||||
These blocks cannot have cache_control applied per Anthropic API requirements.
|
||||
"""
|
||||
code_execution_tool_ids: set[str] = set()
|
||||
|
||||
for message in formatted_messages:
|
||||
if message.get("role") != "assistant":
|
||||
continue
|
||||
content = message.get("content", [])
|
||||
if not isinstance(content, list):
|
||||
continue
|
||||
for block in content:
|
||||
if not isinstance(block, dict):
|
||||
continue
|
||||
if block.get("type") != "tool_use":
|
||||
continue
|
||||
caller = block.get("caller")
|
||||
if isinstance(caller, dict):
|
||||
caller_type = caller.get("type", "")
|
||||
if caller_type.startswith("code_execution"):
|
||||
tool_id = block.get("id")
|
||||
if tool_id:
|
||||
code_execution_tool_ids.add(tool_id)
|
||||
|
||||
return code_execution_tool_ids
|
||||
|
||||
|
||||
def _is_code_execution_related_block(
|
||||
block: dict,
|
||||
code_execution_tool_ids: set[str],
|
||||
) -> bool:
|
||||
"""Check if a content block is related to code_execution.
|
||||
|
||||
Returns True for blocks that should NOT have cache_control applied.
|
||||
"""
|
||||
if not isinstance(block, dict):
|
||||
return False
|
||||
|
||||
block_type = block.get("type")
|
||||
|
||||
# tool_use blocks called by code_execution
|
||||
if block_type == "tool_use":
|
||||
caller = block.get("caller")
|
||||
if isinstance(caller, dict):
|
||||
caller_type = caller.get("type", "")
|
||||
if caller_type.startswith("code_execution"):
|
||||
return True
|
||||
|
||||
# tool_result blocks for code_execution called tools
|
||||
if block_type == "tool_result":
|
||||
tool_use_id = block.get("tool_use_id")
|
||||
if tool_use_id and tool_use_id in code_execution_tool_ids:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class AnthropicContextOverflowError(anthropic.BadRequestError, ContextOverflowError):
|
||||
"""BadRequestError raised when input exceeds Anthropic's context limit."""
|
||||
|
||||
@@ -1127,44 +1068,6 @@ class ChatAnthropic(BaseChatModel):
|
||||
|
||||
system, formatted_messages = _format_messages(messages)
|
||||
|
||||
# If cache_control is provided in kwargs, add it to the last eligible message
|
||||
# block (Anthropic requires cache_control to be nested within a message block).
|
||||
# Skip blocks related to code_execution as they cannot have cache_control.
|
||||
cache_control = kwargs.pop("cache_control", None)
|
||||
if cache_control and formatted_messages:
|
||||
# Collect tool IDs called by code_execution
|
||||
code_execution_tool_ids = _collect_code_execution_tool_ids(
|
||||
formatted_messages
|
||||
)
|
||||
|
||||
cache_applied = False
|
||||
for formatted_message in reversed(formatted_messages):
|
||||
if cache_applied:
|
||||
break
|
||||
content = formatted_message.get("content")
|
||||
if isinstance(content, list) and content:
|
||||
# Find last eligible block (not code_execution related)
|
||||
for block in reversed(content):
|
||||
if isinstance(block, dict):
|
||||
if _is_code_execution_related_block(
|
||||
block, code_execution_tool_ids
|
||||
):
|
||||
continue
|
||||
block["cache_control"] = cache_control
|
||||
cache_applied = True
|
||||
break
|
||||
elif isinstance(content, str):
|
||||
formatted_message["content"] = [
|
||||
{
|
||||
"type": "text",
|
||||
"text": content,
|
||||
"cache_control": cache_control,
|
||||
}
|
||||
]
|
||||
cache_applied = True
|
||||
# If we didn't find an eligible block we silently drop the control.
|
||||
# Anthropic would reject a payload with cache_control on
|
||||
# code_execution blocks.
|
||||
payload = {
|
||||
"model": self.model,
|
||||
"max_tokens": self.max_tokens,
|
||||
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
version = "1.3.5"
|
||||
requires-python = ">=3.10.0,<4.0.0"
|
||||
dependencies = [
|
||||
"anthropic>=0.78.0,<1.0.0",
|
||||
"anthropic>=0.85.0,<1.0.0",
|
||||
"langchain-core>=1.2.19,<2.0.0",
|
||||
"pydantic>=2.7.4,<3.0.0",
|
||||
]
|
||||
|
||||
@@ -15,11 +15,7 @@ from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
|
||||
from langchain_core.outputs import ChatGeneration, ChatResult
|
||||
from langgraph.runtime import Runtime
|
||||
|
||||
from langchain_anthropic.chat_models import (
|
||||
ChatAnthropic,
|
||||
_collect_code_execution_tool_ids,
|
||||
_is_code_execution_related_block,
|
||||
)
|
||||
from langchain_anthropic.chat_models import ChatAnthropic
|
||||
from langchain_anthropic.middleware import AnthropicPromptCachingMiddleware
|
||||
|
||||
|
||||
@@ -338,204 +334,3 @@ async def test_anthropic_prompt_caching_middleware_async_default_values() -> Non
|
||||
assert modified_request.model_settings == {
|
||||
"cache_control": {"type": "ephemeral", "ttl": "5m"}
|
||||
}
|
||||
|
||||
|
||||
class TestCollectCodeExecutionToolIds:
|
||||
"""Tests for _collect_code_execution_tool_ids function."""
|
||||
|
||||
def test_empty_messages(self) -> None:
|
||||
"""Test with empty messages list."""
|
||||
result = _collect_code_execution_tool_ids([])
|
||||
assert result == set()
|
||||
|
||||
def test_no_code_execution_calls(self) -> None:
|
||||
"""Test messages without any code_execution calls."""
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [{"type": "text", "text": "Hello"}],
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "tool_use",
|
||||
"id": "toolu_regular",
|
||||
"name": "get_weather",
|
||||
"input": {"location": "NYC"},
|
||||
}
|
||||
],
|
||||
},
|
||||
]
|
||||
result = _collect_code_execution_tool_ids(messages)
|
||||
assert result == set()
|
||||
|
||||
def test_single_code_execution_call(self) -> None:
|
||||
"""Test with a single code_execution tool call."""
|
||||
messages = [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "tool_use",
|
||||
"id": "toolu_code_exec_1",
|
||||
"name": "get_weather",
|
||||
"input": {"location": "NYC"},
|
||||
"caller": {
|
||||
"type": "code_execution_20250825",
|
||||
"tool_id": "srvtoolu_abc123",
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
]
|
||||
result = _collect_code_execution_tool_ids(messages)
|
||||
assert result == {"toolu_code_exec_1"}
|
||||
|
||||
def test_multiple_code_execution_calls(self) -> None:
|
||||
"""Test with multiple code_execution tool calls."""
|
||||
messages = [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "tool_use",
|
||||
"id": "toolu_regular",
|
||||
"name": "search",
|
||||
"input": {"query": "test"},
|
||||
},
|
||||
{
|
||||
"type": "tool_use",
|
||||
"id": "toolu_code_exec_1",
|
||||
"name": "get_weather",
|
||||
"input": {"location": "NYC"},
|
||||
"caller": {
|
||||
"type": "code_execution_20250825",
|
||||
"tool_id": "srvtoolu_abc",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "tool_use",
|
||||
"id": "toolu_code_exec_2",
|
||||
"name": "get_weather",
|
||||
"input": {"location": "SF"},
|
||||
"caller": {
|
||||
"type": "code_execution_20250825",
|
||||
"tool_id": "srvtoolu_def",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
result = _collect_code_execution_tool_ids(messages)
|
||||
assert result == {"toolu_code_exec_1", "toolu_code_exec_2"}
|
||||
assert "toolu_regular" not in result
|
||||
|
||||
def test_future_code_execution_version(self) -> None:
|
||||
"""Test with a hypothetical future code_execution version."""
|
||||
messages = [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "tool_use",
|
||||
"id": "toolu_future",
|
||||
"name": "get_weather",
|
||||
"input": {},
|
||||
"caller": {
|
||||
"type": "code_execution_20260101",
|
||||
"tool_id": "srvtoolu_future",
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
]
|
||||
result = _collect_code_execution_tool_ids(messages)
|
||||
assert result == {"toolu_future"}
|
||||
|
||||
def test_ignores_user_messages(self) -> None:
|
||||
"""Test that user messages are ignored."""
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "tool_result",
|
||||
"tool_use_id": "toolu_123",
|
||||
"content": "result",
|
||||
}
|
||||
],
|
||||
},
|
||||
]
|
||||
result = _collect_code_execution_tool_ids(messages)
|
||||
assert result == set()
|
||||
|
||||
def test_handles_string_content(self) -> None:
|
||||
"""Test that string content is handled gracefully."""
|
||||
messages = [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Just a text response",
|
||||
},
|
||||
]
|
||||
result = _collect_code_execution_tool_ids(messages)
|
||||
assert result == set()
|
||||
|
||||
|
||||
class TestIsCodeExecutionRelatedBlock:
|
||||
"""Tests for _is_code_execution_related_block function."""
|
||||
|
||||
def test_regular_tool_use_block(self) -> None:
|
||||
"""Test regular tool_use block without caller."""
|
||||
block = {
|
||||
"type": "tool_use",
|
||||
"id": "toolu_regular",
|
||||
"name": "get_weather",
|
||||
"input": {"location": "NYC"},
|
||||
}
|
||||
assert not _is_code_execution_related_block(block, set())
|
||||
|
||||
def test_code_execution_tool_use_block(self) -> None:
|
||||
"""Test tool_use block called by code_execution."""
|
||||
block = {
|
||||
"type": "tool_use",
|
||||
"id": "toolu_code_exec",
|
||||
"name": "get_weather",
|
||||
"input": {"location": "NYC"},
|
||||
"caller": {
|
||||
"type": "code_execution_20250825",
|
||||
"tool_id": "srvtoolu_abc",
|
||||
},
|
||||
}
|
||||
assert _is_code_execution_related_block(block, set())
|
||||
|
||||
def test_regular_tool_result_block(self) -> None:
|
||||
"""Test tool_result block for regular tool."""
|
||||
block = {
|
||||
"type": "tool_result",
|
||||
"tool_use_id": "toolu_regular",
|
||||
"content": "Sunny, 72°F",
|
||||
}
|
||||
code_exec_ids = {"toolu_code_exec"}
|
||||
assert not _is_code_execution_related_block(block, code_exec_ids)
|
||||
|
||||
def test_code_execution_tool_result_block(self) -> None:
|
||||
"""Test tool_result block for code_execution called tool."""
|
||||
block = {
|
||||
"type": "tool_result",
|
||||
"tool_use_id": "toolu_code_exec",
|
||||
"content": "Sunny, 72°F",
|
||||
}
|
||||
code_exec_ids = {"toolu_code_exec"}
|
||||
assert _is_code_execution_related_block(block, code_exec_ids)
|
||||
|
||||
def test_text_block(self) -> None:
|
||||
"""Test that text blocks are not flagged."""
|
||||
block = {"type": "text", "text": "Hello world"}
|
||||
assert not _is_code_execution_related_block(block, set())
|
||||
|
||||
def test_non_dict_block(self) -> None:
|
||||
"""Test that non-dict values return False."""
|
||||
assert not _is_code_execution_related_block("string", set()) # type: ignore[arg-type]
|
||||
assert not _is_code_execution_related_block(None, set()) # type: ignore[arg-type]
|
||||
assert not _is_code_execution_related_block(123, set()) # type: ignore[arg-type]
|
||||
|
||||
@@ -1711,65 +1711,16 @@ def test_cache_control_kwarg() -> None:
|
||||
|
||||
messages = [HumanMessage("foo"), AIMessage("bar"), HumanMessage("baz")]
|
||||
payload = llm._get_request_payload(messages)
|
||||
assert "cache_control" not in payload
|
||||
|
||||
payload = llm._get_request_payload(messages, cache_control={"type": "ephemeral"})
|
||||
assert payload["cache_control"] == {"type": "ephemeral"}
|
||||
assert payload["messages"] == [
|
||||
{"role": "user", "content": "foo"},
|
||||
{"role": "assistant", "content": "bar"},
|
||||
{"role": "user", "content": "baz"},
|
||||
]
|
||||
|
||||
payload = llm._get_request_payload(messages, cache_control={"type": "ephemeral"})
|
||||
assert payload["messages"] == [
|
||||
{"role": "user", "content": "foo"},
|
||||
{"role": "assistant", "content": "bar"},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "baz", "cache_control": {"type": "ephemeral"}}
|
||||
],
|
||||
},
|
||||
]
|
||||
assert isinstance(messages[-1].content, str) # test no mutation
|
||||
|
||||
messages = [
|
||||
HumanMessage("foo"),
|
||||
AIMessage("bar"),
|
||||
HumanMessage(
|
||||
content=[
|
||||
{"type": "text", "text": "baz"},
|
||||
{"type": "text", "text": "qux"},
|
||||
]
|
||||
),
|
||||
]
|
||||
payload = llm._get_request_payload(messages, cache_control={"type": "ephemeral"})
|
||||
assert payload["messages"] == [
|
||||
{"role": "user", "content": "foo"},
|
||||
{"role": "assistant", "content": "bar"},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "baz"},
|
||||
{"type": "text", "text": "qux", "cache_control": {"type": "ephemeral"}},
|
||||
],
|
||||
},
|
||||
]
|
||||
assert "cache_control" not in messages[-1].content[-1] # test no mutation
|
||||
|
||||
|
||||
def test_cache_control_kwarg_skips_empty_messages() -> None:
|
||||
llm = ChatAnthropic(model=MODEL_NAME)
|
||||
|
||||
messages = [HumanMessage("foo"), AIMessage(content=[])]
|
||||
payload = llm._get_request_payload(messages, cache_control={"type": "ephemeral"})
|
||||
assert payload["messages"] == [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}}
|
||||
],
|
||||
},
|
||||
{"role": "assistant", "content": []},
|
||||
]
|
||||
|
||||
|
||||
def test_context_management_in_payload() -> None:
|
||||
llm = ChatAnthropic(
|
||||
|
||||
10
libs/partners/anthropic/uv.lock
generated
10
libs/partners/anthropic/uv.lock
generated
@@ -1,5 +1,5 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
revision = 2
|
||||
requires-python = ">=3.10.0, <4.0.0"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.13' and platform_python_implementation == 'PyPy'",
|
||||
@@ -24,7 +24,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "anthropic"
|
||||
version = "0.78.0"
|
||||
version = "0.85.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
@@ -36,9 +36,9 @@ dependencies = [
|
||||
{ name = "sniffio" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ec/51/32849a48f9b1cfe80a508fd269b20bd8f0b1357c70ba092890fde5a6a10b/anthropic-0.78.0.tar.gz", hash = "sha256:55fd978ab9b049c61857463f4c4e9e092b24f892519c6d8078cee1713d8af06e", size = 509136, upload-time = "2026-02-05T17:52:04.986Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c5/08/c620a0eb8625539a8ea9f5a6e06f13d131be0bc8b5b714c235d4b25dd1b5/anthropic-0.85.0.tar.gz", hash = "sha256:d45b2f38a1efb1a5d15515a426b272179a0d18783efa2bb4c3925fa773eb50b9", size = 542034, upload-time = "2026-03-16T17:00:44.324Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/03/2f50931a942e5e13f80e24d83406714672c57964be593fc046d81369335b/anthropic-0.78.0-py3-none-any.whl", hash = "sha256:2a9887d2e99d1b0f9fe08857a1e9fe5d2d4030455dbf9ac65aab052e2efaeac4", size = 405485, upload-time = "2026-02-05T17:52:03.674Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/5a/9d85b85686d5cdd79f5488c8667e668d7920d06a0a1a1beb454a5b77b2db/anthropic-0.85.0-py3-none-any.whl", hash = "sha256:b4f54d632877ed7b7b29c6d9ba7299d5e21c4c92ae8de38947e9d862bff74adf", size = 458237, upload-time = "2026-03-16T17:00:45.877Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -607,7 +607,7 @@ typing = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "anthropic", specifier = ">=0.78.0,<1.0.0" },
|
||||
{ name = "anthropic", specifier = ">=0.85.0,<1.0.0" },
|
||||
{ name = "langchain-core", editable = "../../core" },
|
||||
{ name = "pydantic", specifier = ">=2.7.4,<3.0.0" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user