@tokenbuddy/tokenbuddy 1.0.33 → 1.0.35

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.
@@ -13,7 +13,7 @@
13
13
  <link rel="icon" type="image/png" sizes="192x192" href="/icons/tokenbuddy-192.png" />
14
14
  <link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
15
15
  <title>TokenBuddy · Local Control</title>
16
- <script type="module" crossorigin src="/assets/index-Mt3BZFuP.js"></script>
16
+ <script type="module" crossorigin src="/assets/index-BVbeDEwq.js"></script>
17
17
  <link rel="stylesheet" crossorigin href="/assets/index-0MVXD7bH.css">
18
18
  </head>
19
19
  <body>
@@ -55,6 +55,7 @@ describe("TokenbuddyDaemon classifyFailureStatus", () => {
55
55
  expect((daemon as any).endpointProtocol("/v1/responses")).toBe("responses");
56
56
  expect((daemon as any).endpointProtocol("/v1/messages")).toBe("messages");
57
57
  expect((daemon as any).endpointProtocol("/messages")).toBe("messages");
58
+ expect((daemon as any).endpointProtocol("/v1/images/generations")).toBe("images_generations");
58
59
  expect((daemon as any).endpointProtocol("/v1/unknown")).toBeUndefined();
59
60
  });
60
61
 
@@ -63,6 +64,7 @@ describe("TokenbuddyDaemon classifyFailureStatus", () => {
63
64
  expect((daemon as any).extractModelId("/v1/chat/completions", { model: " " })).toBeUndefined();
64
65
  expect((daemon as any).extractModelId("/v1/responses", { model_id: " resp-model " })).toBe("resp-model");
65
66
  expect((daemon as any).extractModelId("/v1/responses", { model_id: 42 })).toBeUndefined();
67
+ expect((daemon as any).extractModelId("/v1/images/generations", { model: " gpt-image-2 " })).toBe("gpt-image-2");
66
68
  expect((daemon as any).extractModelId("/v1/responses", null)).toBeUndefined();
67
69
  });
68
70
 
@@ -116,6 +118,12 @@ describe("TokenbuddyDaemon classifyFailureStatus", () => {
116
118
  cacheReadTokens: 4,
117
119
  billedMicros: 48,
118
120
  });
121
+ expect((daemon as any).readUsage(JSON.stringify({ usage: { input_tokens: 25, output_tokens: 75, total_tokens: 100 } }))).toEqual({
122
+ promptTokens: 25,
123
+ completionTokens: 75,
124
+ cacheReadTokens: 0,
125
+ billedMicros: 400,
126
+ });
119
127
  });
120
128
 
121
129
  test("parses seller settlement headers", () => {
@@ -0,0 +1,230 @@
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
+ }