ysr666/dsh-vision-router
ysr666/dsh-vision-router为纯文本 Agent 提供视觉能力:内置免 Key 视觉链 + 像素级视觉工具(看图问答、定位、裁剪、像素对比、取色、OCR、矢量化、抠图、截图);粘贴图片即可用。
为纯文本 DeepSeek 模型提供按需视觉:上传图片后,模型通过 view_image 工具调用任意 OpenAI 兼容视觉端点(默认 Qwen/DashScope)。
中文 · English
Give a text-only LLM (e.g. DeepSeek) in DeepSeek Harness "eyes on demand": pasted/attached images stay in the backend, and the model calls the view_image tool whenever it wants to look at one (backed by any OpenAI-compatible vision endpoint; DashScope Qwen by default) — as if the model were natively multimodal.
No toggle, no manual tool call — paste a screenshot with Ctrl+V and ask like any normal message. DeepSeek decides in its reasoning that it needs to look, calls view_image on its own, and answers directly, in one smooth flow:
Real capture above: paste an MSN screenshot and ask "describe the layout of this page". Notice the
Think → Tool call · view_image → Thinkchain — recognition is never forced into the first step; the model decides to look exactly when it needs to, and vision extraction flows seamlessly into the final answer, just as if DeepSeek were natively multimodal.
DeepSeek is a text-only model, so the Harness refuses to send image-bearing messages when "the current model does not support images". This plugin:
view_image tool that the main model calls on demand to "see" an image; the vision model returns the description/OCR text and the main model continues answering.# 1) Install the plugin (github form; a bare npm package name also works)
dsh plugin --profile web add github:Leeminjing/dsh-eyes
# 2) Configure the API key (Windows; use your vision provider's key)
setx VISION_API_KEY "sk-your-key"
# 3) Configure the vision model (required — use a model your account can call)
setx VISION_MODEL "qwen-vl-plus"
# 4) Configure the endpoint (defaults to DashScope; change it for other OpenAI-compatible providers)
setx VISION_ENDPOINT "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"
# 5) Restart dsh so the environment variables take effect
setxonly affects newly started processes, so restart dsh after configuring. The model id and endpoint are provider-specific: DashScope usesqwen-vl-plus/qwen-vl-max, OpenAI usesgpt-4o, OpenRouter usesqwen/qwen2.5-vl-72b-instruct, etc. See the table below.
view_image(attachment_id=…);view_image extracts the image content via the vision model, and the main model answers from that text.You can keep asking about the same image in later turns ("what's the number in the second row again?") — the image stays in the backend and can be viewed repeatedly.
paste image + question
│
▼
send admission ──(1) mark target model as image-capable── persist image (attachment_id)
│
▼
before dispatch ──(2) strip image blocks → 【图片N attachment_id=…】note, register in the session's shard
│
▼
main model receives text (note + your question)
│ model decides to look
▼
call view_image(attachment_id) ──(3) read bytes → base64 → POST to vision endpoint
│
▼
vision model returns description/OCR text
│
▼
main model answers from the text
Three parts:
llm.resolveModelInfo so the target main model declares inputModalities: ['text','image'], letting image messages pass the host's send-admission check and be persisted.llm.streamWithRegistration to swap each image block (including those nested inside tool-result) for a reference note right before dispatch, and register attachment_id → ImageAttachmentRef sharded by sessionId. A new options object is built because the incoming one may be frozen.view_image tool: reads image bytes by a single attachment_id, or an attachment_ids array to view several at once (or a local image_path), turns them into data: URLs, POSTs to the configured vision endpoint (Chat Completions or Responses protocol per VISION_API_STYLE), and returns text (each image labeled 【图片N】 when batching).| Item | Environment variable | Default |
|---|---|---|
| API key | VISION_API_KEY | (required) |
| Vision model | VISION_MODEL | (required, no default) |
| Endpoint | VISION_ENDPOINT | https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions |
| API style | VISION_API_STYLE | auto (derived from the endpoint URL) |
| Target text model | targetProvider (in code) | deepseek-official |
Max local image bytes (image_path) | maxImageBytes (in code) | 15 MB |
Pasted/attached images are capped by the Harness attachment store (5 MB by default), independent of the local-file limit above.
Chat Completions and Responses API are both supported:
VISION_API_STYLEisauto(default),chat, orresponses.
auto: derived from the endpoint URL —.../chat/completions→ Chat Completions,.../responses→ Responses API; a bare base URL defaults to Chat Completions and gets the path appended automatically.chat/responses: forced; the plugin normalizes the endpoint path to the matching protocol. Both the request body and the response parsing switch with the style (messages/image_url↔input/input_image), so switching is transparent to how you use the plugin.
Common OpenAI-compatible vision providers:
| Provider | Endpoint | Example models |
|---|---|---|
| Alibaba Cloud Model Studio | https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions | qwen-vl-plus / qwen-vl-max |
| OpenAI | https://api.openai.com/v1/chat/completions | gpt-4o / gpt-4o-mini |
| Moonshot | https://api.moonshot.cn/v1/chat/completions | moonshot-v1-8k-vision-preview |
| OpenRouter | https://openrouter.ai/api/v1/chat/completions | qwen/qwen2.5-vl-72b-instruct |
You can also pass a config block for the row in cordis.patch.yml (overrides defaults / env vars):
- insert:
- id: dsh-eyes
name: dsh-eyes
config:
apiKey: sk-xxx # same as VISION_API_KEY
model: qwen-vl-plus # same as VISION_MODEL
# endpoint, targetProvider, maxImageBytes likewise
llm.resolveModelInfo and llm.streamWithRegistration directly. Side effect: text-only main models show as image-capable in the model picker (intentional — required to admit image messages).sessionId (a session can only view its own attachments) and persisted to .dsh/attachments/v1/dsh-eyes-index.json (loaded on startup, written on new images). So historical images remain viewable by attachment_id even after context compaction or a process restart.