diff --git a/src/strands/models/bedrock.py b/src/strands/models/bedrock.py index 8a6d5116f..aa19b114d 100644 --- a/src/strands/models/bedrock.py +++ b/src/strands/models/bedrock.py @@ -293,7 +293,9 @@ def _format_bedrock_messages(self, messages: Messages) -> Messages: https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ToolResultBlock.html """ cleaned_messages = [] + filtered_unknown_members = False + dropped_deepseek_reasoning_content = False for message in messages: cleaned_content: list[ContentBlock] = [] @@ -304,6 +306,12 @@ def _format_bedrock_messages(self, messages: Messages) -> Messages: filtered_unknown_members = True continue + # DeepSeek models have issues with reasoningContent + # TODO: Replace with systematic model configuration registry (https://github.com/strands-agents/sdk-python/issues/780) + if "deepseek" in self.config["model_id"].lower() and "reasoningContent" in content_block: + dropped_deepseek_reasoning_content = True + continue + if "toolResult" in content_block: # Create a new content block with only the cleaned toolResult tool_result: ToolResult = content_block["toolResult"] @@ -327,14 +335,19 @@ def _format_bedrock_messages(self, messages: Messages) -> Messages: # Keep other content blocks as-is cleaned_content.append(content_block) - # Create new message with cleaned content - cleaned_message: Message = Message(content=cleaned_content, role=message["role"]) - cleaned_messages.append(cleaned_message) + # Create new message with cleaned content (skip if empty for DeepSeek) + if cleaned_content: + cleaned_message: Message = Message(content=cleaned_content, role=message["role"]) + cleaned_messages.append(cleaned_message) if filtered_unknown_members: logger.warning( "Filtered out SDK_UNKNOWN_MEMBER content blocks from messages, consider upgrading boto3 version" ) + if dropped_deepseek_reasoning_content: + logger.debug( + "Filtered DeepSeek reasoningContent content blocks from messages - https://api-docs.deepseek.com/guides/reasoning_model#multi-round-conversation" + ) return cleaned_messages @@ -386,7 +399,8 @@ def _generate_redaction_events(self) -> list[StreamEvent]: { "redactContent": { "redactAssistantContentMessage": self.config.get( - "guardrail_redact_output_message", "[Assistant output redacted.]" + "guardrail_redact_output_message", + "[Assistant output redacted.]", ) } } @@ -699,7 +713,11 @@ def _find_detected_and_blocked_policy(self, input: Any) -> bool: @override async def structured_output( - self, output_model: Type[T], prompt: Messages, system_prompt: Optional[str] = None, **kwargs: Any + self, + output_model: Type[T], + prompt: Messages, + system_prompt: Optional[str] = None, + **kwargs: Any, ) -> AsyncGenerator[dict[str, Union[T, Any]], None]: """Get structured output from the model. @@ -714,7 +732,12 @@ async def structured_output( """ tool_spec = convert_pydantic_to_tool_spec(output_model) - response = self.stream(messages=prompt, tool_specs=[tool_spec], system_prompt=system_prompt, **kwargs) + response = self.stream( + messages=prompt, + tool_specs=[tool_spec], + system_prompt=system_prompt, + **kwargs, + ) async for event in streaming.process_stream(response): yield event diff --git a/tests/strands/models/test_bedrock.py b/tests/strands/models/test_bedrock.py index 13918b6ea..f2e459bde 100644 --- a/tests/strands/models/test_bedrock.py +++ b/tests/strands/models/test_bedrock.py @@ -1392,3 +1392,56 @@ def test_format_request_filters_sdk_unknown_member_content_blocks(model, model_i for block in content: assert "SDK_UNKNOWN_MEMBER" not in block + + +@pytest.mark.asyncio +async def test_stream_deepseek_filters_reasoning_content(bedrock_client, alist): + """Test that DeepSeek models filter reasoningContent from messages during streaming.""" + model = BedrockModel(model_id="us.deepseek.r1-v1:0") + + messages = [ + {"role": "user", "content": [{"text": "Hello"}]}, + { + "role": "assistant", + "content": [ + {"text": "Response"}, + {"reasoningContent": {"reasoningText": {"text": "Thinking..."}}}, + ], + }, + ] + + bedrock_client.converse_stream.return_value = {"stream": []} + + await alist(model.stream(messages)) + + # Verify the request was made with filtered messages (no reasoningContent) + call_args = bedrock_client.converse_stream.call_args[1] + sent_messages = call_args["messages"] + + assert len(sent_messages) == 2 + assert sent_messages[0]["content"] == [{"text": "Hello"}] + assert sent_messages[1]["content"] == [{"text": "Response"}] + + +@pytest.mark.asyncio +async def test_stream_deepseek_skips_empty_messages(bedrock_client, alist): + """Test that DeepSeek models skip messages that would be empty after filtering reasoningContent.""" + model = BedrockModel(model_id="us.deepseek.r1-v1:0") + + messages = [ + {"role": "user", "content": [{"text": "Hello"}]}, + {"role": "assistant", "content": [{"reasoningContent": {"reasoningText": {"text": "Only reasoning..."}}}]}, + {"role": "user", "content": [{"text": "Follow up"}]}, + ] + + bedrock_client.converse_stream.return_value = {"stream": []} + + await alist(model.stream(messages)) + + # Verify the request was made with only non-empty messages + call_args = bedrock_client.converse_stream.call_args[1] + sent_messages = call_args["messages"] + + assert len(sent_messages) == 2 + assert sent_messages[0]["content"] == [{"text": "Hello"}] + assert sent_messages[1]["content"] == [{"text": "Follow up"}]