dsh-image-tiler
Manifest validDeepSeek Harness plugin: shows oversized images to the model at readable resolution by tiling them before admission.
dsh-image-tiler
Upload a screenshot, a photographed document, or a long comic page and the model reads the small text in it. Without this plugin it does not: one request image carries at most 1024 vision tokens, so the harness rescales anything larger before the model sees it, and an 8000x8000 screenshot arrives as a ~1300px-wide picture with the text already gone.
This is not an official DeepSeek product and does not imply DeepSeek endorsement.
What it does
The plugin replaces the attachment store's admission step. When a prompt carries an image too large to survive the harness' own downscaling, it cuts the image into adjacent regions and submits each region as its own image, in the same message. Every region is then transmitted at its own resolution, so the model sees the source at the resolution it was uploaded at instead of at the resolution one image can hold.
Measured on a real boot (DSH 0.2.0-rc.2, desktop host, an 8000x8000 PNG of small text):
| as one image | tiled (default settings) | |
|---|---|---|
| what the model receives | 2048x2048, re-encoded | 20 regions of 2000x1600, one per image part |
| glyph height in the image | 10 px in the source, unreadable after scaling | 10 px, unchanged |
| text rows the model can resolve | 0 | 8 |
| contrast in the text band | 11.2 | 29.7 |
Small images are not touched: anything the provider would transmit as-is is admitted exactly as before, so ordinary screenshots keep their current path.
Compatibility
- DeepSeek Harness
>=0.2.0-rc.1 <0.3.0(declared inengines.dsh, and aspeerDependencieson@deepseek-ai/dsh-attachment/-attachment-local; verified against0.2.0-rc.2). The range covers the whole0.2.xline, prereleases included, so a harness patch or rc bump does not disable the plugin. - Node.js 20 or newer (the host's own requirement).
- Status: experimental. The tiling policy is verified, but the plugin replaces a core service, so treat a version bump of the harness as a reason to re-check it.
sharpis bundled as a dependency. The plugin ships no prebuilt binaries of its own; pnpm installssharp's platform package for your machine as usual.
Install
From npm (recommended)
dsh plugin --profile web add dsh-image-tiler@0.1.6
Two different versions matter here, and mixing them up is the usual mistake. Pin the plugin version
(@0.1.6): that is what keeps the install reproducible. Do not pin the harness: this package
accepts any harness 0.2.x, prereleases included, so a harness patch or rc bump does not disable it.
A harness the plugin cannot work with is refused before pnpm runs —
dsh: installation rejected: Plugin <name>@<version> is incompatible with dsh <version> — and
dsh plugin allow-version exists to override that by hand if you are testing an unreleased harness.
Desktop profile, by hand
Add the dependency and the bundle to ~/.dsh/profiles/desktop/package.json, then install and
restart the app from the tray:
{
"dependencies": { "dsh-image-tiler": "0.1.6" },
"dsh": { "profile": { "bundles": ["...", "dsh-image-tiler"] } }
}
Verify the install
Send any image larger than roughly 1500x1500 and watch the host log:
image-tiler: 8000x8000 shown as 20 tiles of up to 2000x1600 (20.4x detail, 54% covered)
The number of image attachments in the message is what changed: one upload becomes N parts.
Update / Uninstall
dsh plugin --profile web add dsh-image-tiler@0.1.6
dsh plugin --profile web remove dsh-image-tiler
Configuration
The plugin adds no settings of its own. It is the store, so the only block it reads is the one the
official attachment-local row already accepts:
- id: attachment-local
disabled: true # the plugin serves `attachments` in its place
- insert:
- id: image-tiler
name: dsh-image-tiler
Every key of the official attachment-local row still applies, unchanged: dshHome,
maxImageBytes, maxImagesPerMessage, maxMessageImageBytes, maxImagePixels,
maxImageDimension, normalizedImageMaxPixels, normalizedImageMaxDimension,
normalizedImageMaxBytes, imageCompressionConcurrency.
Two of them decide what the tiler can do:
| key | default | effect on tiling |
|---|---|---|
maxImagesPerMessage | 20 | The tiling budget, shared evenly by the images in one message. A single 8000x8000 upload is spent as 20 tiles of 2000x1600 (20.4x the detail of the single downscaled image); three uploads in one message get 6 tiles each (about 15.5x) so no image is left behind at the provider's print. Lowering it makes each piece larger and less readable, because a piece bigger than normalizedImageMaxPixels is downscaled after the crop. |
normalizedImageMaxPixels | 4194304 | The ceiling on one published image. The planner is told this number and keeps every piece under it, so no piece is downscaled after being cut. |
maxImageBytes | 20971520 | The ceiling on one stored image. A tile bigger than this would make the store refuse the whole message, so the tiler measures every tile and sends the source whole if one of them is over. In practice a piece lands far below it: PNG is lossless, so even photographic tiles stay in single-digit megabytes. |
The row also has to be reachable: if the profile already raises the global image ceiling for huge uploads, keep those values. The plugin does not change them, and a source the store refuses never reaches the tiler.
Security posture
- No telemetry, no network access, no credentials handling, no background service.
- No
prepare,install, orpostinstallscripts; the package contains JavaScript, YAML, and text. - No child processes. Nothing is spawned or executed;
sharpruns in-process through its native binding. - Images are decoded and re-encoded in memory. Tiles are written by the harness' own attachment store, to the same place the original image would have gone, and nothing is written anywhere else.
- The only files the plugin reads are the ones the store hands it.
How it works
apply(ctx, config)mountsTilingAttachmentStoreunder theattachmentsservice name, so it replaces the officialattachment-localstore while extending it. Storage, admission limits, normalization, and reads all stay the base implementation's.admitPromptContent(content)is the single call the session controller makes before it appends a user message. The override plans a tile layout for each image part, crops the source withsharp, and returns the tiles as image parts.- Because the replacement happens before admission, the tiled images become part of the stored session message itself. Replay, resume, and fork rebuild exactly what the model originally saw.
- The layout comes from
lib/geometry.js, which transcribes the provider's own image projection (patch/downsample grid, 1024-token budget) and picks the grid whose regions land closest to 1:1 with what the provider would otherwise have sent. A source the provider already transmits unchanged yields exactly one tile, which is why small images keep their old path. - The whole batch is checked against the store's own limits before admission — piece count, each piece's stored size, and the total. If it does not fit, or if any tile would be a sliver, the original image is sent instead and the reason is logged. A plugin that tiled badly must never cost the user their picture, and the store refuses a whole message over one oversized member.
Development
node test/geometry.test.mjs # tile geometry invariants, no test runner needed
node test/admission.test.mjs # prompt admission: budgets, fallbacks, part order
node test/_cap-table.mjs # layout table for each tile cap
node test/feasibility.mjs # legibility measurements on a generated fixture
test/ is not part of the published package.
Known limitations
- A tiled upload costs more vision tokens than a single image. Cutting one source into N pieces sends N images; that is the point, but a 20-tile upload is a much larger request than a 1-image upload.
- Tile size is capped by the store's normalization policy. A tile larger than
normalizedImageMaxPixels(4194304, i.e. about 2048x2048) is downscaled after the crop, which is why the planner fills the wholemaxImagesPerMessagebudget (20 by default) instead of using a few big pieces. - The piece count follows
maxImagesPerMessage, so a lower value gives worse tiles. With the budget lowered to 6, an 8000x8000 source becomes five 4579x915 strips: complete and clear, but not the shape a reader would pick. The default of20produces the squarest layout. - Several large uploads in one message share the budget. Ten images at once leave two pieces each, below the four a layout needs to divide a source rather than shave it, so they are sent at the provider's print. Send large images one or two at a time when their detail matters.
- Tiles are whole regions, not overlapping pages. A glyph on a seam is cut in half, and neither neighbouring tile shows it whole.
- Only the inline image path is tiled. Files attached as documents, and images that arrive through other tools, are not.
- The plugin was verified against
0.2.0-rc.2on Windows. Other platforms are untested.
License
MIT — see LICENSE.
Comments
Loading…