fix(bedrock): convert tool_result to XML text when native tools disabled (#10155)

This commit is contained in:
Daniel 2025-12-17 11:46:34 -05:00 committed by GitHub
parent 752a95087e
commit 4693b9eb06
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 167 additions and 5 deletions

View file

@ -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("<tool_result>")
expect(textBlock.text).toContain("<tool_use_id>test-id</tool_use_id>")
expect(textBlock.text).toContain("File contents here")
expect(textBlock.text).toContain("</tool_result>")
} 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("<tool_result>")
expect(textBlock.text).toContain("<tool_use_id>test-id</tool_use_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_use>")
}
// 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("<tool_result>")
}
// 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[] = [
{

View file

@ -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: `<tool_result>\n<tool_use_id>${messageBlock.tool_use_id || ""}</tool_use_id>\n<output>${toolResultContent}</output>\n</tool_result>`,
} 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") {