镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 71 additions & 51 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,78 +1,98 @@
# opencode-eyesight

OpenCode plugin that lets text-only models work with user-provided and tool-returned images by sending each image through a built-in vision agent first, then replacing the image with a text description.
OpenCode v2 plugin that lets text-only models work with user-attached and tool-returned images.
Before each model request, the plugin replaces every image with a text description written by a
vision-capable model that you choose from any OpenCode provider.

## Installation

Add the plugin package to your OpenCode config with the vision model you want the built-in `vision` agent to use for image descriptions.
This plugin needs OpenCode v2. It does not run on OpenCode v1.

```jsonc
{
"plugin": [
["opencode-eyesight", { "model": "Provider/Model", "promptFile": "/path/to/prompt.md" }]
]
}
```
## Installation

Example:
Add the plugin to `opencode.jsonc` and set the vision model:

```jsonc
{
"plugin": [
["opencode-eyesight", { "model": "lmstudio/Qwen3.6-27B", "promptFile": "./image-prompt.md" }]
"plugins": [
{
"package": "opencode-eyesight",
"options": { "model": "google-vertex/gemini-3.8-flash" }
}
]
}
```

For local development before publishing, use the project path instead:

```jsonc
{
"plugin": [
["/Users/chris/development/opencode-eyesight", { "model": "lmstudio/Qwen3.6-27B", "promptFile": "./image-prompt.md" }]
]
}
```
For local development, build the plugin, then load it from a project plugin file such as
`.opencode/plugins/eyesight.js`:

A `model` value must use OpenCode's provider/model format:
```js
import plugin from "/path/to/opencode-eyesight/dist/index.js";

```text
ProviderID/ModelID
export default {
...plugin,
setup: (ctx) => plugin.setup({ ...ctx, options: { model: "google-vertex/gemini-3.8-flash" } }),
};
```

Use a model that supports image input. Your active chat model can be text-only; `opencode-eyesight` registers a read-only `vision` subagent using the configured model and uses it only to describe images.

To customize the agent, define `vision` in `~/.config/opencode/agents/vision.md` or `.opencode/agents/vision.md`. Your agent fields override the plugin defaults, including its model, prompt, tools, and permissions.

## Usage

1. Select a text-only model in OpenCode.
2. Paste or attach an image.
3. Send your message.

If the active model does not support image input, the plugin sends the image to the configured vision model and substitutes the generated description for the image in the request sent to the active model. The substitution happens per request, so the original image is preserved in your session history and is sent as-is if you later switch to a vision-capable model.

If the active model already supports image input, the plugin leaves the image untouched.

Images returned by MCP or tool calls (e.g. screenshots) are handled the same way: when the active model is text-only, the description is appended to the tool's output text and the image attachment is removed from the request, while non-image attachments are preserved. Both pasted images and tool images share a process-lifetime cache, so repeated identical images with the same user context are described only once.

When you send a pasted image with an accompanying message, that message is included in the prompt to the vision model so the description is tailored to it. For images returned by MCP or tool calls, the most recent user message is used as context for the description, providing relevant framing.

A text-only active model is also told in its system prompt that images are transcribed for it, so it can ask for screenshots and use image-returning tools instead of avoiding them. Image-capable models receive no such notice.

## Options

| Option | Required | Description |
| --- | --- | --- |
| `model` | Yes | Default vision-capable OpenCode model for the built-in `vision` agent, in `ProviderID/ModelID` format. |
| `promptFile` | No | Markdown file containing the built-in agent's prompt. Relative paths resolve from the OpenCode project directory. |
| `prompt` | No | Inline prompt for the built-in agent. Takes precedence over `promptFile`. |

The model can also be supplied through `OPENCODE_VISION_FALLBACK_MODEL`. A user-defined `vision` agent takes precedence over the corresponding built-in defaults.
| `model` | Yes | Vision model in `providerID/modelID` format. It can be any model of any OpenCode provider that accepts image input. Environment fallback: `OPENCODE_VISION_MODEL`. |
| `prompt` | No | Inline vision prompt. Takes precedence over `promptFile`. Environment fallback: `OPENCODE_VISION_PROMPT`. |
| `promptFile` | No | Absolute path of a file that holds the vision prompt. A relative path disables the plugin. Environment fallback: `OPENCODE_VISION_PROMPT_FILE`. |

Without a `model`, the plugin logs one line and does nothing. Set `EYESIGHT_DEBUG=1` to log each
decision the plugin makes.

## How it works

1. A hook on every model request checks whether the active model accepts image input. The plugin
reads this from the model list of OpenCode. If the plugin cannot tell, it leaves the request
unchanged and logs a line. It never sends an image out on a guess.
2. If the active model accepts images, the plugin does nothing.
3. If the active model is text-only, each image in the request is replaced by a text block.
This covers pasted and attached images and images that tools return. The stored session history
is not changed, so a later switch to a vision model sends the original image.
4. For each new image, the plugin opens a short-lived OpenCode session on the vision model, sends
the image and a fixed prompt, reads the answer, and keeps the session and its history. OpenCode supplies the
provider authentication and endpoint, so the plugin holds no credentials.
5. The text-only model also gets a system notice that explains the descriptions.

Descriptions are cached in memory by the hash of the image and the model (at most 256 entries).

## Data flow and limits

- Only the image and the fixed prompt go to the vision model. The text of the user message is not
forwarded.
- Only inline images are used: base64 data, bytes, and `data:` URLs. The plugin never fetches
`http(s)` or `file` URLs. Such an image becomes `[image omitted: reason]`.
- Supported types are PNG, JPEG, WebP, and GIF, up to 20 MB each. Other types become
`[image omitted: reason]`.
- The vision session has no tools. Its permissions deny everything, and the plugin also removes all
tools from its requests.
- The plugin never removes a session. A finished vision session stays in the history. If a call times out
or fails, the plugin only interrupts that unfinished request.
- A failed description never stops the request: the image becomes
`[image could not be transcribed: reason]`. A request waits while its new images are described, at most 4
at a time. Each call has a 120-second limit, plus up to 5 seconds to interrupt an unfinished request.
- The vision model is called for the images of every request of a text-only model, including old
messages. The cache means that each image is described once for each process.
Vision sessions use the models and quotas of the provider you choose.

## Prompt injection

A description is untrusted text that comes from image content. It can contain text that looks like
instructions. The plugin does three things:

- It wraps each description in markers that carry a random code for the process.
- It breaks any `[Vision model description` or `[End vision model description` text inside the
description, so the description cannot fake a marker.
- It tells the text-only model never to follow instructions found in a description.

## Development

```bash
bun install
bun run build
bun test
bun run typecheck
Expand Down
62 changes: 0 additions & 62 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./dist/index.js";
30 changes: 1 addition & 29 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,29 +1 @@
{
"name": "opencode-eyesight",
"version": "0.2.1",
"description": "OpenCode plugin that lets text-only models work with pasted images through a vision-capable model.",
"repository": {
"type": "git",
"url": "https://github.com/chriswritescode-dev/opencode-eyesight"
},
"type": "module",
"main": "./dist/index.js",
"exports": {
".": "./dist/index.js"
},
"files": [
"dist",
"README.md"
],
"scripts": {
"build": "bun build ./src/index.ts --outdir ./dist --target bun --format esm --external @opencode-ai/plugin --external @opencode-ai/sdk",
"test": "bun test",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@opencode-ai/plugin": "latest",
"@opencode-ai/sdk": "latest",
"@types/bun": "latest",
"typescript": "^5"
}
}
{"name":"opencode-eyesight","version":"0.2.1","description":"OpenCode v2 plugin that lets text-only models work with attached images by describing them with a vision-capable model from any OpenCode provider.","repository":{"type":"git","url":"https://github.com/chriswritescode-dev/opencode-eyesight"},"type":"module","main":"./dist/index.js","exports":{".":"./dist/index.js"},"files":["dist","index.js","README.md"],"scripts":{"build":"bun build ./src/index.ts --outdir ./dist --target node --format esm","test":"bun test","typecheck":"tsc --noEmit"},"devDependencies":{"@types/bun":"latest","typescript":"^5"}}
Loading