observ_er

your model is text-only.
this is its eyes.

The DeepSeek API is text-only. The variant you actually run — deepseek-v4-flash — returns "this model does not support image input" the second you paste a screenshot. Vision exists only in the web chat, not in the API. observer is a two-file plugin that works around it — it catches every image you paste, has a tiny vision model read it, and hands the words back. No image pollution. No vision budget on your main model.

install two files, one minute read why star/view ★ on GitHub

the problem

deepseek-v4 is text-only at the API. no vision endpoint, no argument.

The api has exactly two names, deepseek-v4-pro and deepseek-v4-flash. Their official capability list is JSON, function calling, thinking mode, and 1M context — the word "image" isn't in it. If you paste a screenshot and ask DeepSeek what's on screen, the model answers honestly: "this model does not support image input."

The vision DeepSeek rolled out lives in the free web chat only; the API exposes no image endpoint at all. So on your normal main model — deepseek-v4-flash, cheap and long-context — a screenshot is just a blob it can't take in. observer steps in: a tiny vision model elsewhere reads the image and hands back plain text DeepSeek can use.

the mechanism

one small pipe, drawn honestly.

No diagram beats a line of prose. Here's the whole shape of it:

you paste a screenshot │ observer saves it, replaces the part with "Image saved: /path/a1b2c3.png" │ your model — text-only, can't take images — calls @observer, the vision sub-agent │ @observer opens the file, writes a description in plain text v words come back — your model answers you.

The image itself never enters your main context, so a base64 blob never touches DeepSeek's big context window. You pay for pixels exactly once, only for the single image in that message.

setup

two files, a model, a restart.

~/.config/opencode/plugins/observer.js
the dispatcher — catches images, saves them, asks the sub-agent to read.
~/.config/opencode/agents/observer.md
the vision model's prompt — how to read a stack, a chart, a page, or text.
  1. Drop the two files where they belong — opencode scans them at startup, no config wiring needed.
  2. Point the agent at a multimodal model you have a key for. This is the only vision step, so keep it cheap:
    model: google/gemini-3.5-flash-lite   # free tier
    # or: openai/gpt-4o, anthropic/claude-sonnet-4-6, moonshotai/kimi-k2.6
  3. Grant the sub-agent one read permission on the scratch image cache:
    {
      "permission": {
        "external_directory": {
          "~/.local/share/opencode/images/**": "allow"
        }
      }
    }
  4. Restart opencode. Paste a screenshot and ask.
Your main model stays whatever it was — deepseek-v4, or the cheapest thing that works. The vision bill is one glance per image, nothing more.
Storage is a scratch cache, not a library. Pasted images live in ~/.local/share/opencode/images/ — when you end a session, delete the folder freely. Nothing is referenced that can't be regenerated; the cache just avoids re-billing identical pastes.

keys

the only secret is the vision key.

Only the tiny vision sub-agent needs an API key — it's the one thing that actually reads pixels. Your DeepSeek main model keeps whatever key it already has.

  1. Pick any multimodal model whose provider you can access, in .config/opencode/agents/observer.md:
    model: google/gemini-3.5-flash-lite   # free-tier friendly
    # or: openai/gpt-4o, anthropic/claude-sonnet-4-6, moonshotai/kimi-k2.6
  2. Give opencode the key for that provider — either log in (opencode auth login) or drop the env var:
    # gemini
    export OPENCODE_GOOGLE_API_KEY=...
    # openai
    export OPENCODE_OPENAI_API_KEY=...
    # anthropic
    export OPENCODE_ANTHROPIC_API_KEY=...
  3. Restart opencode so it reads the key once at startup.
Still getting "@observer can't read the file"? That's a missing or wrong key, or the provider rejects your model string — fix the key, then restart.

what's inside

small, boring, hand-made.

filejob
observer.jsThe plugin — decodes pasted images, saves them, routes reading through the sub-agent.
observer.mdThe vision sub-agent that decided what to read (error log → chart → markup → text).
README.mdHow it fits in all your codebases, cost, troubleshooting.