@goodandready/dsh-key-rotation 0.7.1 → 0.7.2

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 CHANGED
@@ -61,6 +61,8 @@ window.__ModuleLoader__.load({
61
61
  keyWriteFailed: 'could not store the key: {msg}',
62
62
  keyHint: 'The value is stored in DSH credentials and never sent back to the browser — only its last 5 characters are shown. Names are generated for you; hover a key to see the one it uses.',
63
63
  brokenKey: 'broken (3× AUTH)',
64
+ exportPools: 'Export',
65
+ importPools: 'Import',
64
66
  resetCooldown: 'Reset cooldown',
65
67
  testKey: 'Test',
66
68
  testOk: 'OK',
@@ -105,6 +107,8 @@ window.__ModuleLoader__.load({
105
107
  keyWriteFailed: 'не удалось сохранить ключ: {msg}',
106
108
  keyHint: 'Значение хранится в учётных данных DSH и обратно в браузер не отдаётся — показываются только последние 5 символов. Имена переменных создаются автоматически; наведите на ключ, чтобы увидеть используемое имя.',
107
109
  brokenKey: 'сломан (3× AUTH)',
110
+ exportPools: 'Экспорт',
111
+ importPools: 'Импорт',
108
112
  resetCooldown: 'Сбросить кулдаун',
109
113
  testKey: 'Тест',
110
114
  testOk: 'OK',
@@ -319,7 +323,7 @@ window.__ModuleLoader__.load({
319
323
  const providerById = new Map(providers.map((p) => [p.id, p.name]));
320
324
 
321
325
  const setField = (fn) => setDraft(fn(val));
322
- const providerList = Array.isArray(val.providers) ? val.providers : [];
326
+ const providerList = Array.isArray(val.providers) ? val.providers.filter((p) => Array.isArray(p.keys) && p.keys.length > 0) : [];
323
327
 
324
328
  const setProvider = (index, id) => setField((cur) => {
325
329
  const next = [...(Array.isArray(cur.providers) ? cur.providers : [])];
@@ -473,6 +477,7 @@ window.__ModuleLoader__.load({
473
477
  if (typed) meta.push(btn('✓', () => saveSecret(key, rowKey), { title: t('keySave'), key: 'w' }));
474
478
  }
475
479
  if (info && typeof info.usage === 'number' && info.usage > 0) meta.push(h('span', { key: 'u', className: 'krot-tail', title: 'requests through this key' }, String(info.usage)));
480
+ if (info && typeof info.cost === 'number' && info.cost > 0) meta.push(h('span', { key: 'c', className: 'krot-tail', title: 'cost' }, '$' + info.cost.toFixed(2)));
476
481
  const tr = testResult[key];
477
482
  if (tr) meta.push(h('span', { key: 'tr', className: 'krot-tail', title: tr.message || (tr.ok ? t('testOk') : t('testFail')) }, tr.ok ? '✓' : '✕'));
478
483
  meta.push(h('button', { key: 't', className: 'krot-btn', onClick: () => doTest(key), disabled: testing === key, title: t('testKey') }, testing === key ? '…' : t('testKey')));
@@ -510,6 +515,7 @@ window.__ModuleLoader__.load({
510
515
  btn(t('addKey'), () => addKey(pIndex), { title: t('addKeyTitle') }),
511
516
  switchesLine,
512
517
  exhaustionWarning,
518
+ (providerStatus && Array.isArray(providerStatus.events) && providerStatus.events.length > 0 ? h('div', { style: { display: 'flex', gap: '2px', alignItems: 'end', height: '24px', marginTop: '4px' } }, (() => { const now = Date.now(); const buckets = Array(24).fill(0); for (const ev of providerStatus.events) { const h = Math.floor((now - ev.at) / 3600000); if (h >= 0 && h < 24) buckets[23 - h]++; } const max = Math.max(1, ...buckets); return buckets.map((c, i) => h('div', { key: i, title: c + ' switches', style: { flex: 1, background: c ? 'var(--dsw-alias-state-warning-primary)' : 'var(--dsw-alias-border-l2)', height: (c / max * 24) + 'px', minHeight: '2px', borderRadius: '2px' } })); })()) : null),
513
519
  (providerStatus && Array.isArray(providerStatus.events) && providerStatus.events.length > 0 ? h('details', { style: { fontSize: '11px', marginTop: '6px' } }, h('summary', null, 'Recent failures ('+providerStatus.events.length+')'), h('ul', { style: { margin: '4px 0 0', paddingLeft: '16px' } }, providerStatus.events.slice().reverse().map((ev, i) => h('li', { key: i }, new Date(ev.at).toLocaleTimeString() + ' ' + ev.ref + ' ' + ev.reason + ' cd=' + ev.cooldownMs)) )) : null),
514
520
  btn(t('resetCooldown'), () => doReset(entry.provider), { disabled: !(providerStatus && providerStatus.switches > 0) || resetting === entry.provider, title: t('resetCooldown') }),
515
521
  ),
@@ -535,6 +541,22 @@ window.__ModuleLoader__.load({
535
541
  providerRows,
536
542
  h('div', { className: 'krot-foot' }, btn(t('addProvider'), addProvider, {}), noProviders),
537
543
  )),
544
+ h('div', { className: 'krot-foot' }, btn(t('exportPools'), () => {
545
+ const data = JSON.stringify(val.providers ?? [], null, 2);
546
+ const blob = new Blob([data], { type: 'application/json' });
547
+ const url = URL.createObjectURL(blob);
548
+ const a = document.createElement('a'); a.href = url; a.download = 'pools.json'; a.click(); URL.revokeObjectURL(url);
549
+ }, {}), h('label', { className: 'krot-btn', style: { cursor: 'pointer' } }, t('importPools'), h('input', { type: 'file', accept: '.json', style: { display: 'none' }, onChange: (e) => {
550
+ const f = e.target.files[0]; if (!f) return;
551
+ const reader = new FileReader();
552
+ reader.onload = () => { try { const imp = JSON.parse(String(reader.result)); if (!Array.isArray(imp)) throw new Error('expected array'); setField((cur) => {
553
+ const curProviders = Array.isArray(cur.providers) ? [...cur.providers] : [];
554
+ const map = new Map(curProviders.map((p) => [p.provider, p]));
555
+ for (const p of imp) { if (p && typeof p.provider === 'string') map.set(p.provider, p); }
556
+ return { ...cur, providers: [...map.values()] };
557
+ }); } catch (err) { setSecretError(String(err.message || err)); } };
558
+ reader.readAsText(f);
559
+ } }))),
538
560
  h('p', { className: 'krot-hint' }, t('keyHint')),
539
561
  secretError ? h('p', { className: 'krot-err' }, secretError) : null,
540
562
  state.error ? h('p', { className: 'krot-err' }, state.error) : null,
package/lib/index.js CHANGED
@@ -321,6 +321,10 @@ export function apply(ctx, config = {}) {
321
321
  }
322
322
  }
323
323
 
324
+ // auto-cleanup: remove poolState for providers that are now empty or removed
325
+ for (const key of [...poolState.keys()]) {
326
+ if (![...poolByRef.values()].some((p) => p.base === key)) poolState.delete(key);
327
+ }
324
328
  return { switchCodes, cooldownMs, maxCooldownMs, notifyWebhook, notifyThreshold, poolByRef, providerToPool, cloneIds };
325
329
  }
326
330
 
@@ -443,8 +447,9 @@ export function apply(ctx, config = {}) {
443
447
  const failure = chunk.reason?.failure;
444
448
  const code = failure?.code;
445
449
  const message = failure?.message ?? '';
450
+ const effectiveSwitchCodes = pool.switchCodes ?? switchCodes;
446
451
  const switchable = !yielded && kind === 'error' &&
447
- (switchCodes.has(code) || SWITCHABLE_MESSAGE_PATTERN.test(message));
452
+ (effectiveSwitchCodes.has(code) || SWITCHABLE_MESSAGE_PATTERN.test(message));
448
453
  if (switchable) {
449
454
  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); } }
450
455
  pool.state.switches = (pool.state.switches ?? 0) + 1;
@@ -455,6 +460,11 @@ export function apply(ctx, config = {}) {
455
460
  switching = true;
456
461
  break;
457
462
  }
463
+ // cost tracking if provider returns usage.cost
464
+ if (chunk.usage?.cost != null && pool.state.lastUsed) {
465
+ const c = Number(chunk.usage.cost);
466
+ if (!isNaN(c)) pool.state.costPerKey.set(pool.state.lastUsed, (pool.state.costPerKey.get(pool.state.lastUsed) ?? 0) + c);
467
+ }
458
468
  yield chunk;
459
469
  return;
460
470
  }
@@ -551,6 +561,7 @@ export function apply(ctx, config = {}) {
551
561
  active: pool.state.lastUsed === ref,
552
562
  cooldownMsLeft: until !== undefined && until > now ? until - now : 0,
553
563
  usage: pool.state.usageCounts?.get(ref) ?? 0,
564
+ cost: pool.state.costPerKey?.get(ref) ?? 0,
554
565
  broken: pool.state.brokenUntil?.has(ref) ?? false,
555
566
  });
556
567
  }
@@ -562,7 +573,7 @@ export function apply(ctx, config = {}) {
562
573
  lastSwitchAt: pool.state.lastSwitchAt ?? null,
563
574
  lastExhaustionAt: pool.state.lastExhaustionAt ?? null,
564
575
  exhaustionCount: pool.state.exhaustionCount ?? 0,
565
- events: (pool.state.events ?? []).slice(-20),
576
+ events: (pool.state.events ?? []).slice(-50),
566
577
  });
567
578
  }
568
579
  json(res, 200, { providers });
@@ -732,7 +743,8 @@ export function apply(ctx, config = {}) {
732
743
  if (!pool) return next();
733
744
  const code = String(payload?.failure?.code ?? payload?.code ?? '');
734
745
  const message = String(payload?.failure?.message ?? payload?.message ?? '');
735
- const switchable = switchCodes.has(code) || SWITCHABLE_MESSAGE_PATTERN.test(message);
746
+ const effectiveSwitchCodes = pool.switchCodes ?? switchCodes;
747
+ const switchable = effectiveSwitchCodes.has(code) || SWITCHABLE_MESSAGE_PATTERN.test(message);
736
748
  if (!switchable) return next();
737
749
  const ref = pool.state.lastUsed;
738
750
  if (ref) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-key-rotation",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
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",