Skip to content

Images pasted with a custom-provider model are silently replaced by a "[Attached image/png: ...]" text placeholder — no warning, even when the upstream model has vision #47480

Closed as not planned

Description

@aref-alapour

Description

When a model is defined under provider.<id>.models in opencode.json(c) (custom OpenAI-compatible provider), its attachment capability defaults to false unless explicitly set (the bundled model loader resolves attachment: cfg.attachment ?? false for custom models).

If the user pastes/attaches an image in such a session, OpenCode silently rewrites the media part into a plain text part:

[Attached image/png: screenshot.png]

No warning is shown in the TUI. The LLM only ever receives that placeholder — even when the model behind the endpoint fully supports vision. The model then typically hallucinates an explanation ("the current model does not support image input"), which actively misleads the user into concluding their vision model is broken.

Steps to Reproduce

  1. Define a custom provider with a vision-capable model, without attachment:
{
  "provider": {
    "mygw": {
      "options": { "baseURL": "http://localhost:PORT/v1", "apiKey": "{env:KEY}" },
      "models": {
        "my-vision-model": {
          "name": "Vision model",
          "limit": { "context": 1000000, "output": 65536 }
        }
      }
    }
  }
}
  1. Start a session on mygw/my-vision-model.
  2. Paste a screenshot containing visible error text and ask "what does this error say?".
  3. The model answers generically or claims it cannot see images. Inspecting the stored parts shows the image was replaced by the [Attached image/png: ...] text part; the image itself never left OpenCode.

Expected Behavior

  • A visible warning when media is attached to a model with attachment: false (e.g. "model does not support attachments — the image will be sent as a text placeholder only"), or an explicit error instead of silent degradation.
  • The attachment / modalities model keys documented on the custom providers page (I could not find them in the docs and only discovered them by inspecting the bundle).

Actual Behavior

Silent placeholder substitution; the conversation proceeds as if the image had been delivered, and the failure only surfaces as wrong/misleading model answers.

Workaround

Set the capability per model:

"my-vision-model": {
  "attachment": true,
  "modalities": { "input": ["text", "image"] }
}

After this, images are delivered natively — verified end-to-end on OpenCode 1.18.25 (CLI) and the desktop app bundling 1.18.29 against an OpenAI-compatible gateway with Gemini-class heads: the model transcribes pasted screenshots correctly.

Environment

  • OpenCode 1.18.25 CLI + desktop app (bundled server 1.18.27–1.18.29), Windows 11
  • Custom provider using the default @ai-sdk/openai-compatible route

Notes

A related idea: when the active model lacks attachment capability but small_model (or any configured model) is vision-capable, OpenCode could optionally transcribe the image with that model and inject the description as a text part — a built-in fallback that would make images "just work" on text-only models. I currently run this as a chat.message plugin hook and it works well; happy to upstream the approach.

I'd genuinely like to collaborate with the team as a regular contributor — if you point me at where you'd want the warning (TUI vs server) or the fallback, I can prepare a PR.

Activity

  1. github-actions commented on Sep 5, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    If your issue is specifically about the silent text-placeholder substitution with no warning (rather than an outright rejection or missing forwarding), it may be distinct enough to stand on its own — please check the above and close or cross-link as appropriate.

  2. eddoww commented on Sep 18, 2026

    @eddoww

    Confirming this on a self-hosted OpenAI-compatible gateway/router, configured as a custom provider that lives only in opencode.json and uses @ai-sdk/openai-compatible. Two paths hit the same capability gate:

    1. Attaching/pasting an image is rewritten to a [Attached image/png: …] placeholder.
    2. Reading a local image via the Read tool returns ERROR: Cannot read "image.png" (this model does not support image input).

    Minimal repro:

    {
      "provider": {
        "mygateway": {
          "npm": "@ai-sdk/openai-compatible",
          "options": { "baseURL": "https://gateway.example.com/v1" },
          "models": {
            "my-vision-alias": { "name": "my-vision-alias" }
          }
        }
      }
    }

    The upstream route is vision-capable: sending the exact same image directly to the gateway as a base64 image_url data URI via curl works and the model describes it correctly. So this is client-side capability resolution, not the model or the gateway — the custom model's capabilities.input resolves to ["text"] (no models.dev entry, and the gateway's /v1/models carries no modality metadata), so image/* parts are dropped before they reach the already-functional OpenAI-compatible conversion.

    Workaround that works today — declare modalities explicitly per model:

    "my-vision-alias": {
      "name": "my-vision-alias",
      "attachment": true,
      "modalities": { "input": ["text", "image"], "output": ["text"] }
    }

    Related: #44639 (same root cause plus the misleading error), #20802, and the proposed defaults in #21627 / #23501. Flipping the default for custom @ai-sdk/openai-compatible models would remove the need for per-model config that most users won't discover.

  3. saiqulhaq-hh commented on Sep 25, 2026

    @saiqulhaq-hh

    Still hit this in 1.18.32 (newer than the 1.18.25 / 1.18.29 verified versions in this thread). Same LiteLLM OpenAI-compatible custom-provider scenario as @eddoww — litellm.hhstaging.dev/v1 proxying minimax/MiniMax-M3 (model advertises supports_vision: true in the litellm admin UI and accepts the same base64 image_url payload via direct curl with 200 + correct description).

    opencode.jsonc:

    "minimax/MiniMax-M3": {
      "name": "MiniMax M3",
      "attachment": true,
      "modalities": { "input": ["text", "image"] }
    }

    Read tool still returns:

    ERROR: Cannot read "screenshot.png" (this model does not support image input)
    

    Root cause in the binary (verified by strings on the embedded JS bundle in opencode 1.18.32): the conversion cascade for user-defined custom-provider models ends up with Z.capabilities.input as an array ["text","image"], but the read tool's filter does Z.capabilities.input[W] (array index-by-string → undefined). The object form {image: true, ...} is only populated when a models.dev registry entry exists for the model (J.input_modalities?.includes("image")); for custom aliases with no registry entry the cascade falls through to Y?.capabilities.input.image, which is never set because the public user schema doesn't expose capabilities.

    The exact gating line in the read-tool message filter:

    if (Z.capabilities.input[W]) return J;          // W = "image"
    return { type: "text", text: `ERROR: Cannot read ${G ?? W} (this model does not support ${W} input). Inform the user.` };

    Workarounds tried, both inert in 1.18.32:

    • attachment: true — sets a separate capabilities.attachment bool used by the UI affordance; does not populate capabilities.input.image.
    • modalities: { input: ["text", "image"] } — copied into capabilities.input as an array, but the read tool's key lookup still misses.

    Suggested fix: in the user-config → internal-model converter, when no models.dev entry exists for the model, translate modalities.input[] into the object form so capabilities.input.image resolves truthy for the read-tool filter. Or expose capabilities (or just capabilities.input: { image?: boolean, ... }) as a user schema field with the same boolean keys the resolver already consumes.

    Until then, this binary is unusable for vision via custom-provider aliases — even when the upstream endpoint is fully vision-capable.

  4. thdxr commented on Oct 11, 2026

    @thdxr
    Member

    Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.

    — from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions