@omdp/dsh-key-fallback 3.1.6 → 3.2.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 +49 -3
- package/README.zh-CN.md +49 -4
- package/lib/index.js +96 -15
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -4,13 +4,59 @@
|
|
|
4
4
|
|
|
5
5
|
**Multi-key API key pool with automatic rotation for [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness)** — sits between the LLM adapter and the credential store. Before each request the plugin picks a key from the per-provider pool and pre-writes it into the provider's credential reference; 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.
|
|
7
|
+
Current version: **v3.2.0** (`v7` UI generation).
|
|
8
8
|
|
|
9
9
|
## Requirements
|
|
10
10
|
|
|
11
11
|
- DeepSeek Harness with a `web`-profile GUI (`npx @deepseek-ai/dsh web`)
|
|
12
12
|
- Node.js `^22.19` or `>=24`
|
|
13
|
-
- Peer ranges strictly enumerate **only compatibility-tested versions** — `@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`, `@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`, `@deepseek-ai/cordis` `4.0.1 || 4.0.2`, `@deepseek-ai/schemastery` `3.18.1 || 3.18.2`. 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.
|
|
13
|
+
- Peer ranges strictly enumerate **only compatibility-tested versions** — `@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`, `@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`, `@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.
|
|
14
|
+
|
|
15
|
+
## What v3.2.0 offers
|
|
16
|
+
|
|
17
|
+
- **DSH `0.1.7-rc.1` support declared, with `0.1.5-rc.3` kept** (2026-09-24). This release is the first to need an
|
|
18
|
+
actual code change, because DSH `0.1.7` **relocates plugin configuration**: see the next section.
|
|
19
|
+
|
|
20
|
+
### Two config backends, one build
|
|
21
|
+
|
|
22
|
+
Since DSH `0.1.7`, plugin configuration stopped living in `<DSH_HOME>/settings.yaml`. On first boot, `0.1.7`
|
|
23
|
+
**renames that file to `settings.yaml.imported`** and copies each section into the profile patch
|
|
24
|
+
(`~/.dsh/profiles/<profile>/cordis.patch.yml`) as the `config:` of the matching loader entry; from then on the
|
|
25
|
+
`settings` service owns it, and plugins read it through an injected `config` parameter instead of touching the
|
|
26
|
+
YAML file. `ctx.settings.get()` / `register()` / `installSection()` — the rc.x API — **no longer exist** on `0.1.7`.
|
|
27
|
+
|
|
28
|
+
This plugin therefore auto-detects the backend at startup and keeps both paths working:
|
|
29
|
+
|
|
30
|
+
| | DSH `0.1.5-rc.3` (and older rc.x) | DSH `0.1.7-rc.1` (and newer) |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| Where pools live | `<DSH_HOME>/settings.yaml`, section `key-fallback:` | profile patch, the `key-fallback` entry's `config` |
|
|
33
|
+
| How the plugin reads/writes | reads/writes that YAML file directly (with timestamped backups) | `config.providers` is a live volatile ref; writes go through `settings.replace()` |
|
|
34
|
+
| Detection | no volatile-schema support ⇒ file backend | `Config` declares a volatile field ⇒ backend comes from `config` |
|
|
35
|
+
|
|
36
|
+
Two implementation notes that matter if you port this pattern elsewhere:
|
|
37
|
+
|
|
38
|
+
1. **`Config` must exist, or the migration silently drops your config.** `0.1.7`'s import path calls
|
|
39
|
+
`settings.update(ns, values)`, which throws unless the entry declares a **volatile** field; on failure it logs
|
|
40
|
+
`settings: section ... was not imported` and the section survives only inside `settings.yaml.imported`. So
|
|
41
|
+
`v3.2.0` exports `Config = z.object({ providers: <dict>.volatile() })`.
|
|
42
|
+
2. **The volatile API differs between the two schemastery builds.** `.volatile()` is available in schemastery
|
|
43
|
+
`3.18.4` (`0.1.7`) but **not** in `3.18.2` (`rc.3`) — calling it there returns `undefined` and registering a
|
|
44
|
+
schema without it would change rc.3 behaviour. The export is therefore guarded:
|
|
45
|
+
`typeof field.volatile === 'function' ? z.object({ providers: field.volatile() }) : undefined` — rc.3 gets
|
|
46
|
+
**no** `Config` (byte-identical behaviour to v3.1.x) while `0.1.7` gets the volatile one.
|
|
47
|
+
|
|
48
|
+
The values from `config.providers` are deep-frozen by DSH, so `readSettings()` hands callers a mutable deep copy;
|
|
49
|
+
writes are applied optimistically to an in-memory copy and flushed via `settings.replace()` (an unresolved-write
|
|
50
|
+
counter prevents the UI from briefly re-reading the pre-write tree).
|
|
51
|
+
|
|
52
|
+
Both backends were exercised end-to-end on real installs: read (both providers appear in `/dsh-key-fallback/pools`
|
|
53
|
+
with live env-key values), create/update/delete (POST/DELETE round-trip, persisted), and the rc.3 regression
|
|
54
|
+
(`settings.yaml` untouched and *not* renamed, `cordis.patch.yml` still `[]`, backups still written).
|
|
55
|
+
|
|
56
|
+
## What v3.1.7 offers
|
|
57
|
+
|
|
58
|
+
- **DSH `0.1.5-rc.2` support declared** (2026-09-15). Source check: `@deepseek-ai/dsh-credentials` / `dsh-llm` / `dsh-settings` `0.1.5-rc.1 → rc.2` differ in **`package.json` only** — every lib/docs file is SHA256-identical. Runtime check: strict peer install (no `--legacy-peer-deps`) against a real `dsh@0.1.5-rc.2` web profile + clean boot, zero browser-console errors, `/dsh-key-fallback/*` routes serve, Settings → API Key 回退 renders.
|
|
59
|
+
- **DSH `0.1.6-alpha.1` support declared** (2026-09-15, source-level file-hash diff **plus a runtime boot smoke test** on a real `0.1.6-alpha.1` install). No code change was needed: `@deepseek-ai/dsh-credentials` and `@deepseek-ai/dsh-settings` are **byte-identical in `lib/`** vs `0.1.5-rc.2` (version/docs bump only), and `@deepseek-ai/dsh-llm`'s removals (`priceImages`, the `projectImagesForTextModel`/request-image-offload family) are outside every API this plugin calls — `credentialRef`, `credentials.resolve/set/unset/describe`, `llm.listProviders/listConfigurableProviders`, `agent/request`, `agent/request-error`, `settings`, `webServer` all verified present (`dsh-host-webserver` — the `webServer.register` provider — is itself byte-identical). Runtime check: plugin activates, `/dsh-key-fallback/*` routes serve, Settings → API Key 回退 renders. Old versions stay supported — both `0.1.5-rc.2` and `0.1.6-alpha.1` are *appended*, nothing dropped.
|
|
14
60
|
|
|
15
61
|
## What v3.1.6 offers
|
|
16
62
|
|
|
@@ -76,7 +122,7 @@ dsh plugin --profile web add link:D:/WorkSpace/omdp/dsh-key-fallback
|
|
|
76
122
|
## Known limitations
|
|
77
123
|
|
|
78
124
|
- Cooldown is a fixed per-pool `cooldownMs` (no exponential backoff); a cooled key becomes live again when the timer expires.
|
|
79
|
-
- The plugin manages rotation/cooldown state in-memory plus persisted pool config
|
|
125
|
+
- The plugin manages rotation/cooldown state in-memory plus persisted pool config; a DSH restart re-reads config and recomputes live status. On DSH `0.1.5-rc.3` and older that config is the `key-fallback` section of `<DSH_HOME>/settings.yaml`; on `0.1.7`+ it is the `key-fallback` entry's `config` inside `~/.dsh/profiles/<profile>/cordis.patch.yml`.
|
|
80
126
|
- The env key cannot be deleted through the UI (it is the pool's identity).
|
|
81
127
|
|
|
82
128
|
## License
|
package/README.zh-CN.md
CHANGED
|
@@ -1,16 +1,61 @@
|
|
|
1
1
|
# @omdp/dsh-key-fallback
|
|
2
2
|
|
|
3
|
-
English
|
|
3
|
+
[English](README.md) | 简体中文
|
|
4
4
|
|
|
5
5
|
**为 [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) 提供多 key 池 + 自动轮换**——插件位于 LLM 适配器与凭证存储之间:每次请求前从按 provider 分组的 key 池里选一把,预写入该 provider 的凭证引用;遇到配置的触发错误时,把失败 key 标记为冷却(固定 `cooldownMs`,无指数退避)并前进到下一把。**重发完全交给 DSH 自带的 `dsh-llm-retry`**——本插件从不自行重发,只负责换 key,重试策略由 retry policy 决定。
|
|
6
6
|
|
|
7
|
-
当前版本:**v3.
|
|
7
|
+
当前版本:**v3.2.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-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`、`@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`、`@deepseek-ai/cordis` `4.0.1 || 4.0.2`、`@deepseek-ai/schemastery` `3.18.1 || 3.18.2`。不使用 `<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-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`、`@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`、`@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.2.0 新增
|
|
16
|
+
|
|
17
|
+
- **声明支持 DSH `0.1.7-rc.1`,同时保留 `0.1.5-rc.3`**(2026-09-24)。这是本插件首个需要改动代码的版本,原因是 DSH `0.1.7`
|
|
18
|
+
**改变了插件配置的存放位置**——详见下节。
|
|
19
|
+
|
|
20
|
+
### 两种配置后端,同一份构建
|
|
21
|
+
|
|
22
|
+
自 DSH `0.1.7` 起,插件配置不再位于 `<DSH_HOME>/settings.yaml`。首次启动时 `0.1.7` 会把该文件
|
|
23
|
+
**改名成 `settings.yaml.imported`**,并把每个段的内容写入 profile 补丁
|
|
24
|
+
(`~/.dsh/profiles/<profile>/cordis.patch.yml`)中对应 loader 条目的 `config:`;此后由 `settings` 服务托管,
|
|
25
|
+
插件通过注入的 `config` 参数读取,而不再自行读写该 YAML。rc.x 时代的 `ctx.settings.get()` /
|
|
26
|
+
`register()` / `installSection()` 在 `0.1.7` 上**已不存在**。
|
|
27
|
+
|
|
28
|
+
因此本插件在启动时自动判别后端,两条路径同时可用:
|
|
29
|
+
|
|
30
|
+
| | DSH `0.1.5-rc.3`(及更早 rc.x) | DSH `0.1.7-rc.1`(及更新) |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| 池配置位置 | `<DSH_HOME>/settings.yaml` 的 `key-fallback:` 段 | profile 补丁中 `key-fallback` 条目的 `config` |
|
|
33
|
+
| 插件如何读写 | 直接读写该 YAML 文件(附时间戳备份) | `config.providers` 是活的 volatile ref;写入走 `settings.replace()` |
|
|
34
|
+
| 判别方式 | 无 volatile schema 支持 ⇒ 文件后端 | `Config` 声明了 volatile 字段 ⇒ 后端来自 `config` |
|
|
35
|
+
|
|
36
|
+
移植该模式时有两个关键点:
|
|
37
|
+
|
|
38
|
+
1. **必须声明 `Config`,否则迁移会静默丢弃配置。** `0.1.7` 的导入路径调用
|
|
39
|
+
`settings.update(ns, values)`,若该条目没有声明 **volatile** 字段就会抛错,仅打印
|
|
40
|
+
`settings: section ... was not imported`,配置只残留在 `settings.yaml.imported` 里。因此
|
|
41
|
+
`v3.2.0` 导出 `Config = z.object({ providers: <dict>.volatile() })`。
|
|
42
|
+
2. **volatile API 在两种 schemastery 构建上不同。** `.volatile()` 在 schemastery `3.18.4`(`0.1.7`)上可用,
|
|
43
|
+
但在 `3.18.2`(`rc.3`)上**不存在**——在那里调用会得到 `undefined`,而若据此注册一个无 volatile 的
|
|
44
|
+
schema 就会改变 rc.3 的行为。故导出做了守卫:
|
|
45
|
+
`typeof field.volatile === 'function' ? z.object({ providers: field.volatile() }) : undefined`——
|
|
46
|
+
rc.3 拿到 **没有** `Config`(行为与 v3.1.x 逐字节一致),`0.1.7` 拿到含 volatile 的那一份。
|
|
47
|
+
|
|
48
|
+
`config.providers` 的值被 DSH 深度冻结,故 `readSettings()` 向调用方返回可变深拷贝;写入先乐观地更新内存副本
|
|
49
|
+
再经 `settings.replace()` 落盘(未决写入计数避免 UI 短暂读回写入前的旧树)。
|
|
50
|
+
|
|
51
|
+
两条后端均已在真实安装上端到端实测:读取(两个 provider 都出现在 `/dsh-key-fallback/pools`,含 env key 实时值)、
|
|
52
|
+
增删改(POST/DELETE 往返并落盘)、以及 rc.3 回归(`settings.yaml` 未被改名也未改内容、`cordis.patch.yml` 仍为
|
|
53
|
+
`[]`、备份照旧生成)。
|
|
54
|
+
|
|
55
|
+
## v3.1.7 新增
|
|
56
|
+
|
|
57
|
+
- **声明支持 DSH `0.1.5-rc.2`**(2026-09-15)。源码核查:`@deepseek-ai/dsh-credentials` / `dsh-llm` / `dsh-settings` 从 `0.1.5-rc.1` 到 `rc.2` **只有 `package.json` 变化**——lib/文档逐文件 SHA256 全一致。运行时核查:对真实 `dsh@0.1.5-rc.2` web profile **严格模式**安装 peer(不加 `--legacy-peer-deps`)零告警通过,启动零浏览器控制台错误,`/dsh-key-fallback/*` 路由服务,设置页「API Key 回退」渲染。
|
|
58
|
+
- **声明支持 DSH `0.1.6-alpha.1`**(2026-09-15,源码级逐文件哈希比对 + **真实运行时冒烟**:在临时安装的 `0.1.6-alpha.1` 上以 web profile 实测加载)。未改动任何代码:`@deepseek-ai/dsh-credentials` 与 `@deepseek-ai/dsh-settings` 的 `lib/` 相对 `0.1.5-rc.2` **逐字节一致**(仅版本号/文档变化),`@deepseek-ai/dsh-llm` 的删除项(`priceImages`、`projectImagesForTextModel`/request-image-offload 系列)均在本插件调用面之外——本插件调用的 `credentialRef`、`credentials.resolve/set/unset/describe`、`llm.listProviders/listConfigurableProviders`、`agent/request`、`agent/request-error`、`settings`、`webServer` 全部实测保留(`webServer` 提供方 `dsh-host-webserver` 整包字节一致)。运行时验证:插件激活、`/dsh-key-fallback/*` 路由服务、设置页「API Key 回退」渲染。**旧版本继续兼容**——`0.1.5-rc.2` 与 `0.1.6-alpha.1` 均为**追加**进枚举,未删除任何版本。
|
|
14
59
|
|
|
15
60
|
## v3.1.6 新增
|
|
16
61
|
|
|
@@ -76,7 +121,7 @@ dsh plugin --profile web add link:D:/WorkSpace/omdp/dsh-key-fallback
|
|
|
76
121
|
## 已知限制
|
|
77
122
|
|
|
78
123
|
- 冷却为每池固定 `cooldownMs`(无指数退避);冷却到期 key 自动恢复 live。
|
|
79
|
-
- 轮换/冷却状态为内存态 +
|
|
124
|
+
- 轮换/冷却状态为内存态 + 持久化池配置;DSH 重启会重读配置并重算 live 状态。在 DSH `0.1.5-rc.3` 及更早版本上,该配置是 `<DSH_HOME>/settings.yaml` 的 `key-fallback` 段;在 `0.1.7`+ 上则是 `~/.dsh/profiles/<profile>/cordis.patch.yml` 中 `key-fallback` 条目的 `config`。
|
|
80
125
|
- env key 不能通过 UI 删除(它是池的身份标识)。
|
|
81
126
|
|
|
82
127
|
## License
|
package/lib/index.js
CHANGED
|
@@ -32,9 +32,16 @@ function deriveEnvName(name) {
|
|
|
32
32
|
export const name = 'key-fallback'
|
|
33
33
|
export const inject = ['llm', 'settings', 'webServer', 'credentials']
|
|
34
34
|
const API_BASE = '/dsh-key-fallback'
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
35
|
+
// 遵循 DSH_HOME(未设置时回落 ~/.dsh):与 dsh-connector / dsh-archived-sessions 的
|
|
36
|
+
// 路径推导保持一致,也让隔离冒烟环境可以直接切换整个 home。
|
|
37
|
+
const DSH_HOME = (() => {
|
|
38
|
+
const h = process.env.DSH_HOME
|
|
39
|
+
return (typeof h === 'string' && h) ? h : join(homedir(), '.dsh')
|
|
40
|
+
})()
|
|
41
|
+
const SETTINGS_FILE = join(DSH_HOME, 'settings.yaml')
|
|
42
|
+
const SETTINGS_FILE_IMPORTED = SETTINGS_FILE + '.imported'
|
|
43
|
+
const CRED_FILE = join(DSH_HOME, '.credentials.yaml')
|
|
44
|
+
const BACKUP_DIR = join(DSH_HOME, 'backups')
|
|
38
45
|
if (!existsSync(BACKUP_DIR)) try { mkdirSync(BACKUP_DIR, { recursive: true }) } catch (e) {}
|
|
39
46
|
let _backupCounter = 0
|
|
40
47
|
function tsName(prefix) {
|
|
@@ -66,21 +73,80 @@ const profileSchema = z.object({
|
|
|
66
73
|
refsCompacted: z.boolean().default(false),
|
|
67
74
|
})
|
|
68
75
|
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
// ── 配置后端(双版本兼容)─────────────────────────────────────────────
|
|
77
|
+
// 0.1.5-rc.3:配置在 <home>/settings.yaml 的 `key-fallback:` 段,插件自己读写该文件。
|
|
78
|
+
// 0.1.7+ :DSH 启动时把 settings.yaml 改名为 settings.yaml.imported,段内容成为该 entry 的
|
|
79
|
+
// config override(写进 profiles/<p>/cordis.patch.yml),由 ctx.settings 服务托管;
|
|
80
|
+
// 插件通过「volatile Config + describe/replace」读写,不再碰 settings.yaml。
|
|
81
|
+
//
|
|
82
|
+
// 关键前提:0.1.7 的迁移走 settings.update(ns, values) → write(),要求该 entry 存在且其
|
|
83
|
+
// Config 含 volatile 字段,否则抛错并打印「section ... was not imported」——配置静默丢失。
|
|
84
|
+
// 因此必须声明 Config(下面),且只在本版本 schemastery 支持 .volatile() 时声明,
|
|
85
|
+
// 使 rc.3 行为与改造前逐字节一致。
|
|
86
|
+
const _providersField = z.dict(z.any()).default({})
|
|
87
|
+
export const Config = (typeof _providersField.volatile === 'function')
|
|
88
|
+
? z.object({ providers: _providersField.volatile() })
|
|
89
|
+
: undefined
|
|
90
|
+
|
|
91
|
+
const ENTRY_ID = 'key-fallback'
|
|
92
|
+
const diagLog = []
|
|
93
|
+
const diag = (kind, msg) => {
|
|
71
94
|
try {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
95
|
+
diagLog.push({ kind, msg: String(msg).slice(0, 300), at: Date.now() })
|
|
96
|
+
if (diagLog.length > 200) diagLog.splice(0, diagLog.length - 200)
|
|
97
|
+
} catch (e) {}
|
|
98
|
+
}
|
|
99
|
+
let _settingsSvc = null // ctx.settings(0.1.7+)
|
|
100
|
+
let _liveRef = null // 0.1.7+: config.providers 的 volatile Ref(原地热更新)
|
|
101
|
+
let _cache = null // 0.1.7+: 内存权威副本(乐观写,避免异步落盘窗口内读到旧值)
|
|
102
|
+
let _writing = 0 // 未确认写入计数
|
|
103
|
+
|
|
104
|
+
function readSettingsFromFile() {
|
|
105
|
+
// settings.yaml 优先;0.1.7 迁移后已改名,回退读 .imported(只读引导,避免配置真空)
|
|
106
|
+
for (const file of [SETTINGS_FILE, SETTINGS_FILE_IMPORTED]) {
|
|
107
|
+
if (!existsSync(file)) continue
|
|
108
|
+
try {
|
|
109
|
+
const doc = loadYaml(readFileSync(file, 'utf8')) || {}
|
|
110
|
+
const block = (doc && (doc[ENTRY_ID] || doc.keyFallback)) || {}
|
|
111
|
+
if (block && block.providers) return block
|
|
112
|
+
} catch (e) {}
|
|
113
|
+
}
|
|
114
|
+
return { providers: {} }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function cloneJson(v) {
|
|
118
|
+
try { return v === undefined ? {} : JSON.parse(JSON.stringify(v)) } catch (e) { return v }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function readSettings() {
|
|
122
|
+
if (_liveRef) {
|
|
123
|
+
// _liveRef 就是 config.providers 这个 Ref:.get() 直接返回 providers 字典。
|
|
124
|
+
// 它由 DSH deepFreeze,调用方却要就地改(新增/删除 provider、compactRefs 改 keyRefs),
|
|
125
|
+
// 所以这里始终返回「可变深拷贝」。未决写入期间以内存副本为源(replace 是异步的,
|
|
126
|
+
// 否则会读到落盘前的旧树,UI 立刻刷新会闪回旧值)。
|
|
127
|
+
let src = _cache
|
|
128
|
+
if (_writing === 0) { try { src = { providers: _liveRef.get() || {} } ; _cache = src } catch (e) {} }
|
|
129
|
+
if (!src) { try { src = { providers: _liveRef.get() || {} }; _cache = src } catch (e) { src = { providers: {} } } }
|
|
130
|
+
return { providers: cloneJson(src.providers) || {} }
|
|
131
|
+
}
|
|
132
|
+
return readSettingsFromFile()
|
|
77
133
|
}
|
|
78
134
|
|
|
79
135
|
function writeSettings(data) {
|
|
136
|
+
if (_liveRef) {
|
|
137
|
+
_cache = { providers: cloneJson((data && data.providers) || {}) }
|
|
138
|
+
if (!_settingsSvc || typeof _settingsSvc.replace !== 'function') return
|
|
139
|
+
_writing++
|
|
140
|
+
Promise.resolve()
|
|
141
|
+
.then(() => _settingsSvc.replace(ENTRY_ID, { providers: cloneJson(_cache.providers) }))
|
|
142
|
+
.catch((e) => diag('settings-write', 'replace failed: ' + ((e && e.message) || e)))
|
|
143
|
+
.finally(() => { _writing-- })
|
|
144
|
+
return
|
|
145
|
+
}
|
|
80
146
|
backupFile(SETTINGS_FILE, 'settings')
|
|
81
147
|
let root = {}
|
|
82
148
|
try { if (existsSync(SETTINGS_FILE)) root = loadYaml(readFileSync(SETTINGS_FILE, 'utf8')) || {} } catch (e) {}
|
|
83
|
-
root[
|
|
149
|
+
root[ENTRY_ID] = data
|
|
84
150
|
writeFileSync(SETTINGS_FILE, dumpYaml(root, { lineWidth: 120 }), 'utf8')
|
|
85
151
|
}
|
|
86
152
|
|
|
@@ -168,11 +234,26 @@ function displayNameOf(entry) {
|
|
|
168
234
|
return ref
|
|
169
235
|
}
|
|
170
236
|
|
|
171
|
-
export function apply(ctx) {
|
|
172
|
-
try { ctx.logger.info('key-fallback:
|
|
173
|
-
const diagLog = []
|
|
174
|
-
const diag = (kind, msg) => { try { diagLog.push({ kind, msg: String(msg).slice(0, 300), at: Date.now() }); if (diagLog.length > 200) diagLog.splice(0, diagLog.length - 200) } catch (e) {} }
|
|
237
|
+
export function apply(ctx, config) {
|
|
238
|
+
try { ctx.logger.info('key-fallback: v7 host apply() starting') } catch (e) {}
|
|
175
239
|
diag('apply', 'host loaded')
|
|
240
|
+
|
|
241
|
+
// ── 0.1.7+:把配置接到 settings 服务托管的那份树 ──
|
|
242
|
+
// config.providers 平时是 volatile Ref(.get() 随编辑热更新);无 config 或非 Ref 时
|
|
243
|
+
// 保持文件后端。两者都在这里判定一次,之后 readSettings/writeSettings 自动分流。
|
|
244
|
+
try {
|
|
245
|
+
const svc = ctx.get('settings')
|
|
246
|
+
const prov = config && config.providers
|
|
247
|
+
if (svc && typeof svc.replace === 'function' && prov && typeof prov.get === 'function') {
|
|
248
|
+
_settingsSvc = svc
|
|
249
|
+
_liveRef = prov // = config.providers,Ref.get() 直接给 providers 字典
|
|
250
|
+
_cache = { providers: prov.get() || {} }
|
|
251
|
+
diag('settings', 'backend = profile entry (0.1.7+), providers=' + Object.keys(_cache.providers).join(','))
|
|
252
|
+
// 无需额外订阅:readSettings() 每次(无未决写入时)都从 Ref 重取,外部编辑自动生效
|
|
253
|
+
} else {
|
|
254
|
+
diag('settings', 'backend = settings.yaml file (rc.x)')
|
|
255
|
+
}
|
|
256
|
+
} catch (e) { diag('settings', 'probe failed: ' + ((e && e.message) || e)) }
|
|
176
257
|
// ── 包装 credentials.resolve:ref 命中池 env 时返回池当前 key(llm-pi-ai 唯一读取入口)──
|
|
177
258
|
const poolsByEnv = new Map()
|
|
178
259
|
const credSvc = ctx.credentials
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omdp/dsh-key-fallback",
|
|
3
|
-
"description": "API Key 回退轮换插件:多 key 池 + 自动轮换(触发码可配置,含 LlmError 标准码全集)+ 环境密钥编辑 + 明文揭示 + 短 ref
|
|
4
|
-
"version": "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.2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
}
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@deepseek-ai/cordis": "4.0.1 || 4.0.2",
|
|
42
|
-
"@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",
|
|
43
|
-
"@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",
|
|
44
|
-
"@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",
|
|
45
|
-
"@deepseek-ai/schemastery": "3.18.1 || 3.18.2"
|
|
41
|
+
"@deepseek-ai/cordis": "4.0.1 || 4.0.2 || 4.0.4",
|
|
42
|
+
"@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",
|
|
43
|
+
"@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",
|
|
44
|
+
"@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",
|
|
45
|
+
"@deepseek-ai/schemastery": "3.18.1 || 3.18.2 || 3.18.4"
|
|
46
46
|
}
|
|
47
47
|
}
|