diff --git a/src/api/providers/__tests__/bedrock-image-limiting.spec.ts b/src/api/providers/__tests__/bedrock-image-limiting.spec.ts new file mode 100644 index 0000000000..47fd9cc51a --- /dev/null +++ b/src/api/providers/__tests__/bedrock-image-limiting.spec.ts @@ -0,0 +1,299 @@ +// npx vitest run src/api/providers/__tests__/bedrock-image-limiting.spec.ts + +import { describe, it, expect, vi, beforeEach } from "vitest" +import { Anthropic } from "@anthropic-ai/sdk" +import { AwsBedrockHandler } from "../bedrock" +import { ApiHandlerOptions } from "../../../shared/api" +import { AWS_BEDROCK_MAX_IMAGES_PER_CONVERSATION } from "../../transform/image-limiting" + +// Valid base64 encoded 1x1 pixel PNG image for testing +const VALID_BASE64_IMAGE = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChAGAWA0+kgAAAABJRU5ErkJggg==" + +// Mock AWS SDK +vi.mock("@aws-sdk/client-bedrock-runtime", () => { + return { + BedrockRuntimeClient: vi.fn().mockImplementation(() => ({ + send: vi.fn(), + })), + ConverseStreamCommand: vi.fn(), + ConverseCommand: vi.fn(), + } +}) + +// Mock credential providers +vi.mock("@aws-sdk/credential-providers", () => ({ + fromIni: vi.fn().mockReturnValue({ + accessKeyId: "test-access-key", + secretAccessKey: "test-secret-key", + }), +})) + +describe("AwsBedrockHandler - Image Limiting", () => { + let handler: AwsBedrockHandler + let mockOptions: ApiHandlerOptions + + beforeEach(() => { + mockOptions = { + apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", + awsAccessKey: "test-access-key", + awsSecretKey: "test-secret-key", + awsRegion: "us-east-1", + } + handler = new AwsBedrockHandler(mockOptions) + }) + + describe("convertToBedrockConverseMessages with image limiting", () => { + it("should not modify messages when under image limit", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { type: "text", text: "Look at this image:" }, + { + type: "image", + source: { type: "base64", media_type: "image/png", data: VALID_BASE64_IMAGE }, + }, + ], + }, + { + role: "user", + content: [ + { type: "text", text: "And this one:" }, + { + type: "image", + source: { type: "base64", media_type: "image/jpeg", data: VALID_BASE64_IMAGE }, + }, + ], + }, + ] + + // Access the private method for testing + const result = (handler as any).convertToBedrockConverseMessages(messages, "System prompt") + + // Should have 2 messages with images intact + expect(result.messages).toHaveLength(2) + + // Check that images are preserved + const firstMessage = result.messages[0] + const secondMessage = result.messages[1] + + expect(firstMessage.content).toHaveLength(2) + expect(firstMessage.content[1]).toHaveProperty("image") + + expect(secondMessage.content).toHaveLength(2) + expect(secondMessage.content[1]).toHaveProperty("image") + }) + + it("should limit images when over AWS Bedrock limit", () => { + const messages: Anthropic.Messages.MessageParam[] = [] + + // Create 25 messages with 1 image each (5 over the limit) + for (let i = 0; i < 25; i++) { + messages.push({ + role: "user", + content: [ + { type: "text", text: `Browser screenshot ${i + 1}` }, + { + type: "image", + source: { type: "base64", media_type: "image/png", data: VALID_BASE64_IMAGE }, + }, + ], + }) + } + + // Access the private method for testing + const result = (handler as any).convertToBedrockConverseMessages(messages, "System prompt") + + // Should have 25 messages + expect(result.messages).toHaveLength(25) + + // Count actual images in the result + let imageCount = 0 + let textPlaceholderCount = 0 + + for (const message of result.messages) { + for (const block of message.content) { + if (block.image) { + imageCount++ + } else if (block.text && block.text.includes("[Image removed due to conversation limit")) { + textPlaceholderCount++ + } + } + } + + // Should have exactly 20 images and 5 text placeholders + expect(imageCount).toBe(AWS_BEDROCK_MAX_IMAGES_PER_CONVERSATION) + expect(textPlaceholderCount).toBe(5) + }) + + it("should preserve text content when limiting images", () => { + const messages: Anthropic.Messages.MessageParam[] = [] + + // Create 22 messages with mixed content (2 over the limit) + for (let i = 0; i < 22; i++) { + messages.push({ + role: "user", + content: [ + { type: "text", text: `Important context ${i + 1}` }, + { + type: "image", + source: { type: "base64", media_type: "image/png", data: VALID_BASE64_IMAGE }, + }, + { type: "text", text: `Additional info ${i + 1}` }, + ], + }) + } + + // Access the private method for testing + const result = (handler as any).convertToBedrockConverseMessages(messages, "System prompt") + + // All text content should be preserved + for (let i = 0; i < 22; i++) { + const message = result.messages[i] + expect(message.content[0].text).toBe(`Important context ${i + 1}`) + expect(message.content[2].text).toBe(`Additional info ${i + 1}`) + } + + // First 2 messages should have image placeholders + expect(result.messages[0].content[1].text).toBe( + "[Image removed due to conversation limit - Browser tool screenshot]", + ) + expect(result.messages[1].content[1].text).toBe( + "[Image removed due to conversation limit - Browser tool screenshot]", + ) + + // Remaining messages should have images + for (let i = 2; i < 22; i++) { + expect(result.messages[i].content[1]).toHaveProperty("image") + } + }) + + it("should handle exactly 20 images without modification", () => { + const messages: Anthropic.Messages.MessageParam[] = [] + + // Create exactly 20 messages with 1 image each + for (let i = 0; i < AWS_BEDROCK_MAX_IMAGES_PER_CONVERSATION; i++) { + messages.push({ + role: "user", + content: [ + { type: "text", text: `Message ${i + 1}` }, + { + type: "image", + source: { type: "base64", media_type: "image/png", data: VALID_BASE64_IMAGE }, + }, + ], + }) + } + + // Access the private method for testing + const result = (handler as any).convertToBedrockConverseMessages(messages, "System prompt") + + // Should have 20 messages with all images intact + expect(result.messages).toHaveLength(20) + + let imageCount = 0 + for (const message of result.messages) { + for (const block of message.content) { + if (block.image) { + imageCount++ + } + } + } + + expect(imageCount).toBe(AWS_BEDROCK_MAX_IMAGES_PER_CONVERSATION) + }) + + it("should handle mixed message types correctly", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: "Text only message" }, + { + role: "assistant", + content: [{ type: "text", text: "I understand." }], + }, + ] + + // Add 21 image messages to exceed the limit + for (let i = 0; i < 21; i++) { + messages.push({ + role: "user", + content: [ + { + type: "image", + source: { type: "base64", media_type: "image/png", data: VALID_BASE64_IMAGE }, + }, + ], + }) + } + + // Access the private method for testing + const result = (handler as any).convertToBedrockConverseMessages(messages, "System prompt") + + // Should have 23 messages total + expect(result.messages).toHaveLength(23) + + // First two messages should be unchanged (no images) + expect(result.messages[0].content[0].text).toBe("Text only message") + expect(result.messages[1].content[0].text).toBe("I understand.") + + // Count images in the result + let imageCount = 0 + let placeholderCount = 0 + + for (const message of result.messages) { + for (const block of message.content) { + if (block.image) { + imageCount++ + } else if (block.text && block.text.includes("[Image removed due to conversation limit")) { + placeholderCount++ + } + } + } + + // Should have exactly 20 images and 1 placeholder + expect(imageCount).toBe(20) + expect(placeholderCount).toBe(1) + }) + + it("should work with system message", () => { + const messages: Anthropic.Messages.MessageParam[] = [] + + // Create 22 messages with images (2 over limit) + for (let i = 0; i < 22; i++) { + messages.push({ + role: "user", + content: [ + { + type: "image", + source: { type: "base64", media_type: "image/png", data: VALID_BASE64_IMAGE }, + }, + ], + }) + } + + const systemMessage = "You are a helpful assistant that can analyze images." + + // Access the private method for testing + const result = (handler as any).convertToBedrockConverseMessages(messages, systemMessage) + + // System message should be preserved + expect(result.system).toHaveLength(1) + expect(result.system[0].text).toBe(systemMessage) + + // Should have 22 messages with limited images + expect(result.messages).toHaveLength(22) + + // Count images + let imageCount = 0 + for (const message of result.messages) { + for (const block of message.content) { + if (block.image) { + imageCount++ + } + } + } + + expect(imageCount).toBe(AWS_BEDROCK_MAX_IMAGES_PER_CONVERSATION) + }) + }) +}) diff --git a/src/api/providers/bedrock.ts b/src/api/providers/bedrock.ts index 76e502e6c7..485b75c80b 100644 --- a/src/api/providers/bedrock.ts +++ b/src/api/providers/bedrock.ts @@ -30,6 +30,7 @@ import { MultiPointStrategy } from "../transform/cache-strategy/multi-point-stra import { ModelInfo as CacheModelInfo } from "../transform/cache-strategy/types" import { convertToBedrockConverseMessages as sharedConverter } from "../transform/bedrock-converse-format" import { getModelParams } from "../transform/model-params" +import { limitImagesInConversation, hasExceededImageLimit } from "../transform/image-limiting" import { shouldUseReasoningBudget } from "../../shared/api" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" @@ -706,8 +707,20 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH modelInfo?: any, conversationId?: string, // Optional conversation ID to track cache points across messages ): { system: SystemContentBlock[]; messages: Message[] } { + // Apply image limiting for AWS Bedrock's 20-image conversation limit + const limitedMessages = limitImagesInConversation(anthropicMessages as Anthropic.Messages.MessageParam[]) + + // Log if images were removed due to the limit + if (hasExceededImageLimit(anthropicMessages as Anthropic.Messages.MessageParam[])) { + logger.info("Applied image limiting for AWS Bedrock conversation", { + ctx: "bedrock", + originalImageCount: limitedMessages.length, + action: "removed_oldest_images_to_stay_within_limit", + }) + } + // First convert messages using shared converter for proper image handling - const convertedMessages = sharedConverter(anthropicMessages as Anthropic.Messages.MessageParam[]) + const convertedMessages = sharedConverter(limitedMessages) // If prompt caching is disabled, return the converted messages directly if (!usePromptCache) { @@ -1121,6 +1134,21 @@ Suggestions: `, logLevel: "error", }, + TOO_MANY_IMAGES: { + patterns: ["too many images", "too many images and documents", "images and documents:", "> 20"], + messageTemplate: `AWS Bedrock "too many images" error detected. + +This error occurs when the conversation contains more than 20 images total. The application has automatically applied image limiting to prevent this error in future requests. + +What happened: +- AWS Bedrock has a hard limit of 20 images per conversation +- Your conversation exceeded this limit (likely from Browser tool screenshots) +- The oldest images have been automatically replaced with text placeholders +- The conversation can now continue normally + +No action needed - the issue has been resolved automatically.`, + logLevel: "info", + }, SERVICE_QUOTA_EXCEEDED: { patterns: ["service quota exceeded", "service quota", "quota exceeded for model"], messageTemplate: `Service quota exceeded. This error indicates you've reached AWS service limits. @@ -1234,6 +1262,7 @@ Please check: const errorTypeOrder = [ "SERVICE_QUOTA_EXCEEDED", // Most specific - check before THROTTLING "MODEL_NOT_READY", + "TOO_MANY_IMAGES", // Check before TOO_MANY_TOKENS for specificity "TOO_MANY_TOKENS", "INTERNAL_SERVER_ERROR", "ON_DEMAND_NOT_SUPPORTED", diff --git a/src/api/transform/__tests__/image-limiting.spec.ts b/src/api/transform/__tests__/image-limiting.spec.ts new file mode 100644 index 0000000000..90c7d117bd --- /dev/null +++ b/src/api/transform/__tests__/image-limiting.spec.ts @@ -0,0 +1,323 @@ +import { describe, it, expect } from "vitest" +import { Anthropic } from "@anthropic-ai/sdk" +import { + countImagesInConversation, + limitImagesInConversation, + hasExceededImageLimit, + AWS_BEDROCK_MAX_IMAGES_PER_CONVERSATION, +} from "../image-limiting" + +describe("image-limiting", () => { + describe("countImagesInConversation", () => { + it("should count zero images in empty conversation", () => { + const messages: Anthropic.Messages.MessageParam[] = [] + expect(countImagesInConversation(messages)).toBe(0) + }) + + it("should count zero images in text-only conversation", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: "Hello" }, + { role: "assistant", content: "Hi there!" }, + ] + expect(countImagesInConversation(messages)).toBe(0) + }) + + it("should count images in single message", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { type: "text", text: "Look at this image:" }, + { type: "image", source: { type: "base64", media_type: "image/png", data: "base64data" } }, + { type: "image", source: { type: "base64", media_type: "image/jpeg", data: "base64data2" } }, + ], + }, + ] + expect(countImagesInConversation(messages)).toBe(2) + }) + + it("should count images across multiple messages", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { type: "text", text: "First image:" }, + { type: "image", source: { type: "base64", media_type: "image/png", data: "base64data1" } }, + ], + }, + { role: "assistant", content: "I see the image." }, + { + role: "user", + content: [ + { type: "text", text: "Second and third images:" }, + { type: "image", source: { type: "base64", media_type: "image/jpeg", data: "base64data2" } }, + { type: "image", source: { type: "base64", media_type: "image/png", data: "base64data3" } }, + ], + }, + ] + expect(countImagesInConversation(messages)).toBe(3) + }) + + it("should handle mixed content types", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { type: "text", text: "Text content" }, + { type: "image", source: { type: "base64", media_type: "image/png", data: "base64data" } }, + { type: "text", text: "More text" }, + ], + }, + ] + expect(countImagesInConversation(messages)).toBe(1) + }) + }) + + describe("hasExceededImageLimit", () => { + it("should return false when under limit", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { type: "image", source: { type: "base64", media_type: "image/png", data: "base64data" } }, + ], + }, + ] + expect(hasExceededImageLimit(messages)).toBe(false) + }) + + it("should return false when exactly at limit", () => { + const messages: Anthropic.Messages.MessageParam[] = [] + // Create exactly 20 images + for (let i = 0; i < AWS_BEDROCK_MAX_IMAGES_PER_CONVERSATION; i++) { + messages.push({ + role: "user", + content: [ + { type: "image", source: { type: "base64", media_type: "image/png", data: `base64data${i}` } }, + ], + }) + } + expect(hasExceededImageLimit(messages)).toBe(false) + }) + + it("should return true when over limit", () => { + const messages: Anthropic.Messages.MessageParam[] = [] + // Create 21 images (1 over limit) + for (let i = 0; i < AWS_BEDROCK_MAX_IMAGES_PER_CONVERSATION + 1; i++) { + messages.push({ + role: "user", + content: [ + { type: "image", source: { type: "base64", media_type: "image/png", data: `base64data${i}` } }, + ], + }) + } + expect(hasExceededImageLimit(messages)).toBe(true) + }) + + it("should work with custom limit", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { type: "image", source: { type: "base64", media_type: "image/png", data: "base64data1" } }, + { type: "image", source: { type: "base64", media_type: "image/png", data: "base64data2" } }, + { type: "image", source: { type: "base64", media_type: "image/png", data: "base64data3" } }, + ], + }, + ] + expect(hasExceededImageLimit(messages, 2)).toBe(true) + expect(hasExceededImageLimit(messages, 3)).toBe(false) + expect(hasExceededImageLimit(messages, 4)).toBe(false) + }) + }) + + describe("limitImagesInConversation", () => { + it("should return unchanged messages when under limit", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { type: "text", text: "Hello" }, + { type: "image", source: { type: "base64", media_type: "image/png", data: "base64data" } }, + ], + }, + ] + const result = limitImagesInConversation(messages) + expect(result).toEqual(messages) + }) + + it("should return unchanged messages when exactly at limit", () => { + const messages: Anthropic.Messages.MessageParam[] = [] + // Create exactly 20 images + for (let i = 0; i < AWS_BEDROCK_MAX_IMAGES_PER_CONVERSATION; i++) { + messages.push({ + role: "user", + content: [ + { type: "image", source: { type: "base64", media_type: "image/png", data: `base64data${i}` } }, + ], + }) + } + const result = limitImagesInConversation(messages) + expect(result).toEqual(messages) + expect(countImagesInConversation(result)).toBe(AWS_BEDROCK_MAX_IMAGES_PER_CONVERSATION) + }) + + it("should limit images when over limit", () => { + const messages: Anthropic.Messages.MessageParam[] = [] + // Create 25 images (5 over limit) + for (let i = 0; i < 25; i++) { + messages.push({ + role: "user", + content: [ + { type: "text", text: `Message ${i}` }, + { type: "image", source: { type: "base64", media_type: "image/png", data: `base64data${i}` } }, + ], + }) + } + + const result = limitImagesInConversation(messages) + + // Should have exactly 20 images after limiting + expect(countImagesInConversation(result)).toBe(AWS_BEDROCK_MAX_IMAGES_PER_CONVERSATION) + + // First 5 images should be replaced with text placeholders + for (let i = 0; i < 5; i++) { + const content = result[i].content as any[] + expect(content[1].type).toBe("text") + expect(content[1].text).toBe("[Image removed due to conversation limit - Browser tool screenshot]") + } + + // Last 20 images should remain as images + for (let i = 5; i < 25; i++) { + const content = result[i].content as any[] + expect(content[1].type).toBe("image") + expect(content[1].source.data).toBe(`base64data${i}`) + } + }) + + it("should preserve text content when limiting images", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { type: "text", text: "First message" }, + { type: "image", source: { type: "base64", media_type: "image/png", data: "image1" } }, + { type: "text", text: "More text" }, + ], + }, + { + role: "user", + content: [ + { type: "text", text: "Second message" }, + { type: "image", source: { type: "base64", media_type: "image/png", data: "image2" } }, + ], + }, + ] + + const result = limitImagesInConversation(messages, 1) // Limit to 1 image + + // Should have exactly 1 image + expect(countImagesInConversation(result)).toBe(1) + + // First image should be replaced, text should remain + const firstContent = result[0].content as any[] + expect(firstContent[0].type).toBe("text") + expect(firstContent[0].text).toBe("First message") + expect(firstContent[1].type).toBe("text") + expect(firstContent[1].text).toBe("[Image removed due to conversation limit - Browser tool screenshot]") + expect(firstContent[2].type).toBe("text") + expect(firstContent[2].text).toBe("More text") + + // Second image should remain + const secondContent = result[1].content as any[] + expect(secondContent[0].type).toBe("text") + expect(secondContent[0].text).toBe("Second message") + expect(secondContent[1].type).toBe("image") + expect(secondContent[1].source.data).toBe("image2") + }) + + it("should not mutate original messages", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { type: "image", source: { type: "base64", media_type: "image/png", data: "image1" } }, + { type: "image", source: { type: "base64", media_type: "image/png", data: "image2" } }, + ], + }, + ] + + const originalContent = JSON.parse(JSON.stringify(messages)) + limitImagesInConversation(messages, 1) + + // Original messages should be unchanged + expect(messages).toEqual(originalContent) + }) + + it("should work with custom limit", () => { + const messages: Anthropic.Messages.MessageParam[] = [] + // Create 5 images + for (let i = 0; i < 5; i++) { + messages.push({ + role: "user", + content: [ + { type: "image", source: { type: "base64", media_type: "image/png", data: `base64data${i}` } }, + ], + }) + } + + const result = limitImagesInConversation(messages, 3) // Limit to 3 images + + // Should have exactly 3 images + expect(countImagesInConversation(result)).toBe(3) + + // First 2 should be replaced with text + for (let i = 0; i < 2; i++) { + const content = result[i].content as any[] + expect(content[0].type).toBe("text") + expect(content[0].text).toBe("[Image removed due to conversation limit - Browser tool screenshot]") + } + + // Last 3 should remain as images + for (let i = 2; i < 5; i++) { + const content = result[i].content as any[] + expect(content[0].type).toBe("image") + expect(content[0].source.data).toBe(`base64data${i}`) + } + }) + + it("should handle messages with string content", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: "Text only message" }, + { + role: "user", + content: [ + { type: "image", source: { type: "base64", media_type: "image/png", data: "image1" } }, + { type: "image", source: { type: "base64", media_type: "image/png", data: "image2" } }, + ], + }, + ] + + const result = limitImagesInConversation(messages, 1) + + // Should have exactly 1 image + expect(countImagesInConversation(result)).toBe(1) + + // String content should remain unchanged + expect(result[0].content).toBe("Text only message") + + // First image should be replaced, second should remain + const arrayContent = result[1].content as any[] + expect(arrayContent[0].type).toBe("text") + expect(arrayContent[0].text).toBe("[Image removed due to conversation limit - Browser tool screenshot]") + expect(arrayContent[1].type).toBe("image") + expect(arrayContent[1].source.data).toBe("image2") + }) + }) + + describe("AWS_BEDROCK_MAX_IMAGES_PER_CONVERSATION constant", () => { + it("should be set to 20", () => { + expect(AWS_BEDROCK_MAX_IMAGES_PER_CONVERSATION).toBe(20) + }) + }) +}) diff --git a/src/api/transform/image-limiting.ts b/src/api/transform/image-limiting.ts new file mode 100644 index 0000000000..d7585de11e --- /dev/null +++ b/src/api/transform/image-limiting.ts @@ -0,0 +1,93 @@ +import { Anthropic } from "@anthropic-ai/sdk" + +/** + * AWS Bedrock has a hard limit of 20 images total per conversation. + * This constant defines that limit. + */ +export const AWS_BEDROCK_MAX_IMAGES_PER_CONVERSATION = 20 + +/** + * Counts the total number of images across all messages in the conversation history + */ +export function countImagesInConversation(messages: Anthropic.Messages.MessageParam[]): number { + let totalImages = 0 + + for (const message of messages) { + if (Array.isArray(message.content)) { + for (const block of message.content) { + if (block.type === "image") { + totalImages++ + } + } + } + } + + return totalImages +} + +/** + * Limits the total number of images in a conversation to the specified maximum. + * When the limit is exceeded, removes the oldest images while preserving the most recent ones. + * This ensures AWS Bedrock's 20-image limit is respected. + * + * @param messages - The conversation messages + * @param maxImages - Maximum number of images allowed (default: AWS_BEDROCK_MAX_IMAGES_PER_CONVERSATION) + * @returns Modified messages with image count limited to maxImages + */ +export function limitImagesInConversation( + messages: Anthropic.Messages.MessageParam[], + maxImages: number = AWS_BEDROCK_MAX_IMAGES_PER_CONVERSATION, +): Anthropic.Messages.MessageParam[] { + const totalImages = countImagesInConversation(messages) + + // If we're within the limit, return messages unchanged + if (totalImages <= maxImages) { + return messages + } + + // We need to remove (totalImages - maxImages) images, starting from the oldest + const imagesToRemove = totalImages - maxImages + let imagesRemoved = 0 + + // Create a deep copy of messages to avoid mutating the original + const modifiedMessages = messages.map((message) => ({ + ...message, + content: Array.isArray(message.content) ? message.content.map((block) => ({ ...block })) : message.content, + })) + + // Iterate through messages from oldest to newest, removing images until we reach the limit + for (let i = 0; i < modifiedMessages.length && imagesRemoved < imagesToRemove; i++) { + const message = modifiedMessages[i] + + if (Array.isArray(message.content)) { + const newContent = [] + + for (const block of message.content) { + if (block.type === "image" && imagesRemoved < imagesToRemove) { + // Replace image with a text placeholder + newContent.push({ + type: "text" as const, + text: "[Image removed due to conversation limit - Browser tool screenshot]", + }) + imagesRemoved++ + } else { + newContent.push(block) + } + } + + message.content = newContent + } + } + + return modifiedMessages +} + +/** + * Checks if a conversation has exceeded the AWS Bedrock image limit + */ +export function hasExceededImageLimit( + messages: Anthropic.Messages.MessageParam[], + maxImages: number = AWS_BEDROCK_MAX_IMAGES_PER_CONVERSATION, +): boolean { + return countImagesInConversation(messages) > maxImages +}