Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions litellm/integrations/anthropic_cache_control_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,7 @@ def _request_has_cache_control(
tools: list | None = None,
cache_control: object = None,
request_kwargs: object = None,
on_messages_route: bool = False,
) -> bool:
"""Return True if the request already carries any client-supplied cache_control.

Expand All @@ -704,10 +705,14 @@ def _request_has_cache_control(
envelope. Configured injection points are an explicit instruction and are
applied alongside the client's marks, bounded by the provider cap.
"""
return (
AnthropicCacheControlHook.count_request_cache_breakpoints(messages, system)
+ AnthropicCacheControlHook.count_external_cache_breakpoints(tools, cache_control, request_kwargs)
) > 0
external_breakpoints: Final = (
AnthropicCacheControlHook.count_external_cache_breakpoints_on_messages_route(
tools, cache_control, request_kwargs
)
if on_messages_route
else AnthropicCacheControlHook.count_external_cache_breakpoints(tools, cache_control, request_kwargs)
)
return AnthropicCacheControlHook.count_request_cache_breakpoints(messages, system) + external_breakpoints > 0

@staticmethod
def get_default_injection_points(
Expand All @@ -719,6 +724,7 @@ def get_default_injection_points(
enable_prompt_caching: bool | None = None,
cache_control: object = None,
request_kwargs: object = None,
on_messages_route: bool = False,
) -> list[CacheControlInjectionPoint]:
"""Default breakpoints when ``litellm.enable_anthropic_prompt_caching`` is on.

Expand All @@ -739,7 +745,9 @@ def get_default_injection_points(
if not supports_anthropic_cache_control(model, custom_llm_provider):
return []

if AnthropicCacheControlHook._request_has_cache_control(messages, system, tools, cache_control, request_kwargs):
if AnthropicCacheControlHook._request_has_cache_control(
messages, system, tools, cache_control, request_kwargs, on_messages_route
):
return []

if is_claude_code_one_shot_subagent_request(
Expand Down Expand Up @@ -968,6 +976,7 @@ def maybe_inject_cache_control(
enable_prompt_caching=enable_prompt_caching,
cache_control=cache_control,
request_kwargs=kwargs,
on_messages_route=True,
)
if model is not None
else ()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2633,6 +2633,42 @@ def test_v1_messages_automatic_defaults_stand_down_for_root_cache_control(self,
assert kwargs["cache_control"] is root_cache_control
assert "litellm_gateway_injected_cache" not in kwargs["litellm_metadata"]

@pytest.mark.parametrize(
"tools,kwargs,injected",
[
([MARKED_V1_TOOL], {"extra_body": {"tools": [UNMARKED_V1_TOOL]}}, False),
(None, {"cache_control": EPHEMERAL, "extra_body": {"cache_control": None}}, False),
([UNMARKED_V1_TOOL], {"extra_body": {"tools": [UNMARKED_V1_TOOL]}}, True),
],
ids=["extra_body_unmarks_direct_tool", "extra_body_nulls_root_cache_control", "no_client_mark_anywhere"],
)
def test_v1_messages_automatic_defaults_stand_down_for_a_direct_mark_extra_body_hides(
self, monkeypatch, tools, kwargs, injected
):
monkeypatch.setattr(litellm, "enable_anthropic_prompt_caching", True)
request_kwargs = {**copy.deepcopy(kwargs), "litellm_metadata": {}}

result_messages, result_system = self._inject(
copy.deepcopy(self.V1_MESSAGES), request_kwargs, tools=copy.deepcopy(tools)
)

assert AnthropicCacheControlHook.count_request_cache_breakpoints(result_messages, result_system) == (
2 if injected else 0
)
assert ("litellm_gateway_injected_cache" in request_kwargs["litellm_metadata"]) is injected

def test_chat_automatic_defaults_apply_when_extra_body_drops_the_only_client_mark(self, monkeypatch):
monkeypatch.setattr(litellm, "enable_anthropic_prompt_caching", True)
params = {"extra_body": {"tools": [self.UNMARKED_TOOL]}}

self._seed(params, copy.deepcopy(self.CLEAN_MESSAGES), tools=[self.MARKED_TOOL_TOP_LEVEL])
affinity = AnthropicCacheControlHook.messages_with_default_injections(
copy.deepcopy(self.CLEAN_MESSAGES), ["claude-sonnet-4-5"], tools=[self.MARKED_TOOL_TOP_LEVEL], request_kwargs=params
)

assert [p["index"] for p in params["cache_control_injection_points"]] == [None, -1]
assert AnthropicCacheControlHook.count_request_cache_breakpoints(affinity) == 2

@pytest.mark.parametrize(
"marked_turns,expected_system",
[(2, [{"type": "text", "text": "sys", "cache_control": {"type": "ephemeral"}}]), (3, "sys")],
Expand Down
Loading