@omdp/dsh-key-fallback 3.3.0 → 3.3.2
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/LICENSE +21 -0
- package/README.md +65 -33
- package/README.zh-CN.md +54 -27
- package/package.json +5 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 XJungit
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -4,43 +4,75 @@
|
|
|
4
4
|
|
|
5
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
6
|
|
|
7
|
-
Current version: **v3.3.
|
|
7
|
+
Current version: **v3.3.2** (`v7` UI generation).
|
|
8
|
+
|
|
9
|
+
## What v3.3.2 offers
|
|
10
|
+
|
|
11
|
+
- **Peer declaration switched to the triple range `>=0.2.0-rc.1 <0.2.1-0` — zero code change**
|
|
12
|
+
(2026-09-30). Background: after DSH `0.2.0-rc.2` shipped, the gate blocked 3.3.1 exactly as it had
|
|
13
|
+
blocked 3.2.4 on `0.2.0-rc.1` — the **third consecutive round** of the same failure (settings item
|
|
14
|
+
gone, routes 404/405). The root cause was never incompatibility but per-version enumeration going
|
|
15
|
+
stale on every rc. Evidence: a full 20-package file-by-file SHA256 diff `0.2.0-rc.1 → rc.2` shows the
|
|
16
|
+
six peers this plugin declares change **only in `package.json` version strings**; the real gate
|
|
17
|
+
function was then run over the whole runtime matrix — `0.2.0-rc.1` / `rc.2` / `rc.3` / `rc.9` /
|
|
18
|
+
stable `0.2.0` all **PASS**, `0.2.1-rc.1` / `0.3.0-rc.1` all **BLOCK**.
|
|
19
|
+
⚠️ **This narrows support**: the `0.1.7` line is **no longer declared** — use **3.3.1** if you need it.
|
|
20
|
+
⚠️ The upper bound must be `<0.2.1-0`, never `<0.2.1`: semver sorts prereleases *before* the stable
|
|
21
|
+
release, so `0.2.1-rc.1 < 0.2.1` holds and `<0.2.1` would leak the next triple's rc.
|
|
22
|
+
|
|
23
|
+
## What v3.3.1 offers
|
|
24
|
+
|
|
25
|
+
- **Documentation correction — no code change** (2026-09-29). The v3.3.0 release notes asserted that the plugin
|
|
26
|
+
had never actually switched keys, because the `process.env` write never reached the credential layer. **That
|
|
27
|
+
assertion was wrong and is retracted here.** The `credentials.resolve` wrapper that supplies the key has
|
|
28
|
+
existed since **v3.0.0** (`git log -S 'credSvc.resolve =' -- dsh-key-fallback/lib/index.js` → `75ea76a`), and
|
|
29
|
+
the **published 3.2.4 tarball** already contains both that wrapper and its `pool-hit` diagnostic (verified by
|
|
30
|
+
downloading `@omdp/dsh-key-fallback@3.2.4` from the registry and reading `lib/index.js:298` / `:305`).
|
|
31
|
+
Rotation was already reaching the provider; the redundant `process.env` writes sat *alongside* the working
|
|
32
|
+
wrapper rather than being the mechanism. v3.3.0's real content is the smaller, accurate list in the section
|
|
33
|
+
below.
|
|
34
|
+
- **Live production evidence that rotation works** — captured on the real desktop install (DSH `0.2.0-rc.1`,
|
|
35
|
+
plugin v3.3.0) while a chat was routed through a pooled provider, from `GET /dsh-key-fallback/diag`:
|
|
36
|
+
200 entries (`resolve` 57 / `agent/request` 51 / `request-error` 90), with the pool's two keys used
|
|
37
|
+
**26 and 25 times** and alternating on each failure:
|
|
38
|
+
`PICK key_fallback_sensenova_key1` → `RATE_LIMIT` → `marked key_fallback_sensenova_key1` →
|
|
39
|
+
`PICK SENSENOVA_API_KEY` → … → `pool-hit SENSENOVA_API_KEY -> key_fallback_sensenova_key1`.
|
|
40
|
+
Each `pool-hit` line names the key that was current at that instant, which is exactly what the adapter
|
|
41
|
+
received on the wire.
|
|
8
42
|
|
|
9
43
|
## What v3.3.0 offers
|
|
10
44
|
|
|
11
|
-
- **
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
`credentials.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
- **Visible behaviour change**: `/pools` now reports `activeRef === currentRef`. Previously the two could
|
|
30
|
-
disagree (and `activeRef` showed the stale boot-time value).
|
|
45
|
+
- **Four real fixes** (2026-09-29). **Note**: the original v3.3.0 notes claimed this release made key switching
|
|
46
|
+
"actually reach the provider" for the first time. That was wrong — see the v3.3.1 correction above. What
|
|
47
|
+
v3.3.0 really changed:
|
|
48
|
+
- **`activeRef` no longer misreports the current key.** `/pools` used to derive `activeRef` by matching the
|
|
49
|
+
value of `process.env[<pool env>]` against the pool's members and falling back to `currentRef` only when no
|
|
50
|
+
member matched — so it could highlight **the wrong key**. On a live 0.2.0-rc.1 install it reported
|
|
51
|
+
`activeRef=SENSENOVA_API_KEY` while `currentRef` was `key_fallback_sensenova_key1`. `activeRef` is now
|
|
52
|
+
simply `currentRef`, so the two always agree.
|
|
53
|
+
- **Five redundant `process.env` writes removed.** DSH freezes `{ ...process.env }` into a *launch
|
|
54
|
+
environment snapshot* at boot (`dsh-app-boot`), `dsh-launch-environment` copies that into a `Map`, and
|
|
55
|
+
`dsh-credentials-local.inherited()` reads only that snapshot — so a post-boot `process.env` write is
|
|
56
|
+
invisible to the credential layer. The writes were therefore dead weight for authentication (the `resolve`
|
|
57
|
+
wrapper is what supplies the key). Removing them also stops the plugin from mutating the host process
|
|
58
|
+
environment. **Behaviour note**: a tool that spawns a subprocess inheriting `process.env` now sees the
|
|
59
|
+
launch-time value rather than the rotated one — no credential consumer depends on it, because every one of
|
|
60
|
+
them resolves through `credentials.resolve`, which the wrapper intercepts.
|
|
61
|
+
- **Failure attribution is guarded.** `agent/request-error` used to fall back to blaming `keyValues[0]` when
|
|
62
|
+
the current key could not be identified; it now only marks a key it can actually name.
|
|
31
63
|
- **Uninstall restores the original `resolve`** via `ctx.effect`, guarded by an identity check so it never
|
|
32
64
|
clobbers a wrapper installed after ours — a hot reload or fiber rebuild no longer stacks wrappers whose
|
|
33
65
|
closures hold expired services.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
66
|
+
- **Unchanged**: re-send remains DSH's job. The rotation handler only changes the pool's current ref and still
|
|
67
|
+
calls `next()`, so `dsh-llm-retry` keeps deciding whether and how often to re-send. Verified: `next()` is
|
|
68
|
+
called on every rotation path.
|
|
69
|
+
- **Verification** (all measured, no inference): the real official `@deepseek-ai/dsh-llm-pi-ai` adapter was
|
|
70
|
+
applied in-process with a real `LocalCredentialProvider` and a local mock upstream; the API key was captured
|
|
71
|
+
from the **actual outbound HTTP `Authorization` header** — `Bearer STORED-ENV-KEY` before the plugin,
|
|
72
|
+
`Bearer KEY-ONE-111` after pre-selection, `Bearer KEY-TWO-222` after a `RATE_LIMIT` rotation, back to
|
|
73
|
+
`STORED-ENV-KEY` after `dispose()`. Two further suites cover the edge cases (`enabled:false`, empty pool,
|
|
74
|
+
unresolvable env, `useKeyRef` lock, `activeRef`/`currentRef` agreement over HTTP, `dispose` idempotence,
|
|
75
|
+
non-matching error codes) — 14/14 and 9/9 pass. The live production timeline is in v3.3.1 above.
|
|
44
76
|
|
|
45
77
|
## What v3.2.4 offers
|
|
46
78
|
|
|
@@ -110,7 +142,7 @@ Current version: **v3.3.0** (`v7` UI generation).
|
|
|
110
142
|
|
|
111
143
|
- DeepSeek Harness with a `web`-profile GUI (`npx @deepseek-ai/dsh web`)
|
|
112
144
|
- Node.js `^22.19` or `>=24`
|
|
113
|
-
- Peer ranges
|
|
145
|
+
- Peer ranges use **per-triple intervals** (since 3.3.2; per-version enumeration before that) — `@deepseek-ai/dsh` / `@deepseek-ai/dsh-credentials` / `@deepseek-ai/dsh-llm` / `@deepseek-ai/dsh-settings` all declare `>=0.2.0-rc.1 <0.2.1-0`: testing the first rc of a triple clears its later rc builds (`0.2.0-rc.2`/`rc.3`…) and the stable `0.2.0`, while crossing a triple (`0.2.1-rc.1`) is still gated off and requires a fresh check. The `-0` upper bound is mandatory: semver sorts prereleases *before* the stable release, so `<0.2.1` would leak the next triple's rc (see `AGENTS.md` rule 3). `@deepseek-ai/cordis` `4.0.1 || 4.0.2 || 4.0.4` and `@deepseek-ai/schemastery` `3.18.1 || 3.18.2 || 3.18.4` stay **enumerated** (neither goes through the gate, which only inspects `@deepseek-ai/dsh` and `dsh-*`, and they have sparse version sets with no prerelease chain). 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 versions); `isCredentialRefName` (added `rc.8`) is implemented locally for compatibility. **Note the `0.1.7` line is no longer declared as of 3.3.2** — install 3.3.1 if you need it.
|
|
114
146
|
|
|
115
147
|
## What v3.2.0 offers
|
|
116
148
|
|
|
@@ -182,7 +214,7 @@ with live env-key values), create/update/delete (POST/DELETE round-trip, persist
|
|
|
182
214
|
## Rotation semantics
|
|
183
215
|
|
|
184
216
|
- **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".
|
|
217
|
+
- 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". **History**: up to 3.2.4 the plugin *also* wrote `process.env[<pool env>]`, but always **alongside** the wrapper above — that write was redundant rather than the mechanism, so rotation worked regardless. The writes were removed in v3.3.0 for hygiene. If you are reading an older note claiming rotation "never worked", it is wrong; see the v3.3.1 section.
|
|
186
218
|
- **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.
|
|
187
219
|
- **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.
|
|
188
220
|
- **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.
|
package/README.zh-CN.md
CHANGED
|
@@ -4,42 +4,69 @@
|
|
|
4
4
|
|
|
5
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.3.
|
|
7
|
+
当前版本:**v3.3.2**(v7 UI 代)。
|
|
8
|
+
|
|
9
|
+
## v3.3.2 新增
|
|
10
|
+
|
|
11
|
+
- **peer 声明改用三元组区间 `>=0.2.0-rc.1 <0.2.1-0`——代码零改动**(2026-09-30)。
|
|
12
|
+
背景:DSH `0.2.0-rc.2` 发布后,门禁把 3.3.1 拦下的方式与当初把 3.2.4 在 `0.2.0-rc.1` 上拦下
|
|
13
|
+
完全一致——已是**连续第三轮**同型故障(设置项消失、路由 404/405)。根因从来不是不兼容,而是
|
|
14
|
+
逐版本枚举在每次 rc 迭代下必然过时。核查依据:`0.2.0-rc.1 → rc.2` 全量 20 包逐文件 SHA256
|
|
15
|
+
对比显示,本插件声明的六个 peer **只有 `package.json` 版本号变化**;再用真门禁跑全版本矩阵——
|
|
16
|
+
`0.2.0-rc.1` / `rc.2` / `rc.3` / `rc.9` / 正式版 `0.2.0` 全 **放行**,
|
|
17
|
+
`0.2.1-rc.1` / `0.3.0-rc.1` 全 **拦截**。
|
|
18
|
+
⚠️ **本条同时收窄支持面**:不再声明 `0.1.7` 线,需要在 0.1.7 上运行请装 **3.3.1**。
|
|
19
|
+
⚠️ 上界必须写 `<0.2.1-0` 而非 `<0.2.1`:semver 里预发布排在正式版**之前**,
|
|
20
|
+
`0.2.1-rc.1 < 0.2.1` 成立,写 `<0.2.1` 会漏放行下一个三元组的 rc。
|
|
8
21
|
|
|
9
22
|
## 环境要求
|
|
10
23
|
|
|
11
24
|
- DeepSeek Harness 带 `web` profile GUI(`npx @deepseek-ai/dsh web`)
|
|
12
25
|
- Node.js `^22.19` 或 `>=24`
|
|
13
|
-
- peer
|
|
26
|
+
- peer 范围用**按三元组划分的区间**(自 3.3.2 起;此前为逐版本枚举)——`@deepseek-ai/dsh` / `@deepseek-ai/dsh-credentials` / `@deepseek-ai/dsh-llm` / `@deepseek-ai/dsh-settings` 四条均声明 `>=0.2.0-rc.1 <0.2.1-0`:实测该三元组的首个 rc 即自动放行其后续 rc(`0.2.0-rc.2`/`rc.3`…)与正式版 `0.2.0`,跨三元组(如 `0.2.1-rc.1`)仍会被门禁拦下、须重新核查。上界写 `-0` 是必须的:semver 里预发布排在正式版之前,写 `<0.2.1` 会漏放行下一个三元组的 rc(规范见 `AGENTS.md` 规范 3)。`@deepseek-ai/cordis` `4.0.1 || 4.0.2 || 4.0.4`、`@deepseek-ai/schemastery` `3.18.1 || 3.18.2 || 3.18.4` 仍**精确枚举**(两者不进版本门禁——门禁只检查 `@deepseek-ai/dsh` 与 `dsh-*`——且版本稀疏无预发布链)。插件只使用 credential-reference 半边(`resolve`/`describe`/`set`/`unset`/`credentialRef`,自 `0.1.0-rc.6` 起稳定)与 `agent/request` + `agent/request-error` waterfall(载荷跨各版本未变);`isCredentialRefName`(rc.8 新增)本地实现兜底。**注意 `0.1.7` 线自 3.3.2 起不再声明**,需要它请装 3.3.1。
|
|
27
|
+
|
|
28
|
+
## v3.3.1 新增
|
|
29
|
+
|
|
30
|
+
- **文档更正——代码零改动**(2026-09-29)。v3.3.0 的发布说明断言「本插件从来没真正换过 key,因为写
|
|
31
|
+
`process.env` 到不了凭证层」。**该断言是错的,在此撤回。** 真正把 key 交给 provider 的
|
|
32
|
+
`credentials.resolve` 包装自 **v3.0.0** 就存在
|
|
33
|
+
(`git log -S 'credSvc.resolve =' -- dsh-key-fallback/lib/index.js` → `75ea76a`);从 registry 下载
|
|
34
|
+
**已发布的 3.2.4 tarball** 核对,`lib/index.js:298` 就是该包装、`:305` 是它的 `pool-hit` 诊断。
|
|
35
|
+
也就是说:**轮换一直在生效**,那几处 `process.env` 写入是与可用包装**并存**的冗余,而不是机制本身。
|
|
36
|
+
v3.3.0 的真实内容见下一节的准确清单。
|
|
37
|
+
- **轮换生效的实机证据** —— 在真实桌面端(DSH `0.2.0-rc.1` + 插件 v3.3.0)会话走池中 provider 时,
|
|
38
|
+
取 `GET /dsh-key-fallback/diag`:200 条(`resolve` 57 / `agent/request` 51 / `request-error` 90),
|
|
39
|
+
池里两把 key 分别被用了 **26 次 / 25 次**,每次失败即交替:
|
|
40
|
+
`PICK key_fallback_sensenova_key1` → `RATE_LIMIT` → `marked key_fallback_sensenova_key1` →
|
|
41
|
+
`PICK SENSENOVA_API_KEY` → … → `pool-hit SENSENOVA_API_KEY -> key_fallback_sensenova_key1`。
|
|
42
|
+
每条 `pool-hit` 都写明当时实际生效的那把 —— 正是适配器在网络上真正拿去鉴权的那把。
|
|
14
43
|
|
|
15
44
|
## v3.3.0 新增
|
|
16
45
|
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
`
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
`process.env
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
`
|
|
31
|
-
- **可见行为变化**:`/pools` 现在报 `activeRef === currentRef`。之前两者可能不一致(`activeRef` 显示
|
|
32
|
-
的是启动时的陈旧值)。
|
|
46
|
+
- **四项真实修复**(2026-09-29)。**注意**:v3.3.0 最初的说明声称本版才让「换 key 第一次真正到达
|
|
47
|
+
provider」,那是错的——更正见上方 v3.3.1。v3.3.0 实际改的是:
|
|
48
|
+
- **`activeRef` 不再报错当前 key。** `/pools` 原先用 `process.env[<池 env>]` 的值去池里反查成员、
|
|
49
|
+
只在没匹配到时才回退 `currentRef`——所以**可能高亮错误的那把**。实机(0.2.0-rc.1)上它报
|
|
50
|
+
`activeRef=SENSENOVA_API_KEY`,而 `currentRef` 是 `key_fallback_sensenova_key1`。现在 `activeRef`
|
|
51
|
+
直接等于 `currentRef`,两者永不矛盾。
|
|
52
|
+
- **删除 5 处冗余的 `process.env` 写入。** DSH 在启动时把 `{ ...process.env }` 冻结成「启动环境快照」
|
|
53
|
+
(`dsh-app-boot`),`dsh-launch-environment` 把它拷进 `Map`,`dsh-credentials-local.inherited()`
|
|
54
|
+
只读这个快照——启动之后写 `process.env` 对凭证层**不可见**。因此这些写入对鉴权是死代码(真正供 key
|
|
55
|
+
的是 `resolve` 包装);删掉同时避免插件改写宿主进程环境。**行为提示**:继承 `process.env` 启的子进程
|
|
56
|
+
现在看到的是启动时的值而非轮换后的值——没有任何凭证消费者依赖它,因为它们全部都经
|
|
57
|
+
`credentials.resolve` 取值,而那正是包装拦截的地方。
|
|
58
|
+
- **失败归因加了守卫。** `agent/request-error` 原先在无法确定当前 key 时会退化成归咎
|
|
59
|
+
`keyValues[0]`;现在只标记它真能指名的那把。
|
|
33
60
|
- **卸载会还原原始 `resolve`**(经 `ctx.effect`),并带身份校验,绝不覆盖「在我们之后安装的」包装层——
|
|
34
61
|
热重载/fiber 重建不再层层叠加、留下持有过期 service 的闭包。
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
62
|
+
- **未改动**:重发仍归 DSH 管。轮换处理只改池的当前 ref,并且仍然调用 `next()`,由 `dsh-llm-retry`
|
|
63
|
+
继续决定是否/重发几次。实测:每条轮换路径都调用了 `next()`。
|
|
64
|
+
- **验证方式**(全部实测,无推断):在进程内用真实的官方 `@deepseek-ai/dsh-llm-pi-ai` 适配器 +
|
|
65
|
+
真实 `LocalCredentialProvider` + 本地 mock 上游,从**真实出站 HTTP `Authorization` 头**里抓 key ——
|
|
66
|
+
装插件前 `Bearer STORED-ENV-KEY`,预选后 `Bearer KEY-ONE-111`,`RATE_LIMIT` 轮换后
|
|
67
|
+
`Bearer KEY-TWO-222`,`dispose()` 后回到 `STORED-ENV-KEY`。另有两套边界用例(`enabled:false`、空池、
|
|
68
|
+
env 解析不到、`useKeyRef` 锁定、HTTP 上 `activeRef`/`currentRef` 一致、`dispose` 幂等、不匹配的错误码)
|
|
69
|
+
——14/14 与 9/9 全绿。实机时间线见上方 v3.3.1。
|
|
43
70
|
|
|
44
71
|
## v3.2.4 新增
|
|
45
72
|
|
|
@@ -167,7 +194,7 @@
|
|
|
167
194
|
## 轮换语义
|
|
168
195
|
|
|
169
196
|
- **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"
|
|
197
|
+
- 写 `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"。**历史说明**:3.2.4 及之前插件**也**会写 `process.env[<池 env>]`,但它始终与上面的包装**并存**——那个写入是冗余的,**不是**机制本身,所以轮换一直照常工作。v3.3.0 出于整洁删掉了这些写入。若你看到旧笔记声称轮换「从未生效」,那是错的,见 v3.3.1 一节。
|
|
171
198
|
- **pick**(`agent/request`,预选):先尊重 `useKeyRef` 锁定(但冷却中的锁定 key 会跳过,避免池在某把坏 key 上死锁),否则按游标在 live key 上轮询。选中的 ref 成为池的当前 ref —— 也就是凭证接缝下一次 resolve 交给适配器的那把。
|
|
172
199
|
- **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。
|
|
173
200
|
- **re-send** 完全交给 DSH 的 `dsh-llm-retry` 与用户自己的 per-provider 重试策略。两者独立互补:重试插件决定"同一把 key 重发几次"(`retryPolicy.retryableCodes` 默认 `EMPTY_RESPONSE/RATE_LIMIT/SERVER/TIMEOUT/TRANSPORT`——注意**不含 AUTH/QUOTA**);本插件决定"换下一把 key"。例如 `AUTH` 失败(重试本来也不会重发,重发也白搭)仍会轮换到下一把——下一次请求就会用新 key 鉴权。
|
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.3.
|
|
4
|
+
"version": "3.3.2",
|
|
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.
|
|
43
|
-
"@deepseek-ai/dsh-credentials": "0.
|
|
44
|
-
"@deepseek-ai/dsh-llm": "0.
|
|
45
|
-
"@deepseek-ai/dsh-settings": "0.
|
|
42
|
+
"@deepseek-ai/dsh": ">=0.2.0-rc.1 <0.2.1-0",
|
|
43
|
+
"@deepseek-ai/dsh-credentials": ">=0.2.0-rc.1 <0.2.1-0",
|
|
44
|
+
"@deepseek-ai/dsh-llm": ">=0.2.0-rc.1 <0.2.1-0",
|
|
45
|
+
"@deepseek-ai/dsh-settings": ">=0.2.0-rc.1 <0.2.1-0",
|
|
46
46
|
"@deepseek-ai/schemastery": "3.18.1 || 3.18.2 || 3.18.4"
|
|
47
47
|
}
|
|
48
48
|
}
|