From d25a8d6dc1d23974d75603096f40e583e4a5cdad Mon Sep 17 00:00:00 2001 From: Aaron Hogue Date: Wed, 16 Sep 2026 21:41:27 -0400 Subject: [PATCH 1/2] fix(session): only keep Bedrock tool-result images for Anthropic and Nova models Bedrock Converse only accepts images inside toolResult for Anthropic and Amazon Nova models; every other provider on Bedrock returns 400 "This model doesn't support the image field for user messages" and the session is stuck because the image stays in history. Hoist tool-result images into a user message for all other Bedrock model IDs, which is the path already used for Bedrock PDFs. Co-Authored-By: claude-fable-5-1 --- packages/opencode/src/session/message-v2.ts | 6 +- .../opencode/test/session/message-v2.test.ts | 97 +++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 9b3f2c46f405..be2850d68796 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -148,7 +148,11 @@ export const toModelMessagesEffect = Effect.fnUntraced(function* ( if (model.api.npm === "@ai-sdk/anthropic") return true if (model.api.npm === "@ai-sdk/openai") return true if (model.api.npm === "@ai-sdk/amazon-bedrock/mantle") return true - if (model.api.npm === "@ai-sdk/amazon-bedrock") return attachment.mime.startsWith("image/") + if (model.api.npm === "@ai-sdk/amazon-bedrock") { + // Converse only accepts toolResult images for Anthropic and Amazon Nova models. + if (!/(^|\.)(anthropic\.|amazon\.nova)/.test(model.api.id.toLowerCase())) return false + return attachment.mime.startsWith("image/") + } if (model.api.npm === "@ai-sdk/xai") return attachment.mime.startsWith("image/") if (model.api.npm === "@ai-sdk/google-vertex/anthropic") return true if (model.api.npm === "@ai-sdk/google") { diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts index 734a30e42454..890a8374907d 100644 --- a/packages/opencode/test/session/message-v2.test.ts +++ b/packages/opencode/test/session/message-v2.test.ts @@ -601,6 +601,103 @@ describe("session.message-v2.toModelMessage", () => { ]) }) + // Bedrock Converse only accepts toolResult images for Anthropic and Amazon Nova models: + // https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ToolResultContentBlock.html + test.each([ + ["anthropic.claude-sonnet-4-6", "keeps"], + ["us.amazon.nova-pro-v1:0", "keeps"], + ["global.openai.gpt-6-astra", "hoists"], + ["us.mistral.pixtral-large-2502-v1:0", "hoists"], + ] as const)("bedrock %s %s image tool-result media", async (apiID, expected) => { + const bedrockModel: Provider.Model = { + ...model, + id: ModelV2.ID.make(`amazon-bedrock/${apiID}`), + providerID: ProviderV2.ID.make("amazon-bedrock"), + api: { + id: apiID, + url: "https://bedrock-runtime.us-east-1.amazonaws.com", + npm: "@ai-sdk/amazon-bedrock", + }, + capabilities: { + ...model.capabilities, + attachment: true, + input: { + ...model.capabilities.input, + image: true, + }, + }, + } + const png = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]).toString("base64") + const userID = `m-user-${apiID}` + const assistantID = `m-assistant-${apiID}` + const input: SessionV1.WithParts[] = [ + { + info: userInfo(userID), + parts: [{ ...basePart(userID, `u1-${apiID}`), type: "text", text: "run tool" }] as SessionV1.Part[], + }, + { + info: assistantInfo(assistantID, userID), + parts: [ + { + ...basePart(assistantID, `a1-${apiID}`), + type: "tool", + callID: `call-${apiID}`, + tool: "read", + state: { + status: "completed", + input: { filePath: "/tmp/example.png" }, + output: "Image read successfully", + title: "Read", + metadata: {}, + time: { start: 0, end: 1 }, + attachments: [ + { + ...basePart(assistantID, `file-${apiID}`), + type: "file", + mime: "image/png", + filename: "example.png", + url: `data:image/png;base64,${png}`, + }, + ], + }, + }, + ] as SessionV1.Part[], + }, + ] + + const result = await MessageV2.toModelMessages(input, bedrockModel) + const toolMessage = result[2] + expect(toolMessage.role).toBe("tool") + + if (expected === "keeps") { + expect(result).toHaveLength(3) + expect(toolMessage.content[0]).toMatchObject({ + type: "tool-result", + output: { + type: "content", + value: [ + { type: "text", text: "Image read successfully" }, + { type: "media", mediaType: "image/png", data: png }, + ], + }, + }) + return + } + + expect(result).toHaveLength(4) + expect(toolMessage.content[0]).toMatchObject({ + type: "tool-result", + output: { type: "text", value: "Image read successfully" }, + }) + expect(result[3]).toStrictEqual({ + role: "user", + content: [ + { type: "text", text: "Attached media from tool result:" }, + { type: "file", mediaType: "image/png", filename: "example.png", data: `data:image/png;base64,${png}` }, + ], + }) + }) + test("omits provider metadata when assistant model differs", async () => { const userID = "m-user" const assistantID = "m-assistant" From 0c95ac55e1d61d0ba1f928e9b96428b92b74be6c Mon Sep 17 00:00:00 2001 From: Aaron Hogue Date: Wed, 16 Sep 2026 22:21:05 -0400 Subject: [PATCH 2/2] test(session): cover unprefixed Nova ID in Bedrock tool-result table Co-Authored-By: claude-fable-5-1 --- packages/opencode/test/session/message-v2.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts index 890a8374907d..b828de03cfe1 100644 --- a/packages/opencode/test/session/message-v2.test.ts +++ b/packages/opencode/test/session/message-v2.test.ts @@ -606,6 +606,7 @@ describe("session.message-v2.toModelMessage", () => { test.each([ ["anthropic.claude-sonnet-4-6", "keeps"], ["us.amazon.nova-pro-v1:0", "keeps"], + ["amazon.nova-pro-v1:0", "keeps"], ["global.openai.gpt-6-astra", "hoists"], ["us.mistral.pixtral-large-2502-v1:0", "hoists"], ] as const)("bedrock %s %s image tool-result media", async (apiID, expected) => {