@goodandready/dsh-key-rotation 0.7.38 → 0.7.39
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 +5 -0
- package/README.ru.md +5 -0
- package/README.zh.md +5 -0
- package/lib/heal.js +2 -2
- package/lib/index.js +12 -7
- package/lib/webhook.js +1 -0
- package/package.json +3 -4
package/README.md
CHANGED
|
@@ -253,6 +253,11 @@ All management routes require loopback authentication (`127.0.0.1` / `::1`) with
|
|
|
253
253
|
|
|
254
254
|
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
255
255
|
|
|
256
|
+
### v0.7.39
|
|
257
|
+
- **Self-Healing & lastUsedAt Accuracy**: Fixed key timestamp lookup in `healIdleCooldowns` to read from the modern `pool.state.lastUsedAt` map (with backwards-compatible fallback). `credentials.resolve` now properly records each key invocation timestamp in `lastUsedAt`, surfacing accurate "last used" indicators in the status dashboard and enabling background idle cooldown restoration.
|
|
258
|
+
- **Hotpath & Runtime Memoization**: Eliminated redundant `buildRuntime()` calls across periodic sweeps, provider exhaustion handling, and `/status` query processing.
|
|
259
|
+
- **Test Suite & CI Hardening**: Periodic interval sweep timers and debounce timers unref'ed to allow Node.js event loop natural exit without stalling CI runners. Purged obsolete incident test artifacts.
|
|
260
|
+
|
|
256
261
|
### v0.7.38
|
|
257
262
|
- **Hot-Path Stream Optimization**: Eliminated 4 redundant `buildRuntime()` calls inside the `rotate()` finish chunk handler by reusing the request-scoped `runtime0` snapshot.
|
|
258
263
|
- **Allocation-Free Rate Limit Header Parsing**: Optimized `extractRateLimit()` with single-pass header inspection and length guards, completely removing dynamic lowercase/uppercase string allocations on every response chunk.
|
package/README.ru.md
CHANGED
|
@@ -253,6 +253,11 @@ dsh-key-rotation:
|
|
|
253
253
|
|
|
254
254
|
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
255
255
|
|
|
256
|
+
### v0.7.39
|
|
257
|
+
- **Самоисцеление ключей и фиксация lastUsedAt**: Исправлено чтение меток времени в `healIdleCooldowns`, использующее теперь карту `pool.state.lastUsedAt`. В `credentials.resolve` добавлено сохранение точного времени каждого обращения к ключу, что активирует вывод времени использования в панели мониторинга и корректное самоисцеление ключей после простоя.
|
|
258
|
+
- **Оптимизация обращений к buildRuntime**: Устранены повторные вызовы `buildRuntime()` в процедурах периодической очистки, маршруте `/status` и блоке обработки исчерпания пула.
|
|
259
|
+
- **Стабилизация CI и устранение утечек таймеров**: Фоновые таймеры дебаунса и очистки теперь отвязываются (`unref`) от цикла событий Node.js, предотвращая зависание раннера. Удалены тесты устаревшего модуля инцидентов.
|
|
260
|
+
|
|
256
261
|
### v0.7.38
|
|
257
262
|
- **Оптимизация горячего пути стриминга**: Устранены 4 избыточных вызова `buildRuntime()` при обработке завершающего чанка в `rotate()` за счёт повторного использования снимка `runtime0`.
|
|
258
263
|
- **Парсинг заголовков лимитов без лишних аллокаций**: Оптимизирована функция `extractRateLimit()` — однократный проход по объекту с проверкой длины ключей без постоянных аллокаций строк `.toLowerCase()` и `.toUpperCase()`.
|
package/README.zh.md
CHANGED
|
@@ -214,6 +214,11 @@ dsh-key-rotation:
|
|
|
214
214
|
|
|
215
215
|
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
216
216
|
|
|
217
|
+
### v0.7.39
|
|
218
|
+
- **自动恢复与 lastUsedAt 修复**:修复了 `healIdleCooldowns` 中对密钥调用时间戳的读取逻辑,直接从 `lastUsedAt` 映射读取。在 `credentials.resolve` 中补充记录每次密钥调用的时间戳,使状态面板的最近使用时间生效并正确支持空闲密钥恢复。
|
|
219
|
+
- **运行期快照缓存优化**:消除了定时维护清理、`/status` 路由及密钥池耗尽处理中重复调用 `buildRuntime()` 的开销。
|
|
220
|
+
- **CI 测试稳定性提升**:对维护定时器与通知防抖定时器执行 `unref`,确保 Node.js 事件循环在测试完成后干净退出,彻底解决 CI 运行器假死问题。
|
|
221
|
+
|
|
217
222
|
### v0.7.38
|
|
218
223
|
- **热路径流处理优化**:在 `rotate()` 结束块处理中消除 4 次多余的 `buildRuntime()` 重复调用,直接复用请求作用域内的 `runtime0` 快照。
|
|
219
224
|
- **零额外字符串分配的限流头解析**:重构 `extractRateLimit()`,采用单次遍历结合键长度检查,彻底消除对每个响应头执行 `.toLowerCase()` / `.toUpperCase()` 的内存碎片分配。
|
package/lib/heal.js
CHANGED
|
@@ -11,13 +11,13 @@ export function healIdleCooldowns(pools, idleMs, now = Date.now()) {
|
|
|
11
11
|
for (const pool of pools) {
|
|
12
12
|
if (!pool || !pool.state || !pool.base) continue;
|
|
13
13
|
const fu = pool.state.failedUntil;
|
|
14
|
-
const
|
|
14
|
+
const lua = pool.state.lastUsedAt ?? pool.state.lastUsed;
|
|
15
15
|
if (!fu || fu.size === 0) continue;
|
|
16
16
|
const expiredRefs = [];
|
|
17
17
|
for (const [ref, until] of fu.entries()) {
|
|
18
18
|
if (!Number.isFinite(until)) continue;
|
|
19
19
|
if (until > now) continue; // cooldown still active
|
|
20
|
-
const last =
|
|
20
|
+
const last = typeof lua?.get === 'function' ? lua.get(ref) : undefined;
|
|
21
21
|
if (!Number.isFinite(last)) continue; // never used → no signal, skip
|
|
22
22
|
if (now - last < idleMs) continue; // used recently → don't heal
|
|
23
23
|
expiredRefs.push(ref);
|
package/lib/index.js
CHANGED
|
@@ -38,7 +38,7 @@ import { keyTail, isLoopbackAddress, isTrustedBridgeRequest, SWITCHABLE_MESSAGE_
|
|
|
38
38
|
|
|
39
39
|
export const name = 'dsh-key-rotation';
|
|
40
40
|
export const inject = ['llm', 'webServer', 'settings', 'credentials'];
|
|
41
|
-
export { keyTail, isLoopbackAddress, isTrustedBridgeRequest, DEFAULT_SWITCH_CODES, isSwitchableError, formatExhaustionMessage };
|
|
41
|
+
export { keyTail, isLoopbackAddress, isTrustedBridgeRequest, DEFAULT_SWITCH_CODES, isSwitchableError, formatExhaustionMessage, getRuntime };
|
|
42
42
|
|
|
43
43
|
/** Settings namespace owning the GUI-editable section (settingsNamespace-valid). */
|
|
44
44
|
const NS = 'dsh-key-rotation';
|
|
@@ -452,13 +452,13 @@ export function apply(ctx, config = {}) {
|
|
|
452
452
|
}
|
|
453
453
|
const n = sweepExpired(poolState, now);
|
|
454
454
|
if (n > 0) console.warn(`[dsh-key-rotation] sweep: cleared ${n} expired cooldown(s)`);
|
|
455
|
-
|
|
455
|
+
const runtime = buildRuntime();
|
|
456
|
+
for (const pool of runtime.poolByRef.values()) {
|
|
456
457
|
compactUsage(pool, 30, now);
|
|
457
458
|
}
|
|
458
459
|
// #207 expiry pre-warning + #208 cost budget - piggybacked on this timer,
|
|
459
460
|
// deduped to one notification per key/window per day (shouldNotifyDaily).
|
|
460
461
|
try {
|
|
461
|
-
const runtime = buildRuntime();
|
|
462
462
|
const seen = new Set();
|
|
463
463
|
for (const pool of runtime.poolByRef.values()) {
|
|
464
464
|
if (seen.has(pool.base)) continue;
|
|
@@ -563,6 +563,7 @@ export function apply(ctx, config = {}) {
|
|
|
563
563
|
}
|
|
564
564
|
} catch (_) { /* maintenance must never crash the sweep */ }
|
|
565
565
|
}, 30000);
|
|
566
|
+
if (typeof id.unref === 'function') id.unref();
|
|
566
567
|
return () => clearInterval(id);
|
|
567
568
|
}, 'dsh-key-rotation: sweep expired cooldowns');
|
|
568
569
|
|
|
@@ -771,6 +772,8 @@ export function apply(ctx, config = {}) {
|
|
|
771
772
|
let hit = await original(candidate);
|
|
772
773
|
if (hit && typeof hit.value === 'string' && hit.value.length > 0) {
|
|
773
774
|
pool.state.lastUsed = candidate;
|
|
775
|
+
if (!pool.state.lastUsedAt) pool.state.lastUsedAt = new Map();
|
|
776
|
+
pool.state.lastUsedAt.set(candidate, now);
|
|
774
777
|
const store = dispatchStorage.getStore();
|
|
775
778
|
if (store && store.pool === pool) store.pickedRef = candidate;
|
|
776
779
|
if (pool.state.failCounts) pool.state.failCounts.delete(candidate);
|
|
@@ -792,6 +795,8 @@ export function apply(ctx, config = {}) {
|
|
|
792
795
|
const envVal = envValue(candidate);
|
|
793
796
|
if (envVal !== undefined) {
|
|
794
797
|
pool.state.lastUsed = candidate;
|
|
798
|
+
if (!pool.state.lastUsedAt) pool.state.lastUsedAt = new Map();
|
|
799
|
+
pool.state.lastUsedAt.set(candidate, now);
|
|
795
800
|
const store = dispatchStorage.getStore();
|
|
796
801
|
if (store && store.pool === pool) store.pickedRef = candidate;
|
|
797
802
|
if (pool.state.failCounts) pool.state.failCounts.delete(candidate);
|
|
@@ -1034,11 +1039,11 @@ export function apply(ctx, config = {}) {
|
|
|
1034
1039
|
pool.state.lastExhaustionAt = Date.now();
|
|
1035
1040
|
pool.state.exhaustionCount = (pool.state.exhaustionCount ?? 0) + 1;
|
|
1036
1041
|
console.warn(`[dsh-key-rotation] ${options.provider}: pool exhausted — all ${pool.refs.length} keys cooling`);
|
|
1042
|
+
const runtime = buildRuntime();
|
|
1037
1043
|
// notify via extracted helper (see notifyExhaustion above)
|
|
1038
|
-
notifyExhaustion(
|
|
1044
|
+
notifyExhaustion(runtime, pool, { provider: options.provider });
|
|
1039
1045
|
|
|
1040
1046
|
// #194: cross-provider cascade failover (guarded against infinite recursion)
|
|
1041
|
-
const runtime = buildRuntime();
|
|
1042
1047
|
if (!options.__isCascade && Array.isArray(runtime.cascade) && runtime.cascade.length > 0) {
|
|
1043
1048
|
const pools = runtime.providerToPool;
|
|
1044
1049
|
const fb = pickCascadeFallback(options.provider, runtime, pools);
|
|
@@ -1079,10 +1084,10 @@ export function apply(ctx, config = {}) {
|
|
|
1079
1084
|
json(res, 403, { error: { code: 'forbidden', message: 'dsh-key-rotation: status is local-only' } });
|
|
1080
1085
|
return;
|
|
1081
1086
|
}
|
|
1082
|
-
const
|
|
1087
|
+
const runtime = buildRuntime();
|
|
1088
|
+
const { poolByRef, providerTags, providerBudgets, latencySloMs } = runtime;
|
|
1083
1089
|
const base = ctx.get('credentials');
|
|
1084
1090
|
const now = Date.now();
|
|
1085
|
-
const latencySloMs = buildRuntime().latencySloMs;
|
|
1086
1091
|
const seen = new Set();
|
|
1087
1092
|
const providers = [];
|
|
1088
1093
|
for (const pool of poolByRef.values()) {
|
package/lib/webhook.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-key-rotation",
|
|
3
|
-
"version": "0.7.
|
|
4
|
-
"packageManager": "pnpm@10.33.2",
|
|
3
|
+
"version": "0.7.39",
|
|
5
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.",
|
|
6
5
|
"keywords": [
|
|
7
6
|
"deepseek-harness",
|
|
@@ -54,6 +53,6 @@
|
|
|
54
53
|
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6"
|
|
55
54
|
},
|
|
56
55
|
"scripts": {
|
|
57
|
-
"test": "node --test test/*.test.js test/*.test.mjs"
|
|
56
|
+
"test": "node --test --test-timeout=10000 test/*.test.js test/*.test.mjs"
|
|
58
57
|
}
|
|
59
|
-
}
|
|
58
|
+
}
|