Route pasted images to a cheap vision model in opencode so a text-only main agent can work from the vision model's text output.
opencode attaches a pasted image to the main (text-only) model's message and drops or errors on it before any skill or subagent runs. A skill alone cannot fix this. The only reliable fix is a plugin hook that intercepts the image at the harness level, resolves it to a readable path, and lets a cheap vision model analyze it.
opencode-vision-router is a self-contained, zero-config plugin that does exactly that β and injects
the vision subagent for you, so there are no separate agent or skill files to manage.
- πΌοΈ Pasted-image routing β
data:URLs,file://paths, and absolute paths all supported. - πΈ Multiple images β handles several pasted images in a single message, routing each one to the vision subagent.
- πΈ Cheap vision model β point it at any image-capable model (
provider/model). - π§ Multimodal-aware β if your main model already sees images, routing is skipped automatically. Set
forceto always route (e.g. to a cheaper vision model). - π§© Self-contained β injects the vision subagent and system instruction at load time.
- π Safe by default β the vision subagent can read the image but is denied edit/bash/webfetch.
- β‘ No build step β opencode runs plugins on Bun, which executes TypeScript natively.
flowchart LR
A[User pastes image] --> B[experimental.chat.messages.transform]
B -->|strip image, write temp file| C[text pointer with path]
C --> D[Main text-only agent]
D -->|system instruction| E[Task tool]
E --> F[Injected vision subagent]
F -->|read temp file| G[Cheap vision model]
G -->|text analysis| D
configβ at load time, declare the chosen model as image-capable (modalities+attachment) and inject thevisionsubagent.experimental.chat.system.transformβ instruct the main agent to delegate any image pointer to the subagent via the Task tool.experimental.chat.messages.transformβ strip the image from the user message and replace it with a text pointer containing the temp-file path, so the text-only model never sees the bytes.
β οΈ This plugin relies on opencode's experimentalexperimental.chat.messages.transformandexperimental.chat.system.transformhooks, which may change in future opencode versions.
Add it to your opencode.json. opencode auto-installs npm plugins at startup via Bun:
{
"plugin": [
["opencode-vision-router", { "model": "opencode-go/qwen3.7-plus" }]
]
}Then restart opencode β plugins are not hot-reloaded.
| Option | Required | Default | Description |
|---|---|---|---|
model | yes | β | Vision-capable model as provider/model (e.g. opencode-go/qwen3.7-plus). If omitted, routing is disabled (a warning is logged). |
agent | no | vision | Name of the injected vision subagent. |
tmpDir | no | os.tmpdir() | Directory under which decoded images are cached (content-hashed, reused across calls). |
force | no | false | Route images to the vision subagent even when the main model is multimodal (e.g. to use a cheaper vision model). By default the subagent is skipped when the main model can already see images. |
{
"plugin": [
["opencode-vision-router", { "model": "opencode-go/qwen3.7-plus" }]
]
}{
"plugin": [
"@dodopayments/opencode-plugin",
["opencode-vision-router", { "model": "opencode-go/qwen3.7-plus" }]
]
}{
"plugin": [
[
"opencode-vision-router",
{
"model": "anthropic/claude-3-5-haiku",
"agent": "image-reader",
"tmpDir": "/var/tmp/opencode-vision"
}
]
]
}If your main model can already see images, routing is skipped by default. Set force: true
to always route β e.g. to send images to a cheaper vision model while keeping a stronger
text model as main:
{
"plugin": [
["opencode-vision-router", {
"model": "openai/gpt-4o-mini",
"force": true
}]
]
}opencode-vision-router is an opencode plugin and is wired up only through your opencode.json
(see Installation / Configuration above). Its helper functions (image.ts, transform.ts,
agent.ts) are plain, dependency-free implementation details used by the plugin itself and
covered by the test suite β they are intentionally not part of the package's public API, so
import them from the source tree only if you are extending the plugin, not from the published
package.
A pasted image is intercepted and routed to the vision subagent:
Request flow:
sequenceDiagram
participant U as User
participant M as Main agent (text-only)
participant V as vision subagent
participant L as Vision LLM
U->>M: paste image + question
M->>M: image stripped β path pointer
M->>V: Task(image path, question)
V->>L: read(path) + analyze
L-->>V: text analysis
V-->>M: text analysis
M-->>U: answer
bun install
bun test# run the test suite
bunx tsc --noEmit # type-checksrc/
index.ts # plugin entrypoint β default export only (no public re-exports)
types.ts # shared option & message types
image.ts # resolveImagePath, decodeDataUrl, extForMime
transform.ts # transformMessages, imagePointer (pure)
agent.ts # buildVisionAgentConfig, applyConfig, delegationInstruction
index.test.ts # Bun tests
Contributions welcome! This is a small, single-purpose plugin, so the bar for patches is low.
- Fork the repo and create a branch:
git checkout -b fix/my-change. - Install deps and run the checks:
bun install && bun test && bunx tsc --noEmit. - Add tests for any new behavior.
- Keep the plugin self-contained β prefer extending the injected subagent over adding new files users must wire up.
- Open a PR with a clear description of the problem and the fix.
Please file issues for bugs, hook-contract changes in opencode, or model-compatibility reports.
Publishing, OIDC/Trusted-Publisher setup, and the buildβdist/ flow are documented in
release.md.
MIT Β© opencode-vision-router contributors.

