anomalyco / anomalyco/opencode

Images in tool results bypass model modality check (text-only models receive base64 images)

Open
#39,824 2 comments 0 reactions 1 assignee View on GitHub

@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:

  1. unsupportedParts (packages/opencode/src/provider/transform.ts) — correctly checks model.capabilities.input[modality] and replaces unsupported image/file parts with a text error. But it only inspects top-level parts of user messages. It does not look inside tool-result parts.

  2. 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, via supportsMediaInToolResult. That function returns true for @ai-sdk/openai (and others), so the image stays embedded in the tool-result part — exactly where unsupportedParts never looks.

The result: for providers where supportsMediaInToolResult is true, images in tool results bypass the modality gate entirely.

Reproduction

  1. 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.

  2. In a session, have the agent use the read tool on a PNG file (e.g. a screenshot). The tool result carries the image as part.state.attachments[].url = "data:image/png;base64,...".

  3. The next LLM call fails with an API 400, e.g.:

    Model only support text input
    statusCode: 400
    isRetryable: false
    
  4. 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 unsupportedParts to also walk the content of tool-result parts and strip/replace unsupported media there.
  • Option B — make supportsMediaInToolResult capability-aware: return false when model.capabilities.input.image is false, so the image gets extracted into a synthetic user message where unsupportedParts can 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.