dsh-llm-error-retry
用于在deepseek-harnees中报错时重试大模型请求,支持配置http状态,字段-值命中。
- Stars
- 3
- Language
- JavaScript
- Created
- Sep 2, 2026
- Updated
- Oct 21, 2026
Introduction
dsh-llm-error-retry
大模型请求错误「休眠后重试」——DSH(DeepSeek Harness)Profile Bundle 插件,带 GUI 配置界面。
模型请求失败(如 429 限流)时,按规则命中 → 休眠配置时长 → 自动重新发起请求;每条规则独立配置休眠时长与最大重试次数,耗尽后错误如实上报。重试过程在会话框内以原生重试卡片展示(第 N/M 次 · 休眠 Xs);规则在 GUI「设置」页实时编辑,保存即生效、跨重启持久。
支持按 HTTP 状态码 / 机器码 / 返回字段=值 / msg~子串 四种形态命中报错(含 DashScope/阿里云百炼这类把状态码压平进 message 的适配器形态,以及 provider 只给一句文案、无 status 且 code 只到 PI_AI_ERROR 的形态)。
另外:模型输出达到 token 上限被截断时,自动注入一条「继续」让它接着输出,无需人工打断再发消息。
报错时重试大模型请求:配置错误码或响应字段 → 休眠 → 自动重发。 输出被截断时自动续写:不用手动打「继续」。
安装(一条命令)
dsh plugin --profile web add github:liaoyuqing/dsh-llm-error-retry
然后重启该 profile(bundle 层在启动时装配)。刷新 GUI 页面后,左下角 设置 → LLM 错误重试 即出现配置分区;备用配置页 http://127.0.0.1:3080/llm-error-retry。
发布到 npm 后可用 dsh plugin --profile web add dsh-llm-error-retry。
其他命令:
dsh plugin --profile web update # 升级到最新版
dsh plugin --profile web remove dsh-llm-error-retry # 卸载
profile 名按你的实际部署替换(桌面/网页版一般是
web)。安装本质是 pnpm 依赖 +dsh.bundle声明自动注册为 profile 层,与 dshmarket、dsh-cost-meter 等社区插件一致;本包无构建脚本、零运行时依赖。
手动安装(不经过 pnpm 的兜底方式)
把仓库文件(host.v15.mjs、client.js、package.json)放到任意目录(如 ~/.dsh/plugins/llm-error-retry-ui/),再在 ~/.dsh/cordis.patch.yml 追加:
- insert:
- id: llm-error-retry
name: ./plugins/llm-error-retry-ui/host.v15.mjs
config:
rules:
- code: '429'
delayMs: 60000
maxRetries: 10
web profile 默认 patchReload: live,保存即热挂载;刷新 GUI 页面加载设置分区(浏览器半由 DSH 的 client-modules 扫描 package.json 的 dsh.client 声明自动并入启动图,零构建)。
规则语法
「错误码 / 条件」支持四种形态,多条规则按顺序取第一条命中;每条规则独立 delayMs + maxRetries:
| 形态 | 例子 | 匹配目标 |
|---|---|---|
| HTTP 状态码 | 429、500、503 | failure.status;failure.code(429 / HTTP_429);message 中的独立数字(词边界,1429 不误伤)——覆盖 pi-ai/DashScope 把状态压平进文本的形态 |
| 机器码 | RATE_LIMIT、QUOTA、SERVER、TRANSPORT、TIMEOUT、EMPTY_RESPONSE | failure.code(忽略大小写,DSH 适配器的 provider 无关分类码) |
| 字段=值 | code=insufficient_quota、type=rate_limit_exceeded | provider 返回 JSON 的字段名与值,三层兜底:① failure.<字段> 标量属性 → ② failure.message 内嵌 {...} JSON 解析 → ③ message 宽松文本 "字段":"值" |
| msg~子串 | msg~finish_reason: error、msg~Provider returned an empty response | failure.message 的子串(忽略大小写)。用于没有 status、code 又被归到通用桶的失败:provider 在 SSE 里以未识别的 finish_reason 终止流时,pi-ai 报成 message='Provider finish_reason: <值>'、code='PI_AI_ERROR',前三种形态都命中不了 |
约束:delayMs ∈ 1003600000;1000(maxRetries ∈ 00 = 命中即上报)。
行为细节:
- 监听
agent/request-errorwaterfall,prepend: true置于最外层:命中即接管,优先于内置 llm-retry 的快速指数退避;预算耗尽直接上报(不再回退到内置策略);未命中的失败原样放行。 - 计数按「同一 Agent + 同一请求序列(provider/turn/step)」,新一轮请求自动重置;Agent 销毁即清理。
- 服务端返回
Retry-After且比配置更长时按Retry-After推迟,但最多只多等honorRetryAfterMaxMs(默认 60s,在设置分区可改,0 = 完全忽略提示)。上游常回Retry-After: 3600(一小时),无上限地照做会让会话整小时不动,还把你规则里写的休眠时长悄悄改掉。 - 休眠可被用户中断/停止即时唤醒,会话卡片显示为已取消。
输出截断自动续写
模型输出达到 token 上限被截断时,插件自动注入一条「继续」,让它接着把话说完:
- 为什么不能靠规则码命中:max-tokens 不是失败。pi-ai 把它映射成
{ kind: 'max-tokens' },agent-loop 收到后结算为步骤结束原因,而agent/request-error瀑布只在 error/aborted 时触发——所以任何错误码规则都命中不了它。 - 为什么必须在轮次收尾处接力:宿主把 max-tokens 记在轮次上且是粘性的(后续步骤正常完成也不能把它降级)。一旦粘住,宿主在每个后续步骤收尾时都会派发
agent/turn-stopping,并在没人投递新步骤时结束轮次——普通「工具调用后自动继续」正是在这里失效的。插件在这个钩子上补回宿主本该做的判断。 - 两类接力分开计数:「截断 → 续写 → 又截断」受
maxContinues封顶、续写前休眠delayMs;「工具结果待回喂」是宿主本来就会做的那一步,不计入maxContinues、不休眠、无上限(真实 agentic 轮常有几十上百步工具调用,共用预算会在第 5 步后把工具结果永远卡在对话外面)。 - 跨轮自动重启:轮次仍以粘性的 max-tokens 收尾时(goal 驱动见该 reason 即 disarm,没人开新轮),只要本轮确有实质推进就等旧 driver 收敛后自动
followup一条「继续」开新轮;连续重启上限restartTurns,正常收尾即清零,无推进则停(防止模型只会复读「继续」时空转)。
对应配置(补丁行 config.maxTokens):
maxTokens:
enabled: true # 关掉则恢复原样:截断后等你手动继续
text: '继续' # 注入内容
maxContinues: 5 # 每轮最多自动续写 5 次(只约束截断续写)
delayMs: 3000 # 截断续写前休眠 3 秒,别打爆 provider
restartTurns: 10 # 轮次以粘性 max-tokens 收尾时,最多连开 10 个新轮
restartDelayMs: 1200 # 重开前等旧 driver 收敛的时间(不能省:running 阶段投递的唤醒会被丢弃)
首字超时
「输出截断自动续写」管的是「说到一半被截断」,首字超时管的是「一个字都不说」:请求发出后超过 firstTokenTimeout.timeoutMs 仍没有任何内容增量,就中断这次请求并报超时。
- 什么叫「第一个字」:
text-delta(正文)、reasoning-delta(思考)、tool-call-delta(工具调用)任一到达即撤销计时。只收到block-start/usage这类不含内容的结构帧不算——所以在计时窗口内一直有思考流出的推理模型不会误伤。 - 接缝是
llm/stream中间件(waterfall,global: true):包住下游流,在第一次拉取处起表。超时后停止拉取,并产出一个合成的终止 chunk{ type: 'finish', reason: { kind: 'error', failure: { code: 'TIMEOUT', message: '首字超时:…' } } }。之所以不抛异常:中间的 waterfall 抛错会直接穿出 agent-loop(llm/src/index.ts明确「middleware 失败仍抛出」),不经过agent/request-error瀑布,本插件的规则引擎就没机会重试;合成 finish 则走宿主正常失败路径——因此再配一条规则码TIMEOUT就能自动重试,否则它作为本轮错误上报,聊天区显示超时原因。 - 只约束 agent 主请求:
compaction/session-title这类带purpose的调用不设限(压缩中途被打断会伤上下文),没有sessionId的零散调用也无从归属。 - 已知限制:放弃消费不等于立刻断开连接。异步生成器停在
await时return()是排队的,上游 HTTP 请求可能在 provider 自己响应或 pi-ai 的流空闲 watchdog(profile.streamIdleTimeoutMs)触发前继续挂着;本接缝拿不到也不该改宿主的请求对象(它被Object.freeze且带 agent-loop 的对象身份标记),无法注入自己的 AbortSignal。
对应配置(补丁行 config.firstTokenTimeout,或设置分区里的「首字超时」开关 + 秒数):
firstTokenTimeout:
enabled: true
timeoutMs: 180000 # 1000–3600000;长思考模型别设太小,否则正常请求也会被判超时
配置来源(优先级从高到低)
- GUI 界面保存:
~/.dsh/llm-error-retry/rules.json(含设置分区与备用页,两处等价;跨重启持久,npm升级不影响)。文件里除rules外还存honorRetryAfterMaxMs(Retry-After 采信上限)与firstTokenTimeout(首字超时)。 ~/.dsh/cordis.patch.yml中同 id(llm-error-retry)覆盖行的config.rules、config.honorRetryAfterMaxMs与config.firstTokenTimeout。- 本包内置默认(
cordis.patch.yml):429 → 60s × 10。
运行快照与事件流水:~/.dsh/llm-error-retry/status.json(config.dataDir 可改数据目录)。
工作原理
- 宿主半(
host.v15.mjs):DSH 每次模型请求失败会派发agent/request-errorwaterfall(payload 携带failure{message,code,status?,providerRetryAfterMs?}、retryPolicy、signal)。插件命中规则后等待delayMs,返回{kind:'retry'}让 agent 循环重新发起同一请求;并写入llm/retry/llm/retry-started会话持久事件(与内置重试同一契约、同一 retryId 贯穿整条链),聊天区原生卡片即时渲染。输出截断的自动续写走agent/assistant-stream(记录每步 finish)+agent/turn-stopping(在轮次收尾处补投一步)+session/event(轮次收尾后跨轮重开)三个钩子;首字超时走llm/streamwaterfall(包一层异步生成器,到点产出合成的 TIMEOUT finish chunk)。 - 浏览器半(
client.js):DSH 的双拼包机制——package.json声明dsh.client.platform=web+exports["./client"],宿主 client-modules 扫描到后把 bundle 并入 GUI 启动图(window.__ModuleLoader__.load注册协议,与官方打包产物一致),在浏览器端注册settings.section设置分区。数据面走宿主webServer路由/llm-error-retry/api/config(同源 fetch)。
二次开发注意(DSH 平台怪癖清单)
- 宿主半代码改动必须换文件名(
host.vN.mjs,同步package.json exports["."]与补丁行 name):DSH 的 config-only HMR 只热更配置,不重载同路径 ESM 模块;只改client.js无需改名(每次重组重读字节)。 - 宿主 HTTP 响应绝不自带
Content-Length(statusCode + setHeader + res.end):web profile 的 gzip 中间件(阈值 1024B)与writeHead钉死的 CL 冲突,压缩后浏览器会丢弃整个响应体。 webServer用ctx.inject(['webServer'], …)异步等待 +register失败轮询重试:冷启动时 HOME 补丁层先于 webserver 挂载;bundle 层同样存在新旧 fiber 交叠的 duplicate-route 窗口。headless profile 无 webServer → 仅无界面,重试逻辑不受影响。- 用户配置绝不能写进包目录(pnpm store 升级即替换)→ 数据目录在
~/.dsh/llm-error-retry/。 - 行为回归用「假 agent + 假 session」探针:
ctx.waterfall('agent/request-error', payload, next)触发真实监听链,session.append用收集器承接,零污染真实会话。
常见问题
- 怎么确认它在工作? 触发一次限流错误后:会话框出现「模型重试」卡片(第 N/M 次 · 休眠 Xs);
~/.dsh/llm-error-retry/status.json的lastEvents会依次记录sleeping → retrying,waiting计数即实时休眠数;宿主日志(llm-error-retry:前缀)同步输出。输出被截断时事件里会记continuation(已自动续写)、restart(已自动重开新轮),耗尽或无进展时记continuation-exhausted/continuation-stalled/restart-exhausted/restart-stalled。 - 会把不该重试的错误重试掉吗? 只有命中规则的才重试;未命中一律原样放行(内置策略行为不变)。规则命中后预算耗尽即上报,不会无限吞错。
- 中断会怎样? 休眠等待可被用户中断/停止即时唤醒,卡片显示为已取消。
- 升级/卸载后我的规则还在吗? 规则在
~/.dsh/llm-error-retry/rules.json,与包无关;dsh plugin --profile web remove dsh-llm-error-retry后想彻底清除数据再删这个目录即可。 dsh plugin add卡住? 本包无构建脚本、零依赖,git 安装不需要allowBuilds;若网络到github.com:443不稳可稍后重试或用tarball安装提示。
许可
MIT © 2026 liaoyuqing