@goodandready/dsh-key-rotation 0.8.78 → 0.8.80
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/concurrency.js +2 -115
- package/lib/config-compat.js +18 -1
- package/lib/core/concurrency.js +115 -0
- package/lib/core/index.js +6 -0
- package/lib/core/pool.js +2 -0
- package/lib/core/quota.js +39 -0
- package/lib/core/rotate.js +2 -0
- package/lib/core/state-machine.js +267 -0
- package/lib/http-bridge.js +2 -0
- package/lib/index.js +41 -27
- package/lib/key-state-machine.js +2 -0
- package/lib/ops-balance.js +2 -147
- package/lib/ops-config-io.js +2 -456
- package/lib/ops-keys.js +2 -567
- package/lib/ops-metrics.js +2 -92
- package/lib/ops-paths.js +2 -24
- package/lib/ops-quick-action.js +2 -168
- package/lib/ops-status.js +2 -269
- package/lib/ops-telemetry.js +2 -276
- package/lib/ops-test.js +2 -255
- package/lib/ops-webhook.js +2 -218
- package/lib/persistence.js +69 -10
- package/lib/pool-state.js +3 -0
- package/lib/pool.js +10 -15
- package/lib/quota.js +2 -39
- package/lib/routes/balance.js +147 -0
- package/lib/routes/config.js +456 -0
- package/lib/routes/guard.js +13 -0
- package/lib/routes/index.js +43 -0
- package/lib/routes/keys.js +567 -0
- package/lib/routes/metrics.js +92 -0
- package/lib/routes/paths.js +24 -0
- package/lib/routes/quick-action.js +168 -0
- package/lib/routes/status.js +269 -0
- package/lib/routes/telemetry.js +276 -0
- package/lib/routes/testing.js +255 -0
- package/lib/routes/webhook.js +218 -0
- package/lib/routes-ops.js +2 -29
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -11,7 +11,8 @@ import { decryptSecret } from './crypto-storage.js';
|
|
|
11
11
|
|
|
12
12
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
13
13
|
import Schema from '@deepseek-ai/schemastery';
|
|
14
|
-
import { createConfigReader, createProviderProfilesReader } from './config-compat.js';
|
|
14
|
+
import { createConfigReader, createProviderProfilesReader, unwrapVolatileConfig } from './config-compat.js';
|
|
15
|
+
import { KeyStateMachine, KEY_STATES, VALID_TRANSITIONS } from './key-state-machine.js';
|
|
15
16
|
import {
|
|
16
17
|
keyTail, KEY_TAIL_CHARS, KEY_TAIL_PLACEHOLDER, isLoopbackAddress, isTrustedBridgeRequest, isDesktopShellRequest, SWITCHABLE_MESSAGE_PATTERN,
|
|
17
18
|
DEFAULT_SWITCH_CODES, isValidRef, pickNext, applyCooldown, recordFailure,
|
|
@@ -54,15 +55,18 @@ import { LOCAL_QUOTA_CODE } from './rotate.js';
|
|
|
54
55
|
import { setupIdleHealEffect, setupAutoUnbreakEffect, setupPersistence, setupProactiveHealthProbeEffect, setupKeyDrainEffect } from './lifecycle.js';
|
|
55
56
|
|
|
56
57
|
export const name = '@goodandready/dsh-key-rotation';
|
|
57
|
-
export const inject = ['llm'
|
|
58
|
+
export const inject = ['llm'];
|
|
58
59
|
export { keyTail, KEY_TAIL_CHARS, KEY_TAIL_PLACEHOLDER, isLoopbackAddress, isTrustedBridgeRequest, isDesktopShellRequest, DEFAULT_SWITCH_CODES, isSwitchableError, formatExhaustionMessage, getRuntime };
|
|
59
60
|
export { notifySwitch, notifyExhaustion, notifyEarlyWarning };
|
|
60
61
|
export { classifyResponseError } from './errors.js';
|
|
61
62
|
export { SessionRouter, defaultSessionRouter, unstickFailedKey } from './session-router.js';
|
|
62
63
|
export { isContentChunk } from './rotate.js';
|
|
63
64
|
export { ModelCatalog, defaultModelCatalog } from './model-catalog.js';
|
|
65
|
+
export { KeyStateMachine, KEY_STATES, VALID_TRANSITIONS } from './key-state-machine.js';
|
|
66
|
+
export { unwrapVolatileConfig } from './config-compat.js';
|
|
64
67
|
export { QuotaRunway, defaultQuotaRunway, estimateRunway } from './quota-runway.js';
|
|
65
68
|
export * as transports from './transports/index.js';
|
|
69
|
+
export * as core from './core/index.js';
|
|
66
70
|
|
|
67
71
|
const NS = 'dsh-key-rotation';
|
|
68
72
|
|
|
@@ -221,7 +225,6 @@ export function apply(ctx, config = {}) {
|
|
|
221
225
|
const { ensureSandboxRunner, probeRef, lastTestCache } = sandboxService;
|
|
222
226
|
|
|
223
227
|
const readProfiles = createProviderProfilesReader(ctx, PIAI_NS);
|
|
224
|
-
registerConfigBridge(ctx, () => buildRuntime().cloneIds);
|
|
225
228
|
ensureSandboxRunner(ctx);
|
|
226
229
|
|
|
227
230
|
setupIdleHealEffect(ctx, getConfig, buildRuntime);
|
|
@@ -243,6 +246,40 @@ export function apply(ctx, config = {}) {
|
|
|
243
246
|
|
|
244
247
|
setupAutoUnbreakEffect(ctx, getConfig, buildRuntime, ensureSandboxRunner, logger);
|
|
245
248
|
|
|
249
|
+
// Dynamic webServer registration (#528)
|
|
250
|
+
const setupWebRoutes = (webCtx = ctx) => {
|
|
251
|
+
const target = webCtx?.webServer ? webCtx : ctx;
|
|
252
|
+
registerConfigBridge(target, () => buildRuntime().cloneIds);
|
|
253
|
+
registerOpsRoutes(target, {
|
|
254
|
+
buildRuntime,
|
|
255
|
+
latencyHistogram,
|
|
256
|
+
lastTestCache,
|
|
257
|
+
ensureSandboxRunner,
|
|
258
|
+
poolState,
|
|
259
|
+
getRotationDisabled: () => rotationDisabled,
|
|
260
|
+
setRotationDisabled: (v) => { rotationDisabled = v; },
|
|
261
|
+
circuitBreaker: moduleBreaker,
|
|
262
|
+
notifyQueue: moduleNotifyQueue,
|
|
263
|
+
quotaStore,
|
|
264
|
+
keyDrainManager,
|
|
265
|
+
selectionTraceStore,
|
|
266
|
+
diagBuffer,
|
|
267
|
+
});
|
|
268
|
+
if (typeof target.webServer?.register === 'function') {
|
|
269
|
+
registerPluginUpdater(target, {
|
|
270
|
+
endpoint: '/api/dsh-key-rotation/update',
|
|
271
|
+
packageName: '@goodandready/dsh-key-rotation',
|
|
272
|
+
manifestUrl: new URL('../package.json', import.meta.url),
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
if (ctx.webServer) {
|
|
278
|
+
setupWebRoutes(ctx);
|
|
279
|
+
} else if (typeof ctx.inject === 'function') {
|
|
280
|
+
ctx.inject(['webServer'], setupWebRoutes);
|
|
281
|
+
}
|
|
282
|
+
|
|
246
283
|
// Periodic sweep of expired cooldowns
|
|
247
284
|
ctx.effect(() => {
|
|
248
285
|
const id = setInterval(() => {
|
|
@@ -468,30 +505,7 @@ export function apply(ctx, config = {}) {
|
|
|
468
505
|
logger,
|
|
469
506
|
});
|
|
470
507
|
|
|
471
|
-
|
|
472
|
-
buildRuntime,
|
|
473
|
-
latencyHistogram,
|
|
474
|
-
lastTestCache,
|
|
475
|
-
ensureSandboxRunner,
|
|
476
|
-
poolState,
|
|
477
|
-
getRotationDisabled: () => rotationDisabled,
|
|
478
|
-
setRotationDisabled: (v) => { rotationDisabled = v; },
|
|
479
|
-
circuitBreaker: moduleBreaker,
|
|
480
|
-
notifyQueue: moduleNotifyQueue,
|
|
481
|
-
quotaStore,
|
|
482
|
-
keyDrainManager,
|
|
483
|
-
selectionTraceStore,
|
|
484
|
-
diagBuffer,
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
ctx.effect(() => {
|
|
488
|
-
if (typeof ctx.webServer?.register !== 'function') return;
|
|
489
|
-
registerPluginUpdater(ctx, {
|
|
490
|
-
endpoint: '/api/dsh-key-rotation/update',
|
|
491
|
-
packageName: '@goodandready/dsh-key-rotation',
|
|
492
|
-
manifestUrl: new URL('../package.json', import.meta.url),
|
|
493
|
-
});
|
|
494
|
-
}, 'dsh-key-rotation: plugin updater');
|
|
508
|
+
// routes registered dynamically above via setupWebRoutes
|
|
495
509
|
|
|
496
510
|
ctx.effect(() => ctx.on('llm/stream', (options, next) => {
|
|
497
511
|
if (options[MARKER]) return next();
|
package/lib/ops-balance.js
CHANGED
|
@@ -1,147 +1,2 @@
|
|
|
1
|
-
// lib/ops-balance.js —
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
json,
|
|
5
|
-
readJson,
|
|
6
|
-
} from './http-bridge.js';
|
|
7
|
-
import {
|
|
8
|
-
isTrustedBridgeRequest,
|
|
9
|
-
isValidRef,
|
|
10
|
-
} from './pool.js';
|
|
11
|
-
import { BALANCE_PATH } from './ops-paths.js';
|
|
12
|
-
import { inspectBalanceForRef } from './balance-inspector.js';
|
|
13
|
-
import { pushEvent } from './notify-events.js';
|
|
14
|
-
|
|
15
|
-
export function registerBalanceRoutes(ctx, deps) {
|
|
16
|
-
const {
|
|
17
|
-
poolState,
|
|
18
|
-
buildRuntime,
|
|
19
|
-
} = deps;
|
|
20
|
-
|
|
21
|
-
ctx.effect(() => ctx.webServer.register({
|
|
22
|
-
kind: 'exact',
|
|
23
|
-
path: BALANCE_PATH,
|
|
24
|
-
handler: async (req, res) => {
|
|
25
|
-
if (!isTrustedBridgeRequest(req)) {
|
|
26
|
-
json(res, 403, { ok: false, error: 'Forbidden' });
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const urlObj = new URL(req.url, 'http://127.0.0.1');
|
|
31
|
-
|
|
32
|
-
// GET: Query single key or provider cached balances
|
|
33
|
-
if (req.method === 'GET') {
|
|
34
|
-
const ref = urlObj.searchParams.get('ref');
|
|
35
|
-
const provider = urlObj.searchParams.get('provider');
|
|
36
|
-
|
|
37
|
-
if (ref) {
|
|
38
|
-
const runtime = buildRuntime();
|
|
39
|
-
const targetPool = provider ? runtime.pools?.find((p) => p.provider === provider) : null;
|
|
40
|
-
const pool = targetPool ?? runtime.poolByRef?.get(ref);
|
|
41
|
-
const cached = pool?.state?.balances?.get(ref) ?? null;
|
|
42
|
-
|
|
43
|
-
if (urlObj.searchParams.get('refresh') === 'true' && provider) {
|
|
44
|
-
const inspectRes = await inspectBalanceForRef(ctx, ref, provider);
|
|
45
|
-
if (inspectRes.ok) {
|
|
46
|
-
if (pool && pool.state) {
|
|
47
|
-
if (!pool.state.balances) pool.state.balances = new Map();
|
|
48
|
-
pool.state.balances.set(ref, { ...inspectRes, at: Date.now() });
|
|
49
|
-
|
|
50
|
-
// Check soft pause
|
|
51
|
-
applyBalanceState(pool, ref, inspectRes);
|
|
52
|
-
}
|
|
53
|
-
json(res, 200, { ok: true, ref, balance: inspectRes });
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
json(res, 200, { ok: false, ref, error: inspectRes.error, cached });
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
json(res, 200, { ok: true, ref, balance: cached });
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Return all cached balances
|
|
65
|
-
const out = {};
|
|
66
|
-
if (poolState && typeof poolState.forEach === 'function') {
|
|
67
|
-
poolState.forEach((st, provName) => {
|
|
68
|
-
if (st && st.balances instanceof Map) {
|
|
69
|
-
for (const [k, v] of st.balances) {
|
|
70
|
-
out[k] = v;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
json(res, 200, { ok: true, balances: out });
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// POST: Trigger refresh of balances for a provider or key list
|
|
80
|
-
if (req.method === 'POST') {
|
|
81
|
-
const body = await readJson(req);
|
|
82
|
-
const provider = body?.provider;
|
|
83
|
-
const ref = body?.ref;
|
|
84
|
-
const testValue = body?.value;
|
|
85
|
-
|
|
86
|
-
if (ref && provider) {
|
|
87
|
-
const inspectRes = await inspectBalanceForRef(ctx, ref, provider, { testValue });
|
|
88
|
-
const runtime = buildRuntime();
|
|
89
|
-
const pool = runtime.pools?.find((p) => p.provider === provider) ?? runtime.poolByRef?.get(ref);
|
|
90
|
-
|
|
91
|
-
if (inspectRes.ok && pool && pool.state) {
|
|
92
|
-
if (!pool.state.balances) pool.state.balances = new Map();
|
|
93
|
-
pool.state.balances.set(ref, { ...inspectRes, at: Date.now() });
|
|
94
|
-
applyBalanceState(pool, ref, inspectRes);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
json(res, inspectRes.ok ? 200 : 400, { ok: inspectRes.ok, ref, balance: inspectRes });
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (provider) {
|
|
102
|
-
const runtime = buildRuntime();
|
|
103
|
-
const pool = runtime.pools?.find((p) => p.provider === provider);
|
|
104
|
-
if (!pool || !Array.isArray(pool.refs) || pool.refs.length === 0) {
|
|
105
|
-
json(res, 404, { ok: false, error: `Provider '${provider}' not found or has no keys` });
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const results = {};
|
|
110
|
-
for (const kRef of pool.refs) {
|
|
111
|
-
const r = await inspectBalanceForRef(ctx, kRef, provider);
|
|
112
|
-
results[kRef] = r;
|
|
113
|
-
if (r.ok && pool.state) {
|
|
114
|
-
if (!pool.state.balances) pool.state.balances = new Map();
|
|
115
|
-
pool.state.balances.set(kRef, { ...r, at: Date.now() });
|
|
116
|
-
applyBalanceState(pool, kRef, r);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
json(res, 200, { ok: true, provider, balances: results });
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
json(res, 400, { ok: false, error: 'ref or provider required' });
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
json(res, 405, { ok: false, error: 'Method Not Allowed' });
|
|
128
|
-
},
|
|
129
|
-
}), 'dsh-key-rotation: balance routes');
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function applyBalanceState(pool, ref, balanceRes) {
|
|
133
|
-
if (!pool || !pool.state) return;
|
|
134
|
-
const isZero = balanceRes.isAvailable === false || (typeof balanceRes.total === 'number' && balanceRes.total <= 0);
|
|
135
|
-
|
|
136
|
-
if (isZero) {
|
|
137
|
-
if (!pool.state.pausedRefs) pool.state.pausedRefs = new Set();
|
|
138
|
-
if (pool.state.pausedRefs instanceof Set) pool.state.pausedRefs.add(ref);
|
|
139
|
-
if (!pool.state.pauseReasons) pool.state.pauseReasons = new Map();
|
|
140
|
-
pool.state.pauseReasons.set(ref, 'EMPTY_BALANCE_PAUSED');
|
|
141
|
-
pushEvent(pool, ref, 'BALANCE_EMPTY', 0);
|
|
142
|
-
} else if (pool.state.pauseReasons?.get?.(ref) === 'EMPTY_BALANCE_PAUSED') {
|
|
143
|
-
if (pool.state.pausedRefs instanceof Set) pool.state.pausedRefs.delete(ref);
|
|
144
|
-
pool.state.pauseReasons.delete(ref);
|
|
145
|
-
pushEvent(pool, ref, 'BALANCE_RESTORED', 0);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
1
|
+
// lib/ops-balance.js — Re-export facade for lib/routes/ (#522).
|
|
2
|
+
export * from "./routes/balance.js";
|