Bundle
dsh-vision-patch
Per-model and per-route image-input (vision) checkboxes on the Models page's custom-provider cards, writing through the llm-pi-ai settings namespace.
- Source
- ryasrk
- License
- MIT
- Updated
- Updated 2 days ago
Readme
# dsh-vision-patch
A plugin with two halves on the `llm-pi-ai` settings namespace. The client
card on the Models page declares which models accept image input, per model
and per route, and picks a per-route **vision fallback model**. The host half
uses that fallback to describe images so a turn never dies on an image the
main model cannot read.
The card renders inside every provider card the Models page draws for a
`llm-pi-ai` route (saved rows, first-run setup rows, add-provider drafts) and
writes through the settings Remote. The host half adds no HTTP route of its
own; it registers a single `agent/pre-step` interceptor.
## What it does
- **Per-model checkbox** — `input: ['text', 'image']` on the model's stored
entry. Unchecked *removes* the `input` key entirely, so the model inherits
the installed catalog's capability (it never writes `['text']`, which would
forbid images outright).
- **Route-wide checkbox** — `defaultInput: ['text', 'image']` (set/unset via
path ops) for every model on the route that declares no list of its own.
- **Vision fallback model** — a per-route dropdown of the route's
vision-capable models (those that declare images themselves or inherit them
from the route). The picked model is stored as `visionFallbackModel`.
- **Model filter** — a search box appears once a route lists more than 8
models; the count line shows how many models currently declare images.
- **Live refresh** — the card re-reads after each write and follows
`settings/document-updated`, so edits made elsewhere on the page converge.
A provider with no stored profile (never saved from the Models page) shows
guidance text instead of checkboxes.
## Vision fallback (host)
A provider route in the `llm-pi-ai` settings namespace may carry a
`visionFallbackModel` field: `{ provider?: string; model: string }`. The
`provider` defaults to the route itself; `model` names a vision-capable model
on it. An absent field turns the feature off for that route. The Models card's
dropdown writes this field; the host reads the same stored profile, so the two
sides always agree.
When the host's `agent/pre-step` interceptor sees a step whose user message
contains an `image` content block AND the step's route (from
`agent.options.provider`) has a configured `visionFallbackModel`, it:
1. streams the turn to the fallback vision model via
`ctx.llm.stream({ provider, model, messages, signal })` to describe the
image(s);
2. replaces each `image` block in the user message with a `text` block holding
that description; and
3. returns the rewritten messages, so the main model continues on the
description.
It does not inspect the active main model's modality: it describes whenever a
fallback is configured and an image is present. If the describe fails, times
out, no fallback is configured, or `ctx.llm` is unavailable, the interceptor
delegates unchanged (`next()`), so the image falls through to the harness's
default handling and the turn is never blocked.
### Durability & logging
When the interceptor describes an image it rewrites the user's own message,
replacing each `image` content block with a `text` block holding the
description. That rewritten text is **not stamped with a plugin `source`
kind**. The whole host seam is validated only against mocks — there is no live
host to confirm a new, non-catalog `source` kind is accepted — and rewriting an
already-present user message (rather than appending a fresh one) risks
clobbering the harness's own `source` marker on that message. Durability of the
injected wording is therefore the harness's call; this plugin makes the
injection **observable instead**, via the host logger:
- `info` when a describe is attempted (an image is present, a fallback model is
configured, and `ctx.llm.stream` is available);
- `debug` when the describe yields no usable text and the interceptor delegates
unchanged (`next()`);
- `debug` when a description is actually injected and the turn enters rewritten;
- `debug` when a describe is skipped because one is already in flight for the
same agent (the per-agent concurrency guard).
All logging is guarded (`ctx?.logger?.info?.(...)` / `debug` / `warn?.(...)`), so
an absent logger is harmless. A silent describe is never silent in the logs: if
the user believes an image was described but no text assembled (for example the
real `ctx.llm.stream` chunk shape differs from the mock), the debug line names
the agent that fell back to `next()` unchanged.
## Install
```bash
dsh plugin --profile web add github:ryasrk/dsh-vision-patch
```
The profile's `dsh.profile.bundles` list picks the package up automatically
(its `package.json` declares `dsh.bundle.patch`). Restart the web host so the
new bundle layer composes, then reload the Models page.
## Layout
| Path | Role |
| --- | --- |
| `cordis.patch.yml` | inserts the `dsh-vision-patch` loader row |
| `lib/index.js` | host entry — `apply(ctx)` registers the `agent/pre-step` interceptor via `lib/host.js` |
| `lib/host.js` | host helpers: the `agent/pre-step` handler that describes images and rewrites the turn, plus the settings fallback reader |
| `client/client.js` | browser bundle: the card (image-input checkboxes + vision fallback dropdown), registered on the `settings.models.provider-card` slot keyed `llm-pi-ai` |
| `package.json` | `dsh.bundle.patch` + `dsh.client` (web platform, inject edges) |
The client bundle requires only `react` (a platform seed word); it needs no
build step and has no runtime dependencies.
## How it writes
All writes go through `ctx.remote.settings` against the `llm-pi-ai` namespace,
carrying the revision the last read observed (`settings-conflict` on a lost
race):
- route-wide toggle → `mutate` path ops on `['providers', <route>, 'defaultInput']`
- vision fallback → `mutate` path ops on `['providers', <route>, 'visionFallbackModel']` (`set` the `{ model }` choice, `unset` to clear)
- per-model → `update` merge restating the whole stored `models` list (path
ops cannot address array elements), every untouched field merge-preserved.
Writes that land return the namespace's post-write view, which folds straight
back into the card.
## Uninstall
```bash
dsh plugin --profile web remove dsh-vision-patch
```
(Then restart the web host so the bundle layer leaves the composition.)
Install
dsh plugin --profile web add github:ryasrk/dsh-vision-patch
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-vision-patch from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.