@goodandready/dsh-key-rotation 0.7.37 → 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 +12 -0
- package/README.ru.md +12 -0
- package/README.zh.md +12 -0
- package/lib/heal.js +2 -2
- package/lib/index.js +27 -21
- package/lib/pool.js +17 -8
- package/lib/webhook.js +1 -0
- package/package.json +3 -4
package/README.md
CHANGED
|
@@ -253,6 +253,18 @@ 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
|
+
|
|
261
|
+
### v0.7.38
|
|
262
|
+
- **Hot-Path Stream Optimization**: Eliminated 4 redundant `buildRuntime()` calls inside the `rotate()` finish chunk handler by reusing the request-scoped `runtime0` snapshot.
|
|
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.
|
|
264
|
+
- **Date ISO String Memoization**: Memoized `todayIso` string calculation once per finishing request instead of creating multiple `Date` instances for `costDays` and `usageDays`.
|
|
265
|
+
- **Zero-Allocation Metrics Aggregation**: Replaced intermediate array allocation `[...values()].reduce()` with iterative summation for `totalUsage` in the `/dsh-key-rotation/status` endpoint.
|
|
266
|
+
- **Stale Notification Cleanup**: Automatically purge stale entries in notification throttling maps (`budgetNotifiedAt`, `lowHealthNotifiedAt`) when provider pools are deleted.
|
|
267
|
+
|
|
256
268
|
### v0.7.37
|
|
257
269
|
- **Request Context Isolation via AsyncLocalStorage**: Scoped active key resolution (`pickedRef`), start timestamps, and retry counts strictly to each async dispatch context using Node's `node:async_hooks`. Eliminates race conditions where concurrent streaming requests could penalize healthy keys.
|
|
258
270
|
- **Failover on Pre-Yield Stream Exceptions**: Fixed fatal stream termination where transport errors (e.g. HTTP 429 thrown before headers, socket hang-up) aborted the generator. The catch block now inspects `isSwitchableError` and cascades seamlessly to the next key if no content tokens have been yielded.
|
package/README.ru.md
CHANGED
|
@@ -253,6 +253,18 @@ 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
|
+
|
|
261
|
+
### v0.7.38
|
|
262
|
+
- **Оптимизация горячего пути стриминга**: Устранены 4 избыточных вызова `buildRuntime()` при обработке завершающего чанка в `rotate()` за счёт повторного использования снимка `runtime0`.
|
|
263
|
+
- **Парсинг заголовков лимитов без лишних аллокаций**: Оптимизирована функция `extractRateLimit()` — однократный проход по объекту с проверкой длины ключей без постоянных аллокаций строк `.toLowerCase()` и `.toUpperCase()`.
|
|
264
|
+
- **Мемоизация строки даты ISO**: Дата текущего дня (`todayIso`) теперь вычисляется ровно один раз на запрос, исключая создание нескольких дублирующих объектов `Date` для `costDays` и `usageDays`.
|
|
265
|
+
- **Итеративный подсчет totalUsage**: В эндпоинте статуса `/dsh-key-rotation/status` расчёт суммарного потребления переведён на прямой цикл без аллокации промежуточного массива `[...values()]`.
|
|
266
|
+
- **Очистка устаревших записей уведомлений**: При удалении пулов провайдеров из конфигурации автоматически вычищаются связанные ключи из карт троттлинга уведомлений (`budgetNotifiedAt`, `lowHealthNotifiedAt`).
|
|
267
|
+
|
|
256
268
|
### v0.7.37
|
|
257
269
|
- **Изоляция контекста запросов через AsyncLocalStorage**: Полная привязка активного ключа (`pickedRef`), времени старта и попыток к асинхронному контексту вызова через `node:async_hooks`. Устранена гонка, при которой параллельные запросы могли ошибочно штрафовать здоровый ключ соседа.
|
|
258
270
|
- **Failover при исключениях в потоке до первого чанка**: Устранено аварийное прерывание потока при транспортных сбоях (например, выброс HTTP 429 до отправки заголовков). Теперь блок перехвата проверяет `isSwitchableError` и прозрачно переключает поток на запасной ключ, если клиенту ещё не было отдано полезных данных.
|
package/README.zh.md
CHANGED
|
@@ -214,6 +214,18 @@ 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
|
+
|
|
222
|
+
### v0.7.38
|
|
223
|
+
- **热路径流处理优化**:在 `rotate()` 结束块处理中消除 4 次多余的 `buildRuntime()` 重复调用,直接复用请求作用域内的 `runtime0` 快照。
|
|
224
|
+
- **零额外字符串分配的限流头解析**:重构 `extractRateLimit()`,采用单次遍历结合键长度检查,彻底消除对每个响应头执行 `.toLowerCase()` / `.toUpperCase()` 的内存碎片分配。
|
|
225
|
+
- **日期 ISO 字符串记忆化**:在统计 `costDays` 与 `usageDays` 时将 `todayIso` 计算收敛为单次,杜绝重复创建 `Date` 实例。
|
|
226
|
+
- **状态统计零数组分配**:在 `/dsh-key-rotation/status` 中将 `totalUsage` 的计算由 `[...values()].reduce()` 改为直接迭代累加,避免频繁轮询引发的垃圾回收波动。
|
|
227
|
+
- **孤立通知记录自动清理**:在配置移除提供商模型池时,自动清理关联的通知限频缓存。
|
|
228
|
+
|
|
217
229
|
### v0.7.37
|
|
218
230
|
- **通过 AsyncLocalStorage 隔离请求上下文**:使用 Node.js 的 `node:async_hooks` 将解析后的密钥 (`pickedRef`)、启动时间和重试严格限定在单个请求上下文内,彻底消除并发请求间的竞态条件与误罚。
|
|
219
231
|
- **流异常自动故障转移**:修复流在首个 token 返回前抛出传输异常(如 HTTP 429)直接终止的问题。若未发送内容块,现在会自动触发 `isSwitchableError` 并顺畅切换到备用密钥。
|
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
|
|
|
@@ -695,7 +696,11 @@ export function apply(ctx, config = {}) {
|
|
|
695
696
|
|
|
696
697
|
// auto-cleanup: remove poolState for providers that are now empty or removed
|
|
697
698
|
for (const key of [...poolState.keys()]) {
|
|
698
|
-
if (![...poolByRef.values()].some((p) => p.base === key))
|
|
699
|
+
if (![...poolByRef.values()].some((p) => p.base === key)) {
|
|
700
|
+
poolState.delete(key);
|
|
701
|
+
lowHealthNotifiedAt.delete(key);
|
|
702
|
+
budgetNotifiedAt.delete(key + ':budget');
|
|
703
|
+
}
|
|
699
704
|
}
|
|
700
705
|
// #192: drop RPM windows for refs that no longer belong to any pool
|
|
701
706
|
for (const st of poolState.values()) {
|
|
@@ -767,6 +772,8 @@ export function apply(ctx, config = {}) {
|
|
|
767
772
|
let hit = await original(candidate);
|
|
768
773
|
if (hit && typeof hit.value === 'string' && hit.value.length > 0) {
|
|
769
774
|
pool.state.lastUsed = candidate;
|
|
775
|
+
if (!pool.state.lastUsedAt) pool.state.lastUsedAt = new Map();
|
|
776
|
+
pool.state.lastUsedAt.set(candidate, now);
|
|
770
777
|
const store = dispatchStorage.getStore();
|
|
771
778
|
if (store && store.pool === pool) store.pickedRef = candidate;
|
|
772
779
|
if (pool.state.failCounts) pool.state.failCounts.delete(candidate);
|
|
@@ -788,6 +795,8 @@ export function apply(ctx, config = {}) {
|
|
|
788
795
|
const envVal = envValue(candidate);
|
|
789
796
|
if (envVal !== undefined) {
|
|
790
797
|
pool.state.lastUsed = candidate;
|
|
798
|
+
if (!pool.state.lastUsedAt) pool.state.lastUsedAt = new Map();
|
|
799
|
+
pool.state.lastUsedAt.set(candidate, now);
|
|
791
800
|
const store = dispatchStorage.getStore();
|
|
792
801
|
if (store && store.pool === pool) store.pickedRef = candidate;
|
|
793
802
|
if (pool.state.failCounts) pool.state.failCounts.delete(candidate);
|
|
@@ -845,12 +854,11 @@ export function apply(ctx, config = {}) {
|
|
|
845
854
|
// the resolve patch hands out the next key on each dispatch.
|
|
846
855
|
function rotate(options, pool) {
|
|
847
856
|
return (async function* () {
|
|
848
|
-
const
|
|
857
|
+
const runtime0 = buildRuntime();
|
|
858
|
+
const { switchCodes, cooldownMs, maxCooldownMs, switchNotify, rateLimitThreshold } = runtime0;
|
|
849
859
|
let lastFailure = null;
|
|
850
860
|
const reqStore = { pool, pickedRef: undefined, startMs: Date.now() };
|
|
851
861
|
_rotateStartMs = reqStore.startMs;
|
|
852
|
-
|
|
853
|
-
const runtime0 = buildRuntime();
|
|
854
862
|
let attemptList = (pool.weightedRefs ?? pool.refs).slice();
|
|
855
863
|
if (runtime0.concurrencyLimit > 0 && concurrencyTracker.isEnabled()) {
|
|
856
864
|
// #193: prefer least-loaded key within limit
|
|
@@ -936,8 +944,8 @@ export function apply(ctx, config = {}) {
|
|
|
936
944
|
lastFailure = chunk;
|
|
937
945
|
console.warn(`[dsh-key-rotation] ${options.provider}: key ${String(activeRef ?? '?')} failed (${String(code)} ${String(message).slice(0, 100)}) - next key`);
|
|
938
946
|
// #216: per-switch webhook (opt-in switchNotify), deduped per provider
|
|
939
|
-
if (
|
|
940
|
-
notifySwitch(
|
|
947
|
+
if (switchNotify && activeRef) {
|
|
948
|
+
notifySwitch(runtime0, pool, {
|
|
941
949
|
provider: options.provider,
|
|
942
950
|
from: activeRef,
|
|
943
951
|
code: String(code ?? 'UNKNOWN'),
|
|
@@ -948,6 +956,7 @@ export function apply(ctx, config = {}) {
|
|
|
948
956
|
break;
|
|
949
957
|
}
|
|
950
958
|
// cost tracking if provider returns usage.cost
|
|
959
|
+
const todayIso = activeRef ? new Date().toISOString().slice(0, 10) : undefined;
|
|
951
960
|
if (chunk.usage?.cost != null && activeRef) {
|
|
952
961
|
const c = Number(chunk.usage.cost);
|
|
953
962
|
if (!isNaN(c)) {
|
|
@@ -955,18 +964,16 @@ export function apply(ctx, config = {}) {
|
|
|
955
964
|
pool.state.costPerKey.set(activeRef, (pool.state.costPerKey.get(activeRef) ?? 0) + c);
|
|
956
965
|
// #208: cost per day per key (mirrors usageDays) for budget checks
|
|
957
966
|
if (!pool.state.costDays) pool.state.costDays = new Map();
|
|
958
|
-
const cday = new Date().toISOString().slice(0, 10);
|
|
959
967
|
const cMap = pool.state.costDays.get(activeRef) || new Map();
|
|
960
|
-
cMap.set(
|
|
968
|
+
cMap.set(todayIso, (cMap.get(todayIso) ?? 0) + c);
|
|
961
969
|
pool.state.costDays.set(activeRef, cMap);
|
|
962
970
|
}
|
|
963
971
|
}
|
|
964
972
|
// Usage by day (#119)
|
|
965
973
|
if (activeRef) {
|
|
966
974
|
if (!pool.state.usageDays) pool.state.usageDays = new Map();
|
|
967
|
-
const day = new Date().toISOString().slice(0, 10);
|
|
968
975
|
const dayMap = pool.state.usageDays.get(activeRef) || new Map();
|
|
969
|
-
dayMap.set(
|
|
976
|
+
dayMap.set(todayIso, (dayMap.get(todayIso) ?? 0) + 1);
|
|
970
977
|
pool.state.usageDays.set(activeRef, dayMap);
|
|
971
978
|
}
|
|
972
979
|
// Per-model request detail (#121)
|
|
@@ -981,7 +988,6 @@ export function apply(ctx, config = {}) {
|
|
|
981
988
|
// We do NOT re-run this (already successful) request — that would double-send.
|
|
982
989
|
const rate = extractRateLimit(chunk?.metadata?.headers ?? chunk?.headers);
|
|
983
990
|
if (rate && activeRef) {
|
|
984
|
-
const { rateLimitThreshold } = buildRuntime();
|
|
985
991
|
if (isRateLimited(rate, rateLimitThreshold ?? 0.1)) {
|
|
986
992
|
const cool = rate.reset && rate.reset > Date.now() ? (rate.reset - Date.now()) : pool.cooldownMs;
|
|
987
993
|
recordFailure(pool, activeRef, Date.now(), cool, pool.maxCooldownMs);
|
|
@@ -1010,8 +1016,8 @@ export function apply(ctx, config = {}) {
|
|
|
1010
1016
|
pool.state.lastSwitchAt = Date.now();
|
|
1011
1017
|
lastFailure = finishError(e?.code ?? 'TRANSPORT', String(e?.message ?? e));
|
|
1012
1018
|
console.warn(`[dsh-key-rotation] ${options.provider}: key ${String(activeRef ?? '?')} stream threw ${String(e?.code ?? e?.message ?? e)} - failover to next key`);
|
|
1013
|
-
if (
|
|
1014
|
-
notifySwitch(
|
|
1019
|
+
if (switchNotify && activeRef) {
|
|
1020
|
+
notifySwitch(runtime0, pool, {
|
|
1015
1021
|
provider: options.provider,
|
|
1016
1022
|
from: activeRef,
|
|
1017
1023
|
code: String(e?.code ?? 'TRANSPORT'),
|
|
@@ -1033,11 +1039,11 @@ export function apply(ctx, config = {}) {
|
|
|
1033
1039
|
pool.state.lastExhaustionAt = Date.now();
|
|
1034
1040
|
pool.state.exhaustionCount = (pool.state.exhaustionCount ?? 0) + 1;
|
|
1035
1041
|
console.warn(`[dsh-key-rotation] ${options.provider}: pool exhausted — all ${pool.refs.length} keys cooling`);
|
|
1042
|
+
const runtime = buildRuntime();
|
|
1036
1043
|
// notify via extracted helper (see notifyExhaustion above)
|
|
1037
|
-
notifyExhaustion(
|
|
1044
|
+
notifyExhaustion(runtime, pool, { provider: options.provider });
|
|
1038
1045
|
|
|
1039
1046
|
// #194: cross-provider cascade failover (guarded against infinite recursion)
|
|
1040
|
-
const runtime = buildRuntime();
|
|
1041
1047
|
if (!options.__isCascade && Array.isArray(runtime.cascade) && runtime.cascade.length > 0) {
|
|
1042
1048
|
const pools = runtime.providerToPool;
|
|
1043
1049
|
const fb = pickCascadeFallback(options.provider, runtime, pools);
|
|
@@ -1078,10 +1084,10 @@ export function apply(ctx, config = {}) {
|
|
|
1078
1084
|
json(res, 403, { error: { code: 'forbidden', message: 'dsh-key-rotation: status is local-only' } });
|
|
1079
1085
|
return;
|
|
1080
1086
|
}
|
|
1081
|
-
const
|
|
1087
|
+
const runtime = buildRuntime();
|
|
1088
|
+
const { poolByRef, providerTags, providerBudgets, latencySloMs } = runtime;
|
|
1082
1089
|
const base = ctx.get('credentials');
|
|
1083
1090
|
const now = Date.now();
|
|
1084
|
-
const latencySloMs = buildRuntime().latencySloMs;
|
|
1085
1091
|
const seen = new Set();
|
|
1086
1092
|
const providers = [];
|
|
1087
1093
|
for (const pool of poolByRef.values()) {
|
|
@@ -1148,7 +1154,7 @@ export function apply(ctx, config = {}) {
|
|
|
1148
1154
|
lastSwitchAt: pool.state.lastSwitchAt ?? null,
|
|
1149
1155
|
lastExhaustionAt: pool.state.lastExhaustionAt ?? null,
|
|
1150
1156
|
exhaustionCount: pool.state.exhaustionCount ?? 0,
|
|
1151
|
-
totalUsage:
|
|
1157
|
+
totalUsage: (() => { let s = 0; if (pool.state.usageCounts) for (const v of pool.state.usageCounts.values()) s += v; return s; })(),
|
|
1152
1158
|
// #225: aggregate p95 across the pool's keys
|
|
1153
1159
|
p95: (() => {
|
|
1154
1160
|
const vals = (pool.refs ?? []).map((r) => latencyHistogram.snapshot(r)).filter((s) => s && s.p95 != null).map((s) => s.p95);
|
package/lib/pool.js
CHANGED
|
@@ -248,14 +248,23 @@ export function computeHealthScore(state) {
|
|
|
248
248
|
/** Extract rate-limit info from an object that may carry response headers. */
|
|
249
249
|
export function extractRateLimit(headers) {
|
|
250
250
|
if (!headers || typeof headers !== 'object') return null;
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
251
|
+
let remaining, limit, reset;
|
|
252
|
+
for (const k in headers) {
|
|
253
|
+
const lk = k.length;
|
|
254
|
+
if (lk === 21 || lk === 16 || lk === 17) {
|
|
255
|
+
const lower = k.toLowerCase();
|
|
256
|
+
if (lower === 'x-ratelimit-remaining') {
|
|
257
|
+
const v = headers[k];
|
|
258
|
+
if (v != null) remaining = Number(String(v));
|
|
259
|
+
} else if (lower === 'x-ratelimit-limit') {
|
|
260
|
+
const v = headers[k];
|
|
261
|
+
if (v != null) limit = Number(String(v));
|
|
262
|
+
} else if (lower === 'x-ratelimit-reset') {
|
|
263
|
+
const v = headers[k];
|
|
264
|
+
if (v != null) reset = Number(String(v));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
259
268
|
if (remaining === undefined && limit === undefined) return null;
|
|
260
269
|
return { remaining, limit, reset };
|
|
261
270
|
}
|
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
|
+
}
|