fix(factory): merge system message into content-block lists without string concat

This commit is contained in:
Devin AI 2026-07-11 17:07:49 +00:00
parent a4199d3c09
commit a760798283
2 changed files with 76 additions and 1 deletions

View file

@ -87,6 +87,18 @@ DEFAULT_ASSISTANT_CONTINUE_MESSAGE = ChatCompletionAssistantMessage(
) # similar to autogen. Only used if `litellm.modify_params=True`.
def _merge_message_content(system_content: Union[str, list], next_content: Union[str, list]) -> Union[str, list]:
if isinstance(system_content, str) and isinstance(next_content, str):
return system_content + " " + next_content
def as_blocks(content: Union[str, list]) -> list:
if isinstance(content, str):
return [{"type": "text", "text": content}]
return list(content)
return as_blocks(system_content) + as_blocks(next_content)
def map_system_message_pt(messages: list) -> list:
"""
Convert 'system' message to 'user' message if provider doesn't support 'system' role.
@ -106,7 +118,7 @@ def map_system_message_pt(messages: list) -> list:
next_role = next_m["role"]
if next_role == "user" or next_role == "assistant": # Next message is a user or assistant message
# Merge system prompt into the next message
next_m["content"] = m["content"] + " " + next_m["content"]
next_m["content"] = _merge_message_content(m["content"], next_m["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"]}

View file

@ -55,6 +55,69 @@ def test_supports_system_message():
assert isinstance(response, litellm.ModelResponse)
def test_map_system_message_pt_content_block_lists():
"""
Regression for https://github.com/BerriAI/litellm/issues/32904
When system and/or user content is a list of content blocks (Anthropic-style),
merging must not string-concatenate a list. Merge as a block list instead.
"""
messages = [
{"role": "system", "content": [{"type": "text", "text": "Be brief."}]},
{"role": "user", "content": [{"type": "text", "text": "Hi"}]},
]
new_messages = map_system_message_pt(messages=messages)
assert len(new_messages) == 1
assert new_messages[0]["role"] == "user"
assert new_messages[0]["content"] == [
{"type": "text", "text": "Be brief."},
{"type": "text", "text": "Hi"},
]
def test_map_system_message_pt_mixed_string_and_content_blocks():
"""
Mixed forms: a plain string on one side and a content-block list on the other
should merge into a block list rather than raising.
"""
system_list_user_str = map_system_message_pt(
messages=[
{"role": "system", "content": [{"type": "text", "text": "Be brief."}]},
{"role": "user", "content": "Hi"},
]
)
assert system_list_user_str[0]["content"] == [
{"type": "text", "text": "Be brief."},
{"type": "text", "text": "Hi"},
]
system_str_user_list = map_system_message_pt(
messages=[
{"role": "system", "content": "Be brief."},
{"role": "user", "content": [{"type": "text", "text": "Hi"}]},
]
)
assert system_str_user_list[0]["content"] == [
{"type": "text", "text": "Be brief."},
{"type": "text", "text": "Hi"},
]
def test_map_system_message_pt_string_content_unchanged():
"""
Plain string content on both sides keeps the original space-joined behavior.
"""
new_messages = map_system_message_pt(
messages=[
{"role": "system", "content": "Be brief."},
{"role": "user", "content": "Hi"},
]
)
assert new_messages[0]["content"] == "Be brief. Hi"
@pytest.mark.parametrize(
"stop_sequence, expected_count", [("\n", 0), (["\n"], 0), (["finish_reason"], 1)]
)