@omdp/dsh-key-fallback 3.3.0 → 3.3.1
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 +50 -32
- package/README.zh-CN.md +40 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,43 +4,61 @@
|
|
|
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.1** (`v7` UI generation).
|
|
8
|
+
|
|
9
|
+
## What v3.3.1 offers
|
|
10
|
+
|
|
11
|
+
- **Documentation correction — no code change** (2026-09-29). The v3.3.0 release notes asserted that the plugin
|
|
12
|
+
had never actually switched keys, because the `process.env` write never reached the credential layer. **That
|
|
13
|
+
assertion was wrong and is retracted here.** The `credentials.resolve` wrapper that supplies the key has
|
|
14
|
+
existed since **v3.0.0** (`git log -S 'credSvc.resolve =' -- dsh-key-fallback/lib/index.js` → `75ea76a`), and
|
|
15
|
+
the **published 3.2.4 tarball** already contains both that wrapper and its `pool-hit` diagnostic (verified by
|
|
16
|
+
downloading `@omdp/dsh-key-fallback@3.2.4` from the registry and reading `lib/index.js:298` / `:305`).
|
|
17
|
+
Rotation was already reaching the provider; the redundant `process.env` writes sat *alongside* the working
|
|
18
|
+
wrapper rather than being the mechanism. v3.3.0's real content is the smaller, accurate list in the section
|
|
19
|
+
below.
|
|
20
|
+
- **Live production evidence that rotation works** — captured on the real desktop install (DSH `0.2.0-rc.1`,
|
|
21
|
+
plugin v3.3.0) while a chat was routed through a pooled provider, from `GET /dsh-key-fallback/diag`:
|
|
22
|
+
200 entries (`resolve` 57 / `agent/request` 51 / `request-error` 90), with the pool's two keys used
|
|
23
|
+
**26 and 25 times** and alternating on each failure:
|
|
24
|
+
`PICK key_fallback_sensenova_key1` → `RATE_LIMIT` → `marked key_fallback_sensenova_key1` →
|
|
25
|
+
`PICK SENSENOVA_API_KEY` → … → `pool-hit SENSENOVA_API_KEY -> key_fallback_sensenova_key1`.
|
|
26
|
+
Each `pool-hit` line names the key that was current at that instant, which is exactly what the adapter
|
|
27
|
+
received on the wire.
|
|
8
28
|
|
|
9
29
|
## What v3.3.0 offers
|
|
10
30
|
|
|
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).
|
|
31
|
+
- **Four real fixes** (2026-09-29). **Note**: the original v3.3.0 notes claimed this release made key switching
|
|
32
|
+
"actually reach the provider" for the first time. That was wrong — see the v3.3.1 correction above. What
|
|
33
|
+
v3.3.0 really changed:
|
|
34
|
+
- **`activeRef` no longer misreports the current key.** `/pools` used to derive `activeRef` by matching the
|
|
35
|
+
value of `process.env[<pool env>]` against the pool's members and falling back to `currentRef` only when no
|
|
36
|
+
member matched — so it could highlight **the wrong key**. On a live 0.2.0-rc.1 install it reported
|
|
37
|
+
`activeRef=SENSENOVA_API_KEY` while `currentRef` was `key_fallback_sensenova_key1`. `activeRef` is now
|
|
38
|
+
simply `currentRef`, so the two always agree.
|
|
39
|
+
- **Five redundant `process.env` writes removed.** DSH freezes `{ ...process.env }` into a *launch
|
|
40
|
+
environment snapshot* at boot (`dsh-app-boot`), `dsh-launch-environment` copies that into a `Map`, and
|
|
41
|
+
`dsh-credentials-local.inherited()` reads only that snapshot — so a post-boot `process.env` write is
|
|
42
|
+
invisible to the credential layer. The writes were therefore dead weight for authentication (the `resolve`
|
|
43
|
+
wrapper is what supplies the key). Removing them also stops the plugin from mutating the host process
|
|
44
|
+
environment. **Behaviour note**: a tool that spawns a subprocess inheriting `process.env` now sees the
|
|
45
|
+
launch-time value rather than the rotated one — no credential consumer depends on it, because every one of
|
|
46
|
+
them resolves through `credentials.resolve`, which the wrapper intercepts.
|
|
47
|
+
- **Failure attribution is guarded.** `agent/request-error` used to fall back to blaming `keyValues[0]` when
|
|
48
|
+
the current key could not be identified; it now only marks a key it can actually name.
|
|
31
49
|
- **Uninstall restores the original `resolve`** via `ctx.effect`, guarded by an identity check so it never
|
|
32
50
|
clobbers a wrapper installed after ours — a hot reload or fiber rebuild no longer stacks wrappers whose
|
|
33
51
|
closures hold expired services.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
52
|
+
- **Unchanged**: re-send remains DSH's job. The rotation handler only changes the pool's current ref and still
|
|
53
|
+
calls `next()`, so `dsh-llm-retry` keeps deciding whether and how often to re-send. Verified: `next()` is
|
|
54
|
+
called on every rotation path.
|
|
55
|
+
- **Verification** (all measured, no inference): the real official `@deepseek-ai/dsh-llm-pi-ai` adapter was
|
|
56
|
+
applied in-process with a real `LocalCredentialProvider` and a local mock upstream; the API key was captured
|
|
57
|
+
from the **actual outbound HTTP `Authorization` header** — `Bearer STORED-ENV-KEY` before the plugin,
|
|
58
|
+
`Bearer KEY-ONE-111` after pre-selection, `Bearer KEY-TWO-222` after a `RATE_LIMIT` rotation, back to
|
|
59
|
+
`STORED-ENV-KEY` after `dispose()`. Two further suites cover the edge cases (`enabled:false`, empty pool,
|
|
60
|
+
unresolvable env, `useKeyRef` lock, `activeRef`/`currentRef` agreement over HTTP, `dispose` idempotence,
|
|
61
|
+
non-matching error codes) — 14/14 and 9/9 pass. The live production timeline is in v3.3.1 above.
|
|
44
62
|
|
|
45
63
|
## What v3.2.4 offers
|
|
46
64
|
|
|
@@ -182,7 +200,7 @@ with live env-key values), create/update/delete (POST/DELETE round-trip, persist
|
|
|
182
200
|
## Rotation semantics
|
|
183
201
|
|
|
184
202
|
- **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".
|
|
203
|
+
- 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
204
|
- **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
205
|
- **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
206
|
- **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,7 +4,7 @@
|
|
|
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.1**(v7 UI 代)。
|
|
8
8
|
|
|
9
9
|
## 环境要求
|
|
10
10
|
|
|
@@ -12,34 +12,48 @@
|
|
|
12
12
|
- Node.js `^22.19` 或 `>=24`
|
|
13
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
14
|
|
|
15
|
+
## v3.3.1 新增
|
|
16
|
+
|
|
17
|
+
- **文档更正——代码零改动**(2026-09-29)。v3.3.0 的发布说明断言「本插件从来没真正换过 key,因为写
|
|
18
|
+
`process.env` 到不了凭证层」。**该断言是错的,在此撤回。** 真正把 key 交给 provider 的
|
|
19
|
+
`credentials.resolve` 包装自 **v3.0.0** 就存在
|
|
20
|
+
(`git log -S 'credSvc.resolve =' -- dsh-key-fallback/lib/index.js` → `75ea76a`);从 registry 下载
|
|
21
|
+
**已发布的 3.2.4 tarball** 核对,`lib/index.js:298` 就是该包装、`:305` 是它的 `pool-hit` 诊断。
|
|
22
|
+
也就是说:**轮换一直在生效**,那几处 `process.env` 写入是与可用包装**并存**的冗余,而不是机制本身。
|
|
23
|
+
v3.3.0 的真实内容见下一节的准确清单。
|
|
24
|
+
- **轮换生效的实机证据** —— 在真实桌面端(DSH `0.2.0-rc.1` + 插件 v3.3.0)会话走池中 provider 时,
|
|
25
|
+
取 `GET /dsh-key-fallback/diag`:200 条(`resolve` 57 / `agent/request` 51 / `request-error` 90),
|
|
26
|
+
池里两把 key 分别被用了 **26 次 / 25 次**,每次失败即交替:
|
|
27
|
+
`PICK key_fallback_sensenova_key1` → `RATE_LIMIT` → `marked key_fallback_sensenova_key1` →
|
|
28
|
+
`PICK SENSENOVA_API_KEY` → … → `pool-hit SENSENOVA_API_KEY -> key_fallback_sensenova_key1`。
|
|
29
|
+
每条 `pool-hit` 都写明当时实际生效的那把 —— 正是适配器在网络上真正拿去鉴权的那把。
|
|
30
|
+
|
|
15
31
|
## v3.3.0 新增
|
|
16
32
|
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
`
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
`process.env
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
`
|
|
31
|
-
- **可见行为变化**:`/pools` 现在报 `activeRef === currentRef`。之前两者可能不一致(`activeRef` 显示
|
|
32
|
-
的是启动时的陈旧值)。
|
|
33
|
+
- **四项真实修复**(2026-09-29)。**注意**:v3.3.0 最初的说明声称本版才让「换 key 第一次真正到达
|
|
34
|
+
provider」,那是错的——更正见上方 v3.3.1。v3.3.0 实际改的是:
|
|
35
|
+
- **`activeRef` 不再报错当前 key。** `/pools` 原先用 `process.env[<池 env>]` 的值去池里反查成员、
|
|
36
|
+
只在没匹配到时才回退 `currentRef`——所以**可能高亮错误的那把**。实机(0.2.0-rc.1)上它报
|
|
37
|
+
`activeRef=SENSENOVA_API_KEY`,而 `currentRef` 是 `key_fallback_sensenova_key1`。现在 `activeRef`
|
|
38
|
+
直接等于 `currentRef`,两者永不矛盾。
|
|
39
|
+
- **删除 5 处冗余的 `process.env` 写入。** DSH 在启动时把 `{ ...process.env }` 冻结成「启动环境快照」
|
|
40
|
+
(`dsh-app-boot`),`dsh-launch-environment` 把它拷进 `Map`,`dsh-credentials-local.inherited()`
|
|
41
|
+
只读这个快照——启动之后写 `process.env` 对凭证层**不可见**。因此这些写入对鉴权是死代码(真正供 key
|
|
42
|
+
的是 `resolve` 包装);删掉同时避免插件改写宿主进程环境。**行为提示**:继承 `process.env` 启的子进程
|
|
43
|
+
现在看到的是启动时的值而非轮换后的值——没有任何凭证消费者依赖它,因为它们全部都经
|
|
44
|
+
`credentials.resolve` 取值,而那正是包装拦截的地方。
|
|
45
|
+
- **失败归因加了守卫。** `agent/request-error` 原先在无法确定当前 key 时会退化成归咎
|
|
46
|
+
`keyValues[0]`;现在只标记它真能指名的那把。
|
|
33
47
|
- **卸载会还原原始 `resolve`**(经 `ctx.effect`),并带身份校验,绝不覆盖「在我们之后安装的」包装层——
|
|
34
48
|
热重载/fiber 重建不再层层叠加、留下持有过期 service 的闭包。
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
49
|
+
- **未改动**:重发仍归 DSH 管。轮换处理只改池的当前 ref,并且仍然调用 `next()`,由 `dsh-llm-retry`
|
|
50
|
+
继续决定是否/重发几次。实测:每条轮换路径都调用了 `next()`。
|
|
51
|
+
- **验证方式**(全部实测,无推断):在进程内用真实的官方 `@deepseek-ai/dsh-llm-pi-ai` 适配器 +
|
|
52
|
+
真实 `LocalCredentialProvider` + 本地 mock 上游,从**真实出站 HTTP `Authorization` 头**里抓 key ——
|
|
53
|
+
装插件前 `Bearer STORED-ENV-KEY`,预选后 `Bearer KEY-ONE-111`,`RATE_LIMIT` 轮换后
|
|
54
|
+
`Bearer KEY-TWO-222`,`dispose()` 后回到 `STORED-ENV-KEY`。另有两套边界用例(`enabled:false`、空池、
|
|
55
|
+
env 解析不到、`useKeyRef` 锁定、HTTP 上 `activeRef`/`currentRef` 一致、`dispose` 幂等、不匹配的错误码)
|
|
56
|
+
——14/14 与 9/9 全绿。实机时间线见上方 v3.3.1。
|
|
43
57
|
|
|
44
58
|
## v3.2.4 新增
|
|
45
59
|
|
|
@@ -167,7 +181,7 @@
|
|
|
167
181
|
## 轮换语义
|
|
168
182
|
|
|
169
183
|
- **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"
|
|
184
|
+
- 写 `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
185
|
- **pick**(`agent/request`,预选):先尊重 `useKeyRef` 锁定(但冷却中的锁定 key 会跳过,避免池在某把坏 key 上死锁),否则按游标在 live key 上轮询。选中的 ref 成为池的当前 ref —— 也就是凭证接缝下一次 resolve 交给适配器的那把。
|
|
172
186
|
- **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
187
|
- **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.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|