@tokenbuddy/tokenbuddy 1.0.35 → 1.0.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,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
- });