@tokenbuddy/tokenbuddy 1.0.5 → 1.0.7

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 (56) hide show
  1. package/dist/src/buyer-store.d.ts +48 -1
  2. package/dist/src/buyer-store.d.ts.map +1 -1
  3. package/dist/src/buyer-store.js +144 -17
  4. package/dist/src/buyer-store.js.map +1 -1
  5. package/dist/src/cli.d.ts +17 -0
  6. package/dist/src/cli.d.ts.map +1 -1
  7. package/dist/src/cli.js +560 -63
  8. package/dist/src/cli.js.map +1 -1
  9. package/dist/src/daemon.d.ts +11 -5
  10. package/dist/src/daemon.d.ts.map +1 -1
  11. package/dist/src/daemon.js +574 -161
  12. package/dist/src/daemon.js.map +1 -1
  13. package/dist/src/doctor-clawtip-wallet.d.ts +14 -0
  14. package/dist/src/doctor-clawtip-wallet.d.ts.map +1 -0
  15. package/dist/src/doctor-clawtip-wallet.js +54 -0
  16. package/dist/src/doctor-clawtip-wallet.js.map +1 -0
  17. package/dist/src/doctor-diagnostics.d.ts +99 -0
  18. package/dist/src/doctor-diagnostics.d.ts.map +1 -0
  19. package/dist/src/doctor-diagnostics.js +552 -0
  20. package/dist/src/doctor-diagnostics.js.map +1 -0
  21. package/dist/src/init-clawtip-activation.d.ts +48 -0
  22. package/dist/src/init-clawtip-activation.d.ts.map +1 -0
  23. package/dist/src/init-clawtip-activation.js +395 -0
  24. package/dist/src/init-clawtip-activation.js.map +1 -0
  25. package/dist/src/init-payment-options.d.ts +56 -0
  26. package/dist/src/init-payment-options.d.ts.map +1 -0
  27. package/dist/src/init-payment-options.js +165 -0
  28. package/dist/src/init-payment-options.js.map +1 -0
  29. package/dist/src/provider-install.d.ts +37 -2
  30. package/dist/src/provider-install.d.ts.map +1 -1
  31. package/dist/src/provider-install.js +317 -67
  32. package/dist/src/provider-install.js.map +1 -1
  33. package/dist/src/seller-catalog.d.ts +79 -0
  34. package/dist/src/seller-catalog.d.ts.map +1 -0
  35. package/dist/src/seller-catalog.js +126 -0
  36. package/dist/src/seller-catalog.js.map +1 -0
  37. package/dist/src/tb-proxyd.js +13 -2
  38. package/dist/src/tb-proxyd.js.map +1 -1
  39. package/dist/src/terminal-image.d.ts +22 -0
  40. package/dist/src/terminal-image.d.ts.map +1 -0
  41. package/dist/src/terminal-image.js +135 -0
  42. package/dist/src/terminal-image.js.map +1 -0
  43. package/package.json +1 -1
  44. package/src/buyer-store.ts +253 -18
  45. package/src/cli.ts +709 -68
  46. package/src/daemon.ts +651 -167
  47. package/src/doctor-clawtip-wallet.ts +70 -0
  48. package/src/doctor-diagnostics.ts +861 -0
  49. package/src/init-clawtip-activation.ts +487 -0
  50. package/src/init-payment-options.ts +249 -0
  51. package/src/provider-install.ts +426 -76
  52. package/src/seller-catalog.ts +222 -0
  53. package/src/tb-proxyd.ts +14 -2
  54. package/src/terminal-image.ts +187 -0
  55. package/tests/e2e.test.ts +88 -5
  56. package/tests/tokenbuddy.test.ts +1362 -27
@@ -0,0 +1,222 @@
1
+ import { createModuleLogger } from "@tokenbuddy/logging";
2
+
3
+ const logger = createModuleLogger("tb-proxyd");
4
+
5
+ export type ProtocolPreference = "chat_completions" | "responses" | "messages";
6
+
7
+ export interface RegistrySeller {
8
+ id: string;
9
+ name?: string;
10
+ url: string;
11
+ supportedProtocols?: string[];
12
+ paymentMethods?: string[];
13
+ }
14
+
15
+ export interface SellerRegistryDocument {
16
+ version: number;
17
+ defaultSeller?: string;
18
+ sellers: RegistrySeller[];
19
+ }
20
+
21
+ export interface SellerManifest {
22
+ sellerId?: string;
23
+ seller_id?: string;
24
+ supportedProtocols?: string[];
25
+ supported_protocols?: string[];
26
+ paymentMethods?: string[];
27
+ payment_methods?: string[];
28
+ models?: ManifestModelRecord[];
29
+ selection?: {
30
+ discountRatio?: number;
31
+ discount_ratio?: number;
32
+ };
33
+ }
34
+
35
+ export interface ManifestModelRecord {
36
+ id: string;
37
+ inputPriceMicrosPer1m?: number;
38
+ outputPriceMicrosPer1m?: number;
39
+ input_price_micros_per_1m?: number;
40
+ output_price_micros_per_1m?: number;
41
+ [key: string]: unknown;
42
+ }
43
+
44
+ export interface ModelCatalogEntry {
45
+ id: string;
46
+ sellerId: string;
47
+ sellerName?: string;
48
+ sellerUrl: string;
49
+ supportedProtocols: string[];
50
+ paymentMethods: string[];
51
+ inputPriceMicrosPer1m?: number;
52
+ outputPriceMicrosPer1m?: number;
53
+ }
54
+
55
+ export interface SellerCatalogEntry {
56
+ id: string;
57
+ name?: string;
58
+ url: string;
59
+ status: string;
60
+ manifestSellerId?: string;
61
+ discountRatio?: number;
62
+ modelCount?: number;
63
+ supportedProtocols?: string[];
64
+ paymentMethods?: string[];
65
+ errorMessage?: string;
66
+ }
67
+
68
+ export interface SellerCatalogResult {
69
+ registryUrl: string;
70
+ version: number;
71
+ defaultSeller?: string;
72
+ models: ModelCatalogEntry[];
73
+ sellers: SellerCatalogEntry[];
74
+ }
75
+
76
+ export type SellerRoutingMode = "auto" | "fixed";
77
+
78
+ export interface SellerRoutingPreference {
79
+ mode: SellerRoutingMode;
80
+ sellerId?: string;
81
+ }
82
+
83
+ export function normalizeSellerUrl(seller: RegistrySeller): string {
84
+ return seller.url.replace(/\/+$/, "");
85
+ }
86
+
87
+ export function manifestProtocols(manifest: SellerManifest, seller: RegistrySeller): string[] {
88
+ const protocols = manifest.supportedProtocols || manifest.supported_protocols || seller.supportedProtocols || [];
89
+ return protocols.includes("anthropic_messages") && !protocols.includes("messages")
90
+ ? [...protocols, "messages"]
91
+ : protocols;
92
+ }
93
+
94
+ export function manifestPaymentMethods(manifest: SellerManifest, seller: RegistrySeller): string[] {
95
+ return manifest.paymentMethods || manifest.payment_methods || seller.paymentMethods || [];
96
+ }
97
+
98
+ export function manifestModelIds(manifest: SellerManifest): string[] {
99
+ return (manifest.models || [])
100
+ .map((model) => model.id)
101
+ .filter((id): id is string => typeof id === "string" && id.trim().length > 0)
102
+ .map((id) => id.trim());
103
+ }
104
+
105
+ function numericPriceField(value: unknown): number | undefined {
106
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
107
+ }
108
+
109
+ function manifestModels(manifest: SellerManifest): ManifestModelRecord[] {
110
+ return (manifest.models || [])
111
+ .filter((model): model is ManifestModelRecord => Boolean(model?.id && typeof model.id === "string"));
112
+ }
113
+
114
+ export async function fetchSellerRegistry(registryUrl: string): Promise<SellerRegistryDocument> {
115
+ const response = await fetch(registryUrl);
116
+ if (!response.ok) {
117
+ throw new Error(`registry returned ${response.status}`);
118
+ }
119
+ const data = await response.json() as SellerRegistryDocument;
120
+ if (!data || !Array.isArray(data.sellers)) {
121
+ throw new Error("registry response missing sellers");
122
+ }
123
+ return data;
124
+ }
125
+
126
+ export async function fetchSellerManifest(seller: RegistrySeller): Promise<SellerManifest> {
127
+ const response = await fetch(`${normalizeSellerUrl(seller)}/manifest`);
128
+ if (!response.ok) {
129
+ throw new Error(`manifest returned ${response.status}`);
130
+ }
131
+ return await response.json() as SellerManifest;
132
+ }
133
+
134
+ export async function discoverSellerBackedModels(registryUrl: string): Promise<SellerCatalogResult> {
135
+ const registry = await fetchSellerRegistry(registryUrl);
136
+ const sellerResults = await Promise.all(registry.sellers.map(async (seller) => {
137
+ try {
138
+ const manifest = await fetchSellerManifest(seller);
139
+ const protocols = manifestProtocols(manifest, seller);
140
+ const paymentMethods = manifestPaymentMethods(manifest, seller);
141
+ const models = manifestModels(manifest).map((model) => ({
142
+ id: model.id.trim(),
143
+ sellerId: seller.id,
144
+ sellerName: seller.name,
145
+ sellerUrl: seller.url,
146
+ supportedProtocols: protocols,
147
+ paymentMethods,
148
+ inputPriceMicrosPer1m: numericPriceField(model.inputPriceMicrosPer1m) ?? numericPriceField(model.input_price_micros_per_1m),
149
+ outputPriceMicrosPer1m: numericPriceField(model.outputPriceMicrosPer1m) ?? numericPriceField(model.output_price_micros_per_1m),
150
+ }));
151
+ return {
152
+ seller: {
153
+ id: seller.id,
154
+ name: seller.name,
155
+ url: seller.url,
156
+ status: "ok",
157
+ manifestSellerId: manifest.sellerId || manifest.seller_id || seller.id,
158
+ discountRatio: manifest.selection?.discountRatio ?? manifest.selection?.discount_ratio,
159
+ modelCount: models.length,
160
+ supportedProtocols: protocols,
161
+ paymentMethods,
162
+ },
163
+ models
164
+ };
165
+ } catch (error: unknown) {
166
+ const errorMessage = error instanceof Error ? error.message : String(error);
167
+ logger.warn("models.refresh.seller_failed", "seller manifest refresh failed", {
168
+ sellerId: seller.id,
169
+ errorMessage
170
+ });
171
+ return {
172
+ seller: {
173
+ id: seller.id,
174
+ name: seller.name,
175
+ url: seller.url,
176
+ status: "failed",
177
+ errorMessage
178
+ },
179
+ models: [] as ModelCatalogEntry[]
180
+ };
181
+ }
182
+ }));
183
+
184
+ return {
185
+ registryUrl,
186
+ version: registry.version,
187
+ defaultSeller: registry.defaultSeller,
188
+ models: sellerResults.flatMap((entry) => entry.models),
189
+ sellers: sellerResults.map((entry) => entry.seller)
190
+ };
191
+ }
192
+
193
+ export function filterCatalogByProtocol(
194
+ models: ModelCatalogEntry[],
195
+ protocol: ProtocolPreference
196
+ ): ModelCatalogEntry[] {
197
+ return models.filter((entry) => entry.supportedProtocols.includes(protocol));
198
+ }
199
+
200
+ export function filterCatalogBySeller(
201
+ models: ModelCatalogEntry[],
202
+ sellerId: string | undefined
203
+ ): ModelCatalogEntry[] {
204
+ if (!sellerId) {
205
+ return models;
206
+ }
207
+ return models.filter((entry) => entry.sellerId === sellerId);
208
+ }
209
+
210
+ export function dedupeCatalogEntries(models: ModelCatalogEntry[]): ModelCatalogEntry[] {
211
+ const seen = new Set<string>();
212
+ const output: ModelCatalogEntry[] = [];
213
+ for (const entry of models) {
214
+ const key = `${entry.sellerId}:${entry.id}`;
215
+ if (seen.has(key)) {
216
+ continue;
217
+ }
218
+ seen.add(key);
219
+ output.push(entry);
220
+ }
221
+ return output;
222
+ }
package/src/tb-proxyd.ts CHANGED
@@ -9,13 +9,15 @@ const controlPort = parsePortEnv("TB_PROXYD_CONTROL_PORT", 17820);
9
9
  const proxyPort = parsePortEnv("TB_PROXYD_PROXY_PORT", 17821);
10
10
  const sellerRegistryUrl = process.env.TB_PROXYD_SELLER_REGISTRY_URL || "https://tb-wallet-bootstrap.fly.dev/registry/sellers";
11
11
  const selectionMode = parseSelectionModeEnv();
12
+ const selectedSellerId = parseSelectedSellerIdEnv();
12
13
 
13
14
  const daemon = new TokenbuddyDaemon({
14
15
  controlPort,
15
16
  proxyPort,
16
17
  dbPath,
17
18
  sellerRegistryUrl,
18
- selectionMode
19
+ selectionMode,
20
+ selectedSellerId
19
21
  });
20
22
 
21
23
  logger.info("proxy.process.initializing", "tb-proxyd process initializing", {
@@ -23,7 +25,8 @@ logger.info("proxy.process.initializing", "tb-proxyd process initializing", {
23
25
  controlPort,
24
26
  proxyPort,
25
27
  sellerRegistryUrl,
26
- selectionMode
28
+ selectionMode,
29
+ selectedSellerId
27
30
  });
28
31
  daemon.start();
29
32
 
@@ -58,3 +61,12 @@ function parseSelectionModeEnv(): "auto" | "manual" {
58
61
  }
59
62
  throw new Error("TB_PROXYD_SELECTION_MODE must be auto or manual");
60
63
  }
64
+
65
+ function parseSelectedSellerIdEnv(): string | undefined {
66
+ const rawValue = process.env.TB_PROXYD_SELECTED_SELLER_ID;
67
+ if (!rawValue) {
68
+ return undefined;
69
+ }
70
+ const trimmed = rawValue.trim();
71
+ return trimmed || undefined;
72
+ }
@@ -0,0 +1,187 @@
1
+ import { spawn } from "child_process";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+
5
+ type TerminalEnv = Readonly<Record<string, string | undefined>>;
6
+
7
+ export type TerminalImageDisplayMethod =
8
+ | "inline-iterm"
9
+ | "inline-kitty"
10
+ | "system-open"
11
+ | "manual";
12
+
13
+ export interface TerminalImageDisplayResult {
14
+ method: TerminalImageDisplayMethod;
15
+ displayed: boolean;
16
+ message: string;
17
+ fallbackCommand?: string;
18
+ }
19
+
20
+ export interface DisplayTerminalImageOptions {
21
+ env?: TerminalEnv;
22
+ platform?: NodeJS.Platform;
23
+ stdoutIsTTY?: boolean;
24
+ write?: (chunk: string) => void;
25
+ runCommand?: (command: string, args: string[]) => Promise<void>;
26
+ fileExists?: (filePath: string) => boolean;
27
+ readFile?: (filePath: string) => Buffer;
28
+ }
29
+
30
+ type InlineImageProtocol = "iterm" | "kitty";
31
+
32
+ function defaultRunCommand(command: string, args: string[]): Promise<void> {
33
+ return new Promise((resolve, reject) => {
34
+ const child = spawn(command, args, {
35
+ stdio: "ignore",
36
+ });
37
+ child.on("error", reject);
38
+ child.on("close", (code) => {
39
+ if (code === 0) {
40
+ resolve();
41
+ return;
42
+ }
43
+ reject(new Error(`${command} exited with ${code}`));
44
+ });
45
+ });
46
+ }
47
+
48
+ function terminalSupportsInlineImages(
49
+ env: TerminalEnv,
50
+ stdoutIsTTY: boolean,
51
+ ): InlineImageProtocol | undefined {
52
+ if (!stdoutIsTTY) {
53
+ return undefined;
54
+ }
55
+
56
+ const term = env.TERM || "";
57
+ if (env.KITTY_WINDOW_ID || term.includes("xterm-kitty")) {
58
+ return "kitty";
59
+ }
60
+
61
+ const termProgram = env.TERM_PROGRAM || "";
62
+ if (termProgram === "iTerm.app" || termProgram === "WezTerm" || env.WEZTERM_EXECUTABLE) {
63
+ return "iterm";
64
+ }
65
+
66
+ return undefined;
67
+ }
68
+
69
+ function basenameBase64(filePath: string): string {
70
+ return Buffer.from(path.basename(filePath), "utf8").toString("base64");
71
+ }
72
+
73
+ function itermInlineImageSequence(filePath: string, image: Buffer): string {
74
+ const payload = image.toString("base64");
75
+ return `\u001B]1337;File=name=${basenameBase64(filePath)};inline=1;width=60%;preserveAspectRatio=1:${payload}\u0007\n`;
76
+ }
77
+
78
+ function kittyInlineImageSequence(filePath: string): string {
79
+ const payload = Buffer.from(filePath, "utf8").toString("base64");
80
+ return `\u001B_Ga=T,t=f,f=100,c=60;${payload}\u001B\\\n`;
81
+ }
82
+
83
+ function openCommandForPlatform(platform: NodeJS.Platform, filePath: string): { command: string; args: string[]; label: string } | undefined {
84
+ if (platform === "darwin") {
85
+ return {
86
+ command: "open",
87
+ args: [filePath],
88
+ label: `open ${filePath}`,
89
+ };
90
+ }
91
+ if (platform === "linux") {
92
+ return {
93
+ command: "xdg-open",
94
+ args: [filePath],
95
+ label: `xdg-open ${filePath}`,
96
+ };
97
+ }
98
+ if (platform === "win32") {
99
+ return {
100
+ command: "cmd",
101
+ args: ["/c", "start", "", filePath],
102
+ label: `start ${filePath}`,
103
+ };
104
+ }
105
+ return undefined;
106
+ }
107
+
108
+ export function detectTerminalImageDisplay(
109
+ options: DisplayTerminalImageOptions = {},
110
+ ): InlineImageProtocol | undefined {
111
+ return terminalSupportsInlineImages(
112
+ options.env || process.env,
113
+ options.stdoutIsTTY ?? Boolean(process.stdout.isTTY),
114
+ );
115
+ }
116
+
117
+ export async function displayTerminalImage(
118
+ filePath: string,
119
+ options: DisplayTerminalImageOptions = {},
120
+ ): Promise<TerminalImageDisplayResult> {
121
+ const fileExists = options.fileExists || fs.existsSync;
122
+ const readFile = options.readFile || fs.readFileSync;
123
+ const write = options.write || ((chunk: string) => process.stdout.write(chunk));
124
+ const env = options.env || process.env;
125
+ const platform = options.platform || process.platform;
126
+ const stdoutIsTTY = options.stdoutIsTTY ?? Boolean(process.stdout.isTTY);
127
+ const openCommand = openCommandForPlatform(platform, filePath);
128
+ const fallbackCommand = openCommand?.label;
129
+
130
+ if (!fileExists(filePath)) {
131
+ return {
132
+ method: "manual",
133
+ displayed: false,
134
+ fallbackCommand,
135
+ message: `QR image file is missing: ${filePath}`,
136
+ };
137
+ }
138
+
139
+ const inlineProtocol = terminalSupportsInlineImages(env, stdoutIsTTY);
140
+ if (inlineProtocol === "kitty") {
141
+ write(kittyInlineImageSequence(filePath));
142
+ return {
143
+ method: "inline-kitty",
144
+ displayed: true,
145
+ fallbackCommand,
146
+ message: "Displayed the ClawTip wallet QR image inline using the Kitty graphics protocol.",
147
+ };
148
+ }
149
+
150
+ if (inlineProtocol === "iterm") {
151
+ write(itermInlineImageSequence(filePath, readFile(filePath)));
152
+ return {
153
+ method: "inline-iterm",
154
+ displayed: true,
155
+ fallbackCommand,
156
+ message: "Displayed the ClawTip wallet QR image inline using the iTerm2 image protocol.",
157
+ };
158
+ }
159
+
160
+ if (openCommand) {
161
+ try {
162
+ const runCommand = options.runCommand || defaultRunCommand;
163
+ await runCommand(openCommand.command, openCommand.args);
164
+ return {
165
+ method: "system-open",
166
+ displayed: true,
167
+ fallbackCommand,
168
+ message: `Opened the ClawTip wallet QR image with the system image viewer: ${openCommand.label}`,
169
+ };
170
+ } catch (error) {
171
+ const message = error instanceof Error ? error.message : String(error);
172
+ return {
173
+ method: "manual",
174
+ displayed: false,
175
+ fallbackCommand,
176
+ message: `Could not open QR image automatically: ${message}`,
177
+ };
178
+ }
179
+ }
180
+
181
+ return {
182
+ method: "manual",
183
+ displayed: false,
184
+ fallbackCommand,
185
+ message: "This terminal does not advertise inline image support and no system opener is available.",
186
+ };
187
+ }
package/tests/e2e.test.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { buildApp as buildBootstrapApp } from "../../wallet-bootstrap/src/server.js";
2
2
  import { buildSellerApp } from "../../seller/src/server.js";
3
3
  import { TokenbuddyDaemon } from "../src/daemon.js";
4
+ import { BuyerStore } from "../src/buyer-store.js";
4
5
  import { AdminClient } from "../../admin-cli/src/client.js";
5
6
  import * as path from "path";
6
7
  import * as fs from "fs";
@@ -39,6 +40,14 @@ describe("TokenBuddy Full End-to-End Integration Flow Tests", () => {
39
40
  if (fs.existsSync(TEMP_BUYER_DB)) {
40
41
  try { fs.unlinkSync(TEMP_BUYER_DB); } catch (e) {}
41
42
  }
43
+ const buyerStore = new BuyerStore({ dbPath: TEMP_BUYER_DB });
44
+ buyerStore.savePayment({
45
+ method: "mock",
46
+ enabled: true,
47
+ isDefault: true,
48
+ config: { channel: "e2e-test" }
49
+ });
50
+ buyerStore.close();
42
51
 
43
52
  // 1. Launch Mock Upstream OpenAI completions server
44
53
  upstreamServer = http.createServer((req, res) => {
@@ -234,6 +243,32 @@ describe("TokenBuddy Full End-to-End Integration Flow Tests", () => {
234
243
 
235
244
  verifyDb.close();
236
245
 
246
+ const buyerAfterConcurrent = new BuyerStore({ dbPath: TEMP_BUYER_DB });
247
+ try {
248
+ const tokenCache = buyerAfterConcurrent.getToken("seller-e2e-node");
249
+ expect(tokenCache).toMatchObject({
250
+ balanceMicros: 1999450,
251
+ reservedMicros: 0,
252
+ spentMicros: 550,
253
+ balanceSource: "seller_settlement_summary"
254
+ });
255
+ const inferenceLedger = buyerAfterConcurrent.listInferenceLedger();
256
+ expect(inferenceLedger.filter((entry) => entry.requestId.startsWith("req_p_"))).toHaveLength(5);
257
+ expect(inferenceLedger).toEqual(expect.arrayContaining([
258
+ expect.objectContaining({
259
+ requestId: "req_p_1",
260
+ billedMicros: 110,
261
+ estimatedMicros: 200,
262
+ settledMicros: 110,
263
+ settledUsdMicros: 110,
264
+ priceVersion: "openrouter_usd.v1",
265
+ balanceSource: "seller_authoritative"
266
+ })
267
+ ]));
268
+ } finally {
269
+ buyerAfterConcurrent.close();
270
+ }
271
+
237
272
  // Step 7: Fire subsequent inference request and assert it HITS local daemon token cache immediately (Zero-Purchase)
238
273
  // We capture time elapsed to verify cache speed
239
274
  const start = Date.now();
@@ -251,14 +286,62 @@ describe("TokenBuddy Full End-to-End Integration Flow Tests", () => {
251
286
  expect(purchaseCount).toBe(1);
252
287
  verifyDb2.close();
253
288
 
254
- // Step 8: Verify newly implemented admin query billing APIs (purchases & requests)
289
+ // Step 8: Force seller-authoritative insufficiency while buyer cache still looks funded.
290
+ // The daemon must refresh /v1/balance, auto-purchase once, and retry the same request idempotently.
291
+ const forcedDb = new DatabaseSync(TEMP_SELLER_DB);
292
+ const oldCredential = forcedDb.prepare("SELECT credential_id FROM credentials ORDER BY created_at ASC LIMIT 1").get() as any;
293
+ forcedDb.prepare("UPDATE credentials SET credit_balance_micros = 1, reserved_micros = 0 WHERE credential_id = ?").run(oldCredential.credential_id);
294
+ forcedDb.prepare("UPDATE tokens SET credit_balance_micros = 1, reserved_micros = 0 WHERE credential_id = ?").run(oldCredential.credential_id);
295
+ forcedDb.close();
296
+ const buyerBefore402 = new BuyerStore({ dbPath: TEMP_BUYER_DB });
297
+ try {
298
+ const cached = buyerBefore402.getToken("seller-e2e-node");
299
+ expect(cached?.balanceMicros).toBeGreaterThan(200000);
300
+ } finally {
301
+ buyerBefore402.close();
302
+ }
303
+
304
+ const retryResponse = await sendProxyRequest("req_402_rebuy_retry");
305
+ expect(retryResponse.ok).toBe(true);
306
+ const retryData = await retryResponse.json() as any;
307
+ expect(retryData.id).toBe("chatcmpl-e2e-ok");
308
+
309
+ const verifyDb3 = new DatabaseSync(TEMP_SELLER_DB);
310
+ const purchaseCountAfterRetry = (verifyDb3.prepare("SELECT COUNT(*) as count FROM purchases").get() as any).count;
311
+ const retryRows = verifyDb3.prepare("SELECT state, settled_micros FROM requests WHERE request_id = ?").all("req_402_rebuy_retry") as any[];
312
+ expect(purchaseCountAfterRetry).toBe(2);
313
+ expect(retryRows).toEqual([
314
+ expect.objectContaining({ state: "settled", settled_micros: 110 })
315
+ ]);
316
+ verifyDb3.close();
317
+
318
+ const buyerAfterRetry = new BuyerStore({ dbPath: TEMP_BUYER_DB });
319
+ try {
320
+ expect(buyerAfterRetry.listInferenceLedger()).toEqual(expect.arrayContaining([
321
+ expect.objectContaining({
322
+ requestId: "req_402_rebuy_retry",
323
+ billedMicros: 110,
324
+ estimatedMicros: 200,
325
+ settledMicros: 110,
326
+ balanceSource: "seller_authoritative"
327
+ })
328
+ ]));
329
+ expect(buyerAfterRetry.getToken("seller-e2e-node")).toMatchObject({
330
+ balanceMicros: 1999890,
331
+ balanceSource: "seller_settlement_summary"
332
+ });
333
+ } finally {
334
+ buyerAfterRetry.close();
335
+ }
336
+
337
+ // Step 9: Verify newly implemented admin query billing APIs (purchases & requests)
255
338
  const billingPurchases = await admin.get("/operator/admin/purchases");
256
- expect(billingPurchases.purchases.length).toBe(1);
257
- expect(billingPurchases.purchases[0].state).toBe("funded");
339
+ expect(billingPurchases.purchases.length).toBe(2);
340
+ expect(billingPurchases.purchases.every((purchase: any) => purchase.state === "funded")).toBe(true);
258
341
 
259
342
  const billingRequests = await admin.get("/operator/admin/requests");
260
- // 5 concurrent requests + 1 subsequent hit = 6 total inference requests tracked on seller database
261
- expect(billingRequests.requests.length).toBe(6);
343
+ // 5 concurrent requests + 1 cache hit + 1 retry-after-402 request = 7 settled inference requests.
344
+ expect(billingRequests.requests.length).toBe(7);
262
345
  expect(billingRequests.requests.every((r: any) => r.state === "settled")).toBe(true);
263
346
  });
264
347
  });