@goodandready/dsh-key-rotation 0.8.6 → 0.8.8
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/lib/client.js +26 -2
- package/lib/index.js +1 -0
- package/lib/routes-ops.js +36 -11
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -27,6 +27,10 @@ window.__ModuleLoader__.load({
|
|
|
27
27
|
// -------------------------------------------------------------- i18n
|
|
28
28
|
const en = {
|
|
29
29
|
title: 'Key Rotation',
|
|
30
|
+
'header.title': 'Key Rotation & Failover',
|
|
31
|
+
'header.sub': 'Automated multi-key round-robin rotation, 429/5xx exponential backoff cooldowns, circuit breaker failover protection, and credential security (~/.dsh/.credentials.yaml).',
|
|
32
|
+
'header.pools_badge': 'pools active',
|
|
33
|
+
'header.keys_badge': 'keys configured',
|
|
30
34
|
sectionStats: 'Pool Health & Telemetry',
|
|
31
35
|
sectionStatsDesc: 'Real-time telemetry of active key pools, healthy credentials, and failover status.',
|
|
32
36
|
sectionFailover: 'Failover Triggers & Error Codes',
|
|
@@ -137,7 +141,7 @@ window.__ModuleLoader__.load({
|
|
|
137
141
|
totalRequestsTitle: 'total requests',
|
|
138
142
|
exportProvider: 'Export this provider',
|
|
139
143
|
recentFailures: 'Recent failures ({n})',
|
|
140
|
-
noActivePools:
|
|
144
|
+
noActivePools: 'No active pools',
|
|
141
145
|
headerPoolsTitle: 'Key rotation pools',
|
|
142
146
|
pausedBudget: 'paused',
|
|
143
147
|
sortUsage: 'Sort by usage',
|
|
@@ -244,7 +248,10 @@ window.__ModuleLoader__.load({
|
|
|
244
248
|
// на поле значения, поэтому имя занимает свою строку, а служебная строка
|
|
245
249
|
// под ним ужимается сама.
|
|
246
250
|
const CARD_CSS = [
|
|
247
|
-
'.krot{display:flex;flex-direction:column;gap:
|
|
251
|
+
'.krot{display:flex;flex-direction:column;gap:20px;max-width:960px;padding:6px 0 24px;box-sizing:border-box}',
|
|
252
|
+
'.krot-header{display:flex;flex-direction:column;gap:8px;padding-bottom:16px;border-bottom:1px solid var(--dsw-alias-border-l2)}',
|
|
253
|
+
'.krot-page-title{font-size:20px;font-weight:700;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:10px;flex-wrap:wrap}',
|
|
254
|
+
'.krot-page-sub{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.5}',
|
|
248
255
|
'.krot p{margin:0}',
|
|
249
256
|
'.krot-hint{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.4}',
|
|
250
257
|
'.krot-err{font-size:13px;color:var(--dsw-alias-state-error-primary);padding:10px 14px;border-radius:8px;background:rgba(239,68,68,0.1);display:flex;align-items:center;gap:8px}',
|
|
@@ -1359,8 +1366,25 @@ window.__ModuleLoader__.load({
|
|
|
1359
1366
|
)
|
|
1360
1367
|
) : null;
|
|
1361
1368
|
|
|
1369
|
+
const headerSection = h('div', { className: 'krot-header' },
|
|
1370
|
+
h('div', { className: 'krot-page-title' },
|
|
1371
|
+
'🔄 ' + t('header.title'),
|
|
1372
|
+
h('span', { className: 'krot-badge ' + (totalPoolsCount > 0 ? 'krot-badge-ok' : 'krot-badge-warn') },
|
|
1373
|
+
totalPoolsCount > 0 ? (totalPoolsCount + ' ' + t('header.pools_badge')) : t('noActivePools')
|
|
1374
|
+
),
|
|
1375
|
+
h('span', { className: 'krot-badge ' + (totalKeysCount > 0 ? 'krot-badge-ok' : 'krot-badge-warn') },
|
|
1376
|
+
totalKeysCount + ' ' + t('header.keys_badge')
|
|
1377
|
+
),
|
|
1378
|
+
h('span', { className: 'krot-badge ' + (healthyKeysCount === totalKeysCount && totalKeysCount > 0 ? 'krot-badge-ok' : 'krot-badge-warn') },
|
|
1379
|
+
healthyKeysCount + '/' + totalKeysCount + ' ' + t('keyReady')
|
|
1380
|
+
)
|
|
1381
|
+
),
|
|
1382
|
+
h('div', { className: 'krot-page-sub' }, t('header.sub'))
|
|
1383
|
+
);
|
|
1384
|
+
|
|
1362
1385
|
return h('div', { className: 'krot' },
|
|
1363
1386
|
confirmModalEl,
|
|
1387
|
+
headerSection,
|
|
1364
1388
|
statsSection,
|
|
1365
1389
|
poolsSection,
|
|
1366
1390
|
failoverSection,
|
package/lib/index.js
CHANGED
|
@@ -827,6 +827,7 @@ export function apply(ctx, config = {}) {
|
|
|
827
827
|
getRotationDisabled: () => rotationDisabled,
|
|
828
828
|
setRotationDisabled: (v) => { rotationDisabled = v; },
|
|
829
829
|
circuitBreaker: moduleBreaker,
|
|
830
|
+
quotaStore,
|
|
830
831
|
});
|
|
831
832
|
|
|
832
833
|
ctx.effect(() => ctx.on('llm/stream', (options, next) => {
|
package/lib/routes-ops.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
NS,
|
|
9
9
|
} from './http-bridge.js';
|
|
10
10
|
import {
|
|
11
|
+
isLoopbackAddress,
|
|
11
12
|
isTrustedBridgeRequest,
|
|
12
13
|
keyTail,
|
|
13
14
|
envValue,
|
|
@@ -48,6 +49,7 @@ export function registerOpsRoutes(ctx, deps) {
|
|
|
48
49
|
getRotationDisabled,
|
|
49
50
|
setRotationDisabled,
|
|
50
51
|
circuitBreaker,
|
|
52
|
+
quotaStore,
|
|
51
53
|
} = deps;
|
|
52
54
|
|
|
53
55
|
// ── status route: what the settings card cannot know on its own ──
|
|
@@ -149,7 +151,7 @@ export function registerOpsRoutes(ctx, deps) {
|
|
|
149
151
|
totalUsage: (() => { let s = 0; if (pool.state.usageCounts) for (const v of pool.state.usageCounts.values()) s += v; return s; })(),
|
|
150
152
|
// #225: aggregate p95 across the pool's keys
|
|
151
153
|
p95: (() => {
|
|
152
|
-
const vals = (pool.refs ?? []).map((r) => latencyHistogram.snapshot(r)).filter((s) => s && s.p95 != null).map((s) => s.p95);
|
|
154
|
+
const vals = (pool.refs ?? []).map((r) => (typeof latencyHistogram?.snapshot === 'function' ? latencyHistogram.snapshot(r) : null)).filter((s) => s && s.p95 != null).map((s) => s.p95);
|
|
153
155
|
return vals.length ? Math.round(Math.max(...vals)) : null;
|
|
154
156
|
})(),
|
|
155
157
|
latencySloMs,
|
|
@@ -158,9 +160,9 @@ export function registerOpsRoutes(ctx, deps) {
|
|
|
158
160
|
// #208: today/week spend + configured budget for the card
|
|
159
161
|
todayCost: costForDay(pool.state.costDays),
|
|
160
162
|
weeklyCost: costForWeek(pool.state.costDays, now),
|
|
161
|
-
budgetDaily: providerBudgets.get(pool.base)?.costBudgetDaily ?? 0,
|
|
162
|
-
budgetWeekly: providerBudgets.get(pool.base)?.costBudgetWeekly ?? 0,
|
|
163
|
-
pauseOnBudget: providerBudgets.get(pool.base)?.pauseOnBudget ?? false,
|
|
163
|
+
budgetDaily: (providerBudgets?.get ? providerBudgets.get(pool.base) : providerBudgets?.[pool.base])?.costBudgetDaily ?? 0,
|
|
164
|
+
budgetWeekly: (providerBudgets?.get ? providerBudgets.get(pool.base) : providerBudgets?.[pool.base])?.costBudgetWeekly ?? 0,
|
|
165
|
+
pauseOnBudget: (providerBudgets?.get ? providerBudgets.get(pool.base) : providerBudgets?.[pool.base])?.pauseOnBudget ?? false,
|
|
164
166
|
});
|
|
165
167
|
} catch (e) {
|
|
166
168
|
console.warn(`[dsh-key-rotation] status: pool ${pool.base} failed: ${String(e?.message ?? e)} ${e?.stack ?? ''}`);
|
|
@@ -305,6 +307,14 @@ export function registerOpsRoutes(ctx, deps) {
|
|
|
305
307
|
try {
|
|
306
308
|
if (req.method === 'DELETE') {
|
|
307
309
|
await credentialsService.unset(ref);
|
|
310
|
+
for (const st of poolState.values()) {
|
|
311
|
+
st.failedUntil?.delete(ref);
|
|
312
|
+
st.failCounts?.delete(ref);
|
|
313
|
+
st.authFailCounts?.delete(ref);
|
|
314
|
+
st.brokenUntil?.delete(ref);
|
|
315
|
+
if (st.lastUsed === ref) st.lastUsed = undefined;
|
|
316
|
+
}
|
|
317
|
+
try { lastTestCache?.delete?.(ref); } catch (_) {}
|
|
308
318
|
json(res, 200, { ok: true, ref });
|
|
309
319
|
return;
|
|
310
320
|
}
|
|
@@ -366,8 +376,8 @@ export function registerOpsRoutes(ctx, deps) {
|
|
|
366
376
|
if (ref) {
|
|
367
377
|
let found = false;
|
|
368
378
|
for (const st of poolState.values()) {
|
|
369
|
-
if (st.failedUntil
|
|
370
|
-
st.failedUntil
|
|
379
|
+
if (st.failedUntil?.has(ref) || st.failCounts?.has(ref) || st.authFailCounts?.has(ref) || st.brokenUntil?.has(ref)) {
|
|
380
|
+
st.failedUntil?.delete(ref);
|
|
371
381
|
st.failCounts?.delete(ref);
|
|
372
382
|
st.authFailCounts?.delete(ref);
|
|
373
383
|
st.brokenUntil?.delete(ref);
|
|
@@ -445,7 +455,7 @@ export function registerOpsRoutes(ctx, deps) {
|
|
|
445
455
|
if (exhausted) exhaustedAny = true;
|
|
446
456
|
pools[pool.base] = { healthy, total, exhausted, healthScore: computeHealthScore(pool.state) };
|
|
447
457
|
}
|
|
448
|
-
json(res, 200, { status: exhaustedAny ? 'degraded' : 'ok', pools, exhaustedAny, latency: latencyHistogram.snapshotAll(), quota: quotaStore.snapshot() });
|
|
458
|
+
json(res, 200, { status: exhaustedAny ? 'degraded' : 'ok', pools, exhaustedAny, latency: latencyHistogram.snapshotAll(), quota: typeof quotaStore?.snapshot === 'function' ? quotaStore.snapshot() : null });
|
|
449
459
|
},
|
|
450
460
|
}), 'dsh-key-rotation: health');
|
|
451
461
|
|
|
@@ -513,7 +523,10 @@ export function registerOpsRoutes(ctx, deps) {
|
|
|
513
523
|
path: SANDBOX_CACHE_PATH,
|
|
514
524
|
handler: (req, res) => {
|
|
515
525
|
if (!isTrustedBridgeRequest(req)) { json(res, 403, { error: { code: 'forbidden', message: 'dsh-key-rotation: cache is local-only' } }); return; }
|
|
516
|
-
|
|
526
|
+
const cacheSnap = typeof lastTestCache?.snapshot === 'function'
|
|
527
|
+
? lastTestCache.snapshot()
|
|
528
|
+
: (lastTestCache instanceof Map ? Object.fromEntries(lastTestCache) : (lastTestCache ?? {}));
|
|
529
|
+
json(res, 200, cacheSnap);
|
|
517
530
|
},
|
|
518
531
|
}), 'dsh-key-rotation: sandbox cache');
|
|
519
532
|
|
|
@@ -594,9 +607,21 @@ export function registerOpsRoutes(ctx, deps) {
|
|
|
594
607
|
return;
|
|
595
608
|
}
|
|
596
609
|
const cleared = st.failedUntil.size;
|
|
597
|
-
st.failedUntil.clear();
|
|
598
|
-
|
|
599
|
-
|
|
610
|
+
st.failedUntil.clear();
|
|
611
|
+
st.failCounts?.clear();
|
|
612
|
+
st.authFailCounts?.clear();
|
|
613
|
+
st.brokenUntil?.clear();
|
|
614
|
+
st.switches = 0;
|
|
615
|
+
st.lastReason = undefined;
|
|
616
|
+
st.lastSwitchAt = undefined;
|
|
617
|
+
let circuitReset = false;
|
|
618
|
+
const br = circuitBreaker ?? buildRuntime().breaker;
|
|
619
|
+
if (br) {
|
|
620
|
+
if (typeof br.reset === 'function') { br.reset(provider); circuitReset = true; }
|
|
621
|
+
else if (typeof br.onSuccess === 'function') { br.onSuccess(provider); circuitReset = true; }
|
|
622
|
+
}
|
|
623
|
+
console.warn(`[dsh-key-rotation] pool ${provider} RESET via webhook action (circuitReset=${circuitReset})`);
|
|
624
|
+
json(res, 200, { ok: true, action, provider, cleared, circuitReset });
|
|
600
625
|
return;
|
|
601
626
|
}
|
|
602
627
|
json(res, 400, { error: { code: 'unknown-action', message: `dsh-key-rotation: unknown action '${action}'` } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-key-rotation",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.8",
|
|
4
4
|
"packageManager": "pnpm@10.33.2",
|
|
5
5
|
"description": "Per-provider API key rotation for DeepSeek Harness: a key pool per provider, auto-created clone routes, and switching to the next key on quota/rate-limit errors. Includes a Settings section (Key Rotation) to edit the key pools, cooldown and switch codes.",
|
|
6
6
|
"keywords": [
|