dsh-multimodal — multimodal eyes and hands for DeepSeek Harness
English | 简体中文
Give DeepSeek a pair of eyes and a paintbrush: paste a screenshot or image into the conversation and the vision provider you configured first transcribes it verbatim (error messages, code, UI text preserved), then DeepSeek keeps solving your problem — all in the same turn, no extra steps. When an illustration is needed, DeepSeek automatically calls the image backend and the generated pictures appear right in the conversation.
Blank slate by design: this plugin ships no built-in models, providers, or backends. Vision endpoints, image backends, and models are all declared by you — plug in whatever API you already have (DeepSeek, Zhipu, Aliyun, SiliconFlow, ModelScope, Xfyun, Qianfan, local Ollama, …). Nothing is preloaded, nothing is assumed.
Compatibility: built for DeepSeek Harness
0.1.0-rc.6(Web and headless). See CHANGELOG.
Features
| Scenario | Behavior |
|---|---|
| Plain-text chat | Straight to the DeepSeek API (unchanged) |
| Image + question (e.g. error screenshot) | Your configured vision provider "looks" first → transcribes to text → DeepSeek continues from the transcription (fix code, explain, propose); hitting "stop" aborts the vision call immediately |
| Attaching an image | No more "current model does not support images" |
| User asks for an image | DeepSeek calls generate_image → the configured image backend produces pictures shown in the conversation; backend failover tries the next backend if the active one fails (AUTH/aborted skips failover — no wasted quota) |
| Any image API | A custom backend plugs any non-OpenAI/DashScope API in via a small adapter file — no plugin code changes |
| Image card | Dedicated generate_image card: thumbnail grid, click-to-zoom lightbox, one-click download, prompt & model metadata, crop-to-ask (drag a region + ask), copy params (reproducible JSON), retry button on failure (refine-aware) |
| Extracting text from an image | DeepSeek can call extract_text (OCR) → Markdown / plain text / JSON; decoupled from the watch route, usable in any session |
| Paste-key auto-connect (0.7+) | Paste any API key into the quick-add box → platform auto-detected (key fingerprint → /models probing) → endpoint + allow-list + credential + model list + feature enablement in one step |
| Vision platforms | extraProviders accepts any OpenAI-compatible vision endpoint + one-click preset cards (Zhipu, Bailian, Xfyun, ModelScope, SiliconFlow, Qianfan, local Ollama) |
| Transcription cache | Same image + same context reuses the previous transcription — no wasted vision quota (LRU, per-session) |
| Vision fallback chain | Primary vision provider rate-limited/failing → automatic switch to fallbackProviders |
| Parallel transcription | parallelImages transcribes each image in its own concurrent call (fast multi-image turns) |
| Scene modes | Built-in transcribeMode presets: error-fix (error-screenshot diagnosis), chart-sql (chart → SQL + Pandas), design-code (design mockup → HTML+CSS) |
| Cost routing | Small images (≤ costMaxPixels) automatically go to a cheap provider |
| Local vision | One-click Ollama preset keeps sensitive images off the network (see Local vision models) |
| Config migration | Settings page exports/imports the whole config as JSON (allow-listed fields only) |
Security
Since 0.2.1, the plugin hardens itself against malicious or hand-edited configurations:
- API key allow-list (
allowedApiKeyEnvs): only listed env/credential names may be read as an API key — a malicious config can no longer nameGITHUB_TOKEN(or any other secret) asapiKeyEnvto exfiltrate it - Trusted base URLs (
trustedBaseUrls): custom vision providers must target official hosts or hosts you explicitly allow — credentials cannot be silently redirected to an attacker endpoint; explicitly listed local endpoints (e.g.http://localhost:11434) are allowed over plain http and need no key - SSRF guard: generated-image downloads and
reference_imageURLs refuse loopback / private (RFC1918) / link-local addresses - No sessionId forwarding: internal harness session ids are never sent to third-party vision APIs
- Prompt-injection markers: vision & OCR outputs are wrapped in explicit "untrusted context" markers before being handed to the text model
- Sensitive-data redaction (
redactSensitive): phone numbers / 18-digit IDs / emails are masked in transcriptions (including cache hits) - Audit log (
auditLog): one line per transcription with time / image count / bytes / latency / provider - Bounded memory: transcription cache, per-session image recall, and image-kind stats are all LRU-capped — no unbounded growth
Installation
Prerequisites: official DeepSeek Harness (0.1.0-rc.6+) installed and running, Node.js 18+.
# Option 1: install straight from the GitHub repo (requires git)
dsh plugin --profile web add https://github.com/MC5lan/dsh-multimodal
# Option 2: clone and install from a local path
git clone https://github.com/MC5lan/dsh-multimodal.git
dsh plugin --profile web add /path/to/dsh-multimodal
# Also add for headless mode when needed:
dsh plugin --profile headless add /path/to/dsh-multimodal
The repo ships prebuilt artifacts (lib/), so you can install right after cloning; to rebuild from source, see Development.
After restarting dsh web, a 「多模态 / Multimodal」 page appears in the Settings sidebar — everything is configured in one place:
- Status bar (one row): 识图 ✓/✗ · 画图 ✓/✗ — computed from real capability (transcription provider set AND registered / backend exists AND active), so it never shows "configured" when the feature is actually off
- 快速接入 (quick add): the paste-any-key auto-detect box is front and center — paste an API key, the plugin figures out the platform (key fingerprint match first, then
/modelsprobing against known endpoints; key travels one-shot, never stored), and one step later the endpoint is registered, the key env allow-listed, the credential stored, the model list pre-filled, and 识图/画图 enabled. Works with any OpenAI-compatible API. Below it: platform preset cards (Zhipu, Bailian, Xfyun, ModelScope, SiliconFlow, Qianfan, local Ollama) and a custom-endpoint form - 已接入 (connected, open by default): platforms with editable model chips + image-backend management (switch / remove / add form)
- 高级 (advanced, collapsed): the two KeyCards (视觉转写 Key / 图像生成 Key), vision details (watch route, transcription provider, mode, four toggles), and config export/import
Everything in the common path is: paste key → done. The picker then shows
deepseek-vision models (e.g. DeepSeek-V4-Flash (Vision)) — that route's
sessions get the "eye".
Usage
- Fastest path (recommended, 0.7+): Settings → Multimodal → 快速接入 → paste your API key — platform auto-detected and connected in one step (endpoint + allow-list + credential + model list; 识图/画图 enabled automatically). If the key can't be recognized, the hint says why; then pick a platform card or use a custom endpoint.
- Manual path: tap a preset card (Zhipu / Bailian / Xfyun / ModelScope / SiliconFlow / Qianfan / local Ollama) and paste the key, or declare any OpenAI-compatible endpoint in
extraProviders; optionally fillproviders.deepseek.modelsto advertise models on the built-in DeepSeek route slot. - In the Web UI model picker choose a
deepseek-visionmodel — that route's sessions get the "eye". - Chat normally; paste/drag an image and ask to have it read, or just say "draw me a …" to generate one.
API Keys (or write the credentials file directly)
The Settings → Multimodal → 密钥管理 page writes both keys for you. To edit
the credentials file directly (path is $DSH_HOME/.credentials.yaml, default
~/.dsh/.credentials.yaml), it takes effect immediately:
VISION_API_KEY: <your-vision-endpoint-key> # vision transcription (see 视觉转写 Key in the UI)
IMAGE_API_KEY: <your-image-backend-key> # image generation (see 图像生成 Key in the UI)
These are your provider keys — any OpenAI-compatible endpoint you register in 平台接入 accepts its key under the env name you give it (
apiKeyEnv).
How it works
User image ──▶ host admission (model directory declares image modality → allowed, no popup)
──▶ agent/pre-step (plugin): detects images → configured vision engine transcribes
└─▶ image blocks replaced by "【图片内容转写】…" text
──▶ DeepSeek API processes the transcription + your question (image bytes never reach DeepSeek)
- The
deepseek-visionroute is the DeepSeek API with a directory that advertises image support so the UI allows attachments — images are actually transcribed by the provider you configured, never sent to DeepSeek - Image bytes are read through the dsh attachment service; the transcription instruction defaults to "transcribe verbatim + extract key error info" and can be customized in the
dsh-multimodal:section of~/.dsh/settings.yaml - On transcription failure it degrades to a
【图片转写失败: 原因】placeholder — DeepSeek keeps going instead of hanging - Settings UI: Settings → Multimodal (
settings.sectionslot, native look & feel like the Models page)
Configuration (optional, ~/.dsh/settings.yaml)
dsh-multimodal:
# 0.2.1+ security: only these env/credential names may be used as API keys
allowedApiKeyEnvs:
- DEEPSEEK_API_KEY
# - MY_PROVIDER_API_KEY # add your custom provider's key env here
# 0.2.1+ security: extra vision hosts allowed to receive credentials
trustedBaseUrls:
# - https://my-vision.example.com # add your custom provider's host here
providers:
deepseek:
models: [] # optional: advertise models on the DeepSeek route slot
vision:
watchProvider: deepseek-vision # sessions on this route get the "eye"
transcribeProvider: '' # the provider that "sees" (blank = transcription off)
fallbackProviders: [] # 0.2.2+: providers tried in order when the primary rate-limits/fails
transcribeMode: auto # auto | verbatim | structured | ocr | describe | error-fix | chart-sql | design-code
parallelImages: false # 0.2.5+: transcribe each image in its own parallel call
costProvider: '' # 0.2.5+: cheap provider used for small images (cost routing)
costModel: ''
costMaxPixels: 1000000 # images ≤ this many pixels go to costProvider
sceneHints: true # 0.2.5+: append diagnosis hint to screenshot transcriptions
customModes: {} # 0.2.4+: custom mode name → prompt pairs (plugin-in transcribers)
redactSensitive: false # 0.2.4+: mask phone/ID/email in transcription output
auditLog: false # 0.2.4+: log a line per transcription (time/images/bytes/ms/provider)
ocr:
provider: '' # OCR tool engine (blank = disabled; any registered vision provider works)
model: ''
image:
backends: {} # add backends in the UI; nothing is preloaded
activeBackend: '' # image backend key (blank = no image generation)
failoverOrder: [] # 0.2.5+: backend keys tried in order when the active backend fails
verifyChineseText: true # 0.2.3+: check generated Chinese text for garbling via a vision model
verifyProvider: '' # vision provider used for the garbling check (blank = check disabled)
transcribePrompt: ... # custom transcription instruction
transcribeTimeoutMs: 90000 # transcription timeout
# Any OpenAI-compatible vision platform (just add the key; shows up in the model picker & Models page)
extraProviders:
xfyun-vision: # Xfyun MaaS
displayName: 讯飞星辰视觉
baseURL: https://maas-api.cn-huabei-1.xf-yun.com/v2
apiKeyEnv: XFYUN_API_KEY
models:
- id: xoppaddleocrv16 # free OCR (PaddleOCR-VL-1.6)
name: PaddleOCR-VL-1.6
siliconflow-ocr: # SiliconFlow (free hosted DeepSeek-OCR)
displayName: 硅基流动 OCR
baseURL: https://api.siliconflow.cn/v1
apiKeyEnv: SILICONFLOW_API_KEY
models:
- id: deepseek-ai/DeepSeek-OCR
name: DeepSeek-OCR
modelscope-vision: # ModelScope (2000 free calls/day)
displayName: 魔搭视觉
baseURL: https://api-inference.modelscope.cn/v1
apiKeyEnv: MODELSCOPE_API_TOKEN
models:
- id: Qwen/Qwen3-VL-8B-Instruct
name: Qwen3-VL-8B
Image backends (OpenAI-compatible /images/generations and DashScope protocols) example — add them in the UI or write them here:
dsh-multimodal:
image:
backends:
z-image: # Aliyun Z-Image family (new multimodal-generation sync protocol)
kind: dashscope-v2
baseURL: https://dashscope.aliyuncs.com
apiKeyEnv: DASHSCOPE_API_KEY
model: z-image-turbo
defaultSize: 1024*1024 # supports 512*512 ~ 2048*2048
modelscope-t2i:
kind: openai-images
baseURL: https://api-inference.modelscope.cn/v1
apiKeyEnv: MODELSCOPE_API_TOKEN
model: <a lightning-badged text-to-image model in the ModelScope plaza>
activeBackend: z-image
Custom image backends (0.2.6+)
Anything that is not OpenAI-compatible or DashScope can be plugged in with a
small adapter file — no plugin code changes. Write an ES module exporting a
default object with a generate() function:
dsh-multimodal:
image:
backends:
my-api:
kind: custom
adapterFile: D:/my-adapters/my-api.mjs # absolute or relative path
baseURL: https://api.example.com/v1 # passed to the adapter
apiKeyEnv: MY_API_KEY
model: my-image-model
defaultSize: 1024*1024
activeBackend: my-api
failoverOrder: [z-image] # fall back to another configured backend
The adapter receives { prompt, size, n, negative_prompt, reference_image, apiKey, baseURL, model, signal, fetch, log } and returns
{ urls: string[], b64s: string[] }. See
scripts/adapters/example-custom.mjs
for a template. Only point adapterFile at files you trust — the adapter
runs with full process privileges. Polling backends can tune
pollIntervalMs / pollTimeoutMs per backend.
Local vision models (keep sensitive images on your machine)
The plugin routes transcription through any OpenAI-compatible endpoint, so a local
Ollama vision model keeps image data off the network. One-click
preset: Settings → Multimodal → 平台接入 → + 本地 Ollama (auto-adds
http://localhost:11434/v1 to trustedBaseUrls; local endpoints need no API key).
Model matrix (pick by VRAM):
| Model | ollama pull |
VRAM | Notes |
|---|---|---|---|
llava |
ollama pull llava |
~4 GB | Classic 7B, good OCR-ish |
llava-llama3 |
ollama pull llava-llama3 |
~6 GB | Stronger baseline |
qwen2.5vl:7b |
ollama pull qwen2.5vl:7b |
~6 GB | Better Chinese text |
minicpm-v |
ollama pull minicpm-v |
~6 GB | Document/OCR focused |
qwen2.5vl:32b |
ollama pull qwen2.5vl:32b |
~20 GB | High quality, big VRAM |
Compare providers before wiring them in:
node scripts/benchmark-vision.mjs shot.png \
my-endpoint=https://your-vision-api.example.com/v1,your-model,YOUR_API_KEY_ENV \
ollama=http://localhost:11434/v1,llava,SKIP
Troubleshooting
| Symptom | Cause & fix |
|---|---|
| Reply says "【图片转写失败: MISSING_CREDENTIAL…】" | Vision key missing: add it under Settings → Multimodal → 密钥管理, or write the credentials file |
| Reply says "【图片转写失败: RATE_LIMIT…】" | Free-tier rate limit; retry later or switch to a paid tier |
| Transcription is off (images ignored) | vision.transcribeProvider is blank — set it to a registered provider in Settings → Multimodal → 视觉转写 |
| No "Multimodal" entry in Settings | Verify the plugin is mounted (dsh --profile web --dump-config should list dsh-multimodal) and hard-refresh the browser (Ctrl+F5) |
| Custom vision provider skipped with a "not trusted" warning | Its base URL isn't allow-listed: add it to trustedBaseUrls (or use one of the official hosts); also make sure its key env is in allowedApiKeyEnvs |
Development
npm install
npm run build # host: tsc; client: tsc check + tsdown builds lib/client.js
No comments yet. Be the first to write one.