- Home
- Plugins
- Vision, Voice & Multimodal
- dsh-image-edge-cap
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-capREADME
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.
- Source code: github.com/mrbeandev/dsh-image-edge-cap
- npm package: npmjs.com/package/dsh-image-edge-cap
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 version | npm tag (at the time of this release) | Status |
|---|---|---|
0.2.1-rc.2 | latest, 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:
| Limit | Effect |
|---|---|
| Max edge (px) | Shrinks each image so its longest side is at most this many pixels, keeping the aspect ratio. 16–16384. |
| Max images | Keeps 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
- An
llm/streamlistener reads the request'sproviderandmodel. If no edge limit applies, it returns the stream untouched. - Otherwise it runs the adapter stream inside an
AsyncLocalStoragescope that holds the limit. The scope reaches the image reads the adapter makes while the stream is consumed, even through otherllm/streamlisteners and concurrent requests. - The plugin wraps
readImageRequeston theattachmentsservice. 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. - 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
- The plugin wraps the LLM runtime's
forAdapterstep, the last step before a request reaches the provider adapter. Plain calls, prepared agent calls, retries and compaction summaries all pass through it. - 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.
- When there are more than N, it marks the older ones
offloadedon 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
limitsform and removes its oldprovidersrule.
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):
| Key | Meaning |
|---|---|
limits | Per-provider, per-model limits edited by the plugin page. |
providers | 0.1.x per-provider rules; still honoured until the provider is saved. |
enabled: false | Load without limiting anything. |
harnessEntry | Path of the running dsh CLI entry (detected automatically). |
allowUntestedHarness: true | Load 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.
| File | Role |
|---|---|
index.mjs | Host entry: version gate, Config, llm/stream scope, the two wrappers. |
cap.mjs | Pure limit resolution, target math and image-count selection. |
client/index.mjs | Source of the plugin page. |
lib/client.js | Generated by npm run build; never edit it by hand. |
icon.svg | Plugin icon shown in the Plugins view. |
scripts/check-release.mjs | Release gate (metadata, stale bundle, private data, packed files). |
scripts/e2e-gui.mjs | Headless-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
dsh-web-ui (dsh-tool-describe-image)
zhu1090093659/dsh-web-ui
modlens
liustack/modlens
ipollowork
devin-axis/ipollowork
dsh-vision-router
ysr666/dsh-vision-router