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:
parent
18f77ff7bc
commit
7e61dbb1df
@ -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)}")
|
||||
|
||||
@ -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"}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user