@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
package/tests/e2e.test.ts DELETED
@@ -1,366 +0,0 @@
1
- import { buildApp as buildBootstrapApp } from "../../wallet-bootstrap/src/server.js";
2
- import { buildSellerApp } from "../../seller/src/server.js";
3
- import { TokenbuddyDaemon } from "../src/daemon.js";
4
- import { BuyerStore } from "../src/buyer-store.js";
5
- import { AdminClient } from "../../admin-cli/src/client.js";
6
- import { PROVIDER_MODE_CONFIG_KEY } from "../src/provider-routing-config.js";
7
- import * as path from "path";
8
- import * as fs from "fs";
9
- import http from "http";
10
- import { AddressInfo } from "net";
11
-
12
- const TEMP_DIR = path.resolve(__dirname, "../../data-test");
13
- const TEMP_BOOTSTRAP_JSON = path.join(TEMP_DIR, "sellers-e2e.json");
14
- const TEMP_SELLER_DB = path.join(TEMP_DIR, "seller-e2e.db");
15
- const TEMP_BUYER_DB = path.join(TEMP_DIR, "buyer-e2e.db");
16
-
17
- describe("TokenBuddy Full End-to-End Integration Flow Tests", () => {
18
- let bootstrapServer: http.Server;
19
- let sellerServer: http.Server;
20
- let upstreamServer: http.Server;
21
- let daemon: TokenbuddyDaemon;
22
-
23
- let bootstrapPort: number;
24
- let sellerPort: number;
25
- let upstreamPort: number;
26
- let daemonControlPort: number;
27
- let daemonProxyPort: number;
28
-
29
- let sellerCloseFn: () => void;
30
-
31
- beforeAll(async () => {
32
- // Ensure clean temp dir
33
- if (!fs.existsSync(TEMP_DIR)) {
34
- fs.mkdirSync(TEMP_DIR, { recursive: true });
35
- }
36
- fs.writeFileSync(TEMP_BOOTSTRAP_JSON, JSON.stringify({
37
- version: 1,
38
- sellers: [
39
- {
40
- id: "seller-e2e-seed",
41
- name: "Seller E2E Seed",
42
- url: "http://127.0.0.1:1",
43
- supportedProtocols: ["chat_completions"],
44
- paymentMethods: ["mock"],
45
- models: ["gpt-4"]
46
- }
47
- ]
48
- }), "utf8");
49
-
50
- if (fs.existsSync(TEMP_SELLER_DB)) {
51
- try { fs.unlinkSync(TEMP_SELLER_DB); } catch (e) {}
52
- }
53
- if (fs.existsSync(TEMP_BUYER_DB)) {
54
- try { fs.unlinkSync(TEMP_BUYER_DB); } catch (e) {}
55
- }
56
- const buyerStore = new BuyerStore({ dbPath: TEMP_BUYER_DB });
57
- buyerStore.savePayment({
58
- method: "mock",
59
- enabled: true,
60
- isDefault: true,
61
- config: { channel: "e2e-test" }
62
- });
63
- buyerStore.saveDaemonRuntimeConfig(PROVIDER_MODE_CONFIG_KEY, {
64
- mode: "auto",
65
- updatedAt: new Date().toISOString()
66
- });
67
- buyerStore.close();
68
-
69
- // 1. Launch Mock Upstream OpenAI completions server
70
- upstreamServer = http.createServer((req, res) => {
71
- res.writeHead(200, { "Content-Type": "application/json" });
72
- if (req.url === "/v1/chat/completions") {
73
- res.end(JSON.stringify({
74
- id: "chatcmpl-e2e-ok",
75
- usage: { prompt_tokens: 20, completion_tokens: 30 },
76
- choices: [{ message: { role: "assistant", content: "Hello back!" } }]
77
- }));
78
- return;
79
- }
80
- res.end("{}");
81
- });
82
- await new Promise<void>((resolve) => upstreamServer.listen(0, "127.0.0.1", () => resolve()));
83
- upstreamPort = (upstreamServer.address() as AddressInfo).port;
84
-
85
- // 2. Launch Wallet Bootstrap server
86
- const bootstrapApp = buildBootstrapApp({
87
- payTo: "test-activation-pay-to",
88
- sm4KeyBase64: "MDEyMzQ1Njc4OUFCQ0RFRg==",
89
- skillSlug: "test-activation-slug",
90
- skillId: "test-activation-id",
91
- description: "Activate registration",
92
- resourceUrl: "http://127.0.0.1/verify",
93
- activationFeeFen: 1,
94
- microsPerFen: 1000000,
95
- sellerRegistryPath: TEMP_BOOTSTRAP_JSON,
96
- operatorSecret: "op-secret",
97
- allowLocalSellerUrls: true
98
- });
99
- bootstrapServer = http.createServer(bootstrapApp);
100
- await new Promise<void>((resolve) => bootstrapServer.listen(0, "127.0.0.1", () => resolve()));
101
- bootstrapPort = (bootstrapServer.address() as AddressInfo).port;
102
-
103
- // 3. Launch Seller server
104
- const { app: sellerApp, close: closeDb } = buildSellerApp(TEMP_SELLER_DB, {
105
- allowMock: true,
106
- publicMockPayments: true,
107
- upstreamUrl: `http://127.0.0.1:${upstreamPort}`,
108
- upstreamApiKey: "upstream-mock-key",
109
- operatorSecret: "op-secret"
110
- });
111
- sellerCloseFn = closeDb;
112
- sellerServer = http.createServer(sellerApp);
113
- await new Promise<void>((resolve) => sellerServer.listen(0, "127.0.0.1", () => resolve()));
114
- sellerPort = (sellerServer.address() as AddressInfo).port;
115
-
116
- // 4. Launch TokenBuddy Buyer Proxy daemon
117
- // Configure process.env to redirect to this mock seller port in the test environment
118
- process.env.TOKENBUDDY_TEST_SELLER_URL = `http://127.0.0.1:${sellerPort}`;
119
-
120
- daemon = new TokenbuddyDaemon({
121
- controlPort: 0, // dynamic port for control plane
122
- proxyPort: 0, // dynamic port for proxy plane
123
- dbPath: TEMP_BUYER_DB,
124
- sellerRegistryUrl: `http://127.0.0.1:${bootstrapPort}/registry/sellers`
125
- });
126
-
127
- daemon.start();
128
-
129
- // Retrieve active control plane & proxy ports
130
- // Since we pass 0, the active listeners inside daemon have been dynamically assigned
131
- // We fetch ports by inspecting process listener fields, or directly reading properties if exposed.
132
- // In packages/tokenbuddy-cli/src/daemon.ts:
133
- // this.controlServer = controlApp.listen(this.config.controlPort);
134
- // this.proxyServer = proxyApp.listen(this.config.proxyPort);
135
- const daemonCtrl = (daemon as any).controlServer.address() as AddressInfo;
136
- const daemonProxy = (daemon as any).proxyServer.address() as AddressInfo;
137
- daemonControlPort = daemonCtrl.port;
138
- daemonProxyPort = daemonProxy.port;
139
- });
140
-
141
- afterAll(async () => {
142
- // Cleanup servers
143
- daemon.stop();
144
- await new Promise<void>((resolve) => sellerServer.close(() => resolve()));
145
- sellerCloseFn();
146
- await new Promise<void>((resolve) => bootstrapServer.close(() => resolve()));
147
- await new Promise<void>((resolve) => upstreamServer.close(() => resolve()));
148
-
149
- // Delete temporary databases
150
- if (fs.existsSync(TEMP_BOOTSTRAP_JSON)) {
151
- try { fs.unlinkSync(TEMP_BOOTSTRAP_JSON); } catch (e) {}
152
- }
153
- if (fs.existsSync(TEMP_SELLER_DB)) {
154
- try { fs.unlinkSync(TEMP_SELLER_DB); } catch (e) {}
155
- }
156
- if (fs.existsSync(TEMP_BUYER_DB)) {
157
- try { fs.unlinkSync(TEMP_BUYER_DB); } catch (e) {}
158
- }
159
- });
160
-
161
- test("💡 Verify Full E2E Loop: operator configure -> bootstrap list -> buyer daemon init -> concurrent proxy inference -> precise ledger check", async () => {
162
- // Step 1: Initialize Admin Client & Verify operator connection
163
- const admin = new AdminClient(`http://127.0.0.1:${sellerPort}`, "op-secret");
164
-
165
- const status = await admin.get("/operator/status");
166
- expect(status.status).toBe("healthy");
167
-
168
- // Step 2: Operator configures dynamically payment methods
169
- const clawtipConfig = {
170
- payTo: "test-merchant-acct",
171
- sm4KeyBase64: "MDEyMzQ1Njc4OUFCQ0RFRg==",
172
- skillSlug: "test-slug",
173
- skillId: "test-id",
174
- description: "Token purchase",
175
- resourceUrl: "http://127.0.0.1/verify"
176
- };
177
- const paymentSetRes = await admin.put("/operator/admin/payments/clawtip", clawtipConfig);
178
- expect(paymentSetRes.status).toBe("success");
179
-
180
- // Verify payments listed
181
- const serviceInfo = await admin.get("/operator/admin/service");
182
- expect(serviceInfo.clawtipConfigured).toBe(true);
183
-
184
- // Step 3: Register this seller in the wallet bootstrap registry
185
- const bootstrapAdmin = new AdminClient(`http://127.0.0.1:${bootstrapPort}`, "op-secret");
186
- const testRegistry = {
187
- version: 1,
188
- sellers: [
189
- {
190
- id: "seller-e2e-node",
191
- name: "Seller E2E Node",
192
- url: `http://127.0.0.1:${sellerPort}`,
193
- supportedProtocols: ["chat_completions"],
194
- paymentMethods: ["mock"],
195
- models: ["gpt-4", "gpt-4o"]
196
- }
197
- ]
198
- };
199
- const registered = await bootstrapAdmin.put("/operator/registry/sellers", testRegistry);
200
- expect(registered.sellers.length).toBe(1);
201
-
202
- // Verify registry list publicly available
203
- const sellersPublicRes = await fetch(`http://127.0.0.1:${bootstrapPort}/registry/sellers`);
204
- const sellersPublic = await sellersPublicRes.json() as any;
205
- expect(sellersPublic.sellers[0].id).toBe("seller-e2e-node");
206
-
207
- // Step 4: Validate daemon connection and check doctor
208
- const daemonStatusRes = await fetch(`http://127.0.0.1:${daemonControlPort}/status`);
209
- const daemonStatus = await daemonStatusRes.json() as any;
210
- expect(daemonStatus.status).toBe("running");
211
-
212
- // Step 5: Send concurrent inference chat requests to the buyer proxy plane (Port 17821 proxy equivalent)
213
- const chatBody = {
214
- model: "gpt-4",
215
- messages: [{ role: "user", content: "How is the weather?" }],
216
- requestId: "e2e_concurrency_req_1"
217
- };
218
-
219
- const sendProxyRequest = (reqId: string) => fetch(`http://127.0.0.1:${daemonProxyPort}/v1/chat/completions`, {
220
- method: "POST",
221
- headers: { "Content-Type": "application/json" },
222
- body: JSON.stringify({ ...chatBody, requestId: reqId })
223
- });
224
-
225
- // Fire 5 concurrent requests representing intense parallel LLM execution by coding terminal (like Claude Code)
226
- const parallelResponses = await Promise.all([
227
- sendProxyRequest("req_p_1"),
228
- sendProxyRequest("req_p_2"),
229
- sendProxyRequest("req_p_3"),
230
- sendProxyRequest("req_p_4"),
231
- sendProxyRequest("req_p_5")
232
- ]);
233
-
234
- for (const res of parallelResponses) {
235
- expect(res.ok).toBe(true);
236
- const data = await res.json() as any;
237
- expect(data.id).toBe("chatcmpl-e2e-ok");
238
- expect(data.choices[0].message.content).toBe("Hello back!");
239
- }
240
-
241
- // Step 6: Verify precise seller billing ledger
242
- // A single token should have been purchased of amount 2,000,000 micros.
243
- // 5 concurrent requests each used: prompt 20 * 1.0 + completion 30 * 3.0 = 110 micros.
244
- // Total cost: 5 * 110 = 550 micros.
245
- // Outstanding remaining balance should be: 2,000,000 - 550 = 1,999,450 micros!
246
- // Let's connect directly to the seller DB using manual sync query or operator billing list
247
- const { DatabaseSync } = require("node:sqlite");
248
- const verifyDb = new DatabaseSync(TEMP_SELLER_DB);
249
- const credRow = verifyDb.prepare("SELECT credit_balance_micros, reserved_micros, spent_micros FROM credentials").get() as any;
250
-
251
- expect(credRow.credit_balance_micros).toBe(1999450);
252
- expect(credRow.reserved_micros).toBe(0);
253
- expect(credRow.spent_micros).toBe(550);
254
-
255
- // Also assert 5 distinct request settlement entries exist in ledger
256
- const requestRows = verifyDb.prepare("SELECT state, settled_micros FROM requests").all() as any[];
257
- expect(requestRows.length).toBe(5);
258
- for (const r of requestRows) {
259
- expect(r.state).toBe("settled");
260
- expect(r.settled_micros).toBe(110);
261
- }
262
-
263
- verifyDb.close();
264
-
265
- const buyerAfterConcurrent = new BuyerStore({ dbPath: TEMP_BUYER_DB });
266
- try {
267
- const tokenCache = buyerAfterConcurrent.getToken("seller-e2e-node");
268
- expect(tokenCache).toMatchObject({
269
- balanceMicros: 1999450,
270
- reservedMicros: 0,
271
- spentMicros: 550,
272
- balanceSource: "seller_settlement_summary"
273
- });
274
- const inferenceLedger = buyerAfterConcurrent.listInferenceLedger();
275
- expect(inferenceLedger.filter((entry) => entry.requestId.startsWith("req_p_"))).toHaveLength(5);
276
- expect(inferenceLedger).toEqual(expect.arrayContaining([
277
- expect.objectContaining({
278
- requestId: "req_p_1",
279
- billedMicros: 110,
280
- estimatedMicros: 200,
281
- settledMicros: 110,
282
- settledUsdMicros: 110,
283
- priceVersion: "openrouter_usd.v1",
284
- balanceSource: "seller_authoritative"
285
- })
286
- ]));
287
- } finally {
288
- buyerAfterConcurrent.close();
289
- }
290
-
291
- // Step 7: Fire subsequent inference request and assert it HITS local daemon token cache immediately (Zero-Purchase)
292
- // We capture time elapsed to verify cache speed
293
- const start = Date.now();
294
- const hitResponse = await sendProxyRequest("req_subsequent_hit");
295
- const duration = Date.now() - start;
296
-
297
- expect(hitResponse.ok).toBe(true);
298
- const hitData = await hitResponse.json() as any;
299
- expect(hitData.id).toBe("chatcmpl-e2e-ok");
300
-
301
- // Total purchases should remain intact, meaning no extra purchases occurred!
302
- const verifyDb2 = new DatabaseSync(TEMP_SELLER_DB);
303
- const purchaseCount = (verifyDb2.prepare("SELECT COUNT(*) as count FROM purchases").get() as any).count;
304
- // 1 original purchase coalesce from PromiseLocks
305
- expect(purchaseCount).toBe(1);
306
- verifyDb2.close();
307
-
308
- // Step 8: Force seller-authoritative insufficiency while buyer cache still looks funded.
309
- // The daemon must refresh /v1/balance, auto-purchase once, and retry the same request idempotently.
310
- const forcedDb = new DatabaseSync(TEMP_SELLER_DB);
311
- const oldCredential = forcedDb.prepare("SELECT credential_id FROM credentials ORDER BY created_at ASC LIMIT 1").get() as any;
312
- forcedDb.prepare("UPDATE credentials SET credit_balance_micros = 1, reserved_micros = 0 WHERE credential_id = ?").run(oldCredential.credential_id);
313
- forcedDb.prepare("UPDATE tokens SET credit_balance_micros = 1, reserved_micros = 0 WHERE credential_id = ?").run(oldCredential.credential_id);
314
- forcedDb.close();
315
- const buyerBefore402 = new BuyerStore({ dbPath: TEMP_BUYER_DB });
316
- try {
317
- const cached = buyerBefore402.getToken("seller-e2e-node");
318
- expect(cached?.balanceMicros).toBeGreaterThan(200000);
319
- } finally {
320
- buyerBefore402.close();
321
- }
322
-
323
- const retryResponse = await sendProxyRequest("req_402_rebuy_retry");
324
- expect(retryResponse.ok).toBe(true);
325
- const retryData = await retryResponse.json() as any;
326
- expect(retryData.id).toBe("chatcmpl-e2e-ok");
327
-
328
- const verifyDb3 = new DatabaseSync(TEMP_SELLER_DB);
329
- const purchaseCountAfterRetry = (verifyDb3.prepare("SELECT COUNT(*) as count FROM purchases").get() as any).count;
330
- const retryRows = verifyDb3.prepare("SELECT state, settled_micros FROM requests WHERE request_id = ?").all("req_402_rebuy_retry") as any[];
331
- expect(purchaseCountAfterRetry).toBe(2);
332
- expect(retryRows).toEqual([
333
- expect.objectContaining({ state: "settled", settled_micros: 110 })
334
- ]);
335
- verifyDb3.close();
336
-
337
- const buyerAfterRetry = new BuyerStore({ dbPath: TEMP_BUYER_DB });
338
- try {
339
- expect(buyerAfterRetry.listInferenceLedger()).toEqual(expect.arrayContaining([
340
- expect.objectContaining({
341
- requestId: "req_402_rebuy_retry",
342
- billedMicros: 110,
343
- estimatedMicros: 200,
344
- settledMicros: 110,
345
- balanceSource: "seller_authoritative"
346
- })
347
- ]));
348
- expect(buyerAfterRetry.getToken("seller-e2e-node")).toMatchObject({
349
- balanceMicros: 1999890,
350
- balanceSource: "seller_settlement_summary"
351
- });
352
- } finally {
353
- buyerAfterRetry.close();
354
- }
355
-
356
- // Step 9: Verify newly implemented admin query billing APIs (purchases & requests)
357
- const billingPurchases = await admin.get("/operator/admin/purchases");
358
- expect(billingPurchases.purchases.length).toBe(2);
359
- expect(billingPurchases.purchases.every((purchase: any) => purchase.state === "funded")).toBe(true);
360
-
361
- const billingRequests = await admin.get("/operator/admin/requests");
362
- // 5 concurrent requests + 1 cache hit + 1 retry-after-402 request = 7 settled inference requests.
363
- expect(billingRequests.requests.length).toBe(7);
364
- expect(billingRequests.requests.every((r: any) => r.state === "settled")).toBe(true);
365
- });
366
- });
@@ -1,230 +0,0 @@
1
- import { buildApp as buildBootstrapApp } from "../../wallet-bootstrap/src/server.js";
2
- import { buildSellerApp } from "../../seller/src/server.js";
3
- import { TokenbuddyDaemon } from "../src/daemon.js";
4
- import { BuyerStore } from "../src/buyer-store.js";
5
- import { PROVIDER_MODE_CONFIG_KEY } from "../src/provider-routing-config.js";
6
- import * as fs from "fs";
7
- import * as http from "http";
8
- import * as os from "os";
9
- import * as path from "path";
10
- import type { AddressInfo } from "net";
11
-
12
- describe("TokenBuddy image generation full chain", () => {
13
- let tempDir: string;
14
- let upstreamServer: http.Server;
15
- let sellerServer: http.Server;
16
- let bootstrapServer: http.Server;
17
- let daemon: TokenbuddyDaemon;
18
- let sellerClose: () => void;
19
- let buyerDbPath: string;
20
- let sellerDbPath: string;
21
- let registryPath: string;
22
-
23
- afterEach(async () => {
24
- daemon?.stop();
25
- await closeServer(sellerServer);
26
- sellerClose?.();
27
- await closeServer(bootstrapServer);
28
- await closeServer(upstreamServer);
29
- if (tempDir) {
30
- fs.rmSync(tempDir, { recursive: true, force: true });
31
- }
32
- });
33
-
34
- test("proxies /v1/images/generations through seller and records authoritative billing", async () => {
35
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tb-image-e2e-"));
36
- buyerDbPath = path.join(tempDir, "buyer.db");
37
- sellerDbPath = path.join(tempDir, "seller.db");
38
- registryPath = path.join(tempDir, "registry.json");
39
-
40
- upstreamServer = http.createServer((req, res) => {
41
- if (req.url !== "/v1/images/generations" || req.method !== "POST") {
42
- res.writeHead(404, { "Content-Type": "application/json" });
43
- res.end(JSON.stringify({ error: "unexpected endpoint" }));
44
- return;
45
- }
46
- let body = "";
47
- req.setEncoding("utf8");
48
- req.on("data", (chunk) => {
49
- body += chunk;
50
- });
51
- req.on("end", () => {
52
- const parsed = JSON.parse(body) as {
53
- model?: string;
54
- prompt?: string;
55
- requestId?: string;
56
- request_id?: string;
57
- size?: string;
58
- quality?: string;
59
- output_format?: string;
60
- };
61
- expect(parsed).toMatchObject({
62
- model: "gpt-image-2",
63
- prompt: "draw a tiny smoke test",
64
- size: "1024x1024",
65
- quality: "high",
66
- output_format: "png"
67
- });
68
- expect(parsed.requestId).toBeUndefined();
69
- expect(parsed.request_id).toBeUndefined();
70
- res.writeHead(200, { "Content-Type": "application/json" });
71
- res.end(JSON.stringify({
72
- created: 123,
73
- data: [{ b64_json: "aW1hZ2U=" }],
74
- usage: {
75
- input_tokens: 25,
76
- output_tokens: 75,
77
- total_tokens: 100
78
- }
79
- }));
80
- });
81
- });
82
- await listen(upstreamServer);
83
- const upstreamPort = portOf(upstreamServer);
84
-
85
- const sellerHandle = buildSellerApp(sellerDbPath, {
86
- allowMock: true,
87
- publicMockPayments: true,
88
- upstreamUrl: `http://127.0.0.1:${upstreamPort}`,
89
- upstreamApiKey: "upstream-test-key",
90
- upstreamCapabilities: {
91
- chatCompletions: "unsupported",
92
- responses: "unsupported",
93
- messages: "unsupported",
94
- imagesGenerations: "supported"
95
- },
96
- models: [
97
- {
98
- id: "gpt-image-2",
99
- inputPriceMicrosPer1m: 1000000,
100
- outputPriceMicrosPer1m: 3000000,
101
- streaming: false
102
- }
103
- ]
104
- });
105
- sellerClose = sellerHandle.close;
106
- sellerServer = http.createServer(sellerHandle.app);
107
- await listen(sellerServer);
108
- const sellerPort = portOf(sellerServer);
109
-
110
- fs.writeFileSync(registryPath, JSON.stringify({
111
- version: 1,
112
- defaultSeller: "seller-image-e2e",
113
- sellers: [
114
- {
115
- id: "seller-image-e2e",
116
- name: "Seller Image E2E",
117
- url: `http://127.0.0.1:${sellerPort}`,
118
- supportedProtocols: ["images_generations"],
119
- paymentMethods: ["mock"],
120
- models: ["gpt-image-2"]
121
- }
122
- ]
123
- }), "utf8");
124
-
125
- const bootstrapApp = buildBootstrapApp({
126
- payTo: "test-activation-pay-to",
127
- sm4KeyBase64: "MDEyMzQ1Njc4OUFCQ0RFRg==",
128
- skillSlug: "test-activation-slug",
129
- skillId: "test-activation-id",
130
- description: "Activate registration",
131
- resourceUrl: "http://127.0.0.1/verify",
132
- activationFeeFen: 1,
133
- microsPerFen: 1000000,
134
- sellerRegistryPath: registryPath,
135
- operatorSecret: "op-secret",
136
- allowLocalSellerUrls: true
137
- });
138
- bootstrapServer = http.createServer(bootstrapApp);
139
- await listen(bootstrapServer);
140
- const bootstrapPort = portOf(bootstrapServer);
141
-
142
- const buyerStore = new BuyerStore({ dbPath: buyerDbPath });
143
- buyerStore.savePayment({
144
- method: "mock",
145
- enabled: true,
146
- isDefault: true,
147
- config: { channel: "image-e2e-test" }
148
- });
149
- buyerStore.saveDaemonRuntimeConfig(PROVIDER_MODE_CONFIG_KEY, {
150
- mode: "auto",
151
- updatedAt: new Date().toISOString()
152
- });
153
- buyerStore.close();
154
-
155
- daemon = new TokenbuddyDaemon({
156
- controlPort: 0,
157
- proxyPort: 0,
158
- dbPath: buyerDbPath,
159
- sellerRegistryUrl: `http://127.0.0.1:${bootstrapPort}/registry/sellers`
160
- });
161
- daemon.start();
162
- const daemonProxyPort = portOf((daemon as any).proxyServer);
163
-
164
- const response = await fetch(`http://127.0.0.1:${daemonProxyPort}/v1/images/generations`, {
165
- method: "POST",
166
- headers: {
167
- "Content-Type": "application/json",
168
- "X-Request-Id": "image_full_chain"
169
- },
170
- body: JSON.stringify({
171
- model: "gpt-image-2",
172
- prompt: "draw a tiny smoke test",
173
- size: "1024x1024",
174
- quality: "high",
175
- output_format: "png"
176
- })
177
- });
178
- expect(response.ok).toBe(true);
179
- const body = await response.json() as any;
180
- expect(body.data[0].b64_json).toBe("aW1hZ2U=");
181
-
182
- const after = new BuyerStore({ dbPath: buyerDbPath });
183
- try {
184
- expect(after.listInferenceLedger()).toEqual(expect.arrayContaining([
185
- expect.objectContaining({
186
- requestId: "image_full_chain",
187
- sellerKey: "seller-image-e2e",
188
- modelId: "gpt-image-2",
189
- endpoint: "/v1/images/generations",
190
- billedMicros: 250,
191
- estimatedMicros: 400,
192
- settledMicros: 250,
193
- settledUsdMicros: 250,
194
- billingUnit: "images",
195
- imageCount: 1,
196
- imageSize: "1024x1024",
197
- imageQuality: "high",
198
- imageOutputFormat: "png",
199
- imageOutputTokens: 75,
200
- imageOutputCostMicros: 225,
201
- imageCostMicrosPerImage: 250,
202
- balanceSource: "seller_authoritative"
203
- })
204
- ]));
205
- expect(after.getToken("seller-image-e2e")).toMatchObject({
206
- balanceMicros: 1999750,
207
- reservedMicros: 0,
208
- spentMicros: 250,
209
- balanceSource: "seller_settlement_summary"
210
- });
211
- } finally {
212
- after.close();
213
- }
214
- });
215
- });
216
-
217
- function listen(server: http.Server): Promise<void> {
218
- return new Promise((resolve) => server.listen(0, "127.0.0.1", () => resolve()));
219
- }
220
-
221
- function closeServer(server: http.Server | undefined): Promise<void> {
222
- if (!server?.listening) {
223
- return Promise.resolve();
224
- }
225
- return new Promise((resolve) => server.close(() => resolve()));
226
- }
227
-
228
- function portOf(server: http.Server): number {
229
- return (server.address() as AddressInfo).port;
230
- }