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 <cursoragent@cursor.com>

* 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 <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Babysitter <claude@anthropic.com>
This commit is contained in:
Sameer Kankute 2026-05-14 00:47:06 +05:30 committed by GitHub
parent 18f77ff7bc
commit 7e61dbb1df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 100 additions and 13 deletions

View File

@ -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)}")

View File

@ -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"}