Repository navigation
Description
Summary
OpenCode re-sends every image that was ever pasted or returned by Read in a session on every model request, with no pruning. On backends that enforce a per-prompt image limit (e.g. vLLM with --limit-mm-per-prompt '{"image":4}'), the session becomes permanently unusable once the history accumulates more images than the cap: every subsequent request fails instantly with a 400, and since the offending images are in history, re-prompting can never recover the session.
Environment
- opencode version: 2.2.0
- OS: Linux 7.0.0-38-generic (Ubuntu, x86_64)
- Terminal: TERM=xterm-256color, COLORTERM=truecolor
- Shell: /bin/bash
- Install/channel: latest (opencode v2.2.0 via ~/.opencode/bin)
- Active plugins: none at time of failure (a local
image-budgetplugin was added afterwards as a workaround)
Reproduction
- Point OpenCode at an OpenAI-compatible server that caps images per request, e.g. vLLM started with
--limit-mm-per-prompt '{"image":4}'. - In one session, paste 2 screenshots and let the agent
Read3 PNG files across later turns (5 images total in history). - Ask anything, or ask the agent to check one more rendered image.
Expected Behavior
The request still succeeds: stale images (tool-result screenshots the model already described in text, superseded screenshots) are pruned from the outgoing request — e.g. replaced by a short text placeholder like Claude Code does — keeping only the newest N images, where N matches the provider's supported per-prompt image count (configurable per model/provider).
Actual Behavior
Every request carries all 5 history images and is rejected at ~46 ms:
Error: At most 4 image(s) may be provided in one prompt. (parameter=image)
The failure is sticky forever: the images live in persisted history and are re-sent on every turn, including new user messages, so the session can never continue. The only current recovery is manual (undo across image turns, or editing the session database).
Additional Context
- Reproduced consistently; any session that iterates on rendered screenshots (a very common agent loop: render → Read PNG → adjust) inevitably crosses the cap.
- Anthropic-compatible clients such as Claude Code avoid this by stripping old image payloads from the request history after the model has consumed them, leaving a text placeholder; persisted history is unaffected. OpenCode sending history verbatim is the difference, not the server cap.
- The V2 plugin system can half-fix it (
ctx.session.hook("context")mutatingevent.messages), which I'm using now, but a built-in policy is better: it needs no per-user plugin, and only OpenCode knows which images are stale vs. freshly attached in the current turn. - Suggested design: a per-model/provider setting (e.g.
limit.imagesPerPromptor reuseattachment/modalities metadata) plus default pruning of image parts older than the current turn (or last-N), with the trimmed placeholder text explaining the image was pruned.
Activity
opencodejr commentedon Oct 28, 2026
Thanks for the detailed write-up and for sharing your workaround!
In short, you're asking for OpenCode to drop old images from the request it sends to the model (keeping the newest N, or only the current turn's, with a text placeholder for the rest), and for a per-model/provider setting for how many images one prompt may include. That way, backends that limit images per prompt, like vLLM with
--limit-mm-per-prompt, don't end up rejecting every request in a session.Right now OpenCode sends the full history to the model as stored, and it has no setting for a per-prompt image limit. So I've filed this as a feature request rather than a bug. Until there's a built-in option, a plugin using the
contextsession hook, like the one you're running, is the way to handle it. Maintainers will consider the request.Sugamsss commentedon Oct 28, 2026
One thing worth knowing before this lands: #59303 caps at a fixed 30 images. Your cap is 4, so a session with more than 30 screenshots would still fail for you — 30 images go out, the backend rejects the request.
Until core has a per-model number, the workaround is to set it yourself. I maintain
opencode-prune-images, which readsOPENCODE_MAX_IMAGES, soOPENCODE_MAX_IMAGES=4keeps the newest 4 as real images and turns the rest into text cards that carry a path to read them again.Your setup is the useful data point here: 30 is fine for the hosted gateway routes, but a self-hosted vLLM can be set to any number.
chipfighter commentedon Nov 3, 2026
One option alongside the fixed last-N policy is to retain references to earlier images and let the agent retrieve an original when needed. That keeps a long visual task usable without making every previous screenshot part of every request.
Disclosure: I maintain FrameKeeper, which implements this locally: earlier-turn images become short numbered references, and the model can fetch an original through an MCP tool. There is a new OpenCode adapter using the provider's base URL setting; no panel is required.
I have not validated that adapter against your OpenCode 2.2.0 / vLLM setup, so this is a candidate to experiment with, not a confirmed fix. It also does not guarantee your four-image cap: images needed within the current turn can still exceed it. A provider-aware budget remains useful even with retrieval. Your existing context-hook workaround addresses that stricter requirement directly.
Sharing in case on-demand access to older screenshots is useful to this workflow. Written with Codex under the project owner's direction.
Sugamsss commentedon Nov 4, 2026
Yes, that’s the approach opencode-prune-images already takes: older screenshots become context cards with observations and a cached file path, so the agent can read the original again when needed. Recall is best-effort because the cache is bounded, but the goal is exactly this—keep useful context without resending every image on every turn.
holny commentedon Nov 4, 2026
I'd like to take this. The shape I'd go with: prune image parts from older turns at request-assembly time, keep the newest N images (configurable, with a default that works under common caps like vLLM's 4 and the 30-image ceiling from #59303), and replace each pruned image with a short text placeholder noting the original file path so
Readcan pull it back when the agent needs it. That follows the direction discussed here without conflicting with #59303's fixed cap. I'll base it onorigin/devand cover both the pruning and the placeholder path with tests.