From a76079828347c72940ebd6e351ef317bc599a884 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 11 Jul 2026 17:07:49 +0000 Subject: [PATCH] fix(factory): merge system message into content-block lists without string concat --- .../prompt_templates/factory.py | 14 ++++- tests/llm_translation/test_optional_params.py | 63 +++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/litellm/litellm_core_utils/prompt_templates/factory.py b/litellm/litellm_core_utils/prompt_templates/factory.py index 8bb0e12905e..06b1756afb4 100644 --- a/litellm/litellm_core_utils/prompt_templates/factory.py +++ b/litellm/litellm_core_utils/prompt_templates/factory.py @@ -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"]} diff --git a/tests/llm_translation/test_optional_params.py b/tests/llm_translation/test_optional_params.py index 9ebdb4b7e97..cd3f5183f50 100644 --- a/tests/llm_translation/test_optional_params.py +++ b/tests/llm_translation/test_optional_params.py @@ -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)] )