diff --git a/litellm/litellm_core_utils/prompt_templates/factory.py b/litellm/litellm_core_utils/prompt_templates/factory.py index 13b85a3dc2ce..3af385ae617e 100644 --- a/litellm/litellm_core_utils/prompt_templates/factory.py +++ b/litellm/litellm_core_utils/prompt_templates/factory.py @@ -2474,8 +2474,22 @@ def _bedrock_converse_messages_pt( # noqa: PLR0915 for element in messages[msg_i]["content"]: if isinstance(element, dict): if element["type"] == "text": - _part = BedrockContentBlock(text=element["text"]) - _parts.append(_part) + if element["text"].strip(): + _part = BedrockContentBlock(text=element["text"]) + else: + # bedrock requires non-empty content + # insert a default text block if the user provided it, or if modify_params is True + if user_continue_message is not None: + _part = BedrockContentBlock( + text=user_continue_message["content"][0]["text"] + ) + elif litellm.modify_params: + _part = BedrockContentBlock( + text=DEFAULT_USER_CONTINUE_MESSAGE["content"][0]["text"] + ) + else: + _part = BedrockContentBlock(text="") + _parts.append(_part) elif element["type"] == "image_url": if isinstance(element["image_url"], dict): image_url = element["image_url"]["url"] @@ -2494,7 +2508,19 @@ def _bedrock_converse_messages_pt( # noqa: PLR0915 _parts.append(_cache_point_block) user_content.extend(_parts) else: - _part = BedrockContentBlock(text=messages[msg_i]["content"]) + if messages[msg_i]["content"].strip(): + # bedrock requires non-empty content + # insert a default text block if the user provided it, or if modify_params is True + if user_continue_message is not None: + _part = BedrockContentBlock( + text=user_continue_message["content"][0]["text"] + ) + elif litellm.modify_params: + _part = BedrockContentBlock( + text=DEFAULT_USER_CONTINUE_MESSAGE["content"][0]["text"] + ) + else: + _part = BedrockContentBlock(text="") _cache_point_block = ( litellm.AmazonConverseConfig()._get_cache_point_block( messages[msg_i], block_type="content_block" @@ -2566,7 +2592,21 @@ def _bedrock_converse_messages_pt( # noqa: PLR0915 for element in messages[msg_i]["content"]: if isinstance(element, dict): if element["type"] == "text": - assistants_part = BedrockContentBlock(text=element["text"]) + if element["text"].strip(): + assistants_part = BedrockContentBlock(text=element["text"]) + else: + # bedrock requires non-empty content + # insert a default text block if the user provided it, or if modify_params is True + if assistant_continue_message is not None: + assistants_part = BedrockContentBlock( + text=assistant_continue_message["content"][0]["text"] + ) + elif litellm.modify_params: + assistants_part = BedrockContentBlock( + text=DEFAULT_ASSISTANT_CONTINUE_MESSAGE["content"][0]["text"] + ) + else: + assistants_part = BedrockContentBlock(text="") assistants_parts.append(assistants_part) elif element["type"] == "image_url": if isinstance(element["image_url"], dict): diff --git a/tests/llm_translation/test_bedrock_completion.py b/tests/llm_translation/test_bedrock_completion.py index dd33e4045cae..6333c1956c04 100644 --- a/tests/llm_translation/test_bedrock_completion.py +++ b/tests/llm_translation/test_bedrock_completion.py @@ -2112,6 +2112,112 @@ def get_base_rerank_call_args(self) -> dict: } +def test_bedrock_empty_content_handling(): + """ + Test that empty content in messages is handled correctly with default messages + """ + messages = [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Hello!" + } + ] + }, + { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "" + } + ] + } + ] + + # Test with default behavior (modify_params=True) + litellm.modify_params = True + formatted_messages = _bedrock_converse_messages_pt( + messages=messages, + model="anthropic.claude-3-sonnet-20240229-v1:0", + llm_provider="bedrock" + ) + + # Verify assistant message with default text was inserted + assert formatted_messages[1]["role"] == "assistant" + assert formatted_messages[1]["content"][0].text == "Please continue." + +def test_bedrock_custom_continue_message(): + """ + Test that custom continue messages are used when provided + """ + messages = [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Hello!" + } + ] + }, + { + "role": "assistant", + "content": [ + { + "type": "text", + "text": " " + } + ] + } + ] + + custom_continue = { + "role": "assistant", + "content": [ + { + "text": "Custom continue message" + } + ] + } + + formatted_messages = _bedrock_converse_messages_pt( + messages=messages, + model="anthropic.claude-3-sonnet-20240229-v1:0", + llm_provider="bedrock", + assistant_continue_message=custom_continue + ) + + # Verify custom message was used + assert formatted_messages[1]["role"] == "assistant" + assert formatted_messages[1]["content"][0].text == "Custom continue message" + +def test_bedrock_no_default_message(): + """ + Test that empty content is handled correctly when modify_params=False + """ + messages = [ + {"role": "user", "content": "Hello!"}, + {"role": "assistant", "content": ""}, + {"role": "user", "content": "Hi again"}, + {"role": "assistant", "content": "Valid response"} + ] + + litellm.modify_params = False + formatted_messages = _bedrock_converse_messages_pt( + messages=messages, + model="anthropic.claude-3-sonnet-20240229-v1:0", + llm_provider="bedrock" + ) + + # Verify empty message is present and valid message remains + assistant_messages = [msg for msg in formatted_messages if msg["role"] == "assistant"] + assert len(assistant_messages) == 2 # Both empty and valid messages present + assert assistant_messages[0]["content"][0].text == "" # First message is empty + assert assistant_messages[1]["content"][0].text == "Valid response" # Second message is valid + @pytest.mark.parametrize("top_k_param", ["top_k", "topK"]) def test_bedrock_nova_topk(top_k_param): litellm.set_verbose = True @@ -2121,3 +2227,4 @@ def test_bedrock_nova_topk(top_k_param): top_k_param: 10, } litellm.completion(**data) +