Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 29 additions & 6 deletions src/strands/models/bedrock.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = []
Expand All @@ -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"]
Expand All @@ -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

Expand Down Expand Up @@ -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.]",
)
}
}
Expand Down Expand Up @@ -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.

Expand All @@ -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

Expand Down
53 changes: 53 additions & 0 deletions tests/strands/models/test_bedrock.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}]
Loading