@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,222 +0,0 @@
1
- import { CreditTracker } from "../src/credit-tracker.js";
2
- import { ModelIndex } from "../src/model-index.js";
3
- import { PrewarmCache } from "../src/prewarm-cache.js";
4
- import { RouteFailover, type DecideContext } from "../src/route-failover.js";
5
- import { SellerPool } from "../src/seller-pool.js";
6
- import type { RegistrySeller } from "../src/seller-catalog.js";
7
-
8
- function makeSeller(overrides: Partial<RegistrySeller> & { id: string; models?: string[] }): RegistrySeller {
9
- return {
10
- id: overrides.id,
11
- name: overrides.name ?? overrides.id,
12
- url: overrides.url ?? `https://${overrides.id}.example.com`,
13
- supportedProtocols: overrides.supportedProtocols ?? ["chat_completions"],
14
- paymentMethods: overrides.paymentMethods ?? ["clawtip"],
15
- models: overrides.models
16
- };
17
- }
18
-
19
- function buildHarness(seed: { id: string; healthScore?: number }[] = []) {
20
- const sellers: RegistrySeller[] = seed.map((s) => makeSeller({ id: s.id, models: ["gpt-4o"] }));
21
- const index = new ModelIndex();
22
- index.rebuild(sellers, { registryVersion: 1, defaultSellerId: seed[0]?.id });
23
- const cache = new PrewarmCache();
24
- cache.commitWarm({
25
- modelId: "gpt-4o",
26
- protocol: "chat_completions",
27
- paymentMethod: "clawtip",
28
- candidates: seed.map((s) => ({
29
- sellerId: s.id,
30
- url: `https://${s.id}.example.com`,
31
- healthScore: s.healthScore ?? 80
32
- }))
33
- });
34
- const credit = new CreditTracker();
35
- const pool = new SellerPool({ modelIndex: index, cache, creditTracker: credit });
36
- pool.sync();
37
- const failover = new RouteFailover({ pool, creditTracker: credit });
38
- return { index, cache, credit, pool, failover, sellers };
39
- }
40
-
41
- describe("RouteFailover", () => {
42
- test("pickNext returns the highest-scoring candidate from the pool", () => {
43
- const { failover } = buildHarness([{ id: "s1", healthScore: 50 }, { id: "s2", healthScore: 90 }, { id: "s3", healthScore: 70 }]);
44
- const next = failover.pickNext("gpt-4o", "chat_completions", "clawtip");
45
- expect(next?.sellerId).toBe("s2");
46
- });
47
-
48
- test("pickNext returns undefined when the pool is exhausted", () => {
49
- const index = new ModelIndex();
50
- index.rebuild([], { registryVersion: 1 });
51
- const cache = new PrewarmCache();
52
- const credit = new CreditTracker();
53
- const pool = new SellerPool({ modelIndex: index, cache, creditTracker: credit });
54
- pool.sync();
55
- const failover = new RouteFailover({ pool, creditTracker: credit });
56
- expect(failover.pickNext("gpt-4o", "chat_completions", "clawtip")).toBeUndefined();
57
- });
58
-
59
- test("decide on hard_4xx immediately returns failover_next and never retries", () => {
60
- const { failover, credit } = buildHarness([{ id: "s1" }]);
61
- credit.recordPurchase("s1", 1_000_000, 800_000);
62
- const context: DecideContext = {
63
- sellerId: "s1",
64
- status: 404,
65
- errorKind: "hard_4xx",
66
- errorMessage: "model not found",
67
- attempt: 0
68
- };
69
- const decision = failover.decide(context, 1);
70
- expect(decision.action).toBe("failover_next");
71
- expect(decision.reason).toBe("hard_failure:hard_4xx");
72
- expect(decision.freshPurchase).toBe(true);
73
- // Hard failure transfers leftover to wasted.
74
- expect(decision.wastedCreditMicros).toBeGreaterThan(0);
75
- });
76
-
77
- test("decide on auth_invalid opens the circuit and surfaces failover", () => {
78
- const { failover, pool } = buildHarness([{ id: "s1" }]);
79
- const context: DecideContext = {
80
- sellerId: "s1",
81
- status: 401,
82
- errorKind: "auth_invalid",
83
- errorMessage: "token_invalid",
84
- attempt: 0
85
- };
86
- const decision = failover.decide(context, 1);
87
- expect(decision.action).toBe("failover_next");
88
- expect(pool.snapshot()[0].circuit).toBe("open");
89
- });
90
-
91
- test("decide on soft_5xx inside fresh-purchase window retries the same seller with jitter", () => {
92
- const { failover, credit } = buildHarness([{ id: "s1" }]);
93
- credit.recordPurchase("s1", 1_000_000, 1_000_000);
94
- const decision = failover.decide(
95
- { sellerId: "s1", status: 503, errorKind: "soft_5xx", errorMessage: "upstream", attempt: 0 },
96
- 1
97
- );
98
- expect(decision.action).toBe("retry_same_seller");
99
- expect(decision.retryDelayMs).toBeGreaterThanOrEqual(100);
100
- expect(decision.retryDelayMs).toBeLessThanOrEqual(400);
101
- expect(decision.freshPurchase).toBe(true);
102
- expect(decision.reason).toBe("soft_failure_fresh_purchase_window");
103
- });
104
-
105
- test("decide on busy_capacity fails over without retrying or opening the circuit", () => {
106
- const { failover, credit, pool } = buildHarness([{ id: "s1" }, { id: "s2" }]);
107
- credit.recordPurchase("s1", 1_000_000, 1_000_000);
108
- const decision = failover.decide(
109
- { sellerId: "s1", status: 429, errorKind: "busy_capacity", errorMessage: "capacity full", attempt: 0 },
110
- 2
111
- );
112
-
113
- expect(decision.action).toBe("failover_next");
114
- expect(decision.reason).toBe("busy_capacity");
115
- const entry = pool.snapshot().find((candidate) => candidate.sellerId === "s1");
116
- expect(entry?.circuit).toBe("closed");
117
- expect(entry?.consecutiveFailures).toBe(0);
118
- expect(entry?.capacityBlockedUntil).toBeGreaterThan(Date.now());
119
- });
120
-
121
- test("decide on soft_5xx after exhausting retries returns failover_next", () => {
122
- const { failover, credit } = buildHarness([{ id: "s1" }]);
123
- credit.recordPurchase("s1", 1_000_000, 1_000_000);
124
- const decision = failover.decide(
125
- { sellerId: "s1", status: 503, errorKind: "soft_5xx", errorMessage: "upstream", attempt: 2 },
126
- 1
127
- );
128
- expect(decision.action).toBe("failover_next");
129
- expect(decision.reason).toBe("soft_failure_exhausted_retries");
130
- });
131
-
132
- test("decide on soft_5xx outside the fresh-purchase window still allows the first retry", () => {
133
- const { failover, credit } = buildHarness([{ id: "s1" }]);
134
- // Record a purchase, then spend it past the threshold so the window exits.
135
- credit.recordPurchase("s1", 1_000_000, 1_000_000);
136
- credit.recordSpend("s1", 100_000);
137
- const decision = failover.decide(
138
- { sellerId: "s1", status: 503, errorKind: "soft_5xx", errorMessage: "upstream", attempt: 0 },
139
- 1
140
- );
141
- expect(decision.action).toBe("retry_same_seller");
142
- expect(decision.freshPurchase).toBe(false);
143
- expect(decision.reason).toBe("soft_failure_retry");
144
- });
145
-
146
- test("decide on deadline uses the soft-error path", () => {
147
- const { failover, credit } = buildHarness([{ id: "s1" }]);
148
- credit.recordPurchase("s1", 1_000_000, 1_000_000);
149
- const decision = failover.decide(
150
- { sellerId: "s1", errorKind: "deadline", errorMessage: "buyer timeout", attempt: 0 },
151
- 1
152
- );
153
- expect(decision.action).toBe("retry_same_seller");
154
- expect(decision.freshPurchase).toBe(true);
155
- });
156
-
157
- test("decide on purchase_failed fails over without scheduling a soft retry", () => {
158
- const { failover, credit } = buildHarness([{ id: "s1" }, { id: "s2" }]);
159
- credit.recordPurchase("s1", 1_000_000, 750_000);
160
- const decision = failover.decide(
161
- { sellerId: "s1", errorKind: "purchase_failed", errorMessage: "purchase create failed", attempt: 0 },
162
- 2
163
- );
164
-
165
- expect(decision.action).toBe("failover_next");
166
- expect(decision.reason).toBe("purchase_failed");
167
- expect(decision.wastedCreditMicros).toBe(750_000);
168
- });
169
-
170
- test("budgetExceeded flag is set when the per-minute purchase budget is exhausted", () => {
171
- const { failover, credit } = buildHarness([{ id: "s1" }]);
172
- // Burn through the per-minute budget.
173
- credit.recordPurchase("s1", 100, 100);
174
- credit.recordPurchase("s2" in credit.summary().perSeller ? "s2" : "s1", 100, 100);
175
- credit.recordPurchase("s1", 100, 100);
176
- credit.recordPurchase("s1", 100, 100);
177
- const decision = failover.decide(
178
- { sellerId: "s1", errorKind: "soft_5xx", attempt: 0 },
179
- 1
180
- );
181
- expect(decision.budgetExceeded).toBe(true);
182
- });
183
-
184
- test("recordSuccess resets failure counters and reports to the credit tracker", () => {
185
- const { failover, credit, pool } = buildHarness([{ id: "s1" }]);
186
- credit.recordPurchase("s1", 1_000_000, 1_000_000);
187
- failover.decide(
188
- { sellerId: "s1", errorKind: "soft_5xx", attempt: 0 },
189
- 1
190
- );
191
- failover.decide(
192
- { sellerId: "s1", errorKind: "soft_5xx", attempt: 1 },
193
- 1
194
- );
195
- failover.recordSuccess("s1", 750_000);
196
- const entry = pool.snapshot()[0];
197
- expect(entry.consecutiveFailures).toBe(0);
198
- expect(credit.getEntry("s1")?.currentBalanceMicros).toBe(750_000);
199
- });
200
-
201
- test("shouldAbort returns true when no more candidates are available", () => {
202
- const { failover } = buildHarness([{ id: "s1" }]);
203
- expect(failover.shouldAbort(1, false)).toBe(true);
204
- expect(failover.shouldAbort(1, true)).toBe(false);
205
- });
206
-
207
- test("canAutoPurchase mirrors the credit tracker budget", () => {
208
- const { failover, credit } = buildHarness([{ id: "s1" }]);
209
- expect(failover.canAutoPurchase()).toBe(true);
210
- credit.recordPurchase("s1", 100, 100);
211
- credit.recordPurchase("s1", 100, 100);
212
- credit.recordPurchase("s1", 100, 100);
213
- expect(failover.canAutoPurchase()).toBe(false);
214
- });
215
-
216
- test("wastedMicrosFor returns the latest known balance for a seller", () => {
217
- const { failover, credit } = buildHarness([{ id: "s1" }]);
218
- expect(failover.wastedMicrosFor("s1")).toBe(0);
219
- credit.recordPurchase("s1", 1_000_000, 500_000);
220
- expect(failover.wastedMicrosFor("s1")).toBe(500_000);
221
- });
222
- });
@@ -1,120 +0,0 @@
1
- import * as crypto from "crypto";
2
- import { RegistryTooLargeError, fetchSellerRegistry, fetchSellerRegistryWithTrust } from "../src/seller-catalog.js";
3
- import { DEFAULT_SELLER_REGISTRY_URL } from "../src/registry-trust.js";
4
-
5
- /**
6
- * v1.2 §18.9: registry-too-large fallback. The bootstrap returns 413
7
- * with a structured body when the serialized registry exceeds 1MB; the
8
- * buyer must surface this as a typed error so the daemon can fall back
9
- * to the last-known snapshot.
10
- */
11
- describe("RegistryTooLargeError + fetchSellerRegistry 413", () => {
12
- const originalFetch = globalThis.fetch;
13
-
14
- afterEach(() => {
15
- globalThis.fetch = originalFetch;
16
- });
17
-
18
- function mockJsonResponse(status: number, body: unknown): Response {
19
- return new Response(JSON.stringify(body), {
20
- status,
21
- headers: { "Content-Type": "application/json" }
22
- });
23
- }
24
-
25
- test("fetchSellerRegistry throws RegistryTooLargeError on HTTP 413 with the bootstrap's payload", async () => {
26
- globalThis.fetch = (async () =>
27
- mockJsonResponse(413, {
28
- error: "registry_too_large",
29
- sizeBytes: 1_500_000,
30
- sellerCount: 2_400,
31
- maxBytes: 1_000_000
32
- })) as unknown as typeof globalThis.fetch;
33
- await expect(
34
- fetchSellerRegistry("https://bootstrap.example/registry/sellers")
35
- ).rejects.toMatchObject({
36
- name: "RegistryTooLargeError",
37
- status: 413,
38
- sizeBytes: 1_500_000,
39
- sellerCount: 2_400,
40
- maxBytes: 1_000_000
41
- });
42
- });
43
-
44
- test("fetchSellerRegistry still surfaces generic non-2xx as plain errors", async () => {
45
- globalThis.fetch = (async () => mockJsonResponse(500, { error: "internal" })) as unknown as typeof globalThis.fetch;
46
- await expect(
47
- fetchSellerRegistry("https://bootstrap.example/registry/sellers")
48
- ).rejects.toThrow(/registry returned 500/);
49
- });
50
-
51
- test("fetchSellerRegistry rejects the default registry when signature is missing", async () => {
52
- globalThis.fetch = (async (url: string | URL | Request) => {
53
- const href = String(url);
54
- if (href.endsWith("/registry.json")) {
55
- return mockJsonResponse(200, { version: 1, sellers: [] });
56
- }
57
- if (href.endsWith("/registry.sig")) {
58
- return mockJsonResponse(404, { error: "missing" });
59
- }
60
- throw new Error(`unexpected fetch ${href}`);
61
- }) as unknown as typeof globalThis.fetch;
62
-
63
- await expect(fetchSellerRegistry(DEFAULT_SELLER_REGISTRY_URL)).rejects.toThrow("registry signature returned 404");
64
- });
65
-
66
- test("fetchSellerRegistry can explicitly allow unsigned registry for local development", async () => {
67
- const previous = process.env.TB_PROXYD_ALLOW_UNSIGNED_REGISTRY;
68
- process.env.TB_PROXYD_ALLOW_UNSIGNED_REGISTRY = "1";
69
- globalThis.fetch = (async () => mockJsonResponse(200, { version: 1, sellers: [] })) as unknown as typeof globalThis.fetch;
70
- try {
71
- await expect(fetchSellerRegistry(DEFAULT_SELLER_REGISTRY_URL)).resolves.toMatchObject({ version: 1, sellers: [] });
72
- } finally {
73
- if (previous === undefined) {
74
- delete process.env.TB_PROXYD_ALLOW_UNSIGNED_REGISTRY;
75
- } else {
76
- process.env.TB_PROXYD_ALLOW_UNSIGNED_REGISTRY = previous;
77
- }
78
- }
79
- });
80
-
81
- test("fetchSellerRegistryWithTrust returns hash and unsigned trust metadata for allowed local development", async () => {
82
- const previous = process.env.TB_PROXYD_ALLOW_UNSIGNED_REGISTRY;
83
- process.env.TB_PROXYD_ALLOW_UNSIGNED_REGISTRY = "1";
84
- const body = JSON.stringify({ version: 1, sellers: [] });
85
- globalThis.fetch = (async () => new Response(body, {
86
- status: 200,
87
- headers: { "Content-Type": "application/json" }
88
- })) as unknown as typeof globalThis.fetch;
89
- try {
90
- const result = await fetchSellerRegistryWithTrust(DEFAULT_SELLER_REGISTRY_URL);
91
- expect(result.registry).toMatchObject({ version: 1, sellers: [] });
92
- expect(result.trust).toMatchObject({
93
- registryUrl: DEFAULT_SELLER_REGISTRY_URL,
94
- registrySha256: crypto.createHash("sha256").update(body).digest("hex"),
95
- verified: false
96
- });
97
- expect(result.trust.signature).toBeUndefined();
98
- expect(result.trust.signingKeyId).toBeUndefined();
99
- } finally {
100
- if (previous === undefined) {
101
- delete process.env.TB_PROXYD_ALLOW_UNSIGNED_REGISTRY;
102
- } else {
103
- process.env.TB_PROXYD_ALLOW_UNSIGNED_REGISTRY = previous;
104
- }
105
- }
106
- });
107
-
108
- test("RegistryTooLargeError is a stable Error subclass with a useful message", () => {
109
- const err = new RegistryTooLargeError({
110
- status: 413,
111
- sizeBytes: 2_500_000,
112
- sellerCount: 1500,
113
- maxBytes: 1_000_000
114
- });
115
- expect(err).toBeInstanceOf(Error);
116
- expect(err.name).toBe("RegistryTooLargeError");
117
- expect(err.message).toContain("1500 sellers");
118
- expect(err.message).toContain("2500000");
119
- });
120
- });
@@ -1,124 +0,0 @@
1
- import {
2
- dedupeCatalogEntries,
3
- discoverSellerBackedModels,
4
- filterCatalogByProtocol,
5
- filterCatalogBySeller,
6
- manifestModelIds,
7
- type ModelCatalogEntry,
8
- type SellerManifest
9
- } from "../src/seller-catalog.js";
10
-
11
- function model(overrides: Partial<ModelCatalogEntry> & { id: string; sellerId: string }): ModelCatalogEntry {
12
- return {
13
- id: overrides.id,
14
- sellerId: overrides.sellerId,
15
- sellerUrl: overrides.sellerUrl ?? `https://${overrides.sellerId}.example.com`,
16
- supportedProtocols: overrides.supportedProtocols ?? ["chat_completions"],
17
- paymentMethods: overrides.paymentMethods ?? ["mock"],
18
- sellerName: overrides.sellerName,
19
- inputPriceMicrosPer1m: overrides.inputPriceMicrosPer1m,
20
- outputPriceMicrosPer1m: overrides.outputPriceMicrosPer1m
21
- };
22
- }
23
-
24
- describe("seller catalog utilities", () => {
25
- test("filters catalog entries by protocol", () => {
26
- const entries = [
27
- model({ id: "gpt-5.4", sellerId: "seller-a", supportedProtocols: ["chat_completions"] }),
28
- model({ id: "claude", sellerId: "seller-b", supportedProtocols: ["messages"] })
29
- ];
30
-
31
- expect(filterCatalogByProtocol([], "chat_completions")).toEqual([]);
32
- expect(filterCatalogByProtocol(entries, "chat_completions")).toEqual([entries[0]]);
33
- expect(filterCatalogByProtocol(entries, "responses")).toEqual([]);
34
- });
35
-
36
- test("filters catalog entries by seller when seller id is provided", () => {
37
- const entries = [
38
- model({ id: "m1", sellerId: "seller-a" }),
39
- model({ id: "m2", sellerId: "seller-b" })
40
- ];
41
-
42
- expect(filterCatalogBySeller([], "seller-a")).toEqual([]);
43
- expect(filterCatalogBySeller(entries, "seller-a")).toEqual([entries[0]]);
44
- expect(filterCatalogBySeller(entries, undefined)).toEqual(entries);
45
- });
46
-
47
- test("deduplicates by seller id and model id while preserving order", () => {
48
- const first = model({ id: "same", sellerId: "seller-a" });
49
- const duplicate = model({ id: "same", sellerId: "seller-a", sellerUrl: "https://duplicate.example.com" });
50
- const sameModelDifferentSeller = model({ id: "same", sellerId: "seller-b" });
51
- const other = model({ id: "other", sellerId: "seller-a" });
52
-
53
- expect(dedupeCatalogEntries([first, duplicate, sameModelDifferentSeller, other])).toEqual([
54
- first,
55
- sameModelDifferentSeller,
56
- other
57
- ]);
58
- });
59
-
60
- test("returns trimmed, non-empty manifest model ids only", () => {
61
- const manifest = {
62
- models: [
63
- { id: " gpt-5.4 " },
64
- { id: "" },
65
- { id: "claude" }
66
- ]
67
- } as unknown as SellerManifest;
68
-
69
- expect(manifestModelIds(manifest)).toEqual(["gpt-5.4", "claude"]);
70
- });
71
-
72
- test("discovers models from active buyer-visible registry sellers only", async () => {
73
- const fetchMock = jest.spyOn(globalThis, "fetch").mockImplementation(async (url) => {
74
- const href = String(url);
75
- if (href.endsWith("/registry/sellers")) {
76
- return jsonResponse({
77
- version: 8,
78
- sellers: [
79
- {
80
- id: "active",
81
- status: "active",
82
- url: "https://active.example.com",
83
- supportedProtocols: ["chat_completions"],
84
- paymentMethods: ["clawtip"]
85
- },
86
- {
87
- id: "pending",
88
- status: "pending",
89
- url: "https://pending.example.com",
90
- supportedProtocols: ["chat_completions"],
91
- paymentMethods: ["clawtip"]
92
- }
93
- ]
94
- });
95
- }
96
- if (href === "https://active.example.com/manifest") {
97
- return jsonResponse({
98
- sellerId: "active",
99
- supportedProtocols: ["chat_completions"],
100
- paymentMethods: ["clawtip"],
101
- models: [{ id: "gpt-5.4" }]
102
- });
103
- }
104
- throw new Error(`unexpected fetch ${href}`);
105
- });
106
-
107
- try {
108
- const catalog = await discoverSellerBackedModels("https://bootstrap.example.com/registry/sellers");
109
-
110
- expect(catalog.sellers.map((seller) => seller.id)).toEqual(["active"]);
111
- expect(catalog.models.map((model) => model.sellerId)).toEqual(["active"]);
112
- expect(fetchMock).not.toHaveBeenCalledWith("https://pending.example.com/manifest");
113
- } finally {
114
- fetchMock.mockRestore();
115
- }
116
- });
117
- });
118
-
119
- function jsonResponse(body: unknown): Response {
120
- return new Response(JSON.stringify(body), {
121
- status: 200,
122
- headers: { "Content-Type": "application/json" }
123
- });
124
- }
@@ -1,83 +0,0 @@
1
- import { SellerConcurrencyLimiter } from "../src/seller-concurrency-limiter.js";
2
-
3
- describe("SellerConcurrencyLimiter", () => {
4
- afterEach(() => {
5
- jest.useRealTimers();
6
- });
7
-
8
- test("returns disabled no-op leases by default", () => {
9
- const limiter = new SellerConcurrencyLimiter();
10
- const first = limiter.tryAcquire("seller-a");
11
- const second = limiter.tryAcquire("seller-a");
12
-
13
- expect(first).toBeDefined();
14
- expect(second).toBeDefined();
15
- expect(limiter.snapshot()).toEqual({
16
- enabled: false,
17
- maxInFlightPerSeller: 2,
18
- leaseTtlMs: 185000,
19
- active: []
20
- });
21
- });
22
-
23
- test("rejects sellers at the local in-flight limit and releases idempotently", () => {
24
- const limiter = new SellerConcurrencyLimiter({
25
- enabled: true,
26
- maxInFlightPerSeller: 1,
27
- leaseTtlMs: 10000
28
- });
29
-
30
- const lease = limiter.tryAcquire("seller-a");
31
- expect(lease).toBeDefined();
32
- expect(limiter.tryAcquire("seller-a")).toBeUndefined();
33
- expect(limiter.tryAcquire("seller-b")).toBeDefined();
34
- expect(limiter.snapshot().active).toEqual([
35
- { sellerId: "seller-a", activeCount: 1 },
36
- { sellerId: "seller-b", activeCount: 1 }
37
- ]);
38
-
39
- lease?.release();
40
- lease?.release();
41
-
42
- expect(limiter.tryAcquire("seller-a")).toBeDefined();
43
- });
44
-
45
- test("expires leaked leases so sellers become selectable again", () => {
46
- jest.useFakeTimers();
47
- const limiter = new SellerConcurrencyLimiter({
48
- enabled: true,
49
- maxInFlightPerSeller: 1,
50
- leaseTtlMs: 1000
51
- });
52
-
53
- expect(limiter.tryAcquire("seller-a")).toBeDefined();
54
- expect(limiter.tryAcquire("seller-a")).toBeUndefined();
55
-
56
- jest.advanceTimersByTime(1000);
57
-
58
- expect(limiter.snapshot().active).toEqual([]);
59
- expect(limiter.tryAcquire("seller-a")).toBeDefined();
60
- });
61
-
62
- test("refresh extends an active lease watchdog", () => {
63
- jest.useFakeTimers();
64
- const limiter = new SellerConcurrencyLimiter({
65
- enabled: true,
66
- maxInFlightPerSeller: 1,
67
- leaseTtlMs: 1000
68
- });
69
-
70
- const lease = limiter.tryAcquire("seller-a");
71
- expect(lease).toBeDefined();
72
-
73
- jest.advanceTimersByTime(900);
74
- lease?.refresh();
75
- jest.advanceTimersByTime(900);
76
-
77
- expect(limiter.snapshot().active).toEqual([{ sellerId: "seller-a", activeCount: 1 }]);
78
-
79
- jest.advanceTimersByTime(100);
80
-
81
- expect(limiter.snapshot().active).toEqual([]);
82
- });
83
- });
@@ -1,89 +0,0 @@
1
- import * as http from "http";
2
- import { SellerMetadataCache } from "../src/seller-metadata-cache.js";
3
- import type { RegistrySeller } from "../src/seller-catalog.js";
4
-
5
- function seller(id: string, url: string): RegistrySeller {
6
- return {
7
- id,
8
- url,
9
- supportedProtocols: ["chat_completions"],
10
- paymentMethods: ["clawtip"],
11
- models: ["gpt-5.4"]
12
- };
13
- }
14
-
15
- function startManifestServer(handler: (req: http.IncomingMessage, res: http.ServerResponse) => void): Promise<{
16
- url: string;
17
- close: () => Promise<void>;
18
- }> {
19
- const server = http.createServer(handler);
20
- return new Promise((resolve) => {
21
- server.listen(0, "127.0.0.1", () => {
22
- const address = server.address();
23
- if (!address || typeof address === "string") {
24
- throw new Error("server did not bind to a TCP port");
25
- }
26
- resolve({
27
- url: `http://127.0.0.1:${address.port}`,
28
- close: () => new Promise<void>((done) => server.close(() => done()))
29
- });
30
- });
31
- });
32
- }
33
-
34
- describe("seller metadata cache", () => {
35
- test("reads discount ratio and manifest version from seller manifest selection", async () => {
36
- let requestCount = 0;
37
- const server = await startManifestServer((req, res) => {
38
- requestCount += 1;
39
- expect(req.url).toBe("/manifest");
40
- res.setHeader("Content-Type", "application/json");
41
- res.end(JSON.stringify({
42
- manifestVersion: "manifest.test",
43
- selection: {
44
- discountRatio: 0.42
45
- }
46
- }));
47
- });
48
-
49
- try {
50
- const cache = new SellerMetadataCache({ ttlMs: 60_000, now: () => 1000 });
51
- await cache.refreshIfStale([seller("s1", server.url)]);
52
- await cache.refreshIfStale([seller("s1", server.url)]);
53
-
54
- expect(requestCount).toBe(1);
55
- expect(cache.snapshot()).toEqual([
56
- {
57
- sellerId: "s1",
58
- discountRatio: 0.42,
59
- manifestVersion: "manifest.test",
60
- lastRefreshAt: 1000,
61
- source: "manifest_selection"
62
- }
63
- ]);
64
- } finally {
65
- await server.close();
66
- }
67
- });
68
-
69
- test("stores manifest fetch failures without throwing", async () => {
70
- const server = await startManifestServer((req, res) => {
71
- res.statusCode = 500;
72
- res.end("nope");
73
- });
74
-
75
- try {
76
- const cache = new SellerMetadataCache({ ttlMs: 60_000, now: () => 2000 });
77
- await expect(cache.refreshIfStale([seller("s1", server.url)])).resolves.toBeUndefined();
78
-
79
- expect(cache.snapshot()[0]).toMatchObject({
80
- sellerId: "s1",
81
- lastRefreshAt: 2000,
82
- source: "manifest_selection",
83
- errorMessage: "manifest returned 500"
84
- });
85
- } finally {
86
- await server.close();
87
- }
88
- });
89
- });