<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Visual Memory on RockB</title><link>https://baeseokjae.github.io/tags/visual-memory/</link><description>Recent content in Visual Memory on RockB</description><image><title>RockB</title><url>https://baeseokjae.github.io/images/og-default.png</url><link>https://baeseokjae.github.io/images/og-default.png</link></image><generator>Hugo</generator><language>en-us</language><lastBuildDate>Thu, 10 Sep 2026 06:34:16 +0000</lastBuildDate><atom:link href="https://baeseokjae.github.io/tags/visual-memory/index.xml" rel="self" type="application/rss+xml"/><item><title>dsh-vision-router Visual Memory: Eyes for Text-Only DeepSeek Harness Agents</title><link>https://baeseokjae.github.io/posts/dsh-vision-router-visual-memory/</link><pubDate>Thu, 10 Sep 2026 06:34:16 +0000</pubDate><guid>https://baeseokjae.github.io/posts/dsh-vision-router-visual-memory/</guid><description>dsh-vision-router gives text-only DeepSeek Harness agents eyes: pixel-faithful vision routing, 14 vision tools, cached visual memory, and a free keyless fallback.</description><content:encoded><![CDATA[<p>dsh-vision-router is a DeepSeek Harness (DSH) plugin that gives text-only agents eyes by routing image turns to a separate vision model while DeepSeek stays the reasoning brain. It installs with one command, needs no Python and no API key, and ships 14 deep vision tools plus cached visual memory so a text-only agent genuinely remembers earlier images without re-spending vision calls.</p>
<h2 id="what-is-dsh-vision-router-and-why-text-only-deepseek-agents-need-eyes">What is dsh-vision-router and why text-only DeepSeek agents need eyes</h2>
<p>DeepSeek Harness is a local agent framework built around DeepSeek models. Many of its most popular models, including deepseek-v4-flash, are text-only: they declare <code>inputModalities: [&quot;text&quot;]</code> and simply cannot accept image attachments. That is a real limitation for agent work, because a growing share of tasks — reading a screenshot, checking a rendered UI, verifying a chart, OCRing a receipt — are fundamentally visual.</p>
<p>dsh-vision-router solves this by keeping the architecture clean: DeepSeek remains the brain, and a separate vision model becomes the eyes. When an image arrives, the plugin hands that turn to a vision model, gets a description or a structured result, and feeds it back to DeepSeek as ordinary text. Text turns are untouched in model, cost, and context. The plugin is free by default, runs with no Python, and installs in a single command.</p>
<p>The project has drawn real attention: it carries roughly 1,093 GitHub stars on the DSH Hub catalog (updated 2026-09-09) and about 1,021 stars with 44 forks on the ysr666 repository. That level of adoption signals that &ldquo;text-only brain plus separate vision model&rdquo; is not a niche hack — it is becoming the standard architecture for local multimodal agent work.</p>
<h2 id="the-problem-dsh-rejects-images-for-text-only-models-model_does_not_support_images">The problem: DSH rejects images for text-only models (MODEL_DOES_NOT_SUPPORT_IMAGES)</h2>
<p>The root problem is documented in the DeepSeek Harness discussions. When the active model is text-only, DSH rejects image attachments at the GUI/host admission layer with the error <code>MODEL_DOES_NOT_SUPPORT_IMAGES</code>. The model&rsquo;s <code>inputModalities</code> array simply does not include <code>&quot;image&quot;</code>, so the harness refuses the attachment before the agent ever sees it.</p>
<p>A common private-deployment setup tries to work around this by pairing a text-only main model with a separate multimodal &ldquo;vision helper&rdquo; — for example Qwen3.6-27B served through llama.cpp as an MCP server exposing a <code>describe_image(path)</code> function. That MCP workaround works when you can hand it a file path, but the GUI attachment path never yields a reference the agent can actually see. The image is dropped at admission, and the agent is left blind.</p>
<p>This is exactly the gap dsh-vision-router fills. Instead of fighting the admission layer, it patches the harness composition so image turns are routed to a vision-capable model before DeepSeek has to process them. The result is that pasting an image into a text-only DeepSeek session just works, like an ordinary tool-calling turn.</p>
<h2 id="routing-bridge-vs-description-bridge-how-dsh-vision-router-keeps-pixels-faithful">Routing bridge vs description bridge: how dsh-vision-router keeps pixels faithful</h2>
<p>Most DSH vision plugins take a &ldquo;description bridge&rdquo; approach: they convert an image into a text description up front, then feed that text to the model. This is lossy, one-shot, and blind to pixels. If the description misses a detail, the model never gets a second chance to look.</p>
<p>dsh-vision-router is a &ldquo;routing bridge&rdquo; instead. It hands the image turn straight to a vision model, pixel-faithful, and lets that model do the actual seeing. The vision model can crop, ground, compare pixels, run OCR, trace contours, and take screenshots — operations that a text description can never capture. DeepSeek then reasons over the structured result.</p>
<p>The plugin is capability-aware: it auto-routes based on measured evidence, never inferred from model names. It classifies errors (region, ToS, quota, rate-limit, context, network) and applies Retry-After-aware cooldowns, so a failing vision backend does not silently break the turn.</p>
<h2 id="one-command-install-and-the--vision-composer-control">One-command install and the &ldquo;👁 Vision&rdquo; composer control</h2>
<p>Installation is a single command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>npx @deepseek-ai/dsh plugin --profile web add dsh-vision-router
</span></span></code></pre></div><p>Then restart <code>dsh web</code>. The plugin ships its own composition patch (<code>dsh.bundle.patch</code>), so there are no manual file edits to make. After restart, a &ldquo;👁 Vision&rdquo; composer control appears in the web UI, and pasting an image into the chat works like any ordinary tool-calling turn.</p>
<p>The plugin is verified against DSH 0.1.5-alpha.1 (v2.1.5), with stable Host support through 0.1.2-rc.1. It requires Node.js 22 or newer.</p>
<h2 id="the-14-deep-vision-tools-and-what-each-one-does">The 14 deep vision tools and what each one does</h2>
<p>The plugin exposes 14 deep vision tools (15 with the privacy-gated <code>vision_screenshot</code> opt-in). Here is what each one does:</p>
<table>
  <thead>
      <tr>
          <th>Tool</th>
          <th>Purpose</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>vision_describe</code></td>
          <td>Free-form visual Q&amp;A about an image</td>
      </tr>
      <tr>
          <td><code>vision_ground</code></td>
          <td>Ground a phrase to a bounding region in the image</td>
      </tr>
      <tr>
          <td><code>vision_detect</code></td>
          <td>Detect objects or regions of interest</td>
      </tr>
      <tr>
          <td><code>vision_crop</code></td>
          <td>Crop a region for closer inspection</td>
      </tr>
      <tr>
          <td><code>vision_present</code></td>
          <td>Present an image or region back to the model</td>
      </tr>
      <tr>
          <td><code>vision_pixel_diff</code></td>
          <td>Compare two images pixel-by-pixel and report the diff</td>
      </tr>
      <tr>
          <td><code>vision_colors</code></td>
          <td>Extract the dominant color palette</td>
      </tr>
      <tr>
          <td><code>vision_ocr</code></td>
          <td>Extract text from the image</td>
      </tr>
      <tr>
          <td><code>vision_trace</code></td>
          <td>Trace contours into SVG paths</td>
      </tr>
      <tr>
          <td><code>vision_extract_foreground</code></td>
          <td>Cut out the foreground subject</td>
      </tr>
      <tr>
          <td><code>vision_html_screenshot</code></td>
          <td>Render HTML to a screenshot</td>
      </tr>
      <tr>
          <td><code>vision_screenshot</code></td>
          <td>Take a screenshot (opt-in, privacy-gated)</td>
      </tr>
      <tr>
          <td><code>vision_long_screenshot_ocr</code></td>
          <td>OCR a long/scrolling screenshot</td>
      </tr>
      <tr>
          <td><code>vision_bootstrap</code></td>
          <td>Initialize the vision pipeline</td>
      </tr>
  </tbody>
</table>
<p>The pipeline runs on <code>sharp</code> (downscale, crop, pixel diff), <code>potrace</code> (SVG trace), <code>tesseract</code> (OCR), and system Chrome (HTML screenshots) — no Python required.</p>
<h2 id="visual-memory-how-image-answers-are-cached-and-reused">Visual memory: how image answers are cached and reused</h2>
<p>The &ldquo;visual memory&rdquo; in the keyword is the plugin&rsquo;s caching layer. Vision answers are cached by attachment content hash, so when the same image appears again in a later text turn, the plugin substitutes the recorded description — marked as untrusted evidence — without re-spending a vision call.</p>
<p>By default the cache keeps answers for <code>cacheTtlSeconds</code> of 3600 seconds (one hour) and up to <code>cacheMaxEntries</code> of 200 entries. This matters for two reasons. First, it saves cost and latency: the same screenshot referenced three times costs one vision call, not three. Second, it gives the text-only agent genuine continuity: it can &ldquo;remember&rdquo; an image from earlier in the conversation even though DeepSeek itself never saw the pixels.</p>
<h2 id="the-free-vision-chain-ovhcloud-fallback-and-free-key-channels">The free vision chain: OVHcloud fallback and free key channels</h2>
<p>The plugin is free out of the box. It ships a built-in keyless OVHcloud anonymous fallback chain with 5 model buckets. The anonymous tier is capped at about 2 requests per minute per IP per model, which across the independent buckets works out to roughly 10 RPM in theory.</p>
<p>User-provided vision models always run first. If you add a free OVHcloud AI Endpoints access key, the same Qwen2.5-VL-72B-Instruct endpoint jumps to 400 requests per minute per project per model — a 200x improvement over the anonymous cap.</p>
<p>Free-tier vision policies rotate, so the plugin&rsquo;s &ldquo;free vision key channels&rdquo; note tracks the current landscape: Cerebras retired its free tier in July 2026, SambaNova dropped to 20 requests per day, and Hugging Face is now $0.10 per month. The keyless fallback means you are never blocked when a free key disappears.</p>
<h2 id="local-first-privacy-ollama-and-lm-studio-backends">Local-first privacy: Ollama and LM Studio backends</h2>
<p>For private or offline work, the plugin supports optional Ollama and LM Studio keyless vision backends. This is the local-first privacy path: recognition happens entirely on your machine, with no external endpoint and no data leaving the host. It is the right choice when the images are sensitive, when you are offline, or when you simply want zero external dependencies.</p>
<h2 id="the-verifiable-pixel-loop-for-ui-restoration">The verifiable pixel loop for UI restoration</h2>
<p>One of the plugin&rsquo;s most distinctive workflows is the verifiable pixel loop. Instead of eyeballing whether a UI rebuild matches a reference, you measure it:</p>
<ol>
<li>Feed the reference image to the agent.</li>
<li>Use <code>vision_html_screenshot</code> to render the current implementation.</li>
<li>Use <code>vision_pixel_diff</code> to compare the two pixel-by-pixel.</li>
<li>Fix the mismatch, re-render, and repeat until the diff converges.</li>
</ol>
<p>The project&rsquo;s demo verified a UI rebuild at a 2.54% final diff — 32,939 differing pixels out of 1,296,000, at a threshold of 16 per channel. That turns UI restoration from a subjective &ldquo;looks close&rdquo; into a measurable, converging number.</p>
<h2 id="configuration-essentials-routingmode-stealth-autowrapproviders-cache">Configuration essentials (routingMode, stealth, autoWrapProviders, cache)</h2>
<p>The plugin&rsquo;s key configuration options:</p>
<ul>
<li><strong>routingMode</strong> — how image turns are routed to vision backends (capability-aware auto-routing by default).</li>
<li><strong>stealth</strong> — optionally take over the official <code>deepseek-official</code> route so the model picker looks exactly like stock while each entry is an auto-vision wrapper.</li>
<li><strong>autoWrapProviders</strong> — automatically wrap provider entries so every model in the picker gets vision capability.</li>
<li><strong>cacheTtlSeconds / cacheMaxEntries</strong> — control the visual-memory cache lifetime and size (defaults 3600s and 200 entries).</li>
</ul>
<h2 id="troubleshooting-common-issues-bom-sharp-conflict-dsh-web-ui-rewrite">Troubleshooting common issues (BOM, sharp conflict, dsh-web-ui rewrite)</h2>
<p>Three issues come up most often. First, a byte-order mark (BOM) in a config or patch file can break parsing — strip it if the plugin fails to load. Second, <code>sharp</code> can conflict with an existing native dependency in the project; resolve the version mismatch before the pipeline runs. Third, if you have customized <code>dsh-web-ui</code>, the plugin&rsquo;s composition patch may need to be re-applied after a rewrite, because the patch targets the stock UI structure.</p>
<h2 id="alternatives-compared">Alternatives compared</h2>
<p>dsh-vision-router is not the only option. Here is how it stacks up against the main alternatives:</p>
<table>
  <thead>
      <tr>
          <th>Approach</th>
          <th>How it works</th>
          <th>Best for</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>dsh-vision-router</td>
          <td>Routing bridge, pixel-faithful, 14 tools, free fallback</td>
          <td>Text-only DeepSeek agents that need real vision</td>
      </tr>
      <tr>
          <td>dsh-vision-sidecar / proxy</td>
          <td>External vision service</td>
          <td>Teams that already run a vision service</td>
      </tr>
      <tr>
          <td>dsh-vision-toolkit</td>
          <td>Tool collection, no routing</td>
          <td>Simple, one-off vision calls</td>
      </tr>
      <tr>
          <td>Pilco-mmbridge</td>
          <td>Turns a text-only model multimodal without fine-tuning</td>
          <td>Running multimodal benchmarks on text-only LLMs</td>
      </tr>
      <tr>
          <td>MCP <code>describe_image</code></td>
          <td>File-path-based description helper</td>
          <td>When you can pass file paths, not GUI attachments</td>
      </tr>
  </tbody>
</table>
<p>The plugin is a poor fit where the harness already provides the required vision route, or where external anonymous endpoints are unsuitable — the fallback is rate-limited to 2 req/min per IP per model. But for a text-only DeepSeek agent that needs real, pixel-faithful eyes with free fallback and cached visual memory, dsh-vision-router is the most complete option.</p>
<h2 id="faq">FAQ</h2>
<p><strong>Does dsh-vision-router require an API key?</strong>
No. It works free out of the box with a built-in keyless OVHcloud anonymous fallback chain. You can optionally add your own vision model or a free OVHcloud key to lift the rate limit from ~2 to 400 requests per minute.</p>
<p><strong>Does it need Python?</strong>
No. The entire pipeline runs on sharp, potrace, tesseract, and system Chrome. It requires Node.js 22 or newer.</p>
<p><strong>How do I install it?</strong>
Run <code>npx @deepseek-ai/dsh plugin --profile web add dsh-vision-router</code>, restart <code>dsh web</code>, and a &ldquo;👁 Vision&rdquo; composer control appears. No manual file edits are needed.</p>
<p><strong>What is visual memory in dsh-vision-router?</strong>
Vision answers are cached by attachment content hash (default 1-hour TTL, 200 entries). When the same image reappears, the plugin substitutes the recorded description without re-spending a vision call, so the text-only agent remembers earlier images.</p>
<p><strong>Which DeepSeek models does it support?</strong>
It is designed for text-only models like deepseek-v4-flash that reject image attachments with <code>MODEL_DOES_NOT_SUPPORT_IMAGES</code>. It is verified against DSH 0.1.5-alpha.1 with stable Host support through 0.1.2-rc.1.</p>
]]></content:encoded></item></channel></rss>