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