@tokenbuddy/tokenbuddy 1.0.9 → 1.0.11
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/dist/src/buyer-store.d.ts +13 -0
- package/dist/src/buyer-store.d.ts.map +1 -1
- package/dist/src/buyer-store.js +21 -2
- package/dist/src/buyer-store.js.map +1 -1
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +54 -0
- package/dist/src/cli.js.map +1 -1
- package/dist/src/credit-tracker.d.ts +118 -0
- package/dist/src/credit-tracker.d.ts.map +1 -0
- package/dist/src/credit-tracker.js +220 -0
- package/dist/src/credit-tracker.js.map +1 -0
- package/dist/src/daemon.d.ts +49 -4
- package/dist/src/daemon.d.ts.map +1 -1
- package/dist/src/daemon.js +541 -405
- package/dist/src/daemon.js.map +1 -1
- package/dist/src/model-index.d.ts +86 -0
- package/dist/src/model-index.d.ts.map +1 -0
- package/dist/src/model-index.js +214 -0
- package/dist/src/model-index.js.map +1 -0
- package/dist/src/prewarm-cache.d.ts +149 -0
- package/dist/src/prewarm-cache.d.ts.map +1 -0
- package/dist/src/prewarm-cache.js +288 -0
- package/dist/src/prewarm-cache.js.map +1 -0
- package/dist/src/prewarm-scheduler.d.ts +150 -0
- package/dist/src/prewarm-scheduler.d.ts.map +1 -0
- package/dist/src/prewarm-scheduler.js +484 -0
- package/dist/src/prewarm-scheduler.js.map +1 -0
- package/dist/src/provider-install.d.ts.map +1 -1
- package/dist/src/provider-install.js +9 -1
- package/dist/src/provider-install.js.map +1 -1
- package/dist/src/route-failover.d.ts +96 -0
- package/dist/src/route-failover.d.ts.map +1 -0
- package/dist/src/route-failover.js +177 -0
- package/dist/src/route-failover.js.map +1 -0
- package/dist/src/seller-catalog.d.ts +26 -0
- package/dist/src/seller-catalog.d.ts.map +1 -1
- package/dist/src/seller-catalog.js +40 -0
- package/dist/src/seller-catalog.js.map +1 -1
- package/dist/src/seller-pool.d.ts +127 -0
- package/dist/src/seller-pool.d.ts.map +1 -0
- package/dist/src/seller-pool.js +243 -0
- package/dist/src/seller-pool.js.map +1 -0
- package/dist/src/stream-failover.d.ts +78 -0
- package/dist/src/stream-failover.d.ts.map +1 -0
- package/dist/src/stream-failover.js +93 -0
- package/dist/src/stream-failover.js.map +1 -0
- package/package.json +1 -1
- package/src/buyer-store.ts +32 -2
- package/src/cli.ts +61 -0
- package/src/credit-tracker.test.ts +165 -0
- package/src/credit-tracker.ts +269 -0
- package/src/daemon.ts +569 -445
- package/src/model-index.test.ts +184 -0
- package/src/model-index.ts +266 -0
- package/src/prewarm-cache.test.ts +281 -0
- package/src/prewarm-cache.ts +373 -0
- package/src/prewarm-scheduler.test.ts +367 -0
- package/src/prewarm-scheduler.ts +581 -0
- package/src/provider-install.ts +9 -1
- package/src/route-failover.test.ts +193 -0
- package/src/route-failover.ts +233 -0
- package/src/seller-catalog-413.test.ts +61 -0
- package/src/seller-catalog.ts +47 -0
- package/src/seller-pool.test.ts +231 -0
- package/src/seller-pool.ts +333 -0
- package/src/stream-failover.test.ts +52 -0
- package/src/stream-failover.ts +129 -0
- package/src/thousand-seller.test.ts +151 -0
- package/tests/daemon-413-fallback.test.ts +92 -0
- package/tests/e2e.test.ts +3 -2
- package/tests/tokenbuddy.test.ts +68 -11
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { ModelIndex } from "../src/model-index.js";
|
|
2
|
+
import { PrewarmCache } from "../src/prewarm-cache.js";
|
|
3
|
+
import { PrewarmScheduler, type ProbeResult, type SellerProber } from "../src/prewarm-scheduler.js";
|
|
4
|
+
import type { RegistrySeller } from "../src/seller-catalog.js";
|
|
5
|
+
|
|
6
|
+
interface FakeClock {
|
|
7
|
+
now: number;
|
|
8
|
+
advance: (ms: number) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function makeClock(start = 1_000_000): FakeClock {
|
|
12
|
+
const clock = { now: start, advance: (ms: number) => { clock.now += ms; } };
|
|
13
|
+
return clock;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function makeSeller(overrides: Partial<RegistrySeller> & { id: string; models?: string[] }): RegistrySeller {
|
|
17
|
+
return {
|
|
18
|
+
id: overrides.id,
|
|
19
|
+
name: overrides.name ?? overrides.id,
|
|
20
|
+
url: overrides.url ?? `https://${overrides.id}.example.com`,
|
|
21
|
+
supportedProtocols: overrides.supportedProtocols ?? ["chat_completions"],
|
|
22
|
+
paymentMethods: overrides.paymentMethods ?? ["clawtip"],
|
|
23
|
+
models: overrides.models
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function makeProberScript(script: Array<{ sellerId: string; ok?: boolean; latencyMs?: number; errorMessage?: string }>): SellerProber & { calls: string[] } {
|
|
28
|
+
const calls: string[] = [];
|
|
29
|
+
const remaining = script.slice();
|
|
30
|
+
const fn = (async (seller: RegistrySeller, _signal: AbortSignal): Promise<ProbeResult> => {
|
|
31
|
+
calls.push(seller.id);
|
|
32
|
+
const next = remaining.shift();
|
|
33
|
+
if (next && next.sellerId === seller.id) {
|
|
34
|
+
return {
|
|
35
|
+
ok: next.ok ?? true,
|
|
36
|
+
latencyMs: next.latencyMs ?? 100,
|
|
37
|
+
errorMessage: next.errorMessage,
|
|
38
|
+
httpStatus: next.ok === false ? 503 : 200
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return { ok: true, latencyMs: 100, httpStatus: 200 };
|
|
42
|
+
}) as SellerProber & { calls: string[] };
|
|
43
|
+
fn.calls = calls;
|
|
44
|
+
return fn;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function flushMicrotasks(times = 5): Promise<void> {
|
|
48
|
+
for (let i = 0; i < times; i += 1) {
|
|
49
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe("PrewarmScheduler", () => {
|
|
54
|
+
test("warm task resolves with successful status when at least one seller probes ok", async () => {
|
|
55
|
+
const index = new ModelIndex();
|
|
56
|
+
index.rebuild([makeSeller({ id: "s1", models: ["gpt-4o"] })]);
|
|
57
|
+
const cache = new PrewarmCache();
|
|
58
|
+
const prober = makeProberScript([{ sellerId: "s1", ok: true, latencyMs: 200 }]);
|
|
59
|
+
const scheduler = new PrewarmScheduler({
|
|
60
|
+
modelIndex: index,
|
|
61
|
+
cache,
|
|
62
|
+
prober,
|
|
63
|
+
// Disable idle loop so it does not race the test.
|
|
64
|
+
idleIntervalMs: 60_000,
|
|
65
|
+
sleep: () => new Promise(() => undefined)
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const task = await scheduler.schedulePrewarm({ modelId: "gpt-4o", reason: "lazy" });
|
|
69
|
+
expect(task.status).toBe("succeeded");
|
|
70
|
+
expect(task.sellerIds).toEqual(["s1"]);
|
|
71
|
+
expect(prober.calls).toEqual(["s1"]);
|
|
72
|
+
|
|
73
|
+
const entry = cache.get("gpt-4o", "chat_completions", "clawtip");
|
|
74
|
+
expect(entry?.state).toBe("warm");
|
|
75
|
+
expect(entry?.candidates).toHaveLength(1);
|
|
76
|
+
expect(entry?.candidates[0].sellerId).toBe("s1");
|
|
77
|
+
expect(entry?.candidates[0].healthScore).toBeGreaterThan(0);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("all-failed probe marks the cache entry stale and the task failed", async () => {
|
|
81
|
+
const index = new ModelIndex();
|
|
82
|
+
index.rebuild([makeSeller({ id: "s1", models: ["gpt-4o"] })]);
|
|
83
|
+
const cache = new PrewarmCache();
|
|
84
|
+
const prober = makeProberScript([{ sellerId: "s1", ok: false, errorMessage: "503" }]);
|
|
85
|
+
const scheduler = new PrewarmScheduler({
|
|
86
|
+
modelIndex: index,
|
|
87
|
+
cache,
|
|
88
|
+
prober,
|
|
89
|
+
sleep: () => new Promise(() => undefined)
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const task = await scheduler.schedulePrewarm({ modelId: "gpt-4o", reason: "lazy" });
|
|
93
|
+
expect(task.status).toBe("failed");
|
|
94
|
+
expect(task.errorMessage).toBe("all probes failed");
|
|
95
|
+
|
|
96
|
+
const entry = cache.get("gpt-4o", "chat_completions", "clawtip");
|
|
97
|
+
expect(entry?.state).toBe("stale");
|
|
98
|
+
expect(entry?.consecutiveWarmingFailures).toBe(1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("no matching sellers in the index marks the task failed and skips probing", async () => {
|
|
102
|
+
const index = new ModelIndex();
|
|
103
|
+
index.rebuild([]); // empty
|
|
104
|
+
const cache = new PrewarmCache();
|
|
105
|
+
const prober = makeProberScript([]);
|
|
106
|
+
const scheduler = new PrewarmScheduler({
|
|
107
|
+
modelIndex: index,
|
|
108
|
+
cache,
|
|
109
|
+
prober,
|
|
110
|
+
sleep: () => new Promise(() => undefined)
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const task = await scheduler.schedulePrewarm({ modelId: "unknown", reason: "lazy" });
|
|
114
|
+
expect(task.status).toBe("failed");
|
|
115
|
+
expect(task.errorMessage).toBe("no sellers for model");
|
|
116
|
+
expect(prober.calls).toEqual([]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("concurrency caps in-flight probes to the configured value", async () => {
|
|
120
|
+
// Each task probes its sellers serially; concurrency is the cap on the
|
|
121
|
+
// number of *tasks* running in parallel. To exercise the cap we enqueue
|
|
122
|
+
// three independent (model, seller) pairs and verify the prober is
|
|
123
|
+
// never invoked more than `concurrency` times at once.
|
|
124
|
+
const sellers = [
|
|
125
|
+
makeSeller({ id: "s1", models: ["m1"] }),
|
|
126
|
+
makeSeller({ id: "s2", models: ["m2"] }),
|
|
127
|
+
makeSeller({ id: "s3", models: ["m3"] })
|
|
128
|
+
];
|
|
129
|
+
const index = new ModelIndex();
|
|
130
|
+
index.rebuild(sellers);
|
|
131
|
+
const cache = new PrewarmCache();
|
|
132
|
+
|
|
133
|
+
let concurrent = 0;
|
|
134
|
+
let peak = 0;
|
|
135
|
+
const prober: SellerProber = async (_seller, _signal) => {
|
|
136
|
+
concurrent += 1;
|
|
137
|
+
peak = Math.max(peak, concurrent);
|
|
138
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 20));
|
|
139
|
+
concurrent -= 1;
|
|
140
|
+
return { ok: true, latencyMs: 50, httpStatus: 200 };
|
|
141
|
+
};
|
|
142
|
+
const scheduler = new PrewarmScheduler({
|
|
143
|
+
modelIndex: index,
|
|
144
|
+
cache,
|
|
145
|
+
prober,
|
|
146
|
+
concurrency: 2,
|
|
147
|
+
sleep: () => new Promise(() => undefined)
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const [t1, t2, t3] = await Promise.all([
|
|
151
|
+
scheduler.schedulePrewarm({ modelId: "m1", reason: "lazy" }),
|
|
152
|
+
scheduler.schedulePrewarm({ modelId: "m2", reason: "lazy" }),
|
|
153
|
+
scheduler.schedulePrewarm({ modelId: "m3", reason: "lazy" })
|
|
154
|
+
]);
|
|
155
|
+
expect([t1, t2, t3].map((t) => t.status)).toEqual(["succeeded", "succeeded", "succeeded"]);
|
|
156
|
+
expect(peak).toBeLessThanOrEqual(2);
|
|
157
|
+
expect(peak).toBe(2);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("per-seller rate limit suppresses repeated probes within the minimum interval", async () => {
|
|
161
|
+
const sellers = [makeSeller({ id: "s1", models: ["m"] })];
|
|
162
|
+
const index = new ModelIndex();
|
|
163
|
+
index.rebuild(sellers);
|
|
164
|
+
const cache = new PrewarmCache();
|
|
165
|
+
const clock = makeClock();
|
|
166
|
+
const prober = makeProberScript([{ sellerId: "s1", ok: true, latencyMs: 50 }]);
|
|
167
|
+
const scheduler = new PrewarmScheduler({
|
|
168
|
+
modelIndex: index,
|
|
169
|
+
cache,
|
|
170
|
+
prober,
|
|
171
|
+
perSellerMinIntervalMs: 30_000,
|
|
172
|
+
now: () => clock.now,
|
|
173
|
+
sleep: () => new Promise(() => undefined)
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// First probe at t=1_000_000 succeeds and updates the rate-limit ledger.
|
|
177
|
+
const first = await scheduler.schedulePrewarm({ modelId: "m", reason: "lazy" });
|
|
178
|
+
expect(first.status).toBe("succeeded");
|
|
179
|
+
|
|
180
|
+
// Second probe 5s later: seller is rate-limited, no new probe call.
|
|
181
|
+
clock.advance(5_000);
|
|
182
|
+
const second = await scheduler.schedulePrewarm({ modelId: "m", reason: "lazy" });
|
|
183
|
+
expect(second.status).toBe("succeeded"); // task itself still resolves
|
|
184
|
+
expect(prober.calls).toEqual(["s1"]); // prober was NOT called again
|
|
185
|
+
|
|
186
|
+
// After 30s have elapsed since the last probe, the seller can be probed again.
|
|
187
|
+
clock.advance(30_000);
|
|
188
|
+
const third = await scheduler.schedulePrewarm({ modelId: "m", reason: "lazy" });
|
|
189
|
+
expect(third.status).toBe("succeeded");
|
|
190
|
+
expect(prober.calls).toEqual(["s1", "s1"]);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("global per-minute probe budget rate-limits excess tasks", async () => {
|
|
194
|
+
const sellers = [
|
|
195
|
+
makeSeller({ id: "s1", models: ["m1"] }),
|
|
196
|
+
makeSeller({ id: "s2", models: ["m2"] }),
|
|
197
|
+
makeSeller({ id: "s3", models: ["m3"] })
|
|
198
|
+
];
|
|
199
|
+
const index = new ModelIndex();
|
|
200
|
+
index.rebuild(sellers);
|
|
201
|
+
const cache = new PrewarmCache();
|
|
202
|
+
const prober = makeProberScript([
|
|
203
|
+
{ sellerId: "s1", ok: true, latencyMs: 10 },
|
|
204
|
+
{ sellerId: "s2", ok: true, latencyMs: 10 },
|
|
205
|
+
{ sellerId: "s3", ok: true, latencyMs: 10 }
|
|
206
|
+
]);
|
|
207
|
+
const scheduler = new PrewarmScheduler({
|
|
208
|
+
modelIndex: index,
|
|
209
|
+
cache,
|
|
210
|
+
prober,
|
|
211
|
+
maxPrewarmPerMinute: 2,
|
|
212
|
+
// Generous per-seller window so it does not interfere.
|
|
213
|
+
perSellerMinIntervalMs: 0,
|
|
214
|
+
sleep: () => new Promise(() => undefined)
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const t1 = await scheduler.schedulePrewarm({ modelId: "m1", reason: "lazy" });
|
|
218
|
+
const t2 = await scheduler.schedulePrewarm({ modelId: "m2", reason: "lazy" });
|
|
219
|
+
const t3 = await scheduler.schedulePrewarm({ modelId: "m3", reason: "lazy" });
|
|
220
|
+
expect(t1.status).toBe("succeeded");
|
|
221
|
+
expect(t2.status).toBe("succeeded");
|
|
222
|
+
expect(t3.status).toBe("rate_limited");
|
|
223
|
+
expect(prober.calls).toEqual(["s1", "s2"]);
|
|
224
|
+
const stats = scheduler.stats();
|
|
225
|
+
expect(stats.totalRateLimited).toBe(1);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("tickIdle enqueues prewarms only for entries that are expiring soon", async () => {
|
|
229
|
+
const index = new ModelIndex();
|
|
230
|
+
index.rebuild([
|
|
231
|
+
makeSeller({ id: "s1", models: ["m1"] }),
|
|
232
|
+
makeSeller({ id: "s2", models: ["m2"] })
|
|
233
|
+
]);
|
|
234
|
+
const cache = new PrewarmCache({ defaultTtlMs: 1000 });
|
|
235
|
+
const prober = makeProberScript([
|
|
236
|
+
{ sellerId: "s1", ok: true, latencyMs: 50 },
|
|
237
|
+
{ sellerId: "s2", ok: true, latencyMs: 50 }
|
|
238
|
+
]);
|
|
239
|
+
const clock = makeClock();
|
|
240
|
+
const scheduler = new PrewarmScheduler({
|
|
241
|
+
modelIndex: index,
|
|
242
|
+
cache,
|
|
243
|
+
prober,
|
|
244
|
+
perSellerMinIntervalMs: 0,
|
|
245
|
+
now: () => clock.now,
|
|
246
|
+
sleep: () => new Promise(() => undefined)
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Seed cache with two entries.
|
|
250
|
+
await scheduler.schedulePrewarm({ modelId: "m1", reason: "startup" });
|
|
251
|
+
await scheduler.schedulePrewarm({ modelId: "m2", reason: "startup" });
|
|
252
|
+
expect(cache.size()).toBe(2);
|
|
253
|
+
|
|
254
|
+
// Advance to t=950 (within 10% of 1000 TTL).
|
|
255
|
+
clock.advance(950);
|
|
256
|
+
const enqueued = scheduler.tickIdle();
|
|
257
|
+
expect(enqueued).toBe(2);
|
|
258
|
+
await flushMicrotasks();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("stop() cancels queued tasks and prevents further dispatch", async () => {
|
|
262
|
+
const sellers = [makeSeller({ id: "s1", models: ["m"] })];
|
|
263
|
+
const index = new ModelIndex();
|
|
264
|
+
index.rebuild(sellers);
|
|
265
|
+
const cache = new PrewarmCache();
|
|
266
|
+
// Prober that observes its abort signal and rejects on abort. This is
|
|
267
|
+
// the contract real probers (e.g. `health-probe.ts`) must follow.
|
|
268
|
+
let proberStarted = false;
|
|
269
|
+
const prober: SellerProber = async (_seller, signal) => {
|
|
270
|
+
proberStarted = true;
|
|
271
|
+
await new Promise<void>((resolve, reject) => {
|
|
272
|
+
if (signal.aborted) {
|
|
273
|
+
reject(new Error("aborted"));
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
signal.addEventListener("abort", () => reject(new Error("aborted")), { once: true });
|
|
277
|
+
});
|
|
278
|
+
return { ok: true, latencyMs: 10, httpStatus: 200 };
|
|
279
|
+
};
|
|
280
|
+
const scheduler = new PrewarmScheduler({
|
|
281
|
+
modelIndex: index,
|
|
282
|
+
cache,
|
|
283
|
+
prober,
|
|
284
|
+
concurrency: 1,
|
|
285
|
+
sleep: () => new Promise(() => undefined)
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const task = scheduler.schedulePrewarm({ modelId: "m", reason: "lazy" });
|
|
289
|
+
// Let the dispatcher pick up the task and start the probe.
|
|
290
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
291
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
292
|
+
expect(proberStarted).toBe(true);
|
|
293
|
+
|
|
294
|
+
await scheduler.stop();
|
|
295
|
+
const result = await task;
|
|
296
|
+
expect(result.status).toBe("canceled");
|
|
297
|
+
expect(scheduler.stats().inFlight).toBe(0);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("runStartupPrewarm honors startup jitter and processes every model", async () => {
|
|
301
|
+
const sellers = [makeSeller({ id: "s1", models: ["m1"] }), makeSeller({ id: "s2", models: ["m2"] })];
|
|
302
|
+
const index = new ModelIndex();
|
|
303
|
+
index.rebuild(sellers);
|
|
304
|
+
const cache = new PrewarmCache();
|
|
305
|
+
const prober = makeProberScript([
|
|
306
|
+
{ sellerId: "s1", ok: true, latencyMs: 50 },
|
|
307
|
+
{ sellerId: "s2", ok: true, latencyMs: 50 }
|
|
308
|
+
]);
|
|
309
|
+
let sleepCalls = 0;
|
|
310
|
+
const scheduler = new PrewarmScheduler({
|
|
311
|
+
modelIndex: index,
|
|
312
|
+
cache,
|
|
313
|
+
prober,
|
|
314
|
+
startupJitterMinMs: 100,
|
|
315
|
+
startupJitterMaxMs: 100,
|
|
316
|
+
sleep: async () => { sleepCalls += 1; },
|
|
317
|
+
perSellerMinIntervalMs: 0
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
await scheduler.runStartupPrewarm(["m1", "m2"]);
|
|
321
|
+
expect(sleepCalls).toBe(1); // single jitter wait before the sweep
|
|
322
|
+
expect(prober.calls.sort()).toEqual(["s1", "s2"]);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("stats reports queue depth, in-flight, and counters", async () => {
|
|
326
|
+
const sellers = [makeSeller({ id: "s1", models: ["m"] })];
|
|
327
|
+
const index = new ModelIndex();
|
|
328
|
+
index.rebuild(sellers);
|
|
329
|
+
const cache = new PrewarmCache();
|
|
330
|
+
const prober = makeProberScript([{ sellerId: "s1", ok: true, latencyMs: 50 }]);
|
|
331
|
+
const scheduler = new PrewarmScheduler({
|
|
332
|
+
modelIndex: index,
|
|
333
|
+
cache,
|
|
334
|
+
prober,
|
|
335
|
+
sleep: () => new Promise(() => undefined)
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
expect(scheduler.stats()).toMatchObject({
|
|
339
|
+
queueDepth: 0,
|
|
340
|
+
inFlight: 0,
|
|
341
|
+
totalScheduled: 0,
|
|
342
|
+
totalSucceeded: 0,
|
|
343
|
+
totalFailed: 0,
|
|
344
|
+
totalRateLimited: 0,
|
|
345
|
+
concurrency: 4,
|
|
346
|
+
maxPrewarmPerMinute: 30
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
await scheduler.schedulePrewarm({ modelId: "m", reason: "lazy" });
|
|
350
|
+
const stats = scheduler.stats();
|
|
351
|
+
expect(stats.totalSucceeded).toBe(1);
|
|
352
|
+
expect(stats.totalScheduled).toBe(1);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test("default options match the v1.2 design defaults", () => {
|
|
356
|
+
const index = new ModelIndex();
|
|
357
|
+
const cache = new PrewarmCache();
|
|
358
|
+
const scheduler = new PrewarmScheduler({
|
|
359
|
+
modelIndex: index,
|
|
360
|
+
cache,
|
|
361
|
+
prober: async () => ({ ok: true, latencyMs: 1, httpStatus: 200 })
|
|
362
|
+
});
|
|
363
|
+
const stats = scheduler.stats();
|
|
364
|
+
expect(stats.concurrency).toBe(4);
|
|
365
|
+
expect(stats.maxPrewarmPerMinute).toBe(30);
|
|
366
|
+
});
|
|
367
|
+
});
|