# dsh-plugin-reasoning-effort-sync 让 DeepSeek Harness 的模型路由**思考档位**与网关自己已经公布的档位保持同步,从而让"新增路由、新增模型"不再等于"逐个手写 `reasoningEfforts`"。 [English](README.md) ## 它解决什么问题 DSH 的档位选择器由每个模型的推理元数据驱动。对于 `@deepseek-ai/dsh-llm-pi-ai` 从 pi-ai 内置目录提供的路由,这些元数据是白送的;对于你自己声明的路由——任何自建或中转网关——就不是了:路由名对目录而言是陌生的,`models` 是**替换**目录而不是扩展它,而省略 `reasoningEfforts` 的模型条目会被当成非推理模型。于是选择器隐藏 Effort 行——这个行为本身是对的,但它是静默的。 档位其实并非未知:你的网关就在 `GET {baseURL}/models` 里公布着。本插件从那里读取,并写进 profile patch。 ## 前提 - DSH `0.2.0-rc.2`(安装时会校验 `peerDependencies` 范围)。 - profile 的 `cordis.patch.yml` 里有一个 `llm-pi-ai` 行,且目标路由**声明在 profile 层**——插件只标注那一行,只寻址 profile 层里的路由。 - 档位至少来自两个来源之一。先问路由自己的端点:从 `GET {baseURL}/models` 的每个条目读取 `reasoning_efforts`(列表)或 `reasoning_fixed_effort`(单档)。端点没描述的模型,再去 [models.dev](https://models.dev/) 查,且只采用该模型名下**所有声明都认同**的档位。两个来源都定位不到的模型保持不动,仍需人工声明。 ## 安装 在 Web GUI 的 **Plugins** 页面按注册表名、绝对路径、Git 地址或 tarball 安装,然后启用该 bundle: ``` C:\documents\dsh\dsh-plugin-reasoning-effort-sync ``` Desktop profile 由 Electron 应用独占管理,所以 `dsh plugin --profile desktop …` 会被拒绝,请走 Plugins 页面。本包不含任何 lifecycle script,因此不需要构建脚本审批。 ## 配置 bundle 的 patch 注册一行,默认值如下: ```yaml - id: reasoning-effort-sync name: dsh-plugin-reasoning-effort-sync config: enabled: true syncOnLoad: true intervalMinutes: 0 # 0 = 不做周期同步 requestTimeoutMs: 10000 dryRun: false routes: {} # 空 = 处理 pi-ai 行声明的全部路由 modelsDev: # 兜底来源,仅用于端点没有描述的模型 enabled: true url: https://models.dev/api.json ttlHours: 12 timeoutMs: 30000 minShare: 0.5 # 主流声明要有多高的一致度才被采信 ``` | 字段 | 含义 | |---|---| | `enabled` | 总开关。关闭则不请求、不写入。 | | `routes` | 空则处理全部路由。非空即为白名单:写上路由名表示纳入,在其下 `enabled: false` 表示排除。 | | `syncOnLoad` | profile 加载时同步一次。 | | `intervalMinutes` | 另外按此间隔同步。默认 `0`,即只在加载时同步。 | | `requestTimeoutMs` | 单次模型列表请求的超时。 | | `dryRun` | 只计算并打印结果,不写盘。建议先从这里开始。 | | `modelsDev.enabled` | 对端点未公布档位的模型,去 models.dev 查。 | | `modelsDev.url` | 数据集地址。默认是 models.dev 全量数据,约 5 MB。 | | `modelsDev.ttlHours` | 取到的数据集在内存中复用的时长。 | | `modelsDev.timeoutMs` | 该次抓取的超时。 | | `modelsDev.minShare` | 主流声明需要的一致度。`1` 要求全体一致(此时 `kimi-k3` 会被跳过),`0` 接受任何相对多数,`0.5` 接受过半。 | 加载时的同步是**不等待**的,所以网关不可达永远不会拖慢 profile。变更通过 `loader/volatile-update` 触发同步——正是 `dsh-llm-pi-ai` 重注册路由时听的同一个事件——因此在 Models 页新增渠道或模型会立刻被捕捉,不需要重启。每个触发器都先算差异:没有新内容的同步不写任何东西。 ### 档位从哪来 1. **路由自己的端点**,当它声明了档位时。它描述的就是正在配置的这个网关,所以它的答案优先。 2. **models.dev**,用于端点没有描述的模型。同一个模型名在那里被几十家提供商声明成不同样子——写这段时 `deepseek-v4.1-flash` 在 75 条声明里有 15 种不同档位集合——所以必须有个规则来挑。提供的档位是**主流声明**:最多声明完全一致的那个集合,但要求至少 `modelsDev.minShare` 比例的声明认同、且至少两家提供商这么说。相对多数偏弱或出现并列时,退回**交集**;因此调低 `minShare` 只会增加某些声明已经点名过的档位。 两个细节让这条规则在实践中成立: - **先丢掉"不思考"档位再比较。** `none`、`off`、`disabled`、`false` 表达的是"不要推理",而选择器的 Default 行已经通过"不发参数"表达了同一件事。计入它们既会重复那一行,也会让一个只记录 "off" 的转售商否决模型的所有真实档位。丢掉它们还会把"仅在是否提及它上有差异"的声明合并计数——这正是 `deepseek-v4.1-flash` 解得 `low, high, max` 的原因,而这恰好是另一个无关网关(WorkBuddy)为同名模型权威声明的集合。 - **偏弱或并列的相对多数一律拒绝。** 少给档位可以补救;上游不接受的档位会在请求时硬失败。两条规则都定位不到的模型保持不动,所以即使开了兜底,仍有模型需要人工声明。 ## 它会写入什么 之前: ```yaml providers: workbody: apiKeyEnv: WORKBODY_API_KEY api: openai-completions baseURL: http://127.0.0.1:8788/v1 models: - id: hy3 contextWindow: 192000 maxTokens: 64000 ``` 在网关为 `hy3` 公布 `reasoning_efforts: [low, high]` 的情况下同步之后: ```yaml models: - id: hy3 contextWindow: 192000 maxTokens: 64000 reasoningEfforts: low: low high: high ``` 该行的其它键——`apiKeyEnv`、`api`、`baseURL`、`compat`、`headers`、本次未触碰的路由,以及每个模型上未改动的字段——一律按原样保留。写入经配置编辑器完成:会先校验、与 profile 变更及热重载串行、原子替换,Host 拒绝时回滚。 ### 档位名 已知档位按 DSH 的升序输出(`off, minimal, low, medium, high, xhigh, max`),网关命名的其它档位排在它们之后并按字母序。值就是档位自己的拼写。 **"不思考"档位会被归一化。** `off`、`none`、`disabled`、`false` 会被写成**无值的 `off:`**,`pi-ai` 对它派发的行为是"完全不发参数"。字面量线值被刻意排除:`reasoning_effort: "off"` 不是同一个请求,实测有网关对它返回 `400`。 ## 它不会做什么 - **绝不增删模型。** 只标注 `models` 里已有的条目,目录范围仍然由你决定。 - **绝不写默认档位。** DSH 的 `defaultEffort` 是路由级单值,而同路由各模型档位并不一致——写一个会让所有不支持的模型以 `UNSUPPORTED_REASONING_EFFORT` 失败。选择器里的 "Default" 行含义是"不发参数",也就是模型自身的默认行为。 - **绝不猜。** 端点没提到的模型不动;`reasoningEfforts: false` 视为你显式的钉死,同样不动。 - **不替换 `dsh-llm-pi-ai`。** 传输、兼容怪癖、图片处理仍归适配器。 一旦某路由进入范围,其模型的 `reasoningEfforts` 块就由本插件托管——这是"不必手写"的代价。 ## 失败行为 **路由之间彼此独立**。每条路由自己解析出来的结果都会被写入;解析不出东西的路由保持原样。因此一个不听话的网关无法压制其它路由的工作——你一旦配置了两条路由,这一点就很重要。 两个来源都是尽力而为,且都不会让同步失败: - 端点不可达、拒绝凭据、或返回无法解析的内容时,会记一条提示,并且该路由转而查询 models.dev。 - models.dev 抓取失败会记一条提示,端点得来的结果照常写入。 每次同步的结果都会记录为 `up to date`、`annotated N model(s)` 或两者都没做到的原因。没有声明 `baseURL` 或没有 models 列表的路由会被跳过并记一条提示:它经由 pi-ai 内置目录到达,那里的档位本插件看不到、也不允许收窄。 写入会触发热重载,从而重新挂载本插件。`runSync` 先计算下一份配置并比较,所以第二轮发现无内容可写,循环到此为止;在途标记覆盖重叠的重载。 ## 排错 | 现象 | 原因 | |---|---| | `... answered 401; check the route's credential` | `apiKeyEnv` 指名的引用在凭据存储里没有值。 | | `no "llm-pi-ai" configuration row is addressable` | profile 里没有 `llm-pi-ai` 行,或它的 id 不唯一。 | | `route "x" declares no baseURL` | 给该路由加 `baseURL`,或给它的模型条目加。 | | `route "x" declares no models list` | 该路由依赖 pi-ai 内置目录,没有可标注的东西。 | | 完全没有日志 | 检查该行的 `enabled`,以及 bundle 是否被选进 profile 的 `dsh.profile.bundles`。 | | 安装时报 `incompatible-version` | DSH 运行时与本包的 peer 范围不符。读完风险提示后显式授权该组合——`plugin_manager` 的 `allow-version`,或 `dsh plugin --profile allow-version --dsh-version --accept-risk`。豁免不会随 DSH 升级继承。 | ## 开发 ```bash pnpm install pnpm typecheck # tsc --noEmit pnpm test # node --test "tests/*.test.mjs" pnpm build # esbuild -> lib/index.js ``` `src/listing.ts`、`src/merge.ts`、`src/options.ts`、`src/routing.ts`、`src/sync.ts` 不 import 任何 Host 包,因此测试套件可以直接驱动它们并注入替身,不需要 Host,也不需要网络。`src/config.ts` 是唯一加载 peer 的模块。 运行时唯一的依赖是 `@deepseek-ai/schemastery`,声明在 `peerDependencies` 里。它在产物中保持外部化,且**刻意不写进 `dependencies`**:DSH 会把插件 `peerDependencies` 里列出的包名路由到自己安装的副本,而打包或本地再装一份会让同一个进程里出现两个 schema 构建器。 ## 许可证 MIT