dsh-effort-router
English | 中文
Version: 0.2.16-rc.2 (DSH 0.2.1-rc.2 compatible) · Changelog · Roadmap
A DSH (DeepSeek Harness) plugin: context-aware per-step reasoning-effort routing, plus reasoning levels for hand-declared llm-pi-ai custom models.
- The drift engine (0.2.8; integer ranks since 0.2.16): the task ORIGIN CLASS is classified once from the brief the user sent (
trivial / standard / engineering / hard);routesmaps it to an integer origin rank, then evidence drifts the rank —targetRank = originRank + error channel + volume channel − frugal, clamped to the hard-anchor ceiling - Error channel (short 6-step window, decay-weighted): tool errors, identical-argument retries, event bumps (a just-completed todo, an orchestration return, a compaction checkpoint — each +1 once per step), and a carry from the previous task's unresolved failure
- Volume channel (long 32-step window): brief and tool-result tokens, gzip-ratio discounted (repetition is not information), graded by two linear ramps
- Warm-up floor: a fresh task never routes below its origin for the first steps (default 3); afterwards drift owns the level — aggressive saving, with the short error window as the feedback loop
- Wish detector: a short delegation-style brief in the cue vacuum zone (
帮我…,can you…) starts at the engineering floor — underspecified tasks get the benefit of the doubt - Frugal signals: subagents, context pressure, or a session token budget demote one rank
- Capability mapping: the target rank maps onto the model's advertised selector ids; the optional global
allowedEffortslist constrains the final id, andeffortFallbackchoosesnearest-up(compat),up, ordownwith boundary clamping - Unknown selector ids: normalized from their advertised weakest-to-strongest order and logged once, never silently treated as
high - Manual wins: a concrete level in the selector passes through verbatim; Auto hands the step to the scheduler
- Subagent inheritance (0.2.15): a child started from a parent running Auto is scheduled from its own spawn brief (origin classification, warm-up floor, frugal demotion); a child started from a parent on a concrete level inherits that level verbatim. The seed is classified by provenance — the parent's recorded decision plus the host's
parentSessionlineage — so a human pick is never mistaken for a schedule
Why an Auto mask instead of a panel
The host's native model selector already is the per-session effort control. The plugin advertises an Auto level on every reasoning-capable model (injected into reasoning.efforts, resolved to a concrete wire level by the request interceptor before validation), so there is no second selector to keep in sync: Auto schedules, a concrete level passes through.
Custom models (llm-pi-ai)
Since DSH 0.2.1 the Models page can declare reasoning levels per custom model in the UI (a switch plus one row per level), so editing the config file is no longer required. The plugin's auto-fill therefore narrows to models declared in the profile layer that the settings user layer has never touched: once a model appears in the user layer (edited in the UI) it is treated as user-owned — the UI's switch stores OFF by REMOVING the field, so re-filling it would silently undo that choice, and the fill skips it. Catalog models that already inherit levels resolve WITH levels and are skipped, so catalog capability is never shadowed; an explicit table or an explicit false is never touched. The filled table is { low, high, max }, deliberately with no off rung: an off selection followed by a tool call poisons the conversation for every later thinking-enabled request, and the router never routes to off either. Models filled on explicit openai-completions routes also gain compat.supportsReasoningEffort: true, the switch that protocol needs to actually send the effort. Set fillCustomModels: false to let the Models page own every declaration. A profile-declared model that still resolves with no levels is named once in the log rather than silently receiving no Auto entry; the plugin never rewrites that user-owned row — declare reasoningEfforts on it (or use the Models page) to enable Auto.
The per-model level → gateway wire-value mapping (e.g. high → ultra) lives in the model's reasoningEfforts table inside the llm-pi-ai config — edit it directly or ask your agent to. The official schema validates it and the next request picks it up. pi-ai serializes the table natively; no plugin-side translation layer.
Configuration (no settings UI — everything lives in the config file)
The plugin deliberately ships NO settings UI (since 0.2.7-rc.6): every user-facing option lives in the plugin configuration, stored on the profile side, so upgrading the plugin package never overwrites it. Change options by editing the config or by asking your agent in natural language ("hide the badge", "allow max").
enabled: master switch (off = requests pass through untouched)defaultLevel: level when nothing is selected (autoor a concrete level)subagentEffort: subagent default (inheritor a concrete level)allowedEfforts: optional global final selector-id allow-list (minimal/low/medium/high/xhigh/max/ultra); empty = every advertised non-off level.off/none/autoare reserved and rejectedeffortFallback: missing-target behaviour —nearest-up(compatibility default),up, ordown; directional modes clamp at the available boundaryallowMax/maxFallback: deprecated in 0.2.16.allowMax=falsemigrates to anallowedEffortsceiling up tomaxFallback; setting both old and new controls is reported as a conflictwish/poisonRecovery/frugalDemote/contextPressure/sessionTokenBudget: wish detector, poison recovery, frugal demotion and their thresholdsshowBadge: show the read-only badge left of the composer model selector (default on)trajectoryAnnotate: project each step'sprovider/model · effortonto the native trajectory ledger (default on; switch off if it conflicts with another plugin)chatAnnotate: add aprovider/model · effortcaption row under each tool call in the conversation view (default off; read once at client load, so a toggle applies on page reload)fillCustomModels: auto-fill default effort tables for hand-declared custom modelsroutes: origin class → anchor rank (minimal/low/medium/high/xhigh/max/ultra; defaults trivial/standard low, engineering high, hard max). The four anchors must be non-decreasing; an invalid table falls back as a whole with a warningshortWindow/longWindow/errorDecay: window sizes and the per-bucket decay weights of the error channel (defaults 6 / 32 /1,0.8,0.6,0.5,0.4,0.3)briefRamp/toolRamp/compressDiscount: linear volume ramps in effective tokens (defaults400,1600/48000,128000) and the gzip-ratio discount switchwarmUpSteps/trivialLiftCap: origin warm-up floor length (default 3) and the total lift a trivial origin allows (default 1)escalateOnErrors/escalateOnRepeats/maxEscalations: error-channel thresholds and its total captodoBump/subagentReturnBump/compactionBump: channel-① event bumps (+1 inside the short window, one per step): a todo completion, a subagent/workflow result landing (integration step), a compaction checkpoint landing (peak map-fidelity risk)downAfter: deprecated (0.2.8 retired hysteresis) — kept so old config files still validate; ignoredcuesStrong/cuesNormal/cuesCheap/cuesWish: custom cue dictionaries (merged on top of the built-in ones)dictStrong/dictNormal/dictCheap/dictWish: full dictionary replacement — when non-blank, this list IS the whole dictionary (the shipped list is dropped; additions AND deletions both work), while the matchingcues*additions still merge on top. Blank (default) = shipped dictionary. Read the effective lists fromGET /effort-router/dictionaries(with thebuiltinreference) before editing, then write the result back todict*; or just tell the agent "remove xx from the strong dictionary"
In-session visibility
- Composer badge (left of the model selector, gated by
showBadge): the mode and actually sent effort of the session's last step (Auto · high); the tooltip carries origin class, target tier/rank, mapping mode, and reason. Read-only — control stays in the native selector. - Trajectory hoisting (
trajectoryAnnotate, default on): each step'sprovider/model · effortis projected onto the NATIVE trajectory ledger rows (DOM projection anchored ondata-trajectory-row-key— the assembly-layer route, no host package touched and no new view added). The anchor table is folded host-side fromrequest/header+assistant/message+tool/callevents and served on the state route. Session identity comes from the conversation root'sdata-conversation-session(0.2.11): a switch blanks the old captions and repaints from the new session's table, and an unknown identity paints nothing rather than a neighbouring session's capsule. The chat flow itself stays annotation-free by default. - Chat captions (
chatAnnotate, default off): when enabled, each tool call gains aprovider/model · effortcaption row fed by the same anchor projection.
Safety and degradation
- The interceptor is one try/catch: any error passes a concrete request through untouched; the plugin's virtual
Autosentinel is stripped even on that path (fail-open without leaking Auto) - Models without reasoning metadata never receive
reasoningEffort— never anUNSUPPORTED_REASONING_EFFORT - Automatic routing never emits
off(thinking continuity on strict thinking-mode APIs). A history PROVEN poisoned — a reasoning-less tool-call turn exists AND the session's decision log recorded a manualoffstep — pinsoffwith a warning until compaction removes the offending turn. Shape alone never pins: models like kimi-K3 emit no reasoning blocks before tool calls with thinking on (measured, 2026-10-10). Disable viapoisonRecoveryif your routes are all non-strict - Session reads go through
deriveMessages()+sessionProjectionsonly; an empty sample raises an explicit warning instead of silently degrading
Authorship & maintenance
- AI model authors by version range (primary participation in each development phase, not exclusive per-commit attribution):
0.1.0–0.2.14:moonshotai-kimi-k3.0.2.15-rc.1–0.2.15-rc.3:deepseek-v4.1-flash-1017.0.2.15-rc.4–present:moonshotai-kimi-k3.1-pro(planning and review),deepseek-v4.1-flash-1017(implementation and testing).
- Producer / repository owner: KEI.NEO (@neo805) — the human publisher responsible for this repository.
- Maintenance scope: a personal hobby (vibe-coding) project, published for sharing. No maintenance or updates are promised in response to external demand.
- Contribution policy: this repository has Issues disabled and does not accept Pull Requests. Forks are welcome under the MIT license.
- Disclaimer: the software is provided "as is", without warranty of any kind (see LICENSE). Evaluate it yourself before production use.
Thanks & license
MIT. The policy core (classification / escalation / hysteresis / tier mapping / signal collection / projection / store) is adapted from @neptune810/dsh-model-router (MIT © Neptune810); the capability layer and Auto-mask pattern follow dsh-thinking-levels (MIT © drscrewdriver); the custom-model default-fill idea comes from @hytime/dsh-thinking-effort (MIT © hytime).
No comments yet. Be the first to write one.