mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-14 09:03:16 +00:00
test(map_system_message_pt): add list-content tests + fix empty sys_text edge case
Address Greptile P1 (no test added) and P2 (empty sys_text edge case) findings on PR #26566: - Add 9 tests in tests/test_litellm/litellm_core_utils/prompt_templates/ covering list-typed content blocks (the original bug), mixed string/list combinations, multi-text-block system messages, the system-at-end case, and the consecutive-system-messages case. - Fix the empty sys_text edge case: when sys_content is a list with no text blocks (e.g. pure-image system message), sys_text is "". The original fix produced either a stray leading space (string path) or an empty {"type":"text","text":""} block (list path). Both are now guarded against with explicit emptiness checks. Refs: https://github.com/BerriAI/litellm/issues/23757 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> AI-Generated: true
This commit is contained in:
parent
9806878638
commit
089d605b3a
2 changed files with 182 additions and 4 deletions
|
|
@ -122,11 +122,17 @@ def map_system_message_pt(messages: list) -> list:
|
|||
|
||||
next_content = next_m["content"]
|
||||
if isinstance(next_content, list):
|
||||
next_m["content"] = [
|
||||
{"type": "text", "text": sys_text}
|
||||
] + next_content
|
||||
# Only prepend a text block when sys_text is non-empty;
|
||||
# otherwise we'd inject an empty {"type":"text","text":""}
|
||||
if sys_text:
|
||||
next_m["content"] = [
|
||||
{"type": "text", "text": sys_text}
|
||||
] + next_content
|
||||
else:
|
||||
next_m["content"] = sys_text + " " + next_content
|
||||
# Avoid a stray leading space when sys_text is empty
|
||||
next_m["content"] = (
|
||||
sys_text + " " + next_content if sys_text else next_content
|
||||
)
|
||||
elif next_role == "system": # Next message is a system message
|
||||
# Append a user message instead of the system message
|
||||
new_message = {"role": "user", "content": m["content"]}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from litellm.litellm_core_utils.prompt_templates.factory import (
|
|||
anthropic_messages_pt,
|
||||
_convert_to_bedrock_tool_call_invoke,
|
||||
convert_to_gemini_tool_call_result,
|
||||
map_system_message_pt,
|
||||
ollama_pt,
|
||||
sanitize_messages_for_tool_calling,
|
||||
)
|
||||
|
|
@ -2367,3 +2368,174 @@ def test_anthropic_messages_pt_file_block_preserves_cache_control():
|
|||
assert text_block["type"] == "text"
|
||||
assert "cache_control" in text_block
|
||||
assert text_block["cache_control"]["type"] == "ephemeral"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# map_system_message_pt — list-typed content support (issue #23757)
|
||||
# ---------------------------------------------------------------------------
|
||||
class TestMapSystemMessagePtListContent:
|
||||
"""
|
||||
Verify that map_system_message_pt merges system messages into the next
|
||||
user/assistant message correctly when either side carries a list of
|
||||
typed content blocks (e.g. [{"type": "text", "text": "..."}]) — the
|
||||
shape produced by Anthropic-format adapters such as Claude Code.
|
||||
|
||||
Regression test for https://github.com/BerriAI/litellm/issues/23757.
|
||||
"""
|
||||
|
||||
def test_string_system_string_user(self):
|
||||
"""Baseline: both sides plain strings — original behaviour preserved."""
|
||||
messages = [
|
||||
{"role": "system", "content": "Be brief."},
|
||||
{"role": "user", "content": "Hello."},
|
||||
]
|
||||
result = map_system_message_pt(messages)
|
||||
assert len(result) == 1
|
||||
assert result[0]["role"] == "user"
|
||||
assert result[0]["content"] == "Be brief. Hello."
|
||||
|
||||
def test_list_system_list_user(self):
|
||||
"""Both sides typed content lists — must NOT raise TypeError."""
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": [{"type": "text", "text": "Be brief."}],
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [{"type": "text", "text": "Hello."}],
|
||||
},
|
||||
]
|
||||
result = map_system_message_pt(messages)
|
||||
assert len(result) == 1
|
||||
assert result[0]["role"] == "user"
|
||||
# System text is prepended as a new text block
|
||||
content = result[0]["content"]
|
||||
assert isinstance(content, list)
|
||||
assert content[0] == {"type": "text", "text": "Be brief."}
|
||||
assert content[1] == {"type": "text", "text": "Hello."}
|
||||
|
||||
def test_list_system_string_user(self):
|
||||
"""System list, user string — extract sys_text and concat."""
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": [{"type": "text", "text": "Be brief."}],
|
||||
},
|
||||
{"role": "user", "content": "Hello."},
|
||||
]
|
||||
result = map_system_message_pt(messages)
|
||||
assert len(result) == 1
|
||||
assert result[0]["content"] == "Be brief. Hello."
|
||||
|
||||
def test_string_system_list_user(self):
|
||||
"""System string, user list — prepend text block to list."""
|
||||
messages = [
|
||||
{"role": "system", "content": "Be brief."},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [{"type": "text", "text": "Hello."}],
|
||||
},
|
||||
]
|
||||
result = map_system_message_pt(messages)
|
||||
assert len(result) == 1
|
||||
content = result[0]["content"]
|
||||
assert content[0] == {"type": "text", "text": "Be brief."}
|
||||
assert content[1] == {"type": "text", "text": "Hello."}
|
||||
|
||||
def test_list_system_with_non_text_blocks_only_no_stray_space(self):
|
||||
"""
|
||||
Edge case (Greptile P2): when sys_content is a list whose only
|
||||
blocks are non-text (e.g. pure-image system), sys_text is "".
|
||||
Must NOT produce a stray leading space on the string path.
|
||||
"""
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": [
|
||||
{
|
||||
"type": "image",
|
||||
"source": {
|
||||
"type": "base64",
|
||||
"media_type": "image/png",
|
||||
"data": "...",
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
{"role": "user", "content": "Hello."},
|
||||
]
|
||||
result = map_system_message_pt(messages)
|
||||
assert len(result) == 1
|
||||
# No leading space when sys_text was ""
|
||||
assert result[0]["content"] == "Hello."
|
||||
|
||||
def test_list_system_with_non_text_blocks_only_no_empty_text_block(self):
|
||||
"""
|
||||
Edge case (Greptile P2): when sys_content is a list whose only
|
||||
blocks are non-text and the next message is also a list, must
|
||||
NOT prepend an empty {"type": "text", "text": ""} block.
|
||||
"""
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": [
|
||||
{
|
||||
"type": "image",
|
||||
"source": {
|
||||
"type": "base64",
|
||||
"media_type": "image/png",
|
||||
"data": "...",
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [{"type": "text", "text": "Hello."}],
|
||||
},
|
||||
]
|
||||
result = map_system_message_pt(messages)
|
||||
assert len(result) == 1
|
||||
content = result[0]["content"]
|
||||
# next_content is preserved as-is — no empty text block prepended
|
||||
assert content == [{"type": "text", "text": "Hello."}]
|
||||
|
||||
def test_multiple_text_blocks_in_system(self):
|
||||
"""System with multiple text blocks — joined with space."""
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": [
|
||||
{"type": "text", "text": "Be brief."},
|
||||
{"type": "text", "text": "Be polite."},
|
||||
],
|
||||
},
|
||||
{"role": "user", "content": "Hello."},
|
||||
]
|
||||
result = map_system_message_pt(messages)
|
||||
assert result[0]["content"] == "Be brief. Be polite. Hello."
|
||||
|
||||
def test_system_at_end_of_list(self):
|
||||
"""System message as the last message — converts to user message."""
|
||||
messages = [
|
||||
{"role": "user", "content": "Hi."},
|
||||
{"role": "system", "content": "Be brief."},
|
||||
]
|
||||
result = map_system_message_pt(messages)
|
||||
assert len(result) == 2
|
||||
assert result[1]["role"] == "user"
|
||||
assert result[1]["content"] == "Be brief."
|
||||
|
||||
def test_consecutive_system_messages(self):
|
||||
"""system→system→user — first system becomes a user message."""
|
||||
messages = [
|
||||
{"role": "system", "content": "Be brief."},
|
||||
{"role": "system", "content": "Be polite."},
|
||||
{"role": "user", "content": "Hello."},
|
||||
]
|
||||
result = map_system_message_pt(messages)
|
||||
# First system is appended as user, second is merged into "Hello."
|
||||
assert result[0]["role"] == "user"
|
||||
assert result[0]["content"] == "Be brief."
|
||||
assert result[-1]["content"] == "Be polite. Hello."
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue