Summary
In OpenCode v2, the capability gate that strips unsupported media from user attachments does not apply to images returned by the read tool. The image is stored as a base64 data URI inside the session history (tool part state.content) and is sent to models whose input capability is text-only (e.g. glm-5.3 with input: ["text"]).
The provider then rejects every request:
Error: messages.content.type is invalid, allowed values: ['text']
The failed assistant message persists in history, so all subsequent requests in that session fail with the same 400 — the session becomes permanently unusable. There is no in-app way to remove the media from history.
There is also a second path that involves no tool call at all: a session where a vision-capable model previously read images gets killed by simply switching the session's model to a text-only one, because the stored images are now sent to a model that cannot accept them.
Reproduction — Path A (read tool)
- Use a text-only model (e.g.
glm-5.3, capabilities input: ["text"])
- Ask the agent to read a local image file (the
read tool returns it as a base64 data URI)
- The image bypasses the capabilities gate and is included in the LLM request
- GLM API returns 400; the failed message stays in history; every follow-up turn fails with the same 400 (session dead)
Observed live (2026-09-28): a session accumulated 4 image file parts (~8.5 MB base64) in history; every subsequent request failed with 400 in ~300 ms until the history was manually repaired.
Reproduction — Path B (model switch)
- With a vision-capable model (e.g.
glm-5.3-flash, input: ["text","image",...]), read an image — this passes, as it should
- Switch the session's model to a text-only one (e.g.
glm-5.3)
- The next request includes the stored image → 400 → session dead
Expected
The request composer should apply the same capability gate to image outputs of the read tool (file parts in tool state.content) as it does to user attachments — replace with a short text notice (or exclude) when the model's input capabilities do not include "image". Because filtering happens at request-composition time, this would also fix Path B (model switching).
Actual
400 messages.content.type is invalid, allowed values: ['text'] on every request; session marked failed. Recovery required directly editing the session DB (replacing base64 blobs in session_message.data with a text notice) — no in-app recovery path exists.
Notes
- Videos appear to take a different path (excluded from the LLM message), so images and videos are treated asymmetrically.
- Local workaround in use: a plugin hooking
execute.before on the read tool that blocks image reads for text-only models. Effective for Path A; cannot prevent Path B since model switching is not a tool call.
- Three live occurrences in a single day of normal use, all on GLM.
Environment
- opencode: v2.0.18 (Windows standalone binary)
- OS: Windows 11 Pro (10.0.26200), x64
- Terminal: Orca embedded terminal
- Shell: cmd.exe
- Install: release channel
- Plugins: 15 local files auto-scanned from
~/.config/opencode/plugins (includes the custom read-blocking gate described above)
Summary
In OpenCode v2, the capability gate that strips unsupported media from user attachments does not apply to images returned by the
readtool. The image is stored as a base64 data URI inside the session history (tool partstate.content) and is sent to models whose input capability is text-only (e.g.glm-5.3withinput: ["text"]).The provider then rejects every request:
The failed assistant message persists in history, so all subsequent requests in that session fail with the same 400 — the session becomes permanently unusable. There is no in-app way to remove the media from history.
There is also a second path that involves no tool call at all: a session where a vision-capable model previously read images gets killed by simply switching the session's model to a text-only one, because the stored images are now sent to a model that cannot accept them.
Reproduction — Path A (read tool)
glm-5.3, capabilitiesinput: ["text"])readtool returns it as a base64 data URI)Observed live (2026-09-28): a session accumulated 4 image file parts (~8.5 MB base64) in history; every subsequent request failed with 400 in ~300 ms until the history was manually repaired.
Reproduction — Path B (model switch)
glm-5.3-flash,input: ["text","image",...]), read an image — this passes, as it shouldglm-5.3)Expected
The request composer should apply the same capability gate to image outputs of the
readtool (file parts in toolstate.content) as it does to user attachments — replace with a short text notice (or exclude) when the model's input capabilities do not include"image". Because filtering happens at request-composition time, this would also fix Path B (model switching).Actual
400
messages.content.type is invalid, allowed values: ['text']on every request; session marked failed. Recovery required directly editing the session DB (replacing base64 blobs insession_message.datawith a text notice) — no in-app recovery path exists.Notes
execute.beforeon thereadtool that blocks image reads for text-only models. Effective for Path A; cannot prevent Path B since model switching is not a tool call.Environment
~/.config/opencode/plugins(includes the custom read-blocking gate described above)