Bundle
dsh-plugin-modality-fallback
DeepSeek Harness (dsh) plugin: route one request to a modality-capable fallback model instead of forcing the whole session onto a single model.
- Source
- lilei0311
- License
- MIT
- Updated
- Updated 20 days ago
Readme
# dsh-plugin-modality-fallback
English | [中文](README.zh.md)
A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh`) plugin. Route **one request** to a modality-capable fallback model instead of forcing the whole session onto a single model.
## The problem
A `dsh` session (Agent) selects one `provider`/`model` for its entire lifetime. That single model may not accept every modality that shows up in the session's history — today that means images. Without this plugin:
- The built-in `read_image` tool refuses outright when the session's model does not declare `image` input: *"switch to an image-capable model to read images."*
- `ApiProxy` refuses to send a message, or to switch models, when the session's history already contains an image the target model can't accept.
Every one of these paths tells the user to manually switch the **whole session** to an image-capable model and back — losing the differentiated model choice they made for everything else in that conversation.
## What this plugin does
It wraps `dsh`'s `agent/request` waterfall (the extension point `dsh-agent-default-model`'s own README documents as deferred: *"per-session selection remains the entry point's responsibility"*). Before a request goes out:
1. It reads the session's derived message history (`agent.session.deriveMessages()`).
2. If that history needs a modality beyond plain text (currently: `image`) and the model resolved by every other listener does not declare that modality (`llm.resolveModelInfo(...).inputModalities`), it looks up a configured fallback route for that modality.
3. If one is configured, it swaps `provider`/`model` for **that request only**. The session's own selection is untouched — the next request (once the image scrolls out of context, or the user switches models) resolves normally again.
No core `deepseek-harness` code is modified. This is an ordinary Cordis plugin, loaded alongside the rest of your `dsh` composition.
## Install
This package declares a `dsh.bundle` manifest, so `dsh plugin` installs and wires it into a profile in one step:
```sh
dsh plugin --profile web add dsh-plugin-modality-fallback
# or straight from GitHub, no npm publish needed:
dsh plugin --profile web add github:lilei0311/dsh-plugin-modality-fallback
```
That appends this package to the profile's `dsh.profile.bundles` and applies [`cordis.patch.yml`](cordis.patch.yml), which inserts the plugin row with an empty `fallback: {}` (no routes configured yet — every request behaves exactly as before). Configure a real route by restating that row's `config` in your own profile's or `$DSH_HOME`'s `cordis.patch.yml` (a patch replaces the whole `config`, so restate the id too):
```yaml
- id: modality-fallback
config:
fallback:
image: { provider: deepseek-official, model: deepseek-vision }
```
`dsh --profile web --dump-config` shows the composed row so you can confirm it landed. See [deepseek-harness's plugin-install tutorial](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/user/develop/basic/publish.md) for the full bundle/profile mechanics this relies on.
### Programmatic use (embedding `dsh` yourself)
```ts
import ModalityFallback from 'dsh-plugin-modality-fallback'
await ctx.plugin(ModalityFallback, {
fallback: {
image: { provider: 'deepseek-official', model: 'deepseek-vision' },
},
})
```
Load it after your `llm` and `agent`/`agent-loop` plugins so `ctx.llm` and the `agent/request` waterfall already exist.
## Known limitations
- **Only `image` is detected today.** The modality vocabulary (`ModelModality`) is open-ended, but this plugin's content check only walks for image blocks. Extending it to another modality means adding a content predicate, not changing the routing mechanism.
- **At most one missing modality is resolved per request.** If a future modality check finds more than one unmet modality at once, only the first is routed; the rest fall through unchanged.
- **`read_image` and `ApiProxy`'s own gates are unaffected.** Those refuse *before* a request is ever built, based only on the session's currently selected model, so they refuse even when this plugin has a working fallback configured for the very modality they're gating. Fixing that requires a change in `deepseek-harness` core itself (those gates would need to consult this plugin, or an equivalent capability, before refusing) — out of scope for a plugin that doesn't touch core.
- **Unknown model capability is treated as capable.** When `resolveModelInfo(...).inputModalities` is `undefined` (capability unknown), the plugin does not redirect — matching `ApiProxy`'s existing send/switch-model gates, not the stricter `read_image` gate (which refuses on unknown capability). A deployment that wants redirection on unknown capability too should have its adapter declare `inputModalities` explicitly.
- **A route switch drops the inherited reasoning effort** rather than forwarding one the fallback model may not support; the fallback route's own adapter/provider default applies instead.
- **A capability-probe failure fails open.** `resolveModelInfo` is adapter-owned I/O and can reject (network, an adapter returning invalid metadata, etc.). This plugin only ever *helps* route around a missing modality, so a probe failure logs a warning (`ctx.logger.warn`) and leaves the route unchanged rather than failing the whole request — even one whose already-resolved model didn't need a fallback in the first place. The probe itself is also skipped entirely when none of the request's needed modalities have a configured route (the default install, `fallback: {}`, never calls it at all), so this failure mode only matters once you've actually configured a route.
## Why a plugin, not a `deepseek-harness` PR
`deepseek-harness` is still at an early developer-preview stage and its `CONTRIBUTING.md` states the project does not accept external pull requests yet. Its own guidance for this situation is to build a plugin and share it — this repository does that, tagged with the `dsh-plugin` GitHub topic for discoverability.
## License
MIT
Install
dsh plugin --profile web add github:lilei0311/dsh-plugin-modality-fallback
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-plugin-modality-fallback from the hub
- This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.