dsh-clinepass-pin
给 DeepSeek Harness 的 cline-pass 路由硬钉一个上游 Provider(默认第一方 deepseek)。
做法是在发往网关的 JSON body 里加一个字段:
"providerOptions": { "gateway": { "only": ["deepseek"] } }
配套 dsh-clinepass 使用:那个插件只观察网关真实用了谁,本插件负责让它一直是 DeepSeek。
它解决什么
api.cline.bot 是个多上游网关。对日期快照 cline-pass/deepseek-v4.1-flash-1017,它默认在十几个渠道里挑一个(deepseek、pareto、fireworks、alibaba、baseten …),同一个进程里几分钟就能从 pareto 飘到 deepseek;稳定别名 cline-pass/deepseek-v4.1-flash 甚至可能落到匿名回退网 openai-compatible-private(contributor 档:数据可能用于训练、无 ZDR)。
实测(2026-10-19,同一把 key):
| 请求 | finalProvider | fallbacksAvailable |
|---|
| 不钉 | deepseek,但抓到过 pareto | 14 |
only: ["deepseek"] | deepseek | 0 |
only: ["__no_such_upstream__"] | — | 500 No available providers match the 'only' filter |
负控制那一行说明:1017 这条快照上网关真的执行 only。稳定别名则无视它(only 无效),所以 models 里别写别名。
为什么不走官方那条路
官方做法是 profile 里写 compat.vercelGatewayRouting.only,再把 pi-ai 的 COMPLETIONS_COMPAT_GATE.vercelGatewayRouting 从 "withhold" 改成 "offer"。在 Desktop 版上这条路是死的:
- 门禁代码在
resources/app.asar/dsh/node_modules/@deepseek-ai/dsh-llm-pi-ai/lib/index.js 里,而 asar header 对每个文件都带 SHA256 integrity,就地改字节过不了完整性校验,且每次应用更新都会被覆盖;
- 随包脚本
patch-gateway-routing.mjs 找的是 <DSH_HOME>/profiles/<profile>/node_modules/@deepseek-ai/dsh-llm-pi-ai/...,Desktop 安装里这个路径不存在(profiles/node_modules 下的那份只是指向全局 npm 安装的 junction,不是运行中 app 加载的那份)。
所以本插件改在比配置门禁更低一层的 globalThis.fetch 上 —— 正好是 dsh-clinepass 观察用的同一个缝。
安装
环境:dsh ≥ 0.2.0-rc.2、node ≥ 20。装完要重启 dsh。
方式 A:从 GitHub 装
dsh plugin --profile desktop add github:liujuntao123/dsh-clinepass-pin
它声明了 dsh.bundle.patch,所以包被追加进 profile 的 dsh.profile.bundles 后配置行随包生效,不用手写。
挂载顺序:如果同时用 dsh-clinepass,请把这行挪到 dsh-clinepass 之前。顺序决定谁在最外层:本插件先装、dsh-clinepass 后装,后者才继续 owns() 着 globalThis.fetch,状态文件里 hook 才会是 installed。反过来钉选照样生效,但 dsh-clinepass 的状态文件会写 hook: "foreign" 并告警。
方式 B:离线手装(不用 pnpm)
$prof = "$env:USERPROFILE\.dsh\profiles\desktop"
Copy-Item . "$prof\plugins\dsh-clinepass-pin" -Recurse
New-Item -ItemType Junction -Path "$prof\node_modules\dsh-clinepass-pin" -Target "$prof\plugins\dsh-clinepass-pin"
然后在 $prof\package.json 的 dsh.profile.bundles 里加一行 "dsh-clinepass-pin"(同样建议排在 dsh-clinepass 之前),重启。
配置
| 键 | 默认 | 作用 |
|---|
upstream | https://api.cline.bot | 网关 origin;路由自己拼 /api/v1/chat/completions |
models | ['cline-pass/deepseek-v4.1-flash-1017'] | 只有这些 model id 会被加钉 |
only | ['deepseek'] | 允许的上游列表,必须非空(空列表会让网关 500) |
statusFile | $DSH_HOME/dsh-clinepass-pin-status.json | 运行证据;false 关掉 |
log | true | 宿主日志 |
改配置写在 profile 的 cordis.patch.yml 里(patch 是整行替换 config):
- id: clinepass-pin
config:
models: [cline-pass/deepseek-v4.1-flash-1017]
only: [deepseek]
行为边界
- 只处理
POST <upstream>/api/v1/chat/completions,且 body 是 JSON 字符串、model 在 models 里;其它请求原样透传。
- 只加
providerOptions.gateway.only;已存在非空 only 时不动(让 profile 侧 compat 优先)。已有的 gateway.order 会保留。
- 任何解析/形状异常都回落成原请求 —— 它不该成为一次对话失败的原因(
counters.errors 可见)。
- 不缓冲流、不改 headers、不重试、不加请求。
验证
npm test # 离线,不需要网络也不需要 dsh
跑起来之后:
Get-Content "$env:USERPROFILE\.dsh\dsh-clinepass-pin-status.json"
{ "hook": "installed", "counters": { "pinned": 12, "skippedRequestShape": 0, "errors": 0 } }
pinned 在涨 = 请求确实被改写了;hook 不是 installed = 有别的插件先占了 globalThis.fetch。
代价
硬钉就是硬钉:deepseek 不可用时请求直接失败(500),不会回落到 pareto / fireworks / 匿名网。想要「优先 deepseek、挂了再回落」得用网关的 gateway.order(本插件未暴露;only 是唯一实测被 1017 执行的形状)。
卸载
从 dsh.profile.bundles 去掉 dsh-clinepass-pin → 删 node_modules\dsh-clinepass-pin junction → 删插件目录 → 重启。
English
dsh-clinepass-pin pins the cline-pass route in DeepSeek Harness to a single upstream provider by adding providerOptions.gateway.only to the outgoing chat body. It exists because the documented route — a model-level compat.vercelGatewayRouting.only plus flipping pi-ai's COMPLETIONS_COMPAT_GATE — cannot be applied to the desktop build, where that gate lives inside an integrity-checked app.asar.
It is the write-side companion to dsh-clinepass, which only observes. It touches nothing but the JSON body of matching chat requests, never buffers a stream, never retries, and falls through to the untouched request on any surprise. npm test runs offline.
Trade-off: a hard pin means requests fail (500) when the pinned upstream is unavailable instead of falling back.
License
MIT