@tokenbuddy/tokenbuddy 1.0.11 → 1.0.13

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 (135) hide show
  1. package/dist/src/buyer-store.d.ts +61 -0
  2. package/dist/src/buyer-store.d.ts.map +1 -1
  3. package/dist/src/buyer-store.js +12 -0
  4. package/dist/src/buyer-store.js.map +1 -1
  5. package/dist/src/cli.d.ts +47 -0
  6. package/dist/src/cli.d.ts.map +1 -1
  7. package/dist/src/cli.js +287 -63
  8. package/dist/src/cli.js.map +1 -1
  9. package/dist/src/credit-tracker.d.ts +26 -0
  10. package/dist/src/credit-tracker.d.ts.map +1 -1
  11. package/dist/src/credit-tracker.js +8 -0
  12. package/dist/src/credit-tracker.js.map +1 -1
  13. package/dist/src/daemon.d.ts +29 -3
  14. package/dist/src/daemon.d.ts.map +1 -1
  15. package/dist/src/daemon.js +292 -65
  16. package/dist/src/daemon.js.map +1 -1
  17. package/dist/src/doctor-clawtip-wallet.d.ts +25 -0
  18. package/dist/src/doctor-clawtip-wallet.d.ts.map +1 -1
  19. package/dist/src/doctor-clawtip-wallet.js +13 -0
  20. package/dist/src/doctor-clawtip-wallet.js.map +1 -1
  21. package/dist/src/doctor-diagnostics.d.ts +63 -0
  22. package/dist/src/doctor-diagnostics.d.ts.map +1 -1
  23. package/dist/src/doctor-diagnostics.js +39 -1
  24. package/dist/src/doctor-diagnostics.js.map +1 -1
  25. package/dist/src/index.d.ts +4 -0
  26. package/dist/src/index.d.ts.map +1 -1
  27. package/dist/src/index.js +4 -0
  28. package/dist/src/index.js.map +1 -1
  29. package/dist/src/init-clawtip-activation.d.ts +103 -0
  30. package/dist/src/init-clawtip-activation.d.ts.map +1 -1
  31. package/dist/src/init-clawtip-activation.js +60 -0
  32. package/dist/src/init-clawtip-activation.js.map +1 -1
  33. package/dist/src/init-payment-options.d.ts +124 -0
  34. package/dist/src/init-payment-options.d.ts.map +1 -1
  35. package/dist/src/init-payment-options.js +68 -0
  36. package/dist/src/init-payment-options.js.map +1 -1
  37. package/dist/src/model-index.d.ts +9 -0
  38. package/dist/src/model-index.d.ts.map +1 -1
  39. package/dist/src/model-index.js.map +1 -1
  40. package/dist/src/prewarm-cache.d.ts +89 -0
  41. package/dist/src/prewarm-cache.d.ts.map +1 -1
  42. package/dist/src/prewarm-cache.js +14 -1
  43. package/dist/src/prewarm-cache.js.map +1 -1
  44. package/dist/src/prewarm-scheduler.d.ts +62 -3
  45. package/dist/src/prewarm-scheduler.d.ts.map +1 -1
  46. package/dist/src/prewarm-scheduler.js +39 -8
  47. package/dist/src/prewarm-scheduler.js.map +1 -1
  48. package/dist/src/provider-install.d.ts +89 -3
  49. package/dist/src/provider-install.d.ts.map +1 -1
  50. package/dist/src/provider-install.js +77 -17
  51. package/dist/src/provider-install.js.map +1 -1
  52. package/dist/src/route-failover.d.ts +48 -0
  53. package/dist/src/route-failover.d.ts.map +1 -1
  54. package/dist/src/route-failover.js.map +1 -1
  55. package/dist/src/seller-catalog.d.ts +158 -10
  56. package/dist/src/seller-catalog.d.ts.map +1 -1
  57. package/dist/src/seller-catalog.js +79 -5
  58. package/dist/src/seller-catalog.js.map +1 -1
  59. package/dist/src/seller-metadata-cache.d.ts +29 -0
  60. package/dist/src/seller-metadata-cache.d.ts.map +1 -0
  61. package/dist/src/seller-metadata-cache.js +71 -0
  62. package/dist/src/seller-metadata-cache.js.map +1 -0
  63. package/dist/src/seller-pool.d.ts +71 -0
  64. package/dist/src/seller-pool.d.ts.map +1 -1
  65. package/dist/src/seller-pool.js +6 -1
  66. package/dist/src/seller-pool.js.map +1 -1
  67. package/dist/src/seller-route-planner.d.ts +118 -0
  68. package/dist/src/seller-route-planner.d.ts.map +1 -0
  69. package/dist/src/seller-route-planner.js +160 -0
  70. package/dist/src/seller-route-planner.js.map +1 -0
  71. package/dist/src/seller-routing-config.d.ts +69 -0
  72. package/dist/src/seller-routing-config.d.ts.map +1 -0
  73. package/dist/src/seller-routing-config.js +164 -0
  74. package/dist/src/seller-routing-config.js.map +1 -0
  75. package/dist/src/seller-routing-strategy.d.ts +118 -0
  76. package/dist/src/seller-routing-strategy.d.ts.map +1 -0
  77. package/dist/src/seller-routing-strategy.js +183 -0
  78. package/dist/src/seller-routing-strategy.js.map +1 -0
  79. package/dist/src/stream-failover.d.ts +23 -0
  80. package/dist/src/stream-failover.d.ts.map +1 -1
  81. package/dist/src/stream-failover.js +4 -0
  82. package/dist/src/stream-failover.js.map +1 -1
  83. package/dist/src/tb-proxyd.js +7 -21
  84. package/dist/src/tb-proxyd.js.map +1 -1
  85. package/dist/src/terminal-detect.d.ts +51 -0
  86. package/dist/src/terminal-detect.d.ts.map +1 -1
  87. package/dist/src/terminal-detect.js +42 -0
  88. package/dist/src/terminal-detect.js.map +1 -1
  89. package/dist/src/terminal-image.d.ts +41 -0
  90. package/dist/src/terminal-image.d.ts.map +1 -1
  91. package/dist/src/terminal-image.js +15 -0
  92. package/dist/src/terminal-image.js.map +1 -1
  93. package/package.json +1 -1
  94. package/src/buyer-store.ts +61 -0
  95. package/src/cli.ts +330 -68
  96. package/src/credit-tracker.ts +26 -0
  97. package/src/daemon.ts +363 -72
  98. package/src/doctor-clawtip-wallet.ts +25 -0
  99. package/src/doctor-diagnostics.ts +63 -1
  100. package/src/index.ts +4 -0
  101. package/src/init-clawtip-activation.ts +103 -0
  102. package/src/init-payment-options.ts +124 -0
  103. package/src/model-index.ts +9 -0
  104. package/src/prewarm-cache.ts +99 -1
  105. package/src/prewarm-scheduler.ts +97 -12
  106. package/src/provider-install.ts +125 -25
  107. package/src/route-failover.ts +48 -0
  108. package/src/seller-catalog.ts +158 -12
  109. package/src/seller-metadata-cache.ts +91 -0
  110. package/src/seller-pool.ts +77 -1
  111. package/src/seller-route-planner.ts +323 -0
  112. package/src/seller-routing-config.ts +198 -0
  113. package/src/seller-routing-strategy.ts +316 -0
  114. package/src/stream-failover.ts +23 -0
  115. package/src/tb-proxyd.ts +7 -23
  116. package/src/terminal-detect.ts +51 -0
  117. package/src/terminal-image.ts +41 -0
  118. package/tests/cli-routing.test.ts +287 -0
  119. package/tests/daemon-classify.test.ts +431 -0
  120. package/tests/daemon-roles.test.ts +92 -0
  121. package/tests/seller-catalog-utilities.test.ts +70 -0
  122. package/tests/seller-metadata-cache.test.ts +89 -0
  123. package/tests/seller-route-planner.test.ts +150 -0
  124. package/tests/seller-routing-config.test.ts +111 -0
  125. package/tests/seller-routing-strategy.test.ts +166 -0
  126. package/tests/tokenbuddy.test.ts +447 -33
  127. /package/{src → tests}/credit-tracker.test.ts +0 -0
  128. /package/{src → tests}/model-index.test.ts +0 -0
  129. /package/{src → tests}/prewarm-cache.test.ts +0 -0
  130. /package/{src → tests}/prewarm-scheduler.test.ts +0 -0
  131. /package/{src → tests}/route-failover.test.ts +0 -0
  132. /package/{src → tests}/seller-catalog-413.test.ts +0 -0
  133. /package/{src → tests}/seller-pool.test.ts +0 -0
  134. /package/{src → tests}/stream-failover.test.ts +0 -0
  135. /package/{src → tests}/thousand-seller.test.ts +0 -0
@@ -0,0 +1,150 @@
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
+ url: overrides.url ?? `https://${overrides.id}.example.com`,
9
+ supportedProtocols: overrides.supportedProtocols ?? ["chat_completions"],
10
+ paymentMethods: overrides.paymentMethods ?? ["clawtip"],
11
+ models: overrides.models ?? ["gpt-5.4"]
12
+ };
13
+ }
14
+
15
+ function plan(overrides: Partial<SellerRoutePlannerInput> = {}) {
16
+ return planSellerRouteSet({
17
+ modelId: "gpt-5.4",
18
+ protocol: "chat_completions",
19
+ paymentMethod: "clawtip",
20
+ registrySellers: [
21
+ seller({ id: "s1" }),
22
+ seller({ id: "s2" }),
23
+ seller({ id: "s3", supportedProtocols: ["responses"] })
24
+ ],
25
+ routing: { mode: "fullAuto", scorer: "balanced" },
26
+ ...overrides
27
+ });
28
+ }
29
+
30
+ describe("seller route planner", () => {
31
+ test("uses compatible prewarm candidates before registry fallback", () => {
32
+ const result = plan({
33
+ prewarmCandidates: [
34
+ { sellerId: "s2", url: "https://s2.example.com", healthScore: 95, avgLatencyMs: 120 },
35
+ { sellerId: "s1", url: "https://s1.example.com", healthScore: 50, avgLatencyMs: 80 },
36
+ { sellerId: "missing", url: "https://missing.example.com", healthScore: 100, avgLatencyMs: 1 }
37
+ ],
38
+ sellerMetrics: [
39
+ { sellerId: "s1", discountRatio: 0.5 },
40
+ { sellerId: "s2", discountRatio: 0.01 }
41
+ ]
42
+ });
43
+
44
+ expect(result.source).toBe("prewarm_cache");
45
+ expect(result.routes.map((route) => route.seller.id)).toEqual(["s2", "s1"]);
46
+ expect(result.candidateCount).toBe(2);
47
+ expect(result.routes[0].metrics).toEqual({
48
+ healthScore: 95,
49
+ avgLatencyMs: 120,
50
+ discountRatio: 0.01,
51
+ registryOrder: 1
52
+ });
53
+ });
54
+
55
+ test("falls back to registry candidates when prewarm has no usable sellers", () => {
56
+ const result = plan({
57
+ prewarmCandidates: [
58
+ { sellerId: "missing", url: "https://missing.example.com", healthScore: 100 },
59
+ { sellerId: "s3", url: "https://s3.example.com", healthScore: 100 }
60
+ ]
61
+ });
62
+
63
+ expect(result.source).toBe("registry_fallback");
64
+ expect(result.sourceReason).toBe("prewarm_no_compatible_candidates");
65
+ expect(result.routes.map((route) => route.seller.id)).toEqual(["s1", "s2"]);
66
+ });
67
+
68
+ test("filters registry fallback by requested model, protocol, and payment method", () => {
69
+ const result = plan({
70
+ registrySellers: [
71
+ seller({ id: "ok", models: ["gpt-5.4"], supportedProtocols: ["chat_completions"], paymentMethods: ["clawtip"] }),
72
+ seller({ id: "wrong-model", models: ["MiniMax-M2.7"], supportedProtocols: ["chat_completions"], paymentMethods: ["clawtip"] }),
73
+ seller({ id: "wrong-protocol", models: ["gpt-5.4"], supportedProtocols: ["responses"], paymentMethods: ["clawtip"] }),
74
+ seller({ id: "wrong-payment", models: ["gpt-5.4"], supportedProtocols: ["chat_completions"], paymentMethods: ["mock"] })
75
+ ]
76
+ });
77
+
78
+ expect(result.source).toBe("registry_fallback");
79
+ expect(result.routes.map((route) => route.seller.id)).toEqual(["ok"]);
80
+ expect(result.reason).toBe("fullAuto:balanced:routes_1");
81
+ });
82
+
83
+ test("fixed mode fails closed when selected seller is outside compatibility", () => {
84
+ const result = plan({
85
+ routing: { mode: "fixed", sellerId: "s3", scorer: "speed" }
86
+ });
87
+
88
+ expect(result.routes).toEqual([]);
89
+ expect(result.reason).toBe("fixed_seller_not_compatible");
90
+ });
91
+
92
+ test("fixedSet mode does not select outside the configured seller pool", () => {
93
+ const result = plan({
94
+ routing: { mode: "fixedSet", sellerIds: ["s1", "s2"], scorer: "speed" },
95
+ sellerMetrics: [
96
+ { sellerId: "outside", healthScore: 100, avgLatencyMs: 10 },
97
+ { sellerId: "s1", healthScore: 40, avgLatencyMs: 20 },
98
+ { sellerId: "s2", healthScore: 90, avgLatencyMs: 30 }
99
+ ],
100
+ registrySellers: [
101
+ seller({ id: "outside" }),
102
+ seller({ id: "s1" }),
103
+ seller({ id: "s2" })
104
+ ]
105
+ });
106
+
107
+ expect(result.routes.map((route) => route.seller.id)).toEqual(["s2", "s1"]);
108
+ });
109
+
110
+ test("discount scorer uses planner metrics during registry fallback", () => {
111
+ const result = plan({
112
+ routing: { mode: "fullAuto", scorer: "discount" },
113
+ sellerMetrics: [
114
+ { sellerId: "s1", healthScore: 100, discountRatio: 1 },
115
+ { sellerId: "s2", healthScore: 30, discountRatio: 0.01 }
116
+ ]
117
+ });
118
+
119
+ expect(result.routes.map((route) => route.seller.id)).toEqual(["s2", "s1"]);
120
+ });
121
+
122
+ test("messages protocol accepts anthropic_messages registry sellers", () => {
123
+ const result = plan({
124
+ modelId: "MiniMax-M2.7",
125
+ protocol: "messages",
126
+ registrySellers: [
127
+ seller({
128
+ id: "minimax",
129
+ models: ["MiniMax-M2.7"],
130
+ supportedProtocols: ["anthropic_messages"],
131
+ paymentMethods: ["clawtip"]
132
+ })
133
+ ]
134
+ });
135
+
136
+ expect(result.routes.map((route) => route.seller.id)).toEqual(["minimax"]);
137
+ });
138
+
139
+ test("open circuit observations are excluded before strategy selection", () => {
140
+ const result = plan({
141
+ routing: { mode: "fullAuto", scorer: "speed" },
142
+ sellerMetrics: [
143
+ { sellerId: "s1", healthScore: 100, circuit: "open" },
144
+ { sellerId: "s2", healthScore: 20, circuit: "closed" }
145
+ ]
146
+ });
147
+
148
+ expect(result.routes.map((route) => route.seller.id)).toEqual(["s2"]);
149
+ });
150
+ });
@@ -0,0 +1,111 @@
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
+ scorer: "speed"
23
+ })).toEqual({
24
+ mode: "fixed",
25
+ sellerId: "tbs-1",
26
+ scorer: "speed"
27
+ });
28
+
29
+ expect(normalizeSellerRoutingConfig({
30
+ mode: "fixedSet",
31
+ sellerIds: ["tbs-1", " ", "tbs-2", "tbs-1"],
32
+ scorer: "discount"
33
+ })).toEqual({
34
+ mode: "fixedSet",
35
+ sellerIds: ["tbs-1", "tbs-2"],
36
+ scorer: "discount"
37
+ });
38
+ });
39
+
40
+ test("does not accept legacy auto/manual modes", () => {
41
+ expect(() => normalizeSellerRoutingConfig({ mode: "auto" })).toThrow("seller routing mode");
42
+ expect(() => parseSellerRoutingEnv({ TB_PROXYD_ROUTING_MODE: "manual" })).toThrow("seller routing mode");
43
+ });
44
+
45
+ test("rejects invalid routing scorer values", () => {
46
+ expect(() => normalizeSellerRoutingConfig({ mode: "fullAuto", scorer: "fast" })).toThrow("seller routing scorer");
47
+ expect(() => parseSellerRoutingEnv({ TB_PROXYD_ROUTING_SCORER: "random" })).toThrow("seller routing scorer");
48
+ });
49
+
50
+ test("parses only the new routing environment variables", () => {
51
+ expect(parseSellerRoutingEnv({
52
+ TB_PROXYD_ROUTING_MODE: "fixedSet",
53
+ TB_PROXYD_ROUTING_SELLER_IDS: "tbs-1,tbs-2,tbs-1",
54
+ TB_PROXYD_ROUTING_SCORER: "speed"
55
+ })).toEqual({
56
+ mode: "fixedSet",
57
+ sellerIds: ["tbs-1", "tbs-2"],
58
+ scorer: "speed"
59
+ });
60
+
61
+ expect(parseSellerRoutingEnv({
62
+ TB_PROXYD_SELECTION_MODE: "manual",
63
+ TB_PROXYD_SELECTED_SELLER_ID: "old-seller"
64
+ })).toBeUndefined();
65
+ });
66
+
67
+ test("infers routing mode from individual environment variables", () => {
68
+ expect(parseSellerRoutingEnv({
69
+ TB_PROXYD_ROUTING_SELLER_ID: "tbs-1"
70
+ })).toEqual({
71
+ mode: "fixed",
72
+ sellerId: "tbs-1",
73
+ scorer: "balanced"
74
+ });
75
+
76
+ expect(parseSellerRoutingEnv({
77
+ TB_PROXYD_ROUTING_SELLER_IDS: "tbs-1,tbs-2"
78
+ })).toEqual({
79
+ mode: "fixedSet",
80
+ sellerIds: ["tbs-1", "tbs-2"],
81
+ scorer: "balanced"
82
+ });
83
+
84
+ expect(parseSellerRoutingEnv({
85
+ TB_PROXYD_ROUTING_SCORER: "discount"
86
+ })).toEqual({
87
+ mode: "fullAuto",
88
+ scorer: "discount"
89
+ });
90
+ });
91
+
92
+ test("merges explicit overrides over stored config", () => {
93
+ expect(mergeSellerRoutingConfig(
94
+ { mode: "fullAuto", scorer: "balanced" },
95
+ { mode: "fixed", sellerId: "tbs-1", scorer: "discount" }
96
+ )).toEqual({
97
+ mode: "fixed",
98
+ sellerId: "tbs-1",
99
+ scorer: "discount"
100
+ });
101
+ });
102
+
103
+ test("validates required fixed strategy parameters", () => {
104
+ expect(() => assertSellerRoutingConfig({ mode: "fixed", scorer: "balanced" })).toThrow("--seller");
105
+ expect(() => assertSellerRoutingConfig({ mode: "fixedSet", sellerIds: [], scorer: "balanced" })).toThrow("--seller-set");
106
+ });
107
+
108
+ test("parses comma-separated seller id lists", () => {
109
+ expect(parseSellerIdList(" a, b,,a ")).toEqual(["a", "b"]);
110
+ });
111
+ });
@@ -0,0 +1,166 @@
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
+ discountRatio: overrides.discountRatio,
16
+ registryOrder: overrides.registryOrder
17
+ };
18
+ }
19
+
20
+ function planIds(candidates: RoutingCandidate[], config: SellerRoutingStrategyConfig): string[] {
21
+ return planSellerRoutes(candidates, config).routes.map((route) => route.sellerId);
22
+ }
23
+
24
+ describe("seller routing strategy", () => {
25
+ test("fixed mode returns only the configured seller", () => {
26
+ const result = planSellerRoutes(
27
+ [
28
+ candidate({ sellerId: "s1", registryOrder: 0, healthScore: 40 }),
29
+ candidate({ sellerId: "s2", registryOrder: 1, healthScore: 99 })
30
+ ],
31
+ { mode: "fixed", sellerId: "s1", scorer: "speed" }
32
+ );
33
+
34
+ expect(result.routes.map((route) => route.sellerId)).toEqual(["s1"]);
35
+ expect(result.reason).toBe("fixed:speed:routes_1");
36
+ });
37
+
38
+ test("fixed mode matches seller ids case-insensitively", () => {
39
+ const ids = planIds(
40
+ [
41
+ candidate({ sellerId: "TBS-1", registryOrder: 0 }),
42
+ candidate({ sellerId: "tbs-2", registryOrder: 1 })
43
+ ],
44
+ { mode: "fixed", sellerId: "tbs-1" }
45
+ );
46
+
47
+ expect(ids).toEqual(["TBS-1"]);
48
+ });
49
+
50
+ test("fixed mode fails closed when the configured seller is not compatible", () => {
51
+ const result = planSellerRoutes(
52
+ [
53
+ candidate({ sellerId: "s1", registryOrder: 0, supportsPayment: false }),
54
+ candidate({ sellerId: "s2", registryOrder: 1 })
55
+ ],
56
+ { mode: "fixed", sellerId: "s1" }
57
+ );
58
+
59
+ expect(result.routes).toEqual([]);
60
+ expect(result.reason).toBe("fixed_seller_not_compatible");
61
+ });
62
+
63
+ test("fixed mode requires a seller id", () => {
64
+ const result = planSellerRoutes([candidate({ sellerId: "s1", registryOrder: 0 })], { mode: "fixed" });
65
+
66
+ expect(result.routes).toEqual([]);
67
+ expect(result.reason).toBe("fixed_seller_missing");
68
+ });
69
+
70
+ test("fixedSet mode never selects sellers outside the configured pool", () => {
71
+ const ids = planIds(
72
+ [
73
+ candidate({ sellerId: "outside-fast", registryOrder: 0, healthScore: 100 }),
74
+ candidate({ sellerId: "pool-a", registryOrder: 1, healthScore: 30 }),
75
+ candidate({ sellerId: "pool-b", registryOrder: 2, healthScore: 70 })
76
+ ],
77
+ { mode: "fixedSet", sellerIds: ["pool-a", "pool-b"], scorer: "speed" }
78
+ );
79
+
80
+ expect(ids).toEqual(["pool-b", "pool-a"]);
81
+ });
82
+
83
+ test("fixedSet mode matches seller pools case-insensitively", () => {
84
+ const ids = planIds(
85
+ [
86
+ candidate({ sellerId: "TBS-A", registryOrder: 0, healthScore: 30 }),
87
+ candidate({ sellerId: "tbs-B", registryOrder: 1, healthScore: 70 })
88
+ ],
89
+ { mode: "fixedSet", sellerIds: ["TBS-A", "TBS-B"], scorer: "balanced" }
90
+ );
91
+
92
+ expect(ids).toEqual(["tbs-B", "TBS-A"]);
93
+ });
94
+
95
+ test("fixedSet mode reports empty pool before routing", () => {
96
+ const result = planSellerRoutes([candidate({ sellerId: "s1", registryOrder: 0 })], { mode: "fixedSet", sellerIds: [] });
97
+
98
+ expect(result.routes).toEqual([]);
99
+ expect(result.reason).toBe("fixed_set_empty");
100
+ });
101
+
102
+ test("fullAuto mode uses all compatible candidates", () => {
103
+ const ids = planIds(
104
+ [
105
+ candidate({ sellerId: "s1", registryOrder: 0 }),
106
+ candidate({ sellerId: "s2", registryOrder: 1, supportsProtocol: false }),
107
+ candidate({ sellerId: "s3", registryOrder: 2 })
108
+ ],
109
+ { mode: "fullAuto" }
110
+ );
111
+
112
+ expect(ids).toEqual(["s1", "s3"]);
113
+ });
114
+
115
+ test("speed scorer prefers lower TTFT and average inference time, then health", () => {
116
+ const ids = planIds(
117
+ [
118
+ candidate({ sellerId: "high-slow", registryOrder: 0, healthScore: 90, ttftMs: 800, avgInferenceMs: 800 }),
119
+ candidate({ sellerId: "high-fast", registryOrder: 1, healthScore: 90, ttftMs: 100, avgInferenceMs: 100 }),
120
+ candidate({ sellerId: "low-fast", registryOrder: 2, healthScore: 40, ttftMs: 10, avgInferenceMs: 10 })
121
+ ],
122
+ { mode: "fullAuto", scorer: "speed" }
123
+ );
124
+
125
+ expect(ids).toEqual(["low-fast", "high-fast", "high-slow"]);
126
+ });
127
+
128
+ test("discount scorer prefers lower discount ratio, then health", () => {
129
+ const ids = planIds(
130
+ [
131
+ candidate({ sellerId: "expensive", registryOrder: 0, discountRatio: 1, healthScore: 100 }),
132
+ candidate({ sellerId: "cheap-low-health", registryOrder: 1, discountRatio: 0.01, healthScore: 20 }),
133
+ candidate({ sellerId: "cheap-high-health", registryOrder: 2, discountRatio: 0.01, healthScore: 90 })
134
+ ],
135
+ { mode: "fullAuto", scorer: "discount" }
136
+ );
137
+
138
+ expect(ids).toEqual(["cheap-high-health", "cheap-low-health", "expensive"]);
139
+ });
140
+
141
+ test("balanced scorer combines health, latency, and discount", () => {
142
+ const ids = planIds(
143
+ [
144
+ candidate({ sellerId: "healthy-expensive", registryOrder: 0, healthScore: 90, avgLatencyMs: 100, discountRatio: 1 }),
145
+ candidate({ sellerId: "balanced", registryOrder: 1, healthScore: 80, avgLatencyMs: 100, discountRatio: 0.01 }),
146
+ candidate({ sellerId: "cheap-unhealthy", registryOrder: 2, healthScore: 20, avgLatencyMs: 100, discountRatio: 0.01 })
147
+ ],
148
+ { mode: "fullAuto", scorer: "balanced" }
149
+ );
150
+
151
+ expect(ids).toEqual(["balanced", "cheap-unhealthy", "healthy-expensive"]);
152
+ });
153
+
154
+ test("registry order is the stable tie breaker", () => {
155
+ const ids = planIds(
156
+ [
157
+ candidate({ sellerId: "s2", registryOrder: 2, healthScore: 80, avgLatencyMs: 100, discountRatio: 0.5 }),
158
+ candidate({ sellerId: "s0", registryOrder: 0, healthScore: 80, avgLatencyMs: 100, discountRatio: 0.5 }),
159
+ candidate({ sellerId: "s1", registryOrder: 1, healthScore: 80, avgLatencyMs: 100, discountRatio: 0.5 })
160
+ ],
161
+ { mode: "fullAuto", scorer: "balanced" }
162
+ );
163
+
164
+ expect(ids).toEqual(["s0", "s1", "s2"]);
165
+ });
166
+ });