@goodandready/dsh-clinebot 0.3.25 → 0.4.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/CHANGELOG.md CHANGED
@@ -5,6 +5,38 @@ All notable changes to `@goodandready/dsh-clinebot` will be documented in this f
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.1] - 2026-09-24
9
+
10
+ ### Fixed
11
+ - **Slash Command Contract Fix**: Fixed `/cline` slash command handler to strictly adhere to DSH `@deepseek-ai/dsh-commands` runtime `CommandResult` contract `{ kind: 'success', text }` instead of returning raw strings, preventing `TypeError: command "cline" handler must return a CommandResult` in DSH chat.
12
+ - **Provider Detection via SettingsForms describe() (#102)**: Updated `checkRegisteredInPiAi` to inspect `settings.describe()` descriptors directly without triggering `TypeError` on missing `get()` method in modern DSH `SettingsForms`. Added safe error absorption for `providers.clinebot` removal when already absent.
13
+ - **Model Version Dots Preservation (#103)**: Fixed model parsing to preserve version dot notation (e.g. `claude-3.5-sonnet`, `deepseek-v4.5`), preventing incorrect dot stripping or catalog matching errors.
14
+ - **Strict Credential Validation & HTTPS Key Verify (#82)**: Hardened `/save-key` and `/config` to strictly prevent bypassing `apiKeyEnv` naming rules (forbidding arbitrary standard env variables such as `OPENAI_API_KEY`), and enforced HTTPS for `/key/verify` remote endpoints.
15
+ - **Settings Rollback & Error Display (#81)**: Implemented optimistic UI rollback and explicit error alert banner when model toggle or settings persistence fails, reloading current server state and properly cleaning up debounced unmount timers.
16
+ - **Snapshot Warning Banner & Failover Localization (#104)**: Restored non-blocking warning banner when host `snapshotStatus === 'unavailable'`, and localized `Last failover: ...` via `t('accounts.last_failover')` across English and Chinese locale dictionaries.
17
+
18
+ ### Added
19
+ - **Account Pool Form & Management Routes (#88)**: Completed full account pool UI with dedicated "Add Account" form (label, apiKeyEnv with auto-suggestions, secret input with show/hide toggle), account deletion with confirmation, model search input (`modelsSearch`), and category filter tabs (all, chat, coding, reasoning). Registered `POST /dsh-clinebot/accounts` and `POST /dsh-clinebot/accounts/delete` server endpoints.
20
+ - **Provider Key Failover Verification (#85)**: Added behavioral test assertions confirming that `rotateToNextAccount` mutates `llm-pi-ai` provider settings with the rotated account's `apiKeyEnv`, and replaced source code string inspection in `settings-service.test.js` with pure behavioral mock tests.
21
+ - **Repository Hygiene (#91)**: Pruned obsolete remote tracking branches, confirmed duplicate issue label resolution, and ensured clean working tree.
22
+
23
+ ## [0.4.0] - 2026-09-24
24
+
25
+ ### Added
26
+ - **One-Click In-App Updater (#91)**: Self-update companion plugin directly from the DSH settings card or via loopback `POST /dsh-clinebot/update` with rate limiting, package manifest verification, and restart indicators.
27
+ - **Real DSH Stream 429 & Quota Failover (#79)**: Intercepts HTTP 429 and quota exhaustion at the `llm/stream` waterfall level, automatically rotating to the next account in the pool with 30s storm protection and immediately synchronizing the provider's `apiKeyEnv` in `llm-pi-ai`.
28
+ - **Dynamic Plan Models Discovery (#90)**: Discovers models dynamically from active ClinePass subscription plan via `POST /dsh-clinebot/models/sync`. Removes hardcoded pricing references from UI and settings.
29
+ - **Stream Telemetry & Token Tracking (#83)**: Tracks real DSH chat requests through the ClineBot provider, prompt and completion tokens from stream usage chunks, stream latency, and error counts since process startup.
30
+ - **Live API Key Verification (#84)**: Direct navigation to `https://app.cline.bot/settings/api-keys` and on-the-fly key verification endpoint `POST /dsh-clinebot/key/verify` displaying account email and subscription plan in the UI.
31
+ - **Behavioral Test Suite (#85)**: Comprehensive test suite testing all write endpoints for HTTP 405 Method Not Allowed, HTTP 403 Forbidden on untrusted origins, service error reporting (503), and behavioral execution of all `/cline` slash subcommands.
32
+
33
+ ### Changed
34
+ - **Hardened PUT /dsh-clinebot/config (#89)**: Merges from raw `live()`, accepts only known schema fields, strictly rejects unknown properties with 400 Bad Request, rejects deprecated `enabledModels`, and documents HTTP API in README.
35
+ - **Cordis 4 Clean apply() Return (#89)**: `apply()` now returns `undefined` to eliminate `TypeError: Invalid effect` in Cordis 4, and dead branches without `ctx.inject` were removed.
36
+ - **Strict Credential Naming Pattern (#82)**: Restricts `/dsh-clinebot/save-key` target environment variables strictly to `^CLINEBOT_API_KEY(_[A-Z0-9]+)?$`.
37
+ - **Honest Persistence Reporting (#81)**: Write endpoints report honest errors and HTTP 503 when the DSH settings service is unavailable.
38
+ - **UI and Locale Refinements (#86, #87, #88)**: Eliminated undeclared `ctx` reference in `PluginCard`, removed dead state loops in `SettingsPage`, corrected layout spacing and badge styling across localized cards.
39
+
8
40
  ## [0.3.25] - 2026-09-24
9
41
 
10
42
  ### Fixed
package/README.md CHANGED
@@ -38,7 +38,7 @@
38
38
 
39
39
  ## ⚡ Overview & The Problem
40
40
 
41
- **ClinePass** (`https://cline.bot`) is a flat-rate subscription service ($9.99/mo) providing developers with 2–5x higher rate limits across premier open-weights coding and reasoning models through a single OpenAI-compatible endpoint (`https://api.cline.bot/api/v1`).
41
+ **ClinePass** (`https://cline.bot`) is a subscription service providing developers with 2–5x higher rate limits across premier open-weights coding and reasoning models through a single OpenAI-compatible endpoint (`https://api.cline.bot/api/v1`).
42
42
 
43
43
  Integrating ClinePass into DeepSeek Harness (DSH) natively poses key challenges:
44
44
  1. **No `/v1/models` Discovery**: `GET /v1/models` on `api.cline.bot` returns `404 Not Found`. Dynamic discovery fails silently or leaves the provider with 0 models.
@@ -49,11 +49,11 @@ Integrating ClinePass into DeepSeek Harness (DSH) natively poses key challenges:
49
49
  **`@goodandready/dsh-clinebot`** provides a complete solution:
50
50
  * 🚀 **One-Click In-App Updater**: Upgrade `@goodandready/dsh-clinebot` directly from the DSH UI or trigger secure loopback updates via `/dsh-clinebot/update`.
51
51
  * ⚡ **SWR Quota & Health Caching**: Instantaneous response time (<2ms) on status queries with background revalidation.
52
- * 🔀 **Smart Quota-Aware Failover**: Automatic multi-account rotation skipping exhausted accounts and recovering when `resetsAt` is reached.
52
+ * 🔀 **Smart Quota-Aware Failover**: Automatic multi-account rotation on stream HTTP 429 and exhausted quota (with 30s storm protection; current request is not retried, subsequent chat requests use the next available account).
53
53
  * 🖥️ **Plugin configuration page**: Open the installed ClineBot plugin and choose configure. The page shows the credential name, models, quota, and accounts. It is not a separate sidebar section.
54
- * 🔄 **Dynamic Subscription Model Sync**: Automatically pulls real models included in your ClinePass plan directly from `GET /api/v1/users/me/plan` with one-click DSH provider sync.
54
+ * 🔄 **Dynamic Subscription Model Sync**: Automatically pulls real models included in your ClinePass plan directly from `GET /api/v1/users/me/plan` with one-click DSH provider sync. Only actual plan models are registered in DSH, while the built-in catalog serves as a rich properties reference and fallback when unsynced.
55
55
  * ⚠️ **Quota Exhaustion Alerts**: Real-time visual warning banners when 5-hour rolling limit reaches 80% (warning) and 95% (exhausted), complete with countdown to reset.
56
- * 📈 **Session Metrics Telemetry**: Live dashboard tracking request counts, token consumption estimates, latency, and last-request timestamp.
56
+ * 📈 **Session Metrics Telemetry**: Live dashboard tracking real in-flight DSH chat requests through the ClineBot provider, prompt and completion tokens, stream latency, and error counts since process startup.
57
57
  * 📊 **Live Quota Dashboard**: Visual progress bars for 5-hour rolling limits and weekly windows from the official `GET /users/me/plan/usage-limits` API.
58
58
  * 🔑 **In-UI Key Storage**: Paste your API key directly in the UI; it is saved securely via `ctx.credentials.set()` into `~/.dsh/.credentials.yaml`.
59
59
  * 🎯 **Model Picker Management**: Granular checkboxes to choose which models appear in the chat picker.
@@ -137,7 +137,7 @@ Restart your DeepSeek Harness instance and refresh the browser.
137
137
  From any DSH chat session, type `/cline` to inspect quota, warning alerts, and session telemetry:
138
138
 
139
139
  ```text
140
- ### 🤖 ClinePass Status (ClinePass ($9.99/mo))
140
+ ### 🤖 ClinePass Status (ClinePass)
141
141
  * Latency: ✅ 210 ms
142
142
  * Active Key: CLINEBOT_API_KEY (credentials)
143
143
  * Default Model: `cline-pass/deepseek-v4-flash`
@@ -189,6 +189,22 @@ dsh-clinebot:
189
189
 
190
190
  ---
191
191
 
192
+ ## 🌐 HTTP API Endpoints
193
+
194
+ All endpoints are registered under `/dsh-clinebot/*` and protected against untrusted cross-site origins (same-origin and loopback allowed):
195
+
196
+ * `GET /dsh-clinebot/status` — Live status report including provider health, active credential, quota, and session metrics.
197
+ * `GET /dsh-clinebot/config` — Diagnostic endpoint returning public configuration without secret keys.
198
+ * `PUT /dsh-clinebot/config` — Update configuration fields. Accepts only known schema properties (unknown fields or deprecated `enabledModels` return `400 Bad Request`).
199
+ * `POST /dsh-clinebot/key/verify` — Validates a candidate API key against `api.cline.bot` and returns account email and plan name.
200
+ * `POST /dsh-clinebot/save-key` — Saves a key into DSH credentials service under a valid `CLINEBOT_API_KEY*` name.
201
+ * `POST /dsh-clinebot/models/sync` — Synchronizes models with your active subscription plan.
202
+ * `POST /dsh-clinebot/models/toggle` — Toggles models via `disabledModels`.
203
+ * `POST /dsh-clinebot/accounts/active` — Pins an active account from the account pool.
204
+ * `POST /dsh-clinebot/smoke` — Runs a live latency test ping.
205
+
206
+ ---
207
+
192
208
  ## 🧪 Testing
193
209
 
194
210
  Run the automated test suite:
package/README.ru.md CHANGED
@@ -38,7 +38,7 @@
38
38
 
39
39
  ## ⚡ Обзор и решаемая проблема
40
40
 
41
- **ClinePass** (`https://cline.bot`) — сервис единой фиксированной подписки (\$9.99/мес), предоставляющий разработчикам повышенные лимиты (в 2–5 раз выше стандартных) на передовые open-weights модели программирования и рассуждений через единый OpenAI-совместимый интерфейс (`https://api.cline.bot/api/v1`).
41
+ **ClinePass** (`https://cline.bot`) — сервис единой фиксированной подписки, предоставляющий разработчикам повышенные лимиты (в 2–5 раз выше стандартных) на передовые open-weights модели программирования и рассуждений через единый OpenAI-совместимый интерфейс (`https://api.cline.bot/api/v1`).
42
42
 
43
43
  Интеграция ClinePass в DeepSeek Harness (DSH) напрямую сопряжена со следующими сложностями:
44
44
  1. **Отсутствие эндпоинта `/v1/models`**: запрос `GET /v1/models` к `api.cline.bot` возвращает `404 Not Found`, из-за чего динамический поиск моделей в DSH падает или оставляет список пустым.
@@ -49,11 +49,11 @@
49
49
  Плагин **`@goodandready/dsh-clinebot`** решает эти задачи «из коробки»:
50
50
  * 🚀 **Обновление в один клик**: Обновление плагина прямо из интерфейса DSH или через защищённый loopback-эндпоинт `/dsh-clinebot/update`.
51
51
  * ⚡ **SWR-кэширование квот и здоровья**: Мгновенный ответ (<2 мс) на запросы статуса с фоновым обновлением данных без блокировки UI.
52
- * 🔀 **Smart Quota-Aware Failover**: Интеллектуальная ротация аккаунтов с обходом исчерпанных лимитов и авто-восстановлением при наступлении `resetsAt`.
52
+ * 🔀 **Smart Quota-Aware Failover**: Автоматическая ротация аккаунтов в пуле при ошибках 429 и исчерпании квоты в стриме (с 30-секундной защитой от шторма; текущий запрос не повторяется, последующие запросы сразу идут через следующий аккаунт).
53
53
  * 🖥️ **Страница конфигурации плагина**: откройте установленный ClineBot и перейдите в настройки плагина. Там имя учётных данных, модели, квота и аккаунты. Отдельного пункта бокового меню нет.
54
- * 🔄 **Динамическая синхронизация моделей подписки**: автоматическое получение реального списка моделей из `GET /api/v1/users/me/plan` и мгновенное обновление провайдера DSH в один клик.
54
+ * 🔄 **Динамическая синхронизация моделей подписки**: автоматическое получение реального списка моделей из `GET /api/v1/users/me/plan` и мгновенное обновление провайдера DSH в один клик. В DSH регистрируются строго модели активного тарифа, а встроенный каталог служит справочником характеристик и запасным вариантом при недоступности API.
55
55
  * ⚠️ **Предупреждения об исчерпании квоты**: баннеры предупреждения при достижении 80% (внимание) и 95% (исчерпано) 5-часового лимита с таймером сброса.
56
- * 📈 **Метрики сессии**: учет количества запросов, расчетных токенов (Prompt / Completion), задержки и времени последнего вызова.
56
+ * 📈 **Метрики сессии**: Учет реального трафика DSH-чатов через провайдер ClineBot с момента запуска (успешные, прерванные и ошибочные запросы, токены prompt/completion, задержка стрима).
57
57
  * 📊 **Дашборд лимитов подписки (Usage)**: наглядные прогресс-бары расхода 5-часового и недельного скользящего окна из официального API `GET /users/me/plan/usage-limits`.
58
58
  * 🔑 **Сохранение ключа прямо из UI**: поле ввода ключа с маскировкой; сохранение напрямую в системный сервис `credentials` (`~/.dsh/.credentials.yaml`) без ручной правки файлов на сервере.
59
59
  * 🎯 **Управление моделями в пикере**: включение/выключение отображения конкретных моделей в диалогах чата.
@@ -134,7 +134,7 @@ dsh plugin --profile web add @goodandready/dsh-clinebot
134
134
  В любой сессии чата введите команду `/cline` для проверки остатка лимитов, предупреждений и статистики:
135
135
 
136
136
  ```text
137
- ### 🤖 ClinePass Status (ClinePass ($9.99/mo))
137
+ ### 🤖 ClinePass Status (ClinePass)
138
138
  * Пинг хоста: ✅ 210 мс
139
139
  * Активный ключ: CLINEBOT_API_KEY (credentials)
140
140
  * Модель по умолчанию: `cline-pass/deepseek-v4-flash`
@@ -184,6 +184,22 @@ dsh-clinebot:
184
184
 
185
185
  ---
186
186
 
187
+ ## 🌐 Маршруты HTTP API
188
+
189
+ Все маршруты регистрируются с префиксом `/dsh-clinebot/*` и защищены от несанкционированных межсайтовых запросов (разрешены same-origin и loopback):
190
+
191
+ * `GET /dsh-clinebot/status` — Текущее состояние провайдера, активный аккаунт, квоты и статистика сессий.
192
+ * `GET /dsh-clinebot/config` — Диагностический маршрут, возвращающий открытую конфигурацию (без секретов).
193
+ * `PUT /dsh-clinebot/config` — Обновление параметров конфигурации. Принимает только известные поля схемы (неизвестные поля и устаревшее `enabledModels` возвращают `400 Bad Request`).
194
+ * `POST /dsh-clinebot/key/verify` — Проверка API-ключа на сервере `api.cline.bot` с возвратом email и названия тарифа.
195
+ * `POST /dsh-clinebot/save-key` — Сохранение ключа в сервис credentials DSH под именем шаблона `CLINEBOT_API_KEY*`.
196
+ * `POST /dsh-clinebot/models/sync` — Синхронизация списка моделей с официальным тарифом ClinePass.
197
+ * `POST /dsh-clinebot/models/toggle` — Включение и отключение моделей через `disabledModels`.
198
+ * `POST /dsh-clinebot/accounts/active` — Назначение активного аккаунта из пула.
199
+ * `POST /dsh-clinebot/smoke` — Выполнение тестового пинга задержки.
200
+
201
+ ---
202
+
187
203
  ## 🧪 Тестирование
188
204
 
189
205
  Запуск автоматического набора тестов:
package/README.zh.md CHANGED
@@ -38,7 +38,7 @@
38
38
 
39
39
  ## ⚡ 概述与解决的核心痛点
40
40
 
41
- **ClinePass** (`https://cline.bot`) 是一项固定月费(\$9.99/月)的高性价比订阅服务,为开发者提供主流开源代码模型与推理模型 2–5 倍的高并发调用限额,统一通过 OpenAI 兼容接口 (`https://api.cline.bot/api/v1`) 提供服务。
41
+ **ClinePass** (`https://cline.bot`) 是一项高性价比订阅服务,为开发者提供主流开源代码模型与推理模型 2–5 倍的高并发调用限额,统一通过 OpenAI 兼容接口 (`https://api.cline.bot/api/v1`) 提供服务。
42
42
 
43
43
  在将 ClinePass 接入 DeepSeek Harness (DSH) 时存在以下挑战:
44
44
  1. **缺失 `/v1/models` 接口**:`api.cline.bot` 的 `GET /v1/models` 会直接返回 `404 Not Found`,导致动态模型同步失败或模型列表为空。
@@ -49,11 +49,11 @@
49
49
  **`@goodandready/dsh-clinebot`** 完美解决以上痛点:
50
50
  * 🚀 **应用内一键更新**:直接在 DSH 界面检查并升级插件,或通过受保护的 `/dsh-clinebot/update` 进行本地安全更新。
51
51
  * ⚡ **SWR 配额与健康状态缓存**:状态查询毫秒级响应(<2ms),并在后台静默更新,不阻塞前端渲染。
52
- * 🔀 **智能配额故障转移 (Smart Failover)**:多账号池自动轮询,避开耗尽账号并在 `resetsAt` 到达后自动恢复。
52
+ * 🔀 **智能配额故障转移 (Smart Failover)**:在流式请求遇到 HTTP 429 或配额耗尽时自动轮换多账号(具备 30 秒防风暴保护;当前失败请求不自动重试,后续对话请求无缝使用下一个账号)。
53
53
  * 🖥️ **插件配置页**:打开已安装的 ClineBot,进入配置。页面包含凭据名、模型、配额和账号,不单独占用侧边栏。
54
- * 🔄 **订阅模型动态同步**:从官方 `GET /api/v1/users/me/plan` 自动提取真实包含模型,一键原子级同步至 DSH 提供商配置,无需等待插件更新。
54
+ * 🔄 **订阅模型动态同步**:从官方 `GET /api/v1/users/me/plan` 自动提取真实包含模型,一键原子级同步至 DSH 提供商配置。DSH 严格仅注册当前套餐模型,内置目录作为特性参考并在未同步时作为备用。
55
55
  * ⚠️ **额度耗尽实时预警**:当 5 小时滑动窗口达到 80%(警告黄色)和 95%(即将耗尽红色)时展示醒目预警横幅与重置倒计时。
56
- * 📈 **会话统计与指标看板**:实时追踪请求调用次数、预估 Token(Prompt / Completion)、最近延迟及最后调用时间。
56
+ * 📈 **会话统计与指标看板**:实时监控进程启动后经由 ClineBot 提供商发出的真实 DSH 对话请求、Prompt/Completion Token 消耗、流式延迟及错误统计。
57
57
  * 📊 **实时用量仪表盘**:调用官方 `GET /users/me/plan/usage-limits` API,实时渲染 5 小时与每周额度进度条及重置倒计时。
58
58
  * 🔑 **界面直存密钥**:在 UI 中直接粘贴 API 密钥,通过 `ctx.credentials.set()` 自动安全保存至 `~/.dsh/.credentials.yaml`。
59
59
  * 🎯 **模型选择器管理**:支持勾选开启/关闭特定模型在聊天选择器中的显示。
@@ -136,7 +136,7 @@ dsh plugin --profile web add @goodandready/dsh-clinebot
136
136
  在任何聊天会话中输入 `/cline` 即可即时检查配额、预警状态与会话指标:
137
137
 
138
138
  ```text
139
- ### 🤖 ClinePass Status (ClinePass ($9.99/mo))
139
+ ### 🤖 ClinePass Status (ClinePass)
140
140
  * 响应延迟: ✅ 210 ms
141
141
  * 活跃密钥: CLINEBOT_API_KEY (credentials)
142
142
  * 默认模型: `cline-pass/deepseek-v4-flash`
@@ -186,6 +186,22 @@ dsh-clinebot:
186
186
 
187
187
  ---
188
188
 
189
+ ## 🌐 HTTP API 接口说明
190
+
191
+ 所有接口注册于 `/dsh-clinebot/*` 路径,并受到严格的跨站防护保护(仅允许同源或环回请求):
192
+
193
+ * `GET /dsh-clinebot/status` — 服务运行状态,包括健康探活、当前凭据名称、配额限制及会话统计。
194
+ * `GET /dsh-clinebot/config` — 诊断接口,安全获取脱敏后的公共配置。
195
+ * `PUT /dsh-clinebot/config` — 更新配置项。仅接受模式中的已知字段(未知字段或已废弃的 `enabledModels` 返回 `400 Bad Request`)。
196
+ * `POST /dsh-clinebot/key/verify` — 向 `api.cline.bot` 发送探活请求实时验证密钥,返回绑定邮箱及套餐名称。
197
+ * `POST /dsh-clinebot/save-key` — 将 API 密钥安全存储到 DSH credentials 服务。
198
+ * `POST /dsh-clinebot/models/sync` — 同步 ClinePass 官方套餐内包含的全部动态模型。
199
+ * `POST /dsh-clinebot/models/toggle` — 通过 `disabledModels` 批量切换模型可用性。
200
+ * `POST /dsh-clinebot/accounts/active` — 从多账号池中指定当前主账号。
201
+ * `POST /dsh-clinebot/smoke` — 发起快速探测并返回实时延迟。
202
+
203
+ ---
204
+
189
205
  ## 🧪 测试
190
206
 
191
207
  运行自动化测试套件:
@@ -11,6 +11,27 @@ function textSetting(value) {
11
11
  return typeof current === 'string' ? current.trim() : ''
12
12
  }
13
13
 
14
+ function arraySetting(value) {
15
+ let current = value
16
+ while (current && typeof current === 'object' && !Array.isArray(current) && typeof current.get === 'function') {
17
+ current = current.get()
18
+ }
19
+ return Array.isArray(current) ? current : []
20
+ }
21
+
22
+ function toPlainSnapshot(cfg) {
23
+ if (!cfg || typeof cfg !== 'object') return cfg
24
+ const out = {}
25
+ for (const [k, v] of Object.entries(cfg)) {
26
+ let cur = v
27
+ while (cur && typeof cur === 'object' && !Array.isArray(cur) && typeof cur.get === 'function') {
28
+ cur = cur.get()
29
+ }
30
+ out[k] = cur
31
+ }
32
+ return out
33
+ }
34
+
14
35
  export async function resolveAccountPool(ctx, cfg) {
15
36
  const apiKeyEnv = textSetting(cfg?.apiKeyEnv) || DEFAULT_API_KEY_ENV
16
37
  const defaultSlot = {
@@ -18,7 +39,7 @@ export async function resolveAccountPool(ctx, cfg) {
18
39
  label: 'Default',
19
40
  apiKeyEnv,
20
41
  }
21
- const accounts = Array.isArray(cfg?.accounts) ? cfg.accounts : []
42
+ const accounts = arraySetting(cfg?.accounts)
22
43
  const allSlots = [defaultSlot, ...accounts]
23
44
  const activeAccount = textSetting(cfg?.activeAccount)
24
45
  const resolved = []
@@ -72,7 +93,7 @@ export async function rotateToNextAccount(ctx, cfg, reason = 'rate_limit', setti
72
93
  return { rotated: false, reason, message: 'Pool has only 1 configured account' }
73
94
  }
74
95
 
75
- const active = String(cfg?.activeAccount || configured[0].apiKeyEnv)
96
+ const active = textSetting(cfg?.activeAccount) || configured[0].apiKeyEnv
76
97
  const currentIndex = configured.findIndex((acc) => acc.apiKeyEnv === active)
77
98
 
78
99
  // Candidate pool excluding current account if possible
@@ -105,7 +126,7 @@ export async function rotateToNextAccount(ctx, cfg, reason = 'rate_limit', setti
105
126
  let updated = false
106
127
  if (settingsApi?.replace) {
107
128
  try {
108
- const next = { ...cfg, activeAccount: nextAcc.apiKeyEnv }
129
+ const next = { ...toPlainSnapshot(cfg), activeAccount: nextAcc.apiKeyEnv }
109
130
  await settingsApi.replace(next)
110
131
  updated = true
111
132
  } catch (err) {
@@ -140,11 +161,45 @@ export async function rotateToNextAccount(ctx, cfg, reason = 'rate_limit', setti
140
161
  clearUsageCache()
141
162
  clearProbeCache()
142
163
 
143
- return {
164
+ const result = {
144
165
  rotated: true,
145
166
  previousAccount: active,
146
167
  activeAccount: nextAcc.apiKeyEnv,
147
168
  reason,
148
169
  updatedSettings: true,
149
170
  }
171
+
172
+ setLastRotation({
173
+ at: Date.now(),
174
+ reason,
175
+ from: active,
176
+ to: nextAcc.apiKeyEnv,
177
+ })
178
+
179
+ return result
180
+ }
181
+
182
+ let lastRotation = null
183
+
184
+ export function getLastRotation() {
185
+ return lastRotation
150
186
  }
187
+
188
+ export function setLastRotation(info) {
189
+ lastRotation = info
190
+ }
191
+
192
+ export function isQuotaExceededError(detail) {
193
+ if (!detail || typeof detail !== 'string') return false
194
+ const s = detail.toLowerCase()
195
+ return (
196
+ s.includes('quota') ||
197
+ s.includes('rate limit') ||
198
+ s.includes('exhausted') ||
199
+ s.includes('429') ||
200
+ s.includes('insufficient_quota') ||
201
+ s.includes('credit') ||
202
+ s.includes('too many requests')
203
+ )
204
+ }
205
+