dsh-gateway-provider 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -6
- package/docs/README.zh.md +230 -0
- package/index.js +46 -22
- package/lib/pi-bridge.js +17 -2
- package/package.json +19 -9
package/README.md
CHANGED
|
@@ -50,14 +50,13 @@ re-deploy.
|
|
|
50
50
|
1. Install the plugin into your profile (published on npm):
|
|
51
51
|
|
|
52
52
|
```bash
|
|
53
|
-
dsh plugin --profile web add dsh-gateway-provider
|
|
53
|
+
dsh plugin --profile web add dsh-gateway-provider # latest
|
|
54
|
+
dsh plugin --profile web add dsh-gateway-provider@<version> # pinned
|
|
54
55
|
```
|
|
55
56
|
|
|
56
|
-
`dsh plugin add` forwards to `pnpm add` in the profile directory
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
the suffix to track main). The bundle patch (`cordis.patch.yml`) then
|
|
60
|
-
mounts the `llm-newapi` loader row automatically — no manual patch editing.
|
|
57
|
+
`dsh plugin add` forwards to `pnpm add` in the profile directory.
|
|
58
|
+
The bundle patch (`cordis.patch.yml`) then mounts the `llm-newapi`
|
|
59
|
+
loader row automatically — no manual patch editing.
|
|
61
60
|
|
|
62
61
|
2. Store your gateway key in `$DSH_HOME/.credentials.yaml` (mode 0600,
|
|
63
62
|
hot-reloaded):
|
|
@@ -126,6 +125,21 @@ The **Gateway Models** page exposes the same operations with a UI: search,
|
|
|
126
125
|
hidden/custom filters with counts, a per-model override editor (placeholders
|
|
127
126
|
show discovered values), connection test, and save/cancel semantics.
|
|
128
127
|
|
|
128
|
+
> ⚠️ **Hand-editing YAML?** `settings.yaml` is parsed with YAML 1.2 core
|
|
129
|
+
> semantics: an unquoted `true` / `false` (any casing) becomes a boolean and
|
|
130
|
+
> an unquoted digit run becomes a number. Every plain string field of the
|
|
131
|
+
> section (`id`, `name`, `label`, `baseURL`, `apiKeyEnv`, `modelsUrl`,
|
|
132
|
+
> `userId`, `reasoningLevels` / `excludePatterns` / `endpointPriority`
|
|
133
|
+
> entries, …) tolerates the coerced form, but the cleanest habit is quoting
|
|
134
|
+
> intent-significant tokens — write `reasoningLevels: ["off", "low",
|
|
135
|
+
> "high"]`, not `reasoningLevels: [off, low, high]` — and never `false`
|
|
136
|
+
> where a level name is meant (it survives as the junk level `"false"` and
|
|
137
|
+
> is silently dropped from the picker). A structurally invalid section
|
|
138
|
+
> (objects where strings belong, etc.) is refused at registration: the
|
|
139
|
+
> namespace stays unregistered and every settings write from the web UI
|
|
140
|
+
> then answers `settings-rejected: settings namespace "llm-newapi" is not
|
|
141
|
+
> registered`.
|
|
142
|
+
|
|
129
143
|
## Configuration reference
|
|
130
144
|
|
|
131
145
|
Section `llm-newapi:` of `$DSH_HOME/settings.yaml`. The flat fields
|
|
@@ -152,6 +166,7 @@ the `gateways` array support most of the same fields per gateway
|
|
|
152
166
|
| `excludePatterns` | image/speech/embed/… | Regex patterns excluding models from the picker |
|
|
153
167
|
| `endpointPriority` | `["openai-response","anthropic","openai","gemini"]` | Wire-format preference order (first match wins per model) |
|
|
154
168
|
| `userId` | `1` | `New-Api-User` header for the management API |
|
|
169
|
+
| `headers` | – | Extra HTTP headers (name → value) sent with every provider request; attribution headers (`user-agent`) are filtered out |
|
|
155
170
|
| `maxTokens` | `32768` | Output cap fallback when models.dev lacks data |
|
|
156
171
|
| `defaultContextWindow` | `128000` | Context window fallback when models.dev lacks data |
|
|
157
172
|
| `streamIdleTimeoutMs` | `300000` | Stream idle watchdog |
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# dsh-gateway-provider
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Luck9Star/dsh-gateway-provider/actions/workflows/gitleaks.yml)
|
|
4
|
+
|
|
5
|
+
> English: [README.md](../README.md)
|
|
6
|
+
|
|
7
|
+
把任意 LLM 网关 —— **newapi、LiteLLM、Higress 或任何 OpenAI 兼容端点** —— 接入
|
|
8
|
+
DeepSeek Harness。插件自动发现网关的模型列表,用 [models.dev](https://models.dev)
|
|
9
|
+
的真实参数补齐每个模型,并通过 pi-ai SDK 以模型原生协议(OpenAI / Anthropic /
|
|
10
|
+
Gemini)分发请求。
|
|
11
|
+
|
|
12
|
+
## 为什么需要它
|
|
13
|
+
|
|
14
|
+
DeepSeek Harness 自带的适配器(`llm-deepseek`、`llm-pi-ai`)各自只服务一个官方
|
|
15
|
+
provider。如果你的模型都在网关后面,就得手工维护一份静态模型清单,上下文窗口、
|
|
16
|
+
输出上限全靠猜。
|
|
17
|
+
|
|
18
|
+
本插件直接把网关挂进来:**N 个网关 = N 个 provider 路由,零静态清单。** 每个模型
|
|
19
|
+
带着真实参数,网关侧加模型即可生效——无需重新部署。
|
|
20
|
+
|
|
21
|
+
## 你能得到什么
|
|
22
|
+
|
|
23
|
+
- **多网关** — 默认 `newapi` 路由 + 每个额外网关一条 `gateway:<id>` 路由,
|
|
24
|
+
各自独立 catalog 缓存。
|
|
25
|
+
- **自动发现** — 首选 `GET {base}/v1/models`(含每个模型支持的请求格式),
|
|
26
|
+
管理 API 兜底。
|
|
27
|
+
- **真实参数** — models.dev 补齐上下文窗口、输出上限、推理等级、发布日期;
|
|
28
|
+
配置默认值只是兜底。
|
|
29
|
+
- **全协议覆盖** — 每个模型路由到自己的协议(`openai-completions` /
|
|
30
|
+
`openai-responses` / `anthropic-messages` / `google-generative-ai`),
|
|
31
|
+
无手写 SSE、无手写请求序列化。
|
|
32
|
+
- **图形设置页** — **设置 → 网关模型**:按模板添加/编辑网关、测试连接、
|
|
33
|
+
隐藏/覆写/自定义添加模型——不用碰 YAML。
|
|
34
|
+
- **干净的选择器** — 按发布日期新到旧排序、只保留对话模型、正则剔除。
|
|
35
|
+
|
|
36
|
+
## 环境要求
|
|
37
|
+
|
|
38
|
+
- 装有 profile 的 DeepSeek Harness(`dsh`)
|
|
39
|
+
- 一个可达网关:newapi / LiteLLM / Higress / 任意 OpenAI 兼容端点
|
|
40
|
+
- 该网关的 API Key
|
|
41
|
+
|
|
42
|
+
## 快速开始
|
|
43
|
+
|
|
44
|
+
1. 把插件装进 profile(已发布到 npm):
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
dsh plugin --profile web add dsh-gateway-provider # 最新版
|
|
48
|
+
dsh plugin --profile web add dsh-gateway-provider@<version> # 钉住版本
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`dsh plugin add` 会在 profile 目录里转发执行 `pnpm add`。bundle 补丁
|
|
52
|
+
(`cordis.patch.yml`)随后自动挂载 `llm-newapi` 加载行——无需手工改 patch。
|
|
53
|
+
|
|
54
|
+
2. 把网关 key 写入 `$DSH_HOME/.credentials.yaml`(权限 0600,热加载):
|
|
55
|
+
|
|
56
|
+
```yaml
|
|
57
|
+
NEWAPI_API_KEY: sk-REPLACE_WITH_YOUR_KEY
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
或在启动环境中导出 `NEWAPI_API_KEY`。
|
|
61
|
+
|
|
62
|
+
3. 重启 profile,打开 **设置 → 网关模型**。
|
|
63
|
+
|
|
64
|
+
**成功的样子:** 网关出现并带"已同步 N 个模型"徽标,全部对话模型可选、
|
|
65
|
+
上下文窗口是真实值。基础地址解析顺序:`llm-newapi.baseURL` 设置 →
|
|
66
|
+
`NEWAPI_BASE_URL` / `NEWAPI_API_URL` 环境变量 → 公共默认
|
|
67
|
+
`https://api.newapi.ai`。
|
|
68
|
+
|
|
69
|
+
## 添加更多网关
|
|
70
|
+
|
|
71
|
+
额外网关写在 `gateways` 数组——每个成为独立的 `gateway:<id>` provider 路由:
|
|
72
|
+
|
|
73
|
+
```yaml
|
|
74
|
+
llm-newapi:
|
|
75
|
+
baseURL: https://your-newapi-instance.com
|
|
76
|
+
gateways:
|
|
77
|
+
- id: litellm-prod
|
|
78
|
+
label: LiteLLM 生产
|
|
79
|
+
baseURL: https://litellm.example.com
|
|
80
|
+
apiKeyEnv: LITELLM_API_KEY
|
|
81
|
+
flavor: litellm # 表单模板:newapi / litellm / higress / openai-compatible / custom
|
|
82
|
+
|
|
83
|
+
# 完全自定义网关:各协议写完整端点地址,不共享基础地址;
|
|
84
|
+
# 留空的协议不启用。
|
|
85
|
+
- id: edge
|
|
86
|
+
label: 边缘网关
|
|
87
|
+
flavor: custom
|
|
88
|
+
openaiURL: https://edge.example.com/openai/v1/chat/completions
|
|
89
|
+
responsesURL: https://edge.example.com/openai/v1/responses
|
|
90
|
+
anthropicURL: https://edge.example.com/anthropic/v1/messages
|
|
91
|
+
apiKeyEnv: EDGE_API_KEY
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
也可以在设置页里用同样的模板添加网关,不必编辑 YAML。
|
|
95
|
+
|
|
96
|
+
## 按网关控制模型清单
|
|
97
|
+
|
|
98
|
+
隐藏模型、修正错误元数据、添加网关未列出的模型:
|
|
99
|
+
|
|
100
|
+
```yaml
|
|
101
|
+
llm-newapi:
|
|
102
|
+
models:
|
|
103
|
+
- id: glm-5.2
|
|
104
|
+
disabled: true # 从选择器隐藏
|
|
105
|
+
- id: glm-5.2-highspeed
|
|
106
|
+
contextWindow: 1000000 # 覆盖发现值
|
|
107
|
+
protocol: openai # 强制协议(openai/anthropic/gemini/openai-response)
|
|
108
|
+
- id: my-internal-model # 自定义模型(网关未列出)
|
|
109
|
+
name: 我的内测模型
|
|
110
|
+
contextWindow: 200000
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**网关模型**设置页提供同样的操作:搜索、带计数的已隐藏/自定义筛选、逐模型覆写
|
|
114
|
+
编辑器(placeholder 显示发现值)、连接测试、保存/取消语义。
|
|
115
|
+
|
|
116
|
+
> ⚠️ **手工编辑 YAML?** `settings.yaml` 按 YAML 1.2 core 语义解析:不带引号的
|
|
117
|
+
> `true` / `false`(任意大小写)会变成布尔值,不带引号的数字串会变成数值。本节
|
|
118
|
+
> 所有纯字符串字段(`id`、`name`、`label`、`baseURL`、`apiKeyEnv`、`modelsUrl`、
|
|
119
|
+
> `userId`,以及 `reasoningLevels` / `excludePatterns` / `endpointPriority` 的
|
|
120
|
+
> 条目……)都可容忍这些被强转的形式,但最稳妥的习惯是给有含义的 token 加引号
|
|
121
|
+
> ——写 `reasoningLevels: ["off", "low", "high"]`,而不是
|
|
122
|
+
> `reasoningLevels: [off, low, high]`——更不要在该写级别名的地方写 `false`(它会
|
|
123
|
+
> 以垃圾级别 `"false"` 存活,并被静默排除出选择器)。结构上非法的分节(比如字符
|
|
124
|
+
> 串位置出现对象)会在注册时被拒绝:namespace 保持未注册状态,Web 设置页的每次
|
|
125
|
+
> 写入都会得到
|
|
126
|
+
> `settings-rejected: settings namespace "llm-newapi" is not registered`。
|
|
127
|
+
|
|
128
|
+
## 配置参考
|
|
129
|
+
|
|
130
|
+
`$DSH_HOME/settings.yaml` 的 `llm-newapi:` 节。扁平字段(`baseURL` /
|
|
131
|
+
`apiKeyEnv` 等)构建默认 `newapi` 路由;`gateways` 数组里的每个网关支持
|
|
132
|
+
大部分同名字段(`label` / `apiKeyEnv` / `flavor` / `catalogMode` /
|
|
133
|
+
`endpointPriority` / …)。
|
|
134
|
+
|
|
135
|
+
| 字段 | 默认 | 说明 |
|
|
136
|
+
|------|------|------|
|
|
137
|
+
| `label` | `NewAPI` | 默认网关路由的显示名 |
|
|
138
|
+
| `apiKeyEnv` | `NEWAPI_API_KEY` | 凭据引用(环境变量名) |
|
|
139
|
+
| `baseURL` | env `NEWAPI_BASE_URL` / `NEWAPI_API_URL` → `https://api.newapi.ai` | 网关地址(配置协议 URL 后不再使用) |
|
|
140
|
+
| `flavor` | `newapi` | 网关模板:`newapi` / `litellm` / `higress` / `openai-compatible` / `custom` |
|
|
141
|
+
| `openaiURL` | – | OpenAI 兼容完整端点地址(自定义模板;留空 = 不启用) |
|
|
142
|
+
| `responsesURL` | – | Responses 完整端点地址(自定义模板;留空 = 不启用) |
|
|
143
|
+
| `anthropicURL` | – | Anthropic messages 完整端点地址(自定义模板;留空 = 不启用) |
|
|
144
|
+
| `modelsUrl` | `https://models.dev/models.json` | models.dev 数据源(`file:` URL 可离线) |
|
|
145
|
+
| `useModelsDev` | `true` | 是否用 models.dev 参数增强模型 |
|
|
146
|
+
| `extendedReasoningLevels` | `false` | 未知模型推理等级兜底放宽到 off~max(默认仅 off/low/medium/high) |
|
|
147
|
+
| `sortModelsByRelease` | `true` | 选择器按发布日期新到旧排列(未知日期排最前) |
|
|
148
|
+
| `catalogMode` | `auto` | 模型列表来源:`auto` / `v1` / `management` |
|
|
149
|
+
| `catalogTtlMs` | `1800000` | 模型列表缓存时长 |
|
|
150
|
+
| `includeChatOnly` | `true` | 仅把对话模型放进选择器 |
|
|
151
|
+
| `excludePatterns` | image/speech/embed/… | 选择器剔除的正则列表 |
|
|
152
|
+
| `endpointPriority` | `["openai-response","anthropic","openai","gemini"]` | wire 格式优先级(每模型取首个匹配) |
|
|
153
|
+
| `userId` | `1` | 管理 API 的 `New-Api-User` 头 |
|
|
154
|
+
| `headers` | – | 随每个 provider 请求附带的额外 HTTP 头(名称 → 值);归属头(`user-agent`)会被过滤掉 |
|
|
155
|
+
| `maxTokens` | `32768` | 无 models.dev 数据时的输出上限兜底 |
|
|
156
|
+
| `defaultContextWindow` | `128000` | 无 models.dev 数据时的上下文兜底 |
|
|
157
|
+
| `streamIdleTimeoutMs` | `300000` | 流空闲看门狗 |
|
|
158
|
+
| `retryPolicy` | 标准重试 | 与 `llm-deepseek` 同构 |
|
|
159
|
+
|
|
160
|
+
## 工作原理
|
|
161
|
+
|
|
162
|
+
所有 wire 格式细节都委托给
|
|
163
|
+
[`@earendil-works/pi-ai`](https://www.npmjs.com/package/@earendil-works/pi-ai)
|
|
164
|
+
—— 官方 `dsh-llm-pi-ai` 适配器用的同一 SDK。每次请求:
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
harness GenerateOptions
|
|
168
|
+
→ toPiContext() lib/pi-bridge.js harness 消息 → pi-ai Context
|
|
169
|
+
→ models.streamSimple() pi-ai SDK 按每个模型的 `api` 分发协议
|
|
170
|
+
→ toStreamChunks() lib/pi-bridge.js pi-ai 事件 → harness chunk
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
每个发现的模型带一个 `api` 字段,由网关广告的 `supported_endpoint_types`
|
|
174
|
+
映射而来,遵循 `endpointPriority`;pi-ai provider 收到的是 api *映射表*,
|
|
175
|
+
所以每个模型路由到自己的协议实现。`sdkBaseURL()` 为 OpenAI 协议模型补
|
|
176
|
+
`/v1`,Anthropic/Google 的基础地址保持不变。
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
dsh-gateway-provider/
|
|
180
|
+
├── index.js # 插件入口:Config 校验、provider 注册、设置/凭据接线
|
|
181
|
+
├── cordis.patch.yml # dsh.bundle 补丁(经 `dsh plugin add` 自动挂载)
|
|
182
|
+
├── lib/ # adapter、pi-provider、pi-bridge、catalog、modelsdev、thinking、client
|
|
183
|
+
├── test/ # 冒烟(直连网关)+ 离线单元 + 设置页渲染
|
|
184
|
+
└── scripts/link.sh # 本地检出时链接 profile 的 node_modules
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## 从本地检出开发
|
|
188
|
+
|
|
189
|
+
1. `bash scripts/link.sh` — 把本包 `node_modules` 软链到 profile 的,
|
|
190
|
+
保证 `@deepseek-ai/*` 裸导入解析到 harness 进程使用的同一份模块实例
|
|
191
|
+
(单实例 `instanceof` 安全)。
|
|
192
|
+
2. 把检出注册为 profile 的 link 依赖并安装:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
cd "$DSH_HOME/profiles/web"
|
|
196
|
+
# package.json dependencies 追加:"dsh-gateway-provider": "link:/本包绝对路径/dsh-gateway-provider"
|
|
197
|
+
# 再把 "dsh-gateway-provider" 加进同一文件的 bundles 列表
|
|
198
|
+
pnpm install
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
3. 重启 profile。
|
|
202
|
+
|
|
203
|
+
> ⚠️ **不要**再往 profile 自己的 `cordis.patch.yml` 里插入 `id: llm-newapi`
|
|
204
|
+
> ——bundle 层已挂载,重复会导致启动时报 `duplicate loader entry id`。
|
|
205
|
+
|
|
206
|
+
客户端 bundle 改动刷新浏览器即生效;宿主侧改动需要重启 profile。
|
|
207
|
+
|
|
208
|
+
### 测试
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
node test/smoke.mjs # 直连网关:catalog / openai × 2 / 工具调用 / anthropic / gemini / custom-urls
|
|
212
|
+
node test/smoke.mjs --only custom-urls
|
|
213
|
+
node test/protocol-urls.mjs # 离线:URL 派生 + 网关解析单元测试
|
|
214
|
+
node test/client-render.mjs # 离线:设置页渲染树(中/英)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
冒烟测试凭据解析顺序:进程环境变量 → 插件目录 `.env` →
|
|
218
|
+
`$NEWAPI_ENV_FILE`(另有一个作者本地遗留兜底路径)。
|
|
219
|
+
|
|
220
|
+
## 安全说明
|
|
221
|
+
|
|
222
|
+
- API Key 只经 `$DSH_HOME/.credentials.yaml`(0600)或环境变量解析,
|
|
223
|
+
绝不写入日志、配置或对话;凭据字段在 Web UI 中以掩码编辑。
|
|
224
|
+
- 无运行时 `dependencies`;`peerDependencies` 复用 harness 已装的
|
|
225
|
+
`@deepseek-ai/*` 与 `@earendil-works/pi-ai`。提交在本地(pre-commit)
|
|
226
|
+
与 CI 双层接受 gitleaks 扫描把关。
|
|
227
|
+
|
|
228
|
+
## License
|
|
229
|
+
|
|
230
|
+
MIT
|
package/index.js
CHANGED
|
@@ -60,12 +60,30 @@ export const DEFAULT_MODELS_URL = "https://models.dev/models.json";
|
|
|
60
60
|
const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 600_000;
|
|
61
61
|
const DEFAULT_CATALOG_TTL_MS = 30 * 60 * 1000;
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* String field tolerant of hand-edited YAML scalars. `settings.yaml` is a
|
|
65
|
+
* user-editable document parsed with YAML 1.2 core semantics: an unquoted
|
|
66
|
+
* `true` / `false` / `True` / `FALSE` round-trips as a boolean and an
|
|
67
|
+
* unquoted digit run as a number, long before the value reaches a strict
|
|
68
|
+
* `z.string()` — and because the settings seam refuses to register a
|
|
69
|
+
* namespace whose stored section fails its schema, one such scalar anywhere
|
|
70
|
+
* in the section bricks the whole namespace (the web UI then reports every
|
|
71
|
+
* write as `settings-rejected: settings namespace "llm-newapi" is not
|
|
72
|
+
* registered`). Booleans and finite numbers coerce to their string form;
|
|
73
|
+
* anything else still fails loud with schemastery's own union diagnostic.
|
|
74
|
+
*/
|
|
75
|
+
const coercedString = () => z.union([
|
|
76
|
+
z.string(),
|
|
77
|
+
z.transform(z.boolean(), (value) => String(value)),
|
|
78
|
+
z.transform(z.number(), (value) => String(value)),
|
|
79
|
+
]);
|
|
80
|
+
|
|
63
81
|
/** Schema for one model-level override on a gateway (all fields optional). */
|
|
64
82
|
const ModelOverrideSchema = z.object({
|
|
65
83
|
/** Model id exactly as the gateway accepts it. */
|
|
66
|
-
id:
|
|
84
|
+
id: coercedString().required(),
|
|
67
85
|
/** Display name override. */
|
|
68
|
-
name:
|
|
86
|
+
name: coercedString(),
|
|
69
87
|
/** Hide this model from the picker. */
|
|
70
88
|
disabled: z.boolean(),
|
|
71
89
|
/** Force the wire protocol for this model (openai/anthropic/gemini/…). */
|
|
@@ -75,7 +93,7 @@ const ModelOverrideSchema = z.object({
|
|
|
75
93
|
/** Override the output-token cap. */
|
|
76
94
|
maxTokens: z.number().step(1).min(1),
|
|
77
95
|
/** Reasoning level set (e.g. ["off","low","medium","high"]). */
|
|
78
|
-
reasoningLevels: z.array(
|
|
96
|
+
reasoningLevels: z.array(coercedString()),
|
|
79
97
|
});
|
|
80
98
|
|
|
81
99
|
/** Gateway-type templates the settings UI offers (informational labels). */
|
|
@@ -88,49 +106,49 @@ const FLAVORS = ["newapi", "litellm", "higress", "openai-compatible", "custom"];
|
|
|
88
106
|
*/
|
|
89
107
|
const GatewaySchema = z.object({
|
|
90
108
|
/** Short stable id; becomes the provider route suffix (`gateway:<id>`). */
|
|
91
|
-
id:
|
|
109
|
+
id: coercedString().required(),
|
|
92
110
|
/** Human-readable gateway name. */
|
|
93
|
-
label:
|
|
111
|
+
label: coercedString(),
|
|
94
112
|
/** Gateway base URL (unused when protocol URL fields are set). */
|
|
95
|
-
baseURL:
|
|
113
|
+
baseURL: coercedString(),
|
|
96
114
|
/** Full chat-completions endpoint URL (custom template). */
|
|
97
|
-
openaiURL:
|
|
115
|
+
openaiURL: coercedString(),
|
|
98
116
|
/** Full Responses endpoint URL (custom template). */
|
|
99
|
-
responsesURL:
|
|
117
|
+
responsesURL: coercedString(),
|
|
100
118
|
/** Full Anthropic messages endpoint URL (custom template). */
|
|
101
|
-
anthropicURL:
|
|
119
|
+
anthropicURL: coercedString(),
|
|
102
120
|
/** Environment-variable name (credential ref) holding the API key. */
|
|
103
|
-
apiKeyEnv:
|
|
121
|
+
apiKeyEnv: coercedString().role("credential-ref"),
|
|
104
122
|
/** Gateway type label (newapi/litellm/higress/openai-compatible/custom). */
|
|
105
123
|
flavor: z.union(FLAVORS),
|
|
106
124
|
/** Model-list source: `auto` (prefer /v1/models), `v1`, or `management`. */
|
|
107
125
|
catalogMode: z.union(["auto", "v1", "management"]),
|
|
108
126
|
/** User id sent to the management API when it is used. */
|
|
109
|
-
userId:
|
|
127
|
+
userId: coercedString(),
|
|
110
128
|
/** Per-model overrides and custom models. */
|
|
111
129
|
models: z.array(ModelOverrideSchema),
|
|
112
130
|
/** Wire-format preference order for this gateway. */
|
|
113
|
-
endpointPriority: z.array(
|
|
131
|
+
endpointPriority: z.array(coercedString()),
|
|
114
132
|
});
|
|
115
133
|
|
|
116
134
|
export const Config = z.object({
|
|
117
135
|
// ---- Legacy single-connection fields (build the default `newapi` route) ----
|
|
118
136
|
/** Display name of the default gateway route (defaults to "NewAPI"). */
|
|
119
|
-
label:
|
|
137
|
+
label: coercedString(),
|
|
120
138
|
/** Environment-variable name (credential ref) holding the default gateway API key. */
|
|
121
|
-
apiKeyEnv:
|
|
139
|
+
apiKeyEnv: coercedString().role("credential-ref").default(DEFAULT_API_KEY_ENV),
|
|
122
140
|
/** Default gateway base URL; resolved from NEWAPI_BASE_URL / NEWAPI_API_URL then the public cloud default. */
|
|
123
|
-
baseURL:
|
|
141
|
+
baseURL: coercedString(),
|
|
124
142
|
/** Full chat-completions endpoint URL (custom template for the default gateway). */
|
|
125
|
-
openaiURL:
|
|
143
|
+
openaiURL: coercedString(),
|
|
126
144
|
/** Full Responses endpoint URL (custom template for the default gateway). */
|
|
127
|
-
responsesURL:
|
|
145
|
+
responsesURL: coercedString(),
|
|
128
146
|
/** Full Anthropic messages endpoint URL (custom template for the default gateway). */
|
|
129
|
-
anthropicURL:
|
|
147
|
+
anthropicURL: coercedString(),
|
|
130
148
|
/** Gateway type label shown in the UI (newapi/litellm/higress/openai-compatible/custom). */
|
|
131
149
|
flavor: z.union(FLAVORS).default("newapi"),
|
|
132
150
|
/** models.dev catalog URL (any fetch-able URL; file: works for offline mirrors). */
|
|
133
|
-
modelsUrl:
|
|
151
|
+
modelsUrl: coercedString().default(DEFAULT_MODELS_URL),
|
|
134
152
|
/** Enrich gateway models with models.dev parameters. */
|
|
135
153
|
useModelsDev: z.boolean().default(true),
|
|
136
154
|
/** Widen the unknown-model reasoning fallback to the full off~max set. */
|
|
@@ -144,11 +162,17 @@ export const Config = z.object({
|
|
|
144
162
|
/** Exclude non-chat model families from the picker. */
|
|
145
163
|
includeChatOnly: z.boolean().default(true),
|
|
146
164
|
/** Regex patterns excluding models from the picker. */
|
|
147
|
-
excludePatterns: z.array(
|
|
165
|
+
excludePatterns: z.array(coercedString()).default(DEFAULT_EXCLUDE_PATTERNS),
|
|
148
166
|
/** Wire-format preference order, intersected with each model's supported types. */
|
|
149
|
-
endpointPriority: z.array(
|
|
167
|
+
endpointPriority: z.array(coercedString()).default(DEFAULT_ENDPOINT_PRIORITY),
|
|
150
168
|
/** User id sent to the management API when it is used. */
|
|
151
|
-
userId:
|
|
169
|
+
userId: coercedString().default("1"),
|
|
170
|
+
/**
|
|
171
|
+
* Extra HTTP headers sent with every provider request to the default
|
|
172
|
+
* gateway (name → value). Reserved attribution headers (user-agent) are
|
|
173
|
+
* filtered out at request time; per-request values are unaffected.
|
|
174
|
+
*/
|
|
175
|
+
headers: z.dict(coercedString()),
|
|
152
176
|
/** Fallback output-token cap for models without models.dev data. */
|
|
153
177
|
maxTokens: z.number().step(1).min(1).default(DEFAULT_MAX_TOKENS),
|
|
154
178
|
/** Fallback context window for models without models.dev data. */
|
package/lib/pi-bridge.js
CHANGED
|
@@ -344,10 +344,25 @@ export function toPiContext(options, attachments) {
|
|
|
344
344
|
// Stream translation (pi-ai EventStream → harness StreamChunks)
|
|
345
345
|
// ---------------------------------------------------------------------------
|
|
346
346
|
|
|
347
|
-
/**
|
|
347
|
+
/**
|
|
348
|
+
* Classify a pi-ai error message into a harness error code.
|
|
349
|
+
* Lifted from dsh-llm-pi-ai/stream (rc.7): the coarse codes matter because
|
|
350
|
+
* the harness retry stack (dsh-llm-retry) retries exactly RATE_LIMIT /
|
|
351
|
+
* SERVER / TIMEOUT / TRANSPORT / EMPTY_RESPONSE — a generic fallback code
|
|
352
|
+
* would make gateway blips unretryable.
|
|
353
|
+
*/
|
|
348
354
|
function classifyPiAiError(text) {
|
|
355
|
+
if (/\b(?:401|403)\b/.test(text)) return "AUTH";
|
|
349
356
|
if (isQuotaExceededError(text)) return QUOTA_EXCEEDED_CODE;
|
|
350
|
-
return "
|
|
357
|
+
if (/\b429\b|rate.?limit/i.test(text)) return "RATE_LIMIT";
|
|
358
|
+
if (/\b400\b|invalid.?request/i.test(text)) return "INVALID_REQUEST";
|
|
359
|
+
if (/\b5\d\d\b/.test(text)) return "SERVER";
|
|
360
|
+
if (/\btime(?:d)?\s*out\b|timeout/i.test(text)) return "TIMEOUT";
|
|
361
|
+
if (/stream ended (?:before|without)\b/i.test(text)) return "TRANSPORT";
|
|
362
|
+
if (/\b(?:network|connection|socket|fetch)\b|\bECONN[A-Z]+\b/i.test(text)
|
|
363
|
+
|| /\b(?:other side closed|HTTP2 request did not get a response|WebSocket closed unexpectedly)\b/i.test(text)
|
|
364
|
+
|| /\bterminated\b|premature close/i.test(text)) return "TRANSPORT";
|
|
365
|
+
return "PI_AI_ERROR";
|
|
351
366
|
}
|
|
352
367
|
|
|
353
368
|
/** Map pi-ai usage (reasoning folded into output by pi-ai). */
|
package/package.json
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-gateway-provider",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Generic LLM gateway model provider plugin for DeepSeek Harness: mounts newapi / LiteLLM / Higress / any OpenAI-compatible gateway as provider routes, auto-discovers the model list, enriches every model with models.dev parameters, and dispatches each model over its native wire protocol (OpenAI / Anthropic / Gemini) via the pi-ai SDK",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/Luck9Star/dsh-gateway-provider.git"
|
|
8
|
+
},
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/Luck9Star/dsh-gateway-provider/issues"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/Luck9Star/dsh-gateway-provider#readme",
|
|
5
13
|
"type": "module",
|
|
6
14
|
"main": "index.js",
|
|
7
15
|
"exports": {
|
|
@@ -12,13 +20,16 @@
|
|
|
12
20
|
"files": [
|
|
13
21
|
"index.js",
|
|
14
22
|
"lib",
|
|
15
|
-
"cordis.patch.yml"
|
|
23
|
+
"cordis.patch.yml",
|
|
24
|
+
"docs"
|
|
16
25
|
],
|
|
17
26
|
"scripts": {
|
|
18
27
|
"link": "bash scripts/link.sh",
|
|
19
28
|
"smoke": "node test/smoke.mjs",
|
|
20
29
|
"test:client": "node test/client-render.mjs",
|
|
21
|
-
"test:urls": "node test/protocol-urls.mjs"
|
|
30
|
+
"test:urls": "node test/protocol-urls.mjs",
|
|
31
|
+
"test:schema": "node test/schema-coercion.mjs",
|
|
32
|
+
"test:errors": "node test/error-codes.mjs"
|
|
22
33
|
},
|
|
23
34
|
"keywords": [
|
|
24
35
|
"deepseek-harness",
|
|
@@ -52,12 +63,11 @@
|
|
|
52
63
|
"dependencies": {},
|
|
53
64
|
"peerDependencies": {
|
|
54
65
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
55
|
-
"@deepseek-ai/dsh-
|
|
56
|
-
"@deepseek-ai/dsh-
|
|
57
|
-
"@deepseek-ai/dsh-
|
|
58
|
-
"@deepseek-ai/dsh-
|
|
59
|
-
"@deepseek-ai/dsh-
|
|
60
|
-
"@deepseek-ai/dsh-timeout": "^0.1.0-rc.6",
|
|
66
|
+
"@deepseek-ai/dsh-credentials": "^0.1.0-rc.7",
|
|
67
|
+
"@deepseek-ai/dsh-launch-environment": "^0.1.0-rc.7",
|
|
68
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.7",
|
|
69
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.7",
|
|
70
|
+
"@deepseek-ai/dsh-timeout": "^0.1.0-rc.7",
|
|
61
71
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
62
72
|
"@earendil-works/pi-ai": "^0.82.1"
|
|
63
73
|
}
|