@goodandready/dsh-key-rotation 0.8.8 → 0.8.11

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 CHANGED
@@ -22,12 +22,41 @@
22
22
  <a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
23
23
  </p>
24
24
 
25
+ <table align="center">
26
+ <tr>
27
+ <td align="center">
28
+ ⭐ <strong>If you like this plugin, please star it on GitHub</strong> — it shows me that the plugin is useful to you and motivates me to keep developing it.
29
+ <br><br>
30
+ 🐛 <strong>If you find a bug or would like to request a feature</strong>, open a GitHub issue in any language — I will review your proposal and implement useful suggestions in a future plugin version.
31
+ </td>
32
+ </tr>
33
+ </table>
34
+
25
35
  </div>
26
36
 
27
37
  ---
28
38
 
29
39
  ## ⚡ Overview & The Problem
30
40
 
41
+ ### 🚀 What's New in v0.8.11 (One-click Updater & Quality Gate)
42
+
43
+ - **Plugin updater in Settings**: see current/latest version and update from the card without leaving DSH (`#307`).
44
+ - **Safer best-effort side effects**: intentional non-critical failures log at debug instead of silent empty `catch` (`#315`).
45
+ - **Theme-only client colors** and production-path test cleanup (`#311`, `#314`).
46
+ - **Leaner publication set**: agent-only files no longer ship in git/npm (`#308`).
47
+
48
+ ### 🚀 What's New in v0.8.10 (Stream Concurrency Hardening & Auto-Pruning)
49
+ - **Zero Concurrency Leaks**: Guaranteed release of stream concurrency slots via deterministic `try ... finally` block, preventing key starvation during clean finishes or client stream aborts.
50
+ - **Robust Probe Retry**: Added transient network socket error retry (`PROBE_RETRY_DELAY_MS`) in `SandboxRunner.probeModels` before marking keys as broken.
51
+ - **Memory & State Pruning**: Automated pruning of removed/stale keys from internal pool maps during periodic sweep cycles.
52
+
53
+ ### 🚀 What's New in v0.8.9 (Routing Evolution & UI)
54
+ - **Proactive Rate-Limit Guard**: Automatic key pausing based on `x-ratelimit-remaining-*` and `Retry-After` headers before hitting 429 errors.
55
+ - **Self-Healing / Auto-Unbreak**: Periodic background probe via free `/models` endpoint to automatically revive broken keys without token burn.
56
+ - **Latency-Aware Routing**: Selectable strategies: `round-robin`, `least-loaded` (concurrency), and `lowest-latency` (p95 latency).
57
+ - **UI Evolution in `dsh-clinebot` Style**: Sequential batch "Test All Keys" runner with live progress, Live Event Stream drawer, and Quota Reset countdown badge.
58
+ - **Native Dual Language Support**: Native English (`en`) and Chinese (`zh`) UI dictionaries.
59
+
31
60
  ### 🛠️ What's New in v0.8.0 (Stability)
32
61
  - **🔌 Circuit breaker**: after N consecutive provider failures the circuit opens and requests fail fast (`CIRCUIT_OPEN`) until a cool-down; half-open probes recover automatically.
33
62
  - **🕒 Monotonic clock**: cooldown/breaker durations use process monotonic time so NTP steps cannot invert remaining times.
@@ -38,7 +67,6 @@
38
67
  - **📡 Status extras**: per-provider `circuit` plus `meta.expectedClones` / `meta.notifyQueue`.
39
68
  - **🧪 Smoke harness**: scripted 429 → next-key → success path in `test/smoke-rotation-080.test.mjs`.
40
69
 
41
-
42
70
  ### 🛠️ What's New in v0.7.33 (Stability & Bugfix Release)
43
71
  - **🔍 Resolved Key Probing BaseURL**: Fixed `resolveBaseUrl` to map key credential refs to owning provider pools, restoring live `probeModels` testing.
44
72
  - **🛡️ Guarded Cascade Recursion**: Prevented call stack overflow in cross-provider failover when circular cascade chains occur.
@@ -49,7 +77,6 @@
49
77
  - **🎨 Native Design System (Changed in v0.8.3)**: Unified with `dsh-clinebot` baseline: modular section cards, live pool telemetry stat boxes, pill badges, and complete semantic theme token styling (#281).
50
78
  - **🌐 Localization (Changed in v0.8.2)**: Source strings are English-only. Russian/Chinese UI comes from the DSH core locale service and translation plugins (`props.t`). Active locale: host snapshot → first `navigator.languages` entry → `en` (#277).
51
79
 
52
-
53
80
  ### 🚀 What's New in v0.7.31
54
81
  - **⚡ O(1) TokenBucket Accumulator**: Upgraded rate limiting math to O(1) time and zero-allocation memory with adaptive header synchronization.
55
82
  - **🛡️ Soft vs Hard Backoff**: Differentiates transient infrastructure drops (502/503/timeouts: 10s flat cooldown) from hard quota errors (progressive doubling).
@@ -61,7 +88,6 @@
61
88
  - **🧹 30-Day Usage Compaction**: Automatic bounded memory management with 30-day rolling window data pruning.
62
89
  - **✨ Optimistic UI & Filter Pills**: Instant zero-latency UI updates on reset, plus `All`, `Ready`, `In Cooldown`, and `With Errors` quick filter chips.
63
90
 
64
-
65
91
  High-throughput autonomous agent workflows, parallel subagent swarms, and multi-turn tool loops inevitably hit upstream API rate limits (HTTP 429, RPM/TPM exhaustion, daily quotas, or sudden provider outages). In standard DeepSeek Harness deployments, a single exhausted API key breaks the entire agent execution chain, requiring manual intervention and destroying the session's replay state.
66
92
 
67
93
  **`dsh-key-rotation`** provides a seamless, enterprise-ready **transparent API key pooling, pre-emptive rate-limiting, and cross-provider failover engine** built natively on the Cordis microkernel architecture.
@@ -145,6 +171,17 @@ graph LR
145
171
 
146
172
  ---
147
173
 
174
+
175
+ ### 🔁 8. One-click Plugin Updater
176
+
177
+ The settings card includes an **Updater** section:
178
+
179
+ 1. **Check for updates** — `GET /api/dsh-key-rotation/update` returns `currentVersion`, `latestVersion`, `updateAvailable`, `canAutoUpdate` (version metadata only; no secrets).
180
+ 2. **Update now** — `POST` with the same path installs the exact latest npm version through the standard `dsh plugin add` flow. The request is accepted only from loopback with a matching same-origin `Origin`/`Host` (and the dedicated header). Cross-origin or missing-origin POST is rejected with `403`.
181
+ 3. After a successful install the UI tells you to **restart DSH** so the new host code loads.
182
+
183
+ No `--force`, no raw shell, no install from worktree/DEV paths. Update runs only after an explicit click.
184
+
148
185
  ## 🖥️ Rich Web GUI & Dashboard
149
186
 
150
187
  Access full visual management under **Settings → Key Rotation** or via the Header quick-widget.
package/README.ru.md CHANGED
@@ -22,12 +22,41 @@
22
22
  <a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
23
23
  </p>
24
24
 
25
+ <table align="center">
26
+ <tr>
27
+ <td align="center">
28
+ ⭐ <strong>Если вам нравится этот плагин, поставьте ему звезду на GitHub</strong> — это покажет мне, что плагин вам полезен, и будет мотивировать меня развивать его дальше.
29
+ <br><br>
30
+ 🐛 <strong>Если вы нашли баг или хотите предложить новый функционал</strong>, создайте issue на GitHub на любом языке — я рассмотрю ваше предложение и реализую полезные идеи в одной из следующих версий плагина.
31
+ </td>
32
+ </tr>
33
+ </table>
34
+
25
35
  </div>
26
36
 
27
37
  ---
28
38
 
39
+ ### 🚀 Что нового в v0.8.11 (обновление в один клик и quality gate)
40
+
41
+ - **Обновление плагина в карточке настроек**: текущая/последняя версия и кнопка установки без выхода из DSH (#307).
42
+ - **bestEffort вместо пустых catch**: осознанные побочные эффекты логируются на debug, а не глотаются (#315).
43
+ - **Цвета клиента только из токенов темы** и чистка production-path тестов (#311, #314).
44
+ - **Публикационный набор без служебных файлов агентов** (#308).
45
+
29
46
  ## ⚡ Обзор и решаемая проблема
30
47
 
48
+ ### 🚀 Что нового в версии 0.8.10 (Надёжность стриминга и очистка памяти)
49
+ - **Устранение утечек счётчика соединений**: Гарантированное освобождение слота конкурентности в `try ... finally`, исключающее блокировку ключей при успешном завершении стрима или прерывании клиентом.
50
+ - **Повтор при сетевых сбоях проверки**: 1 автоматический повтор при кратковременных сокетных ошибках в `probeModels` перед дисквалификацией ключа.
51
+ - **Очистка устаревших ключей**: Периодическая автоматическая очистка удалённых ключей из внутренних карт состояния пула при регулярном цикле `sweep`.
52
+
53
+ ### 🚀 Что нового в версии 0.8.9 (Развитие маршрутизации и UI)
54
+ - **Превентивная защита от лимитов (Proactive Rate-Limit Guard)**: Автоматическая пауза ключей по заголовкам `x-ratelimit-remaining-*` и `Retry-After` до наступления ошибки 429.
55
+ - **Авто-восстановление (Self-Healing / Auto-Unbreak)**: Периодический фоновый опрос эндпоинта `/models` для автоматического снятия статуса `broken` без расхода чат-токенов.
56
+ - **Маршрутизация по задержке (Latency-Aware Routing)**: Выбор стратегии балансировки: `round-robin`, `least-loaded` (по активным запросам) и `lowest-latency` (по p95 задержки).
57
+ - **Обновление интерфейса в стиле `dsh-clinebot`**: Пакетное тестирование всех ключей («Test All Keys») с прогрессом, аккордеон живого потока событий ротации (`.krot-event-stream`) и таймер обратного отсчета сброса квот.
58
+ - **Встроенная поддержка двух языков**: Готовые словари интерфейса на английском (`en`) и китайском (`zh`).
59
+
31
60
  ### 🛠️ Что нового в версии 0.8.0 (Стабильность)
32
61
  - **🔌 Автомат выключения провайдера**: после N подряд ошибок контур открывается, запросы быстро завершаются (`CIRCUIT_OPEN`); half-open пробы восстанавливают сервис.
33
62
  - **🕒 Монотонные часы**: cooldown/breaker считаются по процессным часам, NTP не ломает оставшееся время.
@@ -38,7 +67,6 @@
38
67
  - **📡 Status**: `circuit` у провайдера + `meta.expectedClones` / `meta.notifyQueue`.
39
68
  - **🧪 Smoke**: сценарий 429 → следующий ключ → успех.
40
69
 
41
-
42
70
  ### 🛠️ Что нового в версии 0.7.33 (Хотфикс и повышение стабильности)
43
71
  - **🔍 Исправление проверки ключей**: Функция `resolveBaseUrl` теперь сопоставляет ref ключа с его пулом, восстанавливая работу живого тестирования моделей.
44
72
  - **🛡️ Защита от зацикливания каскада**: Устранена возможность переполнения стека при взаимных кольцевых цепочках failover.
@@ -47,7 +75,6 @@
47
75
  - **⚡ Сброс зависших блокировок**: Алгоритм `pickLeastLoaded` теперь учитывает протухшие блокировки при выборе наименее нагруженного ключа.
48
76
  - **🌐 Полная китайская локализация**: Добавлен словарь `zh` в веб-интерфейс настроек для соблюдения стандарта трёх языков (EN/RU/ZH).
49
77
 
50
-
51
78
  ### 🚀 Что нового в версии 0.7.31
52
79
  - **⚡ $O(1)$ Накопитель TokenBucket**: Оптимизация математики лимитов до $O(1)$ по времени и без аллокаций памяти с адаптивной синхронизацией по заголовкам.
53
80
  - **🛡️ Разделение Soft / Hard сбоев**: Кратковременные сетевые сбои (502/503/таймауты) получают короткий 10-секундный кулдаун без штрафного удвоения.
@@ -59,7 +86,6 @@
59
86
  - **🧹 30-дневная компактизация**: Автоматическая очистка истории старше 30 дней для защиты от утечек памяти.
60
87
  - **✨ Оптимистичный UI и фильтры**: Мгновенный отклик кнопок сброса и статус-пилюли (`Все`, `Готовы`, `В кулдауне`, `С ошибками`) над списком ключей.
61
88
 
62
-
63
89
  При активной работе автономных агентов, параллельном запуске субагентов и циклических вызовах инструментов запросы неизбежно упираются в ограничения провайдеров (ошибки HTTP 429 Too Many Requests, исчерпание суточных квот или временные сбои на стороне апстрима). В стандартной конфигурации DeepSeek Harness исчерпание одного API-ключа полностью блокирует цепочку рассуждений агента, ломает сохранённое состояние диалога (Replay State) и требует ручного вмешательства администратора.
64
90
 
65
91
  **`dsh-key-rotation`** реализует отказоустойчивую корпоративную архитектуру **пулов API-ключей с предиктивным контролем лимитов (Token Bucket) и каскадным переключением на резервных провайдеров**, нативно интегрированную в микроядро Cordis.
@@ -171,6 +197,17 @@ graph LR
171
197
 
172
198
  ---
173
199
 
200
+
201
+ ### 🔁 8. Обновление плагина в один клик
202
+
203
+ В карточке настроек есть раздел **Updater**:
204
+
205
+ 1. **Проверить обновления** — `GET /api/dsh-key-rotation/update` возвращает `currentVersion`, `latestVersion`, `updateAvailable`, `canAutoUpdate` (только метаданные версии).
206
+ 2. **Обновить сейчас** — `POST` ставит точную последнюю npm-версию штатным `dsh plugin add`. Запрос принимается только с loopback и совпадающим same-origin `Origin`/`Host`; иначе `403`.
207
+ 3. После установки UI предлагает **перезапустить DSH**.
208
+
209
+ Без `--force`, без raw shell и установки из worktree/DEV. Обновление — только по явному нажатию.
210
+
174
211
  ## 📦 Установка
175
212
 
176
213
  ```bash
package/README.zh.md CHANGED
@@ -22,12 +22,41 @@
22
22
  <a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
23
23
  </p>
24
24
 
25
+ <table align="center">
26
+ <tr>
27
+ <td align="center">
28
+ ⭐ <strong>如果您喜欢这个插件,请在 GitHub 上为它点亮 Star</strong> — 这能让我知道插件对您有用,并鼓励我继续开发和维护它。
29
+ <br><br>
30
+ 🐛 <strong>如果您发现 Bug 或希望增加功能</strong>,请使用任意语言在 GitHub 上提交 Issue — 我会评估您的建议,并在后续版本中实现有价值的改进。
31
+ </td>
32
+ </tr>
33
+ </table>
34
+
25
35
  </div>
26
36
 
27
37
  ---
28
38
 
29
39
  ## ⚡ 概述与核心痛点
30
40
 
41
+ ### 🚀 v0.8.11 新特性(一键更新与质量门禁)
42
+
43
+ - **设置卡片中的插件更新器**:查看当前/最新版本并一键安装(#307)。
44
+ - **bestEffort 替代空 catch**:非关键副作用在 debug 级别记录,不再被静默吞掉(#315).
45
+ - **客户端颜色仅使用主题变量**,并清理 production-path 测试(#311、#314)。
46
+ - **发布集不再包含代理专用文件**(#308)。
47
+
48
+ ### 🚀 v0.8.10 版本新特性(流并发加固与状态自动修剪)
49
+ - **杜绝并发计数泄漏**:在 `try ... finally` 中强制释放流并发占用,防止在正常完成或客户端中断时密钥被永久锁定。
50
+ - **探测抗网络抖动重试**:在 `probeModels` 中遇到套接字网络临时错误时自动重试一次,避免密钥被误判损坏。
51
+ - **内存与过期状态自动清理**:在定期清理周期中自动清除已删除密钥的内部映射记录。
52
+
53
+ ### 🚀 v0.8.9 版本新特性(智能路由与界面升级)
54
+ - **前瞻性限流防护 (Proactive Rate-Limit Guard)**:根据 `x-ratelimit-remaining-*` 和 `Retry-After` 响应头在触发 429 错误前自动预冷密钥。
55
+ - **自愈恢复 (Self-Healing / Auto-Unbreak)**:通过免费的 `/models` 接口进行定期后台探测,自动恢复处于 `broken` 状态的密钥,无需消耗聊天代币。
56
+ - **延迟感知路由 (Latency-Aware Routing)**:可选路由策略:`round-robin`(轮询)、`least-loaded`(并发负载最低)与 `lowest-latency`(p95 延迟最低)。
57
+ - **`dsh-clinebot` 风格界面升级**:支持带进度提示的批量“测试所有密钥”(Test All Keys)、实时事件流折叠抽屉(Live Event Stream)及配额重置倒计时徽标。
58
+ - **原生中英文双语支持**:内置英文(`en`)与中文(`zh`)用户界面词典。
59
+
31
60
  ### 🛠️ v0.8.0 版本新特性(稳定性)
32
61
  - **🔌 熔断器**:连续失败后快速失败(`CIRCUIT_OPEN`),半开探测自动恢复。
33
62
  - **🕒 单调时钟**:冷却/熔断使用进程单调时间,NTP 校时不会颠倒剩余时间。
@@ -38,7 +67,6 @@
38
67
  - **📡 Status**:提供商 `circuit` + `meta`。
39
68
  - **🧪 Smoke**:429 → 切换密钥 → 成功。
40
69
 
41
-
42
70
  ### 🛠️ v0.7.33 版本新特性 (稳定性与问题修复)
43
71
  - **🔍 修复密钥探测 BaseURL 解析**:`resolveBaseUrl` 现已支持从密钥 ref 反查归属提供商池,恢复在线模型连通性探测。
44
72
  - **🛡️ 防御级联无限递归**:在跨提供商故障转移中增加递归深度防护,彻底杜绝循环级联导致的堆栈溢出。
@@ -47,7 +75,6 @@
47
75
  - **⚡ 负载均衡超时锁自动释放**:`pickLeastLoaded` 算法现已检测过期连接锁,确保最小连接调度不发生偏移。
48
76
  - **🌐 完整中文界面本地化**:为 React 设置面板补充全部 `zh` 语言包,实现标准的三语(英/俄/中)无缝对齐。
49
77
 
50
-
51
78
  ### 🚀 v0.7.31 版本新特性
52
79
  - **⚡ O(1) 令牌桶累加器**:将速率限制计算升级为 O(1) 时间复杂度与零内存分配,并支持响应头自适应同步。
53
80
  - **🛡️ 软/硬故障分级退避**:区分临时网络抖动(502/503/超时获得 10 秒平缓冷却)与硬性配额超限(指数退避倍增)。
@@ -59,7 +86,6 @@
59
86
  - **🧹 30 天用量压缩**:自动清理超过 30 天的历史统计数据,保障长期运行内存上限。
60
87
  - **✨ 乐观 UI 与快速筛选标签**:一键重置即时生效,密钥列表新增 `全部`、`就绪`、`冷却中`、`故障` 状态筛选胶囊。
61
88
 
62
-
63
89
  在高吞吐量自主智能体运行、多子智能体并行执行与多轮工具调用场景下,API 极易触发上游服务商的速率限制(HTTP 429 Too Many Requests、RPM/TPM 耗尽、每日配额限制或网络抖动)。在原生的 DeepSeek Harness 中,单个密钥耗尽会导致整个智能体执行链路崩溃,破坏会话的 Replay 状态并要求人工干预。
64
90
 
65
91
  **`dsh-key-rotation`** 基于 Cordis 微内核架构构建,提供了无缝透明的 **API 密钥池轮换、客户端预判限流(Token Bucket)与跨提供商故障转移(Failover Cascade)** 解决方案。
@@ -263,7 +289,6 @@ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
263
289
  - **本地化与文案 (Changed in v0.8.2)**: 插件仅注册英文源字符串 `en`;俄文/中文由 DSH 核心 locale 与 translation 插件通过 `props.t` 提供。活动语言回退:snapshot → 首个 `navigator.languages` → `en`。已移除 `settings.section` 回退与内置 `ru`/`zh` 表 (#236, #275, #277)。
264
290
  - **死代码清理**: 移除 header-chip 迁移后残留的废弃 `mountDashboard` 函数 (#240)。
265
291
 
266
-
267
292
  ### 熔断器参数(v0.8.0)
268
293
 
269
294
  | 参数 | 类型 | 默认 | 说明 |
@@ -0,0 +1,26 @@
1
+ /** Run a non-critical side effect; never throw. Logs at debug when a logger is provided. */
2
+ export function bestEffort(label, fn, logger) {
3
+ const log = (err) => {
4
+ try {
5
+ logger?.debug?.(`dsh-key-rotation: best-effort ${label}`, err);
6
+ } catch {
7
+ /* logger itself must not throw */
8
+ }
9
+ };
10
+ try {
11
+ const result = fn();
12
+ if (result && typeof result.then === 'function') {
13
+ return result.then(
14
+ (value) => value,
15
+ (err) => {
16
+ log(err);
17
+ return undefined;
18
+ },
19
+ );
20
+ }
21
+ return result;
22
+ } catch (err) {
23
+ log(err);
24
+ return undefined;
25
+ }
26
+ }
package/lib/bucket.js CHANGED
@@ -1,65 +1,6 @@
1
- // lib/bucket.js - per-key RPM token bucket (#192) + O(1) accumulator and adaptive tuning.
1
+ // lib/bucket.js - per-key RPM token bucket (#192).
2
2
  const WINDOW_MS = 60000;
3
3
 
4
- /**
5
- * O(1) Mathematical Token Bucket Accumulator.
6
- * tokens = min(capacity, tokens + (now - lastRefill) * refillRate)
7
- */
8
- export class TokenBucketAccumulator {
9
- constructor(capacity, windowMs = WINDOW_MS, now = Date.now()) {
10
- this.capacity = Math.max(1, capacity);
11
- this.tokens = this.capacity;
12
- this.windowMs = windowMs;
13
- this.refillRate = this.capacity / this.windowMs; // tokens per ms
14
- this.lastRefill = now;
15
- }
16
-
17
- refill(now = Date.now()) {
18
- const elapsed = Math.max(0, now - this.lastRefill);
19
- if (elapsed > 0) {
20
- this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillRate);
21
- this.lastRefill = now;
22
- }
23
- }
24
-
25
- allow(cost = 1, now = Date.now()) {
26
- this.refill(now);
27
- if (this.tokens >= cost) {
28
- this.tokens -= cost;
29
- return true;
30
- }
31
- return false;
32
- }
33
-
34
- retryMs(cost = 1, now = Date.now()) {
35
- this.refill(now);
36
- if (this.tokens >= cost) return 0;
37
- const needed = cost - this.tokens;
38
- return Math.ceil(needed / this.refillRate);
39
- }
40
-
41
- updateCapacity(newCapacity, now = Date.now()) {
42
- this.refill(now);
43
- const prevCap = this.capacity;
44
- this.capacity = Math.max(1, newCapacity);
45
- this.refillRate = this.capacity / this.windowMs;
46
- // Scale current tokens proportionally or clamp
47
- this.tokens = Math.min(this.capacity, Math.max(0, this.tokens + (this.capacity - prevCap)));
48
- }
49
-
50
- info(now = Date.now()) {
51
- this.refill(now);
52
- const used = Math.max(0, Math.round(this.capacity - this.tokens));
53
- const remaining = Math.max(0, Math.floor(this.tokens));
54
- return {
55
- used,
56
- remaining,
57
- resetMs: this.retryMs(1, now),
58
- capacity: this.capacity,
59
- };
60
- }
61
- }
62
-
63
4
  /** Sliding-window check: true if `ref` is under `limit` requests/min. */
64
5
  export function bucketAllow(windows, ref, limit, now = Date.now()) {
65
6
  if (!limit || limit <= 0) return true;
@@ -74,14 +15,6 @@ export function bucketAllow(windows, ref, limit, now = Date.now()) {
74
15
  return true;
75
16
  }
76
17
 
77
- /** Record a hit without checking (use after a successful resolve). */
78
- export function bucketHit(windows, ref, now = Date.now()) {
79
- const cut = now - WINDOW_MS;
80
- const hits = (windows.get(ref) ?? []).filter((t) => t > cut);
81
- hits.push(now);
82
- windows.set(ref, hits);
83
- }
84
-
85
18
  /** ms until `ref` may retry again (0 = now). */
86
19
  export function bucketRetryMs(windows, ref, limit, now = Date.now()) {
87
20
  if (!limit || limit <= 0) return 0;
@@ -108,22 +41,3 @@ export function bucketInfo(windows, ref, limit, now = Date.now()) {
108
41
  };
109
42
  }
110
43
 
111
- /**
112
- * Adaptive limit computation from HTTP headers with manual limit precedence.
113
- * If manualLimit is given (> 0), it acts as the upper ceiling.
114
- * If adaptive is enabled and upstream reports a lower limit/remaining, adapt downwards.
115
- */
116
- export function computeEffectiveLimit(headerRateLimit, manualLimit, adaptiveEnabled = true) {
117
- const manual = (Number.isFinite(manualLimit) && manualLimit > 0) ? manualLimit : null;
118
- if (!adaptiveEnabled || !headerRateLimit) {
119
- return manual;
120
- }
121
- const headerLimit = headerRateLimit.limit;
122
- if (Number.isFinite(headerLimit) && headerLimit > 0) {
123
- if (manual) {
124
- return Math.min(manual, headerLimit); // manual acts as upper ceiling
125
- }
126
- return headerLimit;
127
- }
128
- return manual;
129
- }
package/lib/cascade.js CHANGED
@@ -1,39 +1,27 @@
1
- // cascade.js — cross-provider failover cascade (issue #194).
2
- // ponytail: minimal — pick fallback provider from a RegionMap-like config.
3
-
4
- export const CASCADE_MAX_DEPTH = 1;
5
-
6
- export function pickCascadeFallback(provider, cfg, pools) {
7
- const list = Array.isArray(cfg && cfg.cascade) ? cfg.cascade : [];
8
- for (const entry of list) {
9
- const fb = typeof entry === 'string' ? { provider: entry } : entry;
10
- if (!fb || !fb.provider || fb.provider === provider) continue;
11
- const pool = pools instanceof Map ? pools.get(fb.provider) : (pools ? pools[fb.provider] : null);
12
- if (!pool) continue;
13
- const now = Date.now();
14
- let healthy = 0;
15
- for (const ref of pool.refs) {
16
- const failedUntil = (pool.state && pool.state.failedUntil && pool.state.failedUntil.get(ref)) || 0;
17
- if (failedUntil > now) continue;
18
- const exp = pool.expiresAt ? pool.expiresAt[ref] : undefined;
19
- if (exp !== undefined && now >= exp) continue;
20
- healthy += 1;
21
- }
22
- if (healthy === 0) continue;
23
- return { provider: fb.provider, pool, model: fb.model || null };
24
- }
25
- return null;
26
- }
27
-
28
- export function hasHealthyKey(pool, now) {
29
- now = now || Date.now();
30
- if (!pool || !Array.isArray(pool.refs)) return false;
31
- for (const ref of pool.refs) {
32
- const failedUntil = (pool.state && pool.state.failedUntil && pool.state.failedUntil.get(ref)) || 0;
33
- if (failedUntil > now) continue;
34
- const exp = pool.expiresAt ? pool.expiresAt[ref] : undefined;
35
- if (exp !== undefined && now >= exp) continue;
36
- return true;
37
- }
38
- return false;
39
- }
1
+ // cascade.js — cross-provider failover cascade (issue #194).
2
+ // ponytail: minimal — pick fallback provider from a RegionMap-like config.
3
+
4
+ export const CASCADE_MAX_DEPTH = 1;
5
+
6
+ export function pickCascadeFallback(provider, cfg, pools) {
7
+ const list = Array.isArray(cfg && cfg.cascade) ? cfg.cascade : [];
8
+ for (const entry of list) {
9
+ const fb = typeof entry === 'string' ? { provider: entry } : entry;
10
+ if (!fb || !fb.provider || fb.provider === provider) continue;
11
+ const pool = pools instanceof Map ? pools.get(fb.provider) : (pools ? pools[fb.provider] : null);
12
+ if (!pool) continue;
13
+ const now = Date.now();
14
+ let healthy = 0;
15
+ for (const ref of pool.refs) {
16
+ const failedUntil = (pool.state && pool.state.failedUntil && pool.state.failedUntil.get(ref)) || 0;
17
+ if (failedUntil > now) continue;
18
+ const exp = pool.expiresAt ? pool.expiresAt[ref] : undefined;
19
+ if (exp !== undefined && now >= exp) continue;
20
+ healthy += 1;
21
+ }
22
+ if (healthy === 0) continue;
23
+ return { provider: fb.provider, pool, model: fb.model || null };
24
+ }
25
+ return null;
26
+ }
27
+