fix(caching): keep chat completions on the envelope-merged mark count for the default stand-down

Chat completions merge extra_body over the request, so a direct tool mark that extra_body replaces never reaches the provider there. Only /v1/messages, where the native transforms drop the envelope, needs to count marks on both sides.
This commit is contained in:
Yuneng Jiang 2026-09-26 14:26:16 -07:00
parent 01ef7c4725
commit a9c5fa760e
No known key found for this signature in database
2 changed files with 24 additions and 5 deletions

View file

@ -695,6 +695,7 @@ class AnthropicCacheControlHook(CustomPromptManagement):
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.
@ -704,12 +705,14 @@ class AnthropicCacheControlHook(CustomPromptManagement):
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_on_messages_route(
external_breakpoints: Final = (
AnthropicCacheControlHook.count_external_cache_breakpoints_on_messages_route(
tools, cache_control, request_kwargs
)
) > 0
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(
@ -721,6 +724,7 @@ class AnthropicCacheControlHook(CustomPromptManagement):
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.
@ -741,7 +745,9 @@ class AnthropicCacheControlHook(CustomPromptManagement):
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(
@ -970,6 +976,7 @@ class AnthropicCacheControlHook(CustomPromptManagement):
enable_prompt_caching=enable_prompt_caching,
cache_control=cache_control,
request_kwargs=kwargs,
on_messages_route=True,
)
if model is not None
else ()

View file

@ -2657,6 +2657,18 @@ class TestConfiguredInjectionPointsSurviveClientMarks:
)
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")],