You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Attached AVIF images are silently dropped from model context in V2 #41722
In OpenCode V2, pasting or @-mentioning an AVIF image attachment in the TUI inserts an [Image N] mention label, but the image is silently dropped when building the model request: to-llm-message.ts only forwards image/png, image/jpeg, image/gif, and image/webp as media, and attachmentContent returns [] for any other image mime. The model sees the [Image N] label but never receives the image content, so it may hallucinate or fail to work with the attachment. The TUI file picker explicitly supports .avif (see local-attachment.ts mime table), so the format is accepted on input but vanishes on output.
Environment
opencode version: v2 (next)
OS: macOS
Install/channel: next
Reproduction
In the V2 TUI, paste a local .avif image path (supported by the attachment picker)
The TUI inserts [Image 1] and shows the attachment
Submit the prompt and inspect the model request: no media part and no text fallback for the image
Related: #17772 requested AVIF support or auto-conversion; the format is still dropped today. Unsupported formats like image/tiff hit the same path.
Adding scope to this report rather than opening a second one, since it is the same attachmentContent fall-through. Two things beyond the image formats:
1. It is not only images.attachmentContent (packages/core/src/session/runner/to-llm-message.ts:75-83) branches on text/plain, application/x-directory, image/{png,jpeg,gif,webp} and application/pdf, then falls through to return [] for everything else. Mime.detect (packages/core/src/mime.ts:16) returns application/octet-stream for any non-text bytes, and session/prompt.ts:113 uses it when the attachment carries no mime — so an ordinary .docx or .zip attached from disk lands on exactly this path. That is the default, not an edge case.
2. The message can disappear entirely. The caller is to-llm-message.ts:266-272:
With an unsupported attachment and empty text, the whole user message is dropped and the step is sent with no new input.
There is already a convention for the fix: SessionModelRequest.unsupportedParts turns input it cannot represent into a visible part — ERROR: Cannot read "logo.png" (this model does not support image input). Inform the user. (model-request.ts:129-136, test "replaces unsupported user media with a visible error"). It only inspects parts that exist, so it never sees an attachment that attachmentContent already returned [] for. The v1 migration renders the same idea as [Attachment unavailable after migration: … (application/octet-stream)] (test/v1-migration.test.ts:198).
A visible fallback in attachmentContent would make this consistent with both. I can send a PR with that if useful — is that the shape you want, or would you prefer it surface as an explicit error?
Thanks for the detailed report. I can confirm this on v2.2.0, and it still happens on the latest dev (b7992598).
Steps:
With a model that supports image input, paste the path of a small .avif file into the TUI prompt. The prompt shows [Image 1] and a file red.avif chip.
Submit. The request sent to the model has only the text Describe this image: [Image 1] : no image part, no Attached file text, no warning.
Pasting a .png the same way does send an image_url (data:image/png;base64,…) part.
Even with a text-only model, the PNG gets an explicit "this model does not support image input" note, but the AVIF still disappears with nothing in its place.
The screenshot and recording below show the AVIF turn, where the model received text only, next to the PNG turn, which included the image. The script below reproduces it with a simulated model.
This matches what you described: to-llm-message.ts only forwards png/jpeg/gif/webp, and attachmentContent returns nothing for other image types, even though the TUI accepts them. Related: #17772 (an earlier AVIF request, where the provider rejected the image instead of OpenCode dropping it).
recording.mp4
Reproduction script (drive.ts)
// Reproduces anomalyco/opencode#41722: a pasted AVIF image gets an [Image N]// label in the TUI, but the request sent to the model contains no image part.// A PNG is pasted the same way as a control.import{Effect,Stream}from"effect"import{Llm,OpenCodeDriver}from"opencode-drive"import{execFileSync}from"node:child_process"import{mkdirSync}from"node:fs"constdir="/tmp/opencode/repro-41722"mkdirSync(dir,{recursive: true})constavif=`${dir}/red.avif`constpng=`${dir}/blue.png`// tiny real images (requires ffmpeg with libaom)execFileSync("ffmpeg",["-y","-loglevel","error","-f","lavfi","-i","color=c=red:s=64x64","-frames:v","1","-c:v","libaom-av1","-still-picture","1",avif])execFileSync("ffmpeg",["-y","-loglevel","error","-f","lavfi","-i","color=c=blue:s=64x64","-frames:v","1",png])constpaste=(text: string)=>`\x1b[200~${text}\x1b[201~`constsummarize=(body: unknown)=>{constjson=JSON.stringify(body)constlast=(bodyasany)?.messages?.at(-1)constcontent=Array.isArray(last?.content) ? last.content : [last?.content]return{parts: content.map((p: any)=>typeofp==="string" ? `text:${JSON.stringify(p)}` : p?.type==="text" ? `text:${JSON.stringify(p.text)}` : `${p?.type}${p?.image_url ? ":"+String(p.image_url.url).slice(0,30) : ""}`,),hasAvif: json.includes("image/avif"),hasPng: json.includes("image/png"),}}exportdefaultOpenCodeDriver.use({
...(process.env.OC_DEV ? {opencode: {dev: process.env.OC_DEV}} : {}),tui: {recording: true},// make the simulated model accept image input, like any vision modelsetup: ({ config })=>Effect.sync(()=>{constmodel=(configasany).providers.simulation.models["gpt-sim-model"]model.capabilities.input=["text","image"]}),},({ ui, llm })=>Effect.gen(function*(){yield*llm.serve((request,index)=>{consts=summarize(request.body)console.log(`REQUEST ${index}: ${JSON.stringify(s)}`)returnStream.make(Llm.text(`Request ${index+1} parts: ${s.parts.join(" | ")}`))})yield*ui.type("Describe this image: ")yield*ui.type(paste(avif))yield*ui.waitFor("[Image 1]")yield*ui.screenshot("avif-attached")yield*ui.enter()yield*ui.waitFor("parts:",{timeout: 20000})yield*Effect.sleep("1 second")yield*ui.screenshot("avif-response")yield*ui.type("And this one: ")yield*ui.type(paste(png))yield*ui.waitFor("[Image")yield*ui.enter()yield*ui.waitFor("Request 2",{timeout: 20000})yield*Effect.sleep("1 second")yield*ui.screenshot("png-response")}),)
Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.
Activity
github-actions commentedon Aug 11, 2026
This issue might be a duplicate of or closely related to existing issues. Please check:
feiiiiii5 commentedon Oct 1, 2026
Adding scope to this report rather than opening a second one, since it is the same
attachmentContentfall-through. Two things beyond the image formats:1. It is not only images.
attachmentContent(packages/core/src/session/runner/to-llm-message.ts:75-83) branches ontext/plain,application/x-directory,image/{png,jpeg,gif,webp}andapplication/pdf, then falls through toreturn []for everything else.Mime.detect(packages/core/src/mime.ts:16) returnsapplication/octet-streamfor any non-text bytes, andsession/prompt.ts:113uses it when the attachment carries no mime — so an ordinary.docxor.zipattached from disk lands on exactly this path. That is the default, not an edge case.2. The message can disappear entirely. The caller is
to-llm-message.ts:266-272:With an unsupported attachment and empty text, the whole user message is dropped and the step is sent with no new input.
There is already a convention for the fix:
SessionModelRequest.unsupportedPartsturns input it cannot represent into a visible part —ERROR: Cannot read "logo.png" (this model does not support image input). Inform the user.(model-request.ts:129-136, test "replaces unsupported user media with a visible error"). It only inspects parts that exist, so it never sees an attachment thatattachmentContentalready returned[]for. The v1 migration renders the same idea as[Attachment unavailable after migration: … (application/octet-stream)](test/v1-migration.test.ts:198).A visible fallback in
attachmentContentwould make this consistent with both. I can send a PR with that if useful — is that the shape you want, or would you prefer it surface as an explicit error?opencodejr commentedon Oct 28, 2026
Thanks for the detailed report. I can confirm this on v2.2.0, and it still happens on the latest
dev(b7992598).Steps:
.aviffile into the TUI prompt. The prompt shows[Image 1]and afile red.avifchip.Describe this image: [Image 1]: no image part, noAttached filetext, no warning..pngthe same way does send animage_url(data:image/png;base64,…) part.The screenshot and recording below show the AVIF turn, where the model received text only, next to the PNG turn, which included the image. The script below reproduces it with a simulated model.
This matches what you described:
to-llm-message.tsonly forwards png/jpeg/gif/webp, andattachmentContentreturns nothing for other image types, even though the TUI accepts them. Related: #17772 (an earlier AVIF request, where the provider rejected the image instead of OpenCode dropping it).recording.mp4
Reproduction script (drive.ts)
Run it with opencode-drive:
opencode-drive run ./drive.ts. It drivesopencodefrom yourPATH; setOC_DEVto the path of an OpenCode checkout to run that from source instead.