@goodandready/dsh-key-rotation 0.8.57 → 0.8.58
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/CHANGELOG.md +14 -0
- package/lib/index.js +7 -4
- package/lib/lifecycle.js +12 -1
- package/lib/ops-status.js +3 -0
- package/lib/rotate.js +21 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.58 - 2026-10-13
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Early-Warning Notification Dispatch (#477)**:
|
|
7
|
+
- Connected `notifyEarlyWarning` during key penalization in `lib/rotate.js` when healthy pool capacity drops to or below `earlyWarningKeyThreshold`.
|
|
8
|
+
- Added export `notifyEarlyWarning` in `lib/index.js` and wired dependency through `createRotate`.
|
|
9
|
+
- **Proactive Health Probing Logging (#478)**:
|
|
10
|
+
- Replaced empty catch block in `lib/lifecycle.js` `setupProactiveHealthProbeEffect` with structured debug logging (`logger?.debug`), eliminating silent swallowed exceptions.
|
|
11
|
+
- **Key Drain Lifecycle Teardown (#479)**:
|
|
12
|
+
- Registered `setupKeyDrainEffect` in `lib/lifecycle.js` to ensure module-level `drainingKeys` map is wiped cleanly on plugin teardown/dispose via `clearAllDraining()`.
|
|
13
|
+
- **Quota Reset Window Schema Validation (#480)**:
|
|
14
|
+
- Connected `QUOTA_WINDOW_TYPES` into `SettingsSchema` via `Schema.union(QUOTA_WINDOW_TYPES).default('midnight_utc')`.
|
|
15
|
+
- Connected `isBlockedUntilReset` in `lib/ops-status.js` to accurately expose calendar-based reset lock status.
|
|
16
|
+
|
|
3
17
|
## 0.8.57 - 2026-10-12
|
|
4
18
|
|
|
5
19
|
### Added
|
package/lib/index.js
CHANGED
|
@@ -31,7 +31,8 @@ import { NotifyQueue } from './notify-queue.js';
|
|
|
31
31
|
import { classifyFailure } from './error-taxonomy.js';
|
|
32
32
|
import { registerOpsRoutes } from './routes-ops.js';
|
|
33
33
|
import { registerPluginUpdater } from './plugin-updater.js';
|
|
34
|
-
import { notifySwitch, notifyExhaustion, pushEvent } from './notify-events.js';
|
|
34
|
+
import { notifySwitch, notifyExhaustion, notifyEarlyWarning, pushEvent } from './notify-events.js';
|
|
35
|
+
import { QUOTA_WINDOW_TYPES } from './quota-window.js';
|
|
35
36
|
import { getLogger } from './logger.js';
|
|
36
37
|
import { createSandboxService } from './sandbox-service.js';
|
|
37
38
|
import { checkBudgetAndHealthAlerts } from './budget-monitor.js';
|
|
@@ -44,12 +45,12 @@ import {
|
|
|
44
45
|
formatModelQuotaExhaustion,
|
|
45
46
|
} from './model-quota.js';
|
|
46
47
|
import { LOCAL_QUOTA_CODE } from './rotate.js';
|
|
47
|
-
import { setupIdleHealEffect, setupAutoUnbreakEffect, setupPersistence, setupProactiveHealthProbeEffect } from './lifecycle.js';
|
|
48
|
+
import { setupIdleHealEffect, setupAutoUnbreakEffect, setupPersistence, setupProactiveHealthProbeEffect, setupKeyDrainEffect } from './lifecycle.js';
|
|
48
49
|
|
|
49
50
|
export const name = '@goodandready/dsh-key-rotation';
|
|
50
51
|
export const inject = ['llm', 'webServer', 'settings', 'credentials'];
|
|
51
52
|
export { keyTail, KEY_TAIL_CHARS, KEY_TAIL_PLACEHOLDER, isLoopbackAddress, isTrustedBridgeRequest, DEFAULT_SWITCH_CODES, isSwitchableError, formatExhaustionMessage, getRuntime };
|
|
52
|
-
export { notifySwitch, notifyExhaustion };
|
|
53
|
+
export { notifySwitch, notifyExhaustion, notifyEarlyWarning };
|
|
53
54
|
|
|
54
55
|
const NS = 'dsh-key-rotation';
|
|
55
56
|
|
|
@@ -88,7 +89,7 @@ const SettingsSchema = Schema.object({
|
|
|
88
89
|
model: Schema.string(),
|
|
89
90
|
})).default([]),
|
|
90
91
|
quotaResetWindow: Schema.object({
|
|
91
|
-
type: Schema.
|
|
92
|
+
type: Schema.union(QUOTA_WINDOW_TYPES).default('midnight_utc'),
|
|
92
93
|
hour: Schema.number().default(0),
|
|
93
94
|
}),
|
|
94
95
|
rateLimitThreshold: Schema.number().default(0.1),
|
|
@@ -206,6 +207,7 @@ export function apply(ctx, config = {}) {
|
|
|
206
207
|
|
|
207
208
|
setupIdleHealEffect(ctx, getConfig, buildRuntime);
|
|
208
209
|
setupProactiveHealthProbeEffect(ctx, getConfig, buildRuntime, ensureSandboxRunner, logger);
|
|
210
|
+
setupKeyDrainEffect(ctx);
|
|
209
211
|
|
|
210
212
|
const poolState = new Map();
|
|
211
213
|
const { schedulePersist, persistenceSnapshot } = setupPersistence(ctx, {
|
|
@@ -421,6 +423,7 @@ export function apply(ctx, config = {}) {
|
|
|
421
423
|
pushEvent,
|
|
422
424
|
notifySwitch: (runtime, pool, info) => notifySwitch(runtime, pool, info, { webhookSender, notifyQueue: moduleNotifyQueue, now: () => Date.now() }),
|
|
423
425
|
notifyExhaustion: (runtime, pool, options) => notifyExhaustion(runtime, pool, options, { webhookSender, notifyQueue: moduleNotifyQueue, now: () => Date.now() }),
|
|
426
|
+
notifyEarlyWarning: (runtime, pool, info) => notifyEarlyWarning(runtime, pool, info, { webhookSender, notifyQueue: moduleNotifyQueue, now: () => Date.now() }),
|
|
424
427
|
recordLatency,
|
|
425
428
|
latencyHistogram,
|
|
426
429
|
concurrencyTracker,
|
package/lib/lifecycle.js
CHANGED
|
@@ -4,6 +4,7 @@ import { decryptSecret } from './crypto-storage.js';
|
|
|
4
4
|
import { StatePersistence, resolveStatePath } from './persistence.js';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { envValue } from './pool.js';
|
|
7
|
+
import { clearAllDraining } from './key-drain.js';
|
|
7
8
|
|
|
8
9
|
export function setupIdleHealEffect(ctx, getConfig, buildRuntime, timers = globalThis) {
|
|
9
10
|
return ctx.effect(() => {
|
|
@@ -209,7 +210,9 @@ export function setupProactiveHealthProbeEffect(ctx, getConfig, buildRuntime, en
|
|
|
209
210
|
}
|
|
210
211
|
logger?.warn?.(`[dsh-key-rotation] proactive-probe: restored key ${ref} in pool ${pool.base}`);
|
|
211
212
|
}
|
|
212
|
-
} catch (
|
|
213
|
+
} catch (err) {
|
|
214
|
+
logger?.debug?.(`[dsh-key-rotation] proactive-probe failed for ${ref}: ${err?.message || err}`);
|
|
215
|
+
}
|
|
213
216
|
}
|
|
214
217
|
}
|
|
215
218
|
} catch (e) {
|
|
@@ -244,3 +247,11 @@ export function setupProactiveHealthProbeEffect(ctx, getConfig, buildRuntime, en
|
|
|
244
247
|
};
|
|
245
248
|
}, 'dsh-key-rotation: proactive health probing');
|
|
246
249
|
}
|
|
250
|
+
|
|
251
|
+
export function setupKeyDrainEffect(ctx) {
|
|
252
|
+
return ctx.effect(() => {
|
|
253
|
+
return () => {
|
|
254
|
+
clearAllDraining();
|
|
255
|
+
};
|
|
256
|
+
}, 'dsh-key-rotation: clear draining keys on teardown');
|
|
257
|
+
}
|
package/lib/ops-status.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
isLoopbackAddress,
|
|
16
16
|
} from './pool.js';
|
|
17
17
|
import { bucketInfo, tpmInfo } from './bucket.js';
|
|
18
|
+
import { isBlockedUntilReset } from './quota-window.js';
|
|
18
19
|
import { sanitizeSnapshot } from './sanitize-snapshot.js';
|
|
19
20
|
import {
|
|
20
21
|
STATUS_PATH,
|
|
@@ -92,6 +93,7 @@ ctx.effect(() => ctx.webServer.register({
|
|
|
92
93
|
/* describe is optional — the card falls back to editable */
|
|
93
94
|
}
|
|
94
95
|
const until = pool.state.failedUntil?.get(ref);
|
|
96
|
+
const quotaWin = pool.state.quotaWindows?.get(ref);
|
|
95
97
|
const modelQuota = getModelQuotaStatus(pool, ref, now);
|
|
96
98
|
const activeModelPools = runtime?.index ? modelPoolsForRef(runtime.index, ref) : [];
|
|
97
99
|
keys.push({
|
|
@@ -102,6 +104,7 @@ ctx.effect(() => ctx.webServer.register({
|
|
|
102
104
|
writable,
|
|
103
105
|
active: pool.state.lastUsed === ref,
|
|
104
106
|
cooldownMsLeft: until !== undefined && until > now ? until - now : 0,
|
|
107
|
+
blockedUntilReset: isBlockedUntilReset(until, quotaWin, now),
|
|
105
108
|
// #210: RPM capacity snapshot (null when rpmLimit is off)
|
|
106
109
|
rpm: bucketInfo(pool.state.rpmWindows, ref, pool.rpmLimit, now),
|
|
107
110
|
// TPM capacity snapshot (null when tpmLimit is off)
|
package/lib/rotate.js
CHANGED
|
@@ -19,6 +19,7 @@ import { pickCascadeFallback, CASCADE_MAX_DEPTH } from './cascade.js';
|
|
|
19
19
|
import { createRequestBudget, BUDGET_EXHAUSTED_TIMEOUT, BUDGET_EXHAUSTED_ATTEMPTS } from './request-budget.js';
|
|
20
20
|
import { createSelectionTrace } from './selection-trace.js';
|
|
21
21
|
import { isKeyDraining } from './key-drain.js';
|
|
22
|
+
import { notifyEarlyWarning as defaultNotifyEarlyWarning } from './notify-events.js';
|
|
22
23
|
import { nextQuotaReset, poolResetAt } from './quota-window.js';
|
|
23
24
|
import { tpmRecord } from './bucket.js';
|
|
24
25
|
import {
|
|
@@ -64,6 +65,7 @@ export function createRotate(deps) {
|
|
|
64
65
|
pushEvent,
|
|
65
66
|
notifySwitch,
|
|
66
67
|
notifyExhaustion,
|
|
68
|
+
notifyEarlyWarning,
|
|
67
69
|
recordLatency,
|
|
68
70
|
latencyHistogram,
|
|
69
71
|
concurrencyTracker,
|
|
@@ -77,6 +79,7 @@ export function createRotate(deps) {
|
|
|
77
79
|
now = nowMono,
|
|
78
80
|
} = deps;
|
|
79
81
|
|
|
82
|
+
const effectiveNotifyEarlyWarning = notifyEarlyWarning ?? defaultNotifyEarlyWarning;
|
|
80
83
|
const logWarn = (msg) => (logger?.warn ? logger.warn(msg) : null);
|
|
81
84
|
|
|
82
85
|
function rotate(options, pool) {
|
|
@@ -204,6 +207,24 @@ export function createRotate(deps) {
|
|
|
204
207
|
if (typeof schedulePersist === 'function') {
|
|
205
208
|
try { schedulePersist(); } catch (_) { /* best-effort persistence */ }
|
|
206
209
|
}
|
|
210
|
+
if (runtime0.earlyWarningNotify && effectiveNotifyEarlyWarning) {
|
|
211
|
+
const total = pool.refs?.length || 1;
|
|
212
|
+
let available = 0;
|
|
213
|
+
const at = now();
|
|
214
|
+
for (const r of (pool.refs || [])) {
|
|
215
|
+
if (!isKeyRevoked(pool, r) && !isKeyPaused(pool, r) && !isKeyBroken(pool, r, at) && ((pool.state?.failedUntil?.get(r) ?? 0) <= at)) {
|
|
216
|
+
available++;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const threshold = Number.isFinite(runtime0.earlyWarningKeyThreshold) ? runtime0.earlyWarningKeyThreshold : 0.25;
|
|
220
|
+
if (total > 0 && (available / total) <= threshold) {
|
|
221
|
+
effectiveNotifyEarlyWarning(runtime0, pool, {
|
|
222
|
+
provider: options.provider,
|
|
223
|
+
availableCount: available,
|
|
224
|
+
totalCount: total,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
207
228
|
if (!pool.state.authFailCounts) pool.state.authFailCounts = new Map();
|
|
208
229
|
if (!pool.state.brokenUntil) pool.state.brokenUntil = new Map();
|
|
209
230
|
const _cStr = String(errCode ?? '');
|
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.58",
|
|
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": [
|