bedrock: Add placeholder tool when summarising threads with tool history (#48863)

David Turnbull created

Bedrock Converse API requires toolConfig whenever messages have tool use
or result blocks. PR #33174 handled the case where tools are defined but
tool_choice is None. This change addresses another case: the tools array
is empty (e.g. thread summarisation) but messages still have tool blocks
from conversation history. A placeholder tool satisfies the requirement.

Testing:

Tested manually by:
1. Starting a conversation with Bedrock that uses tools
2. Triggering thread summarisation
3. Confirming summarisation now succeeds instead of failing with an API
error

Release Notes:

- Fixed Bedrock thread summarization failing when conversation had tools

Change summary

crates/language_models/src/provider/bedrock.rs | 23 ++++++++++++++++++++
1 file changed, 23 insertions(+)

Detailed changes

crates/language_models/src/provider/bedrock.rs 🔗

@@ -776,6 +776,10 @@ pub fn into_bedrock(
     let mut new_messages: Vec<BedrockMessage> = Vec::new();
     let mut system_message = String::new();
 
+    // Track whether messages contain tool content - Bedrock requires toolConfig
+    // when tool blocks are present, so we may need to add a dummy tool
+    let mut messages_contain_tool_content = false;
+
     for message in request.messages {
         if message.contents_empty() {
             continue;
@@ -829,6 +833,7 @@ pub fn into_bedrock(
                             Some(BedrockInnerContent::ReasoningContent(redacted))
                         }
                         MessageContent::ToolUse(tool_use) => {
+                            messages_contain_tool_content = true;
                             let input = if tool_use.input.is_null() {
                                 // Bedrock API requires valid JsonValue, not null, for tool use input
                                 value_to_aws_document(&serde_json::json!({}))
@@ -845,6 +850,7 @@ pub fn into_bedrock(
                                 .map(BedrockInnerContent::ToolUse)
                         }
                         MessageContent::ToolResult(tool_result) => {
+                            messages_contain_tool_content = true;
                             BedrockToolResultBlock::builder()
                                 .tool_use_id(tool_result.tool_use_id.to_string())
                                 .content(match tool_result.content {
@@ -976,6 +982,23 @@ pub fn into_bedrock(
         })
         .collect();
 
+    // Bedrock requires toolConfig when messages contain tool use/result blocks.
+    // If no tools are defined but messages contain tool content (e.g., when
+    // summarising a conversation that used tools), add a dummy tool to satisfy
+    // the API requirement.
+    if tool_spec.is_empty() && messages_contain_tool_content {
+        tool_spec.push(BedrockTool::ToolSpec(
+            BedrockToolSpec::builder()
+                .name("_placeholder")
+                .description("Placeholder tool to satisfy Bedrock API requirements when conversation history contains tool usage")
+                .input_schema(BedrockToolInputSchema::Json(value_to_aws_document(
+                    &serde_json::json!({"type": "object", "properties": {}}),
+                )))
+                .build()
+                .context("failed to build placeholder tool spec")?,
+        ));
+    }
+
     if !tool_spec.is_empty() && supports_caching {
         tool_spec.push(BedrockTool::CachePoint(
             CachePointBlock::builder()