@tokenbuddy/tokenbuddy 1.0.36 → 1.0.38

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.
Files changed (146) hide show
  1. package/dist/src/buyer-store.d.ts +7 -2
  2. package/dist/src/buyer-store.js +46 -7
  3. package/dist/src/cli.d.ts +1 -0
  4. package/dist/src/cli.js +15 -7
  5. package/dist/src/daemon.d.ts +12 -0
  6. package/dist/src/daemon.js +791 -61
  7. package/dist/src/doctor-diagnostics.js +1 -6
  8. package/dist/src/provider-install.d.ts +2 -2
  9. package/dist/src/provider-install.js +248 -2
  10. package/dist/src/seller-catalog.d.ts +21 -0
  11. package/dist/src/seller-catalog.js +17 -0
  12. package/dist/src/seller-route-planner.d.ts +4 -1
  13. package/dist/src/seller-route-planner.js +3 -0
  14. package/dist/src/seller-routing-strategy.d.ts +3 -0
  15. package/dist/src/terminal-detect.d.ts +1 -1
  16. package/dist/src/terminal-detect.js +3 -2
  17. package/dist/src/workdir.d.ts +10 -0
  18. package/dist/src/workdir.js +26 -0
  19. package/package.json +15 -2
  20. package/static/ui/assets/index-Djfl9tw5.js +271 -0
  21. package/static/ui/assets/index-DkfztCkn.css +1 -0
  22. package/static/ui/index.html +2 -2
  23. package/dist/src/buyer-store.d.ts.map +0 -1
  24. package/dist/src/buyer-store.js.map +0 -1
  25. package/dist/src/clawtip-bootstrap.d.ts.map +0 -1
  26. package/dist/src/clawtip-bootstrap.js.map +0 -1
  27. package/dist/src/cli.d.ts.map +0 -1
  28. package/dist/src/cli.js.map +0 -1
  29. package/dist/src/credit-tracker.d.ts.map +0 -1
  30. package/dist/src/credit-tracker.js.map +0 -1
  31. package/dist/src/daemon.d.ts.map +0 -1
  32. package/dist/src/daemon.js.map +0 -1
  33. package/dist/src/doctor-clawtip-wallet.d.ts.map +0 -1
  34. package/dist/src/doctor-clawtip-wallet.js.map +0 -1
  35. package/dist/src/doctor-diagnostics.d.ts.map +0 -1
  36. package/dist/src/doctor-diagnostics.js.map +0 -1
  37. package/dist/src/index.d.ts.map +0 -1
  38. package/dist/src/index.js.map +0 -1
  39. package/dist/src/init-clawtip-activation.d.ts.map +0 -1
  40. package/dist/src/init-clawtip-activation.js.map +0 -1
  41. package/dist/src/init-payment-options.d.ts.map +0 -1
  42. package/dist/src/init-payment-options.js.map +0 -1
  43. package/dist/src/init-setup.d.ts.map +0 -1
  44. package/dist/src/init-setup.js.map +0 -1
  45. package/dist/src/model-index.d.ts.map +0 -1
  46. package/dist/src/model-index.js.map +0 -1
  47. package/dist/src/package-update.d.ts.map +0 -1
  48. package/dist/src/package-update.js.map +0 -1
  49. package/dist/src/prewarm-cache.d.ts.map +0 -1
  50. package/dist/src/prewarm-cache.js.map +0 -1
  51. package/dist/src/prewarm-scheduler.d.ts.map +0 -1
  52. package/dist/src/prewarm-scheduler.js.map +0 -1
  53. package/dist/src/provider-install.d.ts.map +0 -1
  54. package/dist/src/provider-install.js.map +0 -1
  55. package/dist/src/provider-routing-config.d.ts.map +0 -1
  56. package/dist/src/provider-routing-config.js.map +0 -1
  57. package/dist/src/registry-trust.d.ts.map +0 -1
  58. package/dist/src/registry-trust.js.map +0 -1
  59. package/dist/src/route-failover.d.ts.map +0 -1
  60. package/dist/src/route-failover.js.map +0 -1
  61. package/dist/src/seller-catalog.d.ts.map +0 -1
  62. package/dist/src/seller-catalog.js.map +0 -1
  63. package/dist/src/seller-concurrency-limiter.d.ts.map +0 -1
  64. package/dist/src/seller-concurrency-limiter.js.map +0 -1
  65. package/dist/src/seller-metadata-cache.d.ts.map +0 -1
  66. package/dist/src/seller-metadata-cache.js.map +0 -1
  67. package/dist/src/seller-pool.d.ts.map +0 -1
  68. package/dist/src/seller-pool.js.map +0 -1
  69. package/dist/src/seller-route-planner.d.ts.map +0 -1
  70. package/dist/src/seller-route-planner.js.map +0 -1
  71. package/dist/src/seller-routing-config.d.ts.map +0 -1
  72. package/dist/src/seller-routing-config.js.map +0 -1
  73. package/dist/src/seller-routing-strategy.d.ts.map +0 -1
  74. package/dist/src/seller-routing-strategy.js.map +0 -1
  75. package/dist/src/stream-failover.d.ts.map +0 -1
  76. package/dist/src/stream-failover.js.map +0 -1
  77. package/dist/src/tb-clawtip-proof.d.ts.map +0 -1
  78. package/dist/src/tb-clawtip-proof.js.map +0 -1
  79. package/dist/src/tb-proxyd.d.ts.map +0 -1
  80. package/dist/src/tb-proxyd.js.map +0 -1
  81. package/dist/src/terminal-detect.d.ts.map +0 -1
  82. package/dist/src/terminal-detect.js.map +0 -1
  83. package/dist/src/terminal-image.d.ts.map +0 -1
  84. package/dist/src/terminal-image.js.map +0 -1
  85. package/src/buyer-store.ts +0 -1090
  86. package/src/clawtip-bootstrap.ts +0 -65
  87. package/src/cli.ts +0 -2243
  88. package/src/credit-tracker.ts +0 -295
  89. package/src/daemon.ts +0 -5475
  90. package/src/doctor-clawtip-wallet.ts +0 -95
  91. package/src/doctor-diagnostics.ts +0 -1026
  92. package/src/index.ts +0 -16
  93. package/src/init-clawtip-activation.ts +0 -695
  94. package/src/init-payment-options.ts +0 -373
  95. package/src/init-setup.ts +0 -165
  96. package/src/model-index.ts +0 -278
  97. package/src/package-update.ts +0 -311
  98. package/src/prewarm-cache.ts +0 -485
  99. package/src/prewarm-scheduler.ts +0 -675
  100. package/src/provider-install.ts +0 -1006
  101. package/src/provider-routing-config.ts +0 -410
  102. package/src/registry-trust.ts +0 -51
  103. package/src/route-failover.ts +0 -304
  104. package/src/seller-catalog.ts +0 -505
  105. package/src/seller-concurrency-limiter.ts +0 -161
  106. package/src/seller-metadata-cache.ts +0 -91
  107. package/src/seller-pool.ts +0 -557
  108. package/src/seller-route-planner.ts +0 -513
  109. package/src/seller-routing-config.ts +0 -211
  110. package/src/seller-routing-strategy.ts +0 -362
  111. package/src/stream-failover.ts +0 -152
  112. package/src/tb-clawtip-proof.ts +0 -28
  113. package/src/tb-proxyd.ts +0 -101
  114. package/src/terminal-detect.ts +0 -333
  115. package/src/terminal-image.ts +0 -228
  116. package/static/ui/assets/index-0MVXD7bH.css +0 -1
  117. package/static/ui/assets/index-BVbeDEwq.js +0 -271
  118. package/static/ui/assets/index-BVbeDEwq.js.map +0 -1
  119. package/tests/cli-routing.test.ts +0 -363
  120. package/tests/control-plane-ui-endpoints.test.ts +0 -1630
  121. package/tests/credit-tracker.test.ts +0 -165
  122. package/tests/daemon-413-fallback.test.ts +0 -92
  123. package/tests/daemon-classify.test.ts +0 -452
  124. package/tests/daemon-roles.test.ts +0 -92
  125. package/tests/daemon-trusted-registry-cache.test.ts +0 -132
  126. package/tests/e2e.test.ts +0 -366
  127. package/tests/image-generation-e2e.test.ts +0 -230
  128. package/tests/model-index.test.ts +0 -198
  129. package/tests/package-update.test.ts +0 -147
  130. package/tests/prewarm-cache.test.ts +0 -296
  131. package/tests/prewarm-scheduler.test.ts +0 -367
  132. package/tests/provider-routing-config.test.ts +0 -150
  133. package/tests/registry-trust.test.ts +0 -28
  134. package/tests/route-failover.test.ts +0 -222
  135. package/tests/seller-catalog-413.test.ts +0 -120
  136. package/tests/seller-catalog-utilities.test.ts +0 -124
  137. package/tests/seller-concurrency-limiter.test.ts +0 -83
  138. package/tests/seller-metadata-cache.test.ts +0 -89
  139. package/tests/seller-pool.test.ts +0 -365
  140. package/tests/seller-route-planner.test.ts +0 -312
  141. package/tests/seller-routing-config.test.ts +0 -124
  142. package/tests/seller-routing-strategy.test.ts +0 -167
  143. package/tests/stream-failover.test.ts +0 -52
  144. package/tests/thousand-seller.test.ts +0 -151
  145. package/tests/tokenbuddy.test.ts +0 -4043
  146. package/tsconfig.json +0 -8
@@ -1,367 +0,0 @@
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
- });
@@ -1,150 +0,0 @@
1
- import {
2
- normalizeAutoProviderConfig,
3
- normalizeManualProviderConfig,
4
- normalizeManualProvidersConfig,
5
- normalizeProviderModeConfig,
6
- publicManualProviderConfig
7
- } from "../src/provider-routing-config.js";
8
-
9
- describe("provider routing config", () => {
10
- const NOW = "2026-06-10T00:00:00.000Z";
11
-
12
- test("defaults provider mode to manual", () => {
13
- expect(normalizeProviderModeConfig(undefined, NOW)).toEqual({
14
- mode: "manual",
15
- updatedAt: NOW
16
- });
17
- });
18
-
19
- test("normalizes a manual provider without exposing the key value", () => {
20
- const provider = normalizeManualProviderConfig({
21
- id: "local-openai",
22
- name: "Local OpenAI",
23
- kind: "openai-compatible",
24
- baseUrl: "https://api.openai.example/v1/",
25
- apiKeyEnv: "TB_TEST_PROVIDER_KEY",
26
- models: [" gpt-4o ", "gpt-4o", "gpt-4o-mini"],
27
- supportedProtocols: ["chat_completions", "responses"],
28
- enabled: true,
29
- notes: "fallback"
30
- }, { now: NOW });
31
-
32
- expect(provider).toMatchObject({
33
- id: "local-openai",
34
- name: "Local OpenAI",
35
- baseUrl: "https://api.openai.example/v1",
36
- apiKeyEnv: "TB_TEST_PROVIDER_KEY",
37
- models: ["gpt-4o", "gpt-4o-mini"],
38
- supportedProtocols: ["chat_completions", "responses"],
39
- enabled: true,
40
- createdAt: NOW,
41
- updatedAt: NOW
42
- });
43
- expect(publicManualProviderConfig(provider, undefined, { TB_TEST_PROVIDER_KEY: "secret" })).toMatchObject({
44
- id: "local-openai",
45
- keyRef: {
46
- kind: "env",
47
- name: "TB_TEST_PROVIDER_KEY",
48
- configured: true
49
- }
50
- });
51
- expect(publicManualProviderConfig(provider)).not.toHaveProperty("apiKeyEnv");
52
- });
53
-
54
- test("rejects raw api keys in manual provider config", () => {
55
- expect(() => normalizeManualProviderConfig({
56
- id: "local-openai",
57
- name: "Local OpenAI",
58
- baseUrl: "https://api.openai.example/v1",
59
- apiKey: "sk-secret",
60
- models: ["gpt-4o"]
61
- }, { now: NOW })).toThrow(/raw apiKey/);
62
- });
63
-
64
- test("rejects invalid manual provider base URL and empty models", () => {
65
- expect(() => normalizeManualProviderConfig({
66
- id: "local-openai",
67
- name: "Local OpenAI",
68
- baseUrl: "ftp://api.openai.example/v1",
69
- apiKeyEnv: "TB_TEST_PROVIDER_KEY",
70
- models: ["gpt-4o"]
71
- }, { now: NOW })).toThrow(/http or https/);
72
-
73
- expect(() => normalizeManualProviderConfig({
74
- id: "local-openai",
75
- name: "Local OpenAI",
76
- baseUrl: "https://api.openai.example/v1",
77
- apiKeyEnv: "TB_TEST_PROVIDER_KEY",
78
- models: [" "]
79
- }, { now: NOW })).toThrow(/models/);
80
- });
81
-
82
- test("rejects duplicate manual provider ids", () => {
83
- expect(() => normalizeManualProvidersConfig({
84
- version: 1,
85
- providers: [
86
- {
87
- id: "local-openai",
88
- name: "A",
89
- baseUrl: "https://a.example/v1",
90
- apiKeyEnv: "TB_PROVIDER_A",
91
- models: ["gpt-4o"]
92
- },
93
- {
94
- id: "local-openai",
95
- name: "B",
96
- baseUrl: "https://b.example/v1",
97
- apiKeyEnv: "TB_PROVIDER_B",
98
- models: ["gpt-4o"]
99
- }
100
- ]
101
- }, NOW)).toThrow(/duplicated/);
102
- });
103
-
104
- test("normalizes manual provider routing policy", () => {
105
- expect(normalizeManualProvidersConfig({
106
- version: 1,
107
- providers: [],
108
- routing: { policy: "fallback", lockedProviderId: "ignored-provider" }
109
- }, NOW).routing).toEqual({ policy: "fallback" });
110
-
111
- expect(normalizeManualProvidersConfig({
112
- version: 1,
113
- providers: [
114
- {
115
- id: "local-openai",
116
- name: "A",
117
- baseUrl: "https://a.example/v1",
118
- apiKeyEnv: "TB_PROVIDER_A",
119
- models: ["gpt-4o"]
120
- }
121
- ],
122
- routing: { policy: "locked", lockedProviderId: "local-openai" }
123
- }, NOW).routing).toEqual({ policy: "locked", lockedProviderId: "local-openai" });
124
-
125
- expect(() => normalizeManualProvidersConfig({
126
- version: 1,
127
- providers: [],
128
- routing: { policy: "locked" }
129
- }, NOW)).toThrow(/lockedProviderId/);
130
- });
131
-
132
- test("normalizes auto provider config to the bounded first-version surface", () => {
133
- expect(normalizeAutoProviderConfig({
134
- enabled: true,
135
- range: "custom",
136
- scorer: "speed",
137
- modelIds: [" gpt-4o ", "gpt-4o", "claude-sonnet-4-5"],
138
- sellerIds: ["tbs-a", "tbs-a", "tbs-b"],
139
- maxConcurrentProviders: 99
140
- }, NOW)).toEqual({
141
- enabled: true,
142
- range: "custom",
143
- scorer: "speed",
144
- modelIds: ["gpt-4o", "claude-sonnet-4-5"],
145
- sellerIds: ["tbs-a", "tbs-b"],
146
- maxConcurrentProviders: 10,
147
- updatedAt: NOW
148
- });
149
- });
150
- });
@@ -1,28 +0,0 @@
1
- import * as crypto from "crypto";
2
- import {
3
- DEFAULT_SELLER_REGISTRY_SIGNATURE_URL,
4
- DEFAULT_SELLER_REGISTRY_URL,
5
- signatureUrlForRegistryUrl,
6
- verifyRegistrySignatureWithKeys
7
- } from "../src/registry-trust.js";
8
-
9
- describe("registry trust", () => {
10
- test("derives the default signature URL", () => {
11
- expect(signatureUrlForRegistryUrl(DEFAULT_SELLER_REGISTRY_URL)).toBe(DEFAULT_SELLER_REGISTRY_SIGNATURE_URL);
12
- expect(signatureUrlForRegistryUrl("https://example.test/v1/registry.json")).toBe("https://example.test/v1/registry.sig");
13
- });
14
-
15
- test("verifies Ed25519 detached signatures with supplied keys", () => {
16
- const { privateKey, publicKey } = crypto.generateKeyPairSync("ed25519");
17
- const registry = JSON.stringify({ version: 1, sellers: [] });
18
- const signature = crypto.sign(null, Buffer.from(registry, "utf8"), privateKey).toString("base64url");
19
- const keyId = verifyRegistrySignatureWithKeys(registry, signature, {
20
- "test-key": publicKey.export({ format: "der", type: "spki" }).toString("base64")
21
- });
22
-
23
- expect(keyId).toBe("test-key");
24
- expect(() => verifyRegistrySignatureWithKeys(`${registry}\n`, signature, {
25
- "test-key": publicKey.export({ format: "der", type: "spki" }).toString("base64")
26
- })).toThrow("registry signature verification failed");
27
- });
28
- });