Skip to main content
M

dsh-image-edge-cap

mrbeandev/dsh-image-edge-cap

Limits the images sent to chosen providers or models in DeepSeek Harness: cap the longest side and the number of images per request, with its own plugin page

Install

dsh plugin --profile web add github:mrbeandev/dsh-image-edge-cap

README

dsh-image-edge-cap

Limits the images sent to the providers or models you choose in DeepSeek Harness (DSH): the longest side of each image, and the number of images per request. It has its own page in the Plugins view.

npm version license DSH

Only the copy sent in the model request changes. Stored attachments and session logs never change, and providers or models without limits get exactly the same requests as before.

Contents: Supported DSH versions · Why this plugin exists · Install · The plugin page · How it works · Upgrading from 0.1.x · Compatibility and risk · Development

Supported DSH versions

Supported range: DSH 0.2.0-rc.1 up to (not including) 0.3.0. Check yours with dsh --version.

DSH versionnpm tag (at the time of this release)Status
0.2.1-rc.2latest, next✅ Tested
0.2.0-rc.2—✅ Tested
0.2.0-rc.1—✅ Tested
other releases from 0.2.0-rc.1 up to 0.3.0 (not included)—⚠️ Untested: loads with a warning
older than 0.2.0-rc.1 (0.1.x)—❌ Not supported
0.3.0 and newer (prereleases included)—❌ Refused until tested (override with allowUntestedHarness: true)

Node.js: ^22.19.0 or >=24.0.0.

The plugin has no runtime dependencies and does not need any other community plugin. A refused plugin logs a clear error at startup and does not load; its page then says the host plugin is not running. It never half-loads.

Why this plugin exists

DSH resends the whole conversation, including every image so far, with each model request. Some providers reject a request once its images cross a limit. From then on every turn fails: retries, continue, and /compact all resend the same images.

Image too large (Anthropic). A request with more than 20 images is rejected when any image is over 2000 px on either side:

400 invalid_request_error: messages.N.content.1.image.source.base64.data:
At least one of the image dimensions exceed max allowed size for many-image requests: 2000 pixels

DSH's requestImagePixelBudget limits total pixels (width × height), not side length. Tall or wide images therefore pass it unchanged: a full-page mobile screenshot of 390 × 2475 is only 0.97 MP. Max edge = 2000 sends it as 315 × 2000.

Too many images (Cloudflare Workers AI). Workers AI rejects a request with more than 32 distinct images. DSH's OpenAI client shows this as 400 status code (no body), because Cloudflare puts the reason under errors:

Invalid data for image - reason too many images in request (max 32)

DSH can drop old images, but only when its adapter raises a byte-limit error, which this plain 400 is not. Max images = 32 keeps the newest 32 distinct images in every request. Older ones are sent as DSH's own short text note ([image omitted to fit request image limits; …]), so the session continues.

Both cases were reproduced and fixed against the real services. Through the real DSH runtime, 33 distinct screenshots to @cf/deepseek-ai/… returned the 400 without the plugin and succeeded with Max images = 32; 32 images succeeded either way.

Install from npm

dsh plugin --profile web add dsh-image-edge-cap

Stop the running DSH Web process when your active work is finished, then start it again (for example dsh web --no-open --port 3080) and refresh the page. Open Plugins → dsh-image-edge-cap.

To install from a local checkout instead (keep the folder in place, the profile links to it):

dsh plugin --profile web add "/absolute/path/to/dsh-image-edge-cap"

To remove it:

dsh plugin --profile web remove dsh-image-edge-cap

Alternatively, clear a provider's limits; its next request goes out unchanged.

The plugin page

Open Plugins in the sidebar and choose dsh-image-edge-cap. The page lists every registered model provider. Open a provider to see a table: an All models row, then one row per model from DSH's live model catalog (the same list as the model picker). Each row has two limits:

LimitEffect
Max edge (px)Shrinks each image so its longest side is at most this many pixels, keeping the aspect ratio. 16–16384.
Max imagesKeeps only the newest N distinct images in each request; older ones become a short text note. 1–100000.
  • An empty model cell uses the All models value; its placeholder shows that value (all: 2000). 0 turns that limit off for one model even when All models sets it.
  • Change any cells, then press Save. Discard restores the saved values, and Clear all followed by Save removes the provider's limits.
  • Limits apply from the next request, including resumed sessions, subagents, retries and /compact. No restart is needed.
  • A model that a provider no longer lists, or a provider that was removed, stays visible while it still has limits, so they can be cleared. Long model lists get a filter box.
  • A failed or conflicting save shows an error instead of false success. If the host plugin is not running (for example on an unsupported DSH), the page says so and the controls are disabled.

Typical setups:

  • A router that serves Claude and GPT: leave All models empty and set Max edge 2000 only on the Claude rows.
  • Cloudflare Workers AI: Max images 32 on All models, or only on the models that need it.

The limits are stored in this plugin's entry of the profile patch (cordis.patch.yml), so you can also edit them by hand. * is All models:

- id: dsh-image-edge-cap
  name: dsh-image-edge-cap
  config:
    limits:
      my-router:
        models:
          cc/claude-a: { maxImageEdge: 2000 }
          cc/claude-b: { maxImageEdge: 2000 }
      cf-workers-ai:
        models:
          "*": { maxImages: 32 }
          "@cf/zai-org/glm-5.3": { maxImages: 0 }   # 0 = off for this model

How it works

Max edge

  1. An llm/stream listener reads the request's provider and model. If no edge limit applies, it returns the stream untouched.
  2. Otherwise it runs the adapter stream inside an AsyncLocalStorage scope that holds the limit. The scope reaches the image reads the adapter makes while the stream is consumed, even through other llm/stream listeners and concurrent requests.
  3. The plugin wraps readImageRequest on the attachments service. Inside a limited scope it lowers the requested target so the long edge fits, with the short edge rounded the same way the store resizes.
  4. The attachment store resizes the image with its own code and caches it under a new variant id. The plugin contains no image code.

Max images

  1. The plugin wraps the LLM runtime's forAdapter step, the last step before a request reaches the provider adapter. Plain calls, prepared agent calls, retries and compaction summaries all pass through it.
  2. It counts the request's distinct images, by attachment, at each image's newest position. A screenshot resent five times counts once, as with Cloudflare's limit.
  3. When there are more than N, it marks the older ones offloaded on the request copy only. That is the same mark DSH's own image-offload plugin uses, so every adapter already sends DSH's placeholder text for them.

Each change is logged once per process, for example dsh-image-edge-cap: capped a request image 390×2475 → 315×2000 (max edge 2000) and dsh-image-edge-cap: sent the newest 32 of 33 images to cf-workers-ai/@cf/….

Upgrading from 0.1.x

  • The settings moved from the Settings → Image size tab to this plugin's own page under Plugins.
  • Limits are per model now. 0.1.x had one mode per provider (Off / All models / Selected models) and one max edge.
  • Your 0.1.x rules keep working unchanged. The page shows them already converted, with a note. Saving a provider writes it in the new limits form and removes its old providers rule.

Compatibility and risk

The plugin wraps two internal methods, both reversibly: attachments.readImageRequest and llm.forAdapter. Neither is an official extension API. Each wrapper is installed when its service appears, removed when the plugin is disposed, and never installed twice. The version gate refuses DSH releases outside the tested range. If forAdapter is ever missing, the plugin logs a warning and keeps only the edge limit.

  • Both limits apply to any adapter that prepares request images through these seams. On DSH 0.2 that is the pi-ai adapter (custom OpenAI-compatible, Anthropic, Cloudflare and other pi-ai providers), which the tests cover, and the native DeepSeek adapter.
  • It never enlarges an image and never undoes DSH's own pixel, byte or image budgets; it can only send fewer or smaller images.
  • Dropped images are not seen by the model on that request, as with DSH's own image offload. Set Max images just under the provider's real limit.

Plugin config keys (on the dsh-image-edge-cap entry):

KeyMeaning
limitsPer-provider, per-model limits edited by the plugin page.
providers0.1.x per-provider rules; still honoured until the provider is saved.
enabled: falseLoad without limiting anything.
harnessEntryPath of the running dsh CLI entry (detected automatically).
allowUntestedHarness: trueLoad on a DSH outside the supported range. Not recommended.

Development

The plugin has zero npm runtime or build dependencies. The host code is plain ESM, and a dependency-free builder writes the browser bundle in DSH's ModuleLoader format, using the shell's own React.

FileRole
index.mjsHost entry: version gate, Config, llm/stream scope, the two wrappers.
cap.mjsPure limit resolution, target math and image-count selection.
client/index.mjsSource of the plugin page.
lib/client.jsGenerated by npm run build; never edit it by hand.
icon.svgPlugin icon shown in the Plugins view.
scripts/check-release.mjsRelease gate (metadata, stale bundle, private data, packed files).
scripts/e2e-gui.mjsHeadless-Chrome check of the plugin page in a running DSH Web.
npm run build       # generate lib/client.js from client/index.mjs
npm test            # unit tests: limits, image selection, wrappers, version gate, page
DSH_TEST_HARNESS_ENTRY="/absolute/path/to/dsh/lib/bin.js" npm run test:integration
npm run verify      # build + tests + release gate
npm run pack:check  # inspect the exact published file set

To run the integration suite against another DSH version, install that version in a scratch folder and point DSH_TEST_HARNESS_ENTRY at it:

npm install --prefix /tmp/dsh-021rc2 @deepseek-ai/dsh@0.2.1-rc.2 --ignore-scripts
DSH_TEST_HARNESS_ENTRY=/tmp/dsh-021rc2/node_modules/@deepseek-ai/dsh/lib/bin.js npm run test:integration

The integration suite runs the plugin on that DSH's own Cordis, real LocalAttachmentStore, real LLM runtime and real pi-ai adapter. It sends real requests to a loopback mock server (fake credentials, no paid API calls) and checks the HTTP body:

  • the selected model received capped images, and another model on the same provider the originals;
  • a mock that, like Workers AI, rejects more than 32 distinct images fails on 33 without a limit and succeeds with Max images 32, with the oldest image replaced by DSH's placeholder;
  • limits saved live (0.1.x and 0.2 forms) apply to the next request.

The UI tests use a fake React harness. Version 0.2.0 was also checked in the real DSH Web app on 0.2.0-rc.1, 0.2.0-rc.2 and 0.2.1-rc.2, by installing the packed tarball into an isolated DSH_HOME with mock providers (see LOCAL-SETUP.md). On each version:

  • the page opened from Plugins;
  • 0.1.x rules showed converted;
  • per-model limits saved and persisted across a reload and a server restart;
  • the browser console showed no errors from the plugin.

On 0.2.1-rc.2 it also passed a remove/reinstall.

License

MIT. Maintainer release steps are in RELEASE.md.

Related plugins