From 5b5898456cc1d0ef7f3984aa144ee3e2a02fa92a Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Wed, 7 Oct 2026 21:17:15 -0700 Subject: [PATCH] fix(cache_control): keep a configured point that sits beside junk entries on every chat seed path `configured_injection_points` kept the point dicts of a mixed `cache_control_injection_points` list as a tuple, but only read a `list` back. The chat seed and the `/v1/responses` dialect stamp write the normalized value back onto the request, and on a deployment the OpenAI dialect does not stamp (an Anthropic model, Claude on Bedrock Mantle) that value is the tuple itself, so the hook then read it as no configured points and the valid point was silently dropped. The stamped OpenAI dialect and the `/v1/messages` path kept it only because they build a new list. The normalizer now reads back the tuple it wrote, so the point reaches the wire on every path; an all-dict list still passes through as the same object. --- .../anthropic_cache_control_hook.py | 4 ++-- .../test_anthropic_cache_control_hook.py | 22 +++++++++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/litellm/integrations/anthropic_cache_control_hook.py b/litellm/integrations/anthropic_cache_control_hook.py index faded73d0f3..6047c625665 100644 --- a/litellm/integrations/anthropic_cache_control_hook.py +++ b/litellm/integrations/anthropic_cache_control_hook.py @@ -91,10 +91,10 @@ def _validated_object_list(value: object) -> list[object] | None: def configured_injection_points(value: object) -> Sequence[CacheControlInjectionPoint]: - if not isinstance(value, list): + if not isinstance(value, (list, tuple)): return () if all(isinstance(entry, dict) for entry in value): - return cast(list[CacheControlInjectionPoint], value) + return cast(Sequence[CacheControlInjectionPoint], value) return tuple(cast(CacheControlInjectionPoint, entry) for entry in value if isinstance(entry, dict)) diff --git a/tests/unit/integrations/test_anthropic_cache_control_hook.py b/tests/unit/integrations/test_anthropic_cache_control_hook.py index f7119346566..d96f00cc2b1 100644 --- a/tests/unit/integrations/test_anthropic_cache_control_hook.py +++ b/tests/unit/integrations/test_anthropic_cache_control_hook.py @@ -4189,6 +4189,7 @@ class TestMalformedInjectionPointsAreIgnored: Every entry point now reads it as no configured points, the way ``null`` already read.""" SHAPES = ("system", 5, {"location": "message", "role": "system"}, ["system"], None) + MIXED = ["system", {"location": "message", "role": "system"}, 3] MESSAGES = [{"role": "system", "content": "sys"}, {"role": "user", "content": "hi"}] @pytest.mark.parametrize("value", SHAPES) @@ -4196,8 +4197,25 @@ class TestMalformedInjectionPointsAreIgnored: assert configured_injection_points(value) == () def test_keeps_the_point_entries_of_a_mixed_list(self): - mixed = ["system", {"location": "message", "role": "system"}, 3] - assert configured_injection_points(mixed) == ({"location": "message", "role": "system"},) + assert configured_injection_points(self.MIXED) == ({"location": "message", "role": "system"},) + + def test_the_point_beside_junk_entries_survives_the_chat_seed_on_an_unstamped_deployment(self): + params: dict = {"cache_control_injection_points": copy.deepcopy(self.MIXED)} + AnthropicCacheControlHook.maybe_seed_default_injection_points( + non_default_params=params, + messages=copy.deepcopy(self.MESSAGES), + model="claude-sonnet-4-5", + custom_llm_provider="anthropic", + ) + _, processed, _ = AnthropicCacheControlHook().get_chat_completion_prompt( + model="claude-sonnet-4-5", + messages=copy.deepcopy(self.MESSAGES), + non_default_params=params, + prompt_id=None, + prompt_variables=None, + dynamic_callback_params={}, + ) + assert processed[0] == {"role": "system", "content": "sys", "cache_control": {"type": "ephemeral"}} @pytest.mark.parametrize("value", SHAPES) def test_chat_prompt_hook_leaves_the_request_untouched(self, value):