⚠ This page is served via a proxy. Original site: https://github.com
This service does not collect credentials or authentication data.
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
215 changes: 215 additions & 0 deletions src/api/transform/__tests__/bedrock-converse-format.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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")
}
})
})
})
13 changes: 7 additions & 6 deletions src/api/transform/bedrock-converse-format.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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 || {},
},
Expand All @@ -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,
Expand All @@ -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),
})),
Expand All @@ -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,
Expand All @@ -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 }
Expand All @@ -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 || ""),
Expand Down
Loading