From 531434cabc2929995e91a2b320c54f55e2d7c558 Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Wed, 17 Dec 2025 11:32:28 -0500 Subject: [PATCH] fix(bedrock): convert tool_result to XML text when native tools disabled Fixes Bedrock error 'toolConfig field must be defined when using toolUse and toolResult content blocks' The issue occurred because tool_result blocks were always converted to Bedrock's native toolResult format, regardless of the useNativeTools flag. This was inconsistent with tool_use handling which correctly converted to XML text when native tools were disabled. When resuming tasks with tool blocks in history but native tools disabled (empty tools array, tool_choice: none, model change, etc.), Bedrock's API would reject the request because toolResult blocks were present without a toolConfig. Changes: - Add useNativeTools check for tool_result handling in bedrock-converse-format.ts - When useNativeTools is false: convert tool_result to XML text format - When useNativeTools is true: keep native toolResult format (existing behavior) - Update tests to cover both conversion paths PostHog issue: https://us.posthog.com/error_tracking/019b2c9f-7232-7a13-b1c3-50a55df08310 --- .../__tests__/bedrock-converse-format.spec.ts | 127 +++++++++++++++++- src/api/transform/bedrock-converse-format.ts | 45 ++++++- 2 files changed, 167 insertions(+), 5 deletions(-) diff --git a/src/api/transform/__tests__/bedrock-converse-format.spec.ts b/src/api/transform/__tests__/bedrock-converse-format.spec.ts index c0e3e9103d6..7daf186f478 100644 --- a/src/api/transform/__tests__/bedrock-converse-format.spec.ts +++ b/src/api/transform/__tests__/bedrock-converse-format.spec.ts @@ -141,10 +141,10 @@ describe("convertToBedrockConverseMessages", () => { } }) - it("converts tool result messages correctly", () => { + it("converts tool result messages to XML text format (default, useNativeTools: false)", () => { const messages: Anthropic.Messages.MessageParam[] = [ { - role: "assistant", + role: "user", content: [ { type: "tool_result", @@ -155,6 +155,8 @@ describe("convertToBedrockConverseMessages", () => { }, ] + // Default behavior (useNativeTools: false) converts tool_result to XML text format + // This fixes the Bedrock error "toolConfig field must be defined when using toolUse and toolResult content blocks" const result = convertToBedrockConverseMessages(messages) if (!result[0] || !result[0].content) { @@ -162,7 +164,41 @@ describe("convertToBedrockConverseMessages", () => { return } - expect(result[0].role).toBe("assistant") + expect(result[0].role).toBe("user") + const textBlock = result[0].content[0] as ContentBlock + if ("text" in textBlock) { + expect(textBlock.text).toContain("") + expect(textBlock.text).toContain("test-id") + expect(textBlock.text).toContain("File contents here") + expect(textBlock.text).toContain("") + } else { + expect.fail("Expected text block with XML content not found") + } + }) + + it("converts tool result messages to native format (useNativeTools: true)", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "test-id", + content: [{ type: "text", text: "File contents here" }], + }, + ], + }, + ] + + // With useNativeTools: true, keeps tool_result as native format + const result = convertToBedrockConverseMessages(messages, { useNativeTools: true }) + + if (!result[0] || !result[0].content) { + expect.fail("Expected result to have content") + return + } + + expect(result[0].role).toBe("user") const resultBlock = result[0].content[0] as ContentBlock if ("toolResult" in resultBlock && resultBlock.toolResult) { const expectedContent: ToolResultContentBlock[] = [{ text: "File contents here" }] @@ -176,7 +212,7 @@ describe("convertToBedrockConverseMessages", () => { } }) - it("converts tool result messages with string content correctly", () => { + it("converts tool result messages with string content to XML text format (default)", () => { const messages: Anthropic.Messages.MessageParam[] = [ { role: "user", @@ -197,6 +233,39 @@ describe("convertToBedrockConverseMessages", () => { return } + expect(result[0].role).toBe("user") + const textBlock = result[0].content[0] as ContentBlock + if ("text" in textBlock) { + expect(textBlock.text).toContain("") + expect(textBlock.text).toContain("test-id") + expect(textBlock.text).toContain("File: test.txt") + expect(textBlock.text).toContain("Hello World") + } else { + expect.fail("Expected text block with XML content not found") + } + }) + + it("converts tool result messages with string content to native format (useNativeTools: true)", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "test-id", + content: "File: test.txt\nLines 1-5:\nHello World", + } as any, // Anthropic types don't allow string content but runtime can have it + ], + }, + ] + + const result = convertToBedrockConverseMessages(messages, { useNativeTools: true }) + + if (!result[0] || !result[0].content) { + expect.fail("Expected result to have content") + return + } + expect(result[0].role).toBe("user") const resultBlock = result[0].content[0] as ContentBlock if ("toolResult" in resultBlock && resultBlock.toolResult) { @@ -210,6 +279,56 @@ describe("convertToBedrockConverseMessages", () => { } }) + it("converts both tool_use and tool_result consistently when native tools disabled", () => { + // This test ensures tool_use AND tool_result are both converted to XML text + // when useNativeTools is false, preventing Bedrock toolConfig errors + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { + type: "tool_use", + id: "call-123", + name: "read_file", + input: { path: "test.txt" }, + }, + ], + }, + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "call-123", + content: "File contents here", + } as any, + ], + }, + ] + + const result = convertToBedrockConverseMessages(messages) // default useNativeTools: false + + // Both should be text blocks, not native toolUse/toolResult + const assistantContent = result[0]?.content?.[0] as ContentBlock + const userContent = result[1]?.content?.[0] as ContentBlock + + // tool_use should be XML text + expect("text" in assistantContent).toBe(true) + if ("text" in assistantContent) { + expect(assistantContent.text).toContain("") + } + + // tool_result should also be XML text (this is what the fix addresses) + expect("text" in userContent).toBe(true) + if ("text" in userContent) { + expect(userContent.text).toContain("") + } + + // Neither should have native format + expect("toolUse" in assistantContent).toBe(false) + expect("toolResult" in userContent).toBe(false) + }) + it("handles text content correctly", () => { const messages: Anthropic.Messages.MessageParam[] = [ { diff --git a/src/api/transform/bedrock-converse-format.ts b/src/api/transform/bedrock-converse-format.ts index b6f9b7232a9..1a8e49a20ba 100644 --- a/src/api/transform/bedrock-converse-format.ts +++ b/src/api/transform/bedrock-converse-format.ts @@ -111,7 +111,50 @@ export function convertToBedrockConverseMessages( } if (messageBlock.type === "tool_result") { - // Handle content field - can be string or array + // When NOT using native tools, convert tool_result to text format + // This matches how tool_use is converted to XML text when native tools are disabled. + // Without this, Bedrock will error with "toolConfig field must be defined when using + // toolUse and toolResult content blocks" because toolResult blocks require toolConfig. + if (!useNativeTools) { + let toolResultContent: string + if (messageBlock.content) { + if (typeof messageBlock.content === "string") { + toolResultContent = messageBlock.content + } else if (Array.isArray(messageBlock.content)) { + toolResultContent = messageBlock.content + .map((item) => (typeof item === "string" ? item : item.text || String(item))) + .join("\n") + } else { + toolResultContent = String(messageBlock.output || "") + } + } else if (messageBlock.output) { + if (typeof messageBlock.output === "string") { + toolResultContent = messageBlock.output + } else if (Array.isArray(messageBlock.output)) { + toolResultContent = messageBlock.output + .map((part) => { + if (typeof part === "object" && "text" in part) { + return part.text + } + if (typeof part === "object" && "type" in part && part.type === "image") { + return "(see following message for image)" + } + return String(part) + }) + .join("\n") + } else { + toolResultContent = String(messageBlock.output) + } + } else { + toolResultContent = "" + } + + return { + text: `\n${messageBlock.tool_use_id || ""}\n${toolResultContent}\n`, + } as ContentBlock + } + + // Handle content field - can be string or array (native tool format) if (messageBlock.content) { // Content is a string if (typeof messageBlock.content === "string") {