@goodandready/dsh-key-rotation 0.8.3 → 0.8.5
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 +1 -0
- package/README.ru.md +1 -0
- package/lib/client.js +111 -19
- package/lib/index.js +2 -1
- package/lib/pool.js +17 -0
- package/lib/rotate.js +1 -1
- package/lib/routes-ops.js +8 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
- **🕒 Accurate Midnight PST Resets**: Corrected UTC-8 timezone calculation offset sign for calendar quota reset windows.
|
|
46
46
|
- **🧹 Lifecycle Timer Cleanup**: Wrapped `canaryTimer` and `selfHealTimer` in Cordis effect scopes, eliminating background orphaned intervals on hot reload.
|
|
47
47
|
- **⚡ Stale Lock Recovery in Load Balancer**: Added expired lock detection to `pickLeastLoaded` for uninterrupted least-connections routing.
|
|
48
|
+
- **📊 Load Distribution & Modals (Changed in v0.8.5)**: Interactive segmented load distribution charts per pool, modal action confirmation, and 429/5xx backoff jitter (#283, #284).
|
|
48
49
|
- **🎨 Native Design System (Changed in v0.8.3)**: Unified with `dsh-clinebot` baseline: modular section cards, live pool telemetry stat boxes, pill badges, and complete semantic theme token styling (#281).
|
|
49
50
|
- **🌐 Localization (Changed in v0.8.2)**: Source strings are English-only. Russian/Chinese UI comes from the DSH core locale service and translation plugins (`props.t`). Active locale: host snapshot → first `navigator.languages` entry → `en` (#277).
|
|
50
51
|
|
package/README.ru.md
CHANGED
|
@@ -295,6 +295,7 @@ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
|
295
295
|
- **Очистка жизненного цикла**: Патч `credentials.resolve` и слушатели событий `ctx.on` (`llm/stream`, `agent/request-error`) переведены в скоупы `ctx.effect` с автоматическим восстановлением функций и отпиской при выгрузке плагина (#238, #239).
|
|
296
296
|
- **Роли секретов в схеме**: Полям `incidentGitHubToken` и `webhookActionToken` в схеме `Config` присвоена роль `.role('secret')` для маскирования в UI (#237).
|
|
297
297
|
- **Архитектура настроек**: Добавлена нативная интеграция со снимками `settingsScope` в карточке настроек с безопасным фоллбеком на HTTP-мост (#235).
|
|
298
|
+
- **Распределение нагрузки и модальные окна (Changed in v0.8.5)**: Диаграмма распределения нагрузки (.krot-load-chart), модальные подтверждения (.krot-modal) и джиттер кулдаунов при 429/5xx (#283, #284).
|
|
298
299
|
- **Нативная дизайн-система (Changed in v0.8.3)**: Унификация со стилем `dsh-clinebot`: модульные карточки секций, плитки оперативной телеметрии пулов, капсульные бейджи и системные CSS-токены DSH (#281).
|
|
299
300
|
- **Локализация (Changed in v0.8.2)**: Исходные строки только на английском (`en`). Русский и китайский приходят через core `props.t` / translation-плагины. Фолбек активной локали: snapshot → первый `navigator.languages` → `en`. Свой `settings.section` и встроенные `ru`/`zh`-таблицы удалены (#236, #275, #277).
|
|
300
301
|
- **Удаление мёртвого кода**: Удалена неиспользуемая функция `mountDashboard` после перехода на header chip (#240).
|
package/lib/client.js
CHANGED
|
@@ -38,6 +38,15 @@ window.__ModuleLoader__.load({
|
|
|
38
38
|
statPools: 'Configured Pools',
|
|
39
39
|
statKeys: 'Total Keys',
|
|
40
40
|
statHealthy: 'Healthy / Ready',
|
|
41
|
+
loadDistribution: 'Traffic Distribution',
|
|
42
|
+
statRequests: 'requests',
|
|
43
|
+
noTrafficYet: 'No traffic recorded yet',
|
|
44
|
+
confirm: 'Confirm',
|
|
45
|
+
cancel: 'Cancel',
|
|
46
|
+
confirmResetTitle: 'Reset cooldowns for {p}?',
|
|
47
|
+
confirmResetDesc: 'This will immediately clear all failure counts, cooldown timers, and reset the circuit breaker.',
|
|
48
|
+
confirmRemoveProvTitle: 'Remove provider {p}?',
|
|
49
|
+
confirmRemoveProvDesc: 'Are you sure you want to remove this provider and all configured keys from rotation?',
|
|
41
50
|
filterAll: 'All',
|
|
42
51
|
filterReady: 'Ready',
|
|
43
52
|
filterCooldown: 'In Cooldown',
|
|
@@ -229,6 +238,18 @@ window.__ModuleLoader__.load({
|
|
|
229
238
|
'.krot-stat-box{padding:12px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:4px}',
|
|
230
239
|
'.krot-stat-val{font-size:20px;font-weight:700;color:var(--dsw-alias-label-primary)}',
|
|
231
240
|
'.krot-stat-lbl{font-size:12px;color:var(--dsw-alias-label-secondary)}',
|
|
241
|
+
'.krot-load-chart{margin:10px 0;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2)}',
|
|
242
|
+
'.krot-load-header{display:flex;justify-content:space-between;align-items:center;font-size:12px;color:var(--dsw-alias-label-secondary);margin-bottom:6px}',
|
|
243
|
+
'.krot-load-bar{display:flex;height:12px;border-radius:999px;overflow:hidden;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1)}',
|
|
244
|
+
'.krot-load-segment{height:100%;transition:width 0.2s ease}',
|
|
245
|
+
'.krot-load-legend{display:flex;flex-wrap:wrap;gap:8px;margin-top:6px;font-size:11px}',
|
|
246
|
+
'.krot-load-item{display:inline-flex;align-items:center;gap:4px}',
|
|
247
|
+
'.krot-load-dot{width:7px;height:7px;border-radius:50%}',
|
|
248
|
+
'.krot-modal-backdrop{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.55);z-index:9999;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(2px)}',
|
|
249
|
+
'.krot-modal-card{width:90%;max-width:420px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;box-shadow:0 12px 36px rgba(0,0,0,0.28);padding:20px;display:flex;flex-direction:column;gap:12px}',
|
|
250
|
+
'.krot-modal-title{font-size:16px;font-weight:600;color:var(--dsw-alias-label-primary)}',
|
|
251
|
+
'.krot-modal-desc{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.45}',
|
|
252
|
+
'.krot-modal-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:8px}',
|
|
232
253
|
'.krot-badge{font-size:12px;padding:3px 10px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:5px;font-weight:500;white-space:nowrap}',
|
|
233
254
|
'.krot-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:rgba(16,185,129,0.08)}',
|
|
234
255
|
'.krot-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:rgba(245,158,11,0.08)}',
|
|
@@ -352,12 +373,12 @@ window.__ModuleLoader__.load({
|
|
|
352
373
|
function KeyRotationSection(props) {
|
|
353
374
|
const t = resolveT(props);
|
|
354
375
|
const settingsScope = React.useMemo(() => {
|
|
355
|
-
|
|
356
|
-
|
|
376
|
+
try {
|
|
377
|
+
if (props.ctx && props.ctx.settingsScope) {
|
|
357
378
|
return props.ctx.settingsScope.bind({ namespace: NS });
|
|
358
|
-
} catch (_) {
|
|
359
|
-
return null;
|
|
360
379
|
}
|
|
380
|
+
} catch (_) {
|
|
381
|
+
return null;
|
|
361
382
|
}
|
|
362
383
|
return null;
|
|
363
384
|
}, [props.ctx]);
|
|
@@ -392,6 +413,7 @@ window.__ModuleLoader__.load({
|
|
|
392
413
|
// ── all hooks live ABOVE any early return (React error 310 otherwise) ──
|
|
393
414
|
const [search, setSearch] = React.useState('');
|
|
394
415
|
const [statusFilter, setStatusFilter] = React.useState('all');
|
|
416
|
+
const [confirmModal, setConfirmModal] = React.useState(null);
|
|
395
417
|
const [optimisticReset, setOptimisticReset] = React.useState({});
|
|
396
418
|
const [selected, setSelected] = React.useState(new Set());
|
|
397
419
|
const [bulkCooldown, setBulkCooldown] = React.useState('');
|
|
@@ -913,6 +935,57 @@ window.__ModuleLoader__.load({
|
|
|
913
935
|
document.body.appendChild(a); a.click(); a.remove();
|
|
914
936
|
} }, t('exportCsv'));
|
|
915
937
|
|
|
938
|
+
const loadChart = (() => {
|
|
939
|
+
if (keys.length === 0) return null;
|
|
940
|
+
const ps = status[entry.provider];
|
|
941
|
+
const keyUsages = keys.map((k, idx) => {
|
|
942
|
+
const hit = ps && Array.isArray(ps.keys) ? ps.keys.find((x) => x.ref === k) : null;
|
|
943
|
+
const u = hit && typeof hit.usage === 'number' ? hit.usage : 0;
|
|
944
|
+
return { ref: k, index: idx, usage: u };
|
|
945
|
+
});
|
|
946
|
+
const total = keyUsages.reduce((acc, x) => acc + x.usage, 0);
|
|
947
|
+
const palette = [
|
|
948
|
+
'var(--dsw-alias-state-brand-primary, #6366f1)',
|
|
949
|
+
'var(--dsw-alias-state-success-primary, #10b981)',
|
|
950
|
+
'var(--dsw-alias-state-warning-primary, #f59e0b)',
|
|
951
|
+
'var(--dsw-alias-state-info-primary, #3b82f6)',
|
|
952
|
+
'#8b5cf6',
|
|
953
|
+
'#ec4899',
|
|
954
|
+
'#14b8a6',
|
|
955
|
+
];
|
|
956
|
+
return h('div', { className: 'krot-load-chart' },
|
|
957
|
+
h('div', { className: 'krot-load-header' },
|
|
958
|
+
h('span', { style: { fontWeight: 600 } }, '📊 ' + t('loadDistribution')),
|
|
959
|
+
h('span', null, total > 0 ? (total + ' ' + t('statRequests')) : t('noTrafficYet'))
|
|
960
|
+
),
|
|
961
|
+
h('div', { className: 'krot-load-bar' },
|
|
962
|
+
total > 0
|
|
963
|
+
? keyUsages.map((x, i) => {
|
|
964
|
+
if (x.usage === 0) return null;
|
|
965
|
+
const pct = Math.max(1, Math.round((x.usage / total) * 100));
|
|
966
|
+
const color = palette[i % palette.length];
|
|
967
|
+
return h('div', {
|
|
968
|
+
key: x.ref,
|
|
969
|
+
className: 'krot-load-segment',
|
|
970
|
+
style: { width: pct + '%', background: color },
|
|
971
|
+
title: x.ref + ': ' + x.usage + ' (' + pct + '%)',
|
|
972
|
+
});
|
|
973
|
+
})
|
|
974
|
+
: h('div', { className: 'krot-load-segment', style: { width: '100%', background: 'var(--dsw-alias-bg-layer-2)', opacity: 0.6 } })
|
|
975
|
+
),
|
|
976
|
+
total > 0 ? h('div', { className: 'krot-load-legend' },
|
|
977
|
+
keyUsages.map((x, i) => {
|
|
978
|
+
const pct = total > 0 ? Math.round((x.usage / total) * 100) : 0;
|
|
979
|
+
const color = palette[i % palette.length];
|
|
980
|
+
return h('span', { key: x.ref, className: 'krot-load-item' },
|
|
981
|
+
h('span', { className: 'krot-load-dot', style: { background: color } }),
|
|
982
|
+
h('span', { style: { color: 'var(--dsw-alias-label-secondary)' } }, t('keyLabel').replace('{n}', String(x.index + 1)) + ': ' + x.usage + ' (' + pct + '%)')
|
|
983
|
+
);
|
|
984
|
+
})
|
|
985
|
+
) : null
|
|
986
|
+
);
|
|
987
|
+
})();
|
|
988
|
+
|
|
916
989
|
return h('div', { key: pIndex, className: 'krot-prov' },
|
|
917
990
|
h('div', { className: 'krot-prov-head' },
|
|
918
991
|
h('input', { type: 'checkbox', checked: selected.has(entry.provider), onChange: (e) => { const ns = new Set(selected); if (e.target.checked) ns.add(entry.provider); else ns.delete(entry.provider); setSelected(ns); } }),
|
|
@@ -943,9 +1016,17 @@ window.__ModuleLoader__.load({
|
|
|
943
1016
|
return h('span', { className: 'krot-tail', title: 'health score', style: { flex: 'none', color, fontWeight: 700 } }, String(score));
|
|
944
1017
|
})(),
|
|
945
1018
|
(() => { const ps = status[entry.provider]; const tot = ps && typeof ps.totalUsage === 'number' ? ps.totalUsage : null; return tot !== null ? h('span', { className: 'krot-tail', title: 'total requests', style: { flex: 'none' } }, String(tot)) : null; })(),
|
|
946
|
-
btn('✕', () =>
|
|
1019
|
+
btn('✕', () => setConfirmModal({
|
|
1020
|
+
title: t('confirmRemoveProvTitle').replace('{p}', entry.provider),
|
|
1021
|
+
desc: t('confirmRemoveProvDesc'),
|
|
1022
|
+
actionLabel: t('removeProvider'),
|
|
1023
|
+
danger: true,
|
|
1024
|
+
onConfirm: () => removeProvider(pIndex),
|
|
1025
|
+
}), { title: t('removeProvider') }),
|
|
947
1026
|
),
|
|
948
|
-
filterBar,
|
|
1027
|
+
filterBar,
|
|
1028
|
+
loadChart,
|
|
1029
|
+
h('div', { className: 'krot-keys' }, keyRows),
|
|
949
1030
|
h('div', { className: 'krot-foot' },
|
|
950
1031
|
btn(t('addKey'), () => addKey(pIndex), { title: t('addKeyTitle') }),
|
|
951
1032
|
switchesLine,
|
|
@@ -956,7 +1037,13 @@ window.__ModuleLoader__.load({
|
|
|
956
1037
|
// #224: 7-day switches per day (client-side, from the same events)
|
|
957
1038
|
(providerStatus && Array.isArray(providerStatus.events) && providerStatus.events.length > 0 ? h('div', { style: { display: 'flex', gap: '2px', alignItems: 'end', height: '16px', marginTop: '2px' } }, (() => { const now = Date.now(); const days = Array(7).fill(0); for (const ev of providerStatus.events) { const d = Math.floor((now - ev.at) / 86400000); if (d >= 0 && d < 7) days[6 - d]++; } const max = Math.max(1, ...days); return days.map((c, i) => h('div', { key: i, title: c + ' switches · day -' + (6 - i), style: { flex: 1, background: c ? 'var(--dsw-alias-state-info-primary, var(--dsw-alias-state-warning-primary))' : 'var(--dsw-alias-border-l2)', height: (c / max * 16) + 'px', minHeight: '2px', borderRadius: '2px' } })); })()) : null),
|
|
958
1039
|
(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, style: ev.type === 'probe' ? { opacity: .5 } : null }, new Date(ev.at).toLocaleTimeString() + ' ' + (ev.type === 'probe' ? '[probe] ' : '') + ev.ref + ' ' + ev.reason + ' cd=' + ev.cooldownMs)) )) : null),
|
|
959
|
-
btn(t('resetCooldown'), () =>
|
|
1040
|
+
btn(t('resetCooldown'), () => setConfirmModal({
|
|
1041
|
+
title: t('confirmResetTitle').replace('{p}', entry.provider),
|
|
1042
|
+
desc: t('confirmResetDesc'),
|
|
1043
|
+
actionLabel: t('resetCooldown'),
|
|
1044
|
+
danger: false,
|
|
1045
|
+
onConfirm: () => doReset(entry.provider),
|
|
1046
|
+
}), { disabled: !(providerStatus && providerStatus.switches > 0) || resetting === entry.provider, title: t('resetCooldown') }),
|
|
960
1047
|
btn(testAllProvider === entry.provider ? t('testing') : t('testAll'), () => doTestAll(entry.provider), { disabled: testAllProvider === entry.provider, title: t('testAll') }),
|
|
961
1048
|
exportCsv,
|
|
962
1049
|
),
|
|
@@ -1124,7 +1211,23 @@ window.__ModuleLoader__.load({
|
|
|
1124
1211
|
) : null,
|
|
1125
1212
|
].filter(Boolean);
|
|
1126
1213
|
|
|
1214
|
+
const confirmModalEl = confirmModal ? h('div', { className: 'krot-modal-backdrop', onClick: () => setConfirmModal(null) },
|
|
1215
|
+
h('div', { className: 'krot-modal-card', onClick: (e) => e.stopPropagation() },
|
|
1216
|
+
h('div', { className: 'krot-modal-title' }, confirmModal.title),
|
|
1217
|
+
h('div', { className: 'krot-modal-desc' }, confirmModal.desc),
|
|
1218
|
+
h('div', { className: 'krot-modal-actions' },
|
|
1219
|
+
btn(t('cancel'), () => setConfirmModal(null)),
|
|
1220
|
+
btn(confirmModal.actionLabel || t('confirm'), () => {
|
|
1221
|
+
const fn = confirmModal.onConfirm;
|
|
1222
|
+
setConfirmModal(null);
|
|
1223
|
+
if (fn) fn();
|
|
1224
|
+
}, { primary: !confirmModal.danger, className: confirmModal.danger ? 'krot-btn-danger' : undefined })
|
|
1225
|
+
)
|
|
1226
|
+
)
|
|
1227
|
+
) : null;
|
|
1228
|
+
|
|
1127
1229
|
return h('div', { className: 'krot' },
|
|
1230
|
+
confirmModalEl,
|
|
1128
1231
|
statsSection,
|
|
1129
1232
|
poolsSection,
|
|
1130
1233
|
failoverSection,
|
|
@@ -1191,17 +1294,6 @@ window.__ModuleLoader__.load({
|
|
|
1191
1294
|
type: 'button',
|
|
1192
1295
|
className: 'krot-header-chip',
|
|
1193
1296
|
title: 'Key Rotation',
|
|
1194
|
-
sectionStats: 'Pool Health & Telemetry',
|
|
1195
|
-
sectionStatsDesc: 'Real-time telemetry of active key pools, healthy credentials, and failover status.',
|
|
1196
|
-
sectionFailover: 'Failover Triggers & Error Codes',
|
|
1197
|
-
sectionFailoverDesc: 'Select error conditions that trigger automatic switching to the next available key.',
|
|
1198
|
-
sectionTiming: 'Timing & Rotation Schedule',
|
|
1199
|
-
sectionTimingDesc: 'Configure cooldown durations and scheduled periodic rotation windows.',
|
|
1200
|
-
sectionBackup: 'Backup & Snapshots',
|
|
1201
|
-
sectionBackupDesc: 'Export or import provider pools and create full plugin configuration snapshots.',
|
|
1202
|
-
statPools: 'Configured Pools',
|
|
1203
|
-
statKeys: 'Total Keys',
|
|
1204
|
-
statHealthy: 'Healthy / Ready',
|
|
1205
1297
|
onClick: () => setOpen((v) => !v),
|
|
1206
1298
|
},
|
|
1207
1299
|
h('span', { style: { width: '8px', height: '8px', borderRadius: '50%', background: color, flex: 'none', boxShadow: `0 0 6px ${color}` } }),
|
|
@@ -1273,7 +1365,7 @@ window.__ModuleLoader__.load({
|
|
|
1273
1365
|
}
|
|
1274
1366
|
}
|
|
1275
1367
|
|
|
1276
|
-
module.exports = { apply, inject: ['slots', 'locale'] };
|
|
1368
|
+
module.exports = { apply, inject: ['slots', 'locale', 'settingsScope'] };
|
|
1277
1369
|
return module.exports;
|
|
1278
1370
|
},
|
|
1279
1371
|
});
|
package/lib/index.js
CHANGED
|
@@ -747,6 +747,7 @@ export function apply(ctx, config = {}) {
|
|
|
747
747
|
poolState,
|
|
748
748
|
getRotationDisabled: () => rotationDisabled,
|
|
749
749
|
setRotationDisabled: (v) => { rotationDisabled = v; },
|
|
750
|
+
circuitBreaker: moduleBreaker,
|
|
750
751
|
});
|
|
751
752
|
|
|
752
753
|
ctx.effect(() => ctx.on('llm/stream', (options, next) => {
|
|
@@ -783,7 +784,7 @@ export function apply(ctx, config = {}) {
|
|
|
783
784
|
if (moduleBreaker) moduleBreaker.onFailure(provider);
|
|
784
785
|
const ref = pool.state.lastUsed;
|
|
785
786
|
if (ref) {
|
|
786
|
-
const backoff = recordFailure(pool, ref, Date.now(), pool.cooldownMs ?? 60000, undefined, cls.soft);
|
|
787
|
+
const backoff = recordFailure(pool, ref, Date.now(), pool.cooldownMs ?? 60000, undefined, cls.soft, true);
|
|
787
788
|
pushEvent(pool, ref, code || 'UNKNOWN', backoff);
|
|
788
789
|
pool.state.switches = (pool.state.switches ?? 0) + 1;
|
|
789
790
|
pool.state.lastReason = code || 'UNKNOWN';
|
package/lib/pool.js
CHANGED
|
@@ -381,3 +381,20 @@ export function budgetVerdict(spend, budget) {
|
|
|
381
381
|
const ratio = spend / budget;
|
|
382
382
|
return { spend, budget, ratio, warn: ratio >= 0.8, exceeded: ratio >= 1 };
|
|
383
383
|
}
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
/** Safely reset a provider's circuit breaker to closed state. */
|
|
387
|
+
export function resetCircuitForProvider(circuitBreaker, provider) {
|
|
388
|
+
if (!circuitBreaker || !provider) return false;
|
|
389
|
+
try {
|
|
390
|
+
if (typeof circuitBreaker.reset === 'function') {
|
|
391
|
+
circuitBreaker.reset(provider);
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
if (typeof circuitBreaker.onSuccess === 'function') {
|
|
395
|
+
circuitBreaker.onSuccess(provider);
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
} catch (_) {}
|
|
399
|
+
return false;
|
|
400
|
+
}
|
package/lib/rotate.js
CHANGED
|
@@ -67,7 +67,7 @@ export function createRotate(deps) {
|
|
|
67
67
|
const _max = pool.maxCooldownMs ?? maxCooldownMs;
|
|
68
68
|
const _effBase = _retry !== undefined ? Math.max(_base, Math.min(_retry, _max ?? _base * 8)) : _base;
|
|
69
69
|
const cls = classifyFailure({ code: errCode, message: errMsg });
|
|
70
|
-
const _b = recordFailure(pool, targetRef, now(), _effBase, _max, cls.soft);
|
|
70
|
+
const _b = recordFailure(pool, targetRef, now(), _effBase, _max, cls.soft, true);
|
|
71
71
|
if (circuitBreaker) circuitBreaker.onFailure(pool.base ?? options?.provider);
|
|
72
72
|
pushEvent(pool, targetRef, errCode ?? 'UNKNOWN', _b);
|
|
73
73
|
if (!pool.state.authFailCounts) pool.state.authFailCounts = new Map();
|
package/lib/routes-ops.js
CHANGED
|
@@ -46,6 +46,7 @@ export function registerOpsRoutes(ctx, deps) {
|
|
|
46
46
|
poolState,
|
|
47
47
|
getRotationDisabled,
|
|
48
48
|
setRotationDisabled,
|
|
49
|
+
circuitBreaker,
|
|
49
50
|
} = deps;
|
|
50
51
|
|
|
51
52
|
// ── status route: what the settings card cannot know on its own ──
|
|
@@ -352,7 +353,13 @@ export function registerOpsRoutes(ctx, deps) {
|
|
|
352
353
|
st.authFailCounts?.clear();
|
|
353
354
|
st.brokenUntil?.clear();
|
|
354
355
|
st.switches = 0; st.lastReason = undefined; st.lastSwitchAt = undefined;
|
|
355
|
-
|
|
356
|
+
let circuitReset = false;
|
|
357
|
+
const br = circuitBreaker ?? buildRuntime().breaker;
|
|
358
|
+
if (br) {
|
|
359
|
+
if (typeof br.reset === 'function') { br.reset(provider); circuitReset = true; }
|
|
360
|
+
else if (typeof br.onSuccess === 'function') { br.onSuccess(provider); circuitReset = true; }
|
|
361
|
+
}
|
|
362
|
+
json(res, 200, { ok: true, provider, cleared, circuitReset });
|
|
356
363
|
return;
|
|
357
364
|
}
|
|
358
365
|
if (ref) {
|
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.5",
|
|
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": [
|