@omdp/dsh-key-fallback 3.2.3 → 3.3.0
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 +63 -6
- package/README.zh-CN.md +52 -5
- package/lib/index.js +44 -30
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -2,9 +2,64 @@
|
|
|
2
2
|
|
|
3
3
|
[简体中文](README.zh-CN.md) | English
|
|
4
4
|
|
|
5
|
-
**Multi-key API key pool with automatic rotation for [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness)** —
|
|
6
|
-
|
|
7
|
-
Current version: **v3.
|
|
5
|
+
**Multi-key API key pool with automatic rotation for [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness)** — hooks DSH's credential seam `credentials.resolve`: each time an adapter asks for a key, if that ref belongs to a configured pool the wrapper returns the pool's current key; when a configured trigger error occurs it marks the failed key cooling (fixed `cooldownMs`, no exponential backoff) and advances to the next key. **Re-sending is left entirely to DSH's own `dsh-llm-retry`** — this plugin never re-sends on its own; it only switches the key and lets the retry policy decide.
|
|
6
|
+
|
|
7
|
+
Current version: **v3.3.0** (`v7` UI generation).
|
|
8
|
+
|
|
9
|
+
## What v3.3.0 offers
|
|
10
|
+
|
|
11
|
+
- **The key switch now actually reaches the provider** (2026-09-29) — a real bug fix, not a refactor for
|
|
12
|
+
tidiness. Until 3.2.4 this plugin switched keys by writing `process.env[<pool env>] = <key value>`, which
|
|
13
|
+
**never reached the credential layer**. DSH freezes `{ ...process.env }` into a *launch environment snapshot*
|
|
14
|
+
at boot (`dsh-app-boot`), `dsh-launch-environment` copies that into a `Map`, and
|
|
15
|
+
`dsh-credentials-local.inherited()` reads only that snapshot — so a post-startup `process.env` write is
|
|
16
|
+
invisible. Measured on a live 0.2.0-rc.1 install: `GET /dsh-key-fallback/pools` reported
|
|
17
|
+
`activeRef=SENSENOVA_API_KEY` while the pool's current key was `key_fallback_sensenova_key1`, and the same
|
|
18
|
+
response reported `envSource=file` — proof the write did land on `process.env` yet the provider still saw the
|
|
19
|
+
stored value. In effect the pool rotated in memory while every request kept authenticating with the old key.
|
|
20
|
+
- **The fix**: the plugin now hooks the one seam that is consulted **per request** —
|
|
21
|
+
`credentials.resolve(ref)`. `dsh-llm-pi-ai` (`:2563`), `dsh-llm-deepseek-api-key` (`:44`) and every other
|
|
22
|
+
credential consumer resolve their key through it, so wrapping that single method covers all providers. If
|
|
23
|
+
the resolved ref belongs to a pool, the wrapper returns the pool's current key; `describe()` is deliberately
|
|
24
|
+
left untouched so it still reports the *stored* truth.
|
|
25
|
+
- **All five `process.env` writes removed**, plus the two places that reverse-engineered the current key by
|
|
26
|
+
string-matching `process.env` (a degraded projection that returned the *boot-time* key). `currentRef` is now
|
|
27
|
+
the single source of truth — it survives `readPools()`/`resolvePoolKeys()` rebuilds, which the env write
|
|
28
|
+
never did.
|
|
29
|
+
- **Visible behaviour change**: `/pools` now reports `activeRef === currentRef`. Previously the two could
|
|
30
|
+
disagree (and `activeRef` showed the stale boot-time value).
|
|
31
|
+
- **Uninstall restores the original `resolve`** via `ctx.effect`, guarded by an identity check so it never
|
|
32
|
+
clobbers a wrapper installed after ours — a hot reload or fiber rebuild no longer stacks wrappers whose
|
|
33
|
+
closures hold expired services.
|
|
34
|
+
- **Re-send is still DSH's job.** The rotation handler only changes the pool's current ref and still calls
|
|
35
|
+
`next()`, so `dsh-llm-retry` keeps deciding whether/how often to re-send. Verified: `next()` is called on
|
|
36
|
+
every rotation path.
|
|
37
|
+
- **Verification** (all measured, no inference): the real official `@deepseek-ai/dsh-llm-pi-ai` adapter was
|
|
38
|
+
applied in-process with a real `LocalCredentialProvider` and a local mock upstream; the API key was captured
|
|
39
|
+
from the **actual outbound HTTP `Authorization` header** — `Bearer STORED-ENV-KEY` before the plugin,
|
|
40
|
+
`Bearer KEY-ONE-111` after pre-selection, `Bearer KEY-TWO-222` after a `RATE_LIMIT` rotation, back to
|
|
41
|
+
`STORED-ENV-KEY` after `dispose()`. Two further suites cover the edge cases (`enabled:false`, empty pool,
|
|
42
|
+
unresolvable env, `useKeyRef` lock, `activeRef`/`currentRef` agreement over HTTP, `dispose` idempotence,
|
|
43
|
+
non-matching error codes) — 14/14 and 9/9 pass.
|
|
44
|
+
|
|
45
|
+
## What v3.2.4 offers
|
|
46
|
+
|
|
47
|
+
- **Adds DSH `0.2.0-rc.1` to the peer enumeration** (2026-09-28) — code unchanged. The DSH **desktop** app
|
|
48
|
+
moved to `0.2.0-rc.1`, which made the `0.1.7-rc.2` declaration stale again: the gate skipped the bundle
|
|
49
|
+
(plugin page item gone, `/dsh-key-fallback/*` → 404 — a graceful skip, not a crash). Verification before
|
|
50
|
+
declaring:
|
|
51
|
+
1. **Tarball diff `0.1.7-rc.2` → `0.2.0-rc.1`, file by file** (SHA1 over every `lib/` file of both official
|
|
52
|
+
npm packages): `dsh-credentials`, `dsh-llm`, `dsh-settings`, `dsh-shell`, `dsh-tools`, `dsh-mcp-client`,
|
|
53
|
+
`dsh-web`, `dsh-agent` are **byte-identical** — the whole surface this plugin touches
|
|
54
|
+
(`credentials.resolve/describe/set/unset`, the `agent/request` + `agent/request-error` waterfall,
|
|
55
|
+
`settings.describe/replace`) is unchanged. (Only `dsh-session` changes, additively, plus `dsh-app-boot`
|
|
56
|
+
itself.)
|
|
57
|
+
2. **Gate execution**: ran the `0.2.0-rc.1` `evaluatePluginCompatibility()` against the new manifest → pass;
|
|
58
|
+
against the old 3.2.3 manifest → blocked on all four peers.
|
|
59
|
+
3. **Live assembly**: installed the official `@deepseek-ai/dsh@0.2.0-rc.1` (542 packages) in a sandbox with an
|
|
60
|
+
isolated `DSH_HOME` and ran `dsh --profile smoke --dump-config` → plugin present in the composed tree, no
|
|
61
|
+
gate skip, no errors.
|
|
62
|
+
4. **Published artifact check**: `npm pack` tarball re-verified to carry the new enumeration.
|
|
8
63
|
|
|
9
64
|
## What v3.2.3 offers
|
|
10
65
|
|
|
@@ -55,7 +110,7 @@ Current version: **v3.2.3** (`v7` UI generation).
|
|
|
55
110
|
|
|
56
111
|
- DeepSeek Harness with a `web`-profile GUI (`npx @deepseek-ai/dsh web`)
|
|
57
112
|
- Node.js `^22.19` or `>=24`
|
|
58
|
-
- Peer ranges strictly enumerate **only compatibility-tested versions** — `@deepseek-ai/dsh` `0.1.7-rc.1 || 0.1.7-rc.2` (the plugin's claimed DSH runtime; the gate that enforces it only exists from 0.1.7, so older runtimes just see an unrecognized peer), `@deepseek-ai/dsh-credentials` `0.1.0-rc.6 || 0.1.1-rc.2 || 0.1.2-alpha.1 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.7-rc.1 || 0.1.7-rc.2`, `@deepseek-ai/dsh-llm` / `@deepseek-ai/dsh-settings` `0.1.1-rc.2 || 0.1.2-alpha.1 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.7-rc.1 || 0.1.7-rc.2`, `@deepseek-ai/cordis` `4.0.1 || 4.0.2 || 4.0.4`, `@deepseek-ai/schemastery` `3.18.1 || 3.18.2 || 3.18.4`. No open-ended ranges (`<0.2.0`, caret): untested versions are deliberately excluded until verified. The plugin only uses the credential-reference half (`resolve`/`describe`/`set`/`unset`/`credentialRef` — stable since `0.1.0-rc.6`) and the `agent/request` + `agent/request-error` waterfall (payload unchanged across the enumerated versions); `isCredentialRefName` (added `rc.8`) is implemented locally for compatibility. The `0.1.2-alpha.2 → alpha.5 → 0.1.2-rc.1` companion packages are byte-identical (2026-09-03 verified), so DSH `0.1.2-rc.1` (`next`) needs no plugin change.
|
|
113
|
+
- Peer ranges strictly enumerate **only compatibility-tested versions** — `@deepseek-ai/dsh` `0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1` (the plugin's claimed DSH runtime; the gate that enforces it only exists from 0.1.7, so older runtimes just see an unrecognized peer), `@deepseek-ai/dsh-credentials` `0.1.0-rc.6 || 0.1.1-rc.2 || 0.1.2-alpha.1 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1`, `@deepseek-ai/dsh-llm` / `@deepseek-ai/dsh-settings` `0.1.1-rc.2 || 0.1.2-alpha.1 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1`, `@deepseek-ai/cordis` `4.0.1 || 4.0.2 || 4.0.4`, `@deepseek-ai/schemastery` `3.18.1 || 3.18.2 || 3.18.4`. No open-ended ranges (`<0.2.0`, caret): untested versions are deliberately excluded until verified. The plugin only uses the credential-reference half (`resolve`/`describe`/`set`/`unset`/`credentialRef` — stable since `0.1.0-rc.6`) and the `agent/request` + `agent/request-error` waterfall (payload unchanged across the enumerated versions); `isCredentialRefName` (added `rc.8`) is implemented locally for compatibility. The `0.1.2-alpha.2 → alpha.5 → 0.1.2-rc.1` companion packages are byte-identical (2026-09-03 verified), so DSH `0.1.2-rc.1` (`next`) needs no plugin change.
|
|
59
114
|
|
|
60
115
|
## What v3.2.0 offers
|
|
61
116
|
|
|
@@ -117,7 +172,7 @@ with live env-key values), create/update/delete (POST/DELETE round-trip, persist
|
|
|
117
172
|
**Settings → API Key 回退** — a top-level settings page with a redesigned UI (status dots, badges, gradient pool cards, per-key rows):
|
|
118
173
|
|
|
119
174
|
- **Configurable rotation triggers that are actually enforced** (`rotateOn`): click chips to select which error codes cause rotation. The preset chips cover the **full DSH `LlmError` standard code set** — `QUOTA` / `AUTH` / `RATE_LIMIT` / `TIMEOUT` / `TRANSPORT` / `SERVER` / `EMPTY_RESPONSE` / `INVALID_CREDENTIAL` — or add a non-standard/custom code (matched **exactly** against the provider's `failure.code`). What you save is what runs: there is no "magic upgrade" that rewrites three selected codes back into a six-code superset. `ABORTED` (user cancel) never triggers rotation.
|
|
120
|
-
- **真实当前使用 key 显示**: the page shows which key is actually being used right now (
|
|
175
|
+
- **真实当前使用 key 显示**: the page shows which key is actually being used right now (the pool's current ref — exactly what the credential seam returns to the adapter) — not a truncated hash, not a guessed name.
|
|
121
176
|
- **短 ref 命名**: new keys are auto-named `key_fallback_<provider>_key1`, `key_fallback_<provider>_key2`, … so the UI shows clean short names (`key1`, `key2`, … or your custom `label`) instead of long ref strings. Existing legacy long refs are migrated **once, automatically and idempotently** (write new ref → persist config → best-effort unset old ref; any failure aborts and retries safely).
|
|
122
177
|
- **明文揭示**: every key row has an eye toggle (`👁` / `🙈`) that fetches and shows the real value via `GET /keys/plain` (only for keys owned by that pool, or the pool's env key). The env key row also reveals plaintext — it is not swallowed by the read-only note.
|
|
123
178
|
- **环境密钥可编辑**: the pool's env key (`AGNES_API_KEY` etc.) can be updated right in the page when it is backed by the writable credential file. If it is supplied by the launching environment (read-only), the UI says so and refuses to edit (HTTP 400).
|
|
@@ -126,7 +181,9 @@ with live env-key values), create/update/delete (POST/DELETE round-trip, persist
|
|
|
126
181
|
|
|
127
182
|
## Rotation semantics
|
|
128
183
|
|
|
129
|
-
- **
|
|
184
|
+
- **How a key actually gets used**: DSH's credential seam is `credentials.resolve(ref)`. Every model adapter reads its key through it (`dsh-llm-pi-ai`, `dsh-llm-deepseek-api-key`, and any plugin that resolves a credential ref), and it is called **per request**. This plugin wraps that one method: when the resolved ref belongs to a configured pool, the wrapper returns the pool's current key instead of the stored value. Switching the key is therefore just changing the pool's current ref — the next request picks it up with no restart.
|
|
185
|
+
- Writing to `process.env` would **not** work: the credential provider reads the *launch environment snapshot* that DSH froze at boot (`{...process.env}` in `dsh-app-boot`, copied into a `Map` by `dsh-launch-environment`), so anything written to `process.env` after startup is invisible to it. DSH's own comment on the provider entry says the managed document "is never materialized into the process environment".
|
|
186
|
+
- **pick** (`agent/request`, pre-select): respects `useKeyRef` lock first (but a cooling locked key is skipped so the pool never deadlocks on a bad key), otherwise cursor round-robin over live keys. The chosen ref becomes the pool's current ref, which is what the credential seam hands to the adapter on the next resolve.
|
|
130
187
|
- **fail** (`agent/request-error`, registered `prepend` so it runs before `dsh-llm-retry`): only when the error matches the pool's `rotateOn` — by `failure.code` (exact), by HTTP status mapping (`429→RATE_LIMIT`, `401/403→AUTH`, `402→QUOTA`, `5xx→SERVER`), or by message keyword. Marks the failed key with a fixed `cooldownMs` (default 30 s), then switches to `nextRef` if configured, else the next live key.
|
|
131
188
|
- **re-send** is left entirely to DSH's `dsh-llm-retry` with the user's own per-provider retry policy. The two are independent and complementary: the retry plugin decides "retry the same key N times" (`retryPolicy.retryableCodes`, default `EMPTY_RESPONSE/RATE_LIMIT/SERVER/TIMEOUT/TRANSPORT` — which notably excludes `AUTH`/`QUOTA`); this plugin decides "switch to the next key". So e.g. an `AUTH` failure (which retry would not re-send anyway) still rotates to the next key — that next request will authenticate with the fresh key.
|
|
132
189
|
|
package/README.zh-CN.md
CHANGED
|
@@ -2,15 +2,60 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 简体中文
|
|
4
4
|
|
|
5
|
-
**为 [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) 提供多 key 池 +
|
|
5
|
+
**为 [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) 提供多 key 池 + 自动轮换**——插件挂在 DSH 的凭证接缝 `credentials.resolve` 上:每次适配器取 key 时,若该 ref 属于已配置的池,就返回池的当前 key;遇到配置的触发错误时,把失败 key 标记为冷却(固定 `cooldownMs`,无指数退避)并前进到下一把。**重发完全交给 DSH 自带的 `dsh-llm-retry`**——本插件从不自行重发,只负责换 key,重试策略由 retry policy 决定。
|
|
6
6
|
|
|
7
|
-
当前版本:**v3.
|
|
7
|
+
当前版本:**v3.3.0**(v7 UI 代)。
|
|
8
8
|
|
|
9
9
|
## 环境要求
|
|
10
10
|
|
|
11
11
|
- DeepSeek Harness 带 `web` profile GUI(`npx @deepseek-ai/dsh web`)
|
|
12
12
|
- Node.js `^22.19` 或 `>=24`
|
|
13
|
-
- peer 范围**只枚举已实际进行过兼容测试的版本**——`@deepseek-ai/dsh` `0.1.7-rc.1 || 0.1.7-rc.2`(本插件声明的 DSH 运行时;**执行该声明的门禁本身只有 0.1.7 才有**,更旧的运行时只是看到一条不认识的 peer 而已,不受影响)、`@deepseek-ai/dsh-credentials` `0.1.0-rc.6 || 0.1.1-rc.2 || 0.1.2-alpha.1 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.7-rc.1 || 0.1.7-rc.2`、`@deepseek-ai/dsh-llm` / `@deepseek-ai/dsh-settings` `0.1.1-rc.2 || 0.1.2-alpha.1 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.7-rc.1 || 0.1.7-rc.2`、`@deepseek-ai/cordis` `4.0.1 || 4.0.2 || 4.0.4`、`@deepseek-ai/schemastery` `3.18.1 || 3.18.2 || 3.18.4`。不使用 `<0.2.0`、caret 之类的开放范围:未测试版本在核查通过前刻意排除。插件只使用 credential-reference 半边(`resolve`/`describe`/`set`/`unset`/`credentialRef`,自 `0.1.0-rc.6` 起稳定)与 `agent/request` + `agent/request-error` waterfall(载荷跨上述枚举版本未变);`isCredentialRefName`(rc.8 新增)本地实现兜底。`0.1.2-alpha.2 → alpha.5 → 0.1.2-rc.1` 配套包逐字节一致(2026-09-03 复核),故 DSH `0.1.2-rc.1`(`next`)无需改动插件。
|
|
13
|
+
- peer 范围**只枚举已实际进行过兼容测试的版本**——`@deepseek-ai/dsh` `0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1`(本插件声明的 DSH 运行时;**执行该声明的门禁本身只有 0.1.7 才有**,更旧的运行时只是看到一条不认识的 peer 而已,不受影响)、`@deepseek-ai/dsh-credentials` `0.1.0-rc.6 || 0.1.1-rc.2 || 0.1.2-alpha.1 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1`、`@deepseek-ai/dsh-llm` / `@deepseek-ai/dsh-settings` `0.1.1-rc.2 || 0.1.2-alpha.1 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1`、`@deepseek-ai/cordis` `4.0.1 || 4.0.2 || 4.0.4`、`@deepseek-ai/schemastery` `3.18.1 || 3.18.2 || 3.18.4`。不使用 `<0.2.0`、caret 之类的开放范围:未测试版本在核查通过前刻意排除。插件只使用 credential-reference 半边(`resolve`/`describe`/`set`/`unset`/`credentialRef`,自 `0.1.0-rc.6` 起稳定)与 `agent/request` + `agent/request-error` waterfall(载荷跨上述枚举版本未变);`isCredentialRefName`(rc.8 新增)本地实现兜底。`0.1.2-alpha.2 → alpha.5 → 0.1.2-rc.1` 配套包逐字节一致(2026-09-03 复核),故 DSH `0.1.2-rc.1`(`next`)无需改动插件。
|
|
14
|
+
|
|
15
|
+
## v3.3.0 新增
|
|
16
|
+
|
|
17
|
+
- **换 key 现在真的能到 provider 了**(2026-09-29)——这是真 bug 修复,不是为整洁而重构。3.2.4 及之前,
|
|
18
|
+
插件靠写 `process.env[<池 env>] = <key 值>` 来换 key,而**这个写从来没到达凭证层**。DSH 在启动时把
|
|
19
|
+
`{ ...process.env }` 冻结成「启动环境快照」(`dsh-app-boot`),`dsh-launch-environment` 把它拷进
|
|
20
|
+
`Map`,`dsh-credentials-local.inherited()` 只读这个快照——所以启动之后写 `process.env` 它看不见。
|
|
21
|
+
实机(0.2.0-rc.1)实测:`GET /dsh-key-fallback/pools` 报 `activeRef=SENSENOVA_API_KEY`,而池的当前 key
|
|
22
|
+
是 `key_fallback_sensenova_key1`;同一响应又报 `envSource=file`——既证明写确实落到了真实
|
|
23
|
+
`process.env`,又证明确实没生效。结果是:池在内存里轮换了,每个请求却仍用启动时那把旧 key 鉴权。
|
|
24
|
+
- **修法**:改为挂接唯一「每次请求都问一次」的接缝 `credentials.resolve(ref)`。`dsh-llm-pi-ai`
|
|
25
|
+
(`:2563`)、`dsh-llm-deepseek-api-key`(`:44`)以及所有凭证消费者都经它取 key,因此包装这一个方法
|
|
26
|
+
即可覆盖全部 provider。若解析的 ref 属于某个池,包装层返回池的当前 key;`describe()` 刻意不动,
|
|
27
|
+
仍报告存储里的真实值。
|
|
28
|
+
- **删掉全部 5 处 `process.env` 写入**,以及两处「用 `process.env` 字符串反推当前 key」的逻辑(那是个
|
|
29
|
+
劣化投影,只会返回启动时那把)。`currentRef` 现在是唯一权威——它能跨 `readPools()` /
|
|
30
|
+
`resolvePoolKeys()` 重建保留,而 env 写入本来就做不到。
|
|
31
|
+
- **可见行为变化**:`/pools` 现在报 `activeRef === currentRef`。之前两者可能不一致(`activeRef` 显示
|
|
32
|
+
的是启动时的陈旧值)。
|
|
33
|
+
- **卸载会还原原始 `resolve`**(经 `ctx.effect`),并带身份校验,绝不覆盖「在我们之后安装的」包装层——
|
|
34
|
+
热重载/fiber 重建不再层层叠加、留下持有过期 service 的闭包。
|
|
35
|
+
- **重发仍归 DSH 管**:轮换处理只改池的当前 ref,并且仍然调用 `next()`,由 `dsh-llm-retry` 继续决定是否/
|
|
36
|
+
重发几次。实测:每条轮换路径都调用了 `next()`。
|
|
37
|
+
- **验证方式**(全部实测,无推断):在进程内用真实的官方 `@deepseek-ai/dsh-llm-pi-ai` 适配器 +
|
|
38
|
+
真实 `LocalCredentialProvider` + 本地 mock 上游,从**真实出站 HTTP `Authorization` 头**里抓 key ——
|
|
39
|
+
装插件前 `Bearer STORED-ENV-KEY`,预选后 `Bearer KEY-ONE-111`,`RATE_LIMIT` 轮换后
|
|
40
|
+
`Bearer KEY-TWO-222`,`dispose()` 后回到 `STORED-ENV-KEY`。另有两套边界用例(`enabled:false`、空池、
|
|
41
|
+
env 解析不到、`useKeyRef` 锁定、HTTP 上 `activeRef`/`currentRef` 一致、`dispose` 幂等、不匹配的错误码)
|
|
42
|
+
——14/14 与 9/9 全绿。
|
|
43
|
+
|
|
44
|
+
## v3.2.4 新增
|
|
45
|
+
|
|
46
|
+
- **peer 枚举追加 DSH `0.2.0-rc.1`**(2026-09-28)——**代码零改动**。DSH 桌面端升到 `0.2.0-rc.1` 后,
|
|
47
|
+
`0.1.7-rc.2` 的声明再次过时:门禁跳过 bundle(设置页里本插件整项消失、`/dsh-key-fallback/*` → **404**;
|
|
48
|
+
是**优雅跳过**而非崩溃)。声明前核查四步:
|
|
49
|
+
1. **tarball 逐文件 diff(`0.1.7-rc.2` → `0.2.0-rc.1`)**:`dsh-credentials`、`dsh-llm`、`dsh-settings`、
|
|
50
|
+
`dsh-shell`、`dsh-tools`、`dsh-mcp-client`、`dsh-web`、`dsh-agent` 的**全部 `lib/` 文件 SHA1 一致**——
|
|
51
|
+
本插件用到的整个调用面(`credentials.resolve/describe/set/unset`、`agent/request` +
|
|
52
|
+
`agent/request-error` waterfall、`settings.describe/replace`)逐字节未变(变化的只有 `dsh-session`
|
|
53
|
+
的附加式新增与 `dsh-app-boot` 门禁本体)。
|
|
54
|
+
2. **门禁执行**:用 `0.2.0-rc.1` 的 `evaluatePluginCompatibility()` 跑新声明 → **放行**;
|
|
55
|
+
跑 3.2.3 的旧声明 → **四条 peer 全部拦截**。
|
|
56
|
+
3. **真机装配**:沙箱安装官方 `@deepseek-ai/dsh@0.2.0-rc.1`(542 包)+ 独立 `DSH_HOME`,
|
|
57
|
+
`dsh --profile smoke --dump-config` → 插件入树、零门禁拦截、零错误。
|
|
58
|
+
4. **发布件复核**:`npm pack` 产物内 `package.json` 已含新枚举。
|
|
14
59
|
|
|
15
60
|
## v3.2.3 新增
|
|
16
61
|
|
|
@@ -112,7 +157,7 @@
|
|
|
112
157
|
**设置 → API Key 回退** —— 顶层设置页,全新 UI(状态点、徽章、渐变池卡片、逐 key 行):
|
|
113
158
|
|
|
114
159
|
- **可配置且真正生效的轮转触发码**(`rotateOn`):点选 chips 决定哪些错误码触发轮换。预设 chips 覆盖 **DSH `LlmError` 标准码全集**——`QUOTA` / `AUTH` / `RATE_LIMIT` / `TIMEOUT` / `TRANSPORT` / `SERVER` / `EMPTY_RESPONSE` / `INVALID_CREDENTIAL`——也可添加非标准/自定义错误码(与 provider 的 `failure.code` **精确匹配**)。你保存什么就执行什么:不存在"把选中的三个码偷偷变回六码超集"的魔法。`ABORTED`(用户取消)永不触发轮换。
|
|
115
|
-
- **真实当前使用 key 显示**:页面显示当前真正在用哪把 key
|
|
160
|
+
- **真实当前使用 key 显示**:页面显示当前真正在用哪把 key(池的当前 ref —— 正是凭证接缝返回给适配器的那把)——不是截断的哈希,不是猜的名字。
|
|
116
161
|
- **短 ref 命名**:新 key 自动命名为 `key_fallback_<provider>_key1`、`key_fallback_<provider>_key2`、…,UI 显示干净的短名(`key1`、`key2`、…或你的自定义 `label`)。已有旧长 ref **一次性、幂等地自动迁移**(写新 ref → 持久化配置 → best-effort 删旧 ref;任何一步失败即中止、安全可重试)。
|
|
117
162
|
- **明文揭示**:每行有眼睛开关(`👁` / `🙈`),经 `GET /keys/plain` 显示真实值(仅限本池的 key 或本池 env key)。env key 行也会显示明文——不会被只读说明吞掉。
|
|
118
163
|
- **环境密钥可编辑**:池的 env key(`AGNES_API_KEY` 等)在凭证文件可写时可直接在页面里改。若由启动环境提供(只读),UI 会说明并拒绝编辑(HTTP 400)。
|
|
@@ -121,7 +166,9 @@
|
|
|
121
166
|
|
|
122
167
|
## 轮换语义
|
|
123
168
|
|
|
124
|
-
- **
|
|
169
|
+
- **key 到底怎么被用上**:DSH 的凭证接缝是 `credentials.resolve(ref)`。每个模型适配器都经它取 key(`dsh-llm-pi-ai`、`dsh-llm-deepseek-api-key`,以及任何解析凭证引用的插件),且**每次请求都会调用一次**。本插件包装的就是这一个方法:当解析的 ref 属于已配置的池时,包装层返回池的当前 key,而不是存储里的值。所以"换 key"就是改池的当前 ref —— 下一次请求立刻生效,无需重启。
|
|
170
|
+
- 写 `process.env` **不管用**:凭证 provider 读的是 DSH 启动时冻结的「启动环境快照」(`dsh-app-boot` 里的 `{...process.env}`,由 `dsh-launch-environment` 拷进 `Map`),启动之后再改 `process.env` 对它不可见。DSH 自己在 provider 条目上的注释也写着 managed document "is never materialized into the process environment"。
|
|
171
|
+
- **pick**(`agent/request`,预选):先尊重 `useKeyRef` 锁定(但冷却中的锁定 key 会跳过,避免池在某把坏 key 上死锁),否则按游标在 live key 上轮询。选中的 ref 成为池的当前 ref —— 也就是凭证接缝下一次 resolve 交给适配器的那把。
|
|
125
172
|
- **fail**(`agent/request-error`,注册 `prepend` 保证先于 `dsh-llm-retry` 看到错误):仅当错误匹配本池 `rotateOn` 时才处理——按 `failure.code`(精确)、按 HTTP 状态映射(`429→RATE_LIMIT`、`401/403→AUTH`、`402→QUOTA`、`5xx→SERVER`)、或按消息关键字。把失败 key 标记为固定 `cooldownMs`(默认 30 s),然后切到 `nextRef`(若配置)否则下一把 live key。
|
|
126
173
|
- **re-send** 完全交给 DSH 的 `dsh-llm-retry` 与用户自己的 per-provider 重试策略。两者独立互补:重试插件决定"同一把 key 重发几次"(`retryPolicy.retryableCodes` 默认 `EMPTY_RESPONSE/RATE_LIMIT/SERVER/TIMEOUT/TRANSPORT`——注意**不含 AUTH/QUOTA**);本插件决定"换下一把 key"。例如 `AUTH` 失败(重试本来也不会重发,重发也白搭)仍会轮换到下一把——下一次请求就会用新 key 鉴权。
|
|
127
174
|
|
package/lib/index.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// v6 = v5 全量功能 + 用户要求的修复与增强:
|
|
4
4
|
// - 轮转触发码真正生效:agent/request-error 按池的 rotateOn 判断(可配置、chips 可点选),
|
|
5
5
|
// 数字 status 与消息关键字按所选触发码映射;默认集=旧行为超集(无回归)。
|
|
6
|
-
// - 「当前使用」显示修复:GET /pools 返回 activeRef(=
|
|
7
|
-
// POST/PATCH 改 useKeyRef
|
|
6
|
+
// - 「当前使用」显示修复:GET /pools 返回 activeRef(= 池的 currentRef,即 resolve 包装
|
|
7
|
+
// 实际返回的那把 key),POST/PATCH 改 useKeyRef 后同步刷新内存池,UI 立刻显示真正在用的 key。
|
|
8
8
|
// - key 短命名:新增 key 自动命名为 key_fallback_<provider>_key<N>;
|
|
9
9
|
// 旧的长 ref 在首次加载时一次性迁移(refsCompacted 标记,幂等,先 set 新 ref 再 unset 旧 ref)。
|
|
10
10
|
// - 明文揭示:GET /keys/plain?provider=&ref= 返回真实值(走 rawResolve,绕过池 wrap)。
|
|
@@ -289,13 +289,16 @@ export function apply(ctx, config) {
|
|
|
289
289
|
diag('migrate', 'imported ' + names.length + ' provider(s) from settings.yaml(.imported): ' + names.join(','))
|
|
290
290
|
} catch (e) { diag('migrate', 'failed: ' + ((e && e.message) || e)) }
|
|
291
291
|
}
|
|
292
|
-
// ── 包装 credentials.resolve:ref 命中池 env 时返回池当前 key
|
|
292
|
+
// ── 包装 credentials.resolve:ref 命中池 env 时返回池当前 key ──
|
|
293
|
+
// 这是 DSH 官方唯一的「每次请求都问一次用哪把 key」接缝(dsh-llm-pi-ai:2563、
|
|
294
|
+
// dsh-llm-deepseek-api-key:44、dsh-vision-bridge:91、dsh-cost-meter:957 等全部经它取值),
|
|
295
|
+
// 因此拦这一处即可覆盖所有 provider,且语义与「重发归官方、插件只切 key」完全同构。
|
|
293
296
|
const poolsByEnv = new Map()
|
|
294
297
|
const credSvc = ctx.credentials
|
|
295
298
|
// 原始 resolve(未 wrap 版本):池重建/读 env 真实值必须走它,否则 wrap 会短路成 currentRef 的值(污染)
|
|
296
299
|
const rawResolve = (credSvc && typeof credSvc.resolve === 'function') ? credSvc.resolve.bind(credSvc) : null
|
|
297
300
|
if (credSvc && typeof credSvc.resolve === 'function') {
|
|
298
|
-
|
|
301
|
+
const wrappedResolve = async (ref) => {
|
|
299
302
|
let result
|
|
300
303
|
try { result = await rawResolve(ref) } catch (e) { result = undefined }
|
|
301
304
|
const refName = String(ref || '')
|
|
@@ -306,7 +309,14 @@ export function apply(ctx, config) {
|
|
|
306
309
|
}
|
|
307
310
|
return result
|
|
308
311
|
}
|
|
312
|
+
credSvc.resolve = wrappedResolve
|
|
309
313
|
diag('apply', 'credentials.resolve wrapped')
|
|
314
|
+
// 卸载时还原原始 resolve:热重载 / fiber 重建会重新 apply,不还原则 wrap 逐层叠加,
|
|
315
|
+
// 且旧层闭包持有过期 service 与已清空的 poolsByEnv。仅在仍是本层 wrap 时还原,
|
|
316
|
+
// 避免把后来者的 wrap 覆盖掉。
|
|
317
|
+
ctx.effect(() => () => {
|
|
318
|
+
try { if (credSvc.resolve === wrappedResolve) credSvc.resolve = rawResolve } catch (e) {}
|
|
319
|
+
})
|
|
310
320
|
}
|
|
311
321
|
|
|
312
322
|
const pools = new Map() // provider -> { cfg, keyValues, cursor, currentRef }
|
|
@@ -480,18 +490,21 @@ export function apply(ctx, config) {
|
|
|
480
490
|
return undefined
|
|
481
491
|
}
|
|
482
492
|
|
|
483
|
-
// 把 useKeyRef
|
|
493
|
+
// 把 useKeyRef 选择立即落到内存池,「当前使用」立刻真实。
|
|
494
|
+
// 注意:这里不再写 process.env —— provider 认证走 credentials.resolve,而 DSH 的凭据层读的是
|
|
495
|
+
// 启动时冻结的 launchEnvironment 快照(dsh-app-boot:3435 → dsh-launch-environment:30-35),
|
|
496
|
+
// 运行期改 process.env 对它不可见(官方注释亦称 managed document "is never materialized
|
|
497
|
+
// into the process environment")。真正的切换点只有上面的 resolve 包装。
|
|
484
498
|
function applySelection(pool, ref) {
|
|
485
499
|
if (!pool || !ref) return
|
|
486
500
|
const kv = pool.keyValues.find((k) => k.ref === ref)
|
|
487
501
|
if (!kv) return
|
|
488
502
|
pool.currentRef = ref
|
|
489
|
-
try { process.env[pool.cfg.env] = kv.value } catch (e) {}
|
|
490
503
|
}
|
|
491
504
|
|
|
492
|
-
// ──
|
|
493
|
-
//
|
|
494
|
-
//
|
|
505
|
+
// ── 预热:全局 agent/request(DSH 全局收得到这个 waterfall)每次请求重读 settings、选 key,
|
|
506
|
+
// 写内存池 currentRef;真正的「这次用哪把 key」由上面的 credentials.resolve 包装在
|
|
507
|
+
// 适配器取值时给出,无需(也无法)预写环境变量。
|
|
495
508
|
async function warmPools() {
|
|
496
509
|
readPools()
|
|
497
510
|
for (const name of pools.keys()) {
|
|
@@ -509,11 +522,11 @@ export function apply(ctx, config) {
|
|
|
509
522
|
}
|
|
510
523
|
}
|
|
511
524
|
|
|
512
|
-
// 启动时预热 pools(resolve keyValues
|
|
525
|
+
// 启动时预热 pools(resolve keyValues,供 pickKey 预选用)
|
|
513
526
|
try { warmPools().catch(() => {}) } catch (e) {}
|
|
514
527
|
|
|
515
528
|
ctx.on('agent/request', (payload, next) => {
|
|
516
|
-
// next() 是 async
|
|
529
|
+
// next() 是 async;用 then 链在拿到 config 后同步更新内存池,立即返回 p 不阻塞 waterfall
|
|
517
530
|
const p = next()
|
|
518
531
|
Promise.resolve(p).then((config) => {
|
|
519
532
|
try {
|
|
@@ -529,7 +542,6 @@ export function apply(ctx, config) {
|
|
|
529
542
|
if (!key) return config
|
|
530
543
|
pool.currentRef = key.ref
|
|
531
544
|
diag('agent/request', 'PICK ' + key.ref + ' env=' + pool.cfg.env)
|
|
532
|
-
try { process.env[pool.cfg.env] = key.value } catch (e) {}
|
|
533
545
|
try { backupFile(CRED_FILE, 'credentials') } catch (e) {}
|
|
534
546
|
} catch (e) {}
|
|
535
547
|
return config
|
|
@@ -548,17 +560,20 @@ export function apply(ctx, config) {
|
|
|
548
560
|
const pool = await getPool(provider)
|
|
549
561
|
if (!pool || pool.keyValues.length === 0 || pool.cfg.enabled === false) return next()
|
|
550
562
|
if (!shouldRotate(pool, canonicalCode(code), rawMsg, status)) return next()
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
563
|
+
// 当前 key 只信 currentRef:它是跨 readPools()/resolvePoolKeys() 重建保留的权威值
|
|
564
|
+
// (readPools 只对新建 provider 置空、resolvePoolKeys 全程不碰它),
|
|
565
|
+
// 不再从 process.env 反推 —— 那是它的劣化投影,且运行期改 env 对凭据层不可见。
|
|
566
|
+
const curRef = pool.currentRef
|
|
567
|
+
// 认不出当前 key 时不乱罚:宁可只轮换不冷却,也不要误伤一把没用过的 key
|
|
568
|
+
// (正常情况下 currentRef 已被 agent/request 或 warmPools 置好,此分支近乎不可达)
|
|
569
|
+
if (curRef) markFailed(pool, curRef, code, rawMsg)
|
|
570
|
+
diag('request-error', 'marked ' + (curRef || '(none)'))
|
|
558
571
|
// 失败后优先按 nextRef 轮换(用户配置的顺序);useKeyRef 只决定初始选择
|
|
559
572
|
const nextKey = nextKeyFor(pool, curRef)
|
|
560
573
|
if (nextKey) {
|
|
561
|
-
|
|
574
|
+
// 只改内存池:下一次 credentials.resolve 就会返回新 key。
|
|
575
|
+
// 重发由 DSH 官方 dsh-llm-retry 按用户自己的 retryPolicy 完成,本插件不干预。
|
|
576
|
+
try { await backupFile(CRED_FILE, 'credentials') } catch (e) {}
|
|
562
577
|
pool.currentRef = nextKey.ref
|
|
563
578
|
} else {
|
|
564
579
|
// 无 nextRef 可选时:若 useKeyRef 锁定 key 仍 live 则退回它,否则放弃
|
|
@@ -576,8 +591,6 @@ export function apply(ctx, config) {
|
|
|
576
591
|
if (webServer) {
|
|
577
592
|
const route = async (req, res) => {
|
|
578
593
|
try {
|
|
579
|
-
// 首次请求时补做 legacy 池迁移(此时 fiber 已 ACTIVE,settings.replace 可用)。
|
|
580
|
-
// 放在所有分支之前,保证无论 UI 先请求哪个端点,池都已被搬进 live 配置。
|
|
581
594
|
// 首次请求时补做 legacy 池迁移(此时 fiber 已 ACTIVE,settings.replace 可用)。
|
|
582
595
|
// 放在所有分支之前,保证无论 UI 先请求哪个端点,池都已被搬进 live 配置。
|
|
583
596
|
await ensureLegacyMigration()
|
|
@@ -622,12 +635,13 @@ export function apply(ctx, config) {
|
|
|
622
635
|
for (const [name, pool] of pools) {
|
|
623
636
|
await resolvePoolKeys(pool, name)
|
|
624
637
|
const now = Date.now()
|
|
625
|
-
// 真正当前使用的 key
|
|
638
|
+
// 真正当前使用的 key = 池的 currentRef —— 它就是 resolve 包装返回的那把。
|
|
639
|
+
// 不再用 process.env 反推:运行期改 env 对凭据层(启动快照)不可见,
|
|
640
|
+
// 反推会得到启动时那把旧 key,与 resolve 实际返回值不一致。
|
|
626
641
|
let activeRef = ''
|
|
627
|
-
if (pool.cfg.enabled !== false) {
|
|
628
|
-
const
|
|
629
|
-
if (
|
|
630
|
-
if (!activeRef && pool.currentRef) activeRef = pool.currentRef
|
|
642
|
+
if (pool.cfg.enabled !== false && pool.currentRef) {
|
|
643
|
+
const cur = pool.keyValues.find((k) => k.ref === pool.currentRef)
|
|
644
|
+
if (cur) activeRef = cur.ref
|
|
631
645
|
}
|
|
632
646
|
// 环境密钥可写性(描述:文件=可写,启动环境=只读)
|
|
633
647
|
let envWritable = null
|
|
@@ -702,13 +716,13 @@ export function apply(ctx, config) {
|
|
|
702
716
|
try { validated = profileSchema(upd) } catch (e) { return sendJson(res, 400, { error: 'invalid profile: ' + (e && e.message || e) }) }
|
|
703
717
|
cfg.providers[provider] = validated
|
|
704
718
|
writeSettings(cfg)
|
|
705
|
-
//
|
|
719
|
+
// 同步内存池,让「当前使用」立刻真实(切 key 由 resolve 包装在下次取值时生效)
|
|
706
720
|
try {
|
|
707
721
|
const pool = await getPool(provider)
|
|
708
722
|
if (pool) {
|
|
709
723
|
if (validated.useKeyRef) applySelection(pool, validated.useKeyRef)
|
|
710
724
|
else if (pool.currentRef && !pool.keyValues.find((k) => k.ref === pool.currentRef && isLive(k))) {
|
|
711
|
-
const pk = pickKey(pool); if (pk)
|
|
725
|
+
const pk = pickKey(pool); if (pk) pool.currentRef = pk.ref
|
|
712
726
|
}
|
|
713
727
|
}
|
|
714
728
|
} catch (e) {}
|
|
@@ -805,7 +819,7 @@ export function apply(ctx, config) {
|
|
|
805
819
|
const pool = pools.get(provider)
|
|
806
820
|
if (pool) {
|
|
807
821
|
pool.keyValues = pool.keyValues.filter((k) => k.ref !== ref)
|
|
808
|
-
if (pool.currentRef === ref) { pool.currentRef = ''; try { const pk = pickKey(pool); if (pk)
|
|
822
|
+
if (pool.currentRef === ref) { pool.currentRef = ''; try { const pk = pickKey(pool); if (pk) pool.currentRef = pk.ref } catch (e) {} }
|
|
809
823
|
}
|
|
810
824
|
} catch (e) {}
|
|
811
825
|
return sendJson(res, 200, { ok: true })
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omdp/dsh-key-fallback",
|
|
3
3
|
"description": "API Key 回退轮换插件:多 key 池 + 自动轮换(触发码可配置,含 LlmError 标准码全集)+ 环境密钥编辑 + 明文揭示 + 短 ref 迁移。同时兼容 DSH 0.1.5-rc.3(settings.yaml 后端)与 0.1.7+(profile entry config 后端)。Settings → API Key 回退 UI. v7.",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.3.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@deepseek-ai/cordis": "4.0.1 || 4.0.2 || 4.0.4",
|
|
42
|
-
"@deepseek-ai/dsh": "0.1.7-rc.1 || 0.1.7-rc.2",
|
|
43
|
-
"@deepseek-ai/dsh-credentials": "0.1.0-rc.6 || 0.1.1-rc.2 || 0.1.2-alpha.1 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.7-rc.1 || 0.1.7-rc.2",
|
|
44
|
-
"@deepseek-ai/dsh-llm": "0.1.1-rc.2 || 0.1.2-alpha.1 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.7-rc.1 || 0.1.7-rc.2",
|
|
45
|
-
"@deepseek-ai/dsh-settings": "0.1.1-rc.2 || 0.1.2-alpha.1 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.7-rc.1 || 0.1.7-rc.2",
|
|
42
|
+
"@deepseek-ai/dsh": "0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1",
|
|
43
|
+
"@deepseek-ai/dsh-credentials": "0.1.0-rc.6 || 0.1.1-rc.2 || 0.1.2-alpha.1 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1",
|
|
44
|
+
"@deepseek-ai/dsh-llm": "0.1.1-rc.2 || 0.1.2-alpha.1 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1",
|
|
45
|
+
"@deepseek-ai/dsh-settings": "0.1.1-rc.2 || 0.1.2-alpha.1 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1",
|
|
46
46
|
"@deepseek-ai/schemastery": "3.18.1 || 3.18.2 || 3.18.4"
|
|
47
47
|
}
|
|
48
48
|
}
|