dsh-gateway-provider 1.0.4 → 1.0.6

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 CHANGED
@@ -4,246 +4,175 @@
4
4
 
5
5
  > 中文文档:[docs/README.zh.md](docs/README.zh.md)
6
6
 
7
- Bring any LLM gateway — **newapi, LiteLLM, Higress, or any OpenAI-compatible
8
- endpoint** — into DeepSeek Harness. The plugin discovers the gateway's model
9
- list automatically, enriches every model with real parameters from
10
- [models.dev](https://models.dev), and serves each model over its native wire
11
- protocol (OpenAI / Anthropic / Gemini) through the pi-ai SDK.
7
+ Use **all the models behind your LLM gateway** — newapi, LiteLLM, Higress,
8
+ or any OpenAI-compatible endpoint — directly in DeepSeek Harness.
9
+
10
+ Install the plugin, paste your API key, and every gateway model shows up in
11
+ dsh's model picker with its **real** parameters (context window, output cap,
12
+ reasoning support) fetched from [models.dev](https://models.dev). Requests
13
+ go out over each model's own native protocol — OpenAI, Anthropic, or Gemini
14
+ — so tool calls and streaming behave the way that model's maker intended.
12
15
 
13
16
  ## Why this exists
14
17
 
15
- DeepSeek Harness ships first-party adapters (`llm-deepseek`, `llm-pi-ai`) that
16
- each speak for one provider. If your models live behind a gateway, the
17
- alternative is a hand-maintained static model list with guessed context
18
- windows and output caps.
19
-
20
- This plugin mounts the gateway itself: **N gateways become N provider
21
- routes, with zero static model lists.** Every model keeps its true
22
- parameters, and adding a model on the gateway side is enough — nothing to
23
- re-deploy.
24
-
25
- ## What you get
26
-
27
- - **Multiple gateways** — a default `newapi` route plus one `gateway:<id>`
28
- route per extra gateway, each with its own catalog cache.
29
- - **Automatic discovery** — `GET {base}/v1/models` first (including each
30
- model's supported request formats), management API fallback second.
31
- - **Real parameters** — models.dev data fills context window, output cap,
32
- reasoning levels, and release date; config defaults are only a fallback.
33
- - **Every wire format** — each model routes to its own protocol
34
- (`openai-completions` / `openai-responses` / `anthropic-messages` /
35
- `google-generative-ai`); no hand-written SSE or request serialization.
36
- - **A settings page** — **Settings → Gateway Models**: add and edit gateways
37
- from templates, test connections, hide / override / custom-add models —
38
- no YAML editing required.
39
- - **A clean picker** — newest-first release-date sorting, chat-only filtering,
40
- and regex excludes for non-chat models.
18
+ dsh ships one adapter per official provider. If your models live behind a
19
+ gateway, the manual alternative is a hand-maintained static model list with
20
+ guessed context windows and output caps. This plugin mounts the gateway
21
+ itself instead:
22
+
23
+ - **Nothing to maintain by hand** — the model list is read from the gateway
24
+ (`GET /v1/models`, with a management-API fallback for newapi); add a model
25
+ on the gateway side and it appears in dsh, no re-deploy.
26
+ - **Real numbers, not guesses** — models.dev data fills context window,
27
+ output cap, reasoning levels, release date; config defaults only fill gaps.
28
+ - **Every wire format, one plugin** — each model routes over its own
29
+ protocol (OpenAI chat completions / OpenAI responses / Anthropic messages
30
+ / Gemini), handled by the same
31
+ [pi-ai](https://www.npmjs.com/package/@earendil-works/pi-ai) SDK the
32
+ official dsh adapter uses.
33
+ - **Multiple gateways at once** — a default `newapi` route plus one
34
+ `gateway:<id>` route per extra gateway, each with its own cache and key.
35
+ - **A settings page instead of YAML** — **Settings → Gateway Models**: add
36
+ gateways from templates (NewAPI / LiteLLM / Higress / OpenAI-compatible /
37
+ fully custom), test connections, sync models, hide or override any model,
38
+ add custom models.
41
39
 
42
40
  ## Requirements
43
41
 
44
- - A DeepSeek Harness installation with a profile (`dsh`)
45
- - A reachable gateway: newapi / LiteLLM / Higress / any OpenAI-compatible endpoint
46
- - An API key for that gateway
47
-
48
- ## Quickstart
49
-
50
- 1. Install the plugin into your profile (published on npm):
51
-
52
- ```bash
53
- dsh plugin --profile web add dsh-gateway-provider # latest
54
- dsh plugin --profile web add dsh-gateway-provider@<version> # pinned
55
- ```
56
-
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.
60
-
61
- 2. Store your gateway key in `$DSH_HOME/.credentials.yaml` (mode 0600,
62
- hot-reloaded):
63
-
64
- ```yaml
65
- NEWAPI_API_KEY: sk-REPLACE_WITH_YOUR_KEY
66
- ```
42
+ - DeepSeek Harness (dsh) with a `web` profile (the settings page is a web-UI
43
+ extension; the provider itself works in any profile).
44
+ - A gateway API key (e.g. a newapi token).
67
45
 
68
- Or export `NEWAPI_API_KEY` in the launching environment.
46
+ ## Install
69
47
 
70
- 3. Restart the profile, then open **Settings → Gateway Models**.
48
+ ```sh
49
+ # 1. Install the plugin (dsh plugin add runs pnpm add under the hood)
50
+ dsh plugin --profile web add dsh-gateway-provider
71
51
 
72
- **Success looks like:** the gateway appears with a "synced N models" badge,
73
- and every chat-capable model is selectable with its real context window.
74
- The base URL resolves from `llm-newapi.baseURL` settings →
75
- `NEWAPI_BASE_URL` / `NEWAPI_API_URL` env → the public default
76
- `https://api.newapi.ai`.
52
+ # 2. Store your key — pick ONE of:
53
+ # a) the dsh credentials file (recommended; created with mode 0600, hot-reloaded)
54
+ echo "NEWAPI_API_KEY: sk-REPLACE_WITH_YOUR_KEY" >> ~/.dsh/.credentials.yaml
55
+ # b) or export it in the shell you launch dsh from:
56
+ # export NEWAPI_API_KEY=sk-REPLACE_WITH_YOUR_KEY
77
57
 
78
- ## Add more gateways
79
-
80
- Extra gateways live in the `gateways` array — each becomes its own
81
- `gateway:<id>` provider route:
82
-
83
- ```yaml
84
- llm-newapi:
85
- baseURL: https://your-newapi-instance.com
86
- gateways:
87
- - id: litellm-prod
88
- label: LiteLLM Prod
89
- baseURL: https://litellm.example.com
90
- apiKeyEnv: LITELLM_API_KEY
91
- flavor: litellm # form template: newapi / litellm / higress / openai-compatible / custom
92
-
93
- # Fully-custom gateway: complete per-protocol endpoint URLs, no shared
94
- # base. A protocol left empty stays disabled.
95
- - id: edge
96
- label: Edge GW
97
- flavor: custom
98
- openaiURL: https://edge.example.com/openai/v1/chat/completions
99
- responsesURL: https://edge.example.com/openai/v1/responses
100
- anthropicURL: https://edge.example.com/anthropic/v1/messages
101
- apiKeyEnv: EDGE_API_KEY
58
+ # 3. Restart and open the settings page
59
+ dsh --profile web
60
+ # → Settings → Gateway Models
102
61
  ```
103
62
 
104
- You can also add gateways from the settings page (with the same templates)
105
- instead of editing YAML.
63
+ **Expected result:** the model picker gains a "NewAPI" route listing your
64
+ gateway's chat models, newest first. Click **Test** on the gateway card —
65
+ it should answer `✓ Connected — N models`. Not using the public newapi
66
+ cloud? Set **Base URL** on the card (or `baseURL` in config) to your own
67
+ gateway address first.
106
68
 
107
- ## Control the model list per gateway
69
+ ## Daily use
108
70
 
109
- Hide models, fix wrong metadata, or add models the gateway does not list:
71
+ Everything lives in **Settings → Gateway Models**:
110
72
 
111
- ```yaml
112
- llm-newapi:
113
- models:
114
- - id: glm-5.2
115
- disabled: true # hide from picker
116
- - id: glm-5.2-highspeed
117
- contextWindow: 1000000 # override discovered value
118
- protocol: openai # force protocol (openai/anthropic/gemini/openai-response)
119
- - id: my-internal-model # custom model the gateway does not list
120
- name: My Internal Model
121
- contextWindow: 200000
122
- ```
123
-
124
- The **Gateway Models** page exposes the same operations with a UI: search,
125
- hidden/custom filters with counts, a per-model override editor (placeholders
126
- show discovered values), connection test, and save/cancel semantics.
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`.
73
+ - **Add more gateways** — "Add Gateway", pick a template (LiteLLM, Higress,
74
+ OpenAI-compatible, or fully custom with per-protocol URLs), point it at
75
+ the base URL, name its key env var, Test, Sync. Each gateway becomes its
76
+ own route in the picker.
77
+ - **Tame the model list** — non-chat models (image / speech / embedding /
78
+ rerank …) are excluded by default regexes; hide or rename any model; add
79
+ a custom model by hand if the gateway hides it; per-model protocol,
80
+ context window, output cap, and reasoning levels are all editable.
81
+ - **Keys live in dsh's credential store** — the settings page shows a badge
82
+ (`✓ Key set · NEWAPI_API_KEY` / `⚠ No key set`) and can write the key
83
+ there for you.
142
84
 
143
85
  ## Configuration reference
144
86
 
145
- Section `llm-newapi:` of `$DSH_HOME/settings.yaml`. The flat fields
146
- (`baseURL` / `apiKeyEnv` / …) seed the default `newapi` route; gateways in
147
- the `gateways` array support most of the same fields per gateway
148
- (`label` / `apiKeyEnv` / `flavor` / `catalogMode` / `endpointPriority` / …).
149
-
150
- | Field | Default | Description |
151
- |-------|---------|-------------|
152
- | `label` | `NewAPI` | Display name of the default gateway route |
153
- | `apiKeyEnv` | `NEWAPI_API_KEY` | Credential reference (environment variable name) |
154
- | `baseURL` | env `NEWAPI_BASE_URL` / `NEWAPI_API_URL` → `https://api.newapi.ai` | Gateway base URL (unused when protocol URLs are set) |
155
- | `flavor` | `newapi` | Gateway template: `newapi` / `litellm` / `higress` / `openai-compatible` / `custom` |
156
- | `openaiURL` | – | Full chat-completions endpoint URL (custom template; empty = protocol disabled) |
157
- | `responsesURL` | – | Full Responses endpoint URL (custom template; empty = protocol disabled) |
158
- | `anthropicURL` | – | Full Anthropic messages endpoint URL (custom template; empty = protocol disabled) |
159
- | `modelsUrl` | `https://models.dev/models.json` | models.dev source (`file:` URLs work offline) |
160
- | `useModelsDev` | `true` | Enrich gateway models with models.dev parameters |
161
- | `extendedReasoningLevels` | `false` | Widen the unknown-model reasoning fallback to off~max (default off/low/medium/high) |
162
- | `sortModelsByRelease` | `true` | Sort the picker newest-first by release date (unknown dates first) |
163
- | `catalogMode` | `auto` | Model-list source: `auto` / `v1` / `management` |
164
- | `catalogTtlMs` | `1800000` | Model-list cache freshness window |
165
- | `includeChatOnly` | `true` | Only expose chat-capable models to the picker |
166
- | `excludePatterns` | image/speech/embed/… | Regex patterns excluding models from the picker |
167
- | `endpointPriority` | `["openai-response","anthropic","openai","gemini"]` | Wire-format preference order (first match wins per model) |
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 |
170
- | `maxTokens` | `32768` | Output cap fallback when models.dev lacks data |
171
- | `defaultContextWindow` | `128000` | Context window fallback when models.dev lacks data |
172
- | `streamIdleTimeoutMs` | `300000` | Stream idle watchdog |
173
- | `retryPolicy` | standard | Same shape as `llm-deepseek` |
174
-
175
- ## How it works
176
-
177
- All wire-format concerns are delegated to
178
- [`@earendil-works/pi-ai`](https://www.npmjs.com/package/@earendil-works/pi-ai)
179
- — the same SDK the official `dsh-llm-pi-ai` adapter uses. Per request:
180
-
181
- ```
182
- harness GenerateOptions
183
- → toPiContext() lib/pi-bridge.js harness messages → pi-ai Context
184
- → models.streamSimple() pi-ai SDK dispatches by each model's `api`
185
- → toStreamChunks() lib/pi-bridge.js pi-ai events → harness chunks
186
- ```
187
-
188
- Each discovered model is built with an `api` field mapped from the gateway's
189
- advertised `supported_endpoint_types`, honoring `endpointPriority`; the
190
- pi-ai provider receives an api *map*, so every model routes to its own
191
- protocol implementation. `sdkBaseURL()` appends `/v1` for OpenAI-protocol
192
- models and leaves Anthropic/Google bases untouched.
193
-
194
- ```
195
- dsh-gateway-provider/
196
- ├── index.js # plugin entry: Config, provider registration, settings/credentials
197
- ├── cordis.patch.yml # dsh.bundle patch (auto-mounts via `dsh plugin add`)
198
- ├── lib/ # adapter, pi-provider, pi-bridge, catalog, modelsdev, thinking, client
199
- ├── test/ # smoke (live gateway) + offline units + settings-UI render
200
- └── scripts/link.sh # link profile node_modules for local checkouts
201
- ```
202
-
203
- ## Develop from a checkout
204
-
205
- 1. `bash scripts/link.sh` — symlink this package's `node_modules` to the
206
- profile's so bare `@deepseek-ai/*` imports resolve to the exact module
207
- instances the harness process uses (single-copy `instanceof` safety).
208
- 2. Register the checkout as a profile link dependency and install:
209
-
210
- ```bash
211
- cd "$DSH_HOME/profiles/web"
212
- # package.json dependencies: "dsh-gateway-provider": "link:/absolute/path/to/dsh-gateway-provider"
213
- # and add "dsh-gateway-provider" to the bundles list in the same file
214
- pnpm install
215
- ```
216
-
217
- 3. Restart the profile.
218
-
219
- > ⚠️ Do **not** also insert `id: llm-newapi` into the profile's own
220
- > `cordis.patch.yml` — the bundle layer already mounts it, and a duplicate
221
- > raises `duplicate loader entry id` at boot.
222
-
223
- Client-bundle edits hot-apply after a browser reload; host-half edits need a
224
- profile restart.
225
-
226
- ### Tests
227
-
228
- ```bash
229
- node test/smoke.mjs # live gateway: catalog / openai × 2 / tools / anthropic / gemini / custom-urls
230
- node test/smoke.mjs --only custom-urls
231
- node test/protocol-urls.mjs # offline: URL derivation + gateway resolution
232
- node test/client-render.mjs # offline: settings-UI render tree (zh + en)
87
+ Optional — everything below has a working default. Config lives in the
88
+ `llm-newapi:` section of `~/.dsh/settings.yaml` (the settings page edits
89
+ the same keys). The frequently used ones:
90
+
91
+ | Key | Default | Meaning |
92
+ | --- | --- | --- |
93
+ | `baseURL` | `https://api.newapi.ai` | Your gateway's base URL. Env fallbacks: `NEWAPI_BASE_URL`, `NEWAPI_API_URL`. |
94
+ | `apiKeyEnv` | `NEWAPI_API_KEY` | Which env/credential variable holds the key. |
95
+ | `label` | `NewAPI` | Route label shown in the picker. |
96
+ | `flavor` | `newapi` | Template label only (`newapi` / `litellm` / `higress` / `openai-compatible` / `custom`). |
97
+ | `gateways` | — | Array of extra gateways: `{ id, baseURL, apiKeyEnv, label, … }`, each becoming a `gateway:<id>` route. |
98
+ | `models` | — | Per-model overrides: `{ id, name, disabled, protocol, contextWindow, maxTokens, reasoningLevels }`. |
99
+ | `useModelsDev` / `modelsUrl` | `true` / models.dev | Parameter enrichment source (supports `file:` URLs for offline). |
100
+ | `excludePatterns` | image/speech/… | Regex list of model ids to keep out of the picker. |
101
+ | `sortModelsByRelease` | `true` | Newest models first. |
102
+ | `catalogMode` | `auto` | `v1` (`/v1/models` only) / `management` (newapi user API) / `auto`. |
103
+ | `endpointPriority` | responses → anthropic → openai → gemini | Which protocol to prefer when a model supports several. |
104
+ | `openaiURL` / `responsesURL` / `anthropicURL` | — | Fully-custom gateways only: per-protocol endpoint URLs; unset = that protocol off. |
105
+ | `maxTokens` / `defaultContextWindow` | `32768` / `128000` | Fallbacks when models.dev has no data. |
106
+ | `streamIdleTimeoutMs` | `600000` | Idle timeout while streaming. |
107
+ | `headers` | — | Extra HTTP headers sent to the gateway. |
108
+
109
+ ## Troubleshooting
110
+
111
+ | Symptom | Cause → fix |
112
+ | --- | --- |
113
+ | Picker route exists but zero models | The plugin can't read your model list. Check the gateway base URL; try `catalogMode: "management"` for newapi gateways that restrict `/v1/models`. |
114
+ | `401` / auth errors on every request | Key missing or wrong: check the badge in Settings → Gateway Models, or `NEWAPI_API_KEY` in `~/.dsh/.credentials.yaml`. |
115
+ | A model's context window looks wrong | models.dev had no match. Edit the model on the settings page (or a `models:` override). |
116
+ | Wrong format answers / tool calls flaky for one model | That model is routed over a protocol it handles poorly. Pin `protocol` on the model (`openai`, `openai-response`, `anthropic`, `gemini`). |
117
+ | Custom gateway with separate endpoints | Use `flavor: "custom"` and set `openaiURL` / `responsesURL` / `anthropicURL` explicitly. |
118
+
119
+ ## How it works (one minute version)
120
+
121
+ At startup the plugin registers one provider route per gateway, pulls the
122
+ model list from the gateway, and fuzzy-matches each model id against
123
+ models.dev to fill in real parameters. When you pick a model, dsh's request
124
+ is translated to the pi-ai SDK's format and sent over that model's native
125
+ protocol; the streamed reply is translated back into dsh chunks. Catalogs
126
+ are cached (30 min by default) per gateway. No hand-written protocol code —
127
+ the bridge is lifted from the official `dsh-llm-pi-ai` adapter.
128
+
129
+ ## Development
130
+
131
+ ```sh
132
+ git clone https://github.com/Luck9Star/dsh-gateway-provider
133
+ cd dsh-gateway-provider
134
+ npm install # installs the plugin-pinned pi-ai copy (direct dependency)
135
+ npm run link # symlink @deepseek-ai/* into your dsh profile (single instanceof safety)
136
+ npm run test:client # settings-UI render, both locales
137
+ npm run test:urls # URL/derivation units
138
+ npm run smoke # live gateway round-trip (needs a real key)
233
139
  ```
234
140
 
235
- Smoke-test credentials resolve from: process environment → plugin `.env` →
236
- `$NEWAPI_ENV_FILE` (plus a legacy author-local fallback path).
141
+ ### Why the plugin pins its own pi-ai
142
+
143
+ `@earendil-works/pi-ai` is a direct dependency (exact-pinned), independent of
144
+ the pi-ai version bundled with the harness. This decouples the gateway model
145
+ catalog (thinking levels, per-provider compat such as zhipu GLM's
146
+ `supportsDeveloperRole: false`) from harness upgrades: a model missing from
147
+ the harness's older catalog no longer degrades request encoding. The
148
+ plugin↔harness boundary passes plain data (`GenerateOptions` in, dsh
149
+ `StreamChunk`s out; `lib/pi-bridge.js` never leaks pi-ai objects across), so
150
+ the plugin's pi-ai copy and the harness's own coexist safely in one process.
151
+
152
+ Developing from a checkout: point the profile's `package.json` at
153
+ `"dsh-gateway-provider": "link:/abs/path"` and re-run `pnpm install` in the
154
+ profile. Do **not** also add an `id: llm-newapi` row to the profile's own
155
+ `cordis.patch.yml` — the bundle patch already provides it (duplicate row =
156
+ loader error).
157
+
158
+ ## References & credits
159
+
160
+ - [pi-ai SDK](https://www.npmjs.com/package/@earendil-works/pi-ai) — all
161
+ four wire protocols; the bridge reuses the official `dsh-llm-pi-ai`
162
+ adapter's translation layer.
163
+ - [models.dev](https://models.dev) — the parameter catalog (context
164
+ windows, output caps, reasoning, release dates).
165
+ - [new-api](https://github.com/QuantumNous/new-api),
166
+ [LiteLLM](https://github.com/BerriAI/litellm),
167
+ [Higress](https://github.com/alibaba/higress) — the gateways this plugin
168
+ is tested against (any OpenAI-compatible endpoint works).
237
169
 
238
170
  ## Security
239
171
 
240
- - The API key is only resolved through `$DSH_HOME/.credentials.yaml` (0600)
241
- or the environment — never written to logs, config, or chat.
242
- Credential fields are edited masked on the web UI.
243
- - No runtime `dependencies`; `peerDependencies` reuse the harness-installed
244
- `@deepseek-ai/*` packages and `@earendil-works/pi-ai`. Commits are gated
245
- by gitleaks locally (pre-commit) and in CI.
172
+ Keys live in dsh's credential store or the launching environment — never in
173
+ settings YAML. The repo runs [gitleaks](https://github.com/gitleaks/gitleaks)
174
+ in CI and pre-commit to keep secrets out.
246
175
 
247
176
  ## License
248
177
 
249
- MIT
178
+ [MIT](LICENSE)
package/docs/README.zh.md CHANGED
@@ -4,227 +4,119 @@
4
4
 
5
5
  > English: [README.md](../README.md)
6
6
 
7
- 把任意 LLM 网关 —— **newapi、LiteLLM、Higress 或任何 OpenAI 兼容端点** —— 接入
8
- DeepSeek Harness。插件自动发现网关的模型列表,用 [models.dev](https://models.dev)
9
- 的真实参数补齐每个模型,并通过 pi-ai SDK 以模型原生协议(OpenAI / Anthropic /
10
- Gemini)分发请求。
7
+ 把 **LLM 网关背后的全部模型** —— newapi、LiteLLM、Higress,或任何 OpenAI 兼容端点 —— 直接搬进 DeepSeek Harness 用。
11
8
 
12
- ## 为什么需要它
13
-
14
- DeepSeek Harness 自带的适配器(`llm-deepseek`、`llm-pi-ai`)各自只服务一个官方
15
- provider。如果你的模型都在网关后面,就得手工维护一份静态模型清单,上下文窗口、
16
- 输出上限全靠猜。
9
+ 装好插件、贴上 API key,网关里的每个模型就会出现在 dsh 的模型选择器里,并且带着从 [models.dev](https://models.dev) 拉来的**真实**参数(上下文窗口、输出上限、推理支持)。请求按每个模型各自的原生协议出去 —— OpenAI、Anthropic 或 Gemini —— 工具调用和流式输出都按该模型官方的方式工作。
17
10
 
18
- 本插件直接把网关挂进来:**N 个网关 = N 个 provider 路由,零静态清单。** 每个模型
19
- 带着真实参数,网关侧加模型即可生效——无需重新部署。
11
+ ## 为什么需要它
20
12
 
21
- ## 你能得到什么
13
+ dsh 自带的适配器一个 provider 一个。如果你的模型都在网关后面,手工做法是维护一份静态模型清单,上下文窗口和输出上限全靠猜。本插件直接把网关本身挂进来:
22
14
 
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
- - **干净的选择器** — 按发布日期新到旧排序、只保留对话模型、正则剔除。
15
+ - **不用手工维护** —— 模型列表从网关读取(`GET /v1/models`,newapi 附加管理 API 兜底);网关侧加一个模型,dsh 里就多一个,无需重新部署。
16
+ - **真实数字,不是猜测** —— models.dev 数据补齐上下文窗口、输出上限、推理档位、发布日期;配置默认值只做兜底。
17
+ - **一个插件,所有协议** —— 每个模型走自己的协议(OpenAI chat completions / OpenAI responses / Anthropic messages / Gemini),由官方 dsh 适配器同款的 [pi-ai](https://www.npmjs.com/package/@earendil-works/pi-ai) SDK 处理。
18
+ - **多网关并存** —— 默认 `newapi` 路由,外加每个额外网关一条 `gateway:<id>` 路由,各自的缓存与密钥。
19
+ - **设置页面代替 YAML** —— **Settings → Gateway Models**:从模板添加网关(NewAPI / LiteLLM / Higress / OpenAI 兼容 / 完全自定义)、测连接、同步模型、隐藏或覆盖任意模型、手工添加自定义模型。
35
20
 
36
21
  ## 环境要求
37
22
 
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。
23
+ - DeepSeek Harness (dsh),带 `web` profile(设置页面是 web UI 扩展;provider 本身任何 profile 都能用)。
24
+ - 一个网关 API key(比如 newapi 令牌)。
53
25
 
54
- 2. 把网关 key 写入 `$DSH_HOME/.credentials.yaml`(权限 0600,热加载):
26
+ ## 安装
55
27
 
56
- ```yaml
57
- NEWAPI_API_KEY: sk-REPLACE_WITH_YOUR_KEY
58
- ```
28
+ ```sh
29
+ # 1. 安装插件(dsh plugin add 底层会执行 pnpm add)
30
+ dsh plugin --profile web add dsh-gateway-provider
59
31
 
60
- 或在启动环境中导出 `NEWAPI_API_KEY`。
32
+ # 2. 存好你的 key —— 二选一:
33
+ # a) dsh 凭据文件(推荐;以 0600 权限创建,热加载)
34
+ echo "NEWAPI_API_KEY: sk-REPLACE_WITH_YOUR_KEY" >> ~/.dsh/.credentials.yaml
35
+ # b) 或在启动 dsh 的 shell 里 export:
36
+ # export NEWAPI_API_KEY=sk-REPLACE_WITH_YOUR_KEY
61
37
 
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
38
+ # 3. 重启,打开设置页
39
+ dsh --profile web
40
+ # → Settings → Gateway Models
92
41
  ```
93
42
 
94
- 也可以在设置页里用同样的模板添加网关,不必编辑 YAML。
43
+ **预期结果:** 模型选择器多出一条 "NewAPI" 路由,列出网关的对话模型,最新在前。在网关卡片上点 **Test** —— 应显示 `✓ Connected — N models`。用的不是 newapi 公有云?先在卡片上改 **Base URL**(或配置里的 `baseURL`)指向你自己的网关地址。
95
44
 
96
- ## 按网关控制模型清单
45
+ ## 日常使用
97
46
 
98
- 隐藏模型、修正错误元数据、添加网关未列出的模型:
47
+ 一切都在 **Settings → Gateway Models**:
99
48
 
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`。
49
+ - **加更多网关** —— "Add Gateway",选模板(LiteLLM、Higress、OpenAI 兼容,或按协议分别填 URL 的完全自定义),填 base URL 和 key 的环境变量名,Test、Sync。每个网关在选择器里有自己的路由。
50
+ - **管好模型列表** —— 非对话模型(图像 / 语音 / 向量 / 重排……)默认被正则排除;任意模型可隐藏、可改名;网关藏起来的模型可手工添加;每个模型的协议、上下文窗口、输出上限、推理档位都可改。
51
+ - **密钥放在 dsh 凭据存储里** —— 设置页有状态徽标(`✓ Key set · NEWAPI_API_KEY` / `⚠ No key set`),也可以直接帮你把 key 写进去。
127
52
 
128
53
  ## 配置参考
129
54
 
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
-
55
+ 全部可选 —— 下表每项都有可用默认值。配置写在 `~/.dsh/settings.yaml` 的 `llm-newapi:` 段(设置页改的就是同一批键)。常用项:
56
+
57
+ | 键 | 默认值 | 含义 |
58
+ | --- | --- | --- |
59
+ | `baseURL` | `https://api.newapi.ai` | 网关 base URL。环境变量兜底:`NEWAPI_BASE_URL`、`NEWAPI_API_URL`。 |
60
+ | `apiKeyEnv` | `NEWAPI_API_KEY` | key 存在哪个环境/凭据变量里。 |
61
+ | `label` | `NewAPI` | 选择器里显示的路由名。 |
62
+ | `flavor` | `newapi` | 仅作模板标签(`newapi` / `litellm` / `higress` / `openai-compatible` / `custom`)。 |
63
+ | `gateways` | — | 额外网关数组:`{ id, baseURL, apiKeyEnv, label, … }`,每个成为一条 `gateway:<id>` 路由。 |
64
+ | `models` | — | 按模型覆盖:`{ id, name, disabled, protocol, contextWindow, maxTokens, reasoningLevels }`。 |
65
+ | `useModelsDev` / `modelsUrl` | `true` / models.dev | 参数增补来源(支持 `file:` URL 离线用)。 |
66
+ | `excludePatterns` | 图像/语音/…… | 要从选择器排除的模型 id 正则列表。 |
67
+ | `sortModelsByRelease` | `true` | 最新模型排前面。 |
68
+ | `catalogMode` | `auto` | `v1`(只用 `/v1/models`)/ `management`(newapi 用户 API)/ `auto`。 |
69
+ | `endpointPriority` | responses → anthropic → openai → gemini | 模型支持多种协议时的优先序。 |
70
+ | `openaiURL` / `responsesURL` / `anthropicURL` | — | 仅完全自定义网关:按协议分别填端点 URL;不填 = 该协议关闭。 |
71
+ | `maxTokens` / `defaultContextWindow` | `32768` / `128000` | models.dev 无数据时的兜底。 |
72
+ | `streamIdleTimeoutMs` | `600000` | 流式空闲超时。 |
73
+ | `headers` | — | 发往网关的额外 HTTP 头。 |
74
+
75
+ ## 常见问题
76
+
77
+ | 症状 | 原因 → 处理 |
78
+ | --- | --- |
79
+ | 路由有了但模型为零 | 插件读不到模型列表。检查网关 base URL;newapi 网关若限制 `/v1/models`,试试 `catalogMode: "management"`。 |
80
+ | 每次请求都 `401` / 鉴权错误 | key 缺失或不对:看 Settings → Gateway Models 的徽标,或 `~/.dsh/.credentials.yaml` 里的 `NEWAPI_API_KEY`。 |
81
+ | 某模型的上下文窗口看着不对 | models.dev 没匹配上。在设置页编辑该模型(或写 `models:` 覆盖)。 |
82
+ | 单个模型工具调用不稳、格式怪 | 该模型被路由到了它处理不好的协议。在模型上钉死 `protocol`(`openai`、`openai-response`、`anthropic`、`gemini`)。 |
83
+ | 自定义网关各协议端点分开 | 用 `flavor: "custom"`,显式填 `openaiURL` / `responsesURL` / `anthropicURL`。 |
84
+
85
+ ## 工作原理(一分钟版)
86
+
87
+ 启动时,插件为每个网关注册一条 provider 路由,从网关拉取模型列表,再把每个模型 id 与 models.dev 模糊匹配补齐真实参数。选中模型后,dsh 的请求被翻译成 pi-ai SDK 的格式、按该模型的原生协议发出;流式回复再翻译回 dsh 的分块。模型目录按网关缓存(默认 30 分钟)。没有任何手写协议代码 —— 桥接层直接复用官方 `dsh-llm-pi-ai` 适配器。
88
+
89
+ ## 开发
90
+
91
+ ```sh
92
+ git clone https://github.com/Luck9Star/dsh-gateway-provider
93
+ cd dsh-gateway-provider
94
+ npm install # 安装插件自带的 pi-ai 固定副本(直接依赖)
95
+ npm run link # @deepseek-ai/* 软链进 dsh profile(保证单实例)
96
+ npm run test:client # 设置页渲染测试,双语言
97
+ npm run test:urls # URL 派生单元测试
98
+ npm run smoke # 真实网关往返(需要真 key)
178
99
  ```
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
100
 
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
- ```
101
+ ### 为什么插件自带 pi-ai
200
102
 
201
- 3. 重启 profile。
103
+ `@earendil-works/pi-ai` 是精确锁定的直接依赖,与 harness 自带的 pi-ai 版本解耦:网关模型目录(思考档位、各家 compat,如智谱 GLM 的 `supportsDeveloperRole: false`)不再随 harness 升级被动漂移——harness 旧目录里缺的模型不会再退化请求编码。插件与 harness 的边界只传纯数据(`GenerateOptions` 进、dsh `StreamChunk` 出,`lib/pi-bridge.js` 从不把 pi-ai 对象泄漏到边界外),因此插件副本与 harness 自带副本可在同一进程内安全共存。
202
104
 
203
- > ⚠️ **不要**再往 profile 自己的 `cordis.patch.yml` 里插入 `id: llm-newapi`
204
- > ——bundle 层已挂载,重复会导致启动时报 `duplicate loader entry id`。
105
+ 从本地检出开发:把 profile 的 `package.json` 指向
106
+ `"dsh-gateway-provider": "link:/绝对/路径"`,然后在 profile 里重跑 `pnpm install`。**不要**再往 profile 自己的 `cordis.patch.yml` 里加 `id: llm-newapi` 行 —— bundle patch 已提供(重复行 = 加载器报错)。
205
107
 
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
- ```
108
+ ## 参考与致谢
216
109
 
217
- 冒烟测试凭据解析顺序:进程环境变量 → 插件目录 `.env` →
218
- `$NEWAPI_ENV_FILE`(另有一个作者本地遗留兜底路径)。
110
+ - [pi-ai SDK](https://www.npmjs.com/package/@earendil-works/pi-ai) —— 四种协议全由它实现;桥接层复用官方 `dsh-llm-pi-ai` 适配器的翻译代码。
111
+ - [models.dev](https://models.dev) —— 参数目录(上下文窗口、输出上限、推理、发布日期)。
112
+ - [new-api](https://github.com/QuantumNous/new-api)、
113
+ [LiteLLM](https://github.com/BerriAI/litellm)、
114
+ [Higress](https://github.com/alibaba/higress) —— 本插件测试过的网关(任何 OpenAI 兼容端点都行)。
219
115
 
220
- ## 安全说明
116
+ ## 安全
221
117
 
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 扫描把关。
118
+ 密钥只存放在 dsh 凭据存储或启动环境里 —— 绝不写进 settings YAML。仓库在 CI 与 pre-commit 中运行 [gitleaks](https://github.com/gitleaks/gitleaks) 防泄漏。
227
119
 
228
- ## License
120
+ ## 许可证
229
121
 
230
- MIT
122
+ [MIT](../LICENSE)
package/lib/adapter.js CHANGED
@@ -189,6 +189,7 @@ export class NewapiAdapter extends LlmAdapter {
189
189
  ...reasoning === undefined ? {} : { reasoning },
190
190
  ...options.temperature === undefined ? {} : { temperature: options.temperature },
191
191
  ...options.maxTokens === undefined ? {} : { maxTokens: options.maxTokens },
192
+ ...options.sessionId === undefined ? {} : { sessionId: String(options.sessionId) },
192
193
  signal: watchdog.signal,
193
194
  headers: requestHeaders(connection.headers),
194
195
  maxRetries: 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-gateway-provider",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
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
5
  "repository": {
6
6
  "type": "git",
@@ -26,6 +26,7 @@
26
26
  "scripts": {
27
27
  "link": "bash scripts/link.sh",
28
28
  "smoke": "node test/smoke.mjs",
29
+ "test:session": "node test/session-affinity.mjs",
29
30
  "test:client": "node test/client-render.mjs",
30
31
  "test:urls": "node test/protocol-urls.mjs",
31
32
  "test:schema": "node test/schema-coercion.mjs",
@@ -60,7 +61,9 @@
60
61
  "patch": "./cordis.patch.yml"
61
62
  }
62
63
  },
63
- "dependencies": {},
64
+ "dependencies": {
65
+ "@earendil-works/pi-ai": "0.84.4"
66
+ },
64
67
  "peerDependencies": {
65
68
  "@deepseek-ai/cordis": "^4.0.1",
66
69
  "@deepseek-ai/dsh-credentials": "^0.1.0-rc.7",
@@ -68,7 +71,6 @@
68
71
  "@deepseek-ai/dsh-llm": "^0.1.0-rc.7",
69
72
  "@deepseek-ai/dsh-settings": "^0.1.0-rc.7",
70
73
  "@deepseek-ai/dsh-timeout": "^0.1.0-rc.7",
71
- "@deepseek-ai/schemastery": "^3.18.1",
72
- "@earendil-works/pi-ai": "^0.82.1"
74
+ "@deepseek-ai/schemastery": "^3.18.1"
73
75
  }
74
76
  }