fix: truncate AWS Bedrock toolUseId to 64 characters (#10902)

This commit is contained in:
Daniel 2026-01-24 00:49:33 -05:00 committed by GitHub
parent 3877d02498
commit f9a3a178db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 222 additions and 6 deletions

View file

@ -3,6 +3,7 @@
import { convertToBedrockConverseMessages } from "../bedrock-converse-format"
import { Anthropic } from "@anthropic-ai/sdk"
import { ContentBlock, ToolResultContentBlock } from "@aws-sdk/client-bedrock-runtime"
import { OPENAI_CALL_ID_MAX_LENGTH } from "../../../utils/tool-id"
describe("convertToBedrockConverseMessages", () => {
it("converts simple text messages correctly", () => {
@ -341,4 +342,218 @@ describe("convertToBedrockConverseMessages", () => {
const textBlock = result[0].content[0] as ContentBlock
expect(textBlock).toEqual({ text: "Hello world" })
})
describe("toolUseId sanitization for Bedrock 64-char limit", () => {
it("truncates toolUseId longer than 64 characters in tool_use blocks", () => {
const longId = "a".repeat(100)
const messages: Anthropic.Messages.MessageParam[] = [
{
role: "assistant",
content: [
{
type: "tool_use",
id: longId,
name: "read_file",
input: { path: "test.txt" },
},
],
},
]
const result = convertToBedrockConverseMessages(messages)
const toolBlock = result[0]?.content?.[0] as ContentBlock
if ("toolUse" in toolBlock && toolBlock.toolUse && toolBlock.toolUse.toolUseId) {
expect(toolBlock.toolUse.toolUseId.length).toBeLessThanOrEqual(OPENAI_CALL_ID_MAX_LENGTH)
expect(toolBlock.toolUse.toolUseId.length).toBe(OPENAI_CALL_ID_MAX_LENGTH)
expect(toolBlock.toolUse.toolUseId).toContain("_")
} else {
expect.fail("Expected tool use block not found")
}
})
it("truncates toolUseId longer than 64 characters in tool_result blocks with string content", () => {
const longId = "b".repeat(100)
const messages: Anthropic.Messages.MessageParam[] = [
{
role: "user",
content: [
{
type: "tool_result",
tool_use_id: longId,
content: "Result content",
} as any,
],
},
]
const result = convertToBedrockConverseMessages(messages)
const resultBlock = result[0]?.content?.[0] as ContentBlock
if ("toolResult" in resultBlock && resultBlock.toolResult && resultBlock.toolResult.toolUseId) {
expect(resultBlock.toolResult.toolUseId.length).toBeLessThanOrEqual(OPENAI_CALL_ID_MAX_LENGTH)
expect(resultBlock.toolResult.toolUseId.length).toBe(OPENAI_CALL_ID_MAX_LENGTH)
expect(resultBlock.toolResult.toolUseId).toContain("_")
} else {
expect.fail("Expected tool result block not found")
}
})
it("truncates toolUseId longer than 64 characters in tool_result blocks with array content", () => {
const longId = "c".repeat(100)
const messages: Anthropic.Messages.MessageParam[] = [
{
role: "user",
content: [
{
type: "tool_result",
tool_use_id: longId,
content: [{ type: "text", text: "Result content" }],
},
],
},
]
const result = convertToBedrockConverseMessages(messages)
const resultBlock = result[0]?.content?.[0] as ContentBlock
if ("toolResult" in resultBlock && resultBlock.toolResult && resultBlock.toolResult.toolUseId) {
expect(resultBlock.toolResult.toolUseId.length).toBeLessThanOrEqual(OPENAI_CALL_ID_MAX_LENGTH)
expect(resultBlock.toolResult.toolUseId.length).toBe(OPENAI_CALL_ID_MAX_LENGTH)
} else {
expect.fail("Expected tool result block not found")
}
})
it("keeps toolUseId unchanged when under 64 characters", () => {
const shortId = "short-id-123"
const messages: Anthropic.Messages.MessageParam[] = [
{
role: "assistant",
content: [
{
type: "tool_use",
id: shortId,
name: "read_file",
input: { path: "test.txt" },
},
],
},
]
const result = convertToBedrockConverseMessages(messages)
const toolBlock = result[0]?.content?.[0] as ContentBlock
if ("toolUse" in toolBlock && toolBlock.toolUse) {
expect(toolBlock.toolUse.toolUseId).toBe(shortId)
} else {
expect.fail("Expected tool use block not found")
}
})
it("produces consistent truncated IDs for the same input", () => {
const longId = "d".repeat(100)
const messages: Anthropic.Messages.MessageParam[] = [
{
role: "assistant",
content: [
{
type: "tool_use",
id: longId,
name: "read_file",
input: { path: "test.txt" },
},
],
},
]
const result1 = convertToBedrockConverseMessages(messages)
const result2 = convertToBedrockConverseMessages(messages)
const toolBlock1 = result1[0]?.content?.[0] as ContentBlock
const toolBlock2 = result2[0]?.content?.[0] as ContentBlock
if ("toolUse" in toolBlock1 && toolBlock1.toolUse && "toolUse" in toolBlock2 && toolBlock2.toolUse) {
expect(toolBlock1.toolUse.toolUseId).toBe(toolBlock2.toolUse.toolUseId)
} else {
expect.fail("Expected tool use blocks not found")
}
})
it("produces different truncated IDs for different long inputs", () => {
const longId1 = "e".repeat(100)
const longId2 = "f".repeat(100)
const messages1: Anthropic.Messages.MessageParam[] = [
{
role: "assistant",
content: [{ type: "tool_use", id: longId1, name: "read_file", input: {} }],
},
]
const messages2: Anthropic.Messages.MessageParam[] = [
{
role: "assistant",
content: [{ type: "tool_use", id: longId2, name: "read_file", input: {} }],
},
]
const result1 = convertToBedrockConverseMessages(messages1)
const result2 = convertToBedrockConverseMessages(messages2)
const toolBlock1 = result1[0]?.content?.[0] as ContentBlock
const toolBlock2 = result2[0]?.content?.[0] as ContentBlock
if ("toolUse" in toolBlock1 && toolBlock1.toolUse && "toolUse" in toolBlock2 && toolBlock2.toolUse) {
expect(toolBlock1.toolUse.toolUseId).not.toBe(toolBlock2.toolUse.toolUseId)
} else {
expect.fail("Expected tool use blocks not found")
}
})
it("matching tool_use and tool_result IDs are both truncated consistently", () => {
const longId = "g".repeat(100)
const messages: Anthropic.Messages.MessageParam[] = [
{
role: "assistant",
content: [
{
type: "tool_use",
id: longId,
name: "read_file",
input: { path: "test.txt" },
},
],
},
{
role: "user",
content: [
{
type: "tool_result",
tool_use_id: longId,
content: "File contents",
} as any,
],
},
]
const result = convertToBedrockConverseMessages(messages)
const toolUseBlock = result[0]?.content?.[0] as ContentBlock
const toolResultBlock = result[1]?.content?.[0] as ContentBlock
if (
"toolUse" in toolUseBlock &&
toolUseBlock.toolUse &&
toolUseBlock.toolUse.toolUseId &&
"toolResult" in toolResultBlock &&
toolResultBlock.toolResult &&
toolResultBlock.toolResult.toolUseId
) {
expect(toolUseBlock.toolUse.toolUseId).toBe(toolResultBlock.toolResult.toolUseId)
expect(toolUseBlock.toolUse.toolUseId.length).toBeLessThanOrEqual(OPENAI_CALL_ID_MAX_LENGTH)
} else {
expect.fail("Expected tool use and result blocks not found")
}
})
})
})

View file

@ -1,5 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ConversationRole, Message, ContentBlock } from "@aws-sdk/client-bedrock-runtime"
import { sanitizeOpenAiCallId } from "../../utils/tool-id"
interface BedrockMessageContent {
type: "text" | "image" | "video" | "tool_use" | "tool_result"
@ -90,7 +91,7 @@ export function convertToBedrockConverseMessages(anthropicMessages: Anthropic.Me
// Native-only: keep input as JSON object for Bedrock's toolUse format
return {
toolUse: {
toolUseId: messageBlock.id || "",
toolUseId: sanitizeOpenAiCallId(messageBlock.id || ""),
name: messageBlock.name || "",
input: messageBlock.input || {},
},
@ -104,7 +105,7 @@ export function convertToBedrockConverseMessages(anthropicMessages: Anthropic.Me
if (typeof messageBlock.content === "string") {
return {
toolResult: {
toolUseId: messageBlock.tool_use_id || "",
toolUseId: sanitizeOpenAiCallId(messageBlock.tool_use_id || ""),
content: [
{
text: messageBlock.content,
@ -118,7 +119,7 @@ export function convertToBedrockConverseMessages(anthropicMessages: Anthropic.Me
if (Array.isArray(messageBlock.content)) {
return {
toolResult: {
toolUseId: messageBlock.tool_use_id || "",
toolUseId: sanitizeOpenAiCallId(messageBlock.tool_use_id || ""),
content: messageBlock.content.map((item) => ({
text: typeof item === "string" ? item : item.text || String(item),
})),
@ -132,7 +133,7 @@ export function convertToBedrockConverseMessages(anthropicMessages: Anthropic.Me
if (messageBlock.output && typeof messageBlock.output === "string") {
return {
toolResult: {
toolUseId: messageBlock.tool_use_id || "",
toolUseId: sanitizeOpenAiCallId(messageBlock.tool_use_id || ""),
content: [
{
text: messageBlock.output,
@ -146,7 +147,7 @@ export function convertToBedrockConverseMessages(anthropicMessages: Anthropic.Me
if (Array.isArray(messageBlock.output)) {
return {
toolResult: {
toolUseId: messageBlock.tool_use_id || "",
toolUseId: sanitizeOpenAiCallId(messageBlock.tool_use_id || ""),
content: messageBlock.output.map((part) => {
if (typeof part === "object" && "text" in part) {
return { text: part.text }
@ -165,7 +166,7 @@ export function convertToBedrockConverseMessages(anthropicMessages: Anthropic.Me
// Default case
return {
toolResult: {
toolUseId: messageBlock.tool_use_id || "",
toolUseId: sanitizeOpenAiCallId(messageBlock.tool_use_id || ""),
content: [
{
text: String(messageBlock.output || ""),