@goodandready/dsh-key-rotation 0.8.12 → 0.8.13
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 +196 -0
- package/lib/budget-monitor.js +123 -0
- package/lib/client.js +22 -13
- package/lib/index.js +197 -559
- package/lib/lifecycle.js +122 -0
- package/lib/logger.js +13 -0
- package/lib/ops-status.js +1 -1
- package/lib/ops-webhook.js +3 -3
- package/lib/pool-builder.js +86 -0
- package/lib/rotate.js +10 -7
- package/lib/sandbox-service.js +55 -0
- package/package.json +4 -1
package/lib/lifecycle.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// lib/lifecycle.js — background timers and lifecycle effects
|
|
2
|
+
import { healIdleCooldowns, autoUnbreakBrokenKeys } from './heal.js';
|
|
3
|
+
import { StatePersistence, resolveStatePath } from './persistence.js';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
export function setupIdleHealEffect(ctx, getConfig, buildRuntime) {
|
|
7
|
+
return ctx.effect(() => {
|
|
8
|
+
const cfg = getConfig();
|
|
9
|
+
if (!cfg || cfg.selfHealCooldown === false) return () => {};
|
|
10
|
+
const timer = setInterval(() => {
|
|
11
|
+
try {
|
|
12
|
+
const c = getConfig();
|
|
13
|
+
if (!c || c.selfHealCooldown === false) return;
|
|
14
|
+
const idle = Number.isFinite(c.selfHealIdleMs) && c.selfHealIdleMs > 0 ? c.selfHealIdleMs : 3600000;
|
|
15
|
+
const providers = Array.isArray(c.providers) ? c.providers : [];
|
|
16
|
+
const pools = providers
|
|
17
|
+
.map((p) => buildRuntime().providerToPool.get(p.provider))
|
|
18
|
+
.filter(Boolean);
|
|
19
|
+
healIdleCooldowns(pools, idle);
|
|
20
|
+
} catch (_) { /* ponytail: never crash the timer */ }
|
|
21
|
+
}, 60000);
|
|
22
|
+
if (typeof timer.unref === 'function') timer.unref();
|
|
23
|
+
return () => clearInterval(timer);
|
|
24
|
+
}, 'dsh-key-rotation: self-healing idle');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function setupAutoUnbreakEffect(ctx, getConfig, buildRuntime, ensureSandboxRunner, logger) {
|
|
28
|
+
return ctx.effect(() => {
|
|
29
|
+
const cfg = getConfig();
|
|
30
|
+
const intervalMin = cfg?.selfHealingIntervalMinutes ?? 30;
|
|
31
|
+
if (!intervalMin || intervalMin <= 0) return () => {};
|
|
32
|
+
const intervalMs = intervalMin * 60 * 1000;
|
|
33
|
+
const timer = setInterval(async () => {
|
|
34
|
+
try {
|
|
35
|
+
const c = getConfig();
|
|
36
|
+
if (!c || !c.selfHealingIntervalMinutes || c.selfHealingIntervalMinutes <= 0) return;
|
|
37
|
+
const { pools } = buildRuntime();
|
|
38
|
+
const runner = ensureSandboxRunner(ctx);
|
|
39
|
+
await autoUnbreakBrokenKeys(pools, async (ref) => {
|
|
40
|
+
let val = (await ctx.credentials?.resolve?.(ref))?.value;
|
|
41
|
+
if (!val) return { ok: false };
|
|
42
|
+
return runner.probeModels(ref, val);
|
|
43
|
+
});
|
|
44
|
+
} catch (e) { logger?.warn?.('[dsh-key-rotation] auto-unbreak failed', e); }
|
|
45
|
+
}, intervalMs);
|
|
46
|
+
if (typeof timer.unref === 'function') timer.unref();
|
|
47
|
+
return () => clearInterval(timer);
|
|
48
|
+
}, 'dsh-key-rotation: auto-unbreak');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function setupPersistence(ctx, { cfg0, poolState, moduleBreaker, verboseLoggingOn, logger }) {
|
|
52
|
+
let statePersistence = null;
|
|
53
|
+
try {
|
|
54
|
+
const hostDirs = [
|
|
55
|
+
process.env.DSH_HOME,
|
|
56
|
+
process.cwd(),
|
|
57
|
+
].filter((d) => typeof d === 'string' && d.length > 0);
|
|
58
|
+
const resolvedPath = resolveStatePath({
|
|
59
|
+
configuredPath: cfg0.persistencePath,
|
|
60
|
+
dataDir: hostDirs[0],
|
|
61
|
+
});
|
|
62
|
+
if (cfg0.persistenceEnabled !== false && resolvedPath) {
|
|
63
|
+
statePersistence = new StatePersistence({ filePath: resolvedPath });
|
|
64
|
+
statePersistence.load().then((snap) => {
|
|
65
|
+
if (!snap) return;
|
|
66
|
+
try {
|
|
67
|
+
StatePersistence.restorePools(poolState, snap);
|
|
68
|
+
if (moduleBreaker && snap.circuit) moduleBreaker.restore(snap.circuit);
|
|
69
|
+
if (verboseLoggingOn?.()) {
|
|
70
|
+
logger?.warn?.(`[dsh-key-rotation] restored ${Object.keys(snap.pools ?? {}).length} pool state(s) from ${path.basename(resolvedPath)}`);
|
|
71
|
+
}
|
|
72
|
+
} catch (e) {
|
|
73
|
+
logger?.warn?.('[dsh-key-rotation] persistence restore failed', e?.message ?? e);
|
|
74
|
+
}
|
|
75
|
+
}).catch(() => {});
|
|
76
|
+
} else if (cfg0.persistenceEnabled !== false && !resolvedPath) {
|
|
77
|
+
logger?.warn?.('[dsh-key-rotation] persistence disabled: no data directory');
|
|
78
|
+
}
|
|
79
|
+
} catch (e) {
|
|
80
|
+
logger?.warn?.('[dsh-key-rotation] persistence init failed', e?.message ?? e);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function persistenceSnapshot() {
|
|
84
|
+
if (!statePersistence) return null;
|
|
85
|
+
return StatePersistence.serialize({
|
|
86
|
+
poolState,
|
|
87
|
+
circuitSnapshot: moduleBreaker ? moduleBreaker.snapshot() : {},
|
|
88
|
+
quotaSnapshot: {},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function schedulePersist() {
|
|
93
|
+
if (!statePersistence) return;
|
|
94
|
+
const snap = persistenceSnapshot();
|
|
95
|
+
if (snap) statePersistence.save(snap);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
ctx.effect(() => {
|
|
99
|
+
const timer = setInterval(() => {
|
|
100
|
+
try { schedulePersist(); }
|
|
101
|
+
catch (e) { logger?.warn?.('[dsh-key-rotation] periodic persist failed', e); }
|
|
102
|
+
}, 15000);
|
|
103
|
+
if (typeof timer.unref === 'function') timer.unref();
|
|
104
|
+
return () => {
|
|
105
|
+
clearInterval(timer);
|
|
106
|
+
try {
|
|
107
|
+
if (statePersistence) {
|
|
108
|
+
const snap = persistenceSnapshot();
|
|
109
|
+
if (snap) {
|
|
110
|
+
statePersistence.save(snap);
|
|
111
|
+
void statePersistence.flush();
|
|
112
|
+
}
|
|
113
|
+
statePersistence.dispose();
|
|
114
|
+
}
|
|
115
|
+
} catch (e) {
|
|
116
|
+
logger?.warn?.('[dsh-key-rotation] dispose persist failed', e);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}, 'dsh-key-rotation: state persistence');
|
|
120
|
+
|
|
121
|
+
return { schedulePersist, persistenceSnapshot };
|
|
122
|
+
}
|
package/lib/logger.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// lib/logger.js — safe Cordis logger wrapper
|
|
2
|
+
const noop = () => {};
|
|
3
|
+
const noopLogger = { warn: noop, info: noop, error: noop, debug: noop, log: noop };
|
|
4
|
+
|
|
5
|
+
export function getLogger(ctx, scope = 'dsh-key-rotation') {
|
|
6
|
+
if (typeof ctx?.logger === 'function') {
|
|
7
|
+
return ctx.logger(scope);
|
|
8
|
+
}
|
|
9
|
+
if (ctx?.logger && typeof ctx.logger.warn === 'function') {
|
|
10
|
+
return ctx.logger;
|
|
11
|
+
}
|
|
12
|
+
return noopLogger;
|
|
13
|
+
}
|
package/lib/ops-status.js
CHANGED
|
@@ -143,7 +143,7 @@ ctx.effect(() => ctx.webServer.register({
|
|
|
143
143
|
proactiveRateLimitGuard: pool.proactiveRateLimitGuard ?? runtime.proactiveRateLimitGuard ?? true,
|
|
144
144
|
});
|
|
145
145
|
} catch (e) {
|
|
146
|
-
|
|
146
|
+
(ctx?.logger ? ctx.logger('dsh-key-rotation') : null)?.warn?.(`[dsh-key-rotation] status: pool ${pool.base} failed: ${String(e?.message ?? e)} ${e?.stack ?? ''}`);
|
|
147
147
|
providers.push({ provider: pool.base, keys: [], statusError: String(e?.message ?? e) });
|
|
148
148
|
}
|
|
149
149
|
}
|
package/lib/ops-webhook.js
CHANGED
|
@@ -64,7 +64,7 @@ ctx.effect(() => ctx.webServer.register({
|
|
|
64
64
|
try {
|
|
65
65
|
if (action === 'disable-rotation') {
|
|
66
66
|
setRotationDisabled(true);
|
|
67
|
-
|
|
67
|
+
(ctx?.logger ? ctx.logger('dsh-key-rotation') : null)?.warn?.('[dsh-key-rotation] rotation DISABLED via webhook action');
|
|
68
68
|
json(res, 200, { ok: true, action });
|
|
69
69
|
return;
|
|
70
70
|
}
|
|
@@ -84,7 +84,7 @@ ctx.effect(() => ctx.webServer.register({
|
|
|
84
84
|
if (p.base !== provider) continue;
|
|
85
85
|
for (const ref of p.refs) st.failedUntil.set(ref, Math.max(st.failedUntil.get(ref) ?? 0, until));
|
|
86
86
|
}
|
|
87
|
-
|
|
87
|
+
(ctx?.logger ? ctx.logger('dsh-key-rotation') : null)?.warn?.(`[dsh-key-rotation] pool ${provider} PAUSED 1h via webhook action`);
|
|
88
88
|
json(res, 200, { ok: true, action, provider, until: Date.now() + 3600000 });
|
|
89
89
|
return;
|
|
90
90
|
}
|
|
@@ -102,7 +102,7 @@ ctx.effect(() => ctx.webServer.register({
|
|
|
102
102
|
if (typeof br.reset === 'function') { br.reset(provider); circuitReset = true; }
|
|
103
103
|
else if (typeof br.onSuccess === 'function') { br.onSuccess(provider); circuitReset = true; }
|
|
104
104
|
}
|
|
105
|
-
|
|
105
|
+
(ctx?.logger ? ctx.logger('dsh-key-rotation') : null)?.warn?.(`[dsh-key-rotation] pool ${provider} RESET via webhook action (circuitReset=${circuitReset})`);
|
|
106
106
|
json(res, 200, { ok: true, action, provider, cleared, circuitReset });
|
|
107
107
|
return;
|
|
108
108
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// lib/pool-builder.js — provider & per-model pool assembly and cleanup
|
|
2
|
+
import { bucketSweep } from './bucket.js';
|
|
3
|
+
|
|
4
|
+
export function parseExpiry(v) {
|
|
5
|
+
if (typeof v === 'number' && Number.isFinite(v) && v > 0) return v;
|
|
6
|
+
if (typeof v === 'string' && v.length > 0) {
|
|
7
|
+
const t = Date.parse(v);
|
|
8
|
+
if (!Number.isNaN(t)) return t;
|
|
9
|
+
}
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function buildPoolItem({
|
|
14
|
+
base,
|
|
15
|
+
keys,
|
|
16
|
+
weights,
|
|
17
|
+
poolCooldown,
|
|
18
|
+
poolMax,
|
|
19
|
+
expiresAt,
|
|
20
|
+
poolStrategy,
|
|
21
|
+
poolGuard,
|
|
22
|
+
rpmLimit,
|
|
23
|
+
makeState,
|
|
24
|
+
}) {
|
|
25
|
+
const refs = (keys ?? []).filter((ref) => typeof ref === 'string' && ref.length > 0);
|
|
26
|
+
if (refs.length === 0) return null;
|
|
27
|
+
const w = Array.isArray(weights) ? weights : [];
|
|
28
|
+
const weightedRefs = [];
|
|
29
|
+
for (let i = 0; i < refs.length; i++) {
|
|
30
|
+
const ww = typeof w[i] === 'number' && w[i] > 0 ? Math.floor(w[i]) : 1;
|
|
31
|
+
for (let k = 0; k < ww; k++) weightedRefs.push(refs[i]);
|
|
32
|
+
}
|
|
33
|
+
const parsedExpiry = {};
|
|
34
|
+
if (Array.isArray(expiresAt)) {
|
|
35
|
+
for (let i = 0; i < refs.length; i++) {
|
|
36
|
+
const exp = parseExpiry(expiresAt[i]);
|
|
37
|
+
if (exp !== undefined) parsedExpiry[refs[i]] = exp;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
base,
|
|
42
|
+
refs,
|
|
43
|
+
weights: refs.map((_, i) => (typeof w[i] === 'number' && w[i] > 0 ? Math.floor(w[i]) : 1)),
|
|
44
|
+
weightedRefs: weightedRefs.length > 0 ? weightedRefs : refs,
|
|
45
|
+
state: makeState(base),
|
|
46
|
+
cooldownMs: poolCooldown,
|
|
47
|
+
maxCooldownMs: poolMax,
|
|
48
|
+
expiresAt: parsedExpiry,
|
|
49
|
+
rpmLimit,
|
|
50
|
+
routingStrategy: poolStrategy,
|
|
51
|
+
proactiveRateLimitGuard: poolGuard,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function cleanupRemovedProviders({
|
|
56
|
+
cfg,
|
|
57
|
+
poolState,
|
|
58
|
+
poolByRef,
|
|
59
|
+
providerToPool,
|
|
60
|
+
expectedClones,
|
|
61
|
+
moduleBreaker,
|
|
62
|
+
lowHealthNotifiedAt,
|
|
63
|
+
budgetNotifiedAt,
|
|
64
|
+
}) {
|
|
65
|
+
// auto-cleanup: remove poolState for providers that are now empty or removed
|
|
66
|
+
for (const key of [...poolState.keys()]) {
|
|
67
|
+
if (![...poolByRef.values()].some((p) => p.base === key)) {
|
|
68
|
+
poolState.delete(key);
|
|
69
|
+
lowHealthNotifiedAt?.delete?.(key);
|
|
70
|
+
budgetNotifiedAt?.delete?.(key + ':budget');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// #192: drop RPM windows for refs that no longer belong to any pool
|
|
74
|
+
for (const st of poolState.values()) {
|
|
75
|
+
if (st.rpmWindows) bucketSweep(st.rpmWindows, new Set(poolByRef.keys()));
|
|
76
|
+
}
|
|
77
|
+
// drop breaker entries for removed providers
|
|
78
|
+
if (moduleBreaker) {
|
|
79
|
+
for (const key of Object.keys(moduleBreaker.snapshot())) {
|
|
80
|
+
if (![...providerToPool.keys()].includes(key) && !expectedClones.has(key)) {
|
|
81
|
+
const still = (cfg.providers ?? []).some((p) => p.provider === key);
|
|
82
|
+
if (!still) moduleBreaker.reset(key);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
package/lib/rotate.js
CHANGED
|
@@ -33,9 +33,12 @@ export function createRotate(deps) {
|
|
|
33
33
|
setRotateStartMs,
|
|
34
34
|
quotaStore,
|
|
35
35
|
circuitBreaker,
|
|
36
|
+
logger,
|
|
36
37
|
now = nowMono,
|
|
37
38
|
} = deps;
|
|
38
39
|
|
|
40
|
+
const logWarn = (msg) => (logger?.warn ? logger.warn(msg) : null);
|
|
41
|
+
|
|
39
42
|
function rotate(options, pool) {
|
|
40
43
|
return (async function* () {
|
|
41
44
|
const runtime0 = buildRuntime();
|
|
@@ -93,7 +96,7 @@ export function createRotate(deps) {
|
|
|
93
96
|
|
|
94
97
|
// #260: fail fast when provider circuit is open
|
|
95
98
|
if (circuitBreaker && !circuitBreaker.canRequest(options.provider)) {
|
|
96
|
-
|
|
99
|
+
logWarn(`[dsh-key-rotation] ${options.provider}: circuit open — skipping dispatch`);
|
|
97
100
|
yield finishError('CIRCUIT_OPEN', `[dsh-key-rotation] provider '${options.provider}' circuit is open`);
|
|
98
101
|
return;
|
|
99
102
|
}
|
|
@@ -116,7 +119,7 @@ export function createRotate(deps) {
|
|
|
116
119
|
penalizeRef(curRef, e?.code ?? 'TRANSPORT', String(e?.message ?? ''));
|
|
117
120
|
lastFailure = finishError(e?.code ?? 'TRANSPORT',
|
|
118
121
|
`dsh-key-rotation: dispatch failed: ${String(e?.message ?? e)}`);
|
|
119
|
-
|
|
122
|
+
logWarn(`[dsh-key-rotation] ${options.provider}: key ${String(curRef ?? '?')} threw ${String(e?.code ?? e?.message ?? e)}`);
|
|
120
123
|
continue;
|
|
121
124
|
}
|
|
122
125
|
|
|
@@ -146,7 +149,7 @@ export function createRotate(deps) {
|
|
|
146
149
|
pool.state.lastReason = String(code ?? 'UNKNOWN');
|
|
147
150
|
pool.state.lastSwitchAt = now();
|
|
148
151
|
lastFailure = chunk;
|
|
149
|
-
|
|
152
|
+
logWarn(`[dsh-key-rotation] ${options.provider}: key ${String(activeRef ?? '?')} failed (${String(code)} ${String(message).slice(0, 100)}) - next key`);
|
|
150
153
|
// #216: per-switch webhook (opt-in switchNotify), deduped per provider
|
|
151
154
|
if (switchNotify && activeRef) {
|
|
152
155
|
notifySwitch(runtime0, pool, {
|
|
@@ -206,7 +209,7 @@ export function createRotate(deps) {
|
|
|
206
209
|
const effCool = Math.min(cool, maxCool ?? cool);
|
|
207
210
|
recordFailure(pool, activeRef, now(), effCool, maxCool);
|
|
208
211
|
pushEvent(pool, activeRef, 'RATE_LIMIT', effCool);
|
|
209
|
-
|
|
212
|
+
logWarn(`[dsh-key-rotation] ${options.provider}: key ${activeRef} proactive pause (remaining ${String(rate.remaining ?? '?')}/${String(rate.limit ?? '?')}, cool ${Math.round(effCool / 1000)}s) — next request will rotate`);
|
|
210
213
|
}
|
|
211
214
|
}
|
|
212
215
|
// #7: persist quota snapshot regardless of threshold (so dashboard widget can show it).
|
|
@@ -229,7 +232,7 @@ export function createRotate(deps) {
|
|
|
229
232
|
pool.state.lastReason = String(e?.code ?? 'TRANSPORT');
|
|
230
233
|
pool.state.lastSwitchAt = now();
|
|
231
234
|
lastFailure = finishError(e?.code ?? 'TRANSPORT', String(e?.message ?? e));
|
|
232
|
-
|
|
235
|
+
logWarn(`[dsh-key-rotation] ${options.provider}: key ${String(activeRef ?? '?')} stream threw ${String(e?.code ?? e?.message ?? e)} - failover to next key`);
|
|
233
236
|
if (switchNotify && activeRef) {
|
|
234
237
|
notifySwitch(runtime0, pool, {
|
|
235
238
|
provider: options.provider,
|
|
@@ -256,7 +259,7 @@ export function createRotate(deps) {
|
|
|
256
259
|
// pool exhausted — all keys cooling or missing
|
|
257
260
|
pool.state.lastExhaustionAt = now();
|
|
258
261
|
pool.state.exhaustionCount = (pool.state.exhaustionCount ?? 0) + 1;
|
|
259
|
-
|
|
262
|
+
logWarn(`[dsh-key-rotation] ${options.provider}: pool exhausted — all ${pool.refs.length} keys cooling`);
|
|
260
263
|
const runtime = buildRuntime();
|
|
261
264
|
// notify via extracted helper (see notifyExhaustion above)
|
|
262
265
|
notifyExhaustion(runtime, pool, { provider: options.provider });
|
|
@@ -266,7 +269,7 @@ export function createRotate(deps) {
|
|
|
266
269
|
const pools = runtime.providerToPool;
|
|
267
270
|
const fb = pickCascadeFallback(options.provider, runtime, pools);
|
|
268
271
|
if (fb && fb.pool && fb.pool !== pool) {
|
|
269
|
-
|
|
272
|
+
logWarn(`[dsh-key-rotation] ${options.provider}: pool exhausted — cascading to ${fb.provider}`);
|
|
270
273
|
pool.state.lastReason = 'CASCADE';
|
|
271
274
|
pool.state.lastSwitchAt = now();
|
|
272
275
|
// Re-dispatch on the fallback pool (depth-1 via __isCascade guard)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// lib/sandbox-service.js — sandbox runner and cache service
|
|
2
|
+
import { LastTestCache, SandboxRunner } from './sandbox.js';
|
|
3
|
+
|
|
4
|
+
export function createSandboxService({ getRuntime }) {
|
|
5
|
+
let sandboxRunner = null;
|
|
6
|
+
let defaultCtx = null;
|
|
7
|
+
const lastTestCache = new LastTestCache();
|
|
8
|
+
|
|
9
|
+
function ensureSandboxRunner(ctx) {
|
|
10
|
+
if (ctx) defaultCtx = ctx;
|
|
11
|
+
if (sandboxRunner) return sandboxRunner;
|
|
12
|
+
function resolveBaseUrl(providerOrRef) {
|
|
13
|
+
try {
|
|
14
|
+
let provider = providerOrRef;
|
|
15
|
+
const rt = typeof getRuntime === 'function' ? getRuntime() : null;
|
|
16
|
+
const pool = rt?.poolByRef?.get(providerOrRef);
|
|
17
|
+
if (pool?.base) provider = pool.base;
|
|
18
|
+
else if (pool?.provider) provider = pool.provider;
|
|
19
|
+
|
|
20
|
+
const c = ctx || defaultCtx;
|
|
21
|
+
const pInfo = c?.llm?.getProvider?.(provider);
|
|
22
|
+
if (pInfo && (pInfo.baseUrl || pInfo.endpoint || pInfo.url)) {
|
|
23
|
+
return String(pInfo.baseUrl || pInfo.endpoint || pInfo.url);
|
|
24
|
+
}
|
|
25
|
+
for (const info of (c?.llm?.listProviders?.() || [])) {
|
|
26
|
+
if (info && (info.id === provider || info.name === provider)) {
|
|
27
|
+
const u = info.baseUrl || info.endpoint || info.url;
|
|
28
|
+
if (u) return String(u);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const ns = c?.get ? c.get('llm-pi-ai') : null;
|
|
32
|
+
const list = ns && (ns.providers || (ns.config && ns.config.providers) || []);
|
|
33
|
+
if (Array.isArray(list)) {
|
|
34
|
+
const hit = list.find((p) => p && (p.id === provider || p.name === provider || (Array.isArray(p.aliases) && p.aliases.includes(provider))));
|
|
35
|
+
const base = hit && (hit.baseUrl || hit.endpoint || hit.url);
|
|
36
|
+
if (base) return String(base);
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
} catch (_) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
sandboxRunner = new SandboxRunner({ fetchImpl: globalThis.fetch, resolveBaseUrl });
|
|
44
|
+
return sandboxRunner;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function probeRef(ref, key) {
|
|
48
|
+
const runner = ensureSandboxRunner();
|
|
49
|
+
const result = await runner.probeModels(ref, key);
|
|
50
|
+
lastTestCache.set(ref, { ...result, at: Date.now() });
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { ensureSandboxRunner, probeRef, lastTestCache };
|
|
55
|
+
}
|
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.13",
|
|
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": [
|
|
@@ -33,6 +33,9 @@
|
|
|
33
33
|
"lib",
|
|
34
34
|
"cordis.patch.yml",
|
|
35
35
|
"README.md",
|
|
36
|
+
"README.zh.md",
|
|
37
|
+
"README.ru.md",
|
|
38
|
+
"CHANGELOG.md",
|
|
36
39
|
"LICENSE"
|
|
37
40
|
],
|
|
38
41
|
"dsh": {
|