@leaf233/dsh-model-gateway
Exposes the models already configured in DeepSeek Harness through an OpenAI-compatible gateway, so other local projects can call them with any standard OpenAI client. Multiple provider candidates share one named group with ordered, balanced or random dispatch; upstream failures before any output swi
Install
dsh plugin --profile web add @leaf233/dsh-model-gateway
Sandbox test
This plugin has not been tested yet. Submit a test →
README
模型中转站 (@leaf233/dsh-model-gateway)
给 DeepSeek Harness 挂一个 OpenAI 兼容的 /v1 接口,把 harness 里已注册的大模型(比如 @tnnevol/dsh-codebuddy 里的 CodeBuddy 模型)反代出去,其他项目用标准 OpenAI 客户端就能调用。
在 设置 → 模型中转站 里可以看到所有接口地址、分组、创建和管理 API 密钥、开关鉴权、查看可用模型。
为什么不是"再登一次"
这个插件不碰任何凭据。它自己不会读写token等,而是把请求交给 DSH 的 llm 服务转发——也就是直接复用已注册的 provider 适配器。
这是因为一些事实性存在的问题:CodeBuddy 的 refresh token 会轮换,而且 @tnnevol/dsh-codebuddy 内部用单飞(single-flight) 保证并发刷新只发生一次。如果本插件自己再建一个 session,两次刷新会互相作废,结果是 Web 界面里的登录被踢掉。走 ctx.llm 就不可能出现这种情况。
由此还顺带复用了上游插件的账号故障转移、额度感知、图片序列化和模型目录。
本仓库是 qilin-zhu/dsh-model-relay 的 fork,包名@leaf233/dsh-model-gateway。上游 npm 包dsh-model-relay(0.1.1)与本 fork 无关、也不兼容0.1.2-rc.1,见下文「安装」。
DSH 版本支持
0.1.2-rc.1、0.1.7-alpha.1+与0.2.0-rc.1+为本 fork 新增的支持范围,0.1.2-rc.1需要下面三处修复;0.1.1及更早不在声明范围内。0.2.0上的设置页图标做了跨版本回退,0.1.5–0.2.0 均可正常渲染
本仓库的 package.json 声明:
"engines": { "dsh": "0.1.2-rc.1 || ^0.1.5-rc.2 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || ^0.2.0-rc.1" }
六个 @deepseek-ai/* peer 依赖同样带上这五段范围。
0.1.2-rc.1 上必须的三处修复
上游 0.1.1 的代码在 0.1.2-rc.1 上装得上但跑不起来,本 fork 修的正是这三处:
1. 系统提示词:0.1.2-rc.1 的 @deepseek-ai/dsh-llm 不再导出 createSystemMessage,MessageSourceMap 里也没有 system。原代码的顶层命名导入会直接抛 SyntaxError,插件完全加载不了。正确形状是请求级的 options.system(GenerateOptions.system),适配器会把它映射到 provider 自己的 system slot。
2. Tag 组件:dsh-client-ui-primitives 在 0.1.2-rc.1 里不导出 Tag,primitives.Tag 是 undefined,渲染设置页时抛错、整个区块挂不上。改用实际存在的 Pill(注意它接的是 active,不是 tone)。
3. connection patch:见上文「配置」里的说明,已删除。
compatibility.json 里的 dshPluginApi.version 也改了,但那是惰性元数据——DSH 运行时不读这个字段,真正起作用的是 package.json 的 engines.dsh 和 peer 范围。
安装
命令行安装:
# 从 npm 安装 dsh plugin --profile web add @leaf233/dsh-model-gateway # 本地开发(file: 链接) dsh plugin --profile web add /绝对路径/relay-upstream
装完重启 Web profile,服务地址是:
http://127.0.0.1:3080/v1
这个 fork 用@leaf233作用域名,不是上游的dsh-model-relay。
npm 上那个不带 scope 的dsh-model-relay属于上游作者(maintainerjarvistop,仓库qilin-zhu/dsh-model-relay),本 fork 无权发布它,而且它在0.1.2-rc.1上会直接崩(详见下文「DSH 版本支持」)。
所以如果需要版本支持或上游不一样的功能,需要使用 @leaf233/dsh-model-gateway(本 fork)。
注意区分三个名字
| 名字 | 值 | 用途 |
|---|---|---|
| 包名 |@leaf233/dsh-model-gateway| npm 安装、bundles列表 |
| patch 行的id|dsh-model-gateway| 你在自己 profile 里覆盖配置时的锚点 |
| provider id |dsh-model-gateway| DSH 模型选择器里的分组命名空间(<provider>_<模型>) |
只有包名带 scope。id 和 provider id 保持不带 scope,所以已有的配置覆盖和分组名不受影响。
用法
任意 OpenAI 客户端都可以,base_url 指向上面的地址。
鉴权是可选的:还没创建密钥、也没在配置里写固定密钥时,接口对所有人开放(随便填一个 sk-xxx 或留空都行)。创建第一个密钥后自动开始校验。
from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:3080/v1", api_key="sk-dshgw-...")
resp = client.chat.completions.create(
model="codebuddy_deepseek-v4.1-flash",
messages=[{"role": "user", "content": "你好"}],
)
print(resp.choices[0].message.content)
curl http://127.0.0.1:3080/v1/models
curl http://127.0.0.1:3080/v1/chat/completions \
-H 'content-type: application/json' \
-d '{"model":"codebuddy_deepseek-v4.1-flash","messages":[{"role":"user","content":"你好"}]}'
支持的接口:
方法 · 路径 · 说明
GET · /v1/models · 列出所有已注册 provider 的模型,以及你的模型分组
POST · /v1/chat/completions · 对话,支持 stream: true
请求体与 OpenAI Chat Completions 一致:model、messages、stream、tools、tool_choice、temperature、max_tokens、stop、reasoning_effort。支持 role: "tool" 的工具结果回传、assistant 的 tool_calls 回放、以及 image_url 图片输入(data URL 或远程 URL)。
流式响应是标准 SSE,以 data: [DONE] 结束。
模型分组
分组 = 一组模型的候选列表 + 一套调度方式。 给这组模型起个名字,之后用这个名字调用,网关按调度方式决定先试谁,第一个能应答的就被采用。
默认就是"按顺序逐个尝试",也就是"额度轮换"最直接的用法:把同一个模型在不同供应商上的实例排成一组,一个用不了了自动落到下一个。
# 直接调用分组名
resp = client.chat.completions.create(
model="fast-chat",
messages=[{"role": "user", "content": "你好"}],
)
在 设置 → 模型中转站 → 模型分组 里创建和管理,用 ↑ ↓ 调候选顺序。
调度方式
设置页提供四个预设:
预设 · 行为
顺序(默认) · 按候选列表从上到下依次尝试,遇到不可用就顺延。
均衡 · 每次请求轮换起点,把负载摊到所有候选上。
随机 · 每次请求打乱顺序。
顺序 + 限流重试 · 按顺序尝试;某条腿遇到限流(429)时在同一条腿上重试若干次,再顺延到下一个。
存储上其实是两个正交的字段:strategy(sequential / round-robin / random)决定顺序,retry429(0–3)决定同一条腿重试几次。上面四个预设是这两者里最常用的四个组合,不是全部——存储本身能表达 12 种,比如"均衡 + 重试"(round-robin + retry429: 1)可以直接写进 model-relay-groups.json,只是设置页暂时没有对应的按钮。分成两个字段而不是一个枚举,就是为了让这些组合不必为每种顺序各复制一份重试逻辑。
均衡和随机都是完整排列,不是子集。 被排到后面的候选仍然是回退链的一部分,所以任何调度方式都不会缩短分组的兜底能力。
⚠️ 均衡和随机会牺牲会话粘性。 顺序模式下同一个会话始终落在同一条腿上,上游的 prompt cache 因此命中;换成均衡或随机后,多轮对话会在供应商之间跳,prompt cache 基本失效,成本和首字延迟都会上升。另外不同供应商的 tokenizer 不同,同一段文本的 token 计数会漂移,usage 对账会对不上。并非bug。
在 DSH 本体里也能用
本插件会把自己注册成一个名为 dsh-model-gateway 的 DSH provider,它的"模型目录"就是你的分组列表。打开 设置 → 模型,在 dsh-model-gateway 下面就能像选普通模型一样选到分组名。
分组名同样出现在 /v1/models 里,所以外部客户端和 DSH 两边都能用。
DSH 的模型选择器是全局的——注册之后,所有会话都能选到这些分组。
底层模型仍然可以直接调用(codebuddy_deepseek-v4-flash这种全名照常可用)。想让外部只看到分组,把providers配成[dsh-model-gateway]即可——但那是对外的过滤,不影响 DSH 界面里能看到什么。
回退只在"还没有任何输出"之前发生
唯一需要理解的行为约束:
- 上游还没吐出任何内容就失败(连不上、凭证失效、上游报错)→ 自动换下一个候选,客户端完全察觉不到。
- 上游已经吐出内容之后再断 → 不换,流被截断。
第二条不是bug:已经吐出去的内容客户端已经收到了,此时换腿会造成重复输出。所以分组能兜住"这个供应商现在用不了",但兜不住"响应写到一半断了"。
"吐出内容"的判定见上面的排查小节——只有 usage 分片不算。
限流重试(retry429)
开启后,某条腿遇到限流会在同一条腿上重试,而不是立刻换下一条腿。三条约束:
- 只重试限流。 判据是失败码
RATE_LIMIT,不是 HTTP 状态。因为statusForCode把RATE_LIMIT、QUOTA、QUOTA_EXCEEDED全都映射成 429,但额度用尽重试也没用——再问一次还是同一个答案,只是白花调用方的时间
…(truncated)
Rendered from the README published on npm. Formatted only, not edited.
Versions
Published on npm::5,最后更新 2026-11-04