mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-13 08:52:02 +00:00
fix(responses-bridge): keep a system message carrying a prompt cache breakpoint an input item
Since this branch was cut, main carries prompt_cache_breakpoint markers through the bridge on input items' content blocks (#42281, #44119, #45500), and its tests expect a leading system message whose block carries one to stay an input item so the marker survives. Folding that message into `instructions` dropped the marker and broke eight of those tests. The fold now skips a leading system message any of whose blocks carries a prompt_cache_breakpoint, alongside the non-text case it already skipped. A block with only cache_control, the shape #42171 reports, still folds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
921bb461de
commit
cc8e6c7a45
2 changed files with 52 additions and 18 deletions
|
|
@ -129,6 +129,35 @@ def _breakpoint_of(part: object) -> object:
|
|||
return content_part.get("prompt_cache_breakpoint")
|
||||
|
||||
|
||||
def _foldable_system_text(block: object) -> str | None:
|
||||
if isinstance(block, str):
|
||||
return block
|
||||
if not isinstance(block, dict):
|
||||
return None
|
||||
text_block: Final = cast(dict[str, object], block) # cast-ok: isinstance confirms a content block mapping
|
||||
text: Final = text_block.get("text")
|
||||
if text_block.get("type") != "text" or not isinstance(text, str) or _breakpoint_of(text_block) is not None:
|
||||
return None
|
||||
return text
|
||||
|
||||
|
||||
def _system_text_for_instructions(message: Mapping[str, object]) -> str | None:
|
||||
"""A leading system message's text for ``instructions``, or None to keep it a positioned input item.
|
||||
|
||||
List content folds when every block is plain text, the shape a client attaching cache_control sends.
|
||||
A non-text block (an image, say) would be lost in a string, and a prompt_cache_breakpoint only rides
|
||||
on an input item's content block, so either keeps the message an input item.
|
||||
"""
|
||||
content: Final = message.get("content", "")
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
if not isinstance(content, list):
|
||||
return None
|
||||
blocks: Final = cast(list[object], content) # cast-ok: isinstance confirms a list of content blocks
|
||||
texts: Final = tuple(text for text in map(_foldable_system_text, blocks) if text is not None)
|
||||
return " ".join(texts) if len(texts) == len(blocks) else None
|
||||
|
||||
|
||||
def _pending_audio_breakpoint(pending: object, part: object) -> object:
|
||||
if not _is_audio_input_part(part):
|
||||
return None
|
||||
|
|
@ -536,24 +565,9 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
tool_call_id = msg.get("tool_call_id")
|
||||
|
||||
if role == "system":
|
||||
# Extract system message as instructions, but only within the leading run of
|
||||
# system messages: a later one stays a positioned input item so its bytes don't
|
||||
# unsettle the prompt-cache-stable prefix (#40269).
|
||||
extracted_instructions: str | None = None
|
||||
if index < leading_system_count:
|
||||
if isinstance(content, str):
|
||||
extracted_instructions = content
|
||||
elif isinstance(content, list) and all(
|
||||
isinstance(block, str) or (isinstance(block, dict) and block.get("type") == "text")
|
||||
for block in content
|
||||
):
|
||||
# Every block is plain text (a bare string or a `{"type": "text", ...}`
|
||||
# dict), the same shape a client attaching cache_control sends; a
|
||||
# non-text block (an image, say) fails the `all()` above and falls
|
||||
# through to the input-item branch below instead of losing it silently.
|
||||
extracted_instructions = " ".join(
|
||||
block if isinstance(block, str) else block.get("text", "") for block in content
|
||||
)
|
||||
# Only the leading run of system messages folds into instructions: a later one stays
|
||||
# a positioned input item so its bytes don't unsettle the cached prefix (#40269).
|
||||
extracted_instructions = _system_text_for_instructions(msg) if index < leading_system_count else None
|
||||
if extracted_instructions is not None:
|
||||
if instructions:
|
||||
# Concatenate multiple system prompts with a space
|
||||
|
|
|
|||
|
|
@ -4887,6 +4887,26 @@ def test_leading_system_list_with_a_non_text_block_stays_an_input_item():
|
|||
assert input_items[0]["content"][1]["type"] == "input_image"
|
||||
|
||||
|
||||
def test_leading_system_list_with_a_prompt_cache_breakpoint_stays_an_input_item():
|
||||
"""A prompt_cache_breakpoint rides only on an input item's content block, so a leading
|
||||
system message carrying one stays an input item and keeps its marker rather than folding."""
|
||||
handler: Final = LiteLLMResponsesTransformationHandler()
|
||||
|
||||
input_items, instructions = handler.convert_chat_completion_messages_to_responses_api(
|
||||
[
|
||||
{"role": "system", "content": [_MARKED_SYSTEM_PART]},
|
||||
{"role": "user", "content": "hi"},
|
||||
],
|
||||
keep_prompt_cache_breakpoints=True,
|
||||
)
|
||||
|
||||
assert instructions is None
|
||||
assert input_items == [
|
||||
{"type": "message", "role": "system", "content": [{**_MARKED_SYSTEM_PART, "type": "input_text"}]},
|
||||
{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "hi"}]},
|
||||
]
|
||||
|
||||
|
||||
def test_map_optional_params_verbosity_merges_into_text():
|
||||
"""Chat verbosity must land on Responses text.verbosity alongside text.format regardless of key order."""
|
||||
from litellm.completion_extras.litellm_responses_transformation.transformation import (
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue