- Home
- Plugins
- Models & Providers
- DSH-model-router
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.
Install
dsh plugin --profile web add github:bill277048-hash/dsh-model-routerREADME
@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 A | v0.9.10 → v0.9.16 + v0.9.17 收口 | 渠道档案 schema + 声明通道扩能(限额/重置/TPM/来源/备注)+ UI 编辑器 |
| v0.9.10 A 续 | v0.9.18 → v0.9.19 | TPM 节流激活 + 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 阶段 B | v1.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.ttftMs、rpm.first429Rpm、context.maxAccepted、quotaGroup与verdict(primary/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 - 所有请求经
singleRaw带PROBE_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>.exclude设true,该 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.window | minute | hour | day | rolling | 上游限额重置窗口 | ✗(仅声明) |
resetPolicy.at | HH:MM | hour/day 时必填 | ✗ |
resetPolicy.rollingSec | 1-86400 | rolling 时必填 | ✗ |
tpmLimitSourceUrl | http(s) URL | TPM 依据链接 | ✗(仅声明) |
resetPolicySourceUrl | http(s) URL | 重置规则依据链接 | ✗ |
notes | ≤500 字 | 备注 | ✗ |
- 声明优先(OQ1):路由节流只用声明值,不读 observed。
实测仅作提示(
/status与抽屉冲突徽章) - route hop 内联支持:在
rules[].route[].hop里也写rpmLimit/tpmLimit/resetPolicy, 优先级 >providerMeta[provider].*(与既有quotaGroup/tier一致) - 拒绝非 http(s) URL:
javascript:/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 } # 真实可用
重载后正常对话。预期:
- 对话正常完成、无感(agent 侧不报错);
- status 接口
metrics.recent出现p=apikey-202606301659 outcome=failed errorCode=*后紧跟p=apikey-202608290333 outcome=committed; 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 加剩余预算视图与强制排除)。
- 收敛时间窗为全局口径:单用户本地场景够用;多会话同时失败会互相影响收敛窗(已知局限)。
配置参考
| 字段 | 默认 | 说明 |
|---|---|---|
propose | false | true 时经 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.maxRetries | 2 | 首选之后最多切换次数(首分片前);v0.8.0 A-3:未显式声明时自动调优为 max(quotaGroupCount×2, 5),显式声明(含显式 2)一律尊重 |
fallbackPolicy.failureThreshold | 3 | 连续失败进 open 的阈值 |
fallbackPolicy.cooldownSec | 60 | open → half-open 冷却秒数 |
fallbackPolicy.quotaFailureThreshold | 1 | QUOTA(workspace 配额耗尽)触发阈值;v0.6.1 起配额型错误单独阈值 |
fallbackPolicy.quotaCooldownSec | 600 | QUOTA 冷却秒数(10 分钟);half-open 放行试探后自动回归 |
fallbackPolicy.failoverSignals | 14 个错误码 | 可触发切换的 failure.code:QUOTA 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.allCooldownFallback | force-first | 候选全冷却时:force-first 强制重试首选候选 / fail 回退透传 |
fallbackPolicy.switchAfterFirstChunk | false | 硬约束:首分片之后绝不切换(commit-on-first-chunk,防内容拼接错乱) |
firstTokenTimeoutMs | 30000 | 首分片看门狗毫秒(≥1000,30s) |
failoverBudgetMs | 90000 | v0.6.1 一次请求内全部尝试(含看门狗)的总耗时上限;防挂起候选叠加拖死会话 |
exhaustionWindowSec | 120 | 链耗尽后单候选收敛时间窗 |
mode | balanced | v0.7.0 模式预设:stable / balanced / fast,覆盖 watchdog/budget/cooldown/quotaCooldown;不动规则链与 maxRetries |
registryRefreshSec | 300 | 注册表(provider/模型目录)刷新周期秒数 |
probe.enabled | false | v0.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)统一列表与详情 - 峰谷段场景:
timeWindows按timeZone分段,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
Related plugins
dsh-routing-suite
yjh051108/dsh-routing-suite
dsh-plugin-subscriptions
v1ki/dsh-plugin-subscriptions
dsh-commandcode-provider
mars-sea/dsh-commandcode-provider
dsh-workbuddy-connect
corrinehu/dsh-workbuddy-connect