@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.
- package/dist/src/buyer-store.d.ts +7 -2
- package/dist/src/buyer-store.js +46 -7
- package/dist/src/cli.d.ts +1 -0
- package/dist/src/cli.js +15 -7
- package/dist/src/daemon.d.ts +12 -0
- package/dist/src/daemon.js +791 -61
- package/dist/src/doctor-diagnostics.js +1 -6
- package/dist/src/provider-install.d.ts +2 -2
- package/dist/src/provider-install.js +248 -2
- package/dist/src/seller-catalog.d.ts +21 -0
- package/dist/src/seller-catalog.js +17 -0
- package/dist/src/seller-route-planner.d.ts +4 -1
- package/dist/src/seller-route-planner.js +3 -0
- package/dist/src/seller-routing-strategy.d.ts +3 -0
- package/dist/src/terminal-detect.d.ts +1 -1
- package/dist/src/terminal-detect.js +3 -2
- package/dist/src/workdir.d.ts +10 -0
- package/dist/src/workdir.js +26 -0
- package/package.json +15 -2
- package/static/ui/assets/index-Djfl9tw5.js +271 -0
- package/static/ui/assets/index-DkfztCkn.css +1 -0
- package/static/ui/index.html +2 -2
- package/dist/src/buyer-store.d.ts.map +0 -1
- package/dist/src/buyer-store.js.map +0 -1
- package/dist/src/clawtip-bootstrap.d.ts.map +0 -1
- package/dist/src/clawtip-bootstrap.js.map +0 -1
- package/dist/src/cli.d.ts.map +0 -1
- package/dist/src/cli.js.map +0 -1
- package/dist/src/credit-tracker.d.ts.map +0 -1
- package/dist/src/credit-tracker.js.map +0 -1
- package/dist/src/daemon.d.ts.map +0 -1
- package/dist/src/daemon.js.map +0 -1
- package/dist/src/doctor-clawtip-wallet.d.ts.map +0 -1
- package/dist/src/doctor-clawtip-wallet.js.map +0 -1
- package/dist/src/doctor-diagnostics.d.ts.map +0 -1
- package/dist/src/doctor-diagnostics.js.map +0 -1
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js.map +0 -1
- package/dist/src/init-clawtip-activation.d.ts.map +0 -1
- package/dist/src/init-clawtip-activation.js.map +0 -1
- package/dist/src/init-payment-options.d.ts.map +0 -1
- package/dist/src/init-payment-options.js.map +0 -1
- package/dist/src/init-setup.d.ts.map +0 -1
- package/dist/src/init-setup.js.map +0 -1
- package/dist/src/model-index.d.ts.map +0 -1
- package/dist/src/model-index.js.map +0 -1
- package/dist/src/package-update.d.ts.map +0 -1
- package/dist/src/package-update.js.map +0 -1
- package/dist/src/prewarm-cache.d.ts.map +0 -1
- package/dist/src/prewarm-cache.js.map +0 -1
- package/dist/src/prewarm-scheduler.d.ts.map +0 -1
- package/dist/src/prewarm-scheduler.js.map +0 -1
- package/dist/src/provider-install.d.ts.map +0 -1
- package/dist/src/provider-install.js.map +0 -1
- package/dist/src/provider-routing-config.d.ts.map +0 -1
- package/dist/src/provider-routing-config.js.map +0 -1
- package/dist/src/registry-trust.d.ts.map +0 -1
- package/dist/src/registry-trust.js.map +0 -1
- package/dist/src/route-failover.d.ts.map +0 -1
- package/dist/src/route-failover.js.map +0 -1
- package/dist/src/seller-catalog.d.ts.map +0 -1
- package/dist/src/seller-catalog.js.map +0 -1
- package/dist/src/seller-concurrency-limiter.d.ts.map +0 -1
- package/dist/src/seller-concurrency-limiter.js.map +0 -1
- package/dist/src/seller-metadata-cache.d.ts.map +0 -1
- package/dist/src/seller-metadata-cache.js.map +0 -1
- package/dist/src/seller-pool.d.ts.map +0 -1
- package/dist/src/seller-pool.js.map +0 -1
- package/dist/src/seller-route-planner.d.ts.map +0 -1
- package/dist/src/seller-route-planner.js.map +0 -1
- package/dist/src/seller-routing-config.d.ts.map +0 -1
- package/dist/src/seller-routing-config.js.map +0 -1
- package/dist/src/seller-routing-strategy.d.ts.map +0 -1
- package/dist/src/seller-routing-strategy.js.map +0 -1
- package/dist/src/stream-failover.d.ts.map +0 -1
- package/dist/src/stream-failover.js.map +0 -1
- package/dist/src/tb-clawtip-proof.d.ts.map +0 -1
- package/dist/src/tb-clawtip-proof.js.map +0 -1
- package/dist/src/tb-proxyd.d.ts.map +0 -1
- package/dist/src/tb-proxyd.js.map +0 -1
- package/dist/src/terminal-detect.d.ts.map +0 -1
- package/dist/src/terminal-detect.js.map +0 -1
- package/dist/src/terminal-image.d.ts.map +0 -1
- package/dist/src/terminal-image.js.map +0 -1
- package/src/buyer-store.ts +0 -1090
- package/src/clawtip-bootstrap.ts +0 -65
- package/src/cli.ts +0 -2243
- package/src/credit-tracker.ts +0 -295
- package/src/daemon.ts +0 -5475
- package/src/doctor-clawtip-wallet.ts +0 -95
- package/src/doctor-diagnostics.ts +0 -1026
- package/src/index.ts +0 -16
- package/src/init-clawtip-activation.ts +0 -695
- package/src/init-payment-options.ts +0 -373
- package/src/init-setup.ts +0 -165
- package/src/model-index.ts +0 -278
- package/src/package-update.ts +0 -311
- package/src/prewarm-cache.ts +0 -485
- package/src/prewarm-scheduler.ts +0 -675
- package/src/provider-install.ts +0 -1006
- package/src/provider-routing-config.ts +0 -410
- package/src/registry-trust.ts +0 -51
- package/src/route-failover.ts +0 -304
- package/src/seller-catalog.ts +0 -505
- package/src/seller-concurrency-limiter.ts +0 -161
- package/src/seller-metadata-cache.ts +0 -91
- package/src/seller-pool.ts +0 -557
- package/src/seller-route-planner.ts +0 -513
- package/src/seller-routing-config.ts +0 -211
- package/src/seller-routing-strategy.ts +0 -362
- package/src/stream-failover.ts +0 -152
- package/src/tb-clawtip-proof.ts +0 -28
- package/src/tb-proxyd.ts +0 -101
- package/src/terminal-detect.ts +0 -333
- package/src/terminal-image.ts +0 -228
- package/static/ui/assets/index-0MVXD7bH.css +0 -1
- package/static/ui/assets/index-BVbeDEwq.js +0 -271
- package/static/ui/assets/index-BVbeDEwq.js.map +0 -1
- package/tests/cli-routing.test.ts +0 -363
- package/tests/control-plane-ui-endpoints.test.ts +0 -1630
- package/tests/credit-tracker.test.ts +0 -165
- package/tests/daemon-413-fallback.test.ts +0 -92
- package/tests/daemon-classify.test.ts +0 -452
- package/tests/daemon-roles.test.ts +0 -92
- package/tests/daemon-trusted-registry-cache.test.ts +0 -132
- package/tests/e2e.test.ts +0 -366
- package/tests/image-generation-e2e.test.ts +0 -230
- package/tests/model-index.test.ts +0 -198
- package/tests/package-update.test.ts +0 -147
- package/tests/prewarm-cache.test.ts +0 -296
- package/tests/prewarm-scheduler.test.ts +0 -367
- package/tests/provider-routing-config.test.ts +0 -150
- package/tests/registry-trust.test.ts +0 -28
- package/tests/route-failover.test.ts +0 -222
- package/tests/seller-catalog-413.test.ts +0 -120
- package/tests/seller-catalog-utilities.test.ts +0 -124
- package/tests/seller-concurrency-limiter.test.ts +0 -83
- package/tests/seller-metadata-cache.test.ts +0 -89
- package/tests/seller-pool.test.ts +0 -365
- package/tests/seller-route-planner.test.ts +0 -312
- package/tests/seller-routing-config.test.ts +0 -124
- package/tests/seller-routing-strategy.test.ts +0 -167
- package/tests/stream-failover.test.ts +0 -52
- package/tests/thousand-seller.test.ts +0 -151
- package/tests/tokenbuddy.test.ts +0 -4043
- package/tsconfig.json +0 -8
|
@@ -1,365 +0,0 @@
|
|
|
1
|
-
import { CreditTracker } from "../src/credit-tracker.js";
|
|
2
|
-
import { ModelIndex } from "../src/model-index.js";
|
|
3
|
-
import { PrewarmCache } from "../src/prewarm-cache.js";
|
|
4
|
-
import { SellerPool, type FailureKind } from "../src/seller-pool.js";
|
|
5
|
-
import type { RegistrySeller } from "../src/seller-catalog.js";
|
|
6
|
-
|
|
7
|
-
function makeSeller(overrides: Partial<RegistrySeller> & { id: string; models?: string[] }): RegistrySeller {
|
|
8
|
-
return {
|
|
9
|
-
id: overrides.id,
|
|
10
|
-
name: overrides.name ?? overrides.id,
|
|
11
|
-
url: overrides.url ?? `https://${overrides.id}.example.com`,
|
|
12
|
-
supportedProtocols: overrides.supportedProtocols ?? ["chat_completions"],
|
|
13
|
-
paymentMethods: overrides.paymentMethods ?? ["clawtip"],
|
|
14
|
-
models: overrides.models
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function makeClock(start = 1_000_000): { now: number; advance: (ms: number) => void } {
|
|
19
|
-
const clock = { now: start, advance: (ms: number) => { clock.now += ms; } };
|
|
20
|
-
return clock;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function build(seed: { id: string; healthScore?: number }[] = []): { index: ModelIndex; cache: PrewarmCache; credit: CreditTracker; sellers: RegistrySeller[] } {
|
|
24
|
-
const sellers: RegistrySeller[] = seed.map((s) =>
|
|
25
|
-
makeSeller({ id: s.id, models: ["gpt-4o"] })
|
|
26
|
-
);
|
|
27
|
-
const index = new ModelIndex();
|
|
28
|
-
index.rebuild(sellers, { registryVersion: 1, defaultSellerId: seed[0]?.id });
|
|
29
|
-
const cache = new PrewarmCache();
|
|
30
|
-
cache.commitWarm({
|
|
31
|
-
modelId: "gpt-4o",
|
|
32
|
-
protocol: "chat_completions",
|
|
33
|
-
paymentMethod: "clawtip",
|
|
34
|
-
candidates: seed.map((s) => ({
|
|
35
|
-
sellerId: s.id,
|
|
36
|
-
url: `https://${s.id}.example.com`,
|
|
37
|
-
healthScore: s.healthScore ?? 80
|
|
38
|
-
}))
|
|
39
|
-
});
|
|
40
|
-
const credit = new CreditTracker();
|
|
41
|
-
return { index, cache, credit, sellers };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
describe("SellerPool", () => {
|
|
45
|
-
test("sync rebuilds entries from the prewarm cache", () => {
|
|
46
|
-
const ctx = build([{ id: "s1" }, { id: "s2" }]);
|
|
47
|
-
const pool = new SellerPool({
|
|
48
|
-
modelIndex: ctx.index,
|
|
49
|
-
cache: ctx.cache,
|
|
50
|
-
creditTracker: ctx.credit
|
|
51
|
-
});
|
|
52
|
-
const size = pool.sync();
|
|
53
|
-
expect(size).toBe(2);
|
|
54
|
-
expect(pool.snapshot().map((e) => e.sellerId).sort()).toEqual(["s1", "s2"]);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("sync drops entries whose seller disappeared from the registry index", () => {
|
|
58
|
-
const ctx = build([{ id: "s1" }, { id: "s2" }]);
|
|
59
|
-
const pool = new SellerPool({
|
|
60
|
-
modelIndex: ctx.index,
|
|
61
|
-
cache: ctx.cache,
|
|
62
|
-
creditTracker: ctx.credit
|
|
63
|
-
});
|
|
64
|
-
pool.sync();
|
|
65
|
-
expect(pool.size()).toBe(2);
|
|
66
|
-
|
|
67
|
-
// Mutate the index so only s1 remains.
|
|
68
|
-
ctx.index.rebuild([makeSeller({ id: "s1", models: ["gpt-4o"] })], { registryVersion: 2 });
|
|
69
|
-
const size = pool.sync();
|
|
70
|
-
expect(size).toBe(1);
|
|
71
|
-
expect(pool.snapshot().map((e) => e.sellerId)).toEqual(["s1"]);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test("pick returns candidates sorted by health score and skips open circuit entries", () => {
|
|
75
|
-
const ctx = build([{ id: "s1", healthScore: 90 }, { id: "s2", healthScore: 50 }, { id: "s3", healthScore: 70 }]);
|
|
76
|
-
const pool = new SellerPool({ modelIndex: ctx.index, cache: ctx.cache, creditTracker: ctx.credit });
|
|
77
|
-
pool.sync();
|
|
78
|
-
|
|
79
|
-
const result = pool.pick({ modelId: "gpt-4o", protocol: "chat_completions", paymentMethod: "clawtip" });
|
|
80
|
-
expect(result.candidates.map((c) => c.entry.sellerId)).toEqual(["s1", "s3", "s2"]);
|
|
81
|
-
|
|
82
|
-
// Force s1 into the open circuit and verify it is skipped.
|
|
83
|
-
pool.markOpen("s1", "registry_gone");
|
|
84
|
-
const filtered = pool.pick({ modelId: "gpt-4o", protocol: "chat_completions", paymentMethod: "clawtip" });
|
|
85
|
-
expect(filtered.candidates.map((c) => c.entry.sellerId)).toEqual(["s3", "s2"]);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("half_open recycle happens after openStateMs has elapsed", () => {
|
|
89
|
-
const clock = makeClock();
|
|
90
|
-
const ctx = build([{ id: "s1" }]);
|
|
91
|
-
const pool = new SellerPool({
|
|
92
|
-
modelIndex: ctx.index,
|
|
93
|
-
cache: ctx.cache,
|
|
94
|
-
creditTracker: ctx.credit,
|
|
95
|
-
failureThreshold: 1, // open after the very first failure to keep the test focused
|
|
96
|
-
openStateMs: 1000,
|
|
97
|
-
now: () => clock.now
|
|
98
|
-
});
|
|
99
|
-
pool.sync();
|
|
100
|
-
pool.recordFailure("s1", "soft_5xx");
|
|
101
|
-
expect(pool.snapshot()[0].circuit).toBe("open");
|
|
102
|
-
|
|
103
|
-
// Within the open window: still skipped.
|
|
104
|
-
clock.advance(500);
|
|
105
|
-
let result = pool.pick({ modelId: "gpt-4o", protocol: "chat_completions", paymentMethod: "clawtip" });
|
|
106
|
-
expect(result.candidates).toEqual([]);
|
|
107
|
-
|
|
108
|
-
// After the open window: recycled to half_open and re-included.
|
|
109
|
-
clock.advance(600);
|
|
110
|
-
result = pool.pick({ modelId: "gpt-4o", protocol: "chat_completions", paymentMethod: "clawtip" });
|
|
111
|
-
expect(result.candidates.map((c) => c.entry.sellerId)).toEqual(["s1"]);
|
|
112
|
-
expect(pool.snapshot()[0].circuit).toBe("half_open");
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test("recycleOpenCircuits refreshes snapshot-based route planning state", () => {
|
|
116
|
-
const clock = makeClock();
|
|
117
|
-
const ctx = build([{ id: "s1" }, { id: "s2" }]);
|
|
118
|
-
const pool = new SellerPool({
|
|
119
|
-
modelIndex: ctx.index,
|
|
120
|
-
cache: ctx.cache,
|
|
121
|
-
creditTracker: ctx.credit,
|
|
122
|
-
failureThreshold: 1,
|
|
123
|
-
openStateMs: 1000,
|
|
124
|
-
now: () => clock.now
|
|
125
|
-
});
|
|
126
|
-
pool.sync();
|
|
127
|
-
pool.recordFailure("s1", "soft_5xx");
|
|
128
|
-
|
|
129
|
-
clock.advance(500);
|
|
130
|
-
expect(pool.recycleOpenCircuits()).toBe(0);
|
|
131
|
-
expect(pool.snapshot().find((entry) => entry.sellerId === "s1")?.circuit).toBe("open");
|
|
132
|
-
|
|
133
|
-
clock.advance(600);
|
|
134
|
-
expect(pool.recycleOpenCircuits()).toBe(1);
|
|
135
|
-
expect(pool.snapshot().find((entry) => entry.sellerId === "s1")?.circuit).toBe("half_open");
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test("recordFailure escalates to open after the configured threshold", () => {
|
|
139
|
-
const clock = makeClock();
|
|
140
|
-
const ctx = build([{ id: "s1" }]);
|
|
141
|
-
const pool = new SellerPool({
|
|
142
|
-
modelIndex: ctx.index,
|
|
143
|
-
cache: ctx.cache,
|
|
144
|
-
creditTracker: ctx.credit,
|
|
145
|
-
failureThreshold: 3,
|
|
146
|
-
now: () => clock.now
|
|
147
|
-
});
|
|
148
|
-
pool.sync();
|
|
149
|
-
|
|
150
|
-
pool.recordFailure("s1", "soft_5xx");
|
|
151
|
-
expect(pool.snapshot()[0].circuit).toBe("closed");
|
|
152
|
-
pool.recordFailure("s1", "soft_5xx");
|
|
153
|
-
expect(pool.snapshot()[0].circuit).toBe("closed");
|
|
154
|
-
pool.recordFailure("s1", "soft_5xx");
|
|
155
|
-
expect(pool.snapshot()[0].circuit).toBe("open");
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
test("busy_capacity temporarily blocks selection without opening the circuit", () => {
|
|
159
|
-
const clock = makeClock();
|
|
160
|
-
const ctx = build([{ id: "s1", healthScore: 90 }, { id: "s2", healthScore: 50 }]);
|
|
161
|
-
const pool = new SellerPool({
|
|
162
|
-
modelIndex: ctx.index,
|
|
163
|
-
cache: ctx.cache,
|
|
164
|
-
creditTracker: ctx.credit,
|
|
165
|
-
failureThreshold: 1,
|
|
166
|
-
capacityBlockMs: 1000,
|
|
167
|
-
now: () => clock.now
|
|
168
|
-
});
|
|
169
|
-
pool.sync();
|
|
170
|
-
|
|
171
|
-
const blocked = pool.recordFailure("s1", "busy_capacity");
|
|
172
|
-
expect(blocked?.circuit).toBe("closed");
|
|
173
|
-
expect(blocked?.consecutiveFailures).toBe(0);
|
|
174
|
-
expect(blocked?.recentFailures).toEqual([]);
|
|
175
|
-
expect(blocked?.capacityBlockedUntil).toBe(clock.now + 1000);
|
|
176
|
-
|
|
177
|
-
let result = pool.pick({ modelId: "gpt-4o", protocol: "chat_completions", paymentMethod: "clawtip" });
|
|
178
|
-
expect(result.candidates.map((c) => c.entry.sellerId)).toEqual(["s2"]);
|
|
179
|
-
|
|
180
|
-
clock.advance(1001);
|
|
181
|
-
result = pool.pick({ modelId: "gpt-4o", protocol: "chat_completions", paymentMethod: "clawtip" });
|
|
182
|
-
expect(result.candidates.map((c) => c.entry.sellerId)).toEqual(["s1", "s2"]);
|
|
183
|
-
|
|
184
|
-
pool.recordFailure("s1", "busy_capacity");
|
|
185
|
-
pool.recordSuccess("s1", 250_000);
|
|
186
|
-
expect(pool.snapshot().find((entry) => entry.sellerId === "s1")?.capacityBlockedUntil).toBeUndefined();
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
test("sync preserves registry fallback runtime state for sellers still in the registry", () => {
|
|
190
|
-
const clock = makeClock();
|
|
191
|
-
const index = new ModelIndex();
|
|
192
|
-
const sellers = [makeSeller({ id: "s1", models: ["gpt-4o"] })];
|
|
193
|
-
index.rebuild(sellers, { registryVersion: 1 });
|
|
194
|
-
const cache = new PrewarmCache();
|
|
195
|
-
const credit = new CreditTracker();
|
|
196
|
-
const pool = new SellerPool({
|
|
197
|
-
modelIndex: index,
|
|
198
|
-
cache,
|
|
199
|
-
creditTracker: credit,
|
|
200
|
-
capacityBlockMs: 1000,
|
|
201
|
-
now: () => clock.now
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
pool.ensureRegistrySellers(sellers);
|
|
205
|
-
pool.recordFailure("s1", "busy_capacity");
|
|
206
|
-
pool.sync();
|
|
207
|
-
|
|
208
|
-
expect(pool.snapshot()[0].capacityBlockedUntil).toBe(clock.now + 1000);
|
|
209
|
-
index.rebuild([], { registryVersion: 2 });
|
|
210
|
-
pool.sync();
|
|
211
|
-
expect(pool.snapshot()).toEqual([]);
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
test("recordSuccess closes the circuit and reports to the credit tracker", () => {
|
|
215
|
-
const clock = makeClock();
|
|
216
|
-
const ctx = build([{ id: "s1" }]);
|
|
217
|
-
const pool = new SellerPool({
|
|
218
|
-
modelIndex: ctx.index,
|
|
219
|
-
cache: ctx.cache,
|
|
220
|
-
creditTracker: ctx.credit,
|
|
221
|
-
failureThreshold: 1, // open after the first failure
|
|
222
|
-
now: () => clock.now
|
|
223
|
-
});
|
|
224
|
-
pool.sync();
|
|
225
|
-
|
|
226
|
-
ctx.credit.recordPurchase("s1", 1_000_000, 1_000_000);
|
|
227
|
-
pool.recordFailure("s1", "soft_5xx");
|
|
228
|
-
expect(pool.snapshot()[0].circuit).toBe("open");
|
|
229
|
-
|
|
230
|
-
clock.advance(31_000); // wait past the open window
|
|
231
|
-
pool.recordSuccess("s1", 250_000);
|
|
232
|
-
const entry = pool.snapshot()[0];
|
|
233
|
-
expect(entry.circuit).toBe("closed");
|
|
234
|
-
expect(entry.consecutiveFailures).toBe(0);
|
|
235
|
-
expect(ctx.credit.getEntry("s1")?.currentBalanceMicros).toBe(250_000);
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
test("recordRuntimeMetrics updates speed telemetry without changing credit", () => {
|
|
239
|
-
const ctx = build([{ id: "s1" }]);
|
|
240
|
-
const pool = new SellerPool({ modelIndex: ctx.index, cache: ctx.cache, creditTracker: ctx.credit });
|
|
241
|
-
pool.sync();
|
|
242
|
-
ctx.credit.recordPurchase("s1", 1_000_000, 250_000);
|
|
243
|
-
|
|
244
|
-
const entry = pool.recordRuntimeMetrics("s1", {
|
|
245
|
-
ttftMs: 123,
|
|
246
|
-
avgInferenceMs: 456,
|
|
247
|
-
avgTokensPerSecond: 78.9
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
expect(entry).toMatchObject({
|
|
251
|
-
sellerId: "s1",
|
|
252
|
-
ttftMs: 123,
|
|
253
|
-
avgInferenceMs: 456,
|
|
254
|
-
avgLatencyMs: 456,
|
|
255
|
-
avgTokensPerSecond: 78.9
|
|
256
|
-
});
|
|
257
|
-
expect(ctx.credit.getEntry("s1")?.currentBalanceMicros).toBe(250_000);
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
test("sync preserves live runtime speed metrics when prewarm has no newer values", () => {
|
|
261
|
-
const ctx = build([{ id: "s1" }]);
|
|
262
|
-
const pool = new SellerPool({ modelIndex: ctx.index, cache: ctx.cache, creditTracker: ctx.credit });
|
|
263
|
-
pool.sync();
|
|
264
|
-
pool.recordRuntimeMetrics("s1", {
|
|
265
|
-
ttftMs: 123,
|
|
266
|
-
avgInferenceMs: 456,
|
|
267
|
-
avgTokensPerSecond: 78.9
|
|
268
|
-
}, 2_000_000);
|
|
269
|
-
ctx.cache.commitWarm({
|
|
270
|
-
modelId: "gpt-4o",
|
|
271
|
-
protocol: "chat_completions",
|
|
272
|
-
paymentMethod: "clawtip",
|
|
273
|
-
candidates: [{
|
|
274
|
-
sellerId: "s1",
|
|
275
|
-
url: "https://s1.example.com",
|
|
276
|
-
healthScore: 80,
|
|
277
|
-
lastSuccessAt: 1_000_000,
|
|
278
|
-
ttftMs: 1,
|
|
279
|
-
avgInferenceMs: 2,
|
|
280
|
-
avgTokensPerSecond: 0
|
|
281
|
-
}]
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
pool.sync();
|
|
285
|
-
|
|
286
|
-
expect(pool.snapshot()[0]).toMatchObject({
|
|
287
|
-
ttftMs: 123,
|
|
288
|
-
avgInferenceMs: 456,
|
|
289
|
-
avgTokensPerSecond: 78.9
|
|
290
|
-
});
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
test("hard failure kinds (hard_4xx, auth_invalid) immediately open the circuit and transfer leftover", () => {
|
|
294
|
-
const ctx = build([{ id: "s1" }]);
|
|
295
|
-
const pool = new SellerPool({ modelIndex: ctx.index, cache: ctx.cache, creditTracker: ctx.credit });
|
|
296
|
-
pool.sync();
|
|
297
|
-
|
|
298
|
-
ctx.credit.recordPurchase("s1", 1_000_000, 500_000);
|
|
299
|
-
const kinds: FailureKind[] = ["hard_4xx", "auth_invalid"];
|
|
300
|
-
for (const kind of kinds) {
|
|
301
|
-
ctx.credit.recordPurchase("s1", 1_000_000, 500_000);
|
|
302
|
-
const entry = pool.recordFailure("s1", kind, { reason: "test" });
|
|
303
|
-
expect(entry?.circuit).toBe("open");
|
|
304
|
-
}
|
|
305
|
-
const summary = ctx.credit.summary();
|
|
306
|
-
expect(summary.totalWastedMicros).toBeGreaterThan(0);
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
test("inspect surfaces freshPurchase and autoPurchaseAvailable flags", () => {
|
|
310
|
-
const ctx = build([{ id: "s1" }]);
|
|
311
|
-
const pool = new SellerPool({ modelIndex: ctx.index, cache: ctx.cache, creditTracker: ctx.credit });
|
|
312
|
-
pool.sync();
|
|
313
|
-
|
|
314
|
-
ctx.credit.recordPurchase("s1", 1_000_000, 1_000_000);
|
|
315
|
-
const info = pool.inspect("s1");
|
|
316
|
-
expect(info.entry?.sellerId).toBe("s1");
|
|
317
|
-
expect(info.freshPurchase).toBe(true);
|
|
318
|
-
expect(info.autoPurchaseAvailable).toBe(true);
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
test("markOpen force-opens a circuit without changing other state", () => {
|
|
322
|
-
const ctx = build([{ id: "s1" }]);
|
|
323
|
-
const pool = new SellerPool({ modelIndex: ctx.index, cache: ctx.cache, creditTracker: ctx.credit });
|
|
324
|
-
pool.sync();
|
|
325
|
-
pool.markOpen("s1", "registry_disappeared");
|
|
326
|
-
expect(pool.snapshot()[0].circuit).toBe("open");
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
test("pick returns an empty result when the prewarm cache has no entry for the model", () => {
|
|
330
|
-
const index = new ModelIndex();
|
|
331
|
-
index.rebuild([makeSeller({ id: "s1", models: ["gpt-4o"] })], { registryVersion: 1 });
|
|
332
|
-
const cache = new PrewarmCache();
|
|
333
|
-
const credit = new CreditTracker();
|
|
334
|
-
const pool = new SellerPool({ modelIndex: index, cache, creditTracker: credit });
|
|
335
|
-
pool.sync();
|
|
336
|
-
|
|
337
|
-
const result = pool.pick({ modelId: "gpt-4o", protocol: "chat_completions", paymentMethod: "clawtip" });
|
|
338
|
-
expect(result.candidates).toEqual([]);
|
|
339
|
-
// No prewarm has been committed for this model yet, so the pool
|
|
340
|
-
// surfaces "no_prewarm_candidates" (not "prewarm_cache_empty"); the
|
|
341
|
-
// distinction matters for the caller deciding whether to schedule a
|
|
342
|
-
// lazy prewarm.
|
|
343
|
-
expect(result.reason).toBe("no_prewarm_candidates");
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
test("pick falls back to the registry index when the cache has no entry yet", () => {
|
|
347
|
-
const index = new ModelIndex();
|
|
348
|
-
index.rebuild(
|
|
349
|
-
[makeSeller({ id: "s1", models: ["gpt-4o"] }), makeSeller({ id: "s2", models: ["gpt-4o"] })],
|
|
350
|
-
{ registryVersion: 1 }
|
|
351
|
-
);
|
|
352
|
-
const cache = new PrewarmCache();
|
|
353
|
-
const credit = new CreditTracker();
|
|
354
|
-
const pool = new SellerPool({ modelIndex: index, cache, creditTracker: credit });
|
|
355
|
-
pool.sync();
|
|
356
|
-
|
|
357
|
-
// No probe has run yet, so pool is empty. pick should report "no candidates" and
|
|
358
|
-
// also surface the registry-level model resolution so the caller can decide
|
|
359
|
-
// whether to schedule a lazy prewarm.
|
|
360
|
-
const result = pool.pick({ modelId: "gpt-4o", protocol: "chat_completions", paymentMethod: "clawtip" });
|
|
361
|
-
expect(result.candidates).toEqual([]);
|
|
362
|
-
expect(result.resolved.matched).toBe(true);
|
|
363
|
-
expect(result.resolved.candidates).toHaveLength(2);
|
|
364
|
-
});
|
|
365
|
-
});
|
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
import { planSellerRouteSet, type SellerRoutePlannerInput } from "../src/seller-route-planner.js";
|
|
2
|
-
import type { RegistrySeller } from "../src/seller-catalog.js";
|
|
3
|
-
|
|
4
|
-
function seller(overrides: Partial<RegistrySeller> & { id: string; models?: string[] }): RegistrySeller {
|
|
5
|
-
return {
|
|
6
|
-
id: overrides.id,
|
|
7
|
-
name: overrides.name ?? overrides.id,
|
|
8
|
-
status: overrides.status,
|
|
9
|
-
url: overrides.url ?? `https://${overrides.id}.example.com`,
|
|
10
|
-
supportedProtocols: overrides.supportedProtocols ?? ["chat_completions"],
|
|
11
|
-
paymentMethods: overrides.paymentMethods ?? ["clawtip"],
|
|
12
|
-
models: overrides.models ?? ["gpt-5.4"]
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function plan(overrides: Partial<SellerRoutePlannerInput> = {}) {
|
|
17
|
-
return planSellerRouteSet({
|
|
18
|
-
modelId: "gpt-5.4",
|
|
19
|
-
protocol: "chat_completions",
|
|
20
|
-
paymentMethod: "clawtip",
|
|
21
|
-
registrySellers: [
|
|
22
|
-
seller({ id: "s1" }),
|
|
23
|
-
seller({ id: "s2" }),
|
|
24
|
-
seller({ id: "s3", supportedProtocols: ["responses"] })
|
|
25
|
-
],
|
|
26
|
-
routing: { mode: "fullAuto", scorer: "balanced" },
|
|
27
|
-
...overrides
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
describe("seller route planner", () => {
|
|
32
|
-
test("uses compatible prewarm candidates before registry fallback", () => {
|
|
33
|
-
const result = plan({
|
|
34
|
-
prewarmCandidates: [
|
|
35
|
-
{ sellerId: "s2", url: "https://s2.example.com", healthScore: 95, avgLatencyMs: 120, avgTokensPerSecond: 42.5 },
|
|
36
|
-
{ sellerId: "s1", url: "https://s1.example.com", healthScore: 50, avgLatencyMs: 80 },
|
|
37
|
-
{ sellerId: "missing", url: "https://missing.example.com", healthScore: 100, avgLatencyMs: 1 }
|
|
38
|
-
],
|
|
39
|
-
sellerMetrics: [
|
|
40
|
-
{ sellerId: "s1", discountRatio: 0.5 },
|
|
41
|
-
{ sellerId: "s2", discountRatio: 0.01 }
|
|
42
|
-
]
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
expect(result.source).toBe("prewarm_cache");
|
|
46
|
-
expect(result.routes.map((route) => route.seller.id)).toEqual(["s2", "s1"]);
|
|
47
|
-
expect(result.candidateCount).toBe(2);
|
|
48
|
-
expect(result.routes[0].metrics).toEqual({
|
|
49
|
-
healthScore: 95,
|
|
50
|
-
avgLatencyMs: 120,
|
|
51
|
-
avgTokensPerSecond: 42.5,
|
|
52
|
-
discountRatio: 0.01,
|
|
53
|
-
registryOrder: 1
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("prefers live runtime speed metrics over stale prewarm metrics", () => {
|
|
58
|
-
const result = plan({
|
|
59
|
-
routing: { mode: "fullAuto", scorer: "speed" },
|
|
60
|
-
prewarmCandidates: [
|
|
61
|
-
{ sellerId: "s1", url: "https://s1.example.com", healthScore: 90, avgLatencyMs: 100, avgTokensPerSecond: 1 },
|
|
62
|
-
{ sellerId: "s2", url: "https://s2.example.com", healthScore: 90, avgLatencyMs: 100, avgTokensPerSecond: 80 }
|
|
63
|
-
],
|
|
64
|
-
sellerMetrics: [
|
|
65
|
-
{ sellerId: "s1", ttftMs: 20, avgInferenceMs: 100, avgTokensPerSecond: 120 },
|
|
66
|
-
{ sellerId: "s2", ttftMs: 20, avgInferenceMs: 100, avgTokensPerSecond: 2 }
|
|
67
|
-
]
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
expect(result.routes.map((route) => route.seller.id)).toEqual(["s1", "s2"]);
|
|
71
|
-
expect(result.routes[0].metrics).toMatchObject({
|
|
72
|
-
ttftMs: 20,
|
|
73
|
-
avgInferenceMs: 100,
|
|
74
|
-
avgTokensPerSecond: 120
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test("does not hide compatible registry sellers when prewarm cache is incomplete", () => {
|
|
79
|
-
const result = plan({
|
|
80
|
-
routing: { mode: "fullAuto", scorer: "discount" },
|
|
81
|
-
prewarmCandidates: [
|
|
82
|
-
{ sellerId: "s1", url: "https://s1.example.com", healthScore: 95, avgLatencyMs: 30 }
|
|
83
|
-
],
|
|
84
|
-
sellerMetrics: [
|
|
85
|
-
{ sellerId: "s1", discountRatio: 1 },
|
|
86
|
-
{ sellerId: "s2", discountRatio: 0.01 }
|
|
87
|
-
]
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
expect(result.source).toBe("prewarm_cache");
|
|
91
|
-
expect(result.sourceReason).toBe("prewarm_metrics_merged_with_registry");
|
|
92
|
-
expect(result.routes.map((route) => route.seller.id)).toEqual(["s2", "s1"]);
|
|
93
|
-
expect(result.diagnostics).toMatchObject({
|
|
94
|
-
prewarmCandidateCount: 1,
|
|
95
|
-
prewarmUsableCount: 1,
|
|
96
|
-
sourceCandidateCount: 2
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test("falls back to registry candidates when prewarm has no usable sellers", () => {
|
|
101
|
-
const result = plan({
|
|
102
|
-
prewarmCandidates: [
|
|
103
|
-
{ sellerId: "missing", url: "https://missing.example.com", healthScore: 100 },
|
|
104
|
-
{ sellerId: "s3", url: "https://s3.example.com", healthScore: 100 }
|
|
105
|
-
]
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
expect(result.source).toBe("registry_fallback");
|
|
109
|
-
expect(result.sourceReason).toBe("prewarm_no_compatible_candidates");
|
|
110
|
-
expect(result.routes.map((route) => route.seller.id)).toEqual(["s1", "s2"]);
|
|
111
|
-
expect(result.diagnostics).toMatchObject({
|
|
112
|
-
prewarmCandidateCount: 2,
|
|
113
|
-
prewarmUsableCount: 0,
|
|
114
|
-
prewarmMissingSellerIds: ["missing"],
|
|
115
|
-
prewarmBlockedSellerIds: [],
|
|
116
|
-
prewarmIncompatibleSellerIds: ["s3"]
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test("diagnostics explain prewarm fallback caused by blocked warm candidates", () => {
|
|
121
|
-
const now = 10_000;
|
|
122
|
-
const result = plan({
|
|
123
|
-
now,
|
|
124
|
-
prewarmCandidates: [
|
|
125
|
-
{ sellerId: "s1", url: "https://s1.example.com", healthScore: 100 },
|
|
126
|
-
{ sellerId: "s2", url: "https://s2.example.com", healthScore: 90 }
|
|
127
|
-
],
|
|
128
|
-
sellerMetrics: [
|
|
129
|
-
{ sellerId: "s1", circuit: "open" },
|
|
130
|
-
{ sellerId: "s2", capacityBlockedUntil: now + 1000 }
|
|
131
|
-
]
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
expect(result.source).toBe("registry_fallback");
|
|
135
|
-
expect(result.sourceReason).toBe("prewarm_no_compatible_candidates");
|
|
136
|
-
expect(result.routes).toEqual([]);
|
|
137
|
-
expect(result.diagnostics).toMatchObject({
|
|
138
|
-
prewarmCandidateCount: 2,
|
|
139
|
-
prewarmUsableCount: 0,
|
|
140
|
-
prewarmBlockedSellerIds: ["s1", "s2"],
|
|
141
|
-
blockedOpenCircuitCount: 1,
|
|
142
|
-
blockedCapacityCount: 1,
|
|
143
|
-
blockedSellerIds: ["s1", "s2"]
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
test("filters registry fallback by requested model, protocol, and payment method", () => {
|
|
148
|
-
const result = plan({
|
|
149
|
-
registrySellers: [
|
|
150
|
-
seller({ id: "ok", models: ["gpt-5.4"], supportedProtocols: ["chat_completions"], paymentMethods: ["clawtip"] }),
|
|
151
|
-
seller({ id: "wrong-model", models: ["MiniMax-M2.7"], supportedProtocols: ["chat_completions"], paymentMethods: ["clawtip"] }),
|
|
152
|
-
seller({ id: "wrong-protocol", models: ["gpt-5.4"], supportedProtocols: ["responses"], paymentMethods: ["clawtip"] }),
|
|
153
|
-
seller({ id: "wrong-payment", models: ["gpt-5.4"], supportedProtocols: ["chat_completions"], paymentMethods: ["mock"] })
|
|
154
|
-
]
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
expect(result.source).toBe("registry_fallback");
|
|
158
|
-
expect(result.routes.map((route) => route.seller.id)).toEqual(["ok"]);
|
|
159
|
-
expect(result.reason).toBe("fullAuto:balanced:routes_1");
|
|
160
|
-
expect(result.diagnostics).toEqual({
|
|
161
|
-
registryVisibleCount: 4,
|
|
162
|
-
prewarmCandidateCount: 0,
|
|
163
|
-
prewarmUsableCount: 0,
|
|
164
|
-
prewarmMissingSellerIds: [],
|
|
165
|
-
prewarmBlockedSellerIds: [],
|
|
166
|
-
prewarmIncompatibleSellerIds: [],
|
|
167
|
-
sourceCandidateCount: 1,
|
|
168
|
-
blockedOpenCircuitCount: 0,
|
|
169
|
-
blockedCapacityCount: 0,
|
|
170
|
-
blockedLocalConcurrencyCount: 0,
|
|
171
|
-
blockedSellerIds: [],
|
|
172
|
-
incompatibleCount: 3,
|
|
173
|
-
incompatibleSellerIds: ["wrong-model", "wrong-payment", "wrong-protocol"]
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test("only active registry sellers are visible to buyer routing", () => {
|
|
178
|
-
const result = plan({
|
|
179
|
-
registrySellers: [
|
|
180
|
-
seller({ id: "legacy-no-status" }),
|
|
181
|
-
seller({ id: "active", status: "active" }),
|
|
182
|
-
seller({ id: "pending", status: "pending" }),
|
|
183
|
-
seller({ id: "draining", status: "draining" }),
|
|
184
|
-
seller({ id: "offline", status: "offline" })
|
|
185
|
-
]
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
expect(result.routes.map((route) => route.seller.id)).toEqual(["legacy-no-status", "active"]);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
test("fixed mode fails closed when selected seller is outside compatibility", () => {
|
|
192
|
-
const result = plan({
|
|
193
|
-
routing: { mode: "fixed", sellerId: "s3", scorer: "speed" }
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
expect(result.routes).toEqual([]);
|
|
197
|
-
expect(result.reason).toBe("fixed_seller_not_compatible");
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
test("fixedSet mode does not select outside the configured seller pool", () => {
|
|
201
|
-
const result = plan({
|
|
202
|
-
routing: { mode: "fixedSet", sellerIds: ["s1", "s2"], scorer: "speed" },
|
|
203
|
-
sellerMetrics: [
|
|
204
|
-
{ sellerId: "outside", healthScore: 100, avgLatencyMs: 10 },
|
|
205
|
-
{ sellerId: "s1", healthScore: 40, avgLatencyMs: 20 },
|
|
206
|
-
{ sellerId: "s2", healthScore: 90, avgLatencyMs: 30 }
|
|
207
|
-
],
|
|
208
|
-
registrySellers: [
|
|
209
|
-
seller({ id: "outside" }),
|
|
210
|
-
seller({ id: "s1" }),
|
|
211
|
-
seller({ id: "s2" })
|
|
212
|
-
]
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
expect(result.routes.map((route) => route.seller.id)).toEqual(["s2", "s1"]);
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
test("discount scorer uses planner metrics during registry fallback", () => {
|
|
219
|
-
const result = plan({
|
|
220
|
-
routing: { mode: "fullAuto", scorer: "discount" },
|
|
221
|
-
sellerMetrics: [
|
|
222
|
-
{ sellerId: "s1", healthScore: 100, discountRatio: 1 },
|
|
223
|
-
{ sellerId: "s2", healthScore: 30, discountRatio: 0.01 }
|
|
224
|
-
]
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
expect(result.routes.map((route) => route.seller.id)).toEqual(["s2", "s1"]);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
test("messages protocol accepts anthropic_messages registry sellers", () => {
|
|
231
|
-
const result = plan({
|
|
232
|
-
modelId: "MiniMax-M2.7",
|
|
233
|
-
protocol: "messages",
|
|
234
|
-
registrySellers: [
|
|
235
|
-
seller({
|
|
236
|
-
id: "minimax",
|
|
237
|
-
models: ["MiniMax-M2.7"],
|
|
238
|
-
supportedProtocols: ["anthropic_messages"],
|
|
239
|
-
paymentMethods: ["clawtip"]
|
|
240
|
-
})
|
|
241
|
-
]
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
expect(result.routes.map((route) => route.seller.id)).toEqual(["minimax"]);
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
test("open circuit observations are excluded before strategy selection", () => {
|
|
248
|
-
const result = plan({
|
|
249
|
-
routing: { mode: "fullAuto", scorer: "speed" },
|
|
250
|
-
sellerMetrics: [
|
|
251
|
-
{ sellerId: "s1", healthScore: 100, circuit: "open" },
|
|
252
|
-
{ sellerId: "s2", healthScore: 20, circuit: "closed" }
|
|
253
|
-
]
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
expect(result.routes.map((route) => route.seller.id)).toEqual(["s2"]);
|
|
257
|
-
expect(result.diagnostics.blockedOpenCircuitCount).toBe(1);
|
|
258
|
-
expect(result.diagnostics.blockedSellerIds).toEqual(["s1"]);
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
test("active capacity blocks are excluded before strategy selection", () => {
|
|
262
|
-
const now = 10_000;
|
|
263
|
-
const blocked = plan({
|
|
264
|
-
now,
|
|
265
|
-
routing: { mode: "fullAuto", scorer: "speed" },
|
|
266
|
-
sellerMetrics: [
|
|
267
|
-
{ sellerId: "s1", healthScore: 100, avgLatencyMs: 10, capacityBlockedUntil: now + 1000 },
|
|
268
|
-
{ sellerId: "s2", healthScore: 20, avgLatencyMs: 100 }
|
|
269
|
-
]
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
expect(blocked.routes.map((route) => route.seller.id)).toEqual(["s2"]);
|
|
273
|
-
expect(blocked.diagnostics.blockedCapacityCount).toBe(1);
|
|
274
|
-
expect(blocked.diagnostics.blockedSellerIds).toEqual(["s1"]);
|
|
275
|
-
|
|
276
|
-
const expired = plan({
|
|
277
|
-
now: now + 1001,
|
|
278
|
-
routing: { mode: "fullAuto", scorer: "speed" },
|
|
279
|
-
sellerMetrics: [
|
|
280
|
-
{ sellerId: "s1", healthScore: 100, avgLatencyMs: 10, capacityBlockedUntil: now + 1000 },
|
|
281
|
-
{ sellerId: "s2", healthScore: 20, avgLatencyMs: 100 }
|
|
282
|
-
]
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
expect(expired.routes.map((route) => route.seller.id)).toEqual(["s1", "s2"]);
|
|
286
|
-
expect(expired.diagnostics.blockedCapacityCount).toBe(0);
|
|
287
|
-
expect(expired.diagnostics.blockedSellerIds).toEqual([]);
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
test("local concurrency blocks are excluded before choosing prewarm or registry candidates", () => {
|
|
291
|
-
const result = plan({
|
|
292
|
-
prewarmCandidates: [
|
|
293
|
-
{ sellerId: "s1", url: "https://s1.example.com", healthScore: 100 }
|
|
294
|
-
],
|
|
295
|
-
sellerMetrics: [
|
|
296
|
-
{ sellerId: "s1", localConcurrencyActive: 1, localConcurrencyLimit: 1 },
|
|
297
|
-
{ sellerId: "s2", localConcurrencyActive: 0, localConcurrencyLimit: 1 }
|
|
298
|
-
]
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
expect(result.source).toBe("registry_fallback");
|
|
302
|
-
expect(result.sourceReason).toBe("prewarm_no_compatible_candidates");
|
|
303
|
-
expect(result.routes.map((route) => route.seller.id)).toEqual(["s2"]);
|
|
304
|
-
expect(result.diagnostics).toMatchObject({
|
|
305
|
-
prewarmCandidateCount: 1,
|
|
306
|
-
prewarmUsableCount: 0,
|
|
307
|
-
prewarmBlockedSellerIds: ["s1"],
|
|
308
|
-
blockedLocalConcurrencyCount: 1,
|
|
309
|
-
blockedSellerIds: ["s1"]
|
|
310
|
-
});
|
|
311
|
-
});
|
|
312
|
-
});
|