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\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") {