dsh-llm-fallback
visol-456/dsh-llm-fallback
DeepSeek Harness 回退链插件:主模型失败自动切换备用 provider,带 Web UI 配置面板 | Provider fallback chains for DeepSeek Harness
Install
dsh plugin --profile web add github:visol-456/dsh-llm-fallbackREADME
@visol-456/dsh-llm-fallback
English | 中文
DeepSeek Harness 的 provider fallback chain 插件——当主 provider 失败时,同一请求会自动在下一个配置的 (provider, model) 条目上重试,限流、超时或临时不可用的 provider 不会直接终结一轮对话。
DeepSeek Harness
dsh-plugin生态的社区插件,不属于官方仓库。
开发原因
由于众所周知的原因,deepseek要涨价了,对于我一个学生直接用不起了,于是只能去投奔opencode-go。然而,opencode-go的海外链接非常不稳定,挂了代理都不行,在长程任务中经常出错暂停,单 provider 部署在服务不稳定时会直接失败:
- 限流与配额错误
- 服务端错误、5xx 抖动
- 超时与传输层故障
本插件为每条链维护一份按优先级排序的 (provider, model) 路由表,跟踪连续可切换失败(熔断器),并自动把请求故障切换到下一个健康条目。后续请求会继续使用当前服务条目,直到冷却结束、对链头的一次探测成功为止。
快速开始
npm i @visol-456/dsh-llm-fallback
在 cordis.yml 中挂载插件:
- name: '@visol-456/dsh-llm-fallback'
config:
fallbacks:
- provider: pi-ai
model: glm-4.5
switchCodes: [EMPTY_RESPONSE, RATE_LIMIT, SERVER, UNKNOWN_MODEL, TIMEOUT, TRANSPORT]
failureThreshold: 1
cooldownMs: 30000
请求本身永远是链头(你在 UI 里选的 provider/model,或部署默认值),永不被改写。fallbacks 列出请求失败后按顺序切换的备用目标。省略 fallbacks 键合法且插件保持休眠,所有请求原样放行;等你在 Web 界面的 Settings -> 回退链 页保存备用目标之后再生效。
部署到 web profile(dsh web)
A. dsh plugin add(推荐)
本包声明了 dsh.bundle,安装后会作为 profile 层自动激活(无需手写 patch 文件——随包附带的 cordis.patch.yml 会以无备用目标挂载插件,目标在 UI 里创建):
dsh plugin --profile web add @visol-456/dsh-llm-fallback
B. 手动 patch 覆盖层
创建一个覆盖层文件(是 patch 列表,不是裸条目列表),用 --patch 应用:
# cordis.yml
- insert:
- id: llm-fallback
name: '@visol-456/dsh-llm-fallback'
dsh web --patch ./cordis.yml
patch 语法(最大的坑)
-
每个挂载条目必须有
id。 -
新增条目必须放在顶层
- insert:列表里(参照 harness 的examples/web-schedule/cordis.yml)。 -
裸条目列表会被静默拒绝:报
patch: id is required for non-insert patches/entry "xxx" not found,而且dsh web启动不打印任何错误(只有一行dsh web: http://...)。 -
用以下命令诊断组合配置树(含 patch 错误):
node --import tsx/esm apps/cli/src/bin.ts web --dump-config --patch <file>
本地开发 junction 位置
$DSH_HOME/profiles/node_modules 是 launcher 维护的 bundle 回退目录,不参与 cordis.yml 条目的裸 import 解析(loader 从 harness 源码位置向上走 Node 标准 node_modules 解析)。本地挂载未发布的 checkout,必须把 junction 建在 harness 根 node_modules:
New-Item -ItemType Junction -Path 'E:\python_programs\deepseek-harness\node_modules\@visol-456\dsh-llm-fallback' -Target 'E:\python_programs\llm-fallback'
然后按名字挂载(见上)。不再需要时删除 junction。
不要重复挂 llm-retry
web profile 的 base bundle 已经自带 @deepseek-ai/dsh-llm-retry,重复挂载会叠加一层重试。只需挂 llm-fallback 一条——waterfall 顺序(先重试后回退)天然正确。
pnpm supply-chain 策略
发布不足 24 小时的包会被 pnpm 的 minimumReleaseAge 拦截(ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION);失败的 pnpm add 还可能改动官方仓库的 pnpm-workspace.yaml(用 git restore pnpm-workspace.yaml 恢复)。当天安装要么等 24 小时,要么走上面的 junction 方式。
端口被旧进程占用
dsh web 起不来(或浏览器打到旧实例)时,找到并结束旧进程:
netstat -ano | findstr :3080
taskkill /PID <pid> /F
配置项
所有键都是顶层(不再有 chains/match):
fallbacks(需要路由时必填,至少一条):按顺序排列的(provider, model)备用目标,请求失败后切换过去。请求本身是链头,永不被改写;条目不得重复(provider, model)组合。省略fallbacks键合法且插件保持休眠(可在 Settings -> 回退链 页创建,或写入<DSH_HOME>/settings.yaml)。switchCodes(默认EMPTY_RESPONSE, RATE_LIMIT, SERVER, UNKNOWN_MODEL, TIMEOUT, TRANSPORT,覆盖瞬时故障与配置错误类):允许触发切换的失败码;其他错误码永不切换。failureThreshold(默认 1):链头(或某个 fallback)上的连续合格失败数达到该值即打开熔断;冷却探测失败则无条件打开。cooldownMs(默认 0):切换后链头在多长时间内保持排除、之后才可被再次探测。
建议:把
cooldownMs设为至少30000。默认0意味着每个请求都会先探测链头,故障期间每个请求都会先在链头上失败一次,再被备用条目接管。
破坏性变更(0.1.x):配置曾用过
chains[]里的providers(0.1.0)或match+fallbacks(更早的 0.1.1 快照)。这些都没了:链头永远是请求本身,只需顶层fallbacks列表(加上切换规则)。迁移:chains: [{ match: { provider: A.provider, model: A.model }, fallbacks: [B, C] }]→fallbacks: [B, C]。加载含旧chains/match/providers键的配置会报清晰弃用错误。
非空配置非法时,插件加载(或经 settings seam 保存时)会直接报错。
工作方式
- 链头就是请求本身(用户在 harness 首页聊天栏选的 provider/model,或部署默认值)。插件绝不改写链头;任何以可切换错误码失败的请求,都会按顺序在同一个全局
fallbacks列表上重试。 - 失败请求只在实际服务的 provider 匹配、失败码在
switchCodes内时才计。 - 链头的连续失败数达到
failureThreshold(或冷却探测失败)时,同一请求在fallbacks[0]上重试;之后每个 fallback 失败依次切到下一个。 - 链头冷却期间每个请求仍先试链头(它永不被改写);可切换的链头失败会直接在当前服务的 fallback 上重试。
- 最后一个 fallback 永不切换;它的失败保持终态并按正常方式上抛。
- 成功响应会清零当前服务条目的连续失败计数并清除冷却标记,恢复后阈值从头累计。
- 插件不包装
ctx.llm.stream():每次 adapter 调用仍是一次 provider 尝试,每次链尝试都会在同一份持久历史之上开启新的编号轮次。
事件
两个事件都是持久会话事件,永不呈现给模型。
llm/fallback——每次切换时追加。载荷:turn、step、headProvider、headModel、fromProvider、fromModel、toProvider、toModel、reason(threshold|probe)、failure、cooldownMs。llm/fallback-route——每次请求实际由 fallback 目标服务时追加。载荷:turn、step、headProvider、headModel、provider、model(head = 触发路由的那个请求)。
已知限制
- 单一全局备用列表:所有请求共享一个
fallbacks列表;失败按实际服务的(provider, model)归因,一个 agent 的成功不会清除另一个 agent 的待定计数。 - 状态仅进程内:活动条目、冷却与连续计数在重启后归零,重启后的部署会重新从链头探测;持久事件可用于事后审计,但无法还原实时状态。
- 仅 agent-loop 请求参与:直接调用
ctx.llm.stream()的消费者仍是单 provider。 - always 模式重试不委派:retry 策略为
always的 provider 会自己重试一切,fallback 看不到它的失败。
Web UI 配置(dsh web)
无需手写 cordis.yml,也可以在 Harness 的 Web 界面里编辑备用目标。插件加载到 dsh web profile 后,Settings 面板会出现一个 回退链(Fallback)页面(与 Models 并列):
- 没有配置任何备用目标时,页面显示引导空状态:「还没有备用目标」+「添加备用目标」按钮;新建并保存的第一个目标在下一次请求生效。
- 编辑备用目标:每行都用下拉选择(provider 与 model 均取自 harness 模型目录;选中 provider 后联动刷新 model 列表,从根源杜绝手填出
11111这类不存在的 model),上移/下移/删除按钮在行内右侧;切换错误码(宽输入框)、失败阈值与冷却时间在下方同一对齐网格里,然后点击 保存。 - 保存的值写入
<DSH_HOME>/settings.yaml,并在下一次请求生效(无需重启)。解析顺序为 schema 默认 →cordis.yml条目 → 已保存的 UI 段,因此 UI 保存优先,cordis.yml未写的字段回落到默认值。 - 重置为 cordis.yml 会清空已保存段,恢复纯
cordis.yml行为(若条目无链则回到休眠模式)。 - 若其他窗口或设置文档修改了配置,页面会显示冲突横幅,提示先重新加载再应用。
浏览器通过插件在共享 web server 上提供的仅回环端点(/llm-fallback/config)读写该段。端点拒绝非回环来源与跨站请求;它是防误写/防跨站围栏,不是鉴权层。当 web server 绑定 0.0.0.0 时局域网客户端无法写入,但仍不建议将该端点暴露给不受信网络。
许可证
MIT