green-dalii/dsh-shift-router ↗★ 3
dsh-shift-router
Two-tier model router for DeepSeek Harness: an LLM Judge sends each turn to the cheap Fast chain or the Smart chain by expected cost, with multi-model fallback chains, exponential-backoff failover, cache-aware switching and task-level orchestration. A DSH adaptation of pi-shift-router. 适合需要根据成本自动分流任务并配置多模型备份的用户。
インストール
npx -p @deepseek-ai/dsh dsh plugin --profile web add dsh-shift-routerドキュメント
README 全文を読む ↗Configuration
Configuration lives in the shift-router settings namespace: edit it in the GUI (sidebar Plugins → dsh-shift-router → the shift-router row → configure), with /router config commands, or via the profile patch row. All fields have safe defaults.
Tier models are the only thing you must choose: docs/MODELS.md covers how to pick a Fast and a Smart model (the Fast chain also serves the Judge by default), what makes a good fallback, and which models can take images.
| Field | Default | Description |
|---|---|---|
enabled | true | Master switch |
tiers.fast.models | [] | Fast-tier chain (provider/model + priority); also the Judge's default source |
tiers.smart.models | [] | Smart-tier chain |
routing.mode | auto | auto (default): judge + routing + failover + orchestration; manual: no judge, only explicit /route-force overrides; off: fully passive for model selection (commands/telemetry still work) |
routing.judgeTimeout | 5000 | Judge call timeout (ms). decision mode floors it at 15000 — a decision verdict measured 1.4–6.6 s |
routing.judgeMaxTokens | 4000 | Max output tokens for a single Judge call |
routing.judgePromptCap | 6000 | Max prompt characters sent to the Judge (bounds Judge cost) |
routing.judge.mode | fast-chain | Where the verdict comes from: fast-chain (the Fast chain), custom (routing.judge.models), decision (a decision model). A models list with no mode migrates to custom; an unknown value falls back to fast-chain and is logged |
routing.judge.models | [] | Dedicated Judge chain for custom (provider/model + priority), priority ascending. Never substituted with a cheaper model — an unusable chain falls back to the Fast chain |
routing.judge.decision.baseUrl | '' | Decision-model base URL; /v1/systemone is appended. Empty = not configured |
routing.judge.decision.model | jev-latest | Model requested from a decision endpoint. The response reports the resolved id and a change is logged, which is what keeps the alias observable |
routing.judge.decision.apiKeyRef | '' | Name of the credential holding this endpoint's key, resolved through the harness credential seam on every call (a rotated key reaches the next judgement). Empty = ambient auth |
routing.economics.reworkPenalty | 3 | R — how many price-deltas a wrong downgrade costs. The turn runs Smart iff pSmart ≥ θ, where θ = 1/R: higher R → lower θ → stickier on Smart |
routing.economics.downgradeMemory | 2 | Consecutive decisive fast turns required before Smart → Fast (a hold or a smart verdict resets the streak) |
routing.economics.mode | (unset) | Named gear preset, authoritative over reworkPenalty: eco (R=2, θ=0.5) / default (R=3, θ≈0.33) / sport (R=5, θ=0.2) |
routing.window.size | 5 | Decision-memory window size |
routing.window.threshold | (unset) | Legacy raw-θ override. The pre-EV default 0.6 is inert; only a different value is honoured (and flagged ⚠ legacy in /router status) |
routing.window.minConfidence | 0.5 | Verdicts below this are treated as a hold (never a switch, never a streak entry) |
routing.cacheAware.enabled | true | Same-provider cache protection |
routing.cacheAware.sameFamilyPenalty | 1.5 | Divisor applied to θ when both tiers share a provider — a smaller bar means fewer downgrades, so the warm prompt cache survives longer |
routing.cacheAware.idleBoundaryMs | 300000 | Idle gap before a warm cache is considered cold |
routing.cacheAware.sameFamilyThreshold | (unset) | Legacy sentinel. The pre-EV default 0.9 is inert; a different value implies sameFamilyPenalty = 3.0 |
orchestration.mode | auto | auto: complex → Smart CTO; off: plain two-tier routing |
orchestration.maxRounds | 3 | Delegate→review rounds hard cap (enforced: each subagent delegation counts one round; at the cap the subagent tool is denied) |
orchestration.escalationThreshold | 2 | Consecutive worker failures that count as one escalation; a successful worker result resets the streak. At the cap Smart must take over and the subagent tool is denied (enforced) |
orchestration.maxSpendUsd | 0 | Hard budget for one orchestrated task, in USD; 0 disables it. It is part of capHit, so reaching it denies further delegation. Needs a pricing entry to be meaningful — with no pricing the spend is legitimately 0 |
orchestration.workerLedgerCap | 20 | Per-worker cost rows kept for the status report (oldest dropped). The task total is authoritative and unaffected |
orchestration.audit.enabled | true | After a run that actually delegated, audit the acceptance claim: every worker reported, a CTO summary exists, and (one small Fast-tier call) the claim is grounded in the worker results |
orchestration.audit.timeoutMs | 5000 | Auditor call budget. The free deterministic checks always run |
orchestration.audit.promptCap | 6000 | Character cap for the auditor prompt — the cost bound, and the cap that bounds how much worker evidence is kept |
failover.baseMs | 60000 | Cooldown base delay for 5xx failures (1m) |
failover.maxMs | 21600000 | Hard cap on the backoff ladder (6h) |
failover.startAttempts4xx | 3 | 4xx (429/402/quota) failures start at this attempt (16m), client limits usually outlive server blips |
telemetry.callLogCap | 1000 | Max per-message attribution records kept for baseline cost computation |
ux.routerLogVerbose | false | Print router decisions to the plugin's ctx.logger, and emit a route notice on every judged turn (including one that holds position; a release — no Judge at all — notices once per session, not once per turn). The stock DSH profiles register no log exporter, so the log lines are visible only where a deployment mounts one — the notice and /router status are the surfaces that always work |
ux.promptSectionOrder | 150 | Sort position of the orchestrator system-prompt section. DSH allocates prompt order centrally (SECTION_ORDERS) and reserves no slot for third-party sections, so this is a setting, not a constant |
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.minConfidencemust be in [0,1],window.sizea positive integer); invalid values are rejected at load / onset, never silently accepted.
Upgrading from v0.5.0: routing decisions change immediately with no config edit — the decision rule went from counting window votes to weighing expected cost, and two legacy knobs changed meaning. See SPEC.md §15 and the
[0.7.0]section of CHANGELOG.md.
GUI configuration card
The package ships a browser-side (client) module that registers a "Shift-Router" card in the GUI settings page:
- Where (DSH 0.2.0-rc.2+): the sidebar Plugins page → the
dsh-shift-routercard → theshift-routerrow → its configure control. The full form also sits on the bundle's own page, so either route reaches it. The card registers intoplugins.row.config(keyeddsh-shift-router#shift-router) andplugins.bundle.config(keyeddsh-shift-router), both declared by the officialdsh-client-ui-plugin-manager(unchanged through 0.2.1-rc.2); the settings transport isctx.configForms. - Where (up to 0.1.5-rc.x): Settings → Built-in plugins → the configurable tab, where the card occupies the keyed
settings.plugin.itemcell (key: 'shift-router') and the transport isctx.settingsScope. Each generation declares exactly one of the three slots, so the registrations for the other generation stay inert. - Supported DSH range:
engines.dsh = ">=0.2.0-rc.2 =0.2.0-rc.2 <0.3.0-rc.0". Since 0.2.1 that field alone decides compatibility (engines.dshwins andpeerDependenciesno longer gate); the unit gate runs the upstream evaluator itself, seetests/engines-dsh-compat.test.ts. - What: a form over every scalar leaf field (booleans, numbers, enums) plus the model chains (both tiers, and a dedicated Judge chain), grouped into seven sections (General / Models / Routing / Orchestration / Failover / Telemetry / Logs & UX) with sub-groups for the routing section (Judge / Judge source / Decision window / Cache-aware). A control appears only while it is relevant — choosing a Judge source reveals that source's own settings and hides the rest — and no dropdown shows a raw config token. 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 configand the profile patch; the model chains — both tiers and the Judge's — are editable in the card. - Build:
npm run buildemits both the host artifact (dist/index.js) and the client bundle (dist/client.js). The client module is discovered through thedsh.clientmanifest bydsh-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.
Legacy harnesses only: the Web settings whitelist (≤ 0.1.0-rc.x)
Harness builds up to 0.1.0-rc.x filtered a third-party settings namespace out of
the browser's settings.describe response unless it appeared in
WEB_SETTINGS_NAMESPACES — which is why scripts/expose-gui-settings.mjs exists: it
patches the profile's installed dsh-host-apiproxy (idempotent; re-run after a
dependency upgrade). From 0.1.5-rc.2, this project's baseline, that package and its
whitelist are gone and the namespace is exposed natively, so the script reports
"not needed" and exits 0. It is not part of the published package (files), so it is
only reachable from a source checkout.