@goodandready/dsh-key-rotation 0.8.7 → 0.8.10
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 +22 -3
- package/README.ru.md +22 -3
- package/README.zh.md +22 -4
- package/lib/client.js +244 -5
- package/lib/concurrency.js +9 -0
- package/lib/heal.js +47 -0
- package/lib/index.js +47 -8
- package/lib/pool.js +104 -21
- package/lib/rotate.js +127 -107
- package/lib/routes-ops.js +38 -11
- package/lib/sandbox.js +9 -1
- package/package.json +1 -1
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,11 @@ 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,
|
|
166
|
+
routingStrategy: pool.routingStrategy ?? runtime.routingStrategy ?? 'round-robin',
|
|
167
|
+
proactiveRateLimitGuard: pool.proactiveRateLimitGuard ?? runtime.proactiveRateLimitGuard ?? true,
|
|
164
168
|
});
|
|
165
169
|
} catch (e) {
|
|
166
170
|
console.warn(`[dsh-key-rotation] status: pool ${pool.base} failed: ${String(e?.message ?? e)} ${e?.stack ?? ''}`);
|
|
@@ -305,6 +309,14 @@ export function registerOpsRoutes(ctx, deps) {
|
|
|
305
309
|
try {
|
|
306
310
|
if (req.method === 'DELETE') {
|
|
307
311
|
await credentialsService.unset(ref);
|
|
312
|
+
for (const st of poolState.values()) {
|
|
313
|
+
st.failedUntil?.delete(ref);
|
|
314
|
+
st.failCounts?.delete(ref);
|
|
315
|
+
st.authFailCounts?.delete(ref);
|
|
316
|
+
st.brokenUntil?.delete(ref);
|
|
317
|
+
if (st.lastUsed === ref) st.lastUsed = undefined;
|
|
318
|
+
}
|
|
319
|
+
try { lastTestCache?.delete?.(ref); } catch (_) {}
|
|
308
320
|
json(res, 200, { ok: true, ref });
|
|
309
321
|
return;
|
|
310
322
|
}
|
|
@@ -366,8 +378,8 @@ export function registerOpsRoutes(ctx, deps) {
|
|
|
366
378
|
if (ref) {
|
|
367
379
|
let found = false;
|
|
368
380
|
for (const st of poolState.values()) {
|
|
369
|
-
if (st.failedUntil
|
|
370
|
-
st.failedUntil
|
|
381
|
+
if (st.failedUntil?.has(ref) || st.failCounts?.has(ref) || st.authFailCounts?.has(ref) || st.brokenUntil?.has(ref)) {
|
|
382
|
+
st.failedUntil?.delete(ref);
|
|
371
383
|
st.failCounts?.delete(ref);
|
|
372
384
|
st.authFailCounts?.delete(ref);
|
|
373
385
|
st.brokenUntil?.delete(ref);
|
|
@@ -445,7 +457,7 @@ export function registerOpsRoutes(ctx, deps) {
|
|
|
445
457
|
if (exhausted) exhaustedAny = true;
|
|
446
458
|
pools[pool.base] = { healthy, total, exhausted, healthScore: computeHealthScore(pool.state) };
|
|
447
459
|
}
|
|
448
|
-
json(res, 200, { status: exhaustedAny ? 'degraded' : 'ok', pools, exhaustedAny, latency: latencyHistogram.snapshotAll(), quota: quotaStore.snapshot() });
|
|
460
|
+
json(res, 200, { status: exhaustedAny ? 'degraded' : 'ok', pools, exhaustedAny, latency: latencyHistogram.snapshotAll(), quota: typeof quotaStore?.snapshot === 'function' ? quotaStore.snapshot() : null });
|
|
449
461
|
},
|
|
450
462
|
}), 'dsh-key-rotation: health');
|
|
451
463
|
|
|
@@ -513,7 +525,10 @@ export function registerOpsRoutes(ctx, deps) {
|
|
|
513
525
|
path: SANDBOX_CACHE_PATH,
|
|
514
526
|
handler: (req, res) => {
|
|
515
527
|
if (!isTrustedBridgeRequest(req)) { json(res, 403, { error: { code: 'forbidden', message: 'dsh-key-rotation: cache is local-only' } }); return; }
|
|
516
|
-
|
|
528
|
+
const cacheSnap = typeof lastTestCache?.snapshot === 'function'
|
|
529
|
+
? lastTestCache.snapshot()
|
|
530
|
+
: (lastTestCache instanceof Map ? Object.fromEntries(lastTestCache) : (lastTestCache ?? {}));
|
|
531
|
+
json(res, 200, cacheSnap);
|
|
517
532
|
},
|
|
518
533
|
}), 'dsh-key-rotation: sandbox cache');
|
|
519
534
|
|
|
@@ -594,9 +609,21 @@ export function registerOpsRoutes(ctx, deps) {
|
|
|
594
609
|
return;
|
|
595
610
|
}
|
|
596
611
|
const cleared = st.failedUntil.size;
|
|
597
|
-
st.failedUntil.clear();
|
|
598
|
-
|
|
599
|
-
|
|
612
|
+
st.failedUntil.clear();
|
|
613
|
+
st.failCounts?.clear();
|
|
614
|
+
st.authFailCounts?.clear();
|
|
615
|
+
st.brokenUntil?.clear();
|
|
616
|
+
st.switches = 0;
|
|
617
|
+
st.lastReason = undefined;
|
|
618
|
+
st.lastSwitchAt = undefined;
|
|
619
|
+
let circuitReset = false;
|
|
620
|
+
const br = circuitBreaker ?? buildRuntime().breaker;
|
|
621
|
+
if (br) {
|
|
622
|
+
if (typeof br.reset === 'function') { br.reset(provider); circuitReset = true; }
|
|
623
|
+
else if (typeof br.onSuccess === 'function') { br.onSuccess(provider); circuitReset = true; }
|
|
624
|
+
}
|
|
625
|
+
console.warn(`[dsh-key-rotation] pool ${provider} RESET via webhook action (circuitReset=${circuitReset})`);
|
|
626
|
+
json(res, 200, { ok: true, action, provider, cleared, circuitReset });
|
|
600
627
|
return;
|
|
601
628
|
}
|
|
602
629
|
json(res, 400, { error: { code: 'unknown-action', message: `dsh-key-rotation: unknown action '${action}'` } });
|
package/lib/sandbox.js
CHANGED
|
@@ -83,7 +83,15 @@ export class SandboxRunner {
|
|
|
83
83
|
res = await doFetch();
|
|
84
84
|
} catch (e) {
|
|
85
85
|
if (e && e.name === 'AbortError') return { ok: false, code: 'timeout', latencyMs: Date.now() - started };
|
|
86
|
-
|
|
86
|
+
// ponytail: 1 retry on transient socket network errors before marking broken
|
|
87
|
+
await new Promise((r) => setTimeout(r, PROBE_RETRY_DELAY_MS));
|
|
88
|
+
if (ctrl.signal.aborted) return { ok: false, code: 'timeout', latencyMs: Date.now() - started };
|
|
89
|
+
try {
|
|
90
|
+
res = await doFetch();
|
|
91
|
+
} catch (e2) {
|
|
92
|
+
if (e2 && e2.name === 'AbortError') return { ok: false, code: 'timeout', latencyMs: Date.now() - started };
|
|
93
|
+
return { ok: false, code: 'network', latencyMs: Date.now() - started };
|
|
94
|
+
}
|
|
87
95
|
}
|
|
88
96
|
// ponytail: 1 retry on 5xx — naive; classifier refines if needed
|
|
89
97
|
if (res.status >= 500 && res.status < 600) {
|
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.10",
|
|
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": [
|