Skip to content

Commit bce2464

Browse files
fix(bedrock): disable thinking mode when forcing tool_choice (#1495)
--------- Co-authored-by: Dean Schmigelski <dbschmigelski+github@gmail.com>
1 parent 5e733ef commit bce2464

File tree

3 files changed

+150
-5
lines changed

3 files changed

+150
-5
lines changed

src/strands/models/bedrock.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -255,11 +255,7 @@ def _format_request(
255255
if tool_specs
256256
else {}
257257
),
258-
**(
259-
{"additionalModelRequestFields": self.config["additional_request_fields"]}
260-
if self.config.get("additional_request_fields")
261-
else {}
262-
),
258+
**(self._get_additional_request_fields(tool_choice)),
263259
**(
264260
{"additionalModelResponseFieldPaths": self.config["additional_response_field_paths"]}
265261
if self.config.get("additional_response_field_paths")
@@ -298,6 +294,34 @@ def _format_request(
298294
),
299295
}
300296

297+
def _get_additional_request_fields(self, tool_choice: ToolChoice | None) -> dict[str, Any]:
298+
"""Get additional request fields, removing thinking if tool_choice forces tool use.
299+
300+
Bedrock's API does not allow thinking mode when tool_choice forces tool use.
301+
When forcing a tool (e.g., for structured_output retry), we temporarily disable thinking.
302+
303+
Args:
304+
tool_choice: The tool choice configuration.
305+
306+
Returns:
307+
A dict containing additionalModelRequestFields if configured, or empty dict.
308+
"""
309+
additional_fields = self.config.get("additional_request_fields")
310+
if not additional_fields:
311+
return {}
312+
313+
# Check if tool_choice is forcing tool use ("any" or specific "tool")
314+
is_forcing_tool = tool_choice is not None and ("any" in tool_choice or "tool" in tool_choice)
315+
316+
if is_forcing_tool and "thinking" in additional_fields:
317+
# Create a copy without the thinking key
318+
fields_without_thinking = {k: v for k, v in additional_fields.items() if k != "thinking"}
319+
if fields_without_thinking:
320+
return {"additionalModelRequestFields": fields_without_thinking}
321+
return {}
322+
323+
return {"additionalModelRequestFields": additional_fields}
324+
301325
def _format_bedrock_messages(self, messages: Messages) -> list[dict[str, Any]]:
302326
"""Format messages for Bedrock API compatibility.
303327
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"""Tests for thinking mode behavior in BedrockModel."""
2+
3+
import pytest
4+
5+
from strands.models.bedrock import BedrockModel
6+
7+
8+
@pytest.fixture
9+
def model_with_thinking():
10+
"""Create a BedrockModel with thinking enabled."""
11+
return BedrockModel(
12+
model_id="anthropic.claude-sonnet-4-20250514-v1:0",
13+
additional_request_fields={"thinking": {"type": "enabled", "budget_tokens": 5000}},
14+
)
15+
16+
17+
@pytest.fixture
18+
def model_without_thinking():
19+
"""Create a BedrockModel without thinking."""
20+
return BedrockModel(model_id="anthropic.claude-sonnet-4-20250514-v1:0")
21+
22+
23+
@pytest.fixture
24+
def model_with_thinking_and_other_fields():
25+
"""Create a BedrockModel with thinking and other additional fields."""
26+
return BedrockModel(
27+
model_id="anthropic.claude-sonnet-4-20250514-v1:0",
28+
additional_request_fields={
29+
"thinking": {"type": "enabled", "budget_tokens": 5000},
30+
"some_other_field": "value",
31+
},
32+
)
33+
34+
35+
def test_thinking_removed_when_forcing_tool_any(model_with_thinking):
36+
"""Thinking should be removed when tool_choice forces tool use with 'any'."""
37+
tool_choice = {"any": {}}
38+
result = model_with_thinking._get_additional_request_fields(tool_choice)
39+
assert result == {} # thinking removed, no other fields
40+
41+
42+
def test_thinking_removed_when_forcing_specific_tool(model_with_thinking):
43+
"""Thinking should be removed when tool_choice forces a specific tool."""
44+
tool_choice = {"tool": {"name": "structured_output_tool"}}
45+
result = model_with_thinking._get_additional_request_fields(tool_choice)
46+
assert result == {} # thinking removed, no other fields
47+
48+
49+
def test_thinking_preserved_with_auto_tool_choice(model_with_thinking):
50+
"""Thinking should be preserved when tool_choice is 'auto'."""
51+
tool_choice = {"auto": {}}
52+
result = model_with_thinking._get_additional_request_fields(tool_choice)
53+
assert result == {"additionalModelRequestFields": {"thinking": {"type": "enabled", "budget_tokens": 5000}}}
54+
55+
56+
def test_thinking_preserved_with_none_tool_choice(model_with_thinking):
57+
"""Thinking should be preserved when tool_choice is None."""
58+
result = model_with_thinking._get_additional_request_fields(None)
59+
assert result == {"additionalModelRequestFields": {"thinking": {"type": "enabled", "budget_tokens": 5000}}}
60+
61+
62+
def test_other_fields_preserved_when_thinking_removed(model_with_thinking_and_other_fields):
63+
"""Other additional fields should be preserved when thinking is removed."""
64+
tool_choice = {"any": {}}
65+
result = model_with_thinking_and_other_fields._get_additional_request_fields(tool_choice)
66+
assert result == {"additionalModelRequestFields": {"some_other_field": "value"}}
67+
68+
69+
def test_no_fields_when_model_has_no_additional_fields(model_without_thinking):
70+
"""Should return empty dict when model has no additional_request_fields."""
71+
tool_choice = {"any": {}}
72+
result = model_without_thinking._get_additional_request_fields(tool_choice)
73+
assert result == {}
74+
75+
76+
def test_fields_preserved_when_no_thinking_and_forcing_tool():
77+
"""Additional fields without thinking should be preserved when forcing tool."""
78+
model = BedrockModel(
79+
model_id="anthropic.claude-sonnet-4-20250514-v1:0",
80+
additional_request_fields={"some_field": "value"},
81+
)
82+
tool_choice = {"any": {}}
83+
result = model._get_additional_request_fields(tool_choice)
84+
assert result == {"additionalModelRequestFields": {"some_field": "value"}}

tests_integ/models/test_model_bedrock.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,43 @@ def test_redacted_content_handling():
275275
assert isinstance(result.message["content"][0]["reasoningContent"]["redactedContent"], bytes)
276276

277277

278+
def test_reasoning_content_in_messages_with_thinking_disabled():
279+
"""Test that messages with reasoningContent are accepted when thinking is explicitly disabled."""
280+
# First, get a real reasoning response with thinking enabled
281+
thinking_model = BedrockModel(
282+
model_id="us.anthropic.claude-sonnet-4-20250514-v1:0",
283+
additional_request_fields={
284+
"thinking": {
285+
"type": "enabled",
286+
"budget_tokens": 1024,
287+
}
288+
},
289+
)
290+
agent_with_thinking = Agent(model=thinking_model)
291+
result_with_thinking = agent_with_thinking("What is 2+2?")
292+
293+
# Verify we got reasoning content
294+
assert "reasoningContent" in result_with_thinking.message["content"][0]
295+
296+
# Now create a model with thinking disabled and use the messages from the thinking session
297+
disabled_model = BedrockModel(
298+
model_id="us.anthropic.claude-sonnet-4-20250514-v1:0",
299+
additional_request_fields={
300+
"thinking": {
301+
"type": "disabled",
302+
}
303+
},
304+
)
305+
306+
# Use the conversation history that includes reasoning content
307+
messages = agent_with_thinking.messages
308+
309+
agent_disabled = Agent(model=disabled_model, messages=messages)
310+
result = agent_disabled("What about 3+3?")
311+
312+
assert result.stop_reason == "end_turn"
313+
314+
278315
def test_multi_prompt_system_content():
279316
"""Test multi-prompt system content blocks."""
280317
system_prompt_content = [

0 commit comments

Comments
 (0)