diff --git a/packages/ai/src/protocols/utils/bedrock-media.ts b/packages/ai/src/protocols/utils/bedrock-media.ts index 6de2385d9b9..d2a6ab64311 100644 --- a/packages/ai/src/protocols/utils/bedrock-media.ts +++ b/packages/ai/src/protocols/utils/bedrock-media.ts @@ -1,4 +1,4 @@ -import { Effect, Schema } from "effect" +import { Effect, Encoding, Schema } from "effect" import type { MediaPart } from "../../schema/index.js" import { ProviderShared } from "../shared.js" @@ -57,6 +57,16 @@ const documentBlock = (name: string, format: DocumentFormat, bytes: string): Doc }, }) +const mediaBase64 = Effect.fn("BedrockMedia.mediaBase64")(function* (part: MediaPart) { + const media = ProviderShared.normalizeMedia(part) + const bytes = yield* Effect.fromResult(Encoding.decodeBase64(media.base64)).pipe( + Effect.mapError((cause) => + ProviderShared.invalidRequest("Bedrock Converse media data must be valid base64", cause), + ), + ) + return Encoding.encodeBase64(bytes) +}) + // Route by MIME. Known image/document formats lower into a typed block; anything // else fails with a clear error instead of silently degrading to a malformed // document block. Image MIME types not in `IMAGE_FORMATS` (e.g. `image/svg+xml`) @@ -66,8 +76,7 @@ export const lower = Effect.fn("BedrockMedia.lower")(function* (part: MediaPart) const mime = part.mediaType.toLowerCase() const imageFormat = IMAGE_FORMATS[mime as keyof typeof IMAGE_FORMATS] if (imageFormat) { - const media = ProviderShared.normalizeMedia(part) - return { image: { format: imageFormat, source: { bytes: media.base64 } } } satisfies ImageBlock + return { image: { format: imageFormat, source: { bytes: yield* mediaBase64(part) } } } satisfies ImageBlock } if (mime.startsWith("image/")) return yield* ProviderShared.invalidRequest(`Bedrock Converse does not support image media type ${part.mediaType}`) @@ -75,8 +84,7 @@ export const lower = Effect.fn("BedrockMedia.lower")(function* (part: MediaPart) if (documentFormat) { if (!part.filename) return yield* ProviderShared.invalidRequest("Bedrock Converse document media requires a filename") - const media = ProviderShared.normalizeMedia(part) - return documentBlock(part.filename, documentFormat, media.base64) + return documentBlock(part.filename, documentFormat, yield* mediaBase64(part)) } return yield* ProviderShared.invalidRequest(`Bedrock Converse does not support media type ${part.mediaType}`) }) diff --git a/packages/ai/test/provider/bedrock-converse.test.ts b/packages/ai/test/provider/bedrock-converse.test.ts index dfaca9f6f26..39b195289d5 100644 --- a/packages/ai/test/provider/bedrock-converse.test.ts +++ b/packages/ai/test/provider/bedrock-converse.test.ts @@ -1429,6 +1429,20 @@ describe("Bedrock Converse route", () => { }), ) + it.effect("rejects image media that is not valid base64", () => + Effect.gen(function* () { + const error = yield* compileRequest( + LLM.request({ + model, + messages: [Message.user({ type: "media", mediaType: "image/png", data: "https://example.test/image.png" })], + }), + ).pipe(Effect.flip) + + expect(error).toMatchObject({ reason: { _tag: "InvalidRequest" } }) + expect(error.message).toContain("Bedrock Converse media data must be valid base64") + }), + ) + it.effect("lowers document media into Bedrock document blocks with format and name", () => Effect.gen(function* () { const prepared = yield* compileRequest( @@ -1552,6 +1566,37 @@ describe("Bedrock Converse route", () => { }), ) + it.effect("rejects remote media URLs in tool results", () => + Effect.gen(function* () { + const error = yield* compileRequest( + LLM.request({ + model, + messages: [ + Message.assistant([ToolCallPart.make({ id: "call_1", name: "read", input: {} })]), + Message.tool({ + id: "call_1", + name: "read", + result: { + type: "content", + value: [ + { + type: "file", + uri: "https://example.test/report.pdf", + mime: "application/pdf", + name: "report.pdf", + }, + ], + }, + }), + ], + }), + ).pipe(Effect.flip) + + expect(error).toMatchObject({ reason: { _tag: "InvalidRequest" } }) + expect(error.message).toContain("Bedrock Converse media data must be valid base64") + }), + ) + it.effect("rejects unsupported image media types", () => Effect.gen(function* () { const error = yield* compileRequest(