# dsh-eyes

> Give **eyes** to the non-vision models in [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`@deepseek-ai/dsh`).

[中文](README.md) · [MIT](LICENSE)

## ✨ Preview

| Original image | Paste with a text-only model, transcribed automatically | Vision Models settings page (independent config) | Recognition result |
| --- | --- | --- | --- |
| ![Original image](assets/screenshots/original.png) | ![Paste and transcribe](assets/screenshots/paste-transcribe.png) | ![Vision models settings](assets/screenshots/vision-models-page.png) | ![Recognition result](assets/screenshots/recognition-result.png) |

## 

## Features

- **`vision_read` tool** — when a model without vision needs to see an image, the image is sent to a vision-capable model and returned as **text**; no image blocks enter history, so the adapter's modality gate (`UNSUPPORTED_CONTENT`) never triggers.
- **Paste images in the GUI** — overrides the host admission (`admitPastedImages`, on by default), so text-only models like `deepseek-v4-flash` can accept pasted/attached images; transcription happens on send.
- **Independent Vision Models settings page** — a sidebar settings page that marks vision candidates; fully separate from the main model config (`llm-pi-ai`). Provider/model management reuses the built-in Models page.
- **Hot reload** — changes to the `vision-bridge:` section of `settings.yaml` take effect immediately, no restart needed.

## How it works

```
vision_read(file_path)
  ├─ validate extension / attachment service / size limits (same as read_image)
  ├─ does the active route's model declare image?
  │    ├─ yes → return the image itself (native, same as built-in read_image)
  │    └─ no  → probe visionRoutes (registered + resolvable + declares image)
  │           └─ vision model transcribes, returns text (translated)
```

The translated path only injects **text** into the conversation, so subsequent requests of the non-vision model stay within the adapter's modality gate.

## Installation

### Option A: install from npm 

```sh
dsh plugin --profile web add dsh-eyes
```

### Option B: install from source (development / debugging)

```sh
git clone https://github.com/JY626/dsh-eyes.git
cd dsh-eyes
pnpm install                       # install plugin dependencies
dsh plugin --profile web add .     # install into the profile
```

Then **restart the web app** and refresh the page — a "视觉模型" (Vision Models) entry appears in the settings sidebar. Verify with `dsh --profile web --dump-config` (expect a `vision-bridge` row).

Uninstall: `dsh plugin --profile web remove dsh-eyes`

## Configuration

### Option A: Vision Models settings page (recommended)

Settings → **Vision Models**: check models from the registered routes, then save. The selection is stored in `vision-bridge.visionRoutes` and never touches the Models page; when empty, every model that declares `image` input is auto-discovered.

### Option B: settings.yaml

```yaml
vision-bridge:
  visionRoutes: []                          # empty = auto-discovery
  admitPastedImages: true                   # accept pasted images in the GUI
  visionPrompt: "You are an image recognition assistant. ..."
  maxTokens: 2048
  maxOutputChars: 16000
  temperature: 0.1
```

The vision model must declare image input per-model (in the `llm-pi-ai` section, via the Models page or by hand):

```yaml
llm-pi-ai:
  providers:
    opencode-go:
      apiKeyEnv: OPENCODE_GO_API_KEY
      models:
        - id: minimax-m3
          name: MiniMax-M3
          contextWindow: 1000000
          maxTokens: 131072
          input: [text, image]    # ← key: declare image input
```

> ⚠️ Models outside the route's catalog have a protocol requirement: declare `api` + `baseURL` (see the "dedicated vision route" example in the full config), otherwise `llm-pi-ai` validation fails.

### Full options

| Field | Default | Description |
|---|---|---|
| `toolName` | `vision_read` | Registered tool name |
| `visionRoutes` | `[]` (auto-discovery) | Ordered vision route candidates, probed in order |
| `visionPrompt` | see `cordis.patch.yml` | System prompt sent to the vision model |
| `maxTokens` | `2048` | Vision model output token cap |
| `maxOutputChars` | `16000` | Character cap of the returned transcription (truncated with a note) |
| `temperature` | `0.1` | Vision model sampling temperature |
| `admitPastedImages` | `true` | Accept pasted images with text-only models (transcribe on send) |

## Troubleshooting

| Symptom | Cause / fix |
|---|---|
| Pasting an image fails ("current model does not support images") | Plugin not loaded: confirm the profile row exists (see Installation) and **restart the web app** |
| Tool reports "no usable vision route" | No usable vision model: check the model entry declares `input: [text, image]` and its route is registered |
| `UNSUPPORTED_CONTENT` | The vision model does not declare image input |
| `NO_ADAPTER` / `UNKNOWN_MODEL` | Route not registered / model id not in the route's catalog |
| `IMAGE_TYPE_MISMATCH` | Extension does not match the actual image format |

## Development

```sh
pnpm check    # syntax check (host / client / format)
pnpm test     # pure-function tests (node:test)
```

```
lib/index.js      # plugin entry: tool registration + bridge logic + admission override
lib/client.js     # client bundle: Vision Models settings page (__ModuleLoader__.load format)
lib/format.js     # zero-dependency pure helpers
test/             # node:test tests
```

## License

[MIT](LICENSE)
