Generated images are still dropped: processor.ts has no case "file", and issue #12859 was closed by a stale bot rather than by a fix.
Summary
A model that returns images (OpenRouter's google/gemini-3-pro-image, "Nano Banana", and every other image-output model) produces choices[].message.images[]. @openrouter/ai-sdk-provider maps those to AI SDK file stream parts. packages/opencode/src/session/processor.ts switches on reasoning-*, tool-*, text-*, step-* and finish; there is no case "file", so the part falls to the default branch and is discarded.
The turn completes, the tokens are billed, and nothing is shown or stored. There is no error and no warning — from the outside it is indistinguishable from a model that chose not to draw anything.
This was already reported as #12859, and a fix was already written as #12871. The PR was closed unmerged and the issue was auto-closed by a stale bot. Neither closure reflects a fix, so I am reopening the report with independent evidence.
Evidence from a real run
I traced a single OpenCode turn against google/gemini-3-pro-image through OpenRouter:
- The model ran and returned 1,220 output tokens of image data.
- OpenRouter billed $0.035208 for the turn.
- OpenCode's SQLite store contains zero parts of type
file across all 73 sessions on that machine — the image never reached storage.
- The only large blob in that session is a Gemini thought signature in
part.metadata, not the image.
So the bytes were paid for and then thrown away before anything could persist them.
Reproduction
- Configure an OpenRouter provider with an image-output model, e.g.
google/gemini-3-pro-image.
- Ask it to generate an image.
- Observe: the turn finishes with no error, no image is rendered, and
SELECT * FROM part WHERE type = 'file' returns nothing for the session.
Two separate paths, and only one is broken
Worth separating, because they fail for different reasons:
- Model-generated images — broken here, in
processor.ts. This report.
- Tool attachments —
ToolStateCompleted.attachments?: Array<FilePart> IS populated today (MCP tools returning images, screenshots). That path works upstream; consumers that ignore the field drop the image themselves.
We fixed the second on our side and are ready for the first the moment it lands.
Downstream context
DorkOS (https://github.com/dork-labs/dorkos) runs OpenCode as one of its agent runtimes. We have just shipped the receiving half end to end: a file part and a tool attachment both become stored bytes plus an image_attachment event carrying a URL, rendered inline in the transcript, with the image kept behind a size-capped attachment store rather than inlined on the replayed event stream.
Our file-part branch is exercised by tests against synthetic file parts and is inert against a real sidecar today, purely because upstream never emits one. We would rather delete our workaround than carry it.
Suggested fix
Restore the change in #12871: add a case "file" to the processor that persists the part (mime, url/data payload, optional filename) the same way other content parts are persisted, so it reaches the session store and the event stream.
Happy to test a build against a real image model and report back.
Generated images are still dropped:
processor.tshas nocase "file", and issue #12859 was closed by a stale bot rather than by a fix.Summary
A model that returns images (OpenRouter's
google/gemini-3-pro-image, "Nano Banana", and every other image-output model) produceschoices[].message.images[].@openrouter/ai-sdk-providermaps those to AI SDKfilestream parts.packages/opencode/src/session/processor.tsswitches onreasoning-*,tool-*,text-*,step-*andfinish; there is nocase "file", so the part falls to the default branch and is discarded.The turn completes, the tokens are billed, and nothing is shown or stored. There is no error and no warning — from the outside it is indistinguishable from a model that chose not to draw anything.
This was already reported as #12859, and a fix was already written as #12871. The PR was closed unmerged and the issue was auto-closed by a stale bot. Neither closure reflects a fix, so I am reopening the report with independent evidence.
Evidence from a real run
I traced a single OpenCode turn against
google/gemini-3-pro-imagethrough OpenRouter:fileacross all 73 sessions on that machine — the image never reached storage.part.metadata, not the image.So the bytes were paid for and then thrown away before anything could persist them.
Reproduction
google/gemini-3-pro-image.SELECT * FROM part WHERE type = 'file'returns nothing for the session.Two separate paths, and only one is broken
Worth separating, because they fail for different reasons:
processor.ts. This report.ToolStateCompleted.attachments?: Array<FilePart>IS populated today (MCP tools returning images, screenshots). That path works upstream; consumers that ignore the field drop the image themselves.We fixed the second on our side and are ready for the first the moment it lands.
Downstream context
DorkOS (https://github.com/dork-labs/dorkos) runs OpenCode as one of its agent runtimes. We have just shipped the receiving half end to end: a
filepart and a tool attachment both become stored bytes plus animage_attachmentevent carrying a URL, rendered inline in the transcript, with the image kept behind a size-capped attachment store rather than inlined on the replayed event stream.Our
file-part branch is exercised by tests against syntheticfileparts and is inert against a real sidecar today, purely because upstream never emits one. We would rather delete our workaround than carry it.Suggested fix
Restore the change in #12871: add a
case "file"to the processor that persists the part (mime,url/data payload, optionalfilename) the same way other content parts are persisted, so it reaches the session store and the event stream.Happy to test a build against a real image model and report back.