DeepSeek Harness plugin

dsh-shift-router

Two-tier model router for DeepSeek Harness — automatic execution/judgment routing with LLM Judge, multi-model fallback chains, and exponential-backoff runtime failover. A DSH adaptation of pi-shift-ro

Jump to install

Source facts

Repository
green-dalii/dsh-shift-router
Latest update
Aug 15, 2026
Category
Models & Providers
GitHub stars
1
Format
plugin
Catalog evidence
Upstream dsh.bundle evidence
Evidence path
package.json#dsh.bundle
Checked against
0.1.0-rc.8
Upstream check date
2026-08-20

This evidence comes from the upstream catalog. This site has not installed, run, or security-reviewed the plugin.

Install

Start with a prompt that asks an agent to review the GitHub repository and source. Switch to the command if you want to install it yourself.

Copy this prompt into DSH, Codex, or another agent and ask it to review the GitHub repository and source first.

Do not install or run any commands yet. Read this plugin's GitHub repository, README, and relevant source code. Then answer the questions below clearly and directly so I can decide whether it fits my needs:

1. What is this plugin, and what problem does it solve?
2. Who is it for, and what are its typical use cases?
3. How is it used after installation? Include one minimal example.
4. What known limitations or privacy, security, compatibility, or maintenance risks does it have?
5. Give a clear recommendation: recommend, conditionally recommend, or do not recommend, with reasons.

Distinguish statements documented by the repository, inferences from source code, and unknowns. If evidence is insufficient, say so explicitly. Do not guess or simply repeat the README.

GitHub: https://github.com/green-dalii/dsh-shift-router
Plugin: dsh-shift-router
Author: green-dalii

Check the source files

Read the README and other files from this plugin directory before installing.

File explorer4 files
README.mdSource · read only
README language

English · 简体中文

<div align="center">

dsh-shift-router

A two-tier model router for DeepSeek Harness — automatic execution/judgment routing with an LLM Judge, multi-model fallback chains, exponential-backoff runtime failover, and task-level orchestration.

A DSH adaptation of pi-shift-router.

![License](LICENSE) ![Node](https://nodejs.org) ![Tests](#development)

</div>

Routine turns shouldn't cost flagship money. The turns that matter shouldn't be left to a cheap model.

Before every turn of a top-level agent, a small LLM Judge (running on your Fast-tier model chain) classifies the user's message as fast (routine) or smart (consequential). The chosen tier then drives the whole turn — thinking, tool calls, code edits — through the harness's own agent/request pipeline. The Judge only classifies; it never does the work.

🦾 [deepseek-v4-flash] → fix the failing test
🧭 judging…
🧠 [deepseek-v4-pro]   ← "design the auth flow" → upgraded instantly
⚠️ deepseek-v4-flash 429 → cooldown, retrying on glm-5.2 — retry in 1m
🦾 [glm-5.2]           ← same-tier failover

Features

  • Instant upgrades, trend-gated downgrades — one smart verdict switches to the strong tier immediately; coming back down requires a sliding-window majority (default 5 turns, ≥60%, low-confidence votes ignored).
  • Cache-aware routing — when Fast and Smart share a provider, the router raises the downgrade threshold (0.9) and holds off while the prompt cache is warm, so switching to a cheaper model never costs more than staying put.
  • Runtime failover — 429 / 5xx / quota failures put the model into exponential-backoff cooldown (1m → 4m → 16m → 1h → 6h cap; client-side limits start at 16m) and re-resolve the same tier to the next healthy model — same-turn retry, never cross-tier.
  • Task-level orchestration — complex tasks (a smart verdict) run the Smart tier as a CTO that plans, delegates implementation to Fast engineer subagents via the harness's subagent tool, reviews each result, and iterates. The hard caps are enforced by the plugin, not just prompted: each delegation counts a round, each failed worker result counts an escalation, and once a cap is hit the subagent tool is denied outright and the system prompt switches to a "wrap up now" notice.
  • Cost telemetry — per-tier token/throughput tracking and an optional USD pricing table (/router stats shows "what this session would have cost on the Smart model").
  • Zero-config startup — a no-op until you configure tiers; then routing just works. Configuration is editable live via the GUI settings panel and /router config commands (persisted, no restart).

Install

As a bundle (recommended)

git clone https://github.com/green-dalii/dsh-shift-router.git
cd dsh-shift-router
npm install && npm run build
dsh plugin --profile web add /path/to/dsh-shift-router

The bundle's cordis.patch.yml inserts the plugin into any profile that lists it. The plugin loads without any configuration (all defaults are safe); tier models come from the settings panel or the patch row.

Installing from git (dsh plugin --profile <name> add github:green-dalii/dsh-shift-router) builds dist/ automatically via the package's prepare script. pnpm ≥ 10 refuses git dependencies' prepare scripts by default — add this to the profile's pnpm-workspace.yaml and re-add if the build is skipped:

allowBuilds:
  dsh-shift-router: true

> This grants the package permission to run its build script at install time. For a fully lock-down install, use npm run build on a source checkout (below) instead.

From source (local development)

Point the profile's patch layer at the built entry:

# ~/.dsh/profiles/<name>/cordis.patch.yml
- insert:
    - id: shift-router
      name: '/absolute/path/to/dsh-shift-router/dist/index.js'
      config:
        tiers:
          fast:
            models:
              - { provider: opencode-go, model: deepseek-v4-flash, priority: 1 }
          smart:
            models:
              - { provider: opencode-go, model: deepseek-v4-pro, priority: 1 }

Hot reload

DeepSeek Harness supports hot reload through @deepseek-ai/cordis-plugin-hmr, but two things are worth knowing:

1. The official Web bundle ships the shared HMR row disabled (packages/bundle/web-app/cordis.patch.yml has - id: hmr, disabled: true, upstream TODO: "Re-enable shared HMR for Web after its reload lifecycle is tested"). Re-enable it in your profile patch — this is the documented override mechanism:

``yaml # ~/.dsh/profiles/<name>/cordis.patch.yml - id: hmr disabled: false ``

2. What hot-reloads and what doesn't (verified against the current implementation): - ✅ Configuration changes — editing this profile patch (or the home patch) re-runs the affected plugin's apply() with the new config, no restart. The plugin's own config is also hot through the settings namespace (/router config set and the GUI card apply live without HMR at all). - ❌ Module (code) changes — the HMR accepted-dependency graph currently covers the harness's own modules only; editing an external plugin's compiled files (e.g. dist/index.js) does not trigger a reload in the current release, so code changes still require a restart. This is the untested "reload lifecycle" the upstream TODO refers to, not a limitation of this plugin. - ❌ Client package metadata — the dsh.client manifest and exports["./client"] are cached in-process; adding/fixing them requires a profile restart (only dist/client.js content changes ride the client HMR rebuild chain).

In practice: configure with /router config / the settings panel (always live), switch models by editing the patch (live once HMR is on), and restart only when you change plugin code.

Configuration

Configuration lives in the shift-router settings namespace: edit it in the GUI (Settings → Plugins → Plugin configuration — the "Shift-Router" card), with /router config commands, or via the profile patch row. All fields have safe defaults.

FieldDefaultDescription
enabledtrueMaster switch
tiers.fast.models[]Fast-tier chain (provider/model + priority); also the Judge's model chain
tiers.smart.models[]Smart-tier chain
routing.modeautoauto (default): judge + routing + failover + orchestration; manual: no judge, only explicit /route-force overrides; off: fully passive for model selection (commands/telemetry still work)
routing.judgeTimeout5000Judge call timeout (ms)
routing.judgeMaxTokens4000Max output tokens for a single Judge call
routing.judgePromptCap6000Max prompt characters sent to the Judge (bounds Judge cost)
routing.window.size5Downgrade sliding window size
routing.window.threshold0.6Fast-majority ratio required to downgrade
routing.window.minConfidence0.5Ignore judge verdicts below this confidence
routing.cacheAware.enabledtrueSame-provider cache protection
routing.cacheAware.sameFamilyThreshold0.9Downgrade threshold when tiers share a provider
routing.cacheAware.idleBoundaryMs300000Idle gap before a warm cache is considered cold
orchestration.modeautoauto: complex → Smart CTO; off: plain two-tier routing
orchestration.maxRounds3Delegate→review rounds hard cap (enforced: each subagent delegation counts one round; at the cap the subagent tool is denied)
orchestration.escalationThreshold2Failed worker results before Smart must take over the phase (enforced: each isError subagent result counts)
orchestration.requireSmartModeltrueSkip orchestration if the Smart model can't be resolved
failover.baseMs60000Cooldown base delay for 5xx failures (1m)
failover.maxMs21600000Hard cap on the backoff ladder (6h)
failover.startAttempts4xx34xx (429/quota) failures start at this attempt (16m), client limits usually outlive server blips
failover.speedWindowSize5Recent tokens/sec readings kept for the /router stats average
telemetry.callLogCap1000Max per-message attribution records kept for baseline cost computation
ux.routerLogVerbosefalsePrint router decisions to the harness log
pricing[]Optional {provider, model, input, output, cacheRead?, cacheWrite?} USD-per-1M-token table for cost telemetry

> All numeric fields are range-validated by the schema (e.g. window.threshold must be in [0,1], window.size a positive integer); invalid values are rejected at load / on set, never silently accepted.

GUI configuration card

The package ships a browser-side (client) module that registers a "Shift-Router" card in the GUI settings page:

  • Where: Settings → Plugins → Plugin configuration (that page is provided by the official dsh-client-ui-settings-plugins; the card registers into the settings.plugin.item slot).
  • What: a form over every scalar leaf field (booleans, numbers, enums) plus the two tier model chains, grouped into seven sections (General / Models / Routing / Orchestration / Failover / Telemetry / Logs & UX) with sub-groups for the routing section (Judge / Decision window / Cache-aware). Scalar fields use the compact settings-row pattern — label + hint on the left, control right-aligned on the same line — so each field is one tight row instead of three stacked lines. Controls use the host-plane design tokens: toggle switches (contrast-safe in light and dark themes), a styled select for enums, unit suffixes inside numeric inputs (ms, tokens, 0–1, …), and an ordered row editor for model chains — the row order is the in-tier fallback order, so the first available model wins and the rest are its fallbacks. The provider/model dropdowns are auto-loaded from DSH's runtime model catalog (llm.models — the same catalog the DSH settings surface reads): only providers with a currently advertised model list appear, no dormant-directory noise, and nothing is hardcoded, so the card works with any deployment's configured models. A "Custom…" escape covers values outside the catalog. Staged saving, per-field reset to default, and override markers work exactly like the official cards.
  • Boundary: only pricing (the optional USD cost table) stays with /router config and the profile patch; the tier model chains are editable in the card.
  • Build: npm run build emits both the host artifact (dist/index.js) and the client bundle (dist/client.js). The client module is discovered through the dsh.client manifest by dsh-client-modules, which requires the plugin to be mounted by package name (dsh-shift-router) — a source-checkout patch (name: '/path/dist/index.js') does not serve the card.

#### Upstream limitation: the Web settings whitelist (0.1.0-rc.6)

The current Harness Web API proxy (@deepseek-ai/dsh-host-apiproxy) whitelists which settings namespaces the browser may read and write (WEB_SETTINGS_NAMESPACES). The official cards (shell, agent-loop, web-search-deepseek) are on the list; a third-party namespace is filtered out of the browser's settings.describe response even though the plugin registered it server-side. The upstream comment explicitly calls moving that decision to settings.register() (letting a plugin self-expose) "deferred work", and the list is not configurable.

So the card needs shift-router added to the whitelist (one-time, idempotent):

npm run build
dsh plugin --profile web add /path/to/dsh-shift-router
node scripts/expose-gui-settings.mjs --profile web   # adds shift-router to the whitelist
# restart the profile (client package metadata and the apiproxy are cached in-process)

scripts/expose-gui-settings.mjs patches the profile's installed dsh-host-apiproxy/lib/index.js (idempotent; re-run after upgrading/reinstalling the dependency).

Commands

| Command | Effect | |---------|--------| | /router | Compact status | | /router status / /router stats | Full status: tiers, window, transitions, cooldowns, tokens, cost telemetry | | /router on / /router off | Enable / disable (session-scoped) | | /router verbose | Toggle verbose router logging | | /router orchestrate auto\|off | Orchestration mode | | /router config | Interactive editor: numbered field list with current values + available providers + usage | | /router config get <N\|path> | Show one field's current value, e.g. get 4 or get routing.judgeTimeout | | /router config set <N\|path> <value> | Set one field (persisted), e.g. set 4 8000, set tiers.fast.models [...] (JSON values auto-parsed) | | /router config unset <N\|path> | Clear a user override — the field reverts to its composition default | | /router config diff | List the overrides the user layer currently holds | | /router config set-fast <provider/model> | Replace the Fast tier chain with one model | | /router config set-smart <provider/model> | Replace the Smart tier chain with one model | | /router config reset | Restore the composition default | | /route-force <fast\|smart\|auto\|provider/model> | Force the next turn to a tier/model (one-shot) |

How it works (DSH integration)

CapabilityDSH mechanism
Turn-start classificationagent/pre-step waterfall (step === 1, top-level agents only)
Model switchingagent/request waterfall (per-step provider/model override)
Runtime failoveragent/request-error waterfall (cooldown + {kind:'retry'} same-tier retry)
Judge LLM callsctx.llm.stream() — reuses the harness's adapters, credentials, and JSON-mode enforcement
Orchestrator instructionctx.systemPrompt.section() rendered per agent while orchestration is active
Orchestration hard capstools/pre-execute denies the subagent tool at the cap; tools/result counts failed workers; the prompt section switches to a "wrap up" notice
Config (GUI + commands)dsh-settings namespace shift-router; /router config is a numbered editor over it (settings.update / settings.mutate path ops); the GUI card is a client module binding the same namespace via settingsScope + the settings.plugin.item slot
Usage telemetry / cooldown recoverysession/event assistant/message (TokenUsage; a successful message clears the model's cooldown)
Commandsctx.commands.register()
Tier-chain prompt variables{{shift_router_fast_chain}} / {{shift_router_smart_chain}}

Subagents are never routed. Workers spawned by the subagent tool carry session.header.origin === 'subagent' and keep their pinned model; the router only drives top-level agents.

Orchestration and the DSH subagent tool

The original pi plugin delegated through pi-subagents with agent: "worker", context: "fresh", and a per-call model pin. DSH's subagent tool differs:

  • The tool takes description + prompt (and run_in_background); a worker runs in its own fresh session — the prompt is its world.
  • The worker model is pinned by deployment configuration (dsh-tool-subagent's agentOptions), not by the tool call. By default a worker inherits the parent's model.
  • Therefore the orchestrator prompt instructs the CTO to delegate with precise task contracts, review/iterate/escalate within the hard caps, and lists the Fast-tier chain the deployment should have pinned in tool-subagent.agentOptions for cost parity.

The caps are enforced by the router, not just described: every subagent tool call while an orchestration turn is active increments orchestration.rounds; every failed (isError) subagent result increments orchestration.escalations; once capHit() is true the subagent tool is denied at tools/pre-execute and the orchestrator prompt section is replaced by a "wrap up now" notice. /router status shows the live counters (round x/max, esc y/threshold).

Development

npm run build       # tsc (host → dist/) + tsc client + tsdown (client bundle → dist/client.js)
npm test            # vitest (95 tests: routing / failover / judge parsing / orchestration / config schema / client form model / whitelist patch)
npm run typecheck

End-to-end test (no credentials)

e2e/ contains a fake LLM adapter that registers the fake provider, so the whole routing pipeline can be exercised without any API key:

# after creating a scratch profile with this bundle + @deepseek-ai/dsh-headless:
dsh --profile <tmp> --patch e2e/overlay.yml "design a migration plan for our billing system"
# → ROUTER-E2E: turn ran on fake/fake-smart   (judge said smart → upgraded to the Smart tier)

The e2e also verifies the settings namespace persists (e2e/settings-probe.mjs).

Architecture

src/
├── index.ts        # plugin entry: event wiring, per-agent state, judge, orchestration section
├── config.ts       # Schemastery schema + deep-merge normalization
├── types.ts        # shared types + defaults
├── router.ts       # pure routing engine (upgrade/downgrade/window/cache-aware)
├── judge.ts        # LLM Judge via ctx.llm.stream() + reply parsing
├── failover.ts     # exponential-backoff cooldown state machine
├── tier.ts         # tier model resolution + display
├── orchestrate.ts  # orchestrator prompt + lifecycle + caps
├── stats.ts        # telemetry snapshot (tokens / throughput / cost estimate)
├── commands.ts     # /router and /route-force
└── client/         # browser half (GUI settings card)
    ├── index.tsx       # client entry: registers into the settings.plugin.item slot
    ├── controller.ts   # staged form → settings-scope writes (one per section)
    ├── form-model.ts   # pure logic: field registry / draft parsing / save plan
    ├── ShiftRouterCard.tsx  # card component (DSW design tokens)
    └── locales.ts      # zh/en dictionaries

Pure logic (router / failover / judge parsing / orchestration) is unit-tested in isolation; DSH wiring is exercised by the headless e2e.

License

[MIT](LICENSE) © 2026 green-dalii and contributors.