From cc46e89c6d40021e324a5dee6a52bea49448cbe5 Mon Sep 17 00:00:00 2001 From: aryan835-datainflexion Date: Mon, 11 Aug 2025 15:40:49 +0400 Subject: [PATCH 1/8] Fix: strip reasoningContent from messages before sending to Bedrock to avoid ValidationException --- src/strands/models/bedrock.py | 142 ++++++++++++++++++++++++++++------ 1 file changed, 117 insertions(+), 25 deletions(-) diff --git a/src/strands/models/bedrock.py b/src/strands/models/bedrock.py index 8a6d5116f..d98426951 100644 --- a/src/strands/models/bedrock.py +++ b/src/strands/models/bedrock.py @@ -7,7 +7,17 @@ import json import logging import os -from typing import Any, AsyncGenerator, Callable, Iterable, Literal, Optional, Type, TypeVar, Union, cast +from typing import ( + Any, + AsyncGenerator, + Callable, + Iterable, + Literal, + Optional, + Type, + TypeVar, + Union, +) import boto3 from botocore.config import Config as BotocoreConfig @@ -144,11 +154,18 @@ def __init__( else: new_user_agent = "strands-agents" - client_config = boto_client_config.merge(BotocoreConfig(user_agent_extra=new_user_agent)) + client_config = boto_client_config.merge( + BotocoreConfig(user_agent_extra=new_user_agent) + ) else: client_config = BotocoreConfig(user_agent_extra="strands-agents") - resolved_region = region_name or session.region_name or os.environ.get("AWS_REGION") or DEFAULT_BEDROCK_REGION + resolved_region = ( + region_name + or session.region_name + or os.environ.get("AWS_REGION") + or DEFAULT_BEDROCK_REGION + ) self.client = session.client( service_name="bedrock-runtime", @@ -157,7 +174,9 @@ def __init__( region_name=resolved_region, ) - logger.debug("region=<%s> | bedrock client created", self.client.meta.region_name) + logger.debug( + "region=<%s> | bedrock client created", self.client.meta.region_name + ) @override def update_config(self, **model_config: Unpack[BedrockConfig]) -> None: # type: ignore @@ -209,7 +228,11 @@ def format_request( "messages": self._format_bedrock_messages(messages), "system": [ *([{"text": system_prompt}] if system_prompt else []), - *([{"cachePoint": {"type": self.config["cache_prompt"]}}] if self.config.get("cache_prompt") else []), + *( + [{"cachePoint": {"type": self.config["cache_prompt"]}}] + if self.config.get("cache_prompt") + else [] + ), ], **( { @@ -229,12 +252,20 @@ def format_request( else {} ), **( - {"additionalModelRequestFields": self.config["additional_request_fields"]} + { + "additionalModelRequestFields": self.config[ + "additional_request_fields" + ] + } if self.config.get("additional_request_fields") else {} ), **( - {"additionalModelResponseFieldPaths": self.config["additional_response_field_paths"]} + { + "additionalModelResponseFieldPaths": self.config[ + "additional_response_field_paths" + ] + } if self.config.get("additional_response_field_paths") else {} ), @@ -245,13 +276,18 @@ def format_request( "guardrailVersion": self.config["guardrail_version"], "trace": self.config.get("guardrail_trace", "enabled"), **( - {"streamProcessingMode": self.config.get("guardrail_stream_processing_mode")} + { + "streamProcessingMode": self.config.get( + "guardrail_stream_processing_mode" + ) + } if self.config.get("guardrail_stream_processing_mode") else {} ), } } - if self.config.get("guardrail_id") and self.config.get("guardrail_version") + if self.config.get("guardrail_id") + and self.config.get("guardrail_version") else {} ), "inferenceConfig": { @@ -266,7 +302,8 @@ def format_request( }, **( self.config["additional_args"] - if "additional_args" in self.config and self.config["additional_args"] is not None + if "additional_args" in self.config + and self.config["additional_args"] is not None else {} ), } @@ -328,7 +365,9 @@ def _format_bedrock_messages(self, messages: Messages) -> Messages: cleaned_content.append(content_block) # Create new message with cleaned content - cleaned_message: Message = Message(content=cleaned_content, role=message["role"]) + cleaned_message: Message = Message( + content=cleaned_content, role=message["role"] + ) cleaned_messages.append(cleaned_message) if filtered_unknown_members: @@ -351,11 +390,17 @@ def _has_blocked_guardrail(self, guardrail_data: dict[str, Any]) -> bool: output_assessments = guardrail_data.get("outputAssessments", {}) # Check input assessments - if any(self._find_detected_and_blocked_policy(assessment) for assessment in input_assessment.values()): + if any( + self._find_detected_and_blocked_policy(assessment) + for assessment in input_assessment.values() + ): return True # Check output assessments - if any(self._find_detected_and_blocked_policy(assessment) for assessment in output_assessments.values()): + if any( + self._find_detected_and_blocked_policy(assessment) + for assessment in output_assessments.values() + ): return True return False @@ -386,7 +431,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.]", ) } } @@ -429,7 +475,9 @@ def callback(event: Optional[StreamEvent] = None) -> None: loop = asyncio.get_event_loop() queue: asyncio.Queue[Optional[StreamEvent]] = asyncio.Queue() - thread = asyncio.to_thread(self._stream, callback, messages, tool_specs, system_prompt) + thread = asyncio.to_thread( + self._stream, callback, messages, tool_specs, system_prompt + ) task = asyncio.create_task(thread) while True: @@ -441,6 +489,18 @@ def callback(event: Optional[StreamEvent] = None) -> None: await task + def _strip_reasoning_content_from_message(self, message: dict) -> dict: + # Deep copy the message to avoid mutating original + import copy + + msg_copy = copy.deepcopy(message) + + content = msg_copy.get("content", []) + # Filter out any content blocks with reasoningContent + filtered_content = [c for c in content if "reasoningContent" not in c] + msg_copy["content"] = filtered_content + return msg_copy + def _stream( self, callback: Callable[..., None], @@ -525,7 +585,10 @@ def _stream( if e.response["Error"]["Code"] == "ThrottlingException": raise ModelThrottledException(error_message) from e - if any(overflow_message in error_message for overflow_message in BEDROCK_CONTEXT_WINDOW_OVERFLOW_MESSAGES): + if any( + overflow_message in error_message + for overflow_message in BEDROCK_CONTEXT_WINDOW_OVERFLOW_MESSAGES + ): logger.warning("bedrock threw context window overflow error") raise ContextWindowOverflowException(e) from e @@ -561,7 +624,9 @@ def _stream( callback() logger.debug("finished streaming response from model") - def _convert_non_streaming_to_streaming(self, response: dict[str, Any]) -> Iterable[StreamEvent]: + def _convert_non_streaming_to_streaming( + self, response: dict[str, Any] + ) -> Iterable[StreamEvent]: """Convert a non-streaming response to the streaming format. Args: @@ -591,7 +656,9 @@ def _convert_non_streaming_to_streaming(self, response: dict[str, Any]) -> Itera # For tool use, we need to yield the input as a delta input_value = json.dumps(content["toolUse"]["input"]) - yield {"contentBlockDelta": {"delta": {"toolUse": {"input": input_value}}}} + yield { + "contentBlockDelta": {"delta": {"toolUse": {"input": input_value}}} + } elif "text" in content: # Then yield the text as a delta yield { @@ -603,7 +670,13 @@ def _convert_non_streaming_to_streaming(self, response: dict[str, Any]) -> Itera # Then yield the reasoning content as a delta yield { "contentBlockDelta": { - "delta": {"reasoningContent": {"text": content["reasoningContent"]["reasoningText"]["text"]}} + "delta": { + "reasoningContent": { + "text": content["reasoningContent"]["reasoningText"][ + "text" + ] + } + } } } @@ -612,7 +685,9 @@ def _convert_non_streaming_to_streaming(self, response: dict[str, Any]) -> Itera "contentBlockDelta": { "delta": { "reasoningContent": { - "signature": content["reasoningContent"]["reasoningText"]["signature"] + "signature": content["reasoningContent"][ + "reasoningText" + ]["signature"] } } } @@ -679,7 +754,11 @@ def _find_detected_and_blocked_policy(self, input: Any) -> bool: # Check if input is a dictionary if isinstance(input, dict): # Check if current dictionary has action: BLOCKED and detected: true - if input.get("action") == "BLOCKED" and input.get("detected") and isinstance(input.get("detected"), bool): + if ( + input.get("action") == "BLOCKED" + and input.get("detected") + and isinstance(input.get("detected"), bool) + ): return True # Recursively check all values in the dictionary @@ -699,7 +778,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,14 +797,21 @@ 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 stop_reason, messages, _, _ = event["stop"] if stop_reason != "tool_use": - raise ValueError(f'Model returned stop_reason: {stop_reason} instead of "tool_use".') + raise ValueError( + f'Model returned stop_reason: {stop_reason} instead of "tool_use".' + ) content = messages["content"] output_response: dict[str, Any] | None = None @@ -734,6 +824,8 @@ async def structured_output( continue if output_response is None: - raise ValueError("No valid tool use or tool use input was found in the Bedrock response.") + raise ValueError( + "No valid tool use or tool use input was found in the Bedrock response." + ) yield {"output": output_model(**output_response)} From b975a79c4a7c25ed215fb10824f42b22a4f12b70 Mon Sep 17 00:00:00 2001 From: aryan835-datainflexion Date: Mon, 11 Aug 2025 16:09:18 +0400 Subject: [PATCH 2/8] Using Message class instead of dict in _strip_reasoning_content_from_message(). --- src/strands/models/bedrock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/strands/models/bedrock.py b/src/strands/models/bedrock.py index d98426951..577f56d61 100644 --- a/src/strands/models/bedrock.py +++ b/src/strands/models/bedrock.py @@ -489,7 +489,7 @@ def callback(event: Optional[StreamEvent] = None) -> None: await task - def _strip_reasoning_content_from_message(self, message: dict) -> dict: + def _strip_reasoning_content_from_message(self, message: Message) -> Message: # Deep copy the message to avoid mutating original import copy From fbaef0ee4fe50d2adce8c3c102ed3b073e9862af Mon Sep 17 00:00:00 2001 From: Dean Schmigelski Date: Tue, 2 Sep 2025 16:37:07 -0400 Subject: [PATCH 3/8] fix(models): filter reasoningContent blocks on Bedrock requests using DeepSeek --- src/strands/models/bedrock.py | 30 ++++++------- tests/strands/models/test_bedrock.py | 66 ++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 16 deletions(-) diff --git a/src/strands/models/bedrock.py b/src/strands/models/bedrock.py index 577f56d61..3efeefe1b 100644 --- a/src/strands/models/bedrock.py +++ b/src/strands/models/bedrock.py @@ -308,6 +308,8 @@ def format_request( ), } + + def _format_bedrock_messages(self, messages: Messages) -> Messages: """Format messages for Bedrock API compatibility. @@ -331,6 +333,8 @@ def _format_bedrock_messages(self, messages: Messages) -> Messages: """ cleaned_messages = [] filtered_unknown_members = False + # TODO: Replace with systematic model configuration registry (https://github.com/strands-agents/sdk-python/issues/780) + is_deepseek = "deepseek" in self.config["model_id"].lower() for message in messages: cleaned_content: list[ContentBlock] = [] @@ -340,7 +344,10 @@ def _format_bedrock_messages(self, messages: Messages) -> Messages: if "SDK_UNKNOWN_MEMBER" in content_block: filtered_unknown_members = True continue - + # DeepSeek models have issues with reasoningContent + if is_deepseek and "reasoningContent" in content_block: + continue + if "toolResult" in content_block: # Create a new content block with only the cleaned toolResult tool_result: ToolResult = content_block["toolResult"] @@ -364,11 +371,12 @@ 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( @@ -489,17 +497,7 @@ def callback(event: Optional[StreamEvent] = None) -> None: await task - def _strip_reasoning_content_from_message(self, message: Message) -> Message: - # Deep copy the message to avoid mutating original - import copy - - msg_copy = copy.deepcopy(message) - content = msg_copy.get("content", []) - # Filter out any content blocks with reasoningContent - filtered_content = [c for c in content if "reasoningContent" not in c] - msg_copy["content"] = filtered_content - return msg_copy def _stream( self, diff --git a/tests/strands/models/test_bedrock.py b/tests/strands/models/test_bedrock.py index 13918b6ea..44d9939b0 100644 --- a/tests/strands/models/test_bedrock.py +++ b/tests/strands/models/test_bedrock.py @@ -1392,3 +1392,69 @@ 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"}] From 871928b8f2fb55b86ef16ff27fbd35f48e1eed27 Mon Sep 17 00:00:00 2001 From: Dean Schmigelski Date: Tue, 2 Sep 2025 16:38:49 -0400 Subject: [PATCH 4/8] fix: formatting and linting --- src/strands/models/bedrock.py | 108 ++++++--------------------- tests/strands/models/test_bedrock.py | 49 +++++------- 2 files changed, 40 insertions(+), 117 deletions(-) diff --git a/src/strands/models/bedrock.py b/src/strands/models/bedrock.py index 3efeefe1b..5706633bb 100644 --- a/src/strands/models/bedrock.py +++ b/src/strands/models/bedrock.py @@ -154,18 +154,11 @@ def __init__( else: new_user_agent = "strands-agents" - client_config = boto_client_config.merge( - BotocoreConfig(user_agent_extra=new_user_agent) - ) + client_config = boto_client_config.merge(BotocoreConfig(user_agent_extra=new_user_agent)) else: client_config = BotocoreConfig(user_agent_extra="strands-agents") - resolved_region = ( - region_name - or session.region_name - or os.environ.get("AWS_REGION") - or DEFAULT_BEDROCK_REGION - ) + resolved_region = region_name or session.region_name or os.environ.get("AWS_REGION") or DEFAULT_BEDROCK_REGION self.client = session.client( service_name="bedrock-runtime", @@ -174,9 +167,7 @@ def __init__( region_name=resolved_region, ) - logger.debug( - "region=<%s> | bedrock client created", self.client.meta.region_name - ) + logger.debug("region=<%s> | bedrock client created", self.client.meta.region_name) @override def update_config(self, **model_config: Unpack[BedrockConfig]) -> None: # type: ignore @@ -228,11 +219,7 @@ def format_request( "messages": self._format_bedrock_messages(messages), "system": [ *([{"text": system_prompt}] if system_prompt else []), - *( - [{"cachePoint": {"type": self.config["cache_prompt"]}}] - if self.config.get("cache_prompt") - else [] - ), + *([{"cachePoint": {"type": self.config["cache_prompt"]}}] if self.config.get("cache_prompt") else []), ], **( { @@ -252,20 +239,12 @@ def format_request( else {} ), **( - { - "additionalModelRequestFields": self.config[ - "additional_request_fields" - ] - } + {"additionalModelRequestFields": self.config["additional_request_fields"]} if self.config.get("additional_request_fields") else {} ), **( - { - "additionalModelResponseFieldPaths": self.config[ - "additional_response_field_paths" - ] - } + {"additionalModelResponseFieldPaths": self.config["additional_response_field_paths"]} if self.config.get("additional_response_field_paths") else {} ), @@ -276,18 +255,13 @@ def format_request( "guardrailVersion": self.config["guardrail_version"], "trace": self.config.get("guardrail_trace", "enabled"), **( - { - "streamProcessingMode": self.config.get( - "guardrail_stream_processing_mode" - ) - } + {"streamProcessingMode": self.config.get("guardrail_stream_processing_mode")} if self.config.get("guardrail_stream_processing_mode") else {} ), } } - if self.config.get("guardrail_id") - and self.config.get("guardrail_version") + if self.config.get("guardrail_id") and self.config.get("guardrail_version") else {} ), "inferenceConfig": { @@ -302,14 +276,11 @@ def format_request( }, **( self.config["additional_args"] - if "additional_args" in self.config - and self.config["additional_args"] is not None + if "additional_args" in self.config and self.config["additional_args"] is not None else {} ), } - - def _format_bedrock_messages(self, messages: Messages) -> Messages: """Format messages for Bedrock API compatibility. @@ -347,7 +318,7 @@ def _format_bedrock_messages(self, messages: Messages) -> Messages: # DeepSeek models have issues with reasoningContent if is_deepseek and "reasoningContent" in content_block: continue - + if "toolResult" in content_block: # Create a new content block with only the cleaned toolResult tool_result: ToolResult = content_block["toolResult"] @@ -373,9 +344,7 @@ def _format_bedrock_messages(self, messages: Messages) -> Messages: # 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_message: Message = Message(content=cleaned_content, role=message["role"]) cleaned_messages.append(cleaned_message) if filtered_unknown_members: @@ -398,17 +367,11 @@ def _has_blocked_guardrail(self, guardrail_data: dict[str, Any]) -> bool: output_assessments = guardrail_data.get("outputAssessments", {}) # Check input assessments - if any( - self._find_detected_and_blocked_policy(assessment) - for assessment in input_assessment.values() - ): + if any(self._find_detected_and_blocked_policy(assessment) for assessment in input_assessment.values()): return True # Check output assessments - if any( - self._find_detected_and_blocked_policy(assessment) - for assessment in output_assessments.values() - ): + if any(self._find_detected_and_blocked_policy(assessment) for assessment in output_assessments.values()): return True return False @@ -483,9 +446,7 @@ def callback(event: Optional[StreamEvent] = None) -> None: loop = asyncio.get_event_loop() queue: asyncio.Queue[Optional[StreamEvent]] = asyncio.Queue() - thread = asyncio.to_thread( - self._stream, callback, messages, tool_specs, system_prompt - ) + thread = asyncio.to_thread(self._stream, callback, messages, tool_specs, system_prompt) task = asyncio.create_task(thread) while True: @@ -497,8 +458,6 @@ def callback(event: Optional[StreamEvent] = None) -> None: await task - - def _stream( self, callback: Callable[..., None], @@ -583,10 +542,7 @@ def _stream( if e.response["Error"]["Code"] == "ThrottlingException": raise ModelThrottledException(error_message) from e - if any( - overflow_message in error_message - for overflow_message in BEDROCK_CONTEXT_WINDOW_OVERFLOW_MESSAGES - ): + if any(overflow_message in error_message for overflow_message in BEDROCK_CONTEXT_WINDOW_OVERFLOW_MESSAGES): logger.warning("bedrock threw context window overflow error") raise ContextWindowOverflowException(e) from e @@ -622,9 +578,7 @@ def _stream( callback() logger.debug("finished streaming response from model") - def _convert_non_streaming_to_streaming( - self, response: dict[str, Any] - ) -> Iterable[StreamEvent]: + def _convert_non_streaming_to_streaming(self, response: dict[str, Any]) -> Iterable[StreamEvent]: """Convert a non-streaming response to the streaming format. Args: @@ -654,9 +608,7 @@ def _convert_non_streaming_to_streaming( # For tool use, we need to yield the input as a delta input_value = json.dumps(content["toolUse"]["input"]) - yield { - "contentBlockDelta": {"delta": {"toolUse": {"input": input_value}}} - } + yield {"contentBlockDelta": {"delta": {"toolUse": {"input": input_value}}}} elif "text" in content: # Then yield the text as a delta yield { @@ -668,13 +620,7 @@ def _convert_non_streaming_to_streaming( # Then yield the reasoning content as a delta yield { "contentBlockDelta": { - "delta": { - "reasoningContent": { - "text": content["reasoningContent"]["reasoningText"][ - "text" - ] - } - } + "delta": {"reasoningContent": {"text": content["reasoningContent"]["reasoningText"]["text"]}} } } @@ -683,9 +629,7 @@ def _convert_non_streaming_to_streaming( "contentBlockDelta": { "delta": { "reasoningContent": { - "signature": content["reasoningContent"][ - "reasoningText" - ]["signature"] + "signature": content["reasoningContent"]["reasoningText"]["signature"] } } } @@ -752,11 +696,7 @@ def _find_detected_and_blocked_policy(self, input: Any) -> bool: # Check if input is a dictionary if isinstance(input, dict): # Check if current dictionary has action: BLOCKED and detected: true - if ( - input.get("action") == "BLOCKED" - and input.get("detected") - and isinstance(input.get("detected"), bool) - ): + if input.get("action") == "BLOCKED" and input.get("detected") and isinstance(input.get("detected"), bool): return True # Recursively check all values in the dictionary @@ -807,9 +747,7 @@ async def structured_output( stop_reason, messages, _, _ = event["stop"] if stop_reason != "tool_use": - raise ValueError( - f'Model returned stop_reason: {stop_reason} instead of "tool_use".' - ) + raise ValueError(f'Model returned stop_reason: {stop_reason} instead of "tool_use".') content = messages["content"] output_response: dict[str, Any] | None = None @@ -822,8 +760,6 @@ async def structured_output( continue if output_response is None: - raise ValueError( - "No valid tool use or tool use input was found in the Bedrock response." - ) + raise ValueError("No valid tool use or tool use input was found in the Bedrock response.") yield {"output": output_model(**output_response)} diff --git a/tests/strands/models/test_bedrock.py b/tests/strands/models/test_bedrock.py index 44d9939b0..f2e459bde 100644 --- a/tests/strands/models/test_bedrock.py +++ b/tests/strands/models/test_bedrock.py @@ -1398,63 +1398,50 @@ def test_format_request_filters_sdk_unknown_member_content_blocks(model, model_i 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": "user", - "content": [{"text": "Hello"}] - }, - { - "role": "assistant", + "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"}] - } + {"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"}] From dbe04d4861a3aa176a8eabd564b53fa1ee72d548 Mon Sep 17 00:00:00 2001 From: Dean Schmigelski Date: Tue, 2 Sep 2025 16:40:05 -0400 Subject: [PATCH 5/8] fix: formatting and linting --- src/strands/models/bedrock.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/strands/models/bedrock.py b/src/strands/models/bedrock.py index 5706633bb..af5584a7b 100644 --- a/src/strands/models/bedrock.py +++ b/src/strands/models/bedrock.py @@ -316,7 +316,8 @@ def _format_bedrock_messages(self, messages: Messages) -> Messages: filtered_unknown_members = True continue # DeepSeek models have issues with reasoningContent - if is_deepseek and "reasoningContent" in content_block: + # 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: continue if "toolResult" in content_block: From c9ad2a09989be63bb2be31152a8496a2ef1f04a4 Mon Sep 17 00:00:00 2001 From: Dean Schmigelski Date: Wed, 3 Sep 2025 10:49:38 -0400 Subject: [PATCH 6/8] remove unrelated registry formatting --- src/strands/tools/registry.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/strands/tools/registry.py b/src/strands/tools/registry.py index 471472a64..6bb76f560 100644 --- a/src/strands/tools/registry.py +++ b/src/strands/tools/registry.py @@ -192,9 +192,9 @@ def register_tool(self, tool: AgentTool) -> None: # Check duplicate tool name, throw on duplicate tool names except if hot_reloading is enabled if tool.tool_name in self.registry and not tool.supports_hot_reload: - raise ValueError( - f"Tool name '{tool.tool_name}' already exists. Cannot register tools with exact same name." - ) + raise ValueError( + f"Tool name '{tool.tool_name}' already exists. Cannot register tools with exact same name." + ) # Check for normalized name conflicts (- vs _) if self.registry.get(tool.tool_name) is None: From d188a90b19aa0e0c2100d1cd0d9bfc73d2d70685 Mon Sep 17 00:00:00 2001 From: Dean Schmigelski Date: Wed, 3 Sep 2025 11:01:02 -0400 Subject: [PATCH 7/8] linting --- src/strands/tools/registry.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/strands/tools/registry.py b/src/strands/tools/registry.py index 6bb76f560..471472a64 100644 --- a/src/strands/tools/registry.py +++ b/src/strands/tools/registry.py @@ -192,9 +192,9 @@ def register_tool(self, tool: AgentTool) -> None: # Check duplicate tool name, throw on duplicate tool names except if hot_reloading is enabled if tool.tool_name in self.registry and not tool.supports_hot_reload: - raise ValueError( - f"Tool name '{tool.tool_name}' already exists. Cannot register tools with exact same name." - ) + raise ValueError( + f"Tool name '{tool.tool_name}' already exists. Cannot register tools with exact same name." + ) # Check for normalized name conflicts (- vs _) if self.registry.get(tool.tool_name) is None: From af0ee2997552af275e7cd75494d953a95cea36bd Mon Sep 17 00:00:00 2001 From: Dean Schmigelski Date: Thu, 4 Sep 2025 14:27:34 -0400 Subject: [PATCH 8/8] add log --- src/strands/models/bedrock.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/strands/models/bedrock.py b/src/strands/models/bedrock.py index af5584a7b..aa19b114d 100644 --- a/src/strands/models/bedrock.py +++ b/src/strands/models/bedrock.py @@ -7,17 +7,7 @@ import json import logging import os -from typing import ( - Any, - AsyncGenerator, - Callable, - Iterable, - Literal, - Optional, - Type, - TypeVar, - Union, -) +from typing import Any, AsyncGenerator, Callable, Iterable, Literal, Optional, Type, TypeVar, Union, cast import boto3 from botocore.config import Config as BotocoreConfig @@ -303,9 +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 - # TODO: Replace with systematic model configuration registry (https://github.com/strands-agents/sdk-python/issues/780) - is_deepseek = "deepseek" in self.config["model_id"].lower() + dropped_deepseek_reasoning_content = False for message in messages: cleaned_content: list[ContentBlock] = [] @@ -315,9 +305,11 @@ def _format_bedrock_messages(self, messages: Messages) -> Messages: if "SDK_UNKNOWN_MEMBER" in content_block: 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: @@ -352,6 +344,10 @@ def _format_bedrock_messages(self, messages: Messages) -> Messages: 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