From e45ab7c77482f0983f23defdbfef4a936d195e23 Mon Sep 17 00:00:00 2001 From: Thiagobhmg Date: Sat, 11 Jul 2026 13:31:12 -0300 Subject: [PATCH] Fix map_system_message_pt for content-block list messages map_system_message_pt (used by supports_system_message=False) merged the system prompt into the next message with string concatenation, raising 'TypeError: can only concatenate list (not "str") to list' whenever the system or user content was a list of content blocks - the format sent by Anthropic-style clients (e.g. Claude Code / Agent SDK) through the /v1/messages endpoint. Merge as a list of content blocks when either side is a list; keep the existing string behavior otherwise. Adds regression tests. --- .../prompt_templates/factory.py | 20 ++++++- ...llm_core_utils_prompt_templates_factory.py | 55 +++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/litellm/litellm_core_utils/prompt_templates/factory.py b/litellm/litellm_core_utils/prompt_templates/factory.py index 8bb0e12905e..bc2e0bcfbd8 100644 --- a/litellm/litellm_core_utils/prompt_templates/factory.py +++ b/litellm/litellm_core_utils/prompt_templates/factory.py @@ -87,6 +87,13 @@ DEFAULT_ASSISTANT_CONTINUE_MESSAGE = ChatCompletionAssistantMessage( ) # similar to autogen. Only used if `litellm.modify_params=True`. +def _content_as_block_list(content) -> list: + """Normalize a message content field to a list of content blocks.""" + if isinstance(content, list): + return content + return [{"type": "text", "text": content}] + + def map_system_message_pt(messages: list) -> list: """ Convert 'system' message to 'user' message if provider doesn't support 'system' role. @@ -105,8 +112,17 @@ def map_system_message_pt(messages: list) -> list: next_m = messages[i + 1] 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"] + # Merge system prompt into the next message. Contents may be + # strings or lists of content blocks (e.g. Anthropic-style + # requests); merge as block lists when either side is a list. + if isinstance(m["content"], list) or isinstance( + next_m["content"], list + ): + next_m["content"] = _content_as_block_list( + m["content"] + ) + _content_as_block_list(next_m["content"]) + else: + next_m["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/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py b/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py index bcda88ea609..b1147536491 100644 --- a/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py +++ b/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py @@ -3166,3 +3166,58 @@ async def test_bedrock_converse_message_level_cache_point_preserves_ttl_async(): ) assert _collect_cache_points(result) == [{"type": "default", "ttl": "1h"}] + + +def test_map_system_message_pt_string_contents(): + """Baseline: string contents keep the existing merge behavior.""" + from litellm.litellm_core_utils.prompt_templates.factory import ( + map_system_message_pt, + ) + + messages = [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": "Hi"}, + ] + result = map_system_message_pt(messages=messages) + assert result == [{"role": "user", "content": "You are helpful. Hi"}] + + +def test_map_system_message_pt_content_block_lists(): + """Contents sent as content-block lists (e.g. Anthropic-style requests) + must not raise 'can only concatenate list (not "str") to list'. + Regression test for supports_system_message=False with block content.""" + from litellm.litellm_core_utils.prompt_templates.factory import ( + map_system_message_pt, + ) + + messages = [ + { + "role": "system", + "content": [{"type": "text", "text": "You are helpful."}], + }, + {"role": "user", "content": [{"type": "text", "text": "Hi"}]}, + ] + result = map_system_message_pt(messages=messages) + assert len(result) == 1 + assert result[0]["role"] == "user" + assert result[0]["content"] == [ + {"type": "text", "text": "You are helpful."}, + {"type": "text", "text": "Hi"}, + ] + + +def test_map_system_message_pt_mixed_contents(): + """String system content merged into a block-list user message.""" + from litellm.litellm_core_utils.prompt_templates.factory import ( + map_system_message_pt, + ) + + messages = [ + {"role": "system", "content": "You are helpful."}, + {"role": "user", "content": [{"type": "text", "text": "Hi"}]}, + ] + result = map_system_message_pt(messages=messages) + assert result[0]["content"] == [ + {"type": "text", "text": "You are helpful."}, + {"type": "text", "text": "Hi"}, + ]