Bundle
dsh-image-vision
Seamless image understanding for DeepSeek Harness: lets pure-text models read pasted/dropped images and Feishu/Lark images via a configurable vision model, without touching the host.
- Source
- VeryInt
- License
- MIT
- Updated
- Updated 5 days ago
Readme
# dsh-image-vision
Seamless image understanding for **DeepSeek Harness** — lets pure-text main
models read pasted/dropped images and Feishu/Lark images via a configurable
vision model, **without modifying any host code**.
A pure "everything is a plugin" implementation: it wraps a documented `llm`
service method and listens to official agent/tool waterfalls. No `packages/`
files are touched, so it portably runs wherever DeepSeek Harness runs.
> 简体中文说明见 [README.zh.md](./README.zh.md)
---
## What it does
- **Paste / drop an image in chat** — even when the main model is text-only
(e.g. DeepSeek V4 Flash routed through pi-ai), the image is accepted and
described to the model as text.
- **Feishu / Lark document images** — `<image token="..."/>` produced by
`lark_read_doc` and friends is downloaded through `lark-cli` and described;
the result is injected back as tool output.
- **`describe_image` model tool** — describes a local image file path.
- **Full description, not just OCR** — people, scenes, tables, charts and
verbatim text are all described, driven by a prompt you can override.
Pick a vision model from **ModelScope free community API** (e.g.
`Qwen/Qwen3-VL-8B-Instruct`) or **SiliconFlow** (e.g.
`Qwen/Qwen3-VL-32B-Instruct`). Routes are auto-discovered from your `settings`.
---
## Why no host code changes
The web API proxy gates image upload/model-switch by calling
`llm.resolveModelInfo(...).inputModalities` and rejecting when the model does
not declare `image`. Since DSH is "everything is a plugin", this plugin:
1. **Wraps `ctx.llm.resolveModelInfo`** — when the target model lacks `image`
input and a vision route is configured, the returned capabilities gain
`image`, so the admission gate lets the image through.
2. **Listens on `agent/pre-step`** — before the real model request, each image
block is transcribed by the vision model and replaced with its text, so the
text-only main model only ever receives plain text.
Both steps use documented DSH extension points. No core files are changed.
---
## Requirements
- DeepSeek Harness `0.1.0-rc.6` (**source checkout or installed release** —
both are supported, see *Installation*).
- Node.js >= 22.19
- A configured **vision model provider** (see *Configuration*).
---
## Installation
Two deployment styles are supported: a **source checkout** (clone the
deepseek-harness repo and run `pnpm dsh web`) and the **installed release**
(`npm i -g @deepseek-ai/dsh`). Both go through the same `dsh plugin` manager;
`@deepseek-ai/*` dependencies are provided by the host and resolved from the
DSH installation anchor.
### From GitHub (recommended)
```bash
npx @deepseek-ai/dsh plugin --profile web add github:VeryInt/dsh-image-vision
```
### From a local path (development / validation)
```bash
git clone https://github.com/VeryInt/dsh-image-vision.git /path/to/dsh-image-vision
npx @deepseek-ai/dsh plugin --profile web add /path/to/dsh-image-vision
```
> Replace `<profile>` with whichever profile you use (`web`, `headless`, ...).
> If pnpm blocks a git dependency's build/prepare script, allowlist the exact
> key it prints under `allowBuilds` in `<profile>/pnpm-workspace.yaml`, then
> re-run.
Restart Harness after installing. The plugin then appears under
**Settings → Plugins**.
---
## Configuration
Image recognition needs a **vision model provider**. Add an OpenAI-compatible
vision endpoint in the DSH Models page (or `~/.dsh/settings.yaml`), and — this
is the part the Models page does not show — declare `input: [text, image]` on
the model entry.
Example `settings.yaml`:
```yaml
llm-pi-ai:
providers:
modelscope:
displayName: ModelScope
apiKeyEnv: MODELSCOPE_API_KEY
api: openai-completions
baseURL: https://api-inference.modelscope.cn/v1
models:
- id: Qwen/Qwen3-VL-8B-Instruct
name: Qwen3-VL-8B
input: [ text, image ]
siliconflow:
displayName: 硅基流动
apiKeyEnv: SILICONFLOW_API_KEY
api: openai-completions
baseURL: https://api.siliconflow.cn/v1
models:
- id: Qwen/Qwen3-VL-32B-Instruct
name: Qwen3-VL-32B-Instruct
input: [ text, image ]
```
Then provide the API key in `~/.credentials.yaml` or as an environment
variable:
```bash
# ModelScope community access: generate at https://modelscope.cn
MODELSCOPE_API_KEY=ms-xxxxxx
# SiliconFlow: generate at https://cloud.siliconflow.cn
SILICONFLOW_API_KEY=sk-xxxxxx
```
> **Use a vision model (VL suffix).** Text-only models such as
> `Qwen/Qwen3.5-27B` return empty results even when declared image-capable.
### Plugin vision route
The plugin's `cordis.patch.yml` carries two relevant fields:
```yaml
- insert:
- id: dsh-image-vision
name: dsh-image-vision
config:
provider: siliconflow # vision provider route (empty = auto-discover)
model: Qwen/Qwen3-VL-32B-Instruct # vision model id (empty = auto-discover)
# prompt: ...
# maxImagesPerMessage: 4
# feishuImages: true
# maxFeishuImages: 5
```
Rules:
- **Both `provider` and `model` set**: used as-is (highest priority).
- **Both empty**: scan every configured provider and pick the first model
declaring `input: [text, image]` (this is the common case).
- Changing the model = update these two fields **and** make sure the model
declares `input: [text, image]` in `settings.yaml`.
---
## Verification
After a restart:
1. **Paste an image in chat** — the main model should describe its content
(rendered as `[图片内容] …`).
2. **Read a Feishu/Lark doc with images** — tool output gains
`[飞书图片 <token>] …` descriptions.
3. **`describe_image` tool** — ask the model to describe a local image path.
On a transient vision failure the reply still comes back with a
`[图片内容识别失败:…]` / `[飞书图片 … 识别失败:…]` placeholder rather than
failing the whole turn.
---
## Options
| Field | Default | Description |
| --- | --- | --- |
| `provider` | empty | Vision provider route; empty = auto-discover |
| `model` | empty | Vision model id; empty = auto-discover |
| `prompt` | (full description) | Instruction sent to the vision model |
| `maxImagesPerMessage` | 4 | Max images bridged per message; beyond throws |
| `feishuImages` | true | Auto-describe Feishu `<image token>` |
| `maxFeishuImages` | 5 | Max Feishu images per tool result |
---
## How this differs from oil-oil/dsh-vision
We looked at the community plugin [oil-oil/dsh-vision](https://github.com/oil-oil/dsh-vision)
before writing this one. Key differences:
| | **oil-oil/dsh-vision** | **dsh-image-vision** |
|---|---|---|
| Mechanism | Replaces the official `deepseek-official` **adapter** with a `VisionBridgeAdapter`; installer disables the built-in `llm-deepseek` | Wraps only `llm.resolveModelInfo` and uses the official `agent/pre-step` waterfall — no adapter replacement, no built-in plugin disabled |
| Works with | Models routed through `deepseek-official` | Any pure-text route (pi-ai `deepseek`, etc.) because it hooks the `llm` service, not a specific route |
| Vision backends | Bundles several (zenmux / Bailian / see config / local OCR) | Reuses your `settings.yaml` / Models vision providers (input: [text, image]) |
| Settings UI | Ships a "Vision Recognition" card | None — config lives in the plugin `cordis.patch.yml` |
| Packaging | TypeScript + tsdown build, many peer deps | Plain JS ESM, minimal peers, no build step |
In short: oil-oil *replaces the DeepSeek adapter* (so it only affects
`deepseek-official` and requires disabling the built-in plugin); this plugin
*only wraps a llm capability lookup and uses the official pre-step* — it works
for any text-only main model route and makes no destructive changes, which fits
"everything is a plugin" more literally.
---
## License
[MIT](./LICENSE)
Install
dsh plugin --profile web add github:VeryInt/dsh-image-vision
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-image-vision from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.