Перейти к основному содержимому
B

DSH-model-router

bill277048-hash/dsh-model-router

Rule-based multi-provider model routing for DeepSeek Harness with pre-first-token failover, cooldown circuit-breaking, usage accounting, and a status API.

Установка

dsh plugin --profile web add github:bill277048-hash/dsh-model-router

README

@botton/dsh-model-router

DSH 多供应商模型路由插件(v1.0.2):规则路由 + 首 token 前无感故障切换 + cooldown 熔断 + 用量记账 + 每日 API 报告(结构化 + Markdown) + 模型全自动测试(独立 phase + 退避 + 短路) + 模型档案窗口限额(5h / 1周 / 自定义) + 面板信息分级 + Windows 适配 + 状态接口 + WebUI 面板 + 诊断页签(按 seqId 分组的切换明细) + 凭据失败黄条 + match 收紧密(allowLegacyMatch) + free-tier 跨请求节流 + context 窗口感知重排 + 每日报告一键启用与生成 toast

  • 兼容:dsh ≥ 0.1.1-rc.1,Node ≥ 22.19,零第三方运行时依赖
  • 许可证:Apache-2.0

阶段交付速览(v0.9.10–v1.0.2)

阶段版本链主题
v0.9.10 Av0.9.10 → v0.9.16 + v0.9.17 收口渠道档案 schema + 声明通道扩能(限额/重置/TPM/来源/备注)+ UI 编辑器
v0.9.10 A 续v0.9.18 → v0.9.19TPM 节流激活 + hop 内联声明 UI
v0.9.9 审核收口v0.9.20 → v0.9.21审核修复(1 Critical + 2 Important + I3/S1–S4):model-test 落盘解耦、writeAtomic tmp 清理、档案按 mtime 排序、hourCycle:'h23'
v0.9.9 审核续v0.9.22 → v0.9.23节流数据源修复(Metrics.snapshot 不再截断到 50)+ 真实数据驱动分析 + 档案重访阈值告警
v1.0 阶段 Bv1.0.0 → v1.0.2契约冻结与交付收口:三类契约文档 / 配置 schema 版本化 + 迁移链 / 集成测试层(13 条契约)/ 人工测试用例集(46 例)/ 数据丢失 Critical 修复/state 改部分更新)/ CI 首次全绿(3 平台 × Node 22/24)/ v1.0.2 安装包

重要审查docs/v0.9.9-审核摘要.md(30 行 TL;DR)+ v0.9.9-代码审核报告.md(完整 331 行)—— 审核了 v0.9.9 阶段的 1 Critical + 3 Important + 4 Suggestion,全部于 v0.9.20/v0.9.21 处理完毕(含等价性穷举 960 组 0 差异、import 图零环、实测推翻原判断 3 项回归审核)。

工作原理

agent 发起流式调用
  └─ llm/stream waterfall 包装层(本插件)
       ├─ 首选路由:原样 next() 透传(prepared 绑定不动,行为与无插件时完全一致)
       ├─ 首分片前收到 error finish 且错误码 ∈ failoverSignals
       │    → 失败路由进 cooldown → 取候选链下一项重发(对 agent loop 无感)
       ├─ 挂起不产分片 → TTFT 看门狗超时 → abort 并按 TIMEOUT 切换
       ├─ 候选耗尽 → 合成 error finish(保留真实错误码)交 agent/request-error
       └─ 首分片之后:绝不切换(commit-on-first-chunk,流文法硬约束)

安全边界:

  • 不注册新 adapter、不读任何 API key——只在既有 ctx.llm provider 路由间切换
  • 首选永远先尝试;cooldown 只约束重发候选
  • 路由替换只走 agent/request(会话级提议)与 llm/stream 包装层两处正规入口
  • propose: false(默认)时对会话模型选择零干预,只做故障切换

安装 / 卸载

方式一:插件市场

打开 dsh 设置 → 插件市场,搜索 dsh-model-router,点安装。

或命令行:

dsh plugin --profile web add @botton/dsh-model-router

方式二:本仓库脚本(手动,适合未上架前自测)

# macOS / Linux(bash):
# 安装(备份 patch → 拷包 → 幂等追加条目;不自动重启)
scripts/deploy.sh
# 卸载(先摘条目后删包,自动备份 patch)
scripts/undeploy.sh
# 重载生效(确认后手动执行):
#   macOS(launchd 守护):
#     launchctl kickstart -k gui/$(id -u)/com.deepseek.dsh
# Windows 10+(PowerShell 5.1+,无需 Git Bash):
# 安装(逻辑与 deploy.sh 一致:备份 patch → 拷包 → 幂等追加条目;不自动重启)
scripts\deploy.ps1
# 卸载(先摘条目后删包,自动备份 patch)
scripts\undeploy.ps1
# 重载生效:Windows 无 launchd——停止 dsh 进程后重新运行 dsh 即可

配置在 profile patch(~/.deepseek-harness/home/profiles/web/cordis.patch.yml)的 insert 条目 config: 块中,改完重载生效。

人工实测指引

状态接口(只读,仅限本机回环访问):

curl --noproxy '*' -s http://127.0.0.1:3081/api/model-router/status

返回:规则与策略快照、cooldown 状态表、最近 50 次尝试(含每次 attemptIndex/TTFT/e2eMs/outcome;失败项附错误码)、按路由聚合统计、用量记账(滚动 5h/1w 窗口)、包装层计数器(wraps/passthroughs/failovers/timeouts/forced/exhaustions/userAborts)。

v0.8.0 新增接口(回环围栏,仅本机)

每日报告reports.enabled=true 后启用;未启用时接口存在但返回 503 明确提示):

curl --noproxy '*' -s 'http://127.0.0.1:3081/api/model-router/reports'          # 最近摘要 + 今日实时快照
curl --noproxy '*' -s 'http://127.0.0.1:3081/api/model-router/reports?l1=1'    # 轻量摘要(面板概览卡 5s 轮询)
curl --noproxy '*' -s 'http://127.0.0.1:3081/api/model-router/reports?day=2026-09-06'  # 指定日报告
curl --noproxy '*' -s -X POST 'http://127.0.0.1:3081/api/model-router/reports/generate' # 立即生成昨日报告

v0.9.4 每日报告 UX 补丁(解决「不知道在哪开」「点击无提示」「不知道落盘在哪」):

  • 面板一键启用:设置 → 模型路由 → 「每日报告」页签,未启用时顶部显示「一键启用每日报告」按钮,点击即 POST /state 提交 reports.enabled=true(无需手改 patch);保存后提示需重启 dsh 让 daily 实例装配(macOS:launchctl kickstart -k gui/$(id -u)/com.deepseek.dsh
  • 生成结果 toast:点「立即生成昨日报告」后有全局浮层反馈——成功显示「已生成 · 调用 N 次 + 文件路径」,失败显示原因;「reports disabled」类错误内嵌「立即启用」按钮直达修复
  • 路径可见/status 顶层回显 reports: { enabled, reportDir, hour }(默认报告目录 ~/Documents/dsh-model-router-reports/),面板已启用态顶部直接显示

v0.9.5 报告格式变更(Markdown)

  • 磁盘只写 <day>.report.md(不再生成 JSON 副本),首行内嵌 <!-- generated by dsh-model-router vX.Y.Z @<ISO8601> -->,可直接用 IDE 或访达双击阅读
  • 结构化数据由实时聚合当日 NDJSON 得出,md 仅作「是否已生成 + 生成时刻」标记;GET /reports?day=POST /reports/generate 响应额外附 markdown 字段(仅已生成才返),面板「查看 Markdown」按钮一键渲染全文
  • 旧版残留的 .report.json 文件不再被读取,可安全删除

模型全自动测试(v0.9.5 §9;需先启用 reports.enabled=true,共享报告目录):

curl --noproxy '*' -s 'http://127.0.0.1:3081/api/model-router/model-test'                 # 运行快照 + 最近一次结果
curl --noproxy '*' -s -X POST 'http://127.0.0.1:3081/api/model-router/model-test' \
  -H 'content-type: application/json' \
  -d '{"targets":[{"provider":"minimax-cn","model":"MiniMax-M3","tier":"free"}]}'          # 启动跑批
curl --noproxy '*' -s -X DELETE 'http://127.0.0.1:3081/api/model-router/model-test'       # 中止
curl --noproxy '*' -s 'http://127.0.0.1:3081/api/model-router/model-test/list'            # 历史报告列表
  • 对指定 (provider, model) 清单串行跑完整 4 相(probe / rpm / context / quota-group), 产出 probe.ttftMsrpm.first429Rpmcontext.maxAcceptedquotaGroupverdictprimary / backup / exclude + score + quotaRisk
  • 报告落盘 <reportDir>/<runId>.model-test.{json,md} 双份;POST /model-test/manual 可补人工字段 (如上游文档标称的 manualTpm),与实测差异 >5× 时记 verdict.warnings 但不阻断
  • 多目标批量测试(v0.9.7):「+ 新建测试」表单为可增删的目标行列表——每行选供应商 + 模型, 可跨供应商任意组合;工具栏提供「+ 添加该供应商全部模型」「+ 添加全部供应商全部模型」「+ 添加目标」 「清空」等批量入口。(provider, model) 自动去重,空列表禁止提交
  • 面板「模型测试」页签可建测试、看 verdict,并用行内三按钮一键桥接进规则: 加入首选(插入当前规则 route[0])/ 加入备用(追加到 route 末尾)/ 排除(写 providerMeta.<provider>.exclude=true
  • 付费保护:tier 非 free 的 target 必须显式带 "confirmPaidBurn": true,否则 400
  • 所有请求经 singleRawPROBE_MARK 直透——不 failover、不写 cooldown/metrics/quota/daily
  • 已知限制:桥接三按钮作用于当前第一条规则rules[0])——面板暂未提供「选择哪条规则」的选择器;需作用于其它规则时,请先在「切换规则」页签调整规则顺序

配额窗口(模型档案)(v0.9.5 §11):

curl --noproxy '*' -s 'http://127.0.0.1:3081/api/model-router/quota/windows?provider=minimax-cn'
curl --noproxy '*' -s -X POST 'http://127.0.0.1:3081/api/model-router/quota/sync' \
  -H 'content-type: application/json' \
  -d '{"provider":"minimax-cn","windows":{"fiveHour":{"limit":1000000,"resetAt":"2026-09-15T10:00:00Z"}}}'
curl --noproxy '*' -s -X POST 'http://127.0.0.1:3081/api/model-router/quota/reset' \
  -H 'content-type: application/json' -d '{"provider":"minimax-cn","windowId":"five-hour"}'
  • providerMeta.<provider>.quotaWindows 声明窗口上限(fiveHour / weekly)与自定义窗口 (customWindows),wrapper 在入口前置拦截:窗口耗尽就跳过该 provider、不打上游 API
  • limit 单位 = token。成功调用按 tokens 累加窗口用量(v0.9.6 起),达到 limit主动避让; 上游返回 429/QUOTA 时自动 markBurnedOut 置满兜底;resetAt 到期自动滑窗清零
  • 动态运行态(used / 滑窗后的 resetAt)独立持久化到 ~/.deepseek-harness/home/quota-state.json (30s 周期 flush + 进程退出同步兜底),与配置态 model-router-state.json 两层分离
  • 配置入口(v0.9.7):「切换日志」→「窗口限额」栏目列出全部已注册供应商(不只已声明的), 每行一个「档案」按钮打开抽屉,可声明 5h / 1周限额、填刷新时刻、重置已用——无需先跑一次模型测试。 「模型测试」结果行内的同名「档案」按钮仍可用(但需先有测试结果)。 「切换日志」的两个折叠块(用量窗口 / 窗口限额)默认展开且带折叠箭头,可手动收起
  • 面板「模型测试」行内「档案」按钮可配置窗口并人工重置;概览卡「窗口限额」段显示 used/limit 使用率
  • 未声明 quotaWindows 的 provider 不受拦截(不会误伤),面板会提示「建议补填窗口限额」

exclude —— 排除某 provider(v0.9.6):

  • providerMeta.<provider>.excludetrue,该 provider 不再进入注册表自动展开的候选池 (same-model / same-provider / exclude-current 三策略均受影响)
  • 语义边界:不否决用户手工写进 rule.route 的条目,也不否决当前会话 seed—— 即「别再自动选它」,而非「全局禁用该 provider」
  • 面板「模型测试」页签的「排除」按钮即写此字段

providerMeta 限额声明字段(v0.9.10 / v0.9.16;方向性方案 §6-A 阶段 A 落地):

字段类型语义进路由
rpmLimit正整数每分钟请求上限✓ 节流(90% 软上限)
tpmLimit正整数每分钟 token 上限✓(待 Task 3c 提供 token 数据)
resetPolicy.windowminute | hour | day | rolling上游限额重置窗口✗(仅声明)
resetPolicy.atHH:MMhour/day 时必填
resetPolicy.rollingSec1-86400rolling 时必填
tpmLimitSourceUrlhttp(s) URLTPM 依据链接✗(仅声明)
resetPolicySourceUrlhttp(s) URL重置规则依据链接
notes≤500 字备注
  • 声明优先(OQ1):路由节流只用声明值,不读 observed。 实测仅作提示(/status 与抽屉冲突徽章)
  • route hop 内联支持:在 rules[].route[].hop 里也写 rpmLimit/tpmLimit/resetPolicy, 优先级 > providerMeta[provider].*(与既有 quotaGroup/tier 一致)
  • 拒绝非 http(s) URLjavascript: / data: / file: 一律 400(XSS 边界)
  • 面板入口:切换日志 / 模型测试 的「档案」按钮 → 抽屉「渠道限额档案」→ ① 限额声明(RPM/TPM/重置/来源/备注)② 实测(近 60s 三类 429 + 估算速率)③ 冲突徽章
  • 回传安全:抽屉把 /status 的 providerMeta 原样回传时,normalizeConfig 自动丢弃 派生字段(observed/conflicts),无污染

按需负载测试(手动触发,复用 probe 原语;默认只测 free tier,不烧付费 token):

curl --noproxy '*' -s 'http://127.0.0.1:3081/api/model-router/loadtest'                                  # 运行快照
curl --noproxy '*' -s -X POST 'http://127.0.0.1:3081/api/model-router/loadtest' -H 'content-type: application/json' -d '{"phase":"probe"}'
curl --noproxy '*' -s -X POST 'http://127.0.0.1:3081/api/model-router/loadtest' -H 'content-type: application/json' -d '{"phase":"rpm","tiers":["free"]}'
curl --noproxy '*' -s -X DELETE 'http://127.0.0.1:3081/api/model-router/loadtest'                        # 中止
  • phase:probe(存活/延迟)/ rpm(QPS 阶梯找 RPM 边界,触发限流自动等待恢复)/ context(多尺寸上下文接受度)/ quota-group(同组共享速率池联动)
  • 已在跑时重复 POST 返回 409;探针请求带 PROBE_MARK 直透——跑完 cooldown/metrics/quota/daily 零污染

场景 A:真实故障切换

把默认规则的首选改为一个故意写错的 model id,备用为真实可用路由:

route:
  - { provider: apikey-202606301659, model: wrong-model-id }   # 故意错
  - { provider: apikey-202608290333, model: glm-5.2 }          # 真实可用

重载后正常对话。预期:

  1. 对话正常完成、无感(agent 侧不报错);
  2. status 接口 metrics.recent 出现 p=apikey-202606301659 outcome=failed errorCode=* 后紧跟 p=apikey-202608290333 outcome=committed
  3. wrapper.failovers 计数 +1;冷却表出现错误路由记录。

场景 B:无感行为基线(回归)

删掉错误 model 恢复正常配置,正常对话多轮,对比 status:wrapper.failovers 不增长、对话行为与装插件前一致。

场景 C:熔断与强制重试(可选)

fallbackPolicy.failureThreshold 调成 1,重复场景 A 三次以上,观察冷却表路由进入 open 状态、cooldownSec 后转 half-open

观察要点与已知边界

  • 首分片后不切换:若首选已开始输出内容后断流,属流中途失败,走 DSH 原生 agent/request-error/retry 恢复(本插件不接手)——这是设计约束(防内容拼接错乱),不是 bug。
  • 链耗尽收敛:一次整链失败后 exhaustionWindowSec(默认 120s)内候选链收敛为单候选,防止与 dsh-llm-retry 叠加造成尝试次数乘法爆炸;status 的 router.converged 字段可见。
  • 看门狗firstTokenTimeoutMs(默认 30000ms = 30s)内首选未产任何分片 → 判 TIMEOUT 切换;用户主动取消不受影响。
  • 用量记账:按 provider 粒度记录滚动 5h/1w token 窗口,仅可视,不参与路由排除(P3 加剩余预算视图与强制排除)。
  • 收敛时间窗为全局口径:单用户本地场景够用;多会话同时失败会互相影响收敛窗(已知局限)。

配置参考

字段默认说明
proposefalsetrue 时经 agent/request 提议会话级 (provider, model)
rules[][]自上而下首个 match 生效;match 支持 provider/model/default
providerMeta{}v0.8.0 A-2:provider → {quotaGroup?, tier?, exclude?, quotaWindows?, customWindows?} 元数据(自家配置域,不跨插件读 settings);route hop 可内联覆盖 quotaGroup/tier(单条特例优先)。exclude:true(v0.9.6)= 该 provider 不再进入注册表自动展开的候选池(不否决手工 route 与当前 seed);quotaWindows {fiveHour?, weekly?}customWindows [{id, windowMs, limit?, used?, resetAt?}](v0.9.5)= 窗口上限声明,limit 单位 = token
fallbackPolicy.maxRetries2首选之后最多切换次数(首分片前);v0.8.0 A-3:未显式声明时自动调优为 max(quotaGroupCount×2, 5),显式声明(含显式 2)一律尊重
fallbackPolicy.failureThreshold3连续失败进 open 的阈值
fallbackPolicy.cooldownSec60open → half-open 冷却秒数
fallbackPolicy.quotaFailureThreshold1QUOTA(workspace 配额耗尽)触发阈值;v0.6.1 起配额型错误单独阈值
fallbackPolicy.quotaCooldownSec600QUOTA 冷却秒数(10 分钟);half-open 放行试探后自动回归
fallbackPolicy.failoverSignals14 个错误码可触发切换的 failure.codeQUOTA QUOTA_EXCEEDED RATE_LIMIT TRANSPORT SERVER UNKNOWN INVALID_CREDENTIAL MISSING_CREDENTIAL EMPTY_RESPONSE TIMEOUT + v0.8.0 INVALID_REQUEST(DSH httpErrorCode(400, 非 quota/context) 归此类)+ v0.9.4 追加 context 类 CONTEXT_LENGTH CONTEXT_WINDOW TOO_MANY_TOKENS(累计上下文超窗 400)
fallbackPolicy.allCooldownFallbackforce-first候选全冷却时:force-first 强制重试首选候选 / fail 回退透传
fallbackPolicy.switchAfterFirstChunkfalse硬约束:首分片之后绝不切换(commit-on-first-chunk,防内容拼接错乱)
firstTokenTimeoutMs30000首分片看门狗毫秒(≥1000,30s)
failoverBudgetMs90000v0.6.1 一次请求内全部尝试(含看门狗)的总耗时上限;防挂起候选叠加拖死会话
exhaustionWindowSec120链耗尽后单候选收敛时间窗
modebalancedv0.7.0 模式预设:stable / balanced / fast,覆盖 watchdog/budget/cooldown/quotaCooldown;不动规则链与 maxRetries
registryRefreshSec300注册表(provider/模型目录)刷新周期秒数
probe.enabledfalsev0.4.0 健康探测开关;默认关闭避免无授权打真实 API
reports{enabled:false, hour:'01:00'}v0.8.0 G1 每日报告开关;enabled:true 后日账本记账 + 每日 hour 生成昨日报告(v0.9.5 起为 .report.md,默认目录 ~/Documents/dsh-model-router-reports/);v0.9.4 起面板「每日报告」页签可一键开启(POST /state),/status 回显 reports.enabled/reportDir/hour。可选 modelTestThreshold: {primary, backup}(v0.9.5,默认 {0.7, 0.4})控制模型测试 verdict 的 primary/backup 评分阈值,须满足 0<=backup<primary<=1
statusPath/api/model-router/status状态接口路径

v1.0.2 范围声明

已实现(逐版本履历见 CHANGELOG.md):

  • 规则路由rules 声明候选链,四种候选扩展策略(explicit / same-model / same-provider / exclude-current,源自 dsh 模型注册表)。v0.9.1 起为「纯选择驱动」——规则 = 命名「规则包」,在对话框模型选择器选中即完整接管;不选任何包 = 透传直连、插件不介入。rules 支持面板热更新(store JSON 持久化,免改 patch 免重启)
  • 无感故障切换:finish 分片驱动 + commit-on-substantive(v0.6.0 修订)+ TTFT 看门狗 + cooldown 熔断 + 候选耗尽合成 error finish(不 throw)
  • 用量记账与限额:滚动 5h/1w 窗口;providerMeta 声明窗口限额(5h / 1周 / 自定义)与重置规则;free-tier 跨请求节流(RPM 与 TPM 各自独立判定,v0.9.18/0.9.22)
  • 每日 API 报告:日账本 NDJSON + 稳定性评级 S/A/B/C/D/N/A + 结构化 / Markdown 双视图;面板一键启用与生成
  • 模型全自动测试与档案:独立 phase + 退避 + 短路;三类档案(model-test / loadtest / probe)统一列表与详情
  • 峰谷段场景timeWindowstimeZone 分段,peak / valley 各自候选链(段判定与显示统一 hourCycle:'h23'
  • WebUI 面板8 页签(概览 / 切换规则 / 切换日志 / 诊断 / 可切换模型 / 每日报告 / 模型测试 / 测试档案)+ 信息分级 + 轮询分级(概览 5s 轮 ?l1=1
  • Windows 适配deploy.ps1 / undeploy.ps1(PS5.1 兼容、幂等)
  • 安全边界:回环围栏(含伪造 Host 拒绝)、runId 格式白名单 + 路径前缀双校验、notes 长度上限;/status 不含任何凭据材料
  • 配置持久化state.json 版本化 + 迁移链(v1.0 起);/state部分更新(只覆盖请求体出现的键,v1.0.1 修复了原「整体替换导致静默擦除」缺陷)
  • 测试291 项全过(单测 278 + 契约 13),CI 跨 3 平台 × Node 22/24

未实现(后续分期):配额剩余预算参与路由排序/排除(P3)、SQLite 聚合与评分卡(P4)、多 key 池轮换(P5)、浏览器看板(P4 可选)、前端模块化拆分client.js 仍为单文件,v1.0 验收项 #6)、升级/回滚脚本(#3)、多 provider 段场景实机验证(#5)。

许可证

Apache-2.0

Похожие плагины