mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-11 03:38:38 +00:00
fix(responses): read prompt_cache_breakpoint without validating content blocks
The marker read ran every chat content block through TypeAdapter(dict[str, object]).validate_python, which rejects dict blocks with non-string keys that chat completion callers passing Python dicts could previously send; the request then failed with a pydantic ValidationError on the bridge keep path, the strip path, and the image/file conversions alike. item is already isinstance-narrowed to a dict at every read site, so read the marker with dict.get directly and drop the adapter. Adds a regression test covering text/image_url/file blocks carrying non-string keys on both the keep (gpt-5.6) and strip (gpt-4o) paths.
This commit is contained in:
parent
ccf03ee7df
commit
77e21ae37a
2 changed files with 42 additions and 5 deletions
|
|
@ -19,7 +19,7 @@ from openai.types.responses.response_input_param import (
|
|||
from openai.types.responses.tool_choice_custom_param import ToolChoiceCustomParam
|
||||
from openai.types.responses.tool_choice_function_param import ToolChoiceFunctionParam
|
||||
from openai.types.responses.tool_param import FunctionToolParam
|
||||
from pydantic import BaseModel, TypeAdapter
|
||||
from pydantic import BaseModel
|
||||
|
||||
import litellm
|
||||
from litellm import ModelResponse
|
||||
|
|
@ -78,7 +78,6 @@ _CHAT_COMPLETION_FIELDS: Final = frozenset((*ModelResponse.model_fields, "usage"
|
|||
_RESPONSES_API_ONLY_FIELDS: Final = frozenset((*Response.model_fields, *ResponsesAPIResponse.model_fields)) - frozenset(
|
||||
ChatCompletion.model_fields
|
||||
)
|
||||
_CHAT_CONTENT_ITEM: Final = TypeAdapter(dict[str, object])
|
||||
|
||||
|
||||
def _strip_prompt_cache_breakpoint_from_content_block(value: object) -> object:
|
||||
|
|
@ -1118,7 +1117,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
if original_type == "text":
|
||||
converted = with_prompt_cache_breakpoint(
|
||||
self._convert_content_str_to_input_text(item.get("text", ""), role),
|
||||
_CHAT_CONTENT_ITEM.validate_python(item).get("prompt_cache_breakpoint"),
|
||||
item.get("prompt_cache_breakpoint"),
|
||||
)
|
||||
result.append(converted)
|
||||
verbose_logger.debug("Chat provider: text -> %s", converted)
|
||||
|
|
@ -1131,7 +1130,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
role,
|
||||
)
|
||||
),
|
||||
_CHAT_CONTENT_ITEM.validate_python(item).get("prompt_cache_breakpoint"),
|
||||
item.get("prompt_cache_breakpoint"),
|
||||
)
|
||||
result.append(converted)
|
||||
verbose_logger.debug("Chat provider: image_url -> %s", converted)
|
||||
|
|
@ -1147,7 +1146,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
_input_file_from_file_value(
|
||||
cast("ChatCompletionFileObject", item).get("file"), # cast-ok: type tag checked
|
||||
),
|
||||
_CHAT_CONTENT_ITEM.validate_python(item).get("prompt_cache_breakpoint"),
|
||||
item.get("prompt_cache_breakpoint"),
|
||||
)
|
||||
result.append(converted)
|
||||
verbose_logger.debug("Chat provider: file -> %s", converted)
|
||||
|
|
|
|||
|
|
@ -4233,6 +4233,44 @@ def test_prompt_cache_breakpoint_survives_chat_to_responses_conversion(
|
|||
assert request["prompt_cache_options"] == cache_breakpoint
|
||||
|
||||
|
||||
def test_prompt_cache_breakpoint_read_tolerates_non_string_content_block_keys() -> None:
|
||||
handler: Final = LiteLLMResponsesTransformationHandler()
|
||||
# Non-string keys are not JSON-representable but are accepted by chat completion
|
||||
# callers passing Python dicts; reading the marker must not validate or reject them.
|
||||
content: Final = [
|
||||
{"type": "text", "text": "Stable prefix", 1: "ignored"},
|
||||
{"type": "image_url", "image_url": "https://example.com/image.png", 2: "ignored"},
|
||||
{"type": "file", "file": {"file_id": "file-123"}, 3: "ignored"},
|
||||
]
|
||||
messages: Final = [{"role": "user", "content": content}]
|
||||
|
||||
for model in ("gpt-5.6", "gpt-4o"): # marker keep path and strip path both read the block
|
||||
request: dict[str, object] = handler.transform_request(
|
||||
model=model,
|
||||
messages=messages,
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
litellm_logging_obj=Mock(),
|
||||
)
|
||||
|
||||
assert request["input"] == [
|
||||
{
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "input_text", "text": "Stable prefix"},
|
||||
{
|
||||
"type": "input_image",
|
||||
"image_url": "https://example.com/image.png",
|
||||
"detail": "auto",
|
||||
},
|
||||
{"type": "input_file", "file_id": "file-123"},
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_prompt_cache_breakpoints_are_dropped_for_unsupported_models() -> None:
|
||||
handler: Final = LiteLLMResponsesTransformationHandler()
|
||||
cache_breakpoint: Final = {"mode": "explicit"}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue