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.
This commit is contained in:
Thiagobhmg 2026-07-11 13:31:12 -03:00
parent 3d63edaa6d
commit e45ab7c774
2 changed files with 73 additions and 2 deletions

View file

@ -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"]}

View file

@ -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"},
]