Install
Inside DeepSeek Harness, with dsh-market
dsh plugin --profile web add dshmarket
Or from the command line
dsh plugin --profile web add @snowamberx/dsh-role-router
Installing runs third-party code with your own permissions — it can read your files, use your credentials and reach the network. Review the source first, and pin a commit (github:owner/repo#sha) when you can.
README
English | 中文
Role Router Plugin (dsh-role-router)
Tired of manually switching models between planning and execution?
dsh-role-router does it for you: type /plan and requests are routed
automatically to the configured planner model; leaving plan mode switches
back to the default model — no manual intervention needed.
- Role routing:
default,plannerandsubagentare configured independently; unconfigured roles pass through (follow the session default).planneris triggered automatically by plan mode (/planand friends);defaultalways tracks the official session model selection. - Web UI: a "Multi-role model routing" configuration page under Plugins with
three model pickers (each with an optional reasoning-effort picker; same
source as
/model— the host's live, provider-grouped catalog, auto-refreshed), plus a composer-adjacent pill that shows the current selection at a glance. - Two configuration layers: cordis.yml (composition) and the
role-routersettings namespace (user layer, which takes precedence); saved settings apply to the next request without a restart.
Screenshots


Routing semantics
Every model request is routed by role; the listeners are registered on the root context, so they observe top-level agents and every in-process subagent:
| Role | Requests | Model source |
|---|---|---|
default |
top-level agents outside plan mode | Configured → forced to that model; unset → pass-through, the official layered selection applies |
planner |
top-level agents while plan mode is active | Configured → forced to that model; unset → pass-through, the official layered selection applies |
subagent |
every in-process subagent request (any depth) | Configured → forced to that model; unset → pass-through, the official layered selection applies |
An unset role passes the request through untouched: the harness's official
per-session model-selection layer decides, with its usual precedence —
explicit in-session switches (composer / /model) > the session's own latest
logged request > the global default (agent-default-model settings). So
in-session model switches take effect for unset roles on the next turn (the
official layer snapshots the selection at prompt assembly, so a mid-turn
switch never splits the running turn), and the composer summary always
agrees with the actual requests.
Switching models drops an inherited adapter-owned reasoningEffort unless
the role configures an explicit one (the routed model may not support the
previous model's effort; prepareCall rejects unsupported explicit efforts);
an explicit effort is applied and validated by prepareCall. Pass-through
requests keep everything the official layer assembled, including its effort.
Plan mode is folded locally from the durable plan/mode session events;
ctx.planMode is consulted first when visible (pending-aware).
Auxiliary model calls (compaction, session-title) do not dispatch through
agent/request and are unaffected, as are out-of-process subagent providers
(acp, codex, …).
Web UI (client half)
The package declares dsh.client (platform: web) and provides two surfaces:
- Plugins → Official → "Multi-role model routing": three
model pickers (default / planner / subagent) fed by the host's live model
catalog (provider-grouped, same source as
/model, refreshed onllm/adapters-updated); after picking a model each field offers an optional reasoning-effort picker whose levels come from that model'sreasoning.effortsin the catalog (adapter-declared, not hard-coded).- All three fields (default / planner / subagent) write the
role-routersettings namespace; a saved setting applies to the next request without a restart. A configured role forces its model; an unset role follows the official model selector. The section is registered with the composition entry as its base layer, so composition-configured routes are shown and can be overridden from the card.
- All three fields (default / planner / subagent) write the
- Composer-adjacent summary: a pill showing
Default model: <configured default or session selection> · planner: <configured>. The official model seat and/modelstay untouched.
Configuration
On install, the bundle inserts the model-router row with no config — all
three roles start unset: requests pass through and the official layered
selection applies. Two ways to personalize, settings first:
Plugins page (user layer, recommended)
Plugins → Official → "Multi-role model routing": saving
writes the role-router namespace into settings.yaml
({ default?, planner?, subagent? }, each role being
{ provider, model, reasoningEffort? }), applying to the next request
without a restart. The Plugins page does not write cordis.patch.yml,
and its values win over the composition layer.
cordis.patch.yml (composition layer, optional)
The bundle already inserts the plugin row under the id model-router; the
user layer only needs to override its config by id. In a profile, write
the profile's cordis.patch.yml:
- id: model-router
name: '@snowamberx/dsh-role-router'
config:
default: # optional; omit the key to keep it unset (pass-through)
provider: deepseek-official
model: deepseek-v4-flash
reasoningEffort: high # optional; unset follows the target model default
planner: # optional
provider: deepseek-official
model: deepseek-v4-pro
reasoningEffort: max # optional
subagent: # optional
provider: deepseek-official
model: deepseek-v4-flash
Unknown keys and blank provider/model/reasoningEffort values fail loud at load. All three roles are optional: an unset role passes requests through to the official layered selection; a configured role forces its model.
settings (user layer)
role-router namespace: { default?, planner?, subagent? }, each role being
{ provider, model, reasoningEffort? }. Settings-document values win over the
composition layer.
Install
dsh plugin --profile web add @snowamberx/dsh-role-router
# local development:
dsh plugin --profile web add link:/path/to/this/repo
Restart dsh web (client-modules rescans package metadata at boot).
A standard DSH community plugin package
This package is a standard DSH community bundle: its manifest declares a
dsh.bundle configuration layer plus a dsh.client web half, matching the
official packaging & installation
guide
and the conventions of the packages/client/* client plugin packages.
dsh.bundlemanifest:"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }inpackage.json.cordis.patch.ymlis a patch layer inserting the plugin row by id (model-router), resolved by package name (@snowamberx/dsh-role-router);dsh plugin addrecognizes the declaration and appends the package to the profile'sdsh.profile.bundleslayer stack (a package withoutdsh.bundleinstalls as a plain dependency and warns).- Web client half:
"dsh": { "client": { "platform": "web", "inject": [...] } }declares the browser half, andexports["./client"]points atlib/client.js— a standard closure-factory artifact (window.__ModuleLoader__.load({ id, factory })) served by client-modules at/plugins/@snowamberx/dsh-role-router/client.js.injectlists the client half's dependency edges (informational: preflight display and HMR diffing; activation order is driven by cordis service injection). - Build:
tsc(node half + type declarations) plustsdown(vendor/tsdown.client.ts, the same clientBundle preset as the officialpackages/client/tsdown.client.ts: inlined CSS Modules, platform modules as externals, sourcemaps mapped back to repository source paths).
Development
pnpm install # install generic build and test dependencies
pnpm link:dsh # link @deepseek-ai/* packages from the adjacent checkout
pnpm build # tsc (host half + types) + tsdown (client bundle)
pnpm test # vitest (host routing integration + config/classify units)
pnpm link:dsh defaults to the adjacent ../deepseek-harness; pass a path
(pnpm link:dsh -- /path/to/deepseek-harness) or set DSH_REPO to override it.
The script only updates symlinks and refuses to replace a real directory.
Generic dependencies remain declared in this plugin and are reused through
pnpm's content-addressable store. tsconfig enables preserveSymlinks and pins
merge-extensible type outlets so declarations share one identity.
Known limitations
- The catalog is advisory (adapters may accept unlisted model ids); the pickers only list catalog models.
- The composer summary shows
default+planneronly (notsubagent). - With no current session the card pickers defer loading ("open a session to
load the model list"); the catalog is fetched through the current session's
session.modelsRPC (the groups are global). - A
planner/subagentprovider without a registered adapter fails the request with the normal NO_ADAPTER turn error (loud, no silent fallback). - Forced routes are persisted into the session's request header, and the
official "latest logged request" layer treats that as the session's current
model. To keep an unset
default(follow-official) from inheriting the planner model after plan mode ends, the plugin snapshots the official route at the plan-entry edge and restores it once at the plan-exit edge (a fixeddefaultrole wins over the restore), then resumes pure pass-through. The restore is edge-scoped: an unconfigured planner never snapshots, so in-plan user picks are not clobbered.
Comments
Comments live in GitHub Discussions. Sign in with GitHub to post or react.