opencode-multimodal-looker
v0.3.1
Published
Let a text-only opencode main agent look at images by routing them to a vision model and working from its text output.
Maintainers
Readme
Origin. This is a self-maintained rename of
Chathula/opencode-vision-router(MIT), taken at upstream0.2.0. It is published to npm asopencode-multimodal-lookerand can also be loaded by directory path. Changes are not sent upstream; see Divergence.
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-multimodal-looker 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.
✨ Features
- 🖼️ Pasted-image routing —
data:URLs,file://paths, absolute paths, and OpenCode V2 raw media parts 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.
- ⚡ OpenCode 1 & 2 — one default export supports both the V1 plugin API (
server()) and the V2 plugin API (setup()), plus V2 model-catalog capability detection and V2 message media parts.
🧠 How it works
The plugin registers the following behavior at load time, in whichever API shape your opencode version supports:
- Vision subagent injection — declare the chosen model as image-capable and inject the
visionsubagent (V1confighook / V2agent.transform). - Capability detection — per model, learn whether the main model can see images
(V1
chat.paramslearning / V2 model-catalog lookup), so multimodal main models are skipped unlessforceis set. - Image rewrite — strip the image from the user message and replace it with a text
pointer containing the resolved path, so a text-only model never sees the bytes:
- V1:
chat.message(primary) +experimental.chat.messages.transform(backup). - V2: a
session.hook("context")registered insetup(), which rewritesmediaparts in the assembled messages immediately before each agent model request and covers both fresh attachments and history.
- V1:
⚠️ V1 relies on opencode's experimental
experimental.chat.messages.transformhook, which may change in future opencode versions. In V2 the equivalent is the stablecontextsession hook.
📦 Installation
Add it to your opencode.json(c) and restart opencode — plugins are not hot-reloaded.
From npm (recommended, per current OpenCode plugin docs):
{
"plugins": [
{
"package": "opencode-multimodal-looker",
"options": { "model": "alibaba-coding-plan/qwen3.7-plus" }
}
]
}From a local directory (fallback for versions whose config schema only
accepts the plugin key — e.g. opencode 2.0.10). Clone it, build it once,
and point a [path, options] tuple at the directory:
git clone https://github.com/nxxxsooo/opencode-multimodal-looker.git ~/path/to/opencode-multimodal-looker
cd ~/path/to/opencode-multimodal-looker && bun install && bun run build{
"plugin": [
[
"/absolute/path/to/opencode-multimodal-looker",
{ "model": "alibaba-coding-plan/qwen3.7-plus" }
]
]
}Then restart opencode — plugins are not hot-reloaded.
⚠️ The directory-tuple form uses the
pluginkey with[path, options]tuples (not thepluginskey with{ package, options }objects from the npm form above). Thehttps://opencode.ai/config.jsonschema shipped with opencode2.0.10only acceptsplugin, whose items arestring | [string, object]. The path must be a directory containingpackage.json; pointing at a file is rejected withconfigured plugin path must be a directory.
⚙️ Configuration
| 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). |
| fallbackModels | no | — | Ordered fallback vision models (provider/model). On a quota / rate-limit failure of the current model (e.g. Bailian's "concurrency allocated quota exceeded"), the vision subagent switches to the next entry and retries immediately. V2 only. |
| fallbackResetMs | no | 1800000 | How long to stay on a fallback before switching back to the primary, so a recovered quota is picked up. 0 disables the reset. |
| 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. |
🚀 Usage examples
Basic
{
"plugin": [
[
"/Users/you/Tuning/opencode-multimodal-looker",
{ "model": "alibaba-coding-plan/qwen3.7-plus" }
]
]
}Alongside other plugins
{
"plugin": [
"[email protected]",
[
"/Users/you/Tuning/opencode-multimodal-looker",
{ "model": "alibaba-coding-plan/qwen3.7-plus" }
]
]
}With quota failover
{
"plugin": [
[
"/Users/you/Tuning/opencode-multimodal-looker",
{
"model": "alibaba-coding-plan/qwen3.7-plus",
"fallbackModels": [
"tencentmaas-openai/custom-model-c4-flash",
"tencentmaas/custom-model-a8"
]
}
]
]
}When the primary vision model fails with a quota / rate-limit error, the
subagent fails over to the next fallback (switching both the running child
session and the agent registry, then retrying with no delay) and logs
vision model … hit a quota/rate limit; switched vision subagent to ….
After fallbackResetMs (default 30 minutes) it switches back to the primary;
if the quota is still exhausted the hook fails over again.
Custom subagent name and cache directory
{
"plugin": [
[
"/Users/you/Tuning/opencode-multimodal-looker",
{
"model": "alibaba-coding-plan/qwen3.7-plus",
"agent": "image-reader",
"tmpDir": "/var/tmp/opencode-vision"
}
]
]
}Force routing on a multimodal main model
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": [
[
"/Users/you/Tuning/opencode-multimodal-looker",
{ "model": "alibaba-coding-plan/qwen3.7-plus", "force": true }
]
]
}Consuming the plugin
opencode-multimodal-looker 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.
🖥️ Screenshots
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🛠️ Development
bun install
bun test # run the test suite
bunx tsc --noEmit # type-checkProject structure
src/
index.ts # plugin entrypoint — dual V1/V2 default export (no public re-exports)
types.ts # shared option & message types
image.ts # resolveImagePath / resolveMediaPath, decodeDataUrl, extForMime
transform.ts # transformMessages / transformV2Messages, imagePointer (pure)
agent.ts # buildVisionAgentConfig, applyConfig, applyAgent, delegationInstruction
index.test.ts # Bun tests🔀 Divergence from upstream
Taken at Chathula/[email protected]. Changes since:
- Fixed: nested media parts (OpenCode 2.0.11+) were never rewritten. The
V2
context-hook rewrite matched only the flat pre-2.0.11 media part ({type:"media", mediaType, data}). OpenCode 2.0.11+ (verified on 2.0.15) delivers AI-SDK-style parts —{type:"media", filename, media:{mediaType, source:{type:"base64"|"bytes", data}}}— so the matcher never fired, the image passed through untouched, and the harness replaced it withERROR: Cannot read ... (this model does not support image input)for text-only main models. Both shapes are now matched, with regression tests. - Fixed:
force: falsenever skipped a multimodal main model. The V2 capability probe calledctx.catalog.model.list().@opencode/pluginexposed the catalog atctx.catalog.modelup to2.0.3but renamed it toctx.modelby2.0.10, so on current opencode the call threw aTypeErrorthat a barecatch { return false }turned into "this model cannot see images" — every model, always. Result: images were routed to the vision subagent even when the main model had native vision. Now readsctx.modelwith a fallback toctx.catalog.model, tolerates both a bare array and{ data }fromlist(), and logs the reason when the lookup genuinely fails instead of failing silently. - Regression tests for the V2 capability path (
OpenCode V2 capability detection), which upstream had none of — hence the shipped bug. - Renamed to
opencode-multimodal-looker;dist/is tracked so the plugin can also be loaded by directory path. npm publishing was restored at0.3.0(Trusted Publishing via.github/workflows/publish.yml).
🧰 Maintenance
bun install
bun test # includes the capability-detection regression tests
bunx tsc --noEmit
bun run build # rebuild dist/ — required after any src changeRebuild dist/ and restart opencode after changing src/; plugins are not hot-reloaded.
To check what upstream has done since the fork:
git fetch upstream && git log --oneline HEAD..upstream/main📜 License
MIT © opencode-multimodal-looker contributors.
