- Главная
- Плагины
- Инструменты и функции
- dsh-model-failover
dsh-model-failover
airisey/dsh-model-failover
Model failover router that queues all connected DSH models, retries with continue text on failure, switches to next model, and quarantines failing models for the day.
Установка
dsh plugin --profile web add github:airisey/dsh-model-failoverREADME
模型接力 · dsh-model-failover
把 已经接入 dsh 的全部模型排成一条优先级队列,然后把这个插件本身当成一个「模型」。 用户在模型选择里挑 模型接力,任务就会按你排好的顺序去跑:靠前的先上,不行就自动往下换, 直到任务跑完。
它解决什么
一个模型不能输出,往往是这几种:额度用尽、余额不足、key 失效、路由挂了、上游 5xx。 以前遇到这些,任务当场就断了。现在:
- 先用第一个模型跑;
- 它出问题 → 用同一个模型补一句「继续」,再试一次;
- 还是不行 → 换第二个模型,补一句「继续」,接着跑;
- 依次往下,直到某个模型把任务跑完;
- 某个模型今天出过这种问题 → 今天所有任务都不再从它开始,直接用排在它后面、 已经验证过能用的那个;
- 到了第二天设定时间(默认 01:00,可改),隔离自动清空,重新从第一个开始试。
另外,整轮以失败告终、或被 token 上限截断时(模型自己没坏,只是这一轮没跑完), 插件会替你自动补一句「继续」接着做 —— 见下面的「自动『继续』」。
装好之后长什么样
模型选择器里多一项 模型接力(provider model-failover,模型 auto)。
选了它,你发任何消息都由接力逻辑接管。
设置 → 模型接力(在「模型」那一节下面)。同一类设置放在一起,从上到下依次是:
| 区块 | 作用 |
|---|---|
| 今日状态 | 今天是哪天、距下次重置还有多久、当前起点、今天隔离了几个(重置时间就在这里) |
| 优先级队列 | 全部已接入模型的候选清单,每行一个思考强度下拉框,↑ ↓ 排序,✕ 移除;下面可以从目录里加回来 |
| 今日隔离名单 | 今天判断「不行」的模型,带错误码和原因;点左边的箭头摊开完整报错 |
| 接力规则 | 总开关、是否「先重试同一个模型」、补发的那句话的内容、全部隔离时要不要硬试 |
| 自动「继续」 | 总开关、两种触发、次数上限、每次失败后等多久再补(见下) |
| 事件日志 | 每次隔离/复活/回滚/自动补发都留一条;记不记、留多少条也在这里 |
隔离中的模型会带一个红色徽标(鼠标停上去能看到错误码),今天验证过能用的带绿色徽标。
隔离名单里的报错可以摊开看。 每一行默认只给摘要 —— 错误码、一句人话、报错正文的前
160 字符 —— 因为它们动辄几百字符(ENOTFOUND、tunnel_destination_denied 这类本机网络
故障尤其啰嗦),铺在列表里会把整页撑得没法看。点行首的 ▸ 摊开,能看到完整的报错原文、
路由、尝试次数和判定时间;▾ 再点一下收回。宿主落盘时就把报错截到 400 字符,点开看到的
就是留下的全文,不会二次截断 —— 点开本来就是为了看被截掉的那半句。
自动「继续」
上一节说的是「这个模型这次没能输出」。但有两种情况是整轮结束才看得见的, 模型自己没坏:
- 输出撞到 token 上限被截断 —— 它写到一半没词了,等的是你再说一句;
- 这一轮以报错告终 —— 队列里所有候选都试完了还是不行。
这两种情况下插件会替你把「继续」再发一句,接着往下做。它走的就是你自己敲回车的那条路
(agent.followup),所以你看到的是一条真的用户消息:计数、上下文、渲染都和手打的一模一样。
中途你按了停止、或是那一轮正常做完了,它都绝不会多嘴。
「继续」不会被无限重发:同一段连续失败里最多补 autoContinueLimit 次(默认 3),
用满就停手等你;你自己发下一句话、或某一轮正常做完,计数就重新开始。
上下文超长、没挂适配器、凭证失效这三类错误直接跳过 —— 重发一遍只会撞同一堵墙。
每次失败之后会先等一会儿再补。 等待时长是 continueDelaySec(默认 5 秒,0 = 不等),
第一次失败也一样等。这一等不是拖延:刚撞完限流或上游抖动的模型,隔 0.2 秒再戳一次
只会再撞一次,除了把队列挨个隔离掉没有任何收益(实测里最难看的一段是 9 秒内落了 12 条隔离)。
等待期间你按了停止、自己先发了一句、或者那一轮已经跑完,插件就收手不再补。
等待同样管着适配器里「换个候选 / 同模型重发」那一跳,所以整个接力的节奏都被这一个数字管着。
本机网络没通的时候也不会补发。 这条单独拿出来说,是因为本机网络故障在 DSH 里
没有专属错误码:DNS 解析不了、代理没起来、证书被本机拦截,各家适配器会分别报成
SERVER、TRANSPORT、UNKNOWN。所以插件不认错误码,而是认报错文本里的本机特征
(ENOTFOUND / EAI_AGAIN / ECONNREFUSED / socket hang up / fetch failed /
local name resolution failed / tunnel_destination_denied / 证书拦截等)。
网线没通的时候,补一句「继续」既救不回来、又要白烧一次上限,还会把整个接力队列
一个个隔离掉 —— 所以这时插件停下来,只在事件日志里记一条
「本轮是本机网络问题(SERVER),没有补发『继续』」,让你知道是网络的事、不是插件坏了。
反过来,远端的问题照旧会补「继续」:上游 5xx、限流、模型太慢都还是要救的。
判据特意写得很具体(upstream returned 502 Bad Gateway、request timed out 这些
都不会被误判成本机网络),宁可漏判也不误判。
自动补发会在事件日志里记成一条「本轮…,自动补发『继续』」。
只要一轮是接力跑的,那一轮的轮尾就多一行小字,写明这一轮是哪个模型跑的,比如 「本轮:Code99 · Beta One」。之所以要它:接力一换人,屏幕上就再没有别的地方告诉你 「现在是谁在答」了 —— 模型名字原本只在你发送时选中的那一个上,换人之后它就不作数了。 判定的是「这一轮有没有真的走接力」,不是「接力有没有换过人」:顺序表里排第一的模型 接下了这一轮也一样显示。你在模型设置里手动挑了具体模型、压根没走接力的轮上不会有, 免得每一条回答下面都挂一行废话。
宿主侧是两段式配对的:适配器看得见 sessionId(DSH 在 GenerateOptions 里给)但看不见轮号,
而 turn/end 反过来。所以适配器负责按会话记下「最后真正产出内容的那个模型」,
监听器在 turn/end 时补上轮号,再用一个轻量端点 GET /notes?sessionId=… 交给前端 ——
前端只为这一句小字去拉几十 KB 的 /state 并不划算。这份便签是纯显示用的,不落盘。
会话标题、上下文压缩这类内部调用与用户看到的那一轮无关,不会被记进去。
监听器判断「这一轮是不是接力跑的」靠的是对象比对:turn/start 时记下当时那份便签,
turn/end 时看它有没有被换成新的 —— 适配器每试一个候选都会写一份新的。不能只看
「便签里有没有模型」,否则上一轮接力跑过之后,你下一轮手动挑个具体模型时,
那行小字会继续拿着上一轮的结论指错人。
每个模型的思考强度
顺序表里每一行都有一个档位下拉框,档位就是那个模型自己在模型设置页里的那几档
(插件向宿主问 resolveModelInfo 拿到的 reasoning.efforts,不是插件自己编的枚举)。
默认停在「默认档位」,也就是你平时直接用这个模型时的表现。
- 这个模型不支持思考强度 → 那一行显示「无档位」,不画控件;
- 还没问到档位表(刚装好、刚扫过目录)→ 显示「档位未知」的禁用控件,扫到之后自动可点, 点「重新扫描」可以立刻催一次;
- 换模型接力时,用的是那个模型自己的档位,不会被上一个模型的档位带过去;
- 某个模型收到一个它不认识的档位会以
UNSUPPORTED_REASONING_EFFORT拒掉整个请求, 那等于把一个好好的模型判成「今天不行」—— 所以插件只发目标模型自己声明过的档位, 认不出来就干脆不发,让它用自己的默认档位。
顺带一提:在模型选择器里给「模型接力」这条路由本身选档位也是可以的,插件对外声明的是 队列里所有已知档位的并集,轮到一个候选时再按它自己的表过滤一遍(用户给某个模型 单独设过的档位优先于路由档位)。
几个刻意的设计
只在自己还没「说话」之前换模型。 模型的输出会先攒在手里:一旦有任何一个字、或一次工具调用准备交给用户,就立刻原样放出, 并且从这一刻起这个模型就锁定这次任务了。它之后再崩,报错就照实报错, 不会偷偷换一个模型接着写。理由很直接:两个模型各写半句话拼在一起, 用户读到的是一个人的口吻加上另一个人的结论,出了错也不知道该找谁。 攒着的那部分没交出去之前就失败,就整段丢掉换下一个模型 —— 用户从头到尾只看到一个模型的输出。
「继续」是补在原消息后面的,不是替换。所以下一个模型看到的是完整的用户请求 + 一句「继续」, 不会丢上下文。补发次数有上限(3 次),不会无限啰嗦。
撞到 token 上限不算「这个模型坏了」。
它明明在写,只是被截断在半句话上 —— 交出去的结局原样还是「撞上限」,不隔离、不换人,
所以下一轮补「继续」时用的还是原来那个模型。这一条看着琐碎,却是最容易写错的地方:
把「被截断」和「产不出内容」混成一个 NO_VISIBLE_OUTPUT,模型当场就被隔离掉,
接力起点从此换成队列里的下一个,「用原模型补继续」也就再也无从谈起。
但也不能在同一个模型上一直撞下去。
连着撞 2 次,下一次调用队列就往前挪一格,换队列里的下一个接手;再撞 2 次,再挪一格,
转完一圈从头接上(两个候选时是「甲 甲 乙 乙 甲 甲 …」)。账按会话累加而不是按模型 ——
按模型记的话,换到乙之后再撞一次就会「重置」,下一轮又回到甲,两个模型之间来回打转。
哪一轮正常写完了,账就一笔勾销,下一个任务还从队首开始。换人时事件日志里会留一条
ceiling-rotate,不然用户只看到排在最前面的模型莫名其妙被跳过。
「压缩会话」「起标题」这类内部调用跟用户看到的那一轮无关,不参与记账。
中止和上下文超长不算「模型坏了」。 用户自己按了停止 → 原样交回;上下文超长 → 交给 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、
autoContinue: true、autoContinueOnFailure: true、autoContinueOnMaxTokens: true、autoContinueLimit: 3、
continueDelaySec: 5、keepEventLog: true、eventLogLimit: 200。
开发
node test/smoke.mjs # 宿主半身:接力、隔离、复活、档位、契约、自动继续接线、补发节奏、轮尾便签、撞上限换人(44 项)
node test/client.mjs # 浏览器半身:真渲染设置页 + 真发请求 + 轮尾小字 + 隔离名单展开(35 项)
node test/real-host.mjs # 挂进真的 dsh-llm 注册表跑一遍,含档位校验(12 项)
node test/autocontinue.mjs # 自动「继续」的判定、节流与等待、本机网络判据、轮尾便签(51 项)
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 给插件一个 能安全丢弃已发出内容的位置。
- 自动「继续」需要 composition 里有
agents服务(桌面/网页版都有)。没有它 (比如纯 TUI、离线测试)时这项功能静默关闭,插件的其余部分照常工作 —— 它走的是ctx.inject(['agents']),不会因为缺这个服务而整个装不上。 - 「本轮失败」判据是
turn/end报的结局,所以只认真失败的轮。模型自己吐了一句 「抱歉我做不到」但正常收尾的轮,插件不会替你追问。 - 候选清单来自宿主里已注册的模型;如果你想排除某个 provider,在设置页里把它移除、
或填进
hiddenProviders。 - 插件自己的路由永远不会出现在候选里(否则会自我递归)。
- 档位表是启动后异步问出来的(问一次记一次,不再重复问)。万一某次任务来得比 扫描还早,那一个模型这一轮会按「不知道它认什么档位」处理 —— 不发档位,用它自己的 默认档位。这是刻意的取舍:宁可按默认档位跑,也不能发一个它不认的档位把它误判成 「今天不行」而隔离掉。设置页里点一次「重新扫描」就会补齐。
Похожие плагины
archify (deepseek-harness)
tt-a1i/archify
WeKnora (dsh-weknora)
tencent/weknora
weknora
tencent/weknora
BrowserSkill (dsh-plugin-browserskill)
tencent/browserskill