English · 简体中文
面向 DeepSeek Harness 的双层模型路由器 —— LLM 裁判按期望成本把每一轮交给便宜的 Fast 层或 Smart 层,并配以多模型回退链、指数退避故障转移、缓存感知切换,以及「Smart 规划、Fast 执行」的任务级编排。
由 pi-shift-router 适配到 DSH 的版本。 本项目分叉自上游 v1.0.0(本仓库首个提交与上游该 tag 同日,均为 2026-08-14,因此上游若干版本 的内容在此本就存在),此后重新对齐到上游 v1.6.0 —— 逐版本状态(已对齐/已适配/刻意不移植) 见 ROADMAP.md 的 Upstream alignment 表,契约见 SPEC.md。
日常对话不该花旗舰模型的钱;真正重要的对话也不该交给便宜模型。
在每个顶层 Agent 的每一轮开始之前,一个轻量的 LLM 裁判(运行在你的 Fast 层模型链上)会把用户消息判定为 fast(日常)或 smart(重要)。被选中的层随后通过 harness 自身的 agent/request 管线驱动整轮——思考、工具调用、代码编辑。裁判只做判定,从不干活。
🦾 [deepseek-flash] → fix the failing test
🧭 judging…
🧠 [deepseek-v4-pro] ← "design the auth flow" → 立即升级
⚠️ deepseek-flash 429 → 冷却中,同层故障转移 — 1 分钟后重试
🦾 [deepseek-v4-flash] ← Fast 链中的下一个健康模型
- 期望成本路由(EV) —— 当且仅当
pSmart ≥ θ时走 Smart,其中θ = 1/reworkPenalty:这条门槛与模型价格无关,所以唯一的旋钮就是"错误降级有多痛"。升级是即时的;降回来需要downgradeMemory次连续的决定性fast判定。裁判不可用或判定不确信时一律保持原位,绝不猜测。 - 缓存感知路由 —— 当 Fast 与 Smart 共享同一 provider 时,决策门槛会除以
sameFamilyPenalty(默认 1.5),并在 prompt 缓存仍热时抑制降级,避免切到便宜模型反而更贵。 - 运行时故障转移 —— 429 / 402 / 5xx / 配额 / 用量上限 / 模型下线 等失败会把模型置入指数退避冷却(1m → 4m → 16m → 1h04m → 4h16m,上限 6h;客户端侧限流从 16m 起步),并在同一层内重新解析到下一个健康模型——同一轮内重试,绝不跨层。
- 任务级编排 —— 复杂任务会让 Smart 层担任 CTO:规划、通过 harness 的
subagent工具把实现委派给 Fast 层工程师子代理、逐个审查结果并迭代。硬上限由插件强制执行而非仅靠提示词:每次委派计一轮、连续工作代理失败计一次升级,一旦触顶subagent工具会被直接拒绝、系统提示词切换为"立即收尾"通知。 - 成本遥测 —— 按层统计 token,可选的 USD 计价表(
/router status会显示"本次会话若全程使用 Smart 模型将花费多少")。吞吐速率不在此列:DSH 原生已在消息页脚与 trajectory 面板显示tok/s,且按解码时间计算,口径更准。 - 动作可见 —— 切换档位或模型时会往对话里写一条
[shift-router] Fast → Smart · …通知(harness 没有给插件预留状态栏座位),所以插件启用后不会"静默地什么都没发生"。把ux.routerLogVerbose打开,则每一轮判定都会有一条通知,而不只是切换时。 - 零配置启动 —— 未配置分层前完全无操作;配置完成后路由立即生效。配置可通过 GUI 设置面板 和
/router config命令实时编辑(持久化,无需重启)。
本包是一个 DSH bundle:cordis.patch.yml 会把插件行插入任何声明了它的 profile。下面每条通道最后都是同一条 dsh plugin --profile <name> add …——它在 profile 目录里转发给 pnpm。
dsh plugin --profile web add dsh-shift-router安装的是预构建产物:不会在你的机器上运行任何构建脚本,因此无需任何授权。
npm pack # 或下载 release 里的 tarball
dsh plugin --profile web add ./dsh-shift-router-0.6.0.tgz同样是预构建产物;无法访问 registry 时用这条。
dsh plugin --profile web add github:green-dalii/dsh-shift-router#v0.6.0git 安装拉到的是源码而非构建产物,因此由包的 prepare 脚本构建 dist/。pnpm ≥ 10 默认拒绝 git 依赖的 prepare——若第一次 add 失败,把 pnpm 打印的确切包键复制进 profile 的 pnpm-workspace.yaml 后重新执行:
allowBuilds:
dsh-shift-router: true这等于允许该包的代码在安装时于你的机器上执行,且不在 agent 沙箱内。请锁定 tag 或 commit(
…#v0.6.0、…#<sha>),避免后续 push 悄悄改变你实际运行的内容。
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-routerdsh --profile web --dump-config | grep -A3 'id: shift-router'插件无需任何配置即可加载(所有默认值都安全);分层模型来自设置卡片(SPEC §12)或 profile 的 patch 行。后应用的层胜出,且 patch 会替换目标行的整个 config 值——覆盖本行的 patch 必须重述它需要的每一个键(SPEC §10)。
DeepSeek Harness 通过 @deepseek-ai/cordis-plugin-hmr 支持热重载,但有两点需要了解:
-
官方 Web bundle 默认禁用了共享 HMR 行(
packages/bundle/web-app/cordis.patch.yml中是- id: hmr, disabled: true,上游 TODO:"在 Web 的重载生命周期测试通过后重新启用共享 HMR")。在 profile patch 中重新启用它——这是文档化的覆盖机制:# ~/.dsh/profiles/<name>/cordis.patch.yml - id: hmr disabled: false
-
哪些能热重载、哪些不能(已对照当前实现实测):
- ✅ 配置改动 —— 编辑 profile patch(或 home patch)会以新配置重新执行受影响插件的
apply(),无需重启。插件自身配置也通过 settings 命名空间热生效(/router config set与 GUI 卡片本来就不依赖 HMR)。 - ❌ 模块(代码)改动 —— 当前 HMR 的 accepted 依赖图只覆盖 harness 自身模块;修改外部插件的编译产物(如
dist/index.js)在现行版本中不会触发重载,因此代码改动仍需重启。这正是上游 TODO 所指的未经测试的 "reload lifecycle",不是本插件的局限。 - ❌ client 包元数据 ——
dsh.clientmanifest 与exports["./client"]在进程内缓存,新增/修正后必须重启 profile;仅dist/client.js内容变化可走 client HMR 重建链。
实践建议:用
/router config/ 设置面板做配置(始终实时);改模型就编辑 patch(开启 HMR 后实时);只有改动插件代码时才需要重启。 - ✅ 配置改动 —— 编辑 profile patch(或 home patch)会以新配置重新执行受影响插件的
配置位于 shift-router settings 命名空间:可在 GUI 的 设置 → 插件 → 插件配置(「Shift-Router」卡片)中编辑、用 /router config 命令修改,或通过 profile patch 行配置。所有字段都有安全的默认值。
唯一必须由你决定的是档位模型:docs/MODELS.zh-CN.md 说明如何挑选 Fast 与 Smart 模型(Fast 链同时也是裁判链)、什么样的模型适合做回退、以及哪些模型能接收图片。
| 字段 | 默认值 | 说明 |
|---|---|---|
enabled |
true |
总开关 |
tiers.fast.models |
[] |
Fast 层模型链(provider/model + priority);同时也是裁判的模型链 |
tiers.smart.models |
[] |
Smart 层模型链 |
routing.mode |
auto |
auto(默认):裁判 + 路由 + 故障转移 + 编排;manual:无裁判,仅显式 /route-force 覆盖;off:模型选择完全被动(命令/遥测仍可用) |
routing.judgeTimeout |
5000 |
裁判调用超时(毫秒) |
routing.judgeMaxTokens |
4000 |
单次裁判调用最大输出 token |
routing.judgePromptCap |
6000 |
发送给裁判的最大 prompt 字符数(限制裁判成本) |
routing.economics.reworkPenalty |
3 |
R —— 一次错误降级的返工代价(以价差为倍数)。当且仅当 pSmart ≥ θ 时走 Smart,θ = 1/R:R 越大 θ 越小,越黏在 Smart |
routing.economics.downgradeMemory |
2 |
Smart → Fast 所需的连续决定性 fast 判定次数(hold 或 smart 判定都会打断连击) |
routing.economics.mode |
(未设置) | 命名档位预设,优先级高于 reworkPenalty:eco(R=2, θ=0.5) / default(R=3, θ≈0.33) / sport(R=5, θ=0.2) |
routing.window.size |
5 |
决策记忆窗口大小 |
routing.window.threshold |
(未设置) | 遗留原始 θ 覆盖值。EV 之前的默认值 0.6 已失效;只有不同取值才生效(并在 /router status 标记 ⚠ legacy) |
routing.window.minConfidence |
0.5 |
低于此置信度的判定按 hold 处理(不切换、不计入连击) |
routing.cacheAware.enabled |
true |
同 provider 缓存保护 |
routing.cacheAware.sameFamilyPenalty |
1.5 |
两层共享 provider 时对 θ 的除数——门槛更低意味着降级更少,热缓存存活更久 |
routing.cacheAware.idleBoundaryMs |
300000 |
热缓存被认为变冷前的空闲间隔 |
routing.cacheAware.sameFamilyThreshold |
(未设置) | 遗留哨兵。EV 之前的默认值 0.9 已失效;不同取值等价于 sameFamilyPenalty = 3.0 |
orchestration.mode |
auto |
auto:复杂任务 → Smart CTO;off:仅普通双层路由 |
orchestration.maxRounds |
3 |
委派→审查轮次硬上限(强制执行:每次 subagent 委派计一轮;触顶后拒绝 subagent 工具) |
orchestration.escalationThreshold |
2 |
连续工作代理失败计一次升级;工作代理成功会清零连击。达到上限后 Smart 必须亲自接管,且 subagent 工具被拒绝(强制执行) |
orchestration.maxSpendUsd |
0 |
单个编排任务的硬预算(USD);0 表示不启用。它是 capHit 的一部分,触顶即拒绝继续委派。需要 pricing 才有意义——未配置定价时花费合理地保持为 0 |
orchestration.workerLedgerCap |
20 |
状态报告保留的每 worker 成本行数(超出丢最旧)。任务总额是权威值、不受影响 |
orchestration.audit.enabled |
true |
在一次确实委派过的运行结束后审计验收声明:每个 worker 是否都回报、是否存在 CTO 总结,以及(一次小的 Fast 档调用)声明是否有 worker 结果支撑 |
orchestration.audit.timeoutMs |
5000 |
审计调用预算。免费的确定性检查总会执行 |
orchestration.audit.promptCap |
6000 |
审计提示词字符上限——既是成本上限,也决定保留多少 worker 证据 |
failover.baseMs |
60000 |
5xx 失败的冷却基础延迟(1 分钟) |
failover.maxMs |
21600000 |
退避阶梯硬上限(6 小时) |
failover.startAttempts4xx |
3 |
4xx(429/402/配额)失败从该尝试次数起步(16 分钟),客户端限流通常比服务端抖动更持久 |
telemetry.callLogCap |
1000 |
基线成本计算保留的最大逐条消息归属记录数 |
ux.routerLogVerbose |
false |
把路由决策打印到本插件的 ctx.logger,并在每一轮判定后写一条路由通知(包括保持原位的轮次)。DSH 自带 profile 未挂载任何日志导出器,所以日志只在额外挂载导出器的部署可见;通知与 /router status 才是始终可用的界面 |
ux.promptSectionOrder |
150 |
编排器系统提示词段落的排序位置。DSH 集中分配提示词顺序(SECTION_ORDERS),未给第三方段落预留槽位,因此这是配置项而非常量 |
pricing |
[] |
可选 {provider, model, input, output, cacheRead?, cacheWrite?} 每百万 token 的 USD 计价表,用于成本遥测 |
所有数字字段都经 schema 范围校验(如
window.minConfidence必须在 [0,1]、window.size必须是正整数);非法值在加载 /set时被拒绝,绝不静默接受。
从 v0.5.0 升级: 路由决策会立即发生变化(无需改配置)——决策规则从"数窗口票数"改为"权衡期望成本",另有两个遗留旋钮的含义变更。详见 SPEC.md §15 与 CHANGELOG.md 的
[0.6.0]段。
插件随包构建一个浏览器端(client)模块,在 GUI 的设置页注册一张 「Shift-Router」 卡片:
- 位置:设置 → 插件 → 插件配置(该页由官方
dsh-client-ui-settings-plugins提供,卡片注册进settings.plugin.item槽位)。 - 能力:以表单编辑全部标量叶子字段(开关、数字、枚举)以及两层模型链,分七个分组(通用 / 模型 / 路由 / 编排 / 故障转移 / 遥测 / 日志与体验),路由分组下再分子组(裁判 / 决策窗口 / 缓存感知)。标量字段采用紧凑的「设置行」版式——左侧标签 + 说明,右侧同行右对齐控件——每个字段只占一行,不再上下堆叠三层。控件全部使用宿主平面设计令牌:开关用拨动开关(浅色/深色主题下对比度都清晰)、枚举用带箭头的下拉、数字输入框内嵌单位后缀(
ms、tokens、0–1等)、模型链用有序行编辑器——行的顺序就是层内回退顺序:优先命中排在最前的可用模型,其余作为后备。provider/model 下拉自动载入 DSH 运行时模型目录(llm.models,与设置页模型目录同源):只列出当前有模型清单的 provider,无休眠目录噪音,且插件不硬编码任何模型,跟随任何部署的 DSH 实际配置。另有「自定义…」入口填写目录之外的取值。分段保存、单字段恢复默认与覆盖标记与官方卡片完全一致。 - 边界:仅
pricing(可选的 USD 计价表)仍由/router config或 patch 行编辑;两层模型链都可以在卡片中直接编辑。 - 构建:
npm run build会同时产出 host 产物(dist/index.js)与 client 产物(dist/client.js)。client 模块通过dsh.clientmanifest 被dsh-client-modules扫描,要求插件以包名(dsh-shift-router)挂载——源码检出式 patch(name: '/path/dist/index.js')不会提供卡片。
0.1.0-rc.x 及更早的 harness 会把第三方 settings 命名空间从浏览器的
settings.describe 响应里过滤掉,除非它出现在 WEB_SETTINGS_NAMESPACES 中——这正是
scripts/expose-gui-settings.mjs 存在的原因:它修改 profile 里已安装的
dsh-host-apiproxy(幂等;升级依赖后重跑)。从本项目的基线 0.1.5-rc.2 起,该包与白名单
均已移除,命名空间原生暴露,脚本会输出「不需要」并以 0 退出。该脚本不在发布包内
(files),只能从源码检出获得。
| 命令 | 作用 |
|---|---|
/router |
简洁状态 |
/router status / /router stats |
完整状态:档位(R → θ)、分层、决策窗口(hold 显示为 h)、上一次判定及其原因、实际运行的模型 vs 路由器意图、切换记录、冷却、token、成本遥测 |
/router on / /router off |
启用 / 停用(会话级) |
/router verbose / /router log |
详细日志开关 |
/router orchestrate auto|off |
编排模式 |
/router allow-workers [on|off] |
把本插件的 Fast 链写入 harness 的 subagent-model-selection 白名单,使 worker 可被固定到 Fast(off 只撤销授权、保留路由)。会如实回报写入内容或失败原因 |
/router eco / /router default / /router sport |
档位预设:设置 routing.economics.mode(持久化)——更省 ↔ 更黏在 Smart |
/router config |
交互式编辑器:带编号的字段列表(含当前值)+ 可用 providers + 用法 |
/router config get <N|path> |
显示单个字段当前值,如 get 4 或 get routing.judgeTimeout |
/router config set <N|path> <value> |
设置单个字段(持久化),如 set 4 8000、set tiers.fast.models [...](JSON 值自动解析) |
/router config unset <N|path> |
清除用户覆盖——字段回退到组合默认值 |
/router config diff |
列出用户层当前持有的覆盖项 |
/router config set-fast <provider/model> |
用单个模型替换 Fast 层模型链 |
/router config set-smart <provider/model> |
用单个模型替换 Smart 层模型链 |
/router config reset |
恢复组合默认值 |
/route-force <fast|smart|auto|provider/model> |
强制下一轮走某层/某模型(一次性) |
机制到机制的逐项映射是规范,只在 SPEC.md §1.1。这里有两点 值得单独说明,因为它们不在那张表里:
- 吞吐速率刻意不在此列。 DSH 原生渲染
tok/s(消息页脚、trajectory 面板),口径是解码时间; 路由器负责路由决策与花费,不负责速率展示。 - 子代理永不被路由。
subagent工具派生出的 worker 带session.header.origin === 'subagent'并保持其被钉住的模型;路由器只驱动顶层 agent 的轮次。
轮次级路由决定用哪个模型跑这一轮;任务级编排决定复杂任务怎么执行。当裁判判定 smart
且 orchestration.mode 为 auto(默认)时,路由器把这一轮交给 Smart 层担任 CTO:它制定计划、
通过 harness 的 subagent 工具把实现委派给 Fast 层 worker、逐个审查结果并迭代——最后由一次独立的
验收审计核对它的说法。fast 判定永远不会触发这些。
- 进入 —— 决策档位是 Smart、
subagent工具存在、模式为auto;编排器指令以系统提示词段落 注入(SPEC §7.2)。 - 委派 —— CTO 调用
subagent;每次调用算一轮,且在派发时计数(派发出去的委派已经花掉了 预算,这样maxRounds才是真正的上限)。 - 审查与迭代 —— worker 失败会推进失败连击;收敛协议要求每次重派都携带结构化的
## Failure report(什么失败、在哪、用哪个验收测试复测),重复同一反馈即触发接管 (SPEC §7.2.1)。 - 停止 —— 触顶后
tools/pre-execute直接拒绝subagent工具、提示词段落切换为「立即 收尾」;agent/turn-stopping释放状态。编排是单轮的:不跨轮,泄漏的运行会在下一轮开始时清扫。
| 设置 | 默认值 | 作用 |
|---|---|---|
orchestration.maxRounds |
3 | 每个任务的委派轮数 |
orchestration.escalationThreshold |
2 | 连续多少轮 worker 失败后,Smart 层自己接管该阶段 |
orchestration.maxSpendUsd |
0(不设闸) | 达到 USD 预算即停止委派,按 pricing 表计价 |
orchestration.workerLedgerCap |
20 | 保留用于展示的每 worker 成本行数 |
硬上限能防止跑飞,防不住 CTO 声称验收了却其实没核对。因此真正委派过的运行会被审计:确定性
检查总是跑(每个派发出去的 worker 都回报了、存在 CTO 总结、没有被上限截断),并在
orchestration.audit.enabled 时追加一次小的 Fast 层复核,读取目标、总结与 worker 结果。它从不
阻塞轮次:LLM 那一半是分离执行的,结论以 Last audit: 出现在 /router status。
上游把「每 worker 的模型钉定」称为强制:worker 若继承父会话的模型,编排中途那已是 Smart,
经济学前提直接崩塌。DSH 里这个钉定居于宿主持有的白名单之后(subagent-model-selection,默认
关闭)。/router allow-workers 帮你把 Fast 链写进去;未授权时路由器会在
Worker delegation: 行里如实说明,而不是假装可以(SPEC §7.4)。
/router status 会显示 🪄 active (round x/y, esc a/b, fail streak n),worker 回报后追加
Orchestration spend: $X · N/M workers reported,审计结束后还有 Last audit: …。进入编排时还会
往对话里写一条路由通知(SPEC §13.1),所以不必去读计数器也能看出这一轮的形态。
fast 判定;orchestration.mode: off(或 /router orchestrate off);组合里没有 subagent
工具;或解析不到 Smart 模型——这些情况下这一轮就只是跑在 Smart 上,没有委派、也没有审计。
npm run build # tsc(host → dist/)+ tsc client + tsdown(client bundle → dist/client.js)
npm test # vitest(18 个文件、336 个测试:EV 路由 / 故障转移签名 / 裁判解析与提示词契约 / 编排 / 配置 schema 与迁移 / 遥测 / 路由通知 / 配置注册表与 GUI 表单模型 + 卡片 UX + 模型目录 / 打包安装契约)
npm run typechecknpm run test:e2e该脚本会建一个临时 DSH_HOME,把本检出作为 bundle 装进派生出的 headless profile,用假适配器跑一轮,并断言:
ROUTER-E2E: turn ran on fake/fake-smart notice=yes—— 裁判确实跑了、EV 规则确实升级了、真的切换了上线模型到 Smart 层,并且[shift-router]路由通知确实进入了模型请求(SPEC §13.1);- 在
e2e/legacy-config-overlay.yml下(对齐前配置:遗留旋钮处于旧默认值 + 已被移除的requireSmartModel键)结果相同 —— 覆盖的是升级路径,不只是全新安装; - 在
e2e/orchestration-overlay.yml下结果相同 —— 使用插件的默认编排模式(auto),并挂载 web 专属的subagent-model-selection-settings行,即曾经导致启动失败的那个组合; shift-routersettings 命名空间能完成一次写入并读回,且 Host 模型目录确实广告出该部署配置的路由(e2e/settings-probe.mjs);- 打包产物安装(
npm pack→ tarball → 第二个 scratch profile)能带着插件启动,浏览器端被提供给 client 模块加载器,且 profile 里没有任何@deepseek-ai/*副本 —— 那里没有 devDependencies,因此"运行时导入了未向消费者声明的包"会在 e2e 失败,而不是在用户机器上失败。
它不会碰你真实的 DSH_HOME,跑完自行清理(加 --keep 可保留现场)。手工复现:
DSH_HOME=/tmp/scratch dsh plugin --profile tmp add /path/to/dsh-shift-router
DSH_HOME=/tmp/scratch dsh --profile tmp --patch e2e/overlay.yml "design a migration plan"仓库结构与模块地图(纯逻辑/接线分工、测试分层)以
CONTRIBUTING.md 的 Repository layout 一节 为唯一权威,
这里只留指针:src/ 是 host 半边(纯决策模块 + index.ts 里的 DSH 接线),src/client/ 是
浏览器半边(设置卡片),tests/ 覆盖两者,e2e/ 启动 scratch profile——包括打包产物。
- pi-shift-router —— 本插件所适配的上游项目:
同一套双层架构(LLM 裁判、回退链、指数退避故障转移、任务级编排),面向
pi-coding-agent。 行为对齐与刻意不对齐之处记录在 ALIGNMENT.md。 - dsh-plugin-dev-skill —— 开发 DSH 插件的
Agent 技能:工具(
defineTool)、LLM 适配器、服务、事件、配置与打包,含 Cordis 心智模型与验证 清单。本项目遵循它;可在 DSH、Claude Code 或 Codex 中安装。 - obsidian-llm-wiki —— 把笔记与 PDF 变成互链、 可查询知识库的 Obsidian 插件(实体页与概念页、图检索问答、本地优先、无后端)。同一作者的另一条 产品线。
MIT © 2026 green-dalii and contributors.