diff --git a/src/strands/models/bedrock.py b/src/strands/models/bedrock.py index 3fa907995..a48e810de 100644 --- a/src/strands/models/bedrock.py +++ b/src/strands/models/bedrock.py @@ -926,11 +926,14 @@ def _convert_non_streaming_to_streaming(self, response: dict[str, Any]) -> Itera yield {"messageStart": {"role": response["output"]["message"]["role"]}} # Process content blocks - for content in cast(list[ContentBlock], response["output"]["message"]["content"]): - # Yield contentBlockStart event if needed + for content_block_index, content in enumerate( + cast(list[ContentBlock], response["output"]["message"]["content"]) + ): + # Yield contentBlockStart for all content block types to match streaming behavior if "toolUse" in content: yield { "contentBlockStart": { + "contentBlockIndex": content_block_index, "start": { "toolUse": { "toolUseId": content["toolUse"]["toolUseId"], @@ -945,14 +948,24 @@ def _convert_non_streaming_to_streaming(self, response: dict[str, Any]) -> Itera yield {"contentBlockDelta": {"delta": {"toolUse": {"input": input_value}}}} elif "text" in content: - # Then yield the text as a delta + yield { + "contentBlockStart": { + "contentBlockIndex": content_block_index, + "start": {}, + } + } yield { "contentBlockDelta": { "delta": {"text": content["text"]}, } } elif "reasoningContent" in content: - # Then yield the reasoning content as a delta + yield { + "contentBlockStart": { + "contentBlockIndex": content_block_index, + "start": {}, + } + } yield { "contentBlockDelta": { "delta": {"reasoningContent": {"text": content["reasoningContent"]["reasoningText"]["text"]}} @@ -970,6 +983,12 @@ def _convert_non_streaming_to_streaming(self, response: dict[str, Any]) -> Itera } } elif "citationsContent" in content: + yield { + "contentBlockStart": { + "contentBlockIndex": content_block_index, + "start": {}, + } + } # For non-streaming citations, emit text and metadata deltas in sequence # to match streaming behavior where they flow naturally if "content" in content["citationsContent"]: diff --git a/tests/strands/models/test_bedrock.py b/tests/strands/models/test_bedrock.py index 66fe8ab00..2c121b9bb 100644 --- a/tests/strands/models/test_bedrock.py +++ b/tests/strands/models/test_bedrock.py @@ -1092,6 +1092,7 @@ async def test_stream_with_streaming_false(bedrock_client, alist, messages): tru_events = await alist(response) exp_events = [ {"messageStart": {"role": "assistant"}}, + {"contentBlockStart": {"contentBlockIndex": 0, "start": {}}}, {"contentBlockDelta": {"delta": {"text": "test"}}}, {"contentBlockStop": {}}, {"messageStop": {"stopReason": "end_turn", "additionalModelResponseFields": None}}, @@ -1122,7 +1123,12 @@ async def test_stream_with_streaming_false_and_tool_use(bedrock_client, alist, m tru_events = await alist(response) exp_events = [ {"messageStart": {"role": "assistant"}}, - {"contentBlockStart": {"start": {"toolUse": {"toolUseId": "123", "name": "dummyTool"}}}}, + { + "contentBlockStart": { + "contentBlockIndex": 0, + "start": {"toolUse": {"toolUseId": "123", "name": "dummyTool"}}, + } + }, {"contentBlockDelta": {"delta": {"toolUse": {"input": '{"hello": "world!"}'}}}}, {"contentBlockStop": {}}, {"messageStop": {"stopReason": "tool_use", "additionalModelResponseFields": None}}, @@ -1159,6 +1165,7 @@ async def test_stream_with_streaming_false_and_reasoning(bedrock_client, alist, tru_events = await alist(response) exp_events = [ {"messageStart": {"role": "assistant"}}, + {"contentBlockStart": {"contentBlockIndex": 0, "start": {}}}, {"contentBlockDelta": {"delta": {"reasoningContent": {"text": "Thinking really hard...."}}}}, {"contentBlockDelta": {"delta": {"reasoningContent": {"signature": "123"}}}}, {"contentBlockStop": {}}, @@ -1197,6 +1204,7 @@ async def test_stream_and_reasoning_no_signature(bedrock_client, alist, messages tru_events = await alist(response) exp_events = [ {"messageStart": {"role": "assistant"}}, + {"contentBlockStart": {"contentBlockIndex": 0, "start": {}}}, {"contentBlockDelta": {"delta": {"reasoningContent": {"text": "Thinking really hard...."}}}}, {"contentBlockStop": {}}, {"messageStop": {"stopReason": "tool_use", "additionalModelResponseFields": None}}, @@ -1224,6 +1232,7 @@ async def test_stream_with_streaming_false_with_metrics_and_usage(bedrock_client tru_events = await alist(response) exp_events = [ {"messageStart": {"role": "assistant"}}, + {"contentBlockStart": {"contentBlockIndex": 0, "start": {}}}, {"contentBlockDelta": {"delta": {"text": "test"}}}, {"contentBlockStop": {}}, {"messageStop": {"stopReason": "tool_use", "additionalModelResponseFields": None}}, @@ -1265,6 +1274,7 @@ async def test_stream_input_guardrails(bedrock_client, alist, messages): tru_events = await alist(response) exp_events = [ {"messageStart": {"role": "assistant"}}, + {"contentBlockStart": {"contentBlockIndex": 0, "start": {}}}, {"contentBlockDelta": {"delta": {"text": "test"}}}, {"contentBlockStop": {}}, {"messageStop": {"stopReason": "end_turn", "additionalModelResponseFields": None}}, @@ -1316,6 +1326,7 @@ async def test_stream_output_guardrails(bedrock_client, alist, messages): tru_events = await alist(response) exp_events = [ {"messageStart": {"role": "assistant"}}, + {"contentBlockStart": {"contentBlockIndex": 0, "start": {}}}, {"contentBlockDelta": {"delta": {"text": "test"}}}, {"contentBlockStop": {}}, {"messageStop": {"stopReason": "end_turn", "additionalModelResponseFields": None}}, @@ -1369,6 +1380,7 @@ async def test_stream_output_guardrails_redacts_output(bedrock_client, alist, me tru_events = await alist(response) exp_events = [ {"messageStart": {"role": "assistant"}}, + {"contentBlockStart": {"contentBlockIndex": 0, "start": {}}}, {"contentBlockDelta": {"delta": {"text": "test"}}}, {"contentBlockStop": {}}, {"messageStop": {"stopReason": "end_turn", "additionalModelResponseFields": None}},