diff --git a/litellm/litellm_core_utils/prompt_templates/factory.py b/litellm/litellm_core_utils/prompt_templates/factory.py index c4c56a8d33..53d2ca2f23 100644 --- a/litellm/litellm_core_utils/prompt_templates/factory.py +++ b/litellm/litellm_core_utils/prompt_templates/factory.py @@ -3987,10 +3987,12 @@ class BedrockConverseMessagesProcessor: assistant_parts=assistants_parts, ) elif element["type"] == "text": - assistants_part = BedrockContentBlock( - text=element["text"] - ) - assistants_parts.append(assistants_part) + # Skip completely empty strings to avoid blank content blocks + if element.get("text", "").strip(): + assistants_part = BedrockContentBlock( + text=element["text"] + ) + assistants_parts.append(assistants_part) elif element["type"] == "image_url": if isinstance(element["image_url"], dict): image_url = element["image_url"]["url"] @@ -4015,9 +4017,12 @@ class BedrockConverseMessagesProcessor: elif _assistant_content is not None and isinstance( _assistant_content, str ): - assistant_content.append( - BedrockContentBlock(text=_assistant_content) - ) + # Skip completely empty strings to avoid blank content blocks + if _assistant_content.strip(): + assistant_content.append( + BedrockContentBlock(text=_assistant_content) + ) + # If content is empty/whitespace, skip it (don't add a placeholder) # Add cache point block for assistant string content _cache_point_block = ( litellm.AmazonConverseConfig()._get_cache_point_block( @@ -4348,12 +4353,11 @@ def _bedrock_converse_messages_pt( # noqa: PLR0915 assistant_parts=assistants_parts, ) elif element["type"] == "text": - # AWS Bedrock doesn't allow empty or whitespace-only text content, so use placeholder for empty strings - text_content = ( - element["text"] if element["text"].strip() else "." - ) - assistants_part = BedrockContentBlock(text=text_content) - assistants_parts.append(assistants_part) + # AWS Bedrock doesn't allow empty or whitespace-only text content + # Skip completely empty strings to avoid blank content blocks + if element.get("text", "").strip(): + assistants_part = BedrockContentBlock(text=element["text"]) + assistants_parts.append(assistants_part) elif element["type"] == "image_url": if isinstance(element["image_url"], dict): image_url = element["image_url"]["url"] @@ -4376,9 +4380,9 @@ def _bedrock_converse_messages_pt( # noqa: PLR0915 assistants_parts.append(_cache_point_block) assistant_content.extend(assistants_parts) elif _assistant_content is not None and isinstance(_assistant_content, str): - # AWS Bedrock doesn't allow empty or whitespace-only text content, so use placeholder for empty strings - text_content = _assistant_content if _assistant_content.strip() else "." - assistant_content.append(BedrockContentBlock(text=text_content)) + # Skip completely empty strings to avoid blank content blocks + if _assistant_content.strip(): + assistant_content.append(BedrockContentBlock(text=_assistant_content)) # Add cache point block for assistant string content _cache_point_block = ( litellm.AmazonConverseConfig()._get_cache_point_block( diff --git a/tests/litellm_core_utils/test_bedrock_converse_dedup_factory.py b/tests/litellm_core_utils/test_bedrock_converse_dedup_factory.py index 0969c77299..5bd0c9993a 100644 --- a/tests/litellm_core_utils/test_bedrock_converse_dedup_factory.py +++ b/tests/litellm_core_utils/test_bedrock_converse_dedup_factory.py @@ -330,3 +330,118 @@ async def test_bedrock_converse_sync_async_parity_with_duplicates(): ) assert sync_result == async_result + + +# --------------------------------------------------------------------------- +# Empty content filtering tests +# --------------------------------------------------------------------------- + + +def test_bedrock_converse_filters_empty_assistant_content(): + """Verify that empty assistant content blocks are filtered out to avoid + Bedrock API errors about blank text fields.""" + messages = [ + {"role": "user", "content": "Say hello"}, + {"role": "assistant", "content": "Hello"}, + {"role": "assistant", "content": " there"}, + {"role": "assistant", "content": "!"}, + {"role": "assistant", "content": ""}, # Empty content + {"role": "assistant", "content": ""}, # Empty content + {"role": "user", "content": "How are you?"}, + ] + + result = _bedrock_converse_messages_pt(messages, MODEL, PROVIDER) + + # Should have 3 messages: user, assistant (with merged non-empty content), user + assert len(result) == 3 + assert result[0]["role"] == "user" + assert result[1]["role"] == "assistant" + assert result[2]["role"] == "user" + + # Assistant message should only contain non-empty text blocks + assistant_content = result[1]["content"] + text_blocks = [block for block in assistant_content if "text" in block] + assert len(text_blocks) == 3 # "Hello", " there", "!" + assert text_blocks[0]["text"] == "Hello" + assert text_blocks[1]["text"] == " there" + assert text_blocks[2]["text"] == "!" + + +@pytest.mark.asyncio +async def test_bedrock_converse_filters_empty_assistant_content_async(): + """Verify that the async path also filters empty assistant content blocks.""" + messages = [ + {"role": "user", "content": "Say hello"}, + {"role": "assistant", "content": "Hello"}, + {"role": "assistant", "content": " there"}, + {"role": "assistant", "content": "!"}, + {"role": "assistant", "content": ""}, # Empty content + {"role": "assistant", "content": ""}, # Empty content + {"role": "user", "content": "How are you?"}, + ] + + result = await BedrockConverseMessagesProcessor._bedrock_converse_messages_pt_async( + messages, MODEL, PROVIDER + ) + + # Should have 3 messages: user, assistant (with merged non-empty content), user + assert len(result) == 3 + assert result[0]["role"] == "user" + assert result[1]["role"] == "assistant" + assert result[2]["role"] == "user" + + # Assistant message should only contain non-empty text blocks + assistant_content = result[1]["content"] + text_blocks = [block for block in assistant_content if "text" in block] + assert len(text_blocks) == 3 # "Hello", " there", "!" + assert text_blocks[0]["text"] == "Hello" + assert text_blocks[1]["text"] == " there" + assert text_blocks[2]["text"] == "!" + + +def test_bedrock_converse_filters_whitespace_only_content(): + """Verify that whitespace-only content is also filtered out.""" + messages = [ + {"role": "user", "content": "Test"}, + {"role": "assistant", "content": "Response"}, + {"role": "assistant", "content": " "}, # Whitespace only + {"role": "assistant", "content": "\n\t"}, # Whitespace only + {"role": "assistant", "content": ""}, # Empty + ] + + result = _bedrock_converse_messages_pt(messages, MODEL, PROVIDER) + + # Should have 2 messages: user and assistant + assert len(result) == 2 + assistant_content = result[1]["content"] + text_blocks = [block for block in assistant_content if "text" in block] + # Only "Response" should be present + assert len(text_blocks) == 1 + assert text_blocks[0]["text"] == "Response" + + +def test_bedrock_converse_filters_empty_list_content(): + """Verify that empty text elements in list content are filtered out.""" + messages = [ + {"role": "user", "content": "Test"}, + { + "role": "assistant", + "content": [ + {"type": "text", "text": "Hello"}, + {"type": "text", "text": ""}, # Empty + {"type": "text", "text": "World"}, + {"type": "text", "text": " "}, # Whitespace only + ], + }, + ] + + result = _bedrock_converse_messages_pt(messages, MODEL, PROVIDER) + + # Should have 2 messages: user and assistant + assert len(result) == 2 + assistant_content = result[1]["content"] + text_blocks = [block for block in assistant_content if "text" in block] + # Only "Hello" and "World" should be present + assert len(text_blocks) == 2 + assert text_blocks[0]["text"] == "Hello" + assert text_blocks[1]["text"] == "World" diff --git a/tests/llm_translation/test_bedrock_completion.py b/tests/llm_translation/test_bedrock_completion.py index 9b0b69caeb..d23033c1e4 100644 --- a/tests/llm_translation/test_bedrock_completion.py +++ b/tests/llm_translation/test_bedrock_completion.py @@ -2356,9 +2356,8 @@ def test_bedrock_no_default_message(): assistant_messages = [ msg for msg in formatted_messages if msg["role"] == "assistant" ] - assert len(assistant_messages) == 2 - assert assistant_messages[0]["content"][0]["text"] == "." - assert assistant_messages[1]["content"][0]["text"] == "Valid response" + assert len(assistant_messages) == 1 + assert assistant_messages[0]["content"][0]["text"] == "Valid response" @pytest.mark.parametrize("top_k_param", ["top_k", "topK"])