From 7e61dbb1df1ade78ad5ee9f84a495eed0a5cc270 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Thu, 14 May 2026 00:47:06 +0530 Subject: [PATCH] fix(responses): preserve cache_control in Responses API -> Chat Completion transformation (#27727) * fix(responses): preserve cache_control in Responses API -> Chat Completion transformation cache_control injected by AnthropicCacheControlHook was silently dropped when _transform_responses_api_content_to_chat_completion_content rebuilt content blocks with only {type, text}. Now copies cache_control through so Anthropic prompt caching works correctly when using client.responses.create with cache_control_injection_points. Co-authored-by: Cursor * fix(responses): preserve cache_control for input_image and input_file blocks Extends the cache_control fix to image and file content blocks, which were also silently dropping cache_control during the Responses API -> Chat Completion transformation. Adds tests for all three content block types. Co-authored-by: Cursor --------- Co-authored-by: Cursor Co-authored-by: Claude Babysitter --- .../transformation.py | 30 ++++--- .../test_litellm_completion_responses.py | 83 +++++++++++++++++++ 2 files changed, 100 insertions(+), 13 deletions(-) diff --git a/litellm/responses/litellm_completion_transformation/transformation.py b/litellm/responses/litellm_completion_transformation/transformation.py index 48b12a5fba..e2ba835359 100644 --- a/litellm/responses/litellm_completion_transformation/transformation.py +++ b/litellm/responses/litellm_completion_transformation/transformation.py @@ -1238,6 +1238,8 @@ class LiteLLMCompletionResponsesConfig: file_dict["file_data"] = item["file_data"] new_item: Dict[str, Any] = {"type": "file", "file": file_dict} + if "cache_control" in item: + new_item["cache_control"] = item["cache_control"] return new_item @staticmethod @@ -1282,26 +1284,28 @@ class LiteLLMCompletionResponsesConfig: ) ) elif item.get("type") == "input_image": - content_list.append( - dict( - LiteLLMCompletionResponsesConfig._transform_input_image_item_to_image_item( - item - ) + image_block = dict( + LiteLLMCompletionResponsesConfig._transform_input_image_item_to_image_item( + item ) ) + if "cache_control" in item: + image_block["cache_control"] = item["cache_control"] + content_list.append(image_block) else: # Skip text blocks with None text to avoid downstream errors text_value = item.get("text") if text_value is None: continue - content_list.append( - { - "type": LiteLLMCompletionResponsesConfig._get_chat_completion_request_content_type( - item.get("type") or "text" - ), - "text": text_value, - } - ) + content_block: Dict[str, Any] = { + "type": LiteLLMCompletionResponsesConfig._get_chat_completion_request_content_type( + item.get("type") or "text" + ), + "text": text_value, + } + if "cache_control" in item: + content_block["cache_control"] = item["cache_control"] + content_list.append(content_block) return content_list else: raise ValueError(f"Invalid content type: {type(content)}") diff --git a/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py b/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py index 5d8ff8022e..503a610e01 100644 --- a/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py +++ b/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py @@ -2170,3 +2170,86 @@ class TestEnsureOutputItemContentPartAdded: events = iterator._pending_response_events assert len(events) == 2 + + +class TestCacheControlPreservation: + def test_cache_control_preserved_in_content_transformation(self): + """cache_control injected by AnthropicCacheControlHook must survive + the Responses API -> Chat Completion content transformation.""" + content = [ + { + "type": "text", + "text": "hello", + "cache_control": {"type": "ephemeral"}, + } + ] + result = LiteLLMCompletionResponsesConfig._transform_responses_api_content_to_chat_completion_content( + content + ) + assert isinstance(result, list) + assert len(result) == 1 + assert result[0]["cache_control"] == {"type": "ephemeral"} + + def test_content_without_cache_control_unaffected(self): + """Content blocks that don't have cache_control should be unaffected.""" + content = [{"type": "text", "text": "hello"}] + result = LiteLLMCompletionResponsesConfig._transform_responses_api_content_to_chat_completion_content( + content + ) + assert isinstance(result, list) + assert len(result) == 1 + assert "cache_control" not in result[0] + + def test_cache_control_preserved_in_input_item_transformation(self): + """cache_control survives the full input-item -> messages transformation.""" + input_item = { + "role": "user", + "content": [ + { + "type": "text", + "text": "long context", + "cache_control": {"type": "ephemeral"}, + } + ], + } + messages = LiteLLMCompletionResponsesConfig._transform_responses_api_input_item_to_chat_completion_message( + input_item + ) + assert len(messages) == 1 + msg_content = ( + messages[0].get("content") + if isinstance(messages[0], dict) + else getattr(messages[0], "content", None) + ) + assert isinstance(msg_content, list) + assert msg_content[0]["cache_control"] == {"type": "ephemeral"} + + def test_cache_control_preserved_for_input_file_block(self): + content = [ + { + "type": "input_file", + "file_id": "file-abc123", + "cache_control": {"type": "ephemeral"}, + } + ] + result = LiteLLMCompletionResponsesConfig._transform_responses_api_content_to_chat_completion_content( + content + ) + assert isinstance(result, list) + assert len(result) == 1 + assert result[0]["cache_control"] == {"type": "ephemeral"} + + def test_cache_control_preserved_for_input_image_block(self): + content = [ + { + "type": "input_image", + "image_url": "https://example.com/img.png", + "cache_control": {"type": "ephemeral"}, + } + ] + result = LiteLLMCompletionResponsesConfig._transform_responses_api_content_to_chat_completion_content( + content + ) + assert isinstance(result, list) + assert len(result) == 1 + assert result[0]["cache_control"] == {"type": "ephemeral"}