模型接力 · dsh-model-failover
把 已经接入 dsh 的全部模型排成一条优先级队列,然后把这个插件本身当成一个「模型」。 用户在模型选择里挑 模型接力,任务就会按你排好的顺序去跑:靠前的先上,不行就自动往下换, 直到任务跑完。
它解决什么
一个模型不能输出,往往是这几种:额度用尽、余额不足、key 失效、路由挂了、上游 5xx。 以前遇到这些,任务当场就断了。现在:
- 先用第一个模型跑;
- 它出问题 → 用同一个模型补一句「继续」,再试一次;
- 还是不行 → 换第二个模型,补一句「继续」,接着跑;
- 依次往下,直到某个模型把任务跑完;
- 某个模型今天出过这种问题 → 今天所有任务都不再从它开始,直接用排在它后面、 已经验证过能用的那个;
- 到了第二天设定时间(默认 01:00,可改),隔离自动清空,重新从第一个开始试。
装好之后长什么样
模型选择器里多一项 模型接力(provider model-failover,模型 auto)。
选了它,你发任何消息都由接力逻辑接管。
设置 → 模型接力(在「模型」那一节下面):
| 区块 | 作用 |
|---|---|
| 模型顺序 | 全部已接入模型的候选清单,每行一个思考强度下拉框,↑ ↓ 排序,✕ 移除;下面可以从目录里加回来 |
| 今日状态 | 今天是哪天、下一个重置时间、当前被隔离的模型(带错误码和原因) |
| 行为开关 | 总开关、是否「先重试同一个模型」、补发的那句话的内容、重置时间、全部隔离时要不要硬试、是否记事件日志及条数上限 |
| 事件日志 | 每次隔离/复活/回滚都留一条,方便回头看待到底发生了什么 |
隔离中的模型会带一个红色徽标(鼠标停上去能看到错误码),今天验证过能用的带绿色徽标。
每个模型的思考强度
顺序表里每一行都有一个档位下拉框,档位就是那个模型自己在模型设置页里的那几档
(插件向宿主问 resolveModelInfo 拿到的 reasoning.efforts,不是插件自己编的枚举)。
默认停在「默认档位」,也就是你平时直接用这个模型时的表现。
- 这个模型不支持思考强度 → 那一行显示「无档位」,不画控件;
- 还没问到档位表(刚装好、刚扫过目录)→ 显示「档位未知」的禁用控件,扫到之后自动可点, 点「重新扫描」可以立刻催一次;
- 换模型接力时,用的是那个模型自己的档位,不会被上一个模型的档位带过去;
- 某个模型收到一个它不认识的档位会以
UNSUPPORTED_REASONING_EFFORT拒掉整个请求, 那等于把一个好好的模型判成「今天不行」—— 所以插件只发目标模型自己声明过的档位, 认不出来就干脆不发,让它用自己的默认档位。
顺带一提:在模型选择器里给「模型接力」这条路由本身选档位也是可以的,插件对外声明的是 队列里所有已知档位的并集,轮到一个候选时再按它自己的表过滤一遍(用户给某个模型 单独设过的档位优先于路由档位)。
几个刻意的设计
只在自己还没「说话」之前换模型。 模型的输出会先攒在手里:一旦有任何一个字、或一次工具调用准备交给用户,就立刻原样放出, 并且从这一刻起这个模型就锁定这次任务了。它之后再崩,报错就照实报错, 不会偷偷换一个模型接着写。理由很直接:两个模型各写半句话拼在一起, 用户读到的是一个人的口吻加上另一个人的结论,出了错也不知道该找谁。 攒着的那部分没交出去之前就失败,就整段丢掉换下一个模型 —— 用户从头到尾只看到一个模型的输出。
「继续」是补在原消息后面的,不是替换。所以下一个模型看到的是完整的用户请求 + 一句「继续」, 不会丢上下文。补发次数有上限(3 次),不会无限啰嗦。
中止和上下文超长不算「模型坏了」。 用户自己按了停止 → 原样交回;上下文超长 → 交给 agent loop 去压缩,都不切换、不隔离。 只有额度/凭据/网络/上游这一类「这个模型现在就是不行」的错才触发接力。
隔离是按天记的,写在磁盘上,跨任务、跨重启都有效,到点自动清空。 判定「今天它不行」要等这个模型的「继续」也试完:失败一次、被一句「继续」叫起来接着写完的 模型是能用的,会被记成「已验证可用」而不是被隔离 —— 否则今天反而会绕开一个明明好好的模型。
所有状态都在你自己的机器上:C:\Users\superv\.dsh\dsh-model-failover\state.json(隔离与验证记录)、
settings.json(队列与开关)、log.json(事件日志)。删掉 state.json 就等于「立刻解除所有隔离」。
配置
设置页里能改的都存在 settings.json;重置时间支持 0-23 的整点。默认值:
resetHour: 1、retrySameModelOnce: true、continueText: "继续"、retryWhenAllQuarantined: true、
keepEventLog: true、eventLogLimit: 200。
开发
node test/smoke.mjs # 宿主半身:接力、隔离、复活、档位、契约(20 项)
node test/client.mjs # 浏览器半身:真渲染设置页 + 真发请求(19 项)
node test/real-host.mjs # 挂进真的 dsh-llm 注册表跑一遍,含档位校验(9 项)
test/client.mjs 里有一个几十行的 React 替身,它把 client.js 当真实 bundle 加载
(走 window.__ModuleLoader__.load)、真的把设置页渲染出来、真的按按钮,
所以「页面会不会崩、按钮发的请求对不对」是断言出来的,不是看出来的。
改完 client.js 只要 node test/client.mjs,不用开浏览器。
test/real-host.mjs 用的是真宿主:真的 @deepseek-ai/dsh-llm 注册表、真的 waterfall、
真的校验函数,只把外面两个模型换成脚本化的替身。它证明的不只是「逻辑对」,还包括
「这个适配器装得进去」—— 适配器返回值形状写错时 dsh 会抛 INVALID_ADAPTER /
INVALID_CATALOG / INVALID_MODEL_INFO / INVALID_MODEL_CONTEXT,插件会装不上而不是跑错。
档位那部分同样交给真校验器判:队列的并集要过得了 normalizeModelInfo(档位没有 name
会被拒成 INVALID_MODEL_REASONING),不认识的档位要抛 UNSUPPORTED_REASONING_EFFORT。
它需要本机 _dsh-research/ 下那份从 app.asar 解出来的 dsh 包;找不到就整份跳过(退出码 0),
换位置用 DSH_LLM_HOST_DIR 指向包含 @deepseek-ai/ 的那个 node_modules。
边界
- 只在没有可见输出之前切换模型(见上)。要跨过这条线,得等 dsh 给插件一个 能安全丢弃已发出内容的位置。
- 候选清单来自宿主里已注册的模型;如果你想排除某个 provider,在设置页里把它移除、
或填进
hiddenProviders。 - 插件自己的路由永远不会出现在候选里(否则会自我递归)。
- 档位表是启动后异步问出来的(问一次记一次,不再重复问)。万一某次任务来得比 扫描还早,那一个模型这一轮会按「不知道它认什么档位」处理 —— 不发档位,用它自己的 默认档位。这是刻意的取舍:宁可按默认档位跑,也不能发一个它不认的档位把它误判成 「今天不行」而隔离掉。设置页里点一次「重新扫描」就会补齐。
No comments yet. Be the first to write one.