Skip to content

Commit 8cb53d3

Browse files
fix(models): filter reasoningContent in Bedrock requests using DeepSeek (#652)
* Fix: strip reasoningContent from messages before sending to Bedrock to avoid ValidationException * Using Message class instead of dict in _strip_reasoning_content_from_message(). * fix(models): filter reasoningContent blocks on Bedrock requests using DeepSeek * fix: formatting and linting * fix: formatting and linting * remove unrelated registry formatting * linting * add log --------- Co-authored-by: Dean Schmigelski <[email protected]>
1 parent b568864 commit 8cb53d3

File tree

2 files changed

+82
-6
lines changed

2 files changed

+82
-6
lines changed

src/strands/models/bedrock.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,9 @@ def _format_bedrock_messages(self, messages: Messages) -> Messages:
293293
https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ToolResultBlock.html
294294
"""
295295
cleaned_messages = []
296+
296297
filtered_unknown_members = False
298+
dropped_deepseek_reasoning_content = False
297299

298300
for message in messages:
299301
cleaned_content: list[ContentBlock] = []
@@ -304,6 +306,12 @@ def _format_bedrock_messages(self, messages: Messages) -> Messages:
304306
filtered_unknown_members = True
305307
continue
306308

309+
# DeepSeek models have issues with reasoningContent
310+
# TODO: Replace with systematic model configuration registry (https://github.com/strands-agents/sdk-python/issues/780)
311+
if "deepseek" in self.config["model_id"].lower() and "reasoningContent" in content_block:
312+
dropped_deepseek_reasoning_content = True
313+
continue
314+
307315
if "toolResult" in content_block:
308316
# Create a new content block with only the cleaned toolResult
309317
tool_result: ToolResult = content_block["toolResult"]
@@ -327,14 +335,19 @@ def _format_bedrock_messages(self, messages: Messages) -> Messages:
327335
# Keep other content blocks as-is
328336
cleaned_content.append(content_block)
329337

330-
# Create new message with cleaned content
331-
cleaned_message: Message = Message(content=cleaned_content, role=message["role"])
332-
cleaned_messages.append(cleaned_message)
338+
# Create new message with cleaned content (skip if empty for DeepSeek)
339+
if cleaned_content:
340+
cleaned_message: Message = Message(content=cleaned_content, role=message["role"])
341+
cleaned_messages.append(cleaned_message)
333342

334343
if filtered_unknown_members:
335344
logger.warning(
336345
"Filtered out SDK_UNKNOWN_MEMBER content blocks from messages, consider upgrading boto3 version"
337346
)
347+
if dropped_deepseek_reasoning_content:
348+
logger.debug(
349+
"Filtered DeepSeek reasoningContent content blocks from messages - https://api-docs.deepseek.com/guides/reasoning_model#multi-round-conversation"
350+
)
338351

339352
return cleaned_messages
340353

@@ -386,7 +399,8 @@ def _generate_redaction_events(self) -> list[StreamEvent]:
386399
{
387400
"redactContent": {
388401
"redactAssistantContentMessage": self.config.get(
389-
"guardrail_redact_output_message", "[Assistant output redacted.]"
402+
"guardrail_redact_output_message",
403+
"[Assistant output redacted.]",
390404
)
391405
}
392406
}
@@ -699,7 +713,11 @@ def _find_detected_and_blocked_policy(self, input: Any) -> bool:
699713

700714
@override
701715
async def structured_output(
702-
self, output_model: Type[T], prompt: Messages, system_prompt: Optional[str] = None, **kwargs: Any
716+
self,
717+
output_model: Type[T],
718+
prompt: Messages,
719+
system_prompt: Optional[str] = None,
720+
**kwargs: Any,
703721
) -> AsyncGenerator[dict[str, Union[T, Any]], None]:
704722
"""Get structured output from the model.
705723
@@ -714,7 +732,12 @@ async def structured_output(
714732
"""
715733
tool_spec = convert_pydantic_to_tool_spec(output_model)
716734

717-
response = self.stream(messages=prompt, tool_specs=[tool_spec], system_prompt=system_prompt, **kwargs)
735+
response = self.stream(
736+
messages=prompt,
737+
tool_specs=[tool_spec],
738+
system_prompt=system_prompt,
739+
**kwargs,
740+
)
718741
async for event in streaming.process_stream(response):
719742
yield event
720743

tests/strands/models/test_bedrock.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1392,3 +1392,56 @@ def test_format_request_filters_sdk_unknown_member_content_blocks(model, model_i
13921392

13931393
for block in content:
13941394
assert "SDK_UNKNOWN_MEMBER" not in block
1395+
1396+
1397+
@pytest.mark.asyncio
1398+
async def test_stream_deepseek_filters_reasoning_content(bedrock_client, alist):
1399+
"""Test that DeepSeek models filter reasoningContent from messages during streaming."""
1400+
model = BedrockModel(model_id="us.deepseek.r1-v1:0")
1401+
1402+
messages = [
1403+
{"role": "user", "content": [{"text": "Hello"}]},
1404+
{
1405+
"role": "assistant",
1406+
"content": [
1407+
{"text": "Response"},
1408+
{"reasoningContent": {"reasoningText": {"text": "Thinking..."}}},
1409+
],
1410+
},
1411+
]
1412+
1413+
bedrock_client.converse_stream.return_value = {"stream": []}
1414+
1415+
await alist(model.stream(messages))
1416+
1417+
# Verify the request was made with filtered messages (no reasoningContent)
1418+
call_args = bedrock_client.converse_stream.call_args[1]
1419+
sent_messages = call_args["messages"]
1420+
1421+
assert len(sent_messages) == 2
1422+
assert sent_messages[0]["content"] == [{"text": "Hello"}]
1423+
assert sent_messages[1]["content"] == [{"text": "Response"}]
1424+
1425+
1426+
@pytest.mark.asyncio
1427+
async def test_stream_deepseek_skips_empty_messages(bedrock_client, alist):
1428+
"""Test that DeepSeek models skip messages that would be empty after filtering reasoningContent."""
1429+
model = BedrockModel(model_id="us.deepseek.r1-v1:0")
1430+
1431+
messages = [
1432+
{"role": "user", "content": [{"text": "Hello"}]},
1433+
{"role": "assistant", "content": [{"reasoningContent": {"reasoningText": {"text": "Only reasoning..."}}}]},
1434+
{"role": "user", "content": [{"text": "Follow up"}]},
1435+
]
1436+
1437+
bedrock_client.converse_stream.return_value = {"stream": []}
1438+
1439+
await alist(model.stream(messages))
1440+
1441+
# Verify the request was made with only non-empty messages
1442+
call_args = bedrock_client.converse_stream.call_args[1]
1443+
sent_messages = call_args["messages"]
1444+
1445+
assert len(sent_messages) == 2
1446+
assert sent_messages[0]["content"] == [{"text": "Hello"}]
1447+
assert sent_messages[1]["content"] == [{"text": "Follow up"}]

0 commit comments

Comments
 (0)