diff --git a/litellm/litellm_core_utils/prompt_templates/factory.py b/litellm/litellm_core_utils/prompt_templates/factory.py index 47272b38ad6..38a785f51d9 100644 --- a/litellm/litellm_core_utils/prompt_templates/factory.py +++ b/litellm/litellm_core_utils/prompt_templates/factory.py @@ -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) diff --git a/tests/llm_translation/test_optional_params.py b/tests/llm_translation/test_optional_params.py index 56f05580cb2..565a6e644e7 100644 --- a/tests/llm_translation/test_optional_params.py +++ b/tests/llm_translation/test_optional_params.py @@ -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)] )