diff --git a/src/api/transform/__tests__/bedrock-converse-format.spec.ts b/src/api/transform/__tests__/bedrock-converse-format.spec.ts index ac29a88c369..27319c65620 100644 --- a/src/api/transform/__tests__/bedrock-converse-format.spec.ts +++ b/src/api/transform/__tests__/bedrock-converse-format.spec.ts @@ -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") + } + }) + }) }) diff --git a/src/api/transform/bedrock-converse-format.ts b/src/api/transform/bedrock-converse-format.ts index 08b89c1ef8c..2a49d72bcef 100644 --- a/src/api/transform/bedrock-converse-format.ts +++ b/src/api/transform/bedrock-converse-format.ts @@ -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 || ""),