Skip to main content
Back to plugins
G

dsh-shift-router

green-dalii/dsh-shift-router

Two-tier model router for DeepSeek Harness — LLM-Judge routing, multi-model fallback chains, exponential-backoff failover, and task-level orchestration (DSH adaptation of pi-shift-router)

Install

dsh plugin --profile web add github:green-dalii/dsh-shift-router

README

English · 简体中文

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 Node Tests

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

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:

    # ~/.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 panel 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.

    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 → shift-router), 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.

Commands

CommandEffect
/routerCompact status
/router status / /router statsFull status: tiers, window, transitions, cooldowns, tokens, cost telemetry
/router on / /router offEnable / disable (session-scoped)
/router verboseToggle verbose router logging
/router orchestrate auto|offOrchestration mode
/router configShow effective config + available providers/models + usage
/router config set <path> <value>Set one field (persisted), e.g. set routing.judgeTimeout 8000, set tiers.fast.models [...]
/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 resetRestore 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 (same store for both surfaces)
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 → dist/
npm test            # vitest (62 tests: routing / failover / judge parsing / orchestration / config schema)
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

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

License

MIT © 2026 green-dalii and contributors.

Related plugins