Skip to content
dsh.fish
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

  • 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.
Source