@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 +23 -1
- package/lib/index.js +15 -3
- package/package.json +1 -1
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
|
-
(
|
|
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(-
|
|
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
|
|
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.
|
|
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",
|