@tokenbuddy/tokenbuddy 1.0.36 → 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,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
- });