Skip to content

Commit 7d9a7d5

Browse files
daniel-lxsMy Name
authored andcommitted
fix: truncate AWS Bedrock toolUseId to 64 characters (RooCodeInc#10902)
1 parent 53f924b commit 7d9a7d5

2 files changed

Lines changed: 222 additions & 6 deletions

File tree

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

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { convertToBedrockConverseMessages } from "../bedrock-converse-format"
44
import { Anthropic } from "@anthropic-ai/sdk"
55
import { ContentBlock, ToolResultContentBlock } from "@aws-sdk/client-bedrock-runtime"
6+
import { OPENAI_CALL_ID_MAX_LENGTH } from "../../../utils/tool-id"
67

78
describe("convertToBedrockConverseMessages", () => {
89
it("converts simple text messages correctly", () => {
@@ -341,4 +342,218 @@ describe("convertToBedrockConverseMessages", () => {
341342
const textBlock = result[0].content[0] as ContentBlock
342343
expect(textBlock).toEqual({ text: "Hello world" })
343344
})
345+
346+
describe("toolUseId sanitization for Bedrock 64-char limit", () => {
347+
it("truncates toolUseId longer than 64 characters in tool_use blocks", () => {
348+
const longId = "a".repeat(100)
349+
const messages: Anthropic.Messages.MessageParam[] = [
350+
{
351+
role: "assistant",
352+
content: [
353+
{
354+
type: "tool_use",
355+
id: longId,
356+
name: "read_file",
357+
input: { path: "test.txt" },
358+
},
359+
],
360+
},
361+
]
362+
363+
const result = convertToBedrockConverseMessages(messages)
364+
const toolBlock = result[0]?.content?.[0] as ContentBlock
365+
366+
if ("toolUse" in toolBlock && toolBlock.toolUse && toolBlock.toolUse.toolUseId) {
367+
expect(toolBlock.toolUse.toolUseId.length).toBeLessThanOrEqual(OPENAI_CALL_ID_MAX_LENGTH)
368+
expect(toolBlock.toolUse.toolUseId.length).toBe(OPENAI_CALL_ID_MAX_LENGTH)
369+
expect(toolBlock.toolUse.toolUseId).toContain("_")
370+
} else {
371+
expect.fail("Expected tool use block not found")
372+
}
373+
})
374+
375+
it("truncates toolUseId longer than 64 characters in tool_result blocks with string content", () => {
376+
const longId = "b".repeat(100)
377+
const messages: Anthropic.Messages.MessageParam[] = [
378+
{
379+
role: "user",
380+
content: [
381+
{
382+
type: "tool_result",
383+
tool_use_id: longId,
384+
content: "Result content",
385+
} as any,
386+
],
387+
},
388+
]
389+
390+
const result = convertToBedrockConverseMessages(messages)
391+
const resultBlock = result[0]?.content?.[0] as ContentBlock
392+
393+
if ("toolResult" in resultBlock && resultBlock.toolResult && resultBlock.toolResult.toolUseId) {
394+
expect(resultBlock.toolResult.toolUseId.length).toBeLessThanOrEqual(OPENAI_CALL_ID_MAX_LENGTH)
395+
expect(resultBlock.toolResult.toolUseId.length).toBe(OPENAI_CALL_ID_MAX_LENGTH)
396+
expect(resultBlock.toolResult.toolUseId).toContain("_")
397+
} else {
398+
expect.fail("Expected tool result block not found")
399+
}
400+
})
401+
402+
it("truncates toolUseId longer than 64 characters in tool_result blocks with array content", () => {
403+
const longId = "c".repeat(100)
404+
const messages: Anthropic.Messages.MessageParam[] = [
405+
{
406+
role: "user",
407+
content: [
408+
{
409+
type: "tool_result",
410+
tool_use_id: longId,
411+
content: [{ type: "text", text: "Result content" }],
412+
},
413+
],
414+
},
415+
]
416+
417+
const result = convertToBedrockConverseMessages(messages)
418+
const resultBlock = result[0]?.content?.[0] as ContentBlock
419+
420+
if ("toolResult" in resultBlock && resultBlock.toolResult && resultBlock.toolResult.toolUseId) {
421+
expect(resultBlock.toolResult.toolUseId.length).toBeLessThanOrEqual(OPENAI_CALL_ID_MAX_LENGTH)
422+
expect(resultBlock.toolResult.toolUseId.length).toBe(OPENAI_CALL_ID_MAX_LENGTH)
423+
} else {
424+
expect.fail("Expected tool result block not found")
425+
}
426+
})
427+
428+
it("keeps toolUseId unchanged when under 64 characters", () => {
429+
const shortId = "short-id-123"
430+
const messages: Anthropic.Messages.MessageParam[] = [
431+
{
432+
role: "assistant",
433+
content: [
434+
{
435+
type: "tool_use",
436+
id: shortId,
437+
name: "read_file",
438+
input: { path: "test.txt" },
439+
},
440+
],
441+
},
442+
]
443+
444+
const result = convertToBedrockConverseMessages(messages)
445+
const toolBlock = result[0]?.content?.[0] as ContentBlock
446+
447+
if ("toolUse" in toolBlock && toolBlock.toolUse) {
448+
expect(toolBlock.toolUse.toolUseId).toBe(shortId)
449+
} else {
450+
expect.fail("Expected tool use block not found")
451+
}
452+
})
453+
454+
it("produces consistent truncated IDs for the same input", () => {
455+
const longId = "d".repeat(100)
456+
const messages: Anthropic.Messages.MessageParam[] = [
457+
{
458+
role: "assistant",
459+
content: [
460+
{
461+
type: "tool_use",
462+
id: longId,
463+
name: "read_file",
464+
input: { path: "test.txt" },
465+
},
466+
],
467+
},
468+
]
469+
470+
const result1 = convertToBedrockConverseMessages(messages)
471+
const result2 = convertToBedrockConverseMessages(messages)
472+
473+
const toolBlock1 = result1[0]?.content?.[0] as ContentBlock
474+
const toolBlock2 = result2[0]?.content?.[0] as ContentBlock
475+
476+
if ("toolUse" in toolBlock1 && toolBlock1.toolUse && "toolUse" in toolBlock2 && toolBlock2.toolUse) {
477+
expect(toolBlock1.toolUse.toolUseId).toBe(toolBlock2.toolUse.toolUseId)
478+
} else {
479+
expect.fail("Expected tool use blocks not found")
480+
}
481+
})
482+
483+
it("produces different truncated IDs for different long inputs", () => {
484+
const longId1 = "e".repeat(100)
485+
const longId2 = "f".repeat(100)
486+
487+
const messages1: Anthropic.Messages.MessageParam[] = [
488+
{
489+
role: "assistant",
490+
content: [{ type: "tool_use", id: longId1, name: "read_file", input: {} }],
491+
},
492+
]
493+
const messages2: Anthropic.Messages.MessageParam[] = [
494+
{
495+
role: "assistant",
496+
content: [{ type: "tool_use", id: longId2, name: "read_file", input: {} }],
497+
},
498+
]
499+
500+
const result1 = convertToBedrockConverseMessages(messages1)
501+
const result2 = convertToBedrockConverseMessages(messages2)
502+
503+
const toolBlock1 = result1[0]?.content?.[0] as ContentBlock
504+
const toolBlock2 = result2[0]?.content?.[0] as ContentBlock
505+
506+
if ("toolUse" in toolBlock1 && toolBlock1.toolUse && "toolUse" in toolBlock2 && toolBlock2.toolUse) {
507+
expect(toolBlock1.toolUse.toolUseId).not.toBe(toolBlock2.toolUse.toolUseId)
508+
} else {
509+
expect.fail("Expected tool use blocks not found")
510+
}
511+
})
512+
513+
it("matching tool_use and tool_result IDs are both truncated consistently", () => {
514+
const longId = "g".repeat(100)
515+
const messages: Anthropic.Messages.MessageParam[] = [
516+
{
517+
role: "assistant",
518+
content: [
519+
{
520+
type: "tool_use",
521+
id: longId,
522+
name: "read_file",
523+
input: { path: "test.txt" },
524+
},
525+
],
526+
},
527+
{
528+
role: "user",
529+
content: [
530+
{
531+
type: "tool_result",
532+
tool_use_id: longId,
533+
content: "File contents",
534+
} as any,
535+
],
536+
},
537+
]
538+
539+
const result = convertToBedrockConverseMessages(messages)
540+
541+
const toolUseBlock = result[0]?.content?.[0] as ContentBlock
542+
const toolResultBlock = result[1]?.content?.[0] as ContentBlock
543+
544+
if (
545+
"toolUse" in toolUseBlock &&
546+
toolUseBlock.toolUse &&
547+
toolUseBlock.toolUse.toolUseId &&
548+
"toolResult" in toolResultBlock &&
549+
toolResultBlock.toolResult &&
550+
toolResultBlock.toolResult.toolUseId
551+
) {
552+
expect(toolUseBlock.toolUse.toolUseId).toBe(toolResultBlock.toolResult.toolUseId)
553+
expect(toolUseBlock.toolUse.toolUseId.length).toBeLessThanOrEqual(OPENAI_CALL_ID_MAX_LENGTH)
554+
} else {
555+
expect.fail("Expected tool use and result blocks not found")
556+
}
557+
})
558+
})
344559
})

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Anthropic } from "@anthropic-ai/sdk"
22
import { ConversationRole, Message, ContentBlock } from "@aws-sdk/client-bedrock-runtime"
3+
import { sanitizeOpenAiCallId } from "../../utils/tool-id"
34

45
interface BedrockMessageContent {
56
type: "text" | "image" | "video" | "tool_use" | "tool_result"
@@ -90,7 +91,7 @@ export function convertToBedrockConverseMessages(anthropicMessages: Anthropic.Me
9091
// Native-only: keep input as JSON object for Bedrock's toolUse format
9192
return {
9293
toolUse: {
93-
toolUseId: messageBlock.id || "",
94+
toolUseId: sanitizeOpenAiCallId(messageBlock.id || ""),
9495
name: messageBlock.name || "",
9596
input: messageBlock.input || {},
9697
},
@@ -104,7 +105,7 @@ export function convertToBedrockConverseMessages(anthropicMessages: Anthropic.Me
104105
if (typeof messageBlock.content === "string") {
105106
return {
106107
toolResult: {
107-
toolUseId: messageBlock.tool_use_id || "",
108+
toolUseId: sanitizeOpenAiCallId(messageBlock.tool_use_id || ""),
108109
content: [
109110
{
110111
text: messageBlock.content,
@@ -118,7 +119,7 @@ export function convertToBedrockConverseMessages(anthropicMessages: Anthropic.Me
118119
if (Array.isArray(messageBlock.content)) {
119120
return {
120121
toolResult: {
121-
toolUseId: messageBlock.tool_use_id || "",
122+
toolUseId: sanitizeOpenAiCallId(messageBlock.tool_use_id || ""),
122123
content: messageBlock.content.map((item) => ({
123124
text: typeof item === "string" ? item : item.text || String(item),
124125
})),
@@ -132,7 +133,7 @@ export function convertToBedrockConverseMessages(anthropicMessages: Anthropic.Me
132133
if (messageBlock.output && typeof messageBlock.output === "string") {
133134
return {
134135
toolResult: {
135-
toolUseId: messageBlock.tool_use_id || "",
136+
toolUseId: sanitizeOpenAiCallId(messageBlock.tool_use_id || ""),
136137
content: [
137138
{
138139
text: messageBlock.output,
@@ -146,7 +147,7 @@ export function convertToBedrockConverseMessages(anthropicMessages: Anthropic.Me
146147
if (Array.isArray(messageBlock.output)) {
147148
return {
148149
toolResult: {
149-
toolUseId: messageBlock.tool_use_id || "",
150+
toolUseId: sanitizeOpenAiCallId(messageBlock.tool_use_id || ""),
150151
content: messageBlock.output.map((part) => {
151152
if (typeof part === "object" && "text" in part) {
152153
return { text: part.text }
@@ -165,7 +166,7 @@ export function convertToBedrockConverseMessages(anthropicMessages: Anthropic.Me
165166
// Default case
166167
return {
167168
toolResult: {
168-
toolUseId: messageBlock.tool_use_id || "",
169+
toolUseId: sanitizeOpenAiCallId(messageBlock.tool_use_id || ""),
169170
content: [
170171
{
171172
text: String(messageBlock.output || ""),

0 commit comments

Comments
 (0)