anomalyco / anomalyco/opencode
Images in tool results bypass model modality check (text-only models receive base64 images)
@kitlangton is already working on this.
Since Jul 31, 2026.
- Dominant language
- TypeScript
- Stars
- 209k
- Forks
- 27.5k
- PR merge metrics
- PR metrics pending
Description
Summary
When a model is configured as text-only (modalities.input: ["text"], i.e. capabilities.input.image === false), image attachments inside tool results are still sent to the API as base64 data URLs. The API then rejects the entire request with a 400 error, which permanently breaks the session — every subsequent message replays the same image-bearing tool result.
Root cause
There are two code paths that should strip unsupported media, but only one of them checks the model's input capabilities, and the other is where tool-result images actually live:
-
unsupportedParts(packages/opencode/src/provider/transform.ts) — correctly checksmodel.capabilities.input[modality]and replaces unsupportedimage/fileparts with a text error. But it only inspects top-level parts ofusermessages. It does not look insidetool-resultparts. -
toModelMessagesEffect(packages/opencode/src/session/message-v2.ts) — decides whether to keep media embedded in a tool result or extract it into a synthetic user message, viasupportsMediaInToolResult. That function returnstruefor@ai-sdk/openai(and others), so the image stays embedded in the tool-result part — exactly whereunsupportedPartsnever looks.
The result: for providers where supportsMediaInToolResult is true, images in tool results bypass the modality gate entirely.
Reproduction
-
Configure a text-only model (e.g. via a custom OpenAI-compatible provider whose endpoint rejects image input):
"glm-5.2": { "modalities": { "input": ["text"], "output": ["text"] } }Provider npm:
@ai-sdk/openai. -
In a session, have the agent use the
readtool on a PNG file (e.g. a screenshot). The tool result carries the image aspart.state.attachments[].url = "data:image/png;base64,...". -
The next LLM call fails with an API 400, e.g.:
Model only support text input statusCode: 400 isRetryable: false -
Every subsequent message in the session replays the same tool-result history and fails identically — the session is bricked.
Expected behaviour
capabilities.input.image === false should be honored regardless of where the image sits in the message history. Two possible fixes:
- Option A — extend
unsupportedPartsto also walk the content oftool-resultparts and strip/replace unsupported media there. - Option B — make
supportsMediaInToolResultcapability-aware: returnfalsewhenmodel.capabilities.input.imageisfalse, so the image gets extracted into a synthetic user message whereunsupportedPartscan catch it.
Option B is the smaller change and reuses the existing extraction + filtering pipeline.
Environment
- opencode 1.18.5 (Homebrew)
- Provider: custom,
npm: "@ai-sdk/openai", baseURL pointing at a Volcano Engine (Ark) Responses-API endpoint - Model:
glm-5.2,modalities.input: ["text"]
Workaround
Manually edit the session database (~/.local/share/opencode/opencode.db, part table) to strip the data:image/...;base64,... URL from the offending state.attachments entry, replacing it with a data:text/plain;stripped,... placeholder.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.