From 923589de23be0c6556e2369b56ae8c2e1e33c7e6 Mon Sep 17 00:00:00 2001 From: Ibrahim Khan <2005ibrahimkhan@gmail.com> Date: Fri, 17 Jul 2026 17:56:01 +0000 Subject: [PATCH] fix(opencode): sanitize Bedrock document names from file attachments Bedrock's Converse API rejects document names containing characters outside [alphanumeric, whitespace, hyphen, parentheses, square brackets]. File parts can carry a filesystem path or a synthetic URI (e.g. an MCP "slack-file://..." attachment, which surfaces because Bedrock does not accept PDFs in tool results, so opencode re-injects them as a user file part). The AI SDK passes that filename through verbatim as the Converse document name, so once such an attachment lands in history every subsequent request in the session fails with a ValidationException. Sanitize file-part filenames in the Bedrock branch of ProviderTransform.message (shared by the v1 and v2 request paths), mapping disallowed characters to a space and collapsing runs; drop the name entirely when nothing valid remains so the SDK generates one. --- packages/opencode/src/provider/transform.ts | 24 ++++++- .../opencode/test/provider/transform.test.ts | 68 +++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 92d84eac8cb0..e916c08635c5 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -26,6 +26,21 @@ export function sanitizeSurrogates(content: string) { return content.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(? 0 ? sanitized : undefined +} + // Maps npm package to the key the AI SDK expects for providerOptions function sdkKey(npm: string): string | undefined { switch (npm) { @@ -181,7 +196,14 @@ function normalizeMessages( return true }) if (filtered.length === 0) return undefined - return { ...msg, content: filtered } + return { + ...msg, + content: filtered.map((part) => + part.type === "file" && typeof part.filename === "string" + ? { ...part, filename: sanitizeBedrockDocumentName(part.filename) } + : part, + ), + } }) .filter((msg): msg is ModelMessage => msg !== undefined && msg.content !== "") } diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 4c462bd3dce2..10da2dc60089 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -1908,6 +1908,74 @@ describe("ProviderTransform.message - surrogate sanitization", () => { }) }) +describe("ProviderTransform.message - Bedrock document name sanitization", () => { + const bedrockModel = { + id: "amazon-bedrock/amazon.nova-pro-v1", + providerID: "amazon-bedrock", + api: { + id: "amazon.nova-pro-v1:0", + url: "https://bedrock-runtime.us-east-1.amazonaws.com", + npm: "@ai-sdk/amazon-bedrock", + }, + name: "Amazon Nova Pro", + capabilities: { + temperature: true, + reasoning: false, + attachment: true, + toolcall: true, + input: { text: true, audio: false, image: true, video: false, pdf: true }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: false, + }, + cost: { input: 0.001, output: 0.002, cache: { read: 0.0001, write: 0.0002 } }, + limit: { context: 300000, output: 8192 }, + status: "active", + options: {}, + headers: {}, + } as any + + const fileMessage = (filename: string) => + [ + { + role: "user", + content: [{ type: "file", data: "data:application/pdf;base64,AAAA", mediaType: "application/pdf", filename }], + }, + ] as any[] + + // Matches any character Bedrock's Converse document name rejects. + const invalid = /[^a-zA-Z0-9\s()[\]-]/ + + test("sanitizes a synthetic MCP attachment URI (issue #37191)", () => { + const result = ProviderTransform.message(fileMessage("slack-file://F0ABC123"), bedrockModel, {}) as any[] + const filename = result[0].content[0].filename + expect(filename).toBe("slack-file F0ABC123") + expect(invalid.test(filename)).toBe(false) + }) + + test("sanitizes an absolute filesystem path (issue #36113)", () => { + const result = ProviderTransform.message(fileMessage("/Users/foo/Downloads/SOP.pdf"), bedrockModel, {}) as any[] + const filename = result[0].content[0].filename + expect(filename).toBe("Users foo Downloads SOP pdf") + expect(invalid.test(filename)).toBe(false) + }) + + test("drops the filename when nothing valid remains so the SDK generates one", () => { + const result = ProviderTransform.message(fileMessage("://:/"), bedrockModel, {}) as any[] + expect(result[0].content[0].filename).toBeUndefined() + }) + + test("leaves filenames untouched for non-Bedrock providers", () => { + const anthropicModel = { + ...bedrockModel, + id: "anthropic/claude-sonnet-4", + providerID: "anthropic", + api: { id: "claude-sonnet-4", url: "https://api.anthropic.com", npm: "@ai-sdk/anthropic" }, + } + const result = ProviderTransform.message(fileMessage("slack-file://F0ABC123"), anthropicModel, {}) as any[] + expect(result[0].content[0].filename).toBe("slack-file://F0ABC123") + }) +}) + describe("ProviderTransform.message - empty image handling", () => { const mockModel = { id: "anthropic/claude-3-5-sonnet",