Skip to content

Commit 66a511a

Browse files
committed
fix(opencode): hoist tool-result images for Bedrock OpenAI models
Bedrock Converse rejects images placed inside toolResult.content for OpenAI-family models (openai.gpt-5.6-sol, openai.gpt-6-astra, with or without a us./global. CRIS prefix) with HTTP 400 "This model doesn't support the image field for user messages", even though the same image is accepted in user content. supportsMediaInToolResult treated every @ai-sdk/amazon-bedrock model as image-capable, so a single read of a PNG poisoned the session and every retry failed. Hoist images into the synthetic user message for these models, the same path already used for Bedrock PDFs. Match the whole openai. family rather than only gpt-6-astra (upstream PR #48070) because the failure reproduces on gpt-5.6-sol too. The Mantle path is unchanged; it accepts inline images. Fixes #48069.
1 parent fbdc32f commit 66a511a

2 files changed

Lines changed: 120 additions & 1 deletion

File tree

‎packages/opencode/src/session/message-v2.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,14 @@ export const toModelMessagesEffect = Effect.fnUntraced(function* (
148148
if (model.api.npm === "@ai-sdk/anthropic") return true
149149
if (model.api.npm === "@ai-sdk/openai") return true
150150
if (model.api.npm === "@ai-sdk/amazon-bedrock/mantle") return true
151-
if (model.api.npm === "@ai-sdk/amazon-bedrock") return attachment.mime.startsWith("image/")
151+
if (model.api.npm === "@ai-sdk/amazon-bedrock") {
152+
// OpenAI-family models on Bedrock Converse (openai.gpt-*, with or without a
153+
// CRIS prefix such as us./global.) accept images in user content but reject
154+
// them inside toolResult.content with "This model doesn't support the image
155+
// field for user messages". Hoist them into a user message instead.
156+
if (/(^|\.)openai\./.test(model.api.id)) return false
157+
return attachment.mime.startsWith("image/")
158+
}
152159
if (model.api.npm === "@ai-sdk/xai") return attachment.mime.startsWith("image/")
153160
if (model.api.npm === "@ai-sdk/google-vertex/anthropic") return true
154161
if (model.api.npm === "@ai-sdk/google") {

‎packages/opencode/test/session/message-v2.test.ts‎

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,118 @@ describe("session.message-v2.toModelMessage", () => {
494494
})
495495
})
496496

497+
test.each([
498+
["openai.gpt-6-astra", "@ai-sdk/amazon-bedrock", true],
499+
["us.openai.gpt-6-astra", "@ai-sdk/amazon-bedrock", true],
500+
["global.openai.gpt-5.6-sol", "@ai-sdk/amazon-bedrock", true],
501+
["anthropic.claude-sonnet-4-6", "@ai-sdk/amazon-bedrock", false],
502+
["openai.gpt-5.6-sol", "@ai-sdk/amazon-bedrock/mantle", false],
503+
] as Array<[string, string, boolean]>)(
504+
"handles image tool results for %s via %s",
505+
async (apiID, npm, hoist) => {
506+
const bedrockModel: Provider.Model = {
507+
...model,
508+
id: ModelV2.ID.make("custom-alias"),
509+
providerID: ProviderV2.ID.make("amazon-bedrock"),
510+
api: {
511+
id: apiID,
512+
npm,
513+
url: "https://bedrock-runtime.us-east-2.amazonaws.com",
514+
},
515+
capabilities: {
516+
...model.capabilities,
517+
attachment: true,
518+
input: {
519+
...model.capabilities.input,
520+
image: true,
521+
},
522+
},
523+
}
524+
const userID = "m-user-bedrock-image"
525+
const assistantID = "m-assistant-bedrock-image"
526+
const input: SessionV1.WithParts[] = [
527+
{
528+
info: userInfo(userID),
529+
parts: [
530+
{
531+
...basePart(userID, "u1-bedrock-image"),
532+
type: "text",
533+
text: "read image",
534+
},
535+
] as SessionV1.Part[],
536+
},
537+
{
538+
info: assistantInfo(assistantID, userID),
539+
parts: [
540+
{
541+
...basePart(assistantID, "a1-bedrock-image"),
542+
type: "tool",
543+
callID: "call-bedrock-image-1",
544+
tool: "read",
545+
state: {
546+
status: "completed",
547+
input: { filePath: "/tmp/example.png" },
548+
output: "Image read successfully",
549+
title: "Read",
550+
metadata: {},
551+
time: { start: 0, end: 1 },
552+
attachments: [
553+
{
554+
...basePart(assistantID, "file-bedrock-image-1"),
555+
type: "file",
556+
mime: "image/png",
557+
url: "data:image/png;base64,Zm9v",
558+
},
559+
],
560+
},
561+
},
562+
] as SessionV1.Part[],
563+
},
564+
]
565+
566+
const result = await MessageV2.toModelMessages(input, bedrockModel)
567+
expect(result).toHaveLength(hoist ? 4 : 3)
568+
expect(result[1]).toMatchObject({
569+
role: "assistant",
570+
content: [
571+
{
572+
type: "tool-call",
573+
toolCallId: "call-bedrock-image-1",
574+
toolName: "read",
575+
input: { filePath: "/tmp/example.png" },
576+
},
577+
],
578+
})
579+
expect(result[2]).toMatchObject({
580+
role: "tool",
581+
content: [
582+
{
583+
type: "tool-result",
584+
toolCallId: "call-bedrock-image-1",
585+
toolName: "read",
586+
output: hoist
587+
? { type: "text", value: "Image read successfully" }
588+
: {
589+
type: "content",
590+
value: [
591+
{ type: "text", text: "Image read successfully" },
592+
{ type: "media", mediaType: "image/png", data: "Zm9v" },
593+
],
594+
},
595+
},
596+
],
597+
})
598+
if (hoist)
599+
expect(result[3]).toMatchObject({
600+
role: "user",
601+
content: [
602+
{ type: "text", text: "Attached media from tool result:" },
603+
{ type: "file", mediaType: "image/png", data: "data:image/png;base64,Zm9v" },
604+
],
605+
})
606+
},
607+
)
608+
497609
test("moves bedrock pdf tool-result media into a separate user message", async () => {
498610
const bedrockModel: Provider.Model = {
499611
...model,

0 commit comments

Comments
 (0)