fix(factory): handle list content in map_system_message_pt

Fixes #23757

When the Anthropic pass-through endpoint converts messages to OpenAI
format, content fields can be lists of content blocks rather than
plain strings. map_system_message_pt assumed string content and
crashed with TypeError on concatenation.

Add _get_content_as_str() helper that normalizes both str and list
content to a string before merging, using the existing
convert_content_list_to_str utility.

Tests: 3 new test cases covering list content, mixed str/list, and
list content as last message.
This commit is contained in:
voidborne-d 2026-03-16 22:07:49 +00:00
parent 3dccdde9c8
commit cd710fe919
2 changed files with 69 additions and 3 deletions

View file

@ -86,6 +86,15 @@ DEFAULT_ASSISTANT_CONTINUE_MESSAGE = ChatCompletionAssistantMessage(
) # similar to autogen. Only used if `litellm.modify_params=True`.
def _get_content_as_str(content: Union[str, list]) -> str:
"""Extract text from content that may be a string or a list of content blocks."""
if isinstance(content, str):
return content
if isinstance(content, list):
return convert_content_list_to_str({"content": content})
return str(content)
def map_system_message_pt(messages: list) -> list:
"""
Convert 'system' message to 'user' message if provider doesn't support 'system' role.
@ -100,6 +109,7 @@ def map_system_message_pt(messages: list) -> list:
new_messages = []
for i, m in enumerate(messages):
if m["role"] == "system":
system_text = _get_content_as_str(m["content"])
if i < len(messages) - 1: # Not the last message
next_m = messages[i + 1]
next_role = next_m["role"]
@ -107,13 +117,14 @@ def map_system_message_pt(messages: list) -> list:
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_text = _get_content_as_str(next_m["content"])
next_m["content"] = system_text + " " + next_text
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"]}
new_message = {"role": "user", "content": system_text}
new_messages.append(new_message)
else: # Last message
new_message = {"role": "user", "content": m["content"]}
new_message = {"role": "user", "content": system_text}
new_messages.append(new_message)
else: # Not a system message
new_messages.append(m)

View file

@ -55,6 +55,61 @@ def test_supports_system_message():
assert isinstance(response, litellm.ModelResponse)
def test_supports_system_message_list_content():
"""
Test map_system_message_pt when content is a list of content blocks
(e.g. from Anthropic pass-through endpoint).
Fixes: https://github.com/BerriAI/litellm/issues/23757
"""
# System message with list content (Anthropic format)
messages = [
{"role": "system", "content": [{"type": "text", "text": "You are helpful."}]},
{"role": "user", "content": [{"type": "text", "text": "Hello!"}]},
]
new_messages = map_system_message_pt(messages=messages)
assert len(new_messages) == 1
assert new_messages[0]["role"] == "user"
assert isinstance(new_messages[0]["content"], str)
assert "You are helpful." in new_messages[0]["content"]
assert "Hello!" in new_messages[0]["content"]
def test_supports_system_message_mixed_content():
"""
Test map_system_message_pt with mixed str and list content types.
"""
messages = [
{"role": "system", "content": "System prompt"},
{"role": "user", "content": [{"type": "text", "text": "User message"}]},
]
new_messages = map_system_message_pt(messages=messages)
assert len(new_messages) == 1
assert new_messages[0]["role"] == "user"
assert isinstance(new_messages[0]["content"], str)
assert "System prompt" in new_messages[0]["content"]
assert "User message" in new_messages[0]["content"]
def test_supports_system_message_list_content_last_message():
"""
Test map_system_message_pt when system message with list content is the last message.
"""
messages = [
{"role": "system", "content": [{"type": "text", "text": "Only system"}]},
]
new_messages = map_system_message_pt(messages=messages)
assert len(new_messages) == 1
assert new_messages[0]["role"] == "user"
assert new_messages[0]["content"] == "Only system"
@pytest.mark.parametrize(
"stop_sequence, expected_count", [("\n", 0), (["\n"], 0), (["finish_reason"], 1)]
)