@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.
- package/dist/src/buyer-store.d.ts +6 -1
- package/dist/src/buyer-store.js +43 -4
- package/dist/src/cli.js +2 -2
- 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/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,124 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
assertSellerRoutingConfig,
|
|
3
|
-
defaultSellerRoutingConfig,
|
|
4
|
-
mergeSellerRoutingConfig,
|
|
5
|
-
normalizeSellerRoutingConfig,
|
|
6
|
-
parseSellerIdList,
|
|
7
|
-
parseSellerRoutingEnv
|
|
8
|
-
} from "../src/seller-routing-config.js";
|
|
9
|
-
|
|
10
|
-
describe("seller routing config", () => {
|
|
11
|
-
test("defaults to fullAuto balanced routing", () => {
|
|
12
|
-
expect(defaultSellerRoutingConfig()).toEqual({
|
|
13
|
-
mode: "fullAuto",
|
|
14
|
-
scorer: "balanced"
|
|
15
|
-
});
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
test("normalizes fixed and fixedSet configs", () => {
|
|
19
|
-
expect(normalizeSellerRoutingConfig({
|
|
20
|
-
mode: "fixed",
|
|
21
|
-
sellerId: " tbs-1 ",
|
|
22
|
-
fixedByModel: {
|
|
23
|
-
" gpt-4o ": " tbs-2 ",
|
|
24
|
-
empty: "",
|
|
25
|
-
ignored: 42
|
|
26
|
-
},
|
|
27
|
-
scorer: "speed"
|
|
28
|
-
})).toEqual({
|
|
29
|
-
mode: "fixed",
|
|
30
|
-
sellerId: "tbs-1",
|
|
31
|
-
fixedByModel: {
|
|
32
|
-
"gpt-4o": "tbs-2"
|
|
33
|
-
},
|
|
34
|
-
scorer: "speed"
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
expect(normalizeSellerRoutingConfig({
|
|
38
|
-
mode: "fixedSet",
|
|
39
|
-
sellerIds: ["tbs-1", " ", "tbs-2", "tbs-1"],
|
|
40
|
-
scorer: "discount"
|
|
41
|
-
})).toEqual({
|
|
42
|
-
mode: "fixedSet",
|
|
43
|
-
sellerIds: ["tbs-1", "tbs-2"],
|
|
44
|
-
scorer: "discount"
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test("does not accept legacy auto/manual modes", () => {
|
|
49
|
-
expect(() => normalizeSellerRoutingConfig({ mode: "auto" })).toThrow("seller routing mode");
|
|
50
|
-
expect(() => parseSellerRoutingEnv({ TB_PROXYD_ROUTING_MODE: "manual" })).toThrow("seller routing mode");
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("rejects invalid routing scorer values", () => {
|
|
54
|
-
expect(() => normalizeSellerRoutingConfig({ mode: "fullAuto", scorer: "fast" })).toThrow("seller routing scorer");
|
|
55
|
-
expect(() => parseSellerRoutingEnv({ TB_PROXYD_ROUTING_SCORER: "random" })).toThrow("seller routing scorer");
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test("parses only the new routing environment variables", () => {
|
|
59
|
-
expect(parseSellerRoutingEnv({
|
|
60
|
-
TB_PROXYD_ROUTING_MODE: "fixedSet",
|
|
61
|
-
TB_PROXYD_ROUTING_SELLER_IDS: "tbs-1,tbs-2,tbs-1",
|
|
62
|
-
TB_PROXYD_ROUTING_SCORER: "speed"
|
|
63
|
-
})).toEqual({
|
|
64
|
-
mode: "fixedSet",
|
|
65
|
-
sellerIds: ["tbs-1", "tbs-2"],
|
|
66
|
-
scorer: "speed"
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
expect(parseSellerRoutingEnv({
|
|
70
|
-
TB_PROXYD_SELECTION_MODE: "manual",
|
|
71
|
-
TB_PROXYD_SELECTED_SELLER_ID: "old-seller"
|
|
72
|
-
})).toBeUndefined();
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test("infers routing mode from individual environment variables", () => {
|
|
76
|
-
expect(parseSellerRoutingEnv({
|
|
77
|
-
TB_PROXYD_ROUTING_SELLER_ID: "tbs-1"
|
|
78
|
-
})).toEqual({
|
|
79
|
-
mode: "fixed",
|
|
80
|
-
sellerId: "tbs-1",
|
|
81
|
-
scorer: "balanced"
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
expect(parseSellerRoutingEnv({
|
|
85
|
-
TB_PROXYD_ROUTING_SELLER_IDS: "tbs-1,tbs-2"
|
|
86
|
-
})).toEqual({
|
|
87
|
-
mode: "fixedSet",
|
|
88
|
-
sellerIds: ["tbs-1", "tbs-2"],
|
|
89
|
-
scorer: "balanced"
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
expect(parseSellerRoutingEnv({
|
|
93
|
-
TB_PROXYD_ROUTING_SCORER: "discount"
|
|
94
|
-
})).toEqual({
|
|
95
|
-
mode: "fullAuto",
|
|
96
|
-
scorer: "discount"
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test("merges explicit overrides over stored config", () => {
|
|
101
|
-
expect(mergeSellerRoutingConfig(
|
|
102
|
-
{ mode: "fullAuto", scorer: "balanced" },
|
|
103
|
-
{ mode: "fixed", sellerId: "tbs-1", scorer: "discount" }
|
|
104
|
-
)).toEqual({
|
|
105
|
-
mode: "fixed",
|
|
106
|
-
sellerId: "tbs-1",
|
|
107
|
-
scorer: "discount"
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test("validates required fixed strategy parameters", () => {
|
|
112
|
-
expect(() => assertSellerRoutingConfig({ mode: "fixed", scorer: "balanced" })).toThrow("--seller");
|
|
113
|
-
expect(() => assertSellerRoutingConfig({
|
|
114
|
-
mode: "fixed",
|
|
115
|
-
scorer: "balanced",
|
|
116
|
-
fixedByModel: { "gpt-4o": "tbs-1" }
|
|
117
|
-
})).not.toThrow();
|
|
118
|
-
expect(() => assertSellerRoutingConfig({ mode: "fixedSet", sellerIds: [], scorer: "balanced" })).toThrow("--seller-set");
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("parses comma-separated seller id lists", () => {
|
|
122
|
-
expect(parseSellerIdList(" a, b,,a ")).toEqual(["a", "b"]);
|
|
123
|
-
});
|
|
124
|
-
});
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import { planSellerRoutes, type RoutingCandidate, type SellerRoutingStrategyConfig } from "../src/seller-routing-strategy.js";
|
|
2
|
-
|
|
3
|
-
function candidate(overrides: Partial<RoutingCandidate> & { sellerId: string; registryOrder: number }): RoutingCandidate {
|
|
4
|
-
return {
|
|
5
|
-
sellerId: overrides.sellerId,
|
|
6
|
-
url: overrides.url ?? `https://${overrides.sellerId}.example.com`,
|
|
7
|
-
supportsModel: overrides.supportsModel ?? true,
|
|
8
|
-
supportsProtocol: overrides.supportsProtocol ?? true,
|
|
9
|
-
supportsPayment: overrides.supportsPayment ?? true,
|
|
10
|
-
healthScore: overrides.healthScore,
|
|
11
|
-
avgLatencyMs: overrides.avgLatencyMs,
|
|
12
|
-
healthProbeLatencyMs: overrides.healthProbeLatencyMs,
|
|
13
|
-
ttftMs: overrides.ttftMs,
|
|
14
|
-
avgInferenceMs: overrides.avgInferenceMs,
|
|
15
|
-
avgTokensPerSecond: overrides.avgTokensPerSecond,
|
|
16
|
-
discountRatio: overrides.discountRatio,
|
|
17
|
-
registryOrder: overrides.registryOrder
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function planIds(candidates: RoutingCandidate[], config: SellerRoutingStrategyConfig): string[] {
|
|
22
|
-
return planSellerRoutes(candidates, config).routes.map((route) => route.sellerId);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
describe("seller routing strategy", () => {
|
|
26
|
-
test("fixed mode returns only the configured seller", () => {
|
|
27
|
-
const result = planSellerRoutes(
|
|
28
|
-
[
|
|
29
|
-
candidate({ sellerId: "s1", registryOrder: 0, healthScore: 40 }),
|
|
30
|
-
candidate({ sellerId: "s2", registryOrder: 1, healthScore: 99 })
|
|
31
|
-
],
|
|
32
|
-
{ mode: "fixed", sellerId: "s1", scorer: "speed" }
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
expect(result.routes.map((route) => route.sellerId)).toEqual(["s1"]);
|
|
36
|
-
expect(result.reason).toBe("fixed:speed:routes_1");
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("fixed mode matches seller ids case-insensitively", () => {
|
|
40
|
-
const ids = planIds(
|
|
41
|
-
[
|
|
42
|
-
candidate({ sellerId: "TBS-1", registryOrder: 0 }),
|
|
43
|
-
candidate({ sellerId: "tbs-2", registryOrder: 1 })
|
|
44
|
-
],
|
|
45
|
-
{ mode: "fixed", sellerId: "tbs-1" }
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
expect(ids).toEqual(["TBS-1"]);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("fixed mode fails closed when the configured seller is not compatible", () => {
|
|
52
|
-
const result = planSellerRoutes(
|
|
53
|
-
[
|
|
54
|
-
candidate({ sellerId: "s1", registryOrder: 0, supportsPayment: false }),
|
|
55
|
-
candidate({ sellerId: "s2", registryOrder: 1 })
|
|
56
|
-
],
|
|
57
|
-
{ mode: "fixed", sellerId: "s1" }
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
expect(result.routes).toEqual([]);
|
|
61
|
-
expect(result.reason).toBe("fixed_seller_not_compatible");
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test("fixed mode requires a seller id", () => {
|
|
65
|
-
const result = planSellerRoutes([candidate({ sellerId: "s1", registryOrder: 0 })], { mode: "fixed" });
|
|
66
|
-
|
|
67
|
-
expect(result.routes).toEqual([]);
|
|
68
|
-
expect(result.reason).toBe("fixed_seller_missing");
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("fixedSet mode never selects sellers outside the configured pool", () => {
|
|
72
|
-
const ids = planIds(
|
|
73
|
-
[
|
|
74
|
-
candidate({ sellerId: "outside-fast", registryOrder: 0, healthScore: 100 }),
|
|
75
|
-
candidate({ sellerId: "pool-a", registryOrder: 1, healthScore: 30 }),
|
|
76
|
-
candidate({ sellerId: "pool-b", registryOrder: 2, healthScore: 70 })
|
|
77
|
-
],
|
|
78
|
-
{ mode: "fixedSet", sellerIds: ["pool-a", "pool-b"], scorer: "speed" }
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
expect(ids).toEqual(["pool-b", "pool-a"]);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test("fixedSet mode matches seller pools case-insensitively", () => {
|
|
85
|
-
const ids = planIds(
|
|
86
|
-
[
|
|
87
|
-
candidate({ sellerId: "TBS-A", registryOrder: 0, healthScore: 30 }),
|
|
88
|
-
candidate({ sellerId: "tbs-B", registryOrder: 1, healthScore: 70 })
|
|
89
|
-
],
|
|
90
|
-
{ mode: "fixedSet", sellerIds: ["TBS-A", "TBS-B"], scorer: "balanced" }
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
expect(ids).toEqual(["tbs-B", "TBS-A"]);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test("fixedSet mode reports empty pool before routing", () => {
|
|
97
|
-
const result = planSellerRoutes([candidate({ sellerId: "s1", registryOrder: 0 })], { mode: "fixedSet", sellerIds: [] });
|
|
98
|
-
|
|
99
|
-
expect(result.routes).toEqual([]);
|
|
100
|
-
expect(result.reason).toBe("fixed_set_empty");
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test("fullAuto mode uses all compatible candidates", () => {
|
|
104
|
-
const ids = planIds(
|
|
105
|
-
[
|
|
106
|
-
candidate({ sellerId: "s1", registryOrder: 0 }),
|
|
107
|
-
candidate({ sellerId: "s2", registryOrder: 1, supportsProtocol: false }),
|
|
108
|
-
candidate({ sellerId: "s3", registryOrder: 2 })
|
|
109
|
-
],
|
|
110
|
-
{ mode: "fullAuto" }
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
expect(ids).toEqual(["s1", "s3"]);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
test("speed scorer uses TTFT and ten-minute Tok/s, then health", () => {
|
|
117
|
-
const ids = planIds(
|
|
118
|
-
[
|
|
119
|
-
candidate({ sellerId: "high-slow", registryOrder: 0, healthScore: 90, ttftMs: 800, avgInferenceMs: 10, avgTokensPerSecond: 1 }),
|
|
120
|
-
candidate({ sellerId: "high-fast", registryOrder: 1, healthScore: 90, ttftMs: 100, avgInferenceMs: 1000, avgTokensPerSecond: 80 }),
|
|
121
|
-
candidate({ sellerId: "low-fast", registryOrder: 2, healthScore: 40, ttftMs: 10, avgInferenceMs: 1000, avgTokensPerSecond: 20 })
|
|
122
|
-
],
|
|
123
|
-
{ mode: "fullAuto", scorer: "speed" }
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
expect(ids).toEqual(["high-fast", "low-fast", "high-slow"]);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test("discount scorer prefers lower discount ratio, then health", () => {
|
|
130
|
-
const ids = planIds(
|
|
131
|
-
[
|
|
132
|
-
candidate({ sellerId: "expensive", registryOrder: 0, discountRatio: 1, healthScore: 100 }),
|
|
133
|
-
candidate({ sellerId: "cheap-low-health", registryOrder: 1, discountRatio: 0.01, healthScore: 20 }),
|
|
134
|
-
candidate({ sellerId: "cheap-high-health", registryOrder: 2, discountRatio: 0.01, healthScore: 90 })
|
|
135
|
-
],
|
|
136
|
-
{ mode: "fullAuto", scorer: "discount" }
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
expect(ids).toEqual(["cheap-high-health", "cheap-low-health", "expensive"]);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test("balanced scorer combines health, latency, and discount", () => {
|
|
143
|
-
const ids = planIds(
|
|
144
|
-
[
|
|
145
|
-
candidate({ sellerId: "healthy-expensive", registryOrder: 0, healthScore: 90, avgLatencyMs: 100, discountRatio: 1 }),
|
|
146
|
-
candidate({ sellerId: "balanced", registryOrder: 1, healthScore: 80, avgLatencyMs: 100, discountRatio: 0.01 }),
|
|
147
|
-
candidate({ sellerId: "cheap-unhealthy", registryOrder: 2, healthScore: 20, avgLatencyMs: 100, discountRatio: 0.01 })
|
|
148
|
-
],
|
|
149
|
-
{ mode: "fullAuto", scorer: "balanced" }
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
expect(ids).toEqual(["balanced", "cheap-unhealthy", "healthy-expensive"]);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test("registry order is the stable tie breaker", () => {
|
|
156
|
-
const ids = planIds(
|
|
157
|
-
[
|
|
158
|
-
candidate({ sellerId: "s2", registryOrder: 2, healthScore: 80, avgLatencyMs: 100, discountRatio: 0.5 }),
|
|
159
|
-
candidate({ sellerId: "s0", registryOrder: 0, healthScore: 80, avgLatencyMs: 100, discountRatio: 0.5 }),
|
|
160
|
-
candidate({ sellerId: "s1", registryOrder: 1, healthScore: 80, avgLatencyMs: 100, discountRatio: 0.5 })
|
|
161
|
-
],
|
|
162
|
-
{ mode: "fullAuto", scorer: "balanced" }
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
expect(ids).toEqual(["s0", "s1", "s2"]);
|
|
166
|
-
});
|
|
167
|
-
});
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { STREAM_FAILOVER_RETRY_HINT, StreamFailover } from "../src/stream-failover.js";
|
|
2
|
-
|
|
3
|
-
describe("StreamFailover", () => {
|
|
4
|
-
test("fresh state reports no chunks committed", () => {
|
|
5
|
-
const sf = new StreamFailover();
|
|
6
|
-
expect(sf.snapshot()).toEqual({ firstChunkCommitted: false, bytesFlushed: 0 });
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
test("markFirstChunkCommitted transitions once and only once", () => {
|
|
10
|
-
const sf = new StreamFailover();
|
|
11
|
-
sf.markFirstChunkCommitted();
|
|
12
|
-
sf.markFirstChunkCommitted();
|
|
13
|
-
expect(sf.snapshot().firstChunkCommitted).toBe(true);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
test("recordBytesWritten accumulates flushed bytes", () => {
|
|
17
|
-
const sf = new StreamFailover();
|
|
18
|
-
sf.recordBytesWritten(128);
|
|
19
|
-
sf.recordBytesWritten(64);
|
|
20
|
-
expect(sf.snapshot().bytesFlushed).toBe(192);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test("decideOnStreamAbort before first chunk defers to the controller", () => {
|
|
24
|
-
const sf = new StreamFailover();
|
|
25
|
-
const decision = sf.decideOnStreamAbort("upstream_reset");
|
|
26
|
-
expect(decision.action).toBe("let_stream_complete");
|
|
27
|
-
expect(decision.retryHintValue).toBe("0");
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test("decideOnStreamAbort after first chunk aborts and surfaces retry hint", () => {
|
|
31
|
-
const sf = new StreamFailover();
|
|
32
|
-
sf.markFirstChunkCommitted();
|
|
33
|
-
sf.recordBytesWritten(2048);
|
|
34
|
-
const decision = sf.decideOnStreamAbort("upstream_reset");
|
|
35
|
-
expect(decision.action).toBe("abort_with_retry_hint");
|
|
36
|
-
expect(decision.retryHintValue).toBe(STREAM_FAILOVER_RETRY_HINT);
|
|
37
|
-
expect(decision.bytesFlushed).toBe(2048);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test("reset clears the chunk and byte counters", () => {
|
|
41
|
-
const sf = new StreamFailover();
|
|
42
|
-
sf.markFirstChunkCommitted();
|
|
43
|
-
sf.recordBytesWritten(999);
|
|
44
|
-
sf.reset();
|
|
45
|
-
expect(sf.snapshot()).toEqual({ firstChunkCommitted: false, bytesFlushed: 0 });
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test("default header name is X-TokenBuddy-Retry-Hint and is overridable", () => {
|
|
49
|
-
expect(new StreamFailover().headerName).toBe("X-TokenBuddy-Retry-Hint");
|
|
50
|
-
expect(new StreamFailover({ retryHintHeader: "X-Custom-Retry" }).headerName).toBe("X-Custom-Retry");
|
|
51
|
-
});
|
|
52
|
-
});
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { ModelIndex } from "../src/model-index.js";
|
|
2
|
-
import { PrewarmCache, prewarmKey } from "../src/prewarm-cache.js";
|
|
3
|
-
import { CreditTracker } from "../src/credit-tracker.js";
|
|
4
|
-
import { SellerPool } from "../src/seller-pool.js";
|
|
5
|
-
import { RouteFailover } from "../src/route-failover.js";
|
|
6
|
-
import { PrewarmScheduler, type ProbeResult, type SellerProber } from "../src/prewarm-scheduler.js";
|
|
7
|
-
import type { RegistrySeller } from "../src/seller-catalog.js";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* v1.2 §18.15: "thousand-seller" integration smoke. Validates the
|
|
11
|
-
* end-to-end pipeline at a scale that simulates a real public registry:
|
|
12
|
-
* - the model-index build stays cheap (sub-100ms for 1k sellers)
|
|
13
|
-
* - the prewarm scheduler respects its per-minute and per-seller caps
|
|
14
|
-
* - the route-failover controller still returns a clean decision when
|
|
15
|
-
* a single seller among a thousand fails
|
|
16
|
-
*
|
|
17
|
-
* The test does not exercise live HTTP traffic; it uses stub probers and
|
|
18
|
-
* pre-populated registries so it can run as a fast unit test on every
|
|
19
|
-
* change.
|
|
20
|
-
*/
|
|
21
|
-
describe("v1.2 thousand-seller integration smoke", () => {
|
|
22
|
-
function buildLargeRegistry(size: number, focusModel: string): RegistrySeller[] {
|
|
23
|
-
const sellers: RegistrySeller[] = [];
|
|
24
|
-
for (let i = 0; i < size; i += 1) {
|
|
25
|
-
sellers.push({
|
|
26
|
-
id: `seller-${i.toString().padStart(4, "0")}`,
|
|
27
|
-
name: `Seller ${i}`,
|
|
28
|
-
url: `https://seller-${i}.example.com`,
|
|
29
|
-
supportedProtocols: ["chat_completions"],
|
|
30
|
-
paymentMethods: ["clawtip"],
|
|
31
|
-
// ~1/3 of the sellers serve BOTH models, 2/3 serve only
|
|
32
|
-
// `focusModel`. This simulates a realistic registry mix.
|
|
33
|
-
models: i % 3 === 0 ? [focusModel, "secondary-model"] : [focusModel]
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
return sellers;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
test("model-index builds in well under a second for 1000 sellers", () => {
|
|
40
|
-
const index = new ModelIndex();
|
|
41
|
-
const sellers = buildLargeRegistry(1000, "gpt-4o");
|
|
42
|
-
const started = Date.now();
|
|
43
|
-
index.rebuild(sellers, { registryVersion: 1, defaultSellerId: "seller-0000" });
|
|
44
|
-
const elapsed = Date.now() - started;
|
|
45
|
-
expect(elapsed).toBeLessThan(500);
|
|
46
|
-
expect(index.stats().sellerCount).toBe(1000);
|
|
47
|
-
expect(index.stats().modelCount).toBe(2);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test("picking the focus model returns the configured candidate set", () => {
|
|
51
|
-
const index = new ModelIndex();
|
|
52
|
-
index.rebuild(buildLargeRegistry(1000, "gpt-4o"), { registryVersion: 1 });
|
|
53
|
-
const candidates = index.sellersFor("gpt-4o", { protocol: "chat_completions", paymentMethod: "clawtip" });
|
|
54
|
-
// Every seller in the registry serves `gpt-4o` (either alone or
|
|
55
|
-
// alongside `secondary-model`), so all 1000 are eligible.
|
|
56
|
-
expect(candidates.length).toBe(1000);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test("prewarm scheduler enforces the global per-minute cap across many tasks", async () => {
|
|
60
|
-
const index = new ModelIndex();
|
|
61
|
-
const sellers = buildLargeRegistry(50, "gpt-4o");
|
|
62
|
-
index.rebuild(sellers, { registryVersion: 1 });
|
|
63
|
-
const cache = new PrewarmCache();
|
|
64
|
-
const credit = new CreditTracker();
|
|
65
|
-
|
|
66
|
-
// Prober resolves immediately. The scheduler should still cap the
|
|
67
|
-
// number of actual probe calls per minute.
|
|
68
|
-
const prober: SellerProber = async (): Promise<ProbeResult> => ({ ok: true, latencyMs: 1, httpStatus: 200 });
|
|
69
|
-
const scheduler = new PrewarmScheduler({
|
|
70
|
-
modelIndex: index,
|
|
71
|
-
cache,
|
|
72
|
-
prober,
|
|
73
|
-
// Lower the caps so the test runs in a few ms.
|
|
74
|
-
maxPrewarmPerMinute: 5,
|
|
75
|
-
concurrency: 1,
|
|
76
|
-
sleep: () => new Promise(() => undefined)
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
// Enqueue three independent (model, protocol, payment) tasks; only
|
|
80
|
-
// `gpt-4o` and `gpt-4o` slots exist so the third (and beyond) will
|
|
81
|
-
// be rate-limited after 2 actual probe invocations.
|
|
82
|
-
const tasks = await Promise.all([
|
|
83
|
-
scheduler.schedulePrewarm({ modelId: "gpt-4o", reason: "lazy" }),
|
|
84
|
-
scheduler.schedulePrewarm({ modelId: "gpt-4o", reason: "lazy" }),
|
|
85
|
-
scheduler.schedulePrewarm({ modelId: "gpt-4o", reason: "lazy" })
|
|
86
|
-
]);
|
|
87
|
-
const succeeded = tasks.filter((t) => t.status === "succeeded").length;
|
|
88
|
-
const rateLimited = tasks.filter((t) => t.status === "rate_limited").length;
|
|
89
|
-
expect(succeeded).toBeGreaterThan(0);
|
|
90
|
-
expect(rateLimited + succeeded).toBe(3);
|
|
91
|
-
expect(scheduler.stats().totalRateLimited).toBe(rateLimited);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test("seller-pool + route-failover pipeline still produces a clean decision under a thousand sellers", () => {
|
|
95
|
-
const index = new ModelIndex();
|
|
96
|
-
index.rebuild(buildLargeRegistry(1000, "gpt-4o"), { registryVersion: 1 });
|
|
97
|
-
const cache = new PrewarmCache();
|
|
98
|
-
const credit = new CreditTracker();
|
|
99
|
-
const pool = new SellerPool({ modelIndex: index, cache, creditTracker: credit });
|
|
100
|
-
pool.sync();
|
|
101
|
-
const failover = new RouteFailover({ pool, creditTracker: credit });
|
|
102
|
-
// 1k sellers all serve gpt-4o. Pick the top-4 by health (all
|
|
103
|
-
// default to 80 healthScore from the stub commit) and verify
|
|
104
|
-
// that a hard 4xx on the first one fails over to the next three
|
|
105
|
-
// without ever exhausting the pool.
|
|
106
|
-
const eligible = index.sellersFor("gpt-4o", { protocol: "chat_completions", paymentMethod: "clawtip" });
|
|
107
|
-
const subset = eligible.slice(0, 100);
|
|
108
|
-
cache.commitWarm({
|
|
109
|
-
modelId: "gpt-4o",
|
|
110
|
-
protocol: "chat_completions",
|
|
111
|
-
paymentMethod: "clawtip",
|
|
112
|
-
candidates: subset.map((seller) => ({ sellerId: seller.id, url: seller.url, healthScore: 80 }))
|
|
113
|
-
});
|
|
114
|
-
pool.sync();
|
|
115
|
-
// pool size matches the deduped seller count in the cache (each
|
|
116
|
-
// seller appears exactly once even if listed by multiple registry
|
|
117
|
-
// entries).
|
|
118
|
-
expect(pool.size()).toBe(subset.length);
|
|
119
|
-
const first = failover.pickNext("gpt-4o", "chat_completions", "clawtip");
|
|
120
|
-
expect(first).toBeDefined();
|
|
121
|
-
credit.recordPurchase(first!.sellerId, 1_000_000, 1_000_000);
|
|
122
|
-
const decision = failover.decide(
|
|
123
|
-
{ sellerId: first!.sellerId, status: 404, errorKind: "hard_4xx", attempt: 0 },
|
|
124
|
-
100
|
|
125
|
-
);
|
|
126
|
-
expect(decision.action).toBe("failover_next");
|
|
127
|
-
expect(decision.wastedCreditMicros).toBeGreaterThan(0);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test("prewarm-key collisions are impossible across the (model, protocol, payment) space", () => {
|
|
131
|
-
// Even with 1000 sellers, the (model, protocol, payment) key must
|
|
132
|
-
// be unique. We assert the count of unique keys equals the count of
|
|
133
|
-
// committed entries.
|
|
134
|
-
const cache = new PrewarmCache();
|
|
135
|
-
for (let i = 0; i < 1000; i += 1) {
|
|
136
|
-
const protocol = i % 2 === 0 ? "chat_completions" : "responses";
|
|
137
|
-
const payment = i % 3 === 0 ? "clawtip" : "mock";
|
|
138
|
-
cache.commitWarm({
|
|
139
|
-
modelId: `m-${i}`,
|
|
140
|
-
protocol,
|
|
141
|
-
paymentMethod: payment,
|
|
142
|
-
candidates: [{ sellerId: `s-${i}`, url: "https://x", healthScore: 80 }]
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
const keys = new Set<string>();
|
|
146
|
-
for (const entry of cache.snapshot()) {
|
|
147
|
-
keys.add(prewarmKey(entry.modelId, entry.protocol, entry.paymentMethod));
|
|
148
|
-
}
|
|
149
|
-
expect(keys.size).toBe(1000);
|
|
150
|
-
});
|
|
151
|
-
});
|