@goodandready/dsh-key-rotation 0.7.30 → 0.7.32

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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 dsh-key-rotation contributors
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GooDAnDReaDY
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,172 +1,254 @@
1
- # dsh-key-rotation
2
-
3
- **Per-provider API key rotation** for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh). Instead of failing on a quota/rate-limit error, the plugin transparently retries the request on the **next healthy key** in a per-provider pool.
4
-
5
- > Hermes-style rotation: every configured provider has a key pool; when a key's limit is exhausted, the request is retried on the next key. Exhausted keys stay in cooldown and return to rotation after `cooldownMs`.
6
-
7
- ## What it does
8
-
9
- - **Key pools per provider** — list the API keys (as credential/env names) that a provider may rotate through.
10
- - **The provider you picked stays the provider** — rotation swaps the key, never the route, so a multi-call turn does not break. Legacy clone routes remain registered but are hidden from the model dropdown.
11
- - **Transparent on-failure rotation** — on a switchable error (`QUOTA`, `RATE_LIMIT`, `AUTH`/`INVALID`…) the request is retried on the next key.
12
- - **Cooldown** — an exhausted key is skipped for `cooldownMs`, then returns.
13
- - **Dead/revoked key handling** — an auth/invalid key rotates to the next pool key instead of erroring out.
14
- - **Settings GUI** — a **Settings → Key Rotation** section to manage everything without touching config files:
15
- - **add a key in one place** — press *Add key*, paste the value, done. The credential name is generated for you (`<PROVIDER>_API_KEY`, then `_2`, `_3`, …) and shown only on hover; the card lists keys as *Key 1*, *Key 2*.
16
- - **live key status** — per key: in use / ready / cooling down with a countdown / **no such credential**, which is what catches a mistyped name that would otherwise fail silently.
17
- - **rotation counter** — how many times a provider switched key, on which failure, and how long ago.
18
- - **key order** — ↑/↓ buttons; the order of keys is the order they are tried.
19
- - **switch codes as checkboxes** instead of a comma-separated string.
20
- - **Exponential backoff** — repeated failures on the same key double its cooldown (base → ×2 → ×4 → cap ×8), so a dead key is not retried every window.
21
- - **Reset cooldown** — a *Reset cooldown* button in the card clears a provider's cooldown immediately (also via `POST /dsh-key-rotation/reset`).
22
- - **Env bootstrap** — if a pool ref (e.g. `MYPROVIDER_API_KEY`) is already set in `process.env`, it is treated as a transient credential without needing a DSH credential first.
23
- - **Per-provider cooldown** — override `cooldownMs` (and `maxCooldownMs`) per provider, fallback to the global values.
24
- - **Exhaustion warning** — when every key is cooling, a red warning appears in the card and `lastExhaustionAt`/`exhaustionCount` are exposed via `GET /dsh-key-rotation/status`.
25
- - **Failure log** — last 20 failures per provider (`at`, `ref`, `reason`, `cooldownMs`) via `/status` and a collapsible *Recent failures* list.
26
- - **Non-stream safety net** — an `agent/request-error` hook retries sync calls (embeddings, batch) with the next key when the error is switchable.
27
- - **Search/filter providers** — a search box above the list filters providers by id.
28
- - **Bulk edit cooldown** — checkboxes per provider + a cooldown input + *Apply to selected*.
29
- - **Undo delete** after removing a key or provider, an *Undo* bar appears for 5 seconds.
30
- - **Per-key last used** — `lastUsedAt` shown as "ago" next to each key.
31
- - **Total requests badge** sum of usage across a provider's keys, shown in its header.
32
- - **Export single provider** button exports just that provider's entry.
33
- - **Import from .env** pick a `.env` file; `KEY=val` names are added to the first pool.
34
- - **Copy key name** click a *Key N* label to copy its ref name.
35
- - **Sort by usage** sorts a provider's keys by usage (desc).
36
- - **Probe history** health-probe events appear greyed in *Recent failures*.
37
- - **RPM token bucket** (added in 0.7.27) — optional `rpmLimit` caps requests per minute **per key**: a key that hit the cap is skipped pre-emptively (before a real request is sent) and returns when its sliding 60s window frees up. `0` (default) disables it.
38
- - **Tier/model-aware routing** (added in 0.7.27) — a pool defined for a model also serves model versions in its family: an exact model pool wins, then the longest name-prefix match (a pool for `gpt-4o` serves `gpt-4o-mini-2024`), then the provider base pool. Provider `tags` are free-form labels surfaced in `/status` for grouping.
39
- - **Interactive webhooks** (added in 0.7.27) — set `webhookActionToken` and exhaustion notifications gain action buttons on Telegram (`inline_keyboard`), Discord (buttons) and Slack (actions block): *Reset cooldown* and *Pause 1h* per pool. Buttons call back to `POST /dsh-key-rotation/webhook-action` with a bearer token; actions `reset-<provider>`, `pause-<provider>` (1 h), `disable-rotation`, `enable-rotation`.
40
- - **Token leak detector** (added in 0.7.27) — recognizes live key shapes (OpenAI `sk-`, Anthropic `sk-ant-`, Google `AIza…`, GitHub `ghp_…`, AWS `AKIA…`, Slack `xox…`, Telegram bot tokens, Stripe, PEM blocks). Saving the config section with a real key pasted into a wrong field is rejected (`400 secret-in-config`); the key-save response carries `looksLikeSecret`, and the card hints when the value does not look like an API key.
41
- - **Header status chip** (added in 0.7.27) a small chip in the session header shows one dot for all pools (green = all keys healthy, amber = some cooling, red = a pool fully exhausted) plus a `healthy/total` counter; polls every 4 s.
42
- - **Key expiry pre-warning** (added in 0.7.28) `expiryWarnDays` (default 7): keys expiring within that horizon send one webhook notification per key per day and show a yellow *expires in Nd* badge in the card.
43
- - **Provider cost budget** (added in 0.7.28) per-provider `costBudgetDaily` / `costBudgetWeekly` (0 = off): a warn webhook fires from 80% spend, and with `pauseOnBudget: true` the whole pool is paused for 24 h at 100%. The card shows a live budget line with warn/red coloring.
44
- - **Usage report + CSV export** (added in 0.7.28) `GET /dsh-key-rotation/usage?days=N[&provider=…][&format=csv]` returns per-key requests/cost over the window; an *Export CSV* button in the card downloads the same data for one provider.
45
- - **RPM capacity indicator** (added in 0.7.28) `/status` carries `rpm: {used, remaining, resetMs}` per key; the card shows a ⏱ counter next to the active key.
46
- - **Real key test probe** (added in 0.7.28) the per-key *Test* button now sends `probe=models` through the existing `/test` route, so it validates the key against the live API (models list + latency), not just credential presence.
47
- - **Per-key weights in the GUI** (added in 0.7.29) a small number input per key edits its round-robin weight (1 = equal share); reorder/remove/add keep the positional `weights` array in sync.
48
- - **Switch notifications** (added in 0.7.29) — opt-in `switchNotify: true` sends a webhook on every key switch (who failed, why, when), deduped to at most one message per provider per `switchNotifyThrottleMs` (default 60 s).
49
- - **Budget action buttons** (added in 0.7.29) when `webhookActionToken` is set, budget notifications carry *Pause 1h* / *Reset cooldown* buttons (same callback route as exhaustion alerts).
50
- - **Config snapshot export/restore** (added in 0.7.29) — *Snapshot ⬇* downloads the whole config as one JSON (token fields exported empty, keys are credential names only); *Restore* imports it back — empty token fields never wipe existing secrets, and a live-looking credential in the file is rejected.
51
- - **Last probe result per key** (added in 0.7.29) — the card polls `/sandbox-cache` and shows the most recent probe outcome (✓/✕ + latency) next to each key, greyed out when older than 24 h.
52
- - **Pre-exhaustion alert** (added in 0.7.30) `warnBelowHealthy` (0 = off): while fewer keys than the threshold are healthy, a webhook fires (once per day per pool) with a *Reset cooldown* button, so the pool never silently dries up.
53
- - **Telegram-native callbacks** (added in 0.7.30) — `/webhook-action` parses Telegram update envelopes (`callback_query.data`), so interactive buttons work natively; `POST {"setWebhook": {"botToken": …}}` registers the bot webhook in one call (the token is not stored).
54
- - **Broken-key review** (added in 0.7.30) keys quarantined for repeated AUTH failures get a *Re-test* button: a live probe (models list) that automatically lifts the 30-day quarantine when the key answers again.
55
- - **7-day switches chart** (added in 0.7.30) — a second bar chart under the 24 h sparkline shows switches per day over the last week (client-side, no new server state).
56
- - **Latency SLO alert** (added in 0.7.30) `latencySloMs` (0 = off): when a key's p95 latency exceeds the threshold, a webhook fires (once per day per key); the card shows `p95 / SLO` per provider.
57
-
58
- ## Install
59
-
60
- ```bash
61
- # From npm after publishing:
62
- dsh plugin --profile web add @goodandready/dsh-key-rotation
63
-
64
- # From GitHub:
65
- dsh plugin --profile web add github:GooDAnDReaDY/dsh-key-rotation
66
-
67
- # Locally from a checkout:
68
- dsh plugin --profile web add /path/to/dsh-key-rotation
69
- ```
70
-
71
- Restart the Web UI afterwards.
72
-
73
- ## Configure
74
-
75
- ### Web GUI (recommended)
76
-
77
- Open **Settings Key Rotation** and, for each provider, list the credential names of its keys. The plugin stores this in the `dsh-key-rotation` settings namespace (same place as `settings.yaml`).
78
-
79
- ### `settings.yaml`
80
-
81
- ```yaml
82
- dsh-key-rotation:
83
- switchCodes: [QUOTA, RATE_LIMIT, SERVER, TIMEOUT, TRANSPORT, EMPTY_RESPONSE, UNKNOWN_MODEL]
84
- cooldownMs: 60000
85
- providers:
86
- # `provider` is the id of a provider registered with dsh, as it appears
87
- # in Settings -> Models. `keys` are CREDENTIAL NAMES, never key values.
88
- - provider: my-provider
89
- keys: [MY_PROVIDER_API_KEY, MY_PROVIDER_API_KEY_2, MY_PROVIDER_API_KEY_3]
90
- - provider: another-provider
91
- keys: [ANOTHER_PROVIDER_API_KEY, ANOTHER_PROVIDER_API_KEY_2]
92
- ```
93
-
94
- | Field | Default | Description |
95
- |---|---|---|
96
- | `switchCodes` | `[QUOTA, RATE_LIMIT, SERVER, TIMEOUT, TRANSPORT, EMPTY_RESPONSE, UNKNOWN_MODEL]` | Error codes that trigger a key switch. |
97
- | `cooldownMs` | `60000` | How long an exhausted key stays out of rotation. |
98
- | `providers` | — | `[{ provider, keys: [envName, ...] }]`. `keys` are credential/env **names**, not the key values themselves. |
99
- | `rpmLimit` | `0` | Requests-per-minute cap **per key** (0 = off). A capped key is skipped pre-emptively until its 60 s window frees up. |
100
- | `webhookActionToken` | `''` | Bearer token for the interactive webhook callback route. When set, exhaustion webhooks carry action buttons; empty disables them. |
101
- | `expiryWarnDays` | `7` | Pre-warning horizon (days) for keys with `expiresAt`: webhook + card badge. |
102
- | `switchNotify` | `false` | Send a webhook on every key switch (opt-in — can be chatty). |
103
- | `switchNotifyThrottleMs` | `60000` | Minimum gap between switch notifications for the same provider. |
104
- | `warnBelowHealthy` | `0` | Webhook "pool running low" while healthy keys < N (0 = off). |
105
- | `latencySloMs` | `0` | Latency SLO per key: webhook when p95 exceeds it (0 = off). |
106
- | `providers[].weights` | `[]` | Positional round-robin weights per key (editable in the card, 0.7.29). |
107
- | `providers[].costBudgetDaily` / `.costBudgetWeekly` | `0` | Daily / weekly spend budget per provider (0 = off). Warn webhook from 80%. |
108
- | `providers[].pauseOnBudget` | `false` | Pause the whole pool for 24 h when a budget is exceeded. |
109
- | `providers[].tags` | `[]` | Free-form labels for a provider pool, surfaced in `GET /status`. |
110
-
111
- ### Interactive webhook actions (added in 0.7.27)
112
-
113
- When `webhookActionToken` is set, the exhaustion webhook payload is formatted per platform (Telegram / Discord / Slack, detected from the URL) with two buttons. A button POSTs to the plugin route:
114
-
115
- ```bash
116
- curl -X POST http://127.0.0.1:3080/dsh-key-rotation/webhook-action \
117
- -H "Authorization: Bearer <webhookActionToken>" \
118
- -H "content-type: application/json" \
119
- -d '{"action": "reset-my-provider"}'
120
- ```
121
-
122
- Actions: `reset-<provider>` (clear cooldowns), `pause-<provider>` (pause the whole pool for 1 hour), `disable-rotation` / `enable-rotation` (global). Platform callback payloads (`callback_data`, Discord `custom_id`, Slack button `value`) are accepted too. Without the correct bearer token the route answers `401`; without a configured token — `503`.
123
-
124
- ### Usage report (added in 0.7.28)
125
-
126
- ```bash
127
- curl "http://127.0.0.1:3080/dsh-key-rotation/usage?days=7" # JSON
128
- curl "http://127.0.0.1:3080/dsh-key-rotation/usage?format=csv&days=30&provider=my-provider" > usage.csv
129
- ```
130
-
131
- Per-key rows: `requests`, `cost`, `active`, and per-day counts over the window (1–90 days, default 7).
132
-
133
- ### How keys are stored
134
-
135
- The plugin config only ever references keys by **name** (e.g. `MY_PROVIDER_API_KEY`). The values live in the dsh **Credentials** service or `$DSH_HOME/.credentials.yaml` — never in the plugin config.
136
-
137
- A key typed into the Key Rotation card is written to that same credentials store: the value travels to the host once and is never sent back to the browser. Only its **last 5 characters** are, so two keys can be told apart in the UI. A key supplied by the launching environment is shown as read-only, because overwriting it here would be shadowed anyway.
138
-
139
- ## How it works
140
-
141
- ```
142
- request ──► {provider: rotation} clone route ──► pick next healthy key in pool
143
- ┌────────┐ on switchable failure retry with next key, stay in cooldown
144
- └─────────┘
145
- ```
146
-
147
- - The plugin patches `ctx.credentials.resolve` so a pool reference resolves to the current healthy key (round-robin, skipping keys in cooldown).
148
- - It intercepts `llm/stream` to retry the request on the next key after a switchable failure, instead of surfacing the error to the caller. The hook is deliberately **not** `async`: the loop iterates its result directly, and returning a promise breaks every turn.
149
- - The provider identity never changes only the resolved key does — which keeps the adapter's replay state consistent across a multi-call turn.
150
-
151
- Two local-only routes back the card: `GET /dsh-key-rotation/status` (key state, rotation counters, last 5 characters of each key) and `PUT|DELETE /dsh-key-rotation/key` (store or drop one key value). Both refuse anything that is not a same-origin request from loopback.
152
-
153
- ## Structure
154
-
155
- ```
156
- dsh-key-rotation/
157
- ├── package.json # dsh bundle/plugin metadata + peerDependencies
158
- ├── cordis.patch.yml # bundle layer: registers the virtual route "rotation"
159
- ├── lib/index.js # host: pools, credentials.resolve patch, stream retry
160
- ├── lib/client.js # browser: Settings Key Rotation panel
161
- └── README.md
162
- ```
163
-
164
- ## Security notes
165
-
166
- - Key **values** never leave your Credentials store; the plugin config only holds env/credential **names**.
167
- - `switchCodes` are error classification strings, not expressions — no secrets involved.
168
- - The leak detector (0.7.27) rejects config saves that carry a live-looking key outside the credential fields; only the credential **type** and a short preview are ever reported, never the value.
169
-
170
- ## License
171
-
172
- MIT
1
+ # 📦 @goodandready/dsh-key-rotation
2
+
3
+ <div align="center">
4
+
5
+ <h3>Enterprise-Grade Transparent API Key Rotation, Rate-Limit Pre-emption & Failover Cascade for DeepSeek Harness</h3>
6
+
7
+ <p align="center">
8
+ <a href="https://www.npmjs.com/package/@goodandready/dsh-key-rotation"><img src="https://img.shields.io/npm/v/@goodandready/dsh-key-rotation.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
9
+ <a href="LICENSE"><img src="https://img.shields.io/github/license/GooDAnDReaDY/dsh-key-rotation.svg?style=for-the-badge&color=10b981&labelColor=064e3b" alt="license"></a>
10
+ <a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-8b5cf6.svg?style=for-the-badge&labelColor=2e1065" alt="DSH Plugin"></a>
11
+ <a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-20%2B-f59e0b.svg?style=for-the-badge&labelColor=451a03" alt="Node version"></a>
12
+ </p>
13
+
14
+ <!-- Showcase Link -->
15
+ <p align="center">
16
+ <a href="https://goodandready.app/"><img src="https://img.shields.io/badge/🌐_DSH_Hub-goodandready.app-ff4500.svg?style=for-the-badge&labelColor=1a1a2e" alt="GoodAndReady Showcase"></a>
17
+ </p>
18
+
19
+ <p align="center">
20
+ <a href="README.md"><b>🇬🇧 English</b></a>
21
+ <a href="README.ru.md"><b>🇷🇺 Русский</b></a>
22
+ <a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
23
+ </p>
24
+
25
+ </div>
26
+
27
+ ---
28
+
29
+ ## Overview & The Problem
30
+
31
+ ### 🛠️ What's New in v0.7.32 (Stability & Bugfix Release)
32
+ - **🔍 Resolved Key Probing BaseURL**: Fixed `resolveBaseUrl` to map key credential refs to owning provider pools, restoring live `probeModels` testing.
33
+ - **🛡️ Guarded Cascade Recursion**: Prevented call stack overflow in cross-provider failover when circular cascade chains occur.
34
+ - **🕒 Accurate Midnight PST Resets**: Corrected UTC-8 timezone calculation offset sign for calendar quota reset windows.
35
+ - **🧹 Lifecycle Timer Cleanup**: Wrapped `canaryTimer` and `selfHealTimer` in Cordis effect scopes, eliminating background orphaned intervals on hot reload.
36
+ - **⚡ Stale Lock Recovery in Load Balancer**: Added expired lock detection to `pickLeastLoaded` for uninterrupted least-connections routing.
37
+ - **🌐 Full Chinese Localization**: Added complete `zh` locale dictionary to the React settings dashboard for comprehensive 3-language parity.
38
+
39
+
40
+ ### 🚀 What's New in v0.7.31
41
+ - **⚡ O(1) TokenBucket Accumulator**: Upgraded rate limiting math to O(1) time and zero-allocation memory with adaptive header synchronization.
42
+ - **🛡️ Soft vs Hard Backoff**: Differentiates transient infrastructure drops (502/503/timeouts: 10s flat cooldown) from hard quota errors (progressive doubling).
43
+ - **⏳ Penalty Decay**: Stable keys that operate cleanly automatically decay their failure penalty multiplier every hour.
44
+ - **🎲 Cooldown Jitter**: Adds ±12.5% random dispersion to recovery timers, eliminating thundering herd stampedes.
45
+ - **🎯 Addressable Canary Probing**: Support for probing target pool models with lightweight single-token verification pings.
46
+ - **📊 TTFT Percentiles (p50 / p95 / p99)**: Sub-second high-resolution latency percentile tracking across all key pools.
47
+ - **🔔 Webhook Alert Digest**: Aggregates multiple rapid switch/cooldown events into consolidated incident digests for Telegram, Discord, and Slack.
48
+ - **🧹 30-Day Usage Compaction**: Automatic bounded memory management with 30-day rolling window data pruning.
49
+ - **✨ Optimistic UI & Filter Pills**: Instant zero-latency UI updates on reset, plus `All`, `Ready`, `In Cooldown`, and `With Errors` quick filter chips.
50
+
51
+
52
+ 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.
53
+
54
+ **`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.
55
+
56
+ Unlike naive routing proxies that alter provider identifiers, `dsh-key-rotation` hooks into `ctx.credentials.resolve` and intercepts `llm/stream` at runtime:
57
+ * **The provider identity never changes**: Agent replay states, multi-call turns, and tool schemas remain 100% consistent.
58
+ * **Pre-emptive Token Bucket**: Throttled keys are skipped *before* issuing network calls, eliminating retry latency.
59
+ * **Least-Connections Concurrency Control**: Balances in-flight streams across keys to prevent burst saturation.
60
+ * **Autonomous Self-Healing & Cascades**: Proactively tests quarantined keys via canary probes and smoothly escalates to fallback providers if an entire pool is exhausted.
61
+
62
+ ---
63
+
64
+ ## 🏗️ Architecture & Request Lifecycle
65
+
66
+ ```mermaid
67
+ graph LR
68
+ subgraph ClientLayer ["Client & Agent Turn"]
69
+ UserMsg["User / Subagent Message"] --> Adapter["pi-ai Model Adapter"]
70
+ end
71
+
72
+ subgraph RotationEngine ["dsh-key-rotation Core Engine"]
73
+ Adapter --> StreamHook["llm/stream Interceptor"]
74
+ StreamHook --> BucketCheck{"Token Bucket\nRPM / TPM Check"}
75
+ BucketCheck -->|Under Limit| ConcurrencyCheck{"Concurrency Tracker\nLeast-Connections"}
76
+ BucketCheck -->|Exceeded| NextKey1["Pick Next Healthy Key"]
77
+ ConcurrencyCheck -->|Slot Available| KeyResolver["ctx.credentials.resolve"]
78
+ ConcurrencyCheck -->|Saturated| NextKey1
79
+
80
+ KeyResolver --> ActiveKey["Active Key (In Use)"]
81
+
82
+ ActiveKey -.->|HTTP 429 / Quota / Error| Failover["Instant Failover Handler"]
83
+ Failover --> BackoffCalc["Exponential Backoff & Quarantine"]
84
+ Failover --> NextKey2["Retry Next Key (Zero Token Loss)"]
85
+ Failover -.->|All Pool Keys Exhausted| CascadeEngine["Cross-Provider Cascade"]
86
+
87
+ BackoffCalc --> QuotaWindow["Calendar Reset / Midnight Window"]
88
+ BackoffCalc --> CanaryProbe["Active Canary Prober (Sandbox Ping)"]
89
+ CanaryProbe -->|Verified Healthy| PoolReady["Restored to Ready Pool"]
90
+ end
91
+
92
+ subgraph UpstreamLayer ["Model Provider Endpoints"]
93
+ ActiveKey --> UpstreamAPI["Primary Provider API"]
94
+ CascadeEngine --> FallbackAPI["Backup Provider API"]
95
+ end
96
+
97
+ style ClientLayer fill:#1e1e2e,stroke:#89b4fa,stroke-width:2px,color:#cdd6f4
98
+ style RotationEngine fill:#181825,stroke:#cba6f7,stroke-width:2px,color:#cdd6f4
99
+ style UpstreamLayer fill:#11111b,stroke:#a6e3a1,stroke-width:2px,color:#cdd6f4
100
+ ```
101
+
102
+ ---
103
+
104
+ ## Full Feature Breakdown
105
+
106
+ ### 🔄 1. Transparent Rotation & Failover
107
+ * **Unchanged Provider Identity**: Rotates only the underlying resolved API credential ref, never the provider ID. Prevents `INVALID_REPLAY_STATE` crashes in `pi-ai` multi-turn sessions.
108
+ * **Zero-Token-Loss Stream Retries**: If an API key encounters an error before the first content chunk is emitted, the request is transparently re-dispatched to the next healthy key in the pool.
109
+ * **Comprehensive Switch Codes**: Automatically fails over on `QUOTA`, `RATE_LIMIT`, `SERVER`, `TIMEOUT`, `TRANSPORT`, `EMPTY_RESPONSE`, `UNKNOWN_MODEL`, `AUTH`, and `INVALID` error codes.
110
+ * **Intelligent Message Pattern Matching**: Fallback regex classifier (`SWITCHABLE_MESSAGE_PATTERN`) identifies text-based quota/rate-limit errors thrown as generic exceptions by upstream SDKs.
111
+ * **Non-Streaming Safety Net**: Synchronous calls (e.g., embeddings, batch evaluations) are protected via the `agent/request-error` lifecycle hook.
112
+
113
+ ### ⏱️ 2. Rate-Limit Pre-emption & Concurrency Control
114
+ * **Token Bucket / Leaky Bucket (`lib/bucket.js`)**: Sliding-window tracking of Requests Per Minute (`rpmLimit`) and Tokens Per Minute (`tpmLimit`). Quarantines saturated keys *before* dispatching network requests, preventing 429 roundtrips.
115
+ * **Least-Connections Balancer (`lib/concurrency.js`)**: Tracks active in-flight streams per key (`inFlight`). Distributes concurrent requests evenly across available credentials and enforces `maxConcurrency` limits.
116
+ * **Stale Lock Auto-Release**: Deadlocks from disconnected clients or aborted network sockets are automatically purged after 5 minutes.
117
+
118
+ ### 🛡️ 3. Autonomous Healing & Cascade Escalation
119
+ * **Cross-Provider Failover Cascade (`lib/cascade.js`)**: If all keys for a selected provider are in cooldown, requests automatically cascade to an alternative fallback provider pool (e.g., primary provider → fallback proxy / secondary provider).
120
+ * **Active Canary Prober (`lib/canary.js`)**: Before releasing a key from quarantine, a lightweight background probe (`/models` probe or single-token check via `SandboxRunner`) validates upstream availability without exposing real user traffic to risk.
121
+ * **Calendar & Rolling Quota Reset Windows (`lib/quota-window.js`)**: Supports scheduled quota reset alignments (`midnight_utc`, `midnight_pst`, and `rolling_24h`) so daily free/tier quotas unfreeze exactly when upstream resets them.
122
+ * **Adaptive Exponential Backoff (`lib/pool.js`)**: Successive failures on a key double its quarantine duration (base ×2 ×4 cap ×8). Successful requests gradually restore healthy status.
123
+
124
+ ### 🎯 4. Model-Aware & Geolocation Routing
125
+ * **Model Sub-Pools (`lib/pool.js`)**: Configure dedicated key pools for specific model tiers (e.g. reasoning/heavy models vs fast/cheap utility models).
126
+ * **Tag-Based Routing**: Assign operational tags (`production`, `background`, `eval`) to match key usage with workload priorities.
127
+ * **Region Mapping (`lib/region.js`)**: Route queries through geographically optimal credentials and endpoints.
128
+
129
+ ### 📊 5. Observability, Telemetry & Webhooks
130
+ * **Interactive Multi-Platform Webhooks (`lib/webhook.js`)**: Dispatches rich notifications with HMAC-signed action buttons for **Telegram** (Inline Keyboards), **Discord** (Action Rows), and **Slack** (Block Kit). Administrators can click buttons to reset cooldowns or pause providers directly from their mobile chat.
131
+ * **Usage & Cost Reporting (`lib/usage-report.js`)**: Per-key daily request counters and estimated cost breakdown with one-click CSV/JSON export (`GET /dsh-key-rotation/usage-report`).
132
+ * **Latency SLO & Histogram (`lib/histogram.js`)**: Tracks Time-To-First-Token (TTFT) and stream durations with health score degradation scoring (`0..100`).
133
+ * **Automated Incident Reporting (`lib/incident.js`)**: Lazily creates structured GitHub Issues on sustained upstream outages.
134
+ * **Shadow Traffic Routing (`lib/shadow.js`)**: Fork a configurable percentage of live requests to evaluate secondary providers in shadow mode.
135
+
136
+ ---
137
+
138
+ ## 🖥️ Rich Web GUI & Dashboard
139
+
140
+ Access full visual management under **Settings → Key Rotation** or via the Header quick-widget.
141
+
142
+ | Interface Feature | Description |
143
+ |---|---|
144
+ | **Header Status Widget** | Compact live badge in DSH header: 🟢 `All Healthy` \| 🟡 `Cooldown Active` \| 🔴 `Pool Exhausted` with quick popover actions. |
145
+ | **1-Click Health Matrix** | "Health Matrix" dashboard running parallel sandbox probes across all providers, keys, and models with TTFT latency and status badges. |
146
+ | **Instant Key Provisioning** | Add keys with auto-generated names (`<PROVIDER>_API_KEY`, `_2`, `_3`) and automatic key-tail disambiguation. |
147
+ | **Live Status Badges** | Visual states: `In Use`, `Ready`, `Cooling Down` (with live countdown timer), and `Not Found`. |
148
+ | **Drag & Priority Ordering** | Reorder keys with <kbd>↑</kbd> and <kbd>↓</kbd> buttons to fine-tune selection precedence. |
149
+ | **Switch Code Toggles** | Interactive checkboxes for switchable error conditions. |
150
+ | **Secret Leak Detector** | Real-time input sanitizer (`lib/keycheck.js`) catching accidental pastes of private keys, SSH keys, or misplaced tokens. |
151
+ | **Batch `.env` Import** | Parse standard `.env` key-value pairs directly into corresponding provider pools. |
152
+ | **5-Second Undo Bar** | Non-destructive undo bar for accidental key or pool removals. |
153
+ | **Usage Analytics Chart** | Interactive breakdown of lifetime requests and daily trends per key. |
154
+
155
+ ---
156
+
157
+ ## 🔒 Security & Safe Storage
158
+
159
+ * **Zero Plaintext Secrets in Plugin Config**: Configuration files store only environment variable reference names (e.g. `MY_PROVIDER_API_KEY`).
160
+ * **Secure Vault Storage**: Actual secret values reside securely in `$DSH_HOME/.credentials.yaml` managed by the DSH `Credentials` service.
161
+ * **5-Character Masking (`keyTail`)**: Full secret values are never sent to the client browser; only the trailing 5 characters are exposed for visual identification.
162
+ * **Loopback & Same-Origin Fencing**: Administrative endpoints (`GET /status`, `PUT /key`, `POST /reset`, `POST /test-matrix`) strictly enforce loopback origin checks (`isTrustedBridgeRequest`).
163
+
164
+ ---
165
+
166
+ ## 📦 Installation
167
+
168
+ ```bash
169
+ # Install via DSH Plugin Manager (Web Profile):
170
+ dsh plugin --profile web add @goodandready/dsh-key-rotation
171
+
172
+ # Or directly from GitHub:
173
+ dsh plugin --profile web add github:GooDAnDReaDY/dsh-key-rotation
174
+ ```
175
+
176
+ > [!IMPORTANT]
177
+ > Restart DeepSeek Harness web service after installation and refresh your browser tab:
178
+ > ```bash
179
+ > systemctl --user restart dsh-web
180
+ > ```
181
+
182
+ ---
183
+
184
+ ## ⚙️ Configuration Reference (`settings.yaml`)
185
+
186
+ ```yaml
187
+ dsh-key-rotation:
188
+ switchCodes:
189
+ - QUOTA
190
+ - RATE_LIMIT
191
+ - SERVER
192
+ - TIMEOUT
193
+ - TRANSPORT
194
+ - EMPTY_RESPONSE
195
+ - UNKNOWN_MODEL
196
+ - AUTH
197
+ cooldownMs: 60000
198
+ canaryProbing: true
199
+ concurrencyLimit: 5
200
+ quotaResetWindow:
201
+ type: midnight_utc
202
+ hour: 0
203
+ cascade:
204
+ - provider: backup-provider-id
205
+ model: your-backup-model-id
206
+ webhookUrl: "https://api.telegram.org/bot<TOKEN>/sendMessage?chat_id=<CHAT_ID>"
207
+ providers:
208
+ - provider: your-primary-provider
209
+ rpmLimit: 60
210
+ tpmLimit: 100000
211
+ keys:
212
+ - PRIMARY_API_KEY
213
+ - PRIMARY_API_KEY_2
214
+ - PRIMARY_API_KEY_BACKUP
215
+ - provider: secondary-provider
216
+ keys:
217
+ - SECONDARY_API_KEY
218
+ - SECONDARY_API_KEY_2
219
+ ```
220
+
221
+ ### Parameter Reference
222
+
223
+ | Parameter | Type | Default | Description |
224
+ |---|---|---|---|
225
+ | `switchCodes` | `string[]` | `[QUOTA, RATE_LIMIT, ...]` | List of error codes that immediately trigger failover. |
226
+ | `cooldownMs` | `number` | `60000` (1 min) | Base penalty duration (in ms) for quarantined keys. |
227
+ | `canaryProbing` | `boolean` | `true` | Runs background ping probe before restoring quarantined keys. |
228
+ | `concurrencyLimit` | `number` | `0` (disabled) | Max concurrent in-flight streams per key (0 = unlimited). |
229
+ | `quotaResetWindow` | `object` | `null` | Calendar reset alignment (`midnight_utc`, `midnight_pst`, `rolling_24h`). |
230
+ | `cascade` | `array` | `[]` | Fallback provider chain when primary pool is completely exhausted. |
231
+ | `webhookUrl` | `string` | `""` | Target URL for interactive Telegram, Discord, Slack, or generic alerts. |
232
+ | `providers` | `array` | `[]` | List of `{ provider, keys, rpmLimit, tpmLimit, modelPools }` definitions. |
233
+
234
+ ---
235
+
236
+ ## 🔌 HTTP Bridge API Reference
237
+
238
+ All management routes require loopback authentication (`127.0.0.1` / `::1`) with same-origin validation:
239
+
240
+ | Route | Method | Description |
241
+ |---|---|---|
242
+ | `/dsh-key-rotation/status` | `GET` | Returns real-time health snapshots, active keys, and cooldown states. |
243
+ | `/dsh-key-rotation/config` | `GET` / `PUT` | Read and update active key rotation settings and provider pools. |
244
+ | `/dsh-key-rotation/key` | `PUT` / `DELETE` | Add, update, or remove credentials in host storage and pool. |
245
+ | `/dsh-key-rotation/reset` | `POST` | Instantly resets all cooldowns and restores all keys to `ready`. |
246
+ | `/dsh-key-rotation/test-matrix` | `POST` | Triggers parallel health check across all configured keys and models. |
247
+ | `/dsh-key-rotation/usage-report` | `GET` | Returns aggregated usage metrics in JSON or CSV format (`?format=csv`). |
248
+ | `/dsh-key-rotation/webhook-callback`| `POST` | Receives and executes interactive actions from Telegram/Slack callbacks. |
249
+
250
+ ---
251
+
252
+ ## 📄 License
253
+
254
+ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)