From 410441189ff7b642252d1815f49d48990fccff43 Mon Sep 17 00:00:00 2001 From: nightcityblade Date: Fri, 14 Aug 2026 00:06:53 +0800 Subject: [PATCH] fix(bedrock): drop trailing empty Converse chunk --- litellm/llms/bedrock/chat/invoke_handler.py | 5 +++- .../llms/bedrock/chat/test_invoke_handler.py | 29 ++++++++++++++----- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/litellm/llms/bedrock/chat/invoke_handler.py b/litellm/llms/bedrock/chat/invoke_handler.py index 8d2b3dae71b..82232ccf42c 100644 --- a/litellm/llms/bedrock/chat/invoke_handler.py +++ b/litellm/llms/bedrock/chat/invoke_handler.py @@ -565,6 +565,9 @@ class AWSEventStreamDecoder: if "trace" in chunk_data: trace: Final = chunk_data.get("trace") model_response_provider_specific_fields["trace"] = trace + is_content_event: Final = any( + key in chunk_data for key in ("role", "start", "delta", "contentBlockIndex", "stopReason") + ) response: Final = ModelResponseStream( choices=[ StreamingChoices( @@ -572,7 +575,7 @@ class AWSEventStreamDecoder: index=0, # Always 0 - Bedrock never returns multiple choices delta=Delta( content=text, - role="assistant", + role="assistant" if is_content_event else None, tool_calls=[tool_use] if tool_use else None, provider_specific_fields=(provider_specific_fields if provider_specific_fields else None), thinking_blocks=thinking_blocks, diff --git a/tests/test_litellm/llms/bedrock/chat/test_invoke_handler.py b/tests/test_litellm/llms/bedrock/chat/test_invoke_handler.py index ee50b9db015..f0cf833b968 100644 --- a/tests/test_litellm/llms/bedrock/chat/test_invoke_handler.py +++ b/tests/test_litellm/llms/bedrock/chat/test_invoke_handler.py @@ -4,10 +4,9 @@ from unittest.mock import AsyncMock, MagicMock import pytest -sys.path.insert( - 0, os.path.abspath("../../../../..") -) # Adds the parent directory to the system path +sys.path.insert(0, os.path.abspath("../../../../..")) # Adds the parent directory to the system path +from litellm.litellm_core_utils.model_response_utils import is_model_response_stream_empty from litellm.llms.bedrock.chat.invoke_handler import ( AWSEventStreamDecoder, make_call, @@ -203,9 +202,26 @@ def test_bedrock_converse_streaming_consistent_id(): expected_id = f"chatcmpl-{native_conversation_id}" for response in parsed_responses: - assert ( - response.id == expected_id - ), "All chunk IDs must match the one captured from the messageStart event" + assert response.id == expected_id, "All chunk IDs must match the one captured from the messageStart event" + + +def test_converse_metadata_chunk_is_empty_after_usage_is_stripped(): + decoder = AWSEventStreamDecoder(model="test") + + content_chunk = decoder.converse_chunk_parser({"delta": {"text": "Hello"}}) + assert content_chunk.choices[0].delta.role == "assistant" + + metadata_chunk = decoder.converse_chunk_parser( + { + "usage": {"inputTokens": 10, "outputTokens": 5, "totalTokens": 15}, + "metrics": {"latencyMs": 100}, + } + ) + assert metadata_chunk.usage is not None + assert metadata_chunk.choices[0].delta.role is None + + stripped_chunk = metadata_chunk.model_copy(update={"usage": None}) + assert is_model_response_stream_empty(stripped_chunk) @pytest.mark.asyncio @@ -292,4 +308,3 @@ def test_make_sync_call_honors_explicit_stream_chunk_size(): ) response.iter_bytes.assert_called_once_with(chunk_size=2048) -