@goodandready/dsh-key-rotation 0.7.22 → 0.7.24
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/index.js +43 -11
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -92,8 +92,18 @@ const latencyHistogram = new LatencyHistogram();
|
|
|
92
92
|
const quotaStore = new QuotaStore();
|
|
93
93
|
const agentBudget = new AgentBudget();
|
|
94
94
|
const regionMap = new RegionMap();
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
// IncidentReporter: lazily built when Config provides incidentGitHubToken + incidentGitHubBaseUrl.
|
|
96
|
+
// ponytail: never bake the token into source; repo is hardcoded (this plugin's home repo) but token is per-deploy.
|
|
97
|
+
let incidentReporter = null;
|
|
98
|
+
function ensureIncidentReporter() {
|
|
99
|
+
if (incidentReporter) return incidentReporter;
|
|
100
|
+
const cfg = getConfig();
|
|
101
|
+
const token = cfg ? cfg.incidentGitHubToken : '';
|
|
102
|
+
const baseUrl = cfg ? cfg.incidentGitHubBaseUrl : '';
|
|
103
|
+
if (!token || !baseUrl) return null;
|
|
104
|
+
incidentReporter = new IncidentReporter({ token, baseUrl, repo: 'goodandready/dsh-key-rotation', fetchImpl: globalThis.fetch });
|
|
105
|
+
return incidentReporter;
|
|
106
|
+
}
|
|
97
107
|
const shadowRouter = new ShadowRouter({ primary: '', secondary: '', percent: 0 });let sandboxRunner = null;
|
|
98
108
|
const webhookSender = new WebhookSender({ fetchImpl: globalThis.fetch });
|
|
99
109
|
function ensureSandboxRunner(ctx) {
|
|
@@ -145,6 +155,9 @@ export const Config = Schema.object({
|
|
|
145
155
|
selfHealIdleMs: Schema.number().default(3600000),
|
|
146
156
|
latencyEnabled: Schema.boolean().default(true),
|
|
147
157
|
latencyWindow: Schema.number().default(200),
|
|
158
|
+
incidentGitHubToken: Schema.string().default(''),
|
|
159
|
+
incidentGitHubBaseUrl: Schema.string().default(''),
|
|
160
|
+
incidentThreshold: Schema.number().default(5),
|
|
148
161
|
rateLimitThreshold: Schema.number().default(0.1),
|
|
149
162
|
providers: Schema.array(Schema.object({
|
|
150
163
|
provider: Schema.string().required(),
|
|
@@ -464,6 +477,7 @@ export function apply(ctx, config = {}) {
|
|
|
464
477
|
const backupKeep = cfg.backupKeep ?? 7;
|
|
465
478
|
const rotationScheduleDays = cfg.rotationScheduleDays ?? 0;
|
|
466
479
|
const rateLimitThreshold = cfg.rateLimitThreshold ?? 0.1;
|
|
480
|
+
const incidentThreshold = cfg.incidentThreshold ?? 5;
|
|
467
481
|
|
|
468
482
|
// ref -> pool (every key env of every configured provider)
|
|
469
483
|
const poolByRef = new Map();
|
|
@@ -548,7 +562,7 @@ export function apply(ctx, config = {}) {
|
|
|
548
562
|
for (const key of [...poolState.keys()]) {
|
|
549
563
|
if (![...poolByRef.values()].some((p) => p.base === key)) poolState.delete(key);
|
|
550
564
|
}
|
|
551
|
-
return { switchCodes, cooldownMs, maxCooldownMs, notifyWebhook, notifyThreshold, backupDir, backupIntervalMs, backupKeep, rotationScheduleDays, rateLimitThreshold, poolByRef, providerToPool, modelPoolByProvider, cloneIds };
|
|
565
|
+
return { switchCodes, cooldownMs, maxCooldownMs, notifyWebhook, notifyThreshold, incidentThreshold, backupDir, backupIntervalMs, backupKeep, rotationScheduleDays, rateLimitThreshold, poolByRef, providerToPool, modelPoolByProvider, cloneIds };
|
|
552
566
|
}
|
|
553
567
|
|
|
554
568
|
// ── patch credentials.resolve: pool refs resolve to the next healthy key ──
|
|
@@ -756,14 +770,8 @@ export function apply(ctx, config = {}) {
|
|
|
756
770
|
pool.state.lastExhaustionAt = Date.now();
|
|
757
771
|
pool.state.exhaustionCount = (pool.state.exhaustionCount ?? 0) + 1;
|
|
758
772
|
console.warn(`[dsh-key-rotation] ${options.provider}: pool exhausted — all ${pool.refs.length} keys cooling`);
|
|
759
|
-
// notify
|
|
760
|
-
|
|
761
|
-
const { notifyWebhook, notifyThreshold } = buildRuntime();
|
|
762
|
-
if (notifyWebhook && pool.state.exhaustionCount >= notifyThreshold) {
|
|
763
|
-
const payload = JSON.stringify({ provider: options.provider, exhaustionCount: pool.state.exhaustionCount, at: pool.state.lastExhaustionAt, keys: pool.refs });
|
|
764
|
-
fetch(notifyWebhook, { method: 'POST', headers: { 'content-type': 'application/json' }, body: payload }).catch(()=>{});
|
|
765
|
-
}
|
|
766
|
-
} catch {}
|
|
773
|
+
// notify via extracted helper (see notifyExhaustion above)
|
|
774
|
+
notifyExhaustion(buildRuntime(), pool, { provider: options.provider });
|
|
767
775
|
|
|
768
776
|
yield lastFailure ?? finishError('TRANSPORT', 'dsh-key-rotation: all keys failed');
|
|
769
777
|
})();
|
|
@@ -1200,3 +1208,27 @@ export function apply(ctx, config = {}) {
|
|
|
1200
1208
|
});
|
|
1201
1209
|
});
|
|
1202
1210
|
}
|
|
1211
|
+
|
|
1212
|
+
// Notify on exhaustion: webhook + (optional) GitHub incident.
|
|
1213
|
+
// Extracted at module scope for testability. No I/O outside the injected hooks.
|
|
1214
|
+
// ponytail: thresholds and URLs are runtime-resolved per call, so changing Config is reflected immediately.
|
|
1215
|
+
export function notifyExhaustion(runtime, pool, options, hooks = { webhookSender, ensureIncidentReporter }) {
|
|
1216
|
+
if (!runtime || !pool) return;
|
|
1217
|
+
const count = pool.state ? (pool.state.exhaustionCount ?? 0) : 0;
|
|
1218
|
+
if (count <= 0) return;
|
|
1219
|
+
try {
|
|
1220
|
+
if (runtime.notifyWebhook && count >= (runtime.notifyThreshold ?? 0)) {
|
|
1221
|
+
hooks.webhookSender.send(runtime.notifyWebhook, {
|
|
1222
|
+
provider: options.provider,
|
|
1223
|
+
exhaustionCount: count,
|
|
1224
|
+
at: pool.state.lastExhaustionAt,
|
|
1225
|
+
keys: pool.refs,
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
if (runtime.incidentThreshold && count >= runtime.incidentThreshold) {
|
|
1229
|
+
const reporter = hooks.ensureIncidentReporter();
|
|
1230
|
+
if (reporter) reporter.open(options.provider, pool.state.lastExhaustionAt);
|
|
1231
|
+
}
|
|
1232
|
+
} catch (_) { /* ponytail: never crash rotate() */ }
|
|
1233
|
+
}
|
|
1234
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-key-rotation",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.24",
|
|
4
4
|
"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.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|