dsh-llm-auto
注册自动尝试多条模型路由的备用路由服务 适合需要模型请求在输出前失败时自动重试、按顺序切换备用路由的用户。
安装
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:liuyun847/dsh-llm-auto说明文档
阅读完整 README ↗2. 配置
配置有两个入口:
- 插件页里的表单(0.4.0 起):「已安装」→ 点开
dsh-llm-auto卡片 → 表单在描述与行之间, 只暴露compactWindow一个字段,保存才写入(见下节「插件页里的 compactWindow 表单」); - 包内
cordis.patch.yml的注册行(= 本包自带、随组合包加载的那一行):结构性配置routes/retry只能在这里改;profile 层要覆盖就按id: llm-auto写覆写行(profile 层在包层之后应用)。
标了「热改」的键同时是 schemastery 的 .volatile() 字段,经宿主的设置服务可改、改完即时生效
(效果边界见本节末尾)。
- insert:
- id: llm-auto
name: 'dsh-llm-auto'
config:
routes:
- { provider: opencode-go, model: deepseek-v4.1-flash }
- { provider: commandcode, model: deepseek/deepseek-v4.1-flash }
- { provider: deepseek-official, model: deepseek-flash }
# 压缩点(默认就是 500000,写出来只是显式化)
compactWindow: 500000
# retry 不写 = 官方默认(每路由 5 次重试);要关掉或改参数再放开:
# retry:
# maxRetries: 5
# retryableCodes: [EMPTY_RESPONSE, RATE_LIMIT, SERVER, TIMEOUT, TRANSPORT]
# backoff: { initialDelayMs: 500, maxDelayMs: 10000, jitterRatio: 0.1 }
| 键 | 必填 | 默认 | 热改 | 说明 |
|---|---|---|---|---|
routes | ✅ | — | ✗ | 有序回退链,第一项即首选。每项 { provider, model };model 要写全(settings.yaml 里的真实 id,如 deepseek-v4.1-flash、deepseek/deepseek-v4.1-flash,别省前缀)。改它要改包内 cordis.patch.yml(包层 patch:不需要重启,但改完要有一次触发才被读入 —— 见 §4 第 2 条;插件会重新 apply) |
compactWindow | 500000 | ✅ | 让自动压缩发生在这个 token 数附近(正整数)。对外宣称的窗口由它反算(见下) | |
name | Auto | ✅ | 模型显示名(选择器里的分组名恒为 Auto) | |
contextWindow | 见下 | ✅ | 【旧键】直接声明对外宣称的上下文窗口(正整数)。与 compactWindow 同时给出时以 compactWindow 为准并被忽略(warn 会点名两者) | |
logLimit | 50 | ✅ | 路由日志环形缓冲条数(仅内存) | |
retry.maxRetries | 5 | ✗ | 每路由重试上限(不含首次);0 = 关闭重试,恢复"一次败就切" | |
retry.retryableCodes | EMPTY_RESPONSE/RATE_LIMIT/SERVER/TIMEOUT/TRANSPORT | ✗ | 可重试的错误码白名单;永久错误(不在表里的)一次败就切,不白烧请求 | |
retry.backoff.initialDelayMs | 500 | ✗ | 指数退避起步(毫秒) | |
retry.backoff.maxDelayMs | 10000 | ✗ | 退避封顶;上游 Retry-After 超过它则不等了,直接切下一条 | |
retry.backoff.jitterRatio | 0.1 | ✗ | ±10% 对称抖动 |
「热改」= 该键是 schemastery .volatile() 字段 ⇒ 由宿主设置服务写入后原地更新引用,
插件下次用到时就是新值(不需要重启、也不需要重新 apply);✗ 的键是结构性配置,
由 apply() 一次性消费,只能改包内 cordis.patch.yml。
retry 块的形状与默认值全部复用官方 resolveRetryPolicy(@deepseek-ai/dsh-llm,
即自带 dsh-llm-retry 用的那个):选 auto 与选普通模型的重试语义一致。只支持 mode: 'normal'
(always = 无上限重试,单请求可能无上限计费,挂载时 warn 并回落默认);坏值只 warn 不阻止注册。
compactWindow:压缩点 → 对外宣称的窗口
DSH 的自动压缩引擎 @deepseek-ai/dsh-compaction-basic 不看真实上游窗口,只用"该请求声明的
窗口"算阈值(该包 lib/index.js:124-147):
threshold = floor(min( cw × 0.8, cw − reserved − 65536 )) # 0.8 / 65536 是引擎默认值
reserved = 请求头 maxTokens ?? 适配器声明的 defaultMaxTokens ?? 0
本插件既不声明 defaultMaxTokens、调用方也不给 maxTokens ⇒ reserved = 0,
于是 threshold = floor(min(cw × 0.8, cw − 65536))。反过来解就能让压缩点落在你要的位置:
cw = max(ceil(T × 1.25), T + 65536) # T = compactWindow;T = 500000 ⇒ cw = 625000
验算(T = 500000):0.8 × 625000 = 500000;625000 − 65536 = 559464 > 500000(min 取前者);
保留尾部 floor(625000 × 0.16) = 100000 ⚠ 宿主里 schema 总会给 compactWindow` 补上默认值 500000 ⇒ 默认口径就是反算;
contextWindow只在"compactWindow完全没出现"时才生效(直接调apply()的集成方)。 两者同时给出时会打一条 warn 说明用了哪个、忽略了哪个。 ⚠compactWindow低于最小可用值 12484(见minimumUsableCompactWindow())会另打一条 warn:此时引擎的"保留尾部 (floor(0.16 × (T + 65536)) ≥ T),@deepseek-ai/dsh-compaction-basic每轮抛TargetPressureConfigError;该错误被引擎捕获(agent/pre-step那层):第一次 warn、之后对同一目标 静默跳过压缩、回合照常继续 —— 净效果是压缩从不发生(不会报错,但上下文会一直涨)。 这个下界是算出来的,不是拍脑袋的常量(见minimumUsableCompactWindow())。 ⚠compactWindow/contextWindow非法(非正整数)只 warn 并回落默认值,不会让宿主起不来。 ⚠contextWindow ≤ 65536时引擎的 pressure budget ≤ 0,连阈值都算不出来(同样每轮抛错)—— 这也是引入compactWindow的原因之一;反算路径下T ≥ 12484一定是安全的。 ⚠ 该反算依赖上面三个引擎默认常量。引擎换版本、或你把compaction-basic的thresholdRatio/headroomTokens改成别的值,映射就不再精确 —— 挂载日志里那行假设就是为 这种时候留的证据(可用/api/llm-auto/routes的compactWindow/declaredContextWindow复核)。
插件页里的 compactWindow 表单(0.4.0 起)
0.4.0 起本包自带浏览器半侧(lib/client.js),注册进插件页为组合包预留的 plugins.bundle.config
slot(键 = 本包包名 dsh-llm-auto)⇒ 插件页 →「已安装」→ 点开 dsh-llm-auto 卡片,
官方 SettingsForm 的表单就渲染在卡片描述与行列表之间,不必再点进任何二级页。
位置沿革(两版同为 2026-09-25):初版注册的是
plugins.row.config(键#), 表单只能从「行llm-auto→「配置」」的二级页进入 —— 要钻四层。现按用户要求提到卡片上, 行上的「配置」控件随之消失(本插件不再占用plugins.row.config)。 两个槽位的契约都在dsh-client-ui-plugin-manager的lib/types/client/slot-contract.d.ts; 该页只在 ledger 收录了这个键时才渲染这一段(其client.js:2879的ledger.bundles.has(pkg.name)), 而 ledger 直接读槽位注册的 key(其client.js:48的keysOf)⇒ 键必须恰好是包名。
- 只有一个字段「用于压缩的上下文窗口」(=
compactWindow),与官方四个配置页 (ui-settings-shell/agent-loop/subagent/web-search)同构:用SettingsFormModel/SettingsForm/SettingsValueField暂存草稿,点保存才写入,自带「已覆盖」标记与「恢复默认」; - 写入经宿主 settings 服务,只落
compactWindow这一个键,routes原样不动 (routes/retry不是.volatile()字段,写了也不会原地生效 ⇒ 仍改包内cordis.patch.yml); - 命名空间就是 loader 行 id
llm-auto(dsh-settings按entry.options.id投影)—— 与启停开关、覆写行的寻址键一致; - 前提:这个半侧是 0.4.0 新增的,首次启用必须重启一次 dsh 才会被收录(依据见 §1); 此后改这个文件本身也要重启(或至少刷新页面)才看得到新表单,见 §4 第 2 条;
- 备选改法不变:手编 profile 的
cordis.patch.yml追加按 id 覆写的顶层行 (- id: llm-auto+name: 'dsh-llm-auto'+config: { routes: [...], compactWindow: N }, profile 层在包层之后应用 ⇒ 遮蔽包内那行 insert),或直接改包内cordis.patch.yml。routes这类结构性配置没有表单,只能走这两条。
导出 Config(= 成为可编辑配置条目),以及它的代价与效果边界
0.3.0 起本插件导出一个 schemastery Config(lib/index.js)。效果是配置成为宿主的可编辑条目:
dsh-settings 的 SettingsForms.describe() 会为带 schema 的活动条目生成描述符
(lib/index.js:413-452;schema 取 entry.fiber.runtime.Config,同文件 :538-541),
settings.describe() / settings.mutate() 这条远端通道因此能看到并写入它的字段。
name / compactWindow / contextWindow / logLimit 标了 .volatile(),插件不缓存这些值
(每次用到时重新读引用),所以值一改就生效、不需要重启;routes / retry 是结构性配置,
仍走包内 cordis.patch.yml。
⚠ 效果边界(如实说明):导出
Config这一步本身不生成任何表单 —— 随包发布的 Web 客户端 没有"按 schema 自动生成表单"的页面(@deepseek-ai/dsh-settings的 README 自己写着Each form reports autoGenerate … for clients that build pages from the schema; no shipped client does so yet),插件页(Plugins)只渲染经 slot 注册的表单页(dsh-client-ui-plugin-manager的plugins.item/plugins.bundle.config/plugins.row.config)。0.4.0 起本包自带浏览器半侧 (lib/client.js)注册进plugins.bundle.config,才把compactWindow那一个字段变成插件页里可点的 表单(见上一节);宿主侧 schema 的作用是让settings.describe()/settings.mutate()这条远端 通道能看到并写入这些字段。 本节的结论来自源码阅读 + 单测(test/compact.test.mjs里"真 schema 校验 ⇒ 解包 ⇒ 反算 625000")。
代价与取舍(原作者当初"有意不导出 Config"的理由依然成立,只是被权衡掉了):
- schema 是加载期校验,校验失败 = 整行插件加载失败(不再是本插件那条"打 error 但不注册"
的软失败)。所以
routes/retry用z.any():形状校验继续留在normalizeRoutes()/normalizeRetry()里,坏值依旧只 warn/error;数值字段用z.number()但不加.min()/.step(), 范围与整数性仍由插件自己判并 warn 回落。唯一会硬失败的是类型错误(例如把字符串写进compactWindow这种数字字段)。 - 表单只覆盖标了
.volatile()的字段(volatileForm(),dsh-settingslib/index.js:122-131; 一个 volatile 字段都没有时整条目被跳过),所以routes/retry不在可写字段里 —— 这是有意的: 它们由apply()一次性消费,标成 volatile 等于承诺一个做不到的"改了即时生效"。 .volatile()字段在插件里拿到的是 cosmokit 的 Volatile 引用(不是值本身), 读之前必须unwrapVolatile()(lib/compact.js)。
⚠ 宿主侧这条能力("配置成为可编辑条目")的依据是上面引用的源码位置 + 单测里 "真 schema 校验 → 解包 → 反算 625000"这条用例(见 §7);插件页里的表单入口见上面那节 (它走的是同一套 schema:
ctx.configForms只投影标了.volatile()的字段)。 重启后请先看/api/llm-auto/routes的compactWindow/declaredContextWindow是否符合预期。
contextWindow 不写且 compactWindow 也没给时:逐个试路由,取第一条能给出正整数窗口的那条的窗口;
全都拿不到就用保守值 65536(宁可让压缩早触发,也不要谎报一个大窗口导致请求必撞上游上限)。
解析结果缓存 30 秒,目录被反复重建时不会反复去问上游适配器。
⚠
provider不要写auto(自递归):挂载时会 warn 并跳过该条。 ⚠routes为空/缺失/解析后一条不剩:打一条error并拒绝注册(不抛错,宿主照常启动)。 ⚠ 写成auto的整条目的链要避开两个坑:①provider: auto是自递归(挂载时 warn 并跳过该条); ② 别写本机不可解析的通道或模型 id,否则那一跳每次都以NO_ADAPTER/UNKNOWN_MODEL/MISSING_CREDENTIAL白撞一次(点开/api/llm-auto/routes能看到真实 code)。 ⚠ 本文档里的链是"当时的实例",权威定义在包内cordis.patch.yml(profile 层按id: llm-auto的覆写行优先):那份改了而本文没同步时, 以文件为准(curl http://127.0.0.1:3080/api/llm-auto/routes的chain字段永远反映当前生效值)。
导出 Config(= 成为可编辑配置条目),以及它的代价与效果边界
0.3.0 起本插件导出一个 schemastery Config(lib/index.js)。效果是配置成为宿主的可编辑条目:
dsh-settings 的 SettingsForms.describe() 会为带 schema 的活动条目生成描述符
(lib/index.js:413-452;schema 取 entry.fiber.runtime.Config,同文件 :538-541),
settings.describe() / settings.mutate() 这条远端通道因此能看到并写入它的字段。
name / compactWindow / contextWindow / logLimit 标了 .volatile(),插件不缓存这些值
(每次用到时重新读引用),所以值一改就生效、不需要重启;routes / retry 是结构性配置,
仍走包内 cordis.patch.yml。
⚠ 效果边界(如实说明):导出
Config这一步本身不生成任何表单 —— 随包发布的 Web 客户端 没有"按 schema 自动生成表单"的页面(@deepseek-ai/dsh-settings的 README 自己写着Each form reports autoGenerate … for clients that build pages from the schema; no shipped client does so yet),插件页(Plugins)只渲染经 slot 注册的表单页(dsh-client-ui-plugin-manager的plugins.item/plugins.bundle.config/plugins.row.config)。0.4.0 起本包自带浏览器半侧 (lib/client.js)注册进plugins.bundle.config,才把compactWindow那一个字段变成插件页里可点的 表单(见上一节);宿主侧 schema 的作用是让settings.describe()/settings.mutate()这条远端 通道能看到并写入这些字段。 本节的结论来自源码阅读 + 单测(test/compact.test.mjs里"真 schema 校验 ⇒ 解包 ⇒ 反算 625000")。
代价与取舍(原作者当初"有意不导出 Config"的理由依然成立,只是被权衡掉了):
- schema 是加载期校验,校验失败 = 整行插件加载失败(不再是本插件那条"打 error 但不注册"
的软失败)。所以
routes/retry用z.any():形状校验继续留在normalizeRoutes()/normalizeRetry()里,坏值依旧只 warn/error;数值字段用z.number()但不加.min()/.step(), 范围与整数性仍由插件自己判并 warn 回落。唯一会硬失败的是类型错误(例如把字符串写进compactWindow这种数字字段)。 - 表单只覆盖标了
.volatile()的字段(volatileForm(),dsh-settingslib/index.js:122-131; 一个 volatile 字段都没有时整条目被跳过),所以routes/retry不在可写字段里 —— 这是有意的: 它们由apply()一次性消费,标成 volatile 等于承诺一个做不到的"改了即时生效"。 .volatile()字段在插件里拿到的是 cosmokit 的 Volatile 引用(不是值本身), 读之前必须unwrapVolatile()(lib/compact.js)。
⚠ 宿主侧这条能力("配置成为可编辑条目")的依据是上面引用的源码位置 + 单测里 "真 schema 校验 → 解包 → 反算 625000"这条用例(见 §7);插件页里的表单入口见上面那节 (它走的是同一套 schema:
ctx.configForms只投影标了.volatile()的字段)。 重启后请先看/api/llm-auto/routes的compactWindow/declaredContextWindow是否符合预期。
contextWindow 不写且 compactWindow 也没给时:逐个试路由,取第一条能给出正整数窗口的那条的窗口;
全都拿不到就用保守值 65536(宁可让压缩早触发,也不要谎报一个大窗口导致请求必撞上游上限)。
解析结果缓存 30 秒,目录被反复重建时不会反复去问上游适配器。
⚠
provider不要写auto(自递归):挂载时会 warn 并跳过该条。 ⚠routes为空/缺失/解析后一条不剩:打一条error并拒绝注册(不抛错,宿主照常启动)。 ⚠ 写成auto的整条目的链要避开两个坑:①provider: auto是自递归(挂载时 warn 并跳过该条); ② 别写本机不可解析的通道或模型 id,否则那一跳每次都以NO_ADAPTER/UNKNOWN_MODEL/MISSING_CREDENTIAL白撞一次(点开/api/llm-auto/routes能看到真实 code)。 ⚠ 本文档里的链是"当时的实例",权威定义在包内cordis.patch.yml(profile 层按id: llm-auto的覆写行优先):那份改了而本文没同步时, 以文件为准(curl http://127.0.0.1:3080/api/llm-auto/routes的chain字段永远反映当前生效值)。