@goodandready/dsh-key-rotation 0.7.23 → 0.7.25

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.
Files changed (2) hide show
  1. package/lib/index.js +45 -5
  2. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -477,6 +477,7 @@ export function apply(ctx, config = {}) {
477
477
  const backupKeep = cfg.backupKeep ?? 7;
478
478
  const rotationScheduleDays = cfg.rotationScheduleDays ?? 0;
479
479
  const rateLimitThreshold = cfg.rateLimitThreshold ?? 0.1;
480
+ const incidentThreshold = cfg.incidentThreshold ?? 5;
480
481
 
481
482
  // ref -> pool (every key env of every configured provider)
482
483
  const poolByRef = new Map();
@@ -491,9 +492,24 @@ export function apply(ctx, config = {}) {
491
492
  let st = poolState.get(base);
492
493
  if (!st) {
493
494
  st = {
494
- failedUntil: new Map(), failCounts: new Map(), pointer: 0, lastUsed: undefined,
495
- switches: 0, lastReason: undefined, lastSwitchAt: undefined,
496
- lastExhaustionAt: undefined, exhaustionCount: 0, events: [], usageCounts: new Map(), byModel: new Map(), usageDays: new Map(),
495
+ failedUntil: new Map(),
496
+ failCounts: new Map(),
497
+ authFailCounts: new Map(),
498
+ brokenUntil: new Map(),
499
+ costPerKey: new Map(),
500
+ lastUsedAt: new Map(),
501
+ usageCounts: new Map(),
502
+ byModel: new Map(),
503
+ usageDays: new Map(),
504
+ quotaWindows: new Map(),
505
+ pointer: 0,
506
+ lastUsed: undefined,
507
+ switches: 0,
508
+ lastReason: undefined,
509
+ lastSwitchAt: undefined,
510
+ lastExhaustionAt: undefined,
511
+ exhaustionCount: 0,
512
+ events: [],
497
513
  };
498
514
  poolState.set(base, st);
499
515
  }
@@ -704,7 +720,28 @@ export function apply(ctx, config = {}) {
704
720
  const switchable = !yielded && kind === 'error' &&
705
721
  (effectiveSwitchCodes.has(code) || SWITCHABLE_MESSAGE_PATTERN.test(message));
706
722
  if (switchable) {
707
- if (pool.state.lastUsed) { const _retry = parseRetryAfter(message); const _base = pool.cooldownMs ?? cooldownMs; const _max = pool.maxCooldownMs ?? maxCooldownMs; const _effBase = _retry !== undefined ? Math.max(_base, Math.min(_retry, _max ?? _base * 8)) : _base; const _b = recordFailure(pool, pool.state.lastUsed, Date.now(), _effBase, _max); pushEvent(pool, pool.state.lastUsed, code ?? 'UNKNOWN', _b); const _code2 = String(code ?? ''); if (_code2 === 'AUTH' || /auth/i.test(message)) { const _c2 = (pool.state.authFailCounts.get(pool.state.lastUsed) ?? 0) + 1; pool.state.authFailCounts.set(pool.state.lastUsed, _c2); if (_c2 >= 3) { pool.state.brokenUntil.set(pool.state.lastUsed, Date.now() + 86400000*30); pool.state.failedUntil.set(pool.state.lastUsed, Date.now() + 86400000*30); } } else { pool.state.authFailCounts.delete(pool.state.lastUsed); } }
723
+ if (pool.state.lastUsed) {
724
+ const _retry = parseRetryAfter(message);
725
+ const _base = pool.cooldownMs ?? cooldownMs;
726
+ const _max = pool.maxCooldownMs ?? maxCooldownMs;
727
+ const _effBase = _retry !== undefined ? Math.max(_base, Math.min(_retry, _max ?? _base * 8)) : _base;
728
+ const _b = recordFailure(pool, pool.state.lastUsed, Date.now(), _effBase, _max);
729
+ pushEvent(pool, pool.state.lastUsed, code ?? 'UNKNOWN', _b);
730
+ // authFailCounts/brokenUntil: lazy-init if state was created by an older plugin version
731
+ if (!pool.state.authFailCounts) pool.state.authFailCounts = new Map();
732
+ if (!pool.state.brokenUntil) pool.state.brokenUntil = new Map();
733
+ const _code2 = String(code ?? '');
734
+ if (_code2 === 'AUTH' || /auth/i.test(message)) {
735
+ const _c2 = (pool.state.authFailCounts.get(pool.state.lastUsed) ?? 0) + 1;
736
+ pool.state.authFailCounts.set(pool.state.lastUsed, _c2);
737
+ if (_c2 >= 3) {
738
+ pool.state.brokenUntil.set(pool.state.lastUsed, Date.now() + 86400000*30);
739
+ pool.state.failedUntil.set(pool.state.lastUsed, Date.now() + 86400000*30);
740
+ }
741
+ } else {
742
+ pool.state.authFailCounts.delete(pool.state.lastUsed);
743
+ }
744
+ }
708
745
  pool.state.switches = (pool.state.switches ?? 0) + 1;
709
746
  pool.state.lastReason = String(code ?? 'UNKNOWN');
710
747
  pool.state.lastSwitchAt = Date.now();
@@ -716,7 +753,10 @@ export function apply(ctx, config = {}) {
716
753
  // cost tracking if provider returns usage.cost
717
754
  if (chunk.usage?.cost != null && pool.state.lastUsed) {
718
755
  const c = Number(chunk.usage.cost);
719
- if (!isNaN(c)) pool.state.costPerKey.set(pool.state.lastUsed, (pool.state.costPerKey.get(pool.state.lastUsed) ?? 0) + c);
756
+ if (!isNaN(c)) {
757
+ if (!pool.state.costPerKey) pool.state.costPerKey = new Map();
758
+ pool.state.costPerKey.set(pool.state.lastUsed, (pool.state.costPerKey.get(pool.state.lastUsed) ?? 0) + c);
759
+ }
720
760
  }
721
761
  // Usage by day (#119)
722
762
  if (pool.state.lastUsed) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-key-rotation",
3
- "version": "0.7.23",
3
+ "version": "0.7.25",
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",