From 466022fd848247ada2fe1d74443760ec1e1b9a65 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Mon, 28 Sep 2026 17:09:06 -0700 Subject: [PATCH] fix(caching): skip injection on messages whose tool calls carry marks Reverts 9f08d8aef8. A default 5m mark injected on assistant text lands before the client's 1h tool_use mark, which Anthropic rejects with a 400 because a 1h breakpoint must not follow a 5m one. Keeping the full census in the skip check leaves the client's tool_call breakpoint as the only one on that message. --- .../anthropic_cache_control_hook.py | 12 +++----- .../test_anthropic_cache_control_hook.py | 28 ++++++++++--------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/litellm/integrations/anthropic_cache_control_hook.py b/litellm/integrations/anthropic_cache_control_hook.py index d7b5619c2d9..0b1343d196b 100644 --- a/litellm/integrations/anthropic_cache_control_hook.py +++ b/litellm/integrations/anthropic_cache_control_hook.py @@ -518,26 +518,22 @@ class AnthropicCacheControlHook(CustomPromptManagement): return [] @staticmethod - def _count_message_and_content_breakpoints(message: object) -> int: + def _count_cache_control_blocks(message: object) -> int: message_count: Final = 1 if _carries_cache_breakpoint(message) else 0 content: Final = _as_object_list(_attribute_or_key(message, "content")) content_count: Final = sum(1 for block in content if _carries_cache_breakpoint(block)) if content else 0 - return message_count + content_count - - @staticmethod - def _count_cache_control_blocks(message: object) -> int: tool_calls: Final = _as_object_list(_attribute_or_key(message, "tool_calls")) tool_call_count: Final = ( sum(1 for tool_call in tool_calls if _tool_call_cache_control_is_forwarded(tool_call, message)) if tool_calls else 0 ) - return AnthropicCacheControlHook._count_message_and_content_breakpoints(message) + tool_call_count + return message_count + content_count + tool_call_count @staticmethod def _message_has_cache_control(message: AllMessageValues) -> bool: - """Return True if message-level or content-block cache_control is present.""" - return AnthropicCacheControlHook._count_message_and_content_breakpoints(message) > 0 + """Return True if the message already carries any cache_control.""" + return AnthropicCacheControlHook._count_cache_control_blocks(message) > 0 @staticmethod def _safe_insert_cache_control_in_message( diff --git a/tests/unit/integrations/test_anthropic_cache_control_hook.py b/tests/unit/integrations/test_anthropic_cache_control_hook.py index 181fb810470..5e29ec7d700 100644 --- a/tests/unit/integrations/test_anthropic_cache_control_hook.py +++ b/tests/unit/integrations/test_anthropic_cache_control_hook.py @@ -1202,40 +1202,42 @@ def test_cache_control_hook_counts_pydantic_message_tool_call_marks(): assert AnthropicCacheControlHook.count_request_cache_breakpoints(cast(list[AllMessageValues], [message])) == 1 -def test_injection_adds_message_mark_without_overwriting_tool_call_ttl(): +def test_injection_skips_assistant_whose_tool_call_carries_a_longer_ttl_mark(): hook: Final = AnthropicCacheControlHook() tool_call_ttl: Final = {"type": "ephemeral", "ttl": "1h"} - messages: Final = [ + messages: Final[list[AllMessageValues]] = [ + {"role": "user", "content": "What is the weather in Paris?"}, { "role": "assistant", - "content": "ok", + "content": "Let me look that up.", "tool_calls": [ { - "id": "call_1", + "id": "toolu_01A", "type": "function", - "function": {"name": "lookup", "arguments": "{}"}, + "function": {"name": "get_weather", "arguments": '{"city": "Paris"}'}, "cache_control": tool_call_ttl, } ], - } + }, + {"role": "tool", "tool_call_id": "toolu_01A", "content": "18C and sunny"}, ] _, processed, _ = hook.get_chat_completion_prompt( - model="bedrock/us.anthropic.claude-opus-4-6-v1:0", + model="anthropic/claude-haiku-4-5", messages=messages, - non_default_params={"cache_control_injection_points": [{"location": "message", "index": -1}]}, + non_default_params={"cache_control_injection_points": [{"location": "message", "role": "assistant"}]}, prompt_id=None, prompt_variables=None, dynamic_callback_params={}, ) - assistant_message: Final = processed[0] + assistant_message: Final = processed[1] assistant_tool_calls: Final = assistant_message.get("tool_calls") - assert assistant_message.get("cache_control") == {"type": "ephemeral"} + assert assistant_message.get("cache_control") is None + assert assistant_message.get("content") == "Let me look that up." assert isinstance(assistant_tool_calls, list) - tool_call: Final = assistant_tool_calls[0] - assert tool_call.get("cache_control") == tool_call_ttl - assert AnthropicCacheControlHook.count_request_cache_breakpoints(processed) == 2 + assert assistant_tool_calls[0].get("cache_control") == tool_call_ttl + assert AnthropicCacheControlHook.count_request_cache_breakpoints(processed) == 1 def test_cache_control_hook_caps_at_four_blocks_with_client_cache_control():