DeepSeek brain + automatic image transcription: attach images in the GUI and each one is transcribed via the official deepseek-v4-flash-vision-exp by default (a pure-text V4-Pro brain can see images), with any OpenAI-compatible VLM or local Ollama as alternatives.
Install
# from npm (prebuilt)
dsh plugin --profile web add dsh-vision-proxy
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:Flyvhidbwo/dsh-vision-proxy
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time — pnpm blocks those until you allow them, so an install can stop with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED or ERR_PNPM_IGNORED_BUILDS; dsh prints the exact key to add under allowBuilds in your profile’s pnpm-workspace.yaml, and the install works on the next run. Allowing a build is a trust decision: only install sources you trust, and pin a commit (github:owner/repo#sha).
README
Keep DeepSeek as the conversation brain — attach images anyway. GUI image attachments are auto-transcribed for text-only DeepSeek models on DeepSeek Harness, with a settings page in the dsh Web/Desktop UI.
⚠️ Compatibility & positioning (Oct 2026, v0.5.3): supports dsh 0.2.x and is verified against dsh 0.2.1-rc.5 (desktop runtime and CLI; the rc5 runtime's
dsh-llm/dsh-attachment/ settings and client-module files are byte-identical to the npm0.2.1-rc.5packages, so the contract surface is unchanged). Settings → Vision proxy offers the two-mode configuration (zero-config officialdeepseek-flash, or a custom VLM endpoint) and applies live without a restart.Since dsh 0.2.x, DeepSeek's natively multimodal models (
deepseek-flash,deepseek-v4-flash-vision-exp, …) accept images directly — you do not need this plugin for those. The plugin covers the other half: text-only models still cannot see images (e.g.deepseek-v4-pro). It transcribes images to text before handing the conversation to them. Typical uses: a text-only Pro/Flash brain with eyes, local Ollama (images never leave the machine, free), any OpenAI-compatible VLM. When the selected model already accepts images, images are passed through untranslated.v0.5.0 fixes the root cause of the "silent failure on dsh 0.2.x" reported in issue #2: on 0.2.x
ctx.llm.registration()throwsNO_ADAPTERwhile the wrapped route is not registered yet (it does not returnundefined), and 0.4.1 called it synchronously insideapply(), so the entry never activated and the route never existed. The adapter is now resolved lazily, with inner-route discovery (deepseek-officialordeepseek-account).
Why this exists
DeepSeek Harness natively gates image attachments on the selected model's declared inputModalities. A text-only model rejects attachments by design, and tool-based vision plugins (view_image on a file path) do not help with GUI image attachments.
This plugin closes that gap: it registers a new provider route (deepseek-vision) that wraps the real DeepSeek adapter, claims image input (so the attachment preflight admits images), and transcribes every attached image to text in the request stream before delegating to DeepSeek. The conversation is still answered by DeepSeek; vision is an add-on.
user attaches image ──▶ deepseek-vision route ──▶ transcribe via VLM (OCR + layout + details)
│ │
▼ ▼
DeepSeek answers ◀── text-only conversation (images replaced by [图片转译] text)
Features
- Two-mode settings page. Settings → Vision proxy: the default "DeepSeek official vision" mode is zero-config (auto-uses
deepseek-flashwith yourDEEPSEEK_API_KEYandapi.deepseek.com) and shows nothing but the mode switch; switching to "Custom VLM endpoint" reveals the endpoint / model / timeout fields. Saving writes the profile'scordis.patch.ymland applies to the next turn, live; secret fields only show "stored" and are never sent back to the browser. - Native-vision pass-through. When the selected model declares image input (
deepseek-flash,deepseek-v4-flash-vision-exp, …), images go straight through — no wasted VLM call and no loss of native quality. - Inner-route discovery. If the configured
innerProvideris not registered, the plugin falls back todeepseek-official→deepseek-account→ any registered route. An API-key profile and a signed-in-account profile both work out of the box. - No hangs, ever. Public anonymous endpoints are hard-capped at 20 s and HTTP 429 fails immediately (no useless
Retry-Aftersleep); endpoints that just failed are cooled down for 60 s. Loopback endpoints are exempt from that policy: a cold Ollama load is legitimately slow, so the configured timeout applies andkeep_alivekeeps the model resident. - Multi-model, multi-provider. Any OpenAI-compatible VLM endpoint works — DashScope/Qwen, QwenCloud (international), Zhipu, OpenRouter, local Ollama, or your own. Each
fallbackModelsentry can carry its ownbaseURL/model, so one install can chain providers. - Zero-config local path. With
autoLocalOllama(default on), a running Ollama athttp://localhost:11434is detected at startup and appended to the fallback chain — images never leave your machine. - Thinking-model tolerance.
qwen3-vl,deepseek-flashand friends can spend the whole output budget onreasoningand return an emptycontent; the reasoning text is then used instead of discarding a completed transcription. - Transcription survives session aborts. By default the session-level abort signal is not chained into the transcription request (
abortWithSession: false): dsh cancels and re-issues work around large images, and the old behaviour killed inferences the GPU was already running. Opt back in from the settings page. - Fallback chain with classified errors.
rate_limit/quota/auth/region/model_not_found/context_too_large/httpare classified with actionable hints; the placeholder reports every attempt, not just the first. - Content-hash cache. Transcriptions are cached by the SHA-256 of the image bytes (in-process, capped at 200) — the same image is transcribed at most once per process.
- Auto-downscale (optional). On dsh 0.2.x the attachment service's
readImageRequestprojection does the resize natively (cached); older backends fall back toreadImageplus optionalsharpdownscaling. read_imagecompatible. The nativeread_imagetool also works on this route (its capability gate reads the same model info).
Supported models & providers
One config (baseURL + model, optionally apiKey) covers every backend:
| Scenario | baseURL | model | Notes |
|---|---|---|---|
| DeepSeek official vision — default | https://api.deepseek.com |
deepseek-v4-flash-vision-exp |
Reuses DEEPSEEK_API_KEY, no extra key; best official vision quality |
| DashScope (China) | https://dashscope.aliyuncs.com/compatible-mode/v1 |
qwen3.7-flash / qwen3-vl-flash |
Cheap, fast, no rate limit. Keys: sk-ws-… (Qwen platform) or sk-… (Bailian console) |
| Local Ollama (auto-detected) | http://localhost:11434/v1 |
first vision-capable model | Zero config when installed; images never leave the machine; keep_alive is sent |
| QwenCloud (intl.) | https://dashscope-intl.aliyuncs.com/compatible-mode/v1 |
qwen3-vl-plus etc. |
International variant |
| Zhipu (free tier) | https://open.bigmodel.cn/api/paas/v4 |
glm-4.6v-flash |
Free tier, still needs a (free) Zhipu API key |
| Anything OpenAI-compatible | your endpoint | your model | OpenRouter, Ark, vLLM, gateways — the plugin only speaks /chat/completions |
⚠️ Anonymous third-party free tiers are NOT bundled as a default fallback. In field testing they were strictly rate-limited and occasionally hung; as a default they just reproduce a broken experience. If you still want one, add it via
fallbackModelswithanonymous: true(the 20 s cap still applies).
Pricing (CNY, Alibaba Model Studio mainland + DeepSeek official, Aug 2026 reference)
| Model | Input | Output | One 1080p screenshot (≈2000 tokens) |
|---|---|---|---|
| qwen3-vl-flash | ¥0.15 / M tokens | ¥1.5 / M tokens | ≈ ¥0.0005 |
| qwen3.7-flash | ¥0.2 / M tokens | ¥0.8 / M tokens | ≈ ¥0.001 |
| deepseek-v4-flash-vision-exp (official, default) | ¥3 / M (peak) · ¥1.5 (off-peak) | ¥9 / M (peak) · ¥4.5 (off-peak) | ≈ ¥0.01 / ¥0.005 |
| Local Ollama | free | free | ¥0 (images never leave the machine) |
Images are billed by resolution-derived tokens, not per image. A 1080p screenshot is ≈ 2000 tokens, i.e. well under one fen per image; heavy use (100 images a day) costs a few yuan a month. Local Ollama is free.
Key resolution order: config apiKey → $VISION_API_KEY → $DASHSCOPE_API_KEY. Anonymous and loopback endpoints need none; keyless non-anonymous entries are skipped.
Quick start
# profile name as used by your host: the desktop app uses `desktop`, `dsh web` uses `web`
dsh plugin --profile desktop add dsh-vision-proxy
After a restart:
- the model picker shows DeepSeek + auto vision (select it so a text-only brain receives images);
- Settings → Vision proxy edits the configuration (applies on the next turn, no restart; secrets are never echoed back);
- paste an image into any conversation.
Default strategy (zero config): official
deepseek-flashtranscription that reuses yourDEEPSEEK_API_KEYandhttps://api.deepseek.com— the settings page shows only this mode by default, nothing to fill in. Switch to "Custom VLM endpoint" on that page (DashScope / Zhipu / OpenRouter / local Ollama) to reveal the full configuration. A local Ollama is appended automatically as a safety net; models that already accept images are passed through untranslated.
pnpm ≥ 10 blocks dependency build scripts — the first install exits non-zero with Ignored build scripts: dsh-vision-proxy, sharp. Approve both, then re-run the install:
# profile's pnpm-workspace.yaml
allowBuilds:
dsh-vision-proxy: true
sharp: true
dsh plugin --profile desktop add dsh-vision-proxy # re-run after approval
Slow npm registry?
dsh plugin --profile desktop add dsh-vision-proxy --registry=https://registry.npmmirror.com(arguments are forwarded to pnpm).
Alternative installs (while npm publishing is temporarily unavailable)
- From GitHub:
dsh plugin --profile desktop add github:Flyvhidbwo/dsh-vision-proxy; in the desktop app use Plugins → Add plugin and entergithub:Flyvhidbwo/dsh-vision-proxy(installs the default branch with the latest fixes). - From a local tarball: the Add-plugin field is plain text (there is no file picker). Paste the absolute
.tgzpath, e.g.C:\Users\<you>\Downloads\dsh-vision-proxy-0.5.3.tgz; the plugin manager classifies an absolute.tgzpath as atarball(verified on rc5:inspectreturnskind: tarball). - From a local directory: extract the
.tgzinto a directory and paste that absolute path instead (it is recorded as afile:link, so do not delete the directory afterwards).
Live demo: a real GUI image turn
A real conversation on the deepseek-vision route: the user pasted a sticker and asked "what do you see"; the VLM transcribed it and DeepSeek answered from the text in a single step (~7.6 s).
Configuration
Preferred: the settings page. Settings → Vision proxy edits 15 live fields; saving writes the profile's cordis.patch.yml and applies to the next turn. Secret fields show only "stored".
The four restart-only knobs (providerId, innerProvider, autoLocalOllama, fallbackModels) live in the profile patch as an id-targeted override, never an insert:
# $DSH_HOME/profiles/desktop/cordis.patch.yml — user-layer override
- id: dsh-vision-proxy
name: 'dsh-vision-proxy'
config:
providerId: deepseek-vision
innerProvider: deepseek-official
autoLocalOllama: true
fallbackModels: []
⚠️ Do not write
- insert: [{id: dsh-vision-proxy, …}]. In dsh's patch semanticsinsertappends entries — the bundle's row and yours would both be instantiated and thedeepseek-visionadapter would be registered twice. A top-level- id:entry targets the existing row and replaces its wholeconfig; omitted keys fall back to the plugin's zod defaults. The settings page saves in exactly that form.
| Key | Default | Meaning |
|---|---|---|
providerId |
deepseek-vision |
Route id shown in the model picker (restart) |
innerProvider |
deepseek-official |
Wrapped route; falls back to deepseek-account → any registered route (restart) |
baseURL |
DashScope compatible mode | OpenAI-compatible VLM endpoint (any vendor, including Ollama) |
apiKey |
'' |
VLM key; falls back to $VISION_API_KEY, then $DASHSCOPE_API_KEY. Never echoed to the browser |
anonymous |
false |
Skip the Authorization header (public registration-free endpoints; 20 s cap applies). Loopback is always treated as local |
model |
qwen3.7-flash |
Vision model id (qwen3-vl-flash, glm-4.6v-flash, qwen3-vl:8b, …) |
maxTokens |
8192 |
VLM output cap (thinking models need room for reasoning) |
timeoutMs |
120000 |
Request timeout (public anonymous endpoints are capped at 20 s; loopback keeps this value) |
maxImagePixels |
4000000 |
Downscale images above this many pixels before transcription (0 disables) |
keepAlive |
30m |
keep_alive sent to loopback servers (Ollama residency; empty disables the field) |
marker |
[图片转译] |
Prefix on every transcription |
failureMode |
placeholder |
On total failure: placeholder inserts [图片转译失败: …] and continues; error fails the turn |
abortWithSession |
false |
Abort a running transcription when the conversation is cancelled (off keeps finished inferences) |
source |
deepseek |
Transcription source: deepseek (default: official deepseek-flash with your DEEPSEEK_API_KEY and api.deepseek.com, zero config) or custom (your endpoint / Ollama). Unset infers the mode so older configs keep working |
deepseekVisionModel |
deepseek-flash |
Official model used when source is deepseek |
deepseekThinking |
disabled |
Thinking policy for the official call: disabled answers directly (faster, fewer tokens); adaptive / enabled think first |
deepseekVision |
— | Deprecated, kept for older configs: false is treated as source: custom |
nativeVisionPassThrough |
true |
Models that already accept images are passed straight through |
autoLocalOllama |
true |
Probe http://localhost:11434 at startup (restart) |
localOllamaModel |
'' |
Pin an Ollama model; empty picks the first vision-capable one |
fallbackModels |
[] |
Ordered fallbacks {model, baseURL?, apiKey?, anonymous?, timeoutMs?} (restart) |
Windows and API keys:
dsh --profile <name> --dump-configprints the composed config, including a key written intocordis.patch.yml. Conversely, environment variables set after the process started (explorer.exe caches its environment) may never reach a running dsh. If you exported a key but still seeskipped — no API key, put the key into Settings → Vision proxy (or the plugin config).
Verify the install
dsh --profile desktop --dump-config | grep -A3 dsh-vision-proxy # exactly one row (prints the config, key included)
- Restart dsh → the model picker shows DeepSeek + auto vision.
- Settings shows the Vision proxy page (means the volatile settings namespace is served).
- Paste an image → DeepSeek answers after a
[图片转译]block. - If the selected model already accepts images (
deepseek-flash), images pass through with no transcription request.
Behavior notes
- Only messages containing image blocks are processed; text-only turns go straight to DeepSeek.
- Public anonymous endpoints: 20 s cap, immediate 429 failure, 60 s cooldown after a failure. Loopback endpoints are exempt.
- Transport failures (
fetch failed, connection refused, …) also cool the endpoint down for 60 s. - A session can never be poisoned (
failureMode: placeholder): a failed image becomes[图片转译失败: 原因]text listing every attempt; the failure is remembered by content hash for 60 s. - Thinking models: an empty
contentwithreasoning/reasoning_contentuses the reasoning text instead of discarding the inference. - 34 unit tests (including
scripts/verify-runtime-contract.mjs, which mounts the proxy into a realdsh-llmruntime), run by GitHub Actions on Node 22/24. - Transcription quality: dense UI screenshots may lose small text — that is the VLM's limit, not a plugin bug. Prefer a stronger model (
qwen3-vl-plus) or a largermaxTokensfor OCR-heavy work.
Troubleshooting
| Symptom | Cause & fix |
|---|---|
On dsh 0.2.x: 1 entry did not activate, no route in the model picker (0.4.1) |
0.2.x registration() throws NO_ADAPTER; 0.4.1 called it inside apply(). Upgrade to 0.5.0 |
| No "DeepSeek + auto vision" group in the picker | Check that the profile's cordis.patch.yml has exactly one dsh-vision-proxy row (dsh --profile desktop --dump-config). For a signed-in-account profile set innerProvider: deepseek-account or leave it to auto-discovery |
| No "Vision proxy" page in Settings | The page only registers while the host serves the dsh-vision-proxy settings namespace — confirm the row is enabled and the profile loads it |
skipped — no API key even though VISION_API_KEY is exported |
explorer.exe caches the environment. Put the key in Settings → Vision proxy (or the plugin config) |
| Local Ollama times out every image (20 s) | 0.4.x wrongly applied the anonymous cap to loopback. Fixed in 0.5.0 (loopback is local, keep_alive is sent) |
Local Ollama: request (N tokens) exceeds the available context size (4096) |
Ollama's default context is too small. Set OLLAMA_CONTEXT_LENGTH=32768 and restart Ollama |
With @anionex/dsh-vision-toolkit installed: needs no image-input variant |
Both plugins provide image input; the features overlap. Disable the toolkit's variant for this route (add config: { imageInputVariants: { enabled: false } } to its row in the profile patch) or uninstall one of them |
Install warns Ignored build scripts: dsh-vision-proxy, sharp |
pnpm ≥ 10 blocks build scripts; approve both in the profile's pnpm-workspace.yaml (allowBuilds) and re-run |
ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION right after a release |
pnpm 11's minimumReleaseAge (1 day). Set minimumReleaseAge: 0 in the profile's pnpm-workspace.yaml or pass --config.minimum-release-age=0 |
Privacy
Transcription sends image bytes (base64 over HTTPS) to the configured VLM endpoint — image data leaves your machine unless baseURL points at a local service (e.g. Ollama). Nothing is persisted beyond the harness's own attachment store. For sensitive images use your own endpoint or a local model — or do not install the plugin.
How it works (for plugin developers)
The plugin uses only public dsh 0.2.x interfaces:
ctx.llm.registerAdapter([providerId], proxyAdapter)registers the new route;- the wrapped adapter is resolved lazily:
ctx.llm.registration(inner)throwsNO_ADAPTER(it does not returnundefined), so resolving it insideapply()breaks with cordis activation order; - the proxy's
listModels/resolveModelquery the inner route for metadata and re-stampproviderwith the route the caller asked about —dsh-llmvalidatesmodel.provider === requested routeand otherwise rejects the whole catalog withINVALID_CATALOG; resolveModeloverridesinputModalitieswith['text', 'image']so the attachment preflight and theread_imagegate admit images;prepareCallbinds the innerprepareCallgeneration andstreamtranscribes image blocks beforeyield*-ing the inner stream; image bytes come fromctx.get('attachments').readImageRequest(ref, target, signal)(0.2.x public projection API), with areadImage(ref)fallback;- Config fields exposed to the settings page are declared
.volatile()(@deepseek-ai/schemastery): dsh serves the volatile fields of the active, uniquely-locatable entry as a settings namespace, persists writes into the profile'scordis.patch.yml, and pushes them to the running plugin through references; - the browser half
lib/client.jsis declared throughdsh.client, served under/plugins, registers asettings.sectionnavigation entry, and reads/writes the namespace throughctx.configForms.get('dsh-vision-proxy').
License
MIT
Links
More in this category
liustack/modlens★ 4725
Vision bridge for text-only models: paste an image, get structured JSON evidence (OCR, layout, semantics).
ysr666/dsh-vision-router★ 1211
Free vision for text-only agents: built-in keyless vision chain plus pixel tools (Q&A, grounding, crop, pixel diff, colors, OCR, SVG trace, cutout, screenshots); paste an image to use it.
Anionex/dsh-vision-toolkit★ 922
Vision for text-only models: paste an image and the model switches to a Vision Toolkit variant for image Q&A, multi-image comparison, long-screenshot OCR, screenshot-to-UI reproduction, element grounding, and pixel diff. No API key by default — images are processed by the author-hosted free service, 100 per machine per day; configurable to your own provider.
SAOS-3624/dsh-video-storyboard★ 384
Turns a video (a local file, or a Bilibili/yt-dlp link) into one addressable storyboard grid — numbered cells with exact timestamps, per-cell transcript and an audio envelope on the same timeline — and video_zoom re-reads any window at higher resolution, so a text-only model can look at the video instead of reading a summary.
dickpy/dsh-imagegen★ 167
AI image generation for the DSH Web GUI: text-to-image and image-to-image through a configurable OpenAI-compatible endpoint (gpt-image-2 / gpt-image-1 / dall-e-3), with an api_url/api_key settings card and a sidebar split-pane generation studio.
fandc520/dsh-comfyui★ 159
Drive a local or remote ComfyUI server from DeepSeek Harness: comfyui_run / comfyui_object_info / comfyui_workflow tools generate and edit images and videos, with a workflow library (graph extraction: per component / main flow / all), a load area with resolution auto-match, a live queue, SDXL and Wan 2.1 templates, a companion skill, and a same-origin media proxy.
Community comments
Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.