@tokenbuddy/tokenbuddy 1.0.35 → 1.0.37

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 (143) hide show
  1. package/dist/src/buyer-store.d.ts +6 -1
  2. package/dist/src/buyer-store.js +43 -4
  3. package/dist/src/cli.js +2 -2
  4. package/dist/src/daemon.d.ts +12 -0
  5. package/dist/src/daemon.js +791 -61
  6. package/dist/src/doctor-diagnostics.js +1 -6
  7. package/dist/src/provider-install.d.ts +2 -2
  8. package/dist/src/provider-install.js +248 -2
  9. package/dist/src/seller-catalog.d.ts +21 -0
  10. package/dist/src/seller-catalog.js +17 -0
  11. package/dist/src/seller-route-planner.d.ts +4 -1
  12. package/dist/src/seller-route-planner.js +3 -0
  13. package/dist/src/seller-routing-strategy.d.ts +3 -0
  14. package/dist/src/terminal-detect.d.ts +1 -1
  15. package/dist/src/terminal-detect.js +3 -2
  16. package/package.json +15 -2
  17. package/static/ui/assets/index-Djfl9tw5.js +271 -0
  18. package/static/ui/assets/index-DkfztCkn.css +1 -0
  19. package/static/ui/index.html +2 -2
  20. package/dist/src/buyer-store.d.ts.map +0 -1
  21. package/dist/src/buyer-store.js.map +0 -1
  22. package/dist/src/clawtip-bootstrap.d.ts.map +0 -1
  23. package/dist/src/clawtip-bootstrap.js.map +0 -1
  24. package/dist/src/cli.d.ts.map +0 -1
  25. package/dist/src/cli.js.map +0 -1
  26. package/dist/src/credit-tracker.d.ts.map +0 -1
  27. package/dist/src/credit-tracker.js.map +0 -1
  28. package/dist/src/daemon.d.ts.map +0 -1
  29. package/dist/src/daemon.js.map +0 -1
  30. package/dist/src/doctor-clawtip-wallet.d.ts.map +0 -1
  31. package/dist/src/doctor-clawtip-wallet.js.map +0 -1
  32. package/dist/src/doctor-diagnostics.d.ts.map +0 -1
  33. package/dist/src/doctor-diagnostics.js.map +0 -1
  34. package/dist/src/index.d.ts.map +0 -1
  35. package/dist/src/index.js.map +0 -1
  36. package/dist/src/init-clawtip-activation.d.ts.map +0 -1
  37. package/dist/src/init-clawtip-activation.js.map +0 -1
  38. package/dist/src/init-payment-options.d.ts.map +0 -1
  39. package/dist/src/init-payment-options.js.map +0 -1
  40. package/dist/src/init-setup.d.ts.map +0 -1
  41. package/dist/src/init-setup.js.map +0 -1
  42. package/dist/src/model-index.d.ts.map +0 -1
  43. package/dist/src/model-index.js.map +0 -1
  44. package/dist/src/package-update.d.ts.map +0 -1
  45. package/dist/src/package-update.js.map +0 -1
  46. package/dist/src/prewarm-cache.d.ts.map +0 -1
  47. package/dist/src/prewarm-cache.js.map +0 -1
  48. package/dist/src/prewarm-scheduler.d.ts.map +0 -1
  49. package/dist/src/prewarm-scheduler.js.map +0 -1
  50. package/dist/src/provider-install.d.ts.map +0 -1
  51. package/dist/src/provider-install.js.map +0 -1
  52. package/dist/src/provider-routing-config.d.ts.map +0 -1
  53. package/dist/src/provider-routing-config.js.map +0 -1
  54. package/dist/src/registry-trust.d.ts.map +0 -1
  55. package/dist/src/registry-trust.js.map +0 -1
  56. package/dist/src/route-failover.d.ts.map +0 -1
  57. package/dist/src/route-failover.js.map +0 -1
  58. package/dist/src/seller-catalog.d.ts.map +0 -1
  59. package/dist/src/seller-catalog.js.map +0 -1
  60. package/dist/src/seller-concurrency-limiter.d.ts.map +0 -1
  61. package/dist/src/seller-concurrency-limiter.js.map +0 -1
  62. package/dist/src/seller-metadata-cache.d.ts.map +0 -1
  63. package/dist/src/seller-metadata-cache.js.map +0 -1
  64. package/dist/src/seller-pool.d.ts.map +0 -1
  65. package/dist/src/seller-pool.js.map +0 -1
  66. package/dist/src/seller-route-planner.d.ts.map +0 -1
  67. package/dist/src/seller-route-planner.js.map +0 -1
  68. package/dist/src/seller-routing-config.d.ts.map +0 -1
  69. package/dist/src/seller-routing-config.js.map +0 -1
  70. package/dist/src/seller-routing-strategy.d.ts.map +0 -1
  71. package/dist/src/seller-routing-strategy.js.map +0 -1
  72. package/dist/src/stream-failover.d.ts.map +0 -1
  73. package/dist/src/stream-failover.js.map +0 -1
  74. package/dist/src/tb-clawtip-proof.d.ts.map +0 -1
  75. package/dist/src/tb-clawtip-proof.js.map +0 -1
  76. package/dist/src/tb-proxyd.d.ts.map +0 -1
  77. package/dist/src/tb-proxyd.js.map +0 -1
  78. package/dist/src/terminal-detect.d.ts.map +0 -1
  79. package/dist/src/terminal-detect.js.map +0 -1
  80. package/dist/src/terminal-image.d.ts.map +0 -1
  81. package/dist/src/terminal-image.js.map +0 -1
  82. package/src/buyer-store.ts +0 -1090
  83. package/src/clawtip-bootstrap.ts +0 -65
  84. package/src/cli.ts +0 -2243
  85. package/src/credit-tracker.ts +0 -295
  86. package/src/daemon.ts +0 -5475
  87. package/src/doctor-clawtip-wallet.ts +0 -95
  88. package/src/doctor-diagnostics.ts +0 -1026
  89. package/src/index.ts +0 -16
  90. package/src/init-clawtip-activation.ts +0 -695
  91. package/src/init-payment-options.ts +0 -373
  92. package/src/init-setup.ts +0 -165
  93. package/src/model-index.ts +0 -278
  94. package/src/package-update.ts +0 -311
  95. package/src/prewarm-cache.ts +0 -485
  96. package/src/prewarm-scheduler.ts +0 -675
  97. package/src/provider-install.ts +0 -1006
  98. package/src/provider-routing-config.ts +0 -410
  99. package/src/registry-trust.ts +0 -51
  100. package/src/route-failover.ts +0 -304
  101. package/src/seller-catalog.ts +0 -505
  102. package/src/seller-concurrency-limiter.ts +0 -161
  103. package/src/seller-metadata-cache.ts +0 -91
  104. package/src/seller-pool.ts +0 -557
  105. package/src/seller-route-planner.ts +0 -513
  106. package/src/seller-routing-config.ts +0 -211
  107. package/src/seller-routing-strategy.ts +0 -362
  108. package/src/stream-failover.ts +0 -152
  109. package/src/tb-clawtip-proof.ts +0 -28
  110. package/src/tb-proxyd.ts +0 -101
  111. package/src/terminal-detect.ts +0 -333
  112. package/src/terminal-image.ts +0 -228
  113. package/static/ui/assets/index-0MVXD7bH.css +0 -1
  114. package/static/ui/assets/index-BVbeDEwq.js +0 -271
  115. package/static/ui/assets/index-BVbeDEwq.js.map +0 -1
  116. package/tests/cli-routing.test.ts +0 -363
  117. package/tests/control-plane-ui-endpoints.test.ts +0 -1630
  118. package/tests/credit-tracker.test.ts +0 -165
  119. package/tests/daemon-413-fallback.test.ts +0 -92
  120. package/tests/daemon-classify.test.ts +0 -452
  121. package/tests/daemon-roles.test.ts +0 -92
  122. package/tests/daemon-trusted-registry-cache.test.ts +0 -132
  123. package/tests/e2e.test.ts +0 -366
  124. package/tests/image-generation-e2e.test.ts +0 -230
  125. package/tests/model-index.test.ts +0 -198
  126. package/tests/package-update.test.ts +0 -147
  127. package/tests/prewarm-cache.test.ts +0 -296
  128. package/tests/prewarm-scheduler.test.ts +0 -367
  129. package/tests/provider-routing-config.test.ts +0 -150
  130. package/tests/registry-trust.test.ts +0 -28
  131. package/tests/route-failover.test.ts +0 -222
  132. package/tests/seller-catalog-413.test.ts +0 -120
  133. package/tests/seller-catalog-utilities.test.ts +0 -124
  134. package/tests/seller-concurrency-limiter.test.ts +0 -83
  135. package/tests/seller-metadata-cache.test.ts +0 -89
  136. package/tests/seller-pool.test.ts +0 -365
  137. package/tests/seller-route-planner.test.ts +0 -312
  138. package/tests/seller-routing-config.test.ts +0 -124
  139. package/tests/seller-routing-strategy.test.ts +0 -167
  140. package/tests/stream-failover.test.ts +0 -52
  141. package/tests/thousand-seller.test.ts +0 -151
  142. package/tests/tokenbuddy.test.ts +0 -4043
  143. 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
- });