@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.
Files changed (3) hide show
  1. package/README.md +50 -32
  2. package/README.zh-CN.md +40 -26
  3. 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.0** (`v7` UI generation).
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
- - **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
+ - **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
- - **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.
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.0**(v7 UI 代)。
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
- - **换 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
+ - **四项真实修复**(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
- - **重发仍归 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 全绿。
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.0",
4
+ "version": "3.3.1",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {