Give vision to non-vision models in OpenCode — for free, using Google's Gemini API.
When you attach an image to a chat running on a text-only model (DeepSeek, Qwen, Llama, ...), the image is transparently sent to Gemini, described in exhaustive detail (including verbatim transcription of every piece of text in the image), and the description is delivered to your model instead. Vision-capable models are left completely untouched and keep seeing the raw image.
Screenshots, UI bugs, diagrams, error messages — all become readable by text-only models.
- 🔍 Automatic — no prompt changes, no new tools for the model to learn. The transform happens at the request layer.
- 🎯 Smart targeting — only models that cannot see images get transformed. Vision models (Claude, GPT-4o/5, Gemini, Grok, Pixtral, ...) pass through untouched.
- 💸 Free — uses the Gemini API free tier (
gemini-2.5-flashby default, ~10 RPM / 1500 requests/day). - 📦 Zero dependencies — plain JS, only global
fetchandnode:fs. - ⚡ Efficient — multiple images in one message are sent as a single request; identical images are cached in-memory; retry with exponential backoff on rate limits.
- 🔐 Private by default — the key never touches your config file (read from the environment).
- ✂️ Non-destructive — if Gemini fails or no key is set, image parts pass through unchanged; your session never breaks.
you paste an image
│
▼
┌───────────────────────────────┐
│ experimental.chat.messages │ ← hook installed by this plugin
│ .transform │
└───────────────┬───────────────┘
│ model needs vision?
│ (provider/model not in exclude list)
no ▼ yes
├─── image parts pass through untouched ───▶ vision model sees the raw image
│
▼
Gemini generateContent (free tier)
- inline_data: base64 image(s)
- prompt: "describe exhaustively, transcribe ALL text"
│
▼
detailed text description
│
▼
image parts replaced by a text part
│
▼
non-vision model receives the description
npm i -g opencode-vision-bridgeAdd it to ~/.config/opencode/opencode.json (global) or ./opencode.json (project):
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-vision-bridge"]
}Copy src/index.js into your plugin directory:
# global
mkdir -p ~/.config/opencode/plugins && cp src/index.js ~/.config/opencode/plugins/vision-bridge.js
# or project-local
mkdir -p .opencode/plugins && cp src/index.js .opencode/plugins/vision-bridge.jsFiles in ~/.config/opencode/plugins/ and .opencode/plugins/ are auto-discovered — no config entry required.
- Go to aistudio.google.com → Get API key
- Copy the key — both formats work: the new
AQ.Ab...(Auth key, issued by default since mid-2026) and legacyAIza... - Export it:
# bash / zsh
export GEMINI_API_KEY="AQ.Ab..."
# Windows PowerShell
setx GEMINI_API_KEY "AQ.Ab..."The plugin also accepts
VISION_BRIDGE_API_KEYorGOOGLE_API_KEY.
Config is loaded once at startup — quit and restart OpenCode for the plugin to take effect.
Plugin options (only needed if you want to customize):
{
"plugin": [
[
"opencode-vision-bridge",
{
"apiKey": "{env:GEMINI_API_KEY}",
"geminiModel": "gemini-2.5-flash",
"includeModels": [],
"excludeModels": ["anthropic/.*", "openai/gpt-4o.*"],
"prompt": "Describe this image for a text-only LLM..."
}
]
]
}| Option | Type | Default | Description |
|---|---|---|---|
apiKey |
string |
env | Gemini API key. Read from apiKey → VISION_BRIDGE_API_KEY → GEMINI_API_KEY → GOOGLE_API_KEY. |
geminiModel |
string |
gemini-2.5-flash |
Gemini model used for descriptions. |
includeModels |
string[] |
[] |
Regexes against provider/model. When set, only matching models are transformed (overrides exclusions). |
excludeModels |
string[] |
built-in vision list | Regexes against provider/model that are never transformed. Defaults exclude every known vision-capable provider. |
prompt |
string |
built-in | Custom description prompt sent to Gemini alongside the image. |
| Variable | Description |
|---|---|
GEMINI_API_KEY / GOOGLE_API_KEY / VISION_BRIDGE_API_KEY |
Gemini API key. |
VISION_BRIDGE_MODEL |
Override the Gemini model. |
VISION_BRIDGE_INCLUDE |
Comma-separated regexes (same as includeModels). |
VISION_BRIDGE_EXCLUDE |
Comma-separated regexes, appended to the built-in vision list (same as excludeModels). |
- Start a session with any text-only model (e.g. a DeepSeek provider).
- Paste an image into the input (screenshot, UI mockup, error dialog, diagram...).
- The model receives a detailed description instead of a broken image part.
To verify it works, watch the OpenCode log output — the plugin logs Describing N image(s) for <provider>/<model> via gemini-2.5-flash at debug level.
My vision model (via OpenRouter / custom provider) is being described instead of seeing the image.
Add its pattern to excludeModels (or VISION_BRIDGE_EXCLUDE=openrouter/.*).
I want to force transformation even for vision models.
Set includeModels: ["anthropic/.*"].
What happens if Gemini is down or rate-limited? The plugin retries 4 times with exponential backoff, then leaves the image parts untouched. Your session continues normally.
How do I change the Gemini model?
VISION_BRIDGE_MODEL=gemini-2.5-flash-lite or the geminiModel option.
Is my image data private? Images are sent to Google's Gemini API. Free-tier data may be used by Google to improve products (per Google's terms). Do not attach sensitive content, or use a paid tier / Vertex AI if this matters.
- Built on the
experimental.chat.messages.transformhook — it is experimental and may change across OpenCode versions. - Descriptions are text; the model never sees pixel-level detail (e.g. exact color hex values are approximated by Gemini).
- Free tier is rate-limited (~10 RPM, ~1500 requests/day depending on model).
npm test # unit tests (node --test, no dependencies)
npm run smoke # loads the plugin and verifies the hook surface
npm run check # syntax check + tests
npm run pack:check # inspect what npm pack would publishMIT © hungvip69