fix(responses): guard against None cache_control on content blocks

This commit is contained in:
Elliott de Launay 2026-07-22 16:47:03 -04:00
parent 4b2a436317
commit 4a307e6759
No known key found for this signature in database
GPG key ID: BB899BED766D1806
2 changed files with 40 additions and 6 deletions

View file

@ -1158,7 +1158,7 @@ class LiteLLMCompletionResponsesConfig:
file_dict["file_data"] = item["file_data"]
new_item: dict[str, Any] = {"type": "file", "file": file_dict}
if "cache_control" in item:
if item.get("cache_control") is not None:
new_item["cache_control"] = item["cache_control"]
return new_item
@ -1180,16 +1180,15 @@ class LiteLLMCompletionResponsesConfig:
"""
Normalize a Responses API object (Pydantic model or custom class) to a dictionary
"""
if hasattr(item, "model_dump"):
if hasattr(item, "model_dump"):
try:
return item.model_dump(exclude_none=True)
except Exception: # noqa: BLE001 # fallback if custom model_dump does not accept exclude_none
except TypeError:
return item.model_dump()
elif hasattr(item, "dict"):
try:
return item.dict(exclude_none=True)
except Exception: # noqa: BLE001 # fallback if custom dict does not accept exclude_none
except TypeError:
return item.dict()
target_attrs = (
@ -1247,7 +1246,11 @@ class LiteLLMCompletionResponsesConfig:
elif item.get("type") == "input_image":
image_block = {
**LiteLLMCompletionResponsesConfig._transform_input_image_item_to_image_item(item),
**({"cache_control": item["cache_control"]} if "cache_control" in item else {}),
**(
{"cache_control": item["cache_control"]}
if item.get("cache_control") is not None
else {}
),
}
content_list.append(image_block)
else:
@ -1260,7 +1263,11 @@ class LiteLLMCompletionResponsesConfig:
item.get("type") or "text"
),
"text": text_value,
**({"cache_control": item["cache_control"]} if "cache_control" in item else {}),
**(
{"cache_control": item["cache_control"]}
if item.get("cache_control") is not None
else {}
),
}
content_list.append(content_block)
return content_list

View file

@ -2635,6 +2635,33 @@ class TestCacheControlPreservation:
)
assert messages == [{"role": "user", "content": "hello", "cache_control": {"type": "ephemeral"}}]
def test_none_cache_control_omitted_from_content_blocks(self):
from pydantic import BaseModel
from typing import Optional
class MockContentItem(BaseModel):
type: str = "text"
text: str = "hello world"
cache_control: Optional[dict] = None
item = MockContentItem()
result = LiteLLMCompletionResponsesConfig._transform_responses_api_content_to_chat_completion_content([item])
assert isinstance(result, list)
assert len(result) == 1
assert "cache_control" not in result[0]
dict_item = {"type": "text", "text": "hello", "cache_control": None}
result_dict = LiteLLMCompletionResponsesConfig._transform_responses_api_content_to_chat_completion_content([dict_item])
assert isinstance(result_dict, list)
assert len(result_dict) == 1
assert "cache_control" not in result_dict[0]
image_item = {"type": "input_image", "image_url": "https://example.com/a.png", "cache_control": None}
result_img = LiteLLMCompletionResponsesConfig._transform_responses_api_content_to_chat_completion_content([image_item])
assert isinstance(result_img, list)
assert len(result_img) == 1
assert "cache_control" not in result_img[0]
def test_tool_call_output_as_custom_object(self):
"""Test _transform_responses_api_tool_call_output_to_chat_completion_message with a custom object."""
class MockToolCallOutput: