Back to home@lmh-2026

dsh-periscope

DSH plugin: keep text-only DeepSeek models (V4-Flash / V4-Pro) and auto-route image-bearing requests to the official vision model (deepseek-v4-flash-vision-exp) - no manual model switching.

Stars
0
Language
JavaScript
Created
Aug 23, 2026
Updated
Aug 23, 2026

Introduction

English | 简体中文

🔭 dsh-periscope

DSH plugin license version node

🎯 Like a periscope, dsh-periscope lets a text-only DeepSeek model "see" the imagery without ever switching models.

📎 Keep deepseek-v4-flash / deepseek-v4-pro as the session default and automatically route every request that carries image content to the official vision-capable model deepseek-v4-flash-vision-exp on the same provider. Text-only requests stay on the text model. No manual switching, no third-party vision model, no OCR — the image is sent raw to DeepSeek's own vision model. ✨

🤔 Why

Most DSH "vision" plugins bridge a third-party vision language model (GLM / Qwen / Gemini / Zhipu …) that transcribes the image to text, then hand the description to DeepSeek. That needs an extra API key, adds a lossy image→text step, and often requires picking a different provider. 😮‍💨

dsh-periscope takes the transparent route instead: it keeps your session on a text model and swaps the wire model to deepseek-v4-flash-vision-exp only for turns that actually contain images, using your existing deepseek-official route and API key. The vision model sees the original pixels. 🔍

⚙️ How it works

Two gates stand between pasting an image and it reaching a vision-capable provider in a text-only session; this plugin clears both:

  1. 🚪 Host image admission — when you submit a message with an image, the host's prompt handler rejects it unless the session model declares image input (MODEL_DOES_NOT_SUPPORT_IMAGES). The plugin wraps llm.resolveModelInfo so every configured text model reports image input, letting the prompt (and its image) into the conversation.
  2. 🔀 Stream routing — every request funnels through llm.streamWithRegistration. The plugin wraps it: if messages contain an image block and the request is routed to one of the configured text models, it re-dispatches on the configured vision model, so the image blocks are actually sent (no [image omitted because this model accepts text only] projection, no UNSUPPORTED_CONTENT refusal).
text-only session (flash/pro)
  │  attach an image 📷
  ▼
host admission   ── resolveModelInfo wrapped → image admitted ✅
  ▼
llm.stream       ── streamWithRegistration wrapped
  │   messages contain an image?
  │     ├─ no  → pass through on deepseek-v4-flash / deepseek-v4-pro (zero-cost) ⚡
  │     └─ yes → re-dispatch on deepseek-v4-flash-vision-exp (official vision API) 🎯
  ▼
wire request     ── same provider + API key, image sent raw 📤

📦 Install

# from npm
dsh plugin --profile web add dsh-periscope

# from a local tarball
dsh plugin --profile web add .\dsh-periscope-0.2.0.tgz

Then restart the DSH app (the bundle list is read at startup). 🔄

🛠️ Configuration

Defaults work out of the box for the official DeepSeek setup:

FieldDefaultMeaning
providerdeepseek-officialLLM route provider owning the models
textModels["deepseek-v4-flash", "deepseek-v4-pro"]Text-only models whose image-bearing requests are routed to the vision model
visionModeldeepseek-v4-flash-vision-expModel used for requests with images

Override in the profile's cordis.patch.yml (user layer replaces the whole row config):

- id: periscope
  config:
    provider: deepseek-official
    textModels: [deepseek-v4-flash, deepseek-v4-pro]
    visionModel: deepseek-v4-flash-vision-exp

The provider's catalog must contain the text models and the vision model whose entry declares image input (the DeepSeek catalog already does). ✅

📝 Notes

  • The switch is per-request and content-driven: image turns run on the vision model (including the conversation history), text turns run on the text model.
  • The request-header log and token metering still record the text model (the header is written before the stream starts); this is a known cosmetic inaccuracy.
  • The read_image tool's own capability gate is not affected; it still uses the session route's declared input.

📄 License

MIT