Skip to content

Commit 531434c

Browse files
committed
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
1 parent cc3bc35 commit 531434c

File tree

2 files changed

+167
-5
lines changed

2 files changed

+167
-5
lines changed

src/api/transform/__tests__/bedrock-converse-format.spec.ts

Lines changed: 123 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,10 @@ describe("convertToBedrockConverseMessages", () => {
141141
}
142142
})
143143

144-
it("converts tool result messages correctly", () => {
144+
it("converts tool result messages to XML text format (default, useNativeTools: false)", () => {
145145
const messages: Anthropic.Messages.MessageParam[] = [
146146
{
147-
role: "assistant",
147+
role: "user",
148148
content: [
149149
{
150150
type: "tool_result",
@@ -155,14 +155,50 @@ describe("convertToBedrockConverseMessages", () => {
155155
},
156156
]
157157

158+
// Default behavior (useNativeTools: false) converts tool_result to XML text format
159+
// This fixes the Bedrock error "toolConfig field must be defined when using toolUse and toolResult content blocks"
158160
const result = convertToBedrockConverseMessages(messages)
159161

160162
if (!result[0] || !result[0].content) {
161163
expect.fail("Expected result to have content")
162164
return
163165
}
164166

165-
expect(result[0].role).toBe("assistant")
167+
expect(result[0].role).toBe("user")
168+
const textBlock = result[0].content[0] as ContentBlock
169+
if ("text" in textBlock) {
170+
expect(textBlock.text).toContain("<tool_result>")
171+
expect(textBlock.text).toContain("<tool_use_id>test-id</tool_use_id>")
172+
expect(textBlock.text).toContain("File contents here")
173+
expect(textBlock.text).toContain("</tool_result>")
174+
} else {
175+
expect.fail("Expected text block with XML content not found")
176+
}
177+
})
178+
179+
it("converts tool result messages to native format (useNativeTools: true)", () => {
180+
const messages: Anthropic.Messages.MessageParam[] = [
181+
{
182+
role: "user",
183+
content: [
184+
{
185+
type: "tool_result",
186+
tool_use_id: "test-id",
187+
content: [{ type: "text", text: "File contents here" }],
188+
},
189+
],
190+
},
191+
]
192+
193+
// With useNativeTools: true, keeps tool_result as native format
194+
const result = convertToBedrockConverseMessages(messages, { useNativeTools: true })
195+
196+
if (!result[0] || !result[0].content) {
197+
expect.fail("Expected result to have content")
198+
return
199+
}
200+
201+
expect(result[0].role).toBe("user")
166202
const resultBlock = result[0].content[0] as ContentBlock
167203
if ("toolResult" in resultBlock && resultBlock.toolResult) {
168204
const expectedContent: ToolResultContentBlock[] = [{ text: "File contents here" }]
@@ -176,7 +212,7 @@ describe("convertToBedrockConverseMessages", () => {
176212
}
177213
})
178214

179-
it("converts tool result messages with string content correctly", () => {
215+
it("converts tool result messages with string content to XML text format (default)", () => {
180216
const messages: Anthropic.Messages.MessageParam[] = [
181217
{
182218
role: "user",
@@ -197,6 +233,39 @@ describe("convertToBedrockConverseMessages", () => {
197233
return
198234
}
199235

236+
expect(result[0].role).toBe("user")
237+
const textBlock = result[0].content[0] as ContentBlock
238+
if ("text" in textBlock) {
239+
expect(textBlock.text).toContain("<tool_result>")
240+
expect(textBlock.text).toContain("<tool_use_id>test-id</tool_use_id>")
241+
expect(textBlock.text).toContain("File: test.txt")
242+
expect(textBlock.text).toContain("Hello World")
243+
} else {
244+
expect.fail("Expected text block with XML content not found")
245+
}
246+
})
247+
248+
it("converts tool result messages with string content to native format (useNativeTools: true)", () => {
249+
const messages: Anthropic.Messages.MessageParam[] = [
250+
{
251+
role: "user",
252+
content: [
253+
{
254+
type: "tool_result",
255+
tool_use_id: "test-id",
256+
content: "File: test.txt\nLines 1-5:\nHello World",
257+
} as any, // Anthropic types don't allow string content but runtime can have it
258+
],
259+
},
260+
]
261+
262+
const result = convertToBedrockConverseMessages(messages, { useNativeTools: true })
263+
264+
if (!result[0] || !result[0].content) {
265+
expect.fail("Expected result to have content")
266+
return
267+
}
268+
200269
expect(result[0].role).toBe("user")
201270
const resultBlock = result[0].content[0] as ContentBlock
202271
if ("toolResult" in resultBlock && resultBlock.toolResult) {
@@ -210,6 +279,56 @@ describe("convertToBedrockConverseMessages", () => {
210279
}
211280
})
212281

282+
it("converts both tool_use and tool_result consistently when native tools disabled", () => {
283+
// This test ensures tool_use AND tool_result are both converted to XML text
284+
// when useNativeTools is false, preventing Bedrock toolConfig errors
285+
const messages: Anthropic.Messages.MessageParam[] = [
286+
{
287+
role: "assistant",
288+
content: [
289+
{
290+
type: "tool_use",
291+
id: "call-123",
292+
name: "read_file",
293+
input: { path: "test.txt" },
294+
},
295+
],
296+
},
297+
{
298+
role: "user",
299+
content: [
300+
{
301+
type: "tool_result",
302+
tool_use_id: "call-123",
303+
content: "File contents here",
304+
} as any,
305+
],
306+
},
307+
]
308+
309+
const result = convertToBedrockConverseMessages(messages) // default useNativeTools: false
310+
311+
// Both should be text blocks, not native toolUse/toolResult
312+
const assistantContent = result[0]?.content?.[0] as ContentBlock
313+
const userContent = result[1]?.content?.[0] as ContentBlock
314+
315+
// tool_use should be XML text
316+
expect("text" in assistantContent).toBe(true)
317+
if ("text" in assistantContent) {
318+
expect(assistantContent.text).toContain("<tool_use>")
319+
}
320+
321+
// tool_result should also be XML text (this is what the fix addresses)
322+
expect("text" in userContent).toBe(true)
323+
if ("text" in userContent) {
324+
expect(userContent.text).toContain("<tool_result>")
325+
}
326+
327+
// Neither should have native format
328+
expect("toolUse" in assistantContent).toBe(false)
329+
expect("toolResult" in userContent).toBe(false)
330+
})
331+
213332
it("handles text content correctly", () => {
214333
const messages: Anthropic.Messages.MessageParam[] = [
215334
{

src/api/transform/bedrock-converse-format.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,50 @@ export function convertToBedrockConverseMessages(
111111
}
112112

113113
if (messageBlock.type === "tool_result") {
114-
// Handle content field - can be string or array
114+
// When NOT using native tools, convert tool_result to text format
115+
// This matches how tool_use is converted to XML text when native tools are disabled.
116+
// Without this, Bedrock will error with "toolConfig field must be defined when using
117+
// toolUse and toolResult content blocks" because toolResult blocks require toolConfig.
118+
if (!useNativeTools) {
119+
let toolResultContent: string
120+
if (messageBlock.content) {
121+
if (typeof messageBlock.content === "string") {
122+
toolResultContent = messageBlock.content
123+
} else if (Array.isArray(messageBlock.content)) {
124+
toolResultContent = messageBlock.content
125+
.map((item) => (typeof item === "string" ? item : item.text || String(item)))
126+
.join("\n")
127+
} else {
128+
toolResultContent = String(messageBlock.output || "")
129+
}
130+
} else if (messageBlock.output) {
131+
if (typeof messageBlock.output === "string") {
132+
toolResultContent = messageBlock.output
133+
} else if (Array.isArray(messageBlock.output)) {
134+
toolResultContent = messageBlock.output
135+
.map((part) => {
136+
if (typeof part === "object" && "text" in part) {
137+
return part.text
138+
}
139+
if (typeof part === "object" && "type" in part && part.type === "image") {
140+
return "(see following message for image)"
141+
}
142+
return String(part)
143+
})
144+
.join("\n")
145+
} else {
146+
toolResultContent = String(messageBlock.output)
147+
}
148+
} else {
149+
toolResultContent = ""
150+
}
151+
152+
return {
153+
text: `<tool_result>\n<tool_use_id>${messageBlock.tool_use_id || ""}</tool_use_id>\n<output>${toolResultContent}</output>\n</tool_result>`,
154+
} as ContentBlock
155+
}
156+
157+
// Handle content field - can be string or array (native tool format)
115158
if (messageBlock.content) {
116159
// Content is a string
117160
if (typeof messageBlock.content === "string") {

0 commit comments

Comments
 (0)