Skip to content

Commit 72de9fe

Browse files
fix(opencode): Fixes image reading with OpenAI-compatible providers like Kimi K2.5. (#11323)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
1 parent bec1148 commit 72de9fe

1 file changed

Lines changed: 54 additions & 2 deletions

File tree

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

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,26 @@ export namespace MessageV2 {
438438
export function toModelMessages(input: WithParts[], model: Provider.Model): ModelMessage[] {
439439
const result: UIMessage[] = []
440440
const toolNames = new Set<string>()
441+
// Track media from tool results that need to be injected as user messages
442+
// for providers that don't support media in tool results.
443+
//
444+
// OpenAI-compatible APIs only support string content in tool results, so we need
445+
// to extract media and inject as user messages. Other SDKs (anthropic, google,
446+
// bedrock) handle type: "content" with media parts natively.
447+
//
448+
// Only apply this workaround if the model actually supports image input -
449+
// otherwise there's no point extracting images.
450+
const supportsMediaInToolResults = (() => {
451+
if (model.api.npm === "@ai-sdk/anthropic") return true
452+
if (model.api.npm === "@ai-sdk/openai") return true
453+
if (model.api.npm === "@ai-sdk/amazon-bedrock") return true
454+
if (model.api.npm === "@ai-sdk/google-vertex/anthropic") return true
455+
if (model.api.npm === "@ai-sdk/google") {
456+
const id = model.api.id.toLowerCase()
457+
return id.includes("gemini-3") && !id.includes("gemini-2")
458+
}
459+
return false
460+
})()
441461

442462
const toModelOutput = (output: unknown) => {
443463
if (typeof output === "string") {
@@ -514,6 +534,7 @@ export namespace MessageV2 {
514534

515535
if (msg.info.role === "assistant") {
516536
const differentModel = `${model.providerID}/${model.id}` !== `${msg.info.providerID}/${msg.info.modelID}`
537+
const media: Array<{ mime: string; url: string }> = []
517538

518539
if (
519540
msg.info.error &&
@@ -545,11 +566,23 @@ export namespace MessageV2 {
545566
if (part.state.status === "completed") {
546567
const outputText = part.state.time.compacted ? "[Old tool result content cleared]" : part.state.output
547568
const attachments = part.state.time.compacted ? [] : (part.state.attachments ?? [])
569+
570+
// For providers that don't support media in tool results, extract media files
571+
// (images, PDFs) to be sent as a separate user message
572+
const isMediaAttachment = (a: { mime: string }) =>
573+
a.mime.startsWith("image/") || a.mime === "application/pdf"
574+
const mediaAttachments = attachments.filter(isMediaAttachment)
575+
const nonMediaAttachments = attachments.filter((a) => !isMediaAttachment(a))
576+
if (!supportsMediaInToolResults && mediaAttachments.length > 0) {
577+
media.push(...mediaAttachments)
578+
}
579+
const finalAttachments = supportsMediaInToolResults ? attachments : nonMediaAttachments
580+
548581
const output =
549-
attachments.length > 0
582+
finalAttachments.length > 0
550583
? {
551584
text: outputText,
552-
attachments,
585+
attachments: finalAttachments,
553586
}
554587
: outputText
555588

@@ -593,6 +626,25 @@ export namespace MessageV2 {
593626
}
594627
if (assistantMessage.parts.length > 0) {
595628
result.push(assistantMessage)
629+
// Inject pending media as a user message for providers that don't support
630+
// media (images, PDFs) in tool results
631+
if (media.length > 0) {
632+
result.push({
633+
id: Identifier.ascending("message"),
634+
role: "user",
635+
parts: [
636+
{
637+
type: "text" as const,
638+
text: "Attached image(s) from tool result:",
639+
},
640+
...media.map((attachment) => ({
641+
type: "file" as const,
642+
url: attachment.url,
643+
mediaType: attachment.mime,
644+
})),
645+
],
646+
})
647+
}
596648
}
597649
}
598650
}

0 commit comments

Comments
 (0)