@tokenbuddy/tokenbuddy 1.0.4

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 (43) hide show
  1. package/bin/tb-proxyd.js +2 -0
  2. package/bin/tb.js +3 -0
  3. package/bin/tokenbuddy-proxyd.js +2 -0
  4. package/bin/tokenbuddy.js +3 -0
  5. package/dist/src/buyer-store.d.ts +118 -0
  6. package/dist/src/buyer-store.d.ts.map +1 -0
  7. package/dist/src/buyer-store.js +296 -0
  8. package/dist/src/buyer-store.js.map +1 -0
  9. package/dist/src/cli.d.ts +3 -0
  10. package/dist/src/cli.d.ts.map +1 -0
  11. package/dist/src/cli.js +648 -0
  12. package/dist/src/cli.js.map +1 -0
  13. package/dist/src/daemon.d.ts +48 -0
  14. package/dist/src/daemon.d.ts.map +1 -0
  15. package/dist/src/daemon.js +998 -0
  16. package/dist/src/daemon.js.map +1 -0
  17. package/dist/src/index.d.ts +2 -0
  18. package/dist/src/index.d.ts.map +1 -0
  19. package/dist/src/index.js +12 -0
  20. package/dist/src/index.js.map +1 -0
  21. package/dist/src/provider-install.d.ts +44 -0
  22. package/dist/src/provider-install.d.ts.map +1 -0
  23. package/dist/src/provider-install.js +286 -0
  24. package/dist/src/provider-install.js.map +1 -0
  25. package/dist/src/tb-proxyd.d.ts +2 -0
  26. package/dist/src/tb-proxyd.d.ts.map +1 -0
  27. package/dist/src/tb-proxyd.js +54 -0
  28. package/dist/src/tb-proxyd.js.map +1 -0
  29. package/dist/src/terminal-detect.d.ts +29 -0
  30. package/dist/src/terminal-detect.d.ts.map +1 -0
  31. package/dist/src/terminal-detect.js +209 -0
  32. package/dist/src/terminal-detect.js.map +1 -0
  33. package/package.json +29 -0
  34. package/src/buyer-store.ts +536 -0
  35. package/src/cli.ts +732 -0
  36. package/src/daemon.ts +1158 -0
  37. package/src/index.ts +12 -0
  38. package/src/provider-install.ts +363 -0
  39. package/src/tb-proxyd.ts +60 -0
  40. package/src/terminal-detect.ts +225 -0
  41. package/tests/e2e.test.ts +264 -0
  42. package/tests/tokenbuddy.test.ts +1186 -0
  43. package/tsconfig.json +8 -0
@@ -0,0 +1,998 @@
1
+ import express from "express";
2
+ import * as crypto from "crypto";
3
+ import { spawn } from "child_process";
4
+ import * as fs from "fs";
5
+ import { createModuleLogger } from "@tokenbuddy/logging";
6
+ import { BuyerStore } from "./buyer-store.js";
7
+ import { applyProviderInstall, detectProviders, previewProviderInstall, rollbackProviderInstall } from "./provider-install.js";
8
+ const logger = createModuleLogger("tb-proxyd");
9
+ const PROXY_JSON_BODY_LIMIT = "10mb";
10
+ export class TokenbuddyDaemon {
11
+ config;
12
+ tokenStore;
13
+ controlServer;
14
+ proxyServer;
15
+ selectionMode;
16
+ activePurchases = new Map();
17
+ constructor(config) {
18
+ this.config = config;
19
+ this.selectionMode = config.selectionMode || "auto";
20
+ this.tokenStore = new BuyerStore({ dbPath: config.dbPath });
21
+ }
22
+ activeControlPort() {
23
+ const address = this.controlServer?.address?.();
24
+ return typeof address === "object" && address ? address.port : this.config.controlPort;
25
+ }
26
+ activeProxyPort() {
27
+ const address = this.proxyServer?.address?.();
28
+ return typeof address === "object" && address ? address.port : this.config.proxyPort;
29
+ }
30
+ async fetchRegistry() {
31
+ const response = await fetch(this.config.sellerRegistryUrl);
32
+ if (!response.ok) {
33
+ throw new Error(`registry returned ${response.status}`);
34
+ }
35
+ const data = await response.json();
36
+ if (!data || !Array.isArray(data.sellers)) {
37
+ throw new Error("registry response missing sellers");
38
+ }
39
+ return data;
40
+ }
41
+ async fetchSellerManifest(seller) {
42
+ const baseUrl = seller.url.replace(/\/+$/, "");
43
+ const response = await fetch(`${baseUrl}/manifest`);
44
+ if (!response.ok) {
45
+ throw new Error(`manifest returned ${response.status}`);
46
+ }
47
+ return await response.json();
48
+ }
49
+ runtimeSummary() {
50
+ return {
51
+ status: "running",
52
+ pid: process.pid,
53
+ controlPort: this.activeControlPort(),
54
+ proxyPort: this.activeProxyPort(),
55
+ selectionMode: this.selectionMode,
56
+ dbPath: this.config.dbPath,
57
+ sellerRegistryUrl: this.config.sellerRegistryUrl,
58
+ store: this.tokenStore.summary()
59
+ };
60
+ }
61
+ normalizeSellerUrl(seller) {
62
+ return seller.url.replace(/\/+$/, "");
63
+ }
64
+ endpointProtocol(endpoint) {
65
+ if (endpoint === "/v1/chat/completions") {
66
+ return "chat_completions";
67
+ }
68
+ if (endpoint === "/v1/responses") {
69
+ return "responses";
70
+ }
71
+ if (endpoint === "/v1/messages" || endpoint === "/messages") {
72
+ return "messages";
73
+ }
74
+ return undefined;
75
+ }
76
+ extractModelId(endpoint, body) {
77
+ if (body && typeof body === "object" && "model" in body) {
78
+ const model = body.model;
79
+ return typeof model === "string" && model.trim() ? model.trim() : undefined;
80
+ }
81
+ if (endpoint === "/v1/responses" && body && typeof body === "object" && "model_id" in body) {
82
+ const model = body.model_id;
83
+ return typeof model === "string" && model.trim() ? model.trim() : undefined;
84
+ }
85
+ return undefined;
86
+ }
87
+ manifestProtocols(manifest, seller) {
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
+ manifestPaymentMethods(manifest, seller) {
94
+ return manifest.paymentMethods || manifest.payment_methods || seller.paymentMethods || [];
95
+ }
96
+ manifestModelIds(manifest) {
97
+ return (manifest.models || []).map((model) => model.id).filter((id) => typeof id === "string" && id.length > 0);
98
+ }
99
+ defaultPaymentMethod() {
100
+ const payments = this.tokenStore.listPayments().filter((payment) => payment.enabled);
101
+ return payments.find((payment) => payment.isDefault)?.method || payments.find((payment) => payment.method === "mock")?.method;
102
+ }
103
+ async selectSellerRoutes(endpoint, modelId) {
104
+ const protocol = this.endpointProtocol(endpoint);
105
+ if (!protocol) {
106
+ throw new Error(`unsupported proxy endpoint: ${endpoint}`);
107
+ }
108
+ const paymentMethod = this.defaultPaymentMethod();
109
+ if (!paymentMethod || !["mock", "clawtip"].includes(paymentMethod)) {
110
+ throw new Error("mock or clawtip payment method is not configured as an enabled buyer payment method");
111
+ }
112
+ const registry = await this.fetchRegistry();
113
+ const defaultSellers = registry.sellers.filter((seller) => seller.id === registry.defaultSeller);
114
+ const backupSellers = registry.sellers.filter((seller) => seller.id !== registry.defaultSeller);
115
+ const sellers = this.selectionMode === "manual" ? defaultSellers : [...defaultSellers, ...backupSellers];
116
+ const routes = [];
117
+ for (const seller of sellers) {
118
+ let manifest;
119
+ try {
120
+ manifest = await this.fetchSellerManifest(seller);
121
+ }
122
+ catch (error) {
123
+ logger.warn("route.manifest.failed", "seller manifest unavailable during route selection", {
124
+ sellerKey: seller.id,
125
+ model: modelId,
126
+ endpoint,
127
+ errorMessage: error instanceof Error ? error.message : String(error)
128
+ });
129
+ continue;
130
+ }
131
+ const protocols = this.manifestProtocols(manifest, seller);
132
+ const paymentMethods = this.manifestPaymentMethods(manifest, seller);
133
+ const modelIds = this.manifestModelIds(manifest);
134
+ if (!protocols.includes(protocol) || !paymentMethods.includes(paymentMethod) || !modelIds.includes(modelId)) {
135
+ continue;
136
+ }
137
+ routes.push({
138
+ seller,
139
+ manifest,
140
+ protocol,
141
+ modelId,
142
+ paymentMethod
143
+ });
144
+ }
145
+ if (routes.length === 0) {
146
+ throw new Error(`no compatible seller for ${endpoint} model ${modelId}`);
147
+ }
148
+ logger.info("route.candidates.prewarmed", "seller route candidates prewarmed", {
149
+ model: modelId,
150
+ endpoint,
151
+ protocol,
152
+ paymentMethod,
153
+ selectionMode: this.selectionMode,
154
+ sellerCount: routes.length,
155
+ sellers: routes.map((route) => route.seller.id)
156
+ });
157
+ return routes;
158
+ }
159
+ logRouteSelected(route, endpoint, routeIndex) {
160
+ logger.info("route.selected", "seller route selected", {
161
+ sellerKey: route.seller.id,
162
+ model: route.modelId,
163
+ endpoint,
164
+ protocol: route.protocol,
165
+ paymentMethod: route.paymentMethod,
166
+ routeIndex,
167
+ backup: routeIndex > 0
168
+ });
169
+ }
170
+ shouldFailoverStatus(status) {
171
+ return status === 429 || status >= 500;
172
+ }
173
+ logFailover(route, endpoint, routeIndex, reason, status) {
174
+ logger.warn("route.failover.triggered", "seller route failed over to backup candidate", {
175
+ sellerKey: route.seller.id,
176
+ model: route.modelId,
177
+ endpoint,
178
+ routeIndex,
179
+ reason,
180
+ status
181
+ });
182
+ }
183
+ failoverErrorMessage(error) {
184
+ return error instanceof Error ? error.message : String(error);
185
+ }
186
+ async selectSeller(endpoint, modelId) {
187
+ const routes = await this.selectSellerRoutes(endpoint, modelId);
188
+ const route = routes[0];
189
+ this.logRouteSelected(route, endpoint, 0);
190
+ return route;
191
+ }
192
+ async listSellerBackedModels() {
193
+ const registry = await this.fetchRegistry();
194
+ const sellerResults = await Promise.all(registry.sellers.map(async (seller) => {
195
+ try {
196
+ const manifest = await this.fetchSellerManifest(seller);
197
+ const protocols = this.manifestProtocols(manifest, seller);
198
+ const paymentMethods = this.manifestPaymentMethods(manifest, seller);
199
+ const models = (manifest.models || []).map((model) => ({
200
+ id: model.id,
201
+ sellerId: seller.id,
202
+ sellerName: seller.name,
203
+ sellerUrl: seller.url,
204
+ supportedProtocols: protocols,
205
+ paymentMethods
206
+ }));
207
+ return {
208
+ seller: {
209
+ id: seller.id,
210
+ name: seller.name,
211
+ url: seller.url,
212
+ status: "ok",
213
+ manifestSellerId: manifest.sellerId || manifest.seller_id || seller.id
214
+ },
215
+ models
216
+ };
217
+ }
218
+ catch (error) {
219
+ const errorMessage = error instanceof Error ? error.message : String(error);
220
+ logger.warn("models.refresh.seller_failed", "seller manifest refresh failed", {
221
+ sellerId: seller.id,
222
+ errorMessage
223
+ });
224
+ return {
225
+ seller: {
226
+ id: seller.id,
227
+ name: seller.name,
228
+ url: seller.url,
229
+ status: "failed",
230
+ errorMessage
231
+ },
232
+ models: []
233
+ };
234
+ }
235
+ }));
236
+ return {
237
+ models: sellerResults.flatMap((result) => result.models),
238
+ sellers: sellerResults.map((result) => result.seller)
239
+ };
240
+ }
241
+ readUsage(bodyText) {
242
+ const fallback = {
243
+ promptTokens: 0,
244
+ completionTokens: 0,
245
+ billedMicros: 0
246
+ };
247
+ if (!bodyText.trim()) {
248
+ return fallback;
249
+ }
250
+ try {
251
+ const data = JSON.parse(bodyText);
252
+ const promptTokens = data.usage?.prompt_tokens ?? data.usage?.input_tokens ?? 0;
253
+ const completionTokens = data.usage?.completion_tokens ?? data.usage?.output_tokens ?? 0;
254
+ return {
255
+ promptTokens,
256
+ completionTokens,
257
+ billedMicros: (promptTokens + completionTokens) * 4
258
+ };
259
+ }
260
+ catch {
261
+ return fallback;
262
+ }
263
+ }
264
+ inferPromptForHash(body) {
265
+ if (!body || typeof body !== "object") {
266
+ return undefined;
267
+ }
268
+ const compact = JSON.stringify(body);
269
+ return compact.length > 0 ? compact : undefined;
270
+ }
271
+ autoPurchaseAmountUsdMicros() {
272
+ const raw = process.env.TB_PROXYD_AUTO_PURCHASE_AMOUNT_USD_MICROS;
273
+ const parsed = raw ? Number(raw) : 2000000;
274
+ if (!Number.isInteger(parsed) || parsed < 1) {
275
+ return 2000000;
276
+ }
277
+ return parsed;
278
+ }
279
+ tokenRebuyMinBalanceMicros() {
280
+ const raw = process.env.TB_PROXYD_TOKEN_REBUY_MIN_BALANCE_MICROS;
281
+ const parsed = raw ? Number(raw) : 200000;
282
+ if (!Number.isInteger(parsed) || parsed < 0) {
283
+ return 200000;
284
+ }
285
+ return parsed;
286
+ }
287
+ async getOrPurchaseToken(route) {
288
+ const sellerKey = route.seller.id;
289
+ const sellerUrl = this.normalizeSellerUrl(route.seller);
290
+ const { modelId, paymentMethod } = route;
291
+ const cached = this.tokenStore.getToken(sellerKey);
292
+ const rebuyMinBalanceMicros = this.tokenRebuyMinBalanceMicros();
293
+ if (cached && cached.balanceMicros > rebuyMinBalanceMicros) {
294
+ logger.info("token.cache.hit", "seller token cache hit", {
295
+ sellerKey,
296
+ model: modelId,
297
+ balanceMicros: cached.balanceMicros,
298
+ rebuyMinBalanceMicros
299
+ });
300
+ return cached.token;
301
+ }
302
+ logger.info("token.cache.miss", "seller token cache miss", {
303
+ sellerKey,
304
+ model: modelId,
305
+ balanceMicros: cached?.balanceMicros || 0,
306
+ rebuyMinBalanceMicros
307
+ });
308
+ const purchaseKey = `${sellerKey}:${modelId}:${paymentMethod}`;
309
+ const purchasePromise = this.activePurchases.get(purchaseKey);
310
+ if (purchasePromise) {
311
+ logger.info("purchase.lock.awaited", "parallel request awaiting active purchase", {
312
+ sellerKey,
313
+ model: modelId
314
+ });
315
+ return purchasePromise;
316
+ }
317
+ const purchaseTask = (async () => {
318
+ const startedAt = Date.now();
319
+ const amountUsdMicros = this.autoPurchaseAmountUsdMicros();
320
+ logger.info("purchase.token.started", "seller token purchase started", {
321
+ sellerKey,
322
+ model: modelId,
323
+ paymentMethod,
324
+ amountUsdMicros
325
+ });
326
+ try {
327
+ // 1. purchase/create
328
+ const createRes = await fetch(`${sellerUrl}/purchase/create`, {
329
+ method: "POST",
330
+ headers: { "Content-Type": "application/json" },
331
+ body: JSON.stringify({
332
+ amountUsdMicros,
333
+ currency: "USD",
334
+ requestKey: `pur_req_${crypto.randomBytes(8).toString("hex")}`,
335
+ paymentMethod,
336
+ modelId
337
+ })
338
+ });
339
+ const createData = await createRes.json();
340
+ if (!createRes.ok) {
341
+ logger.warn("purchase.create.failed", "seller purchase create failed", {
342
+ sellerKey,
343
+ model: modelId,
344
+ status: createRes.status,
345
+ errorMessage: createData.error?.message || "purchase/create failed",
346
+ durationMs: Date.now() - startedAt
347
+ });
348
+ throw new Error(createData.error?.message || "purchase/create failed");
349
+ }
350
+ const purchaseId = createData.purchaseId || createData.purchase_id;
351
+ if (!purchaseId) {
352
+ throw new Error("purchase/create response missing purchaseId");
353
+ }
354
+ this.tokenStore.upsertPendingPurchase({
355
+ purchaseId,
356
+ sellerKey,
357
+ modelId,
358
+ paymentMethod,
359
+ amountUsdMicros,
360
+ status: createData.status || "pending",
361
+ paymentReference: createData.paymentReference || createData.payment_reference,
362
+ expiresAt: createData.expiresAt || createData.expires_at
363
+ });
364
+ logger.info("purchase.create.succeeded", "seller purchase created", {
365
+ sellerKey,
366
+ model: modelId,
367
+ purchaseId,
368
+ status: createRes.status
369
+ });
370
+ const paymentProof = await this.resolvePaymentProof(route, createData);
371
+ const completeRes = await fetch(`${sellerUrl}/purchase/complete`, {
372
+ method: "POST",
373
+ headers: { "Content-Type": "application/json" },
374
+ body: JSON.stringify({
375
+ purchaseId,
376
+ paymentProof,
377
+ requestKey: `comp_req_${crypto.randomBytes(8).toString("hex")}`,
378
+ paymentMethod
379
+ })
380
+ });
381
+ const completeData = await completeRes.json();
382
+ if (!completeRes.ok) {
383
+ logger.warn("purchase.complete.failed", "seller purchase complete failed", {
384
+ sellerKey,
385
+ model: modelId,
386
+ purchaseId,
387
+ status: completeRes.status,
388
+ errorMessage: completeData.error?.message || "purchase/complete failed",
389
+ durationMs: Date.now() - startedAt
390
+ });
391
+ throw new Error(completeData.error?.message || "purchase/complete failed");
392
+ }
393
+ const token = completeData.accessToken || completeData.access_token;
394
+ if (!token) {
395
+ throw new Error("purchase/complete response missing accessToken");
396
+ }
397
+ const tokenClass = completeData.tokenClass || completeData.token_class || `model:${modelId}`;
398
+ const creditMicros = completeData.creditMicros ?? completeData.credit_micros ?? createData.creditMicros ?? createData.credit_micros ?? 0;
399
+ const currency = completeData.currency || createData.currency || "USD";
400
+ const expiresAt = new Date(Date.now() + 86400 * 1000).toISOString();
401
+ this.tokenStore.saveToken(sellerKey, token, tokenClass, creditMicros, expiresAt);
402
+ this.tokenStore.recordPurchaseLedger({
403
+ purchaseId,
404
+ sellerKey,
405
+ modelId,
406
+ paymentMethod,
407
+ status: completeData.status || "funded",
408
+ creditMicros,
409
+ currency,
410
+ paymentReference: completeData.paymentReference || completeData.payment_reference,
411
+ completedAt: new Date().toISOString()
412
+ });
413
+ logger.info("purchase.token.succeeded", "seller token purchased", {
414
+ sellerKey,
415
+ model: modelId,
416
+ purchaseId,
417
+ tokenClass,
418
+ creditMicros,
419
+ durationMs: Date.now() - startedAt
420
+ });
421
+ return token;
422
+ }
423
+ catch (error) {
424
+ logger.error("purchase.token.failed", "seller token purchase failed", {
425
+ sellerKey,
426
+ model: modelId,
427
+ errorMessage: error instanceof Error ? error.message : String(error),
428
+ durationMs: Date.now() - startedAt
429
+ });
430
+ throw error;
431
+ }
432
+ finally {
433
+ this.activePurchases.delete(purchaseKey);
434
+ }
435
+ })();
436
+ this.activePurchases.set(purchaseKey, purchaseTask);
437
+ return purchaseTask;
438
+ }
439
+ async resolvePaymentProof(route, createData) {
440
+ if (route.paymentMethod === "mock") {
441
+ return "mock-proof-data";
442
+ }
443
+ if (route.paymentMethod !== "clawtip") {
444
+ throw new Error(`unsupported payment method for auto purchase: ${route.paymentMethod}`);
445
+ }
446
+ const proofCommand = process.env.TB_PROXYD_CLAWTIP_PROOF_COMMAND;
447
+ if (proofCommand?.trim()) {
448
+ return await this.runClawtipProofCommand(route, createData, proofCommand.trim());
449
+ }
450
+ const proofFile = process.env.TOKENBUDDY_CLAWTIP_PAYMENT_PROOF_FILE || process.env.TOKENBUDDY_CLAWTIP_PROOF_FILE;
451
+ if (proofFile?.trim()) {
452
+ return fs.readFileSync(proofFile.trim(), "utf8").trim();
453
+ }
454
+ const proof = process.env.TOKENBUDDY_CLAWTIP_PAYMENT_PROOF;
455
+ if (proof?.trim()) {
456
+ return proof.trim();
457
+ }
458
+ throw new Error("clawtip auto purchase requires TB_PROXYD_CLAWTIP_PROOF_COMMAND or a ClawTip proof env/file");
459
+ }
460
+ runClawtipProofCommand(route, createData, commandPath) {
461
+ const timeoutMs = this.clawtipProofTimeoutMs();
462
+ const payload = JSON.stringify({
463
+ sellerKey: route.seller.id,
464
+ sellerUrl: this.normalizeSellerUrl(route.seller),
465
+ modelId: route.modelId,
466
+ purchase: createData,
467
+ paymentInstructions: createData.paymentInstructions || createData.payment_instructions,
468
+ quote: createData.quote
469
+ });
470
+ logger.info("purchase.clawtip_proof.started", "clawtip proof provider started", {
471
+ sellerKey: route.seller.id,
472
+ model: route.modelId,
473
+ timeoutMs
474
+ });
475
+ return new Promise((resolve, reject) => {
476
+ const child = spawn(commandPath, [], {
477
+ stdio: ["pipe", "pipe", "pipe"],
478
+ env: {
479
+ ...process.env,
480
+ TB_PROXYD_CLAWTIP_SELLER_KEY: route.seller.id,
481
+ TB_PROXYD_CLAWTIP_MODEL_ID: route.modelId
482
+ }
483
+ });
484
+ let stdout = "";
485
+ let stderr = "";
486
+ let settled = false;
487
+ const startedAt = Date.now();
488
+ const timer = setTimeout(() => {
489
+ if (settled) {
490
+ return;
491
+ }
492
+ settled = true;
493
+ child.kill("SIGTERM");
494
+ reject(new Error("clawtip proof provider timed out"));
495
+ }, timeoutMs);
496
+ child.stdout.on("data", (chunk) => {
497
+ stdout += chunk.toString("utf8");
498
+ if (stdout.length > 1024 * 1024) {
499
+ child.kill("SIGTERM");
500
+ }
501
+ });
502
+ child.stderr.on("data", (chunk) => {
503
+ stderr += chunk.toString("utf8").slice(0, 2048);
504
+ });
505
+ child.on("error", (error) => {
506
+ if (settled) {
507
+ return;
508
+ }
509
+ settled = true;
510
+ clearTimeout(timer);
511
+ reject(error);
512
+ });
513
+ child.on("close", (code) => {
514
+ if (settled) {
515
+ return;
516
+ }
517
+ settled = true;
518
+ clearTimeout(timer);
519
+ const proof = stdout.trim();
520
+ if (code !== 0 || !proof) {
521
+ reject(new Error(`clawtip proof provider failed with exit ${code}: ${stderr.trim() || "empty proof"}`));
522
+ return;
523
+ }
524
+ logger.info("purchase.clawtip_proof.succeeded", "clawtip proof provider succeeded", {
525
+ sellerKey: route.seller.id,
526
+ model: route.modelId,
527
+ durationMs: Date.now() - startedAt
528
+ });
529
+ resolve(proof);
530
+ });
531
+ child.stdin.end(payload);
532
+ });
533
+ }
534
+ clawtipProofTimeoutMs() {
535
+ const raw = process.env.TB_PROXYD_CLAWTIP_PROOF_TIMEOUT_MS;
536
+ const parsed = raw ? Number(raw) : 120000;
537
+ if (!Number.isInteger(parsed) || parsed < 1000 || parsed > 600000) {
538
+ return 120000;
539
+ }
540
+ return parsed;
541
+ }
542
+ copyUpstreamHeaders(upstreamResponse, res) {
543
+ upstreamResponse.headers.forEach((value, key) => {
544
+ const lowerKey = key.toLowerCase();
545
+ if (["connection", "keep-alive", "transfer-encoding", "upgrade"].includes(lowerKey)) {
546
+ return;
547
+ }
548
+ res.setHeader(key, value);
549
+ });
550
+ }
551
+ async forwardProxyRequest(endpoint, req, res) {
552
+ const startedAt = Date.now();
553
+ const body = req.body || {};
554
+ const modelId = this.extractModelId(endpoint, body);
555
+ const requestId = req.header("x-request-id") || (body && typeof body === "object" ? body.requestId : undefined) || `proxy_req_${crypto.randomBytes(8).toString("hex")}`;
556
+ const idempotencyKey = req.header("idempotency-key") || `idem_${crypto.randomBytes(12).toString("hex")}`;
557
+ if (!modelId) {
558
+ res.status(400).json({ error: { code: "model_required", message: "request body must include model" } });
559
+ return;
560
+ }
561
+ try {
562
+ const routes = await this.selectSellerRoutes(endpoint, modelId);
563
+ let lastError;
564
+ for (let routeIndex = 0; routeIndex < routes.length; routeIndex += 1) {
565
+ const route = routes[routeIndex];
566
+ const sellerKey = route.seller.id;
567
+ this.logRouteSelected(route, endpoint, routeIndex);
568
+ try {
569
+ logger.info("proxy.request.started", "proxy request started", {
570
+ requestId,
571
+ sellerKey,
572
+ model: modelId,
573
+ endpoint,
574
+ stream: Boolean(body.stream)
575
+ });
576
+ const token = await this.getOrPurchaseToken(route);
577
+ const sellerUrl = this.normalizeSellerUrl(route.seller);
578
+ const upstreamBody = {
579
+ ...body,
580
+ requestId
581
+ };
582
+ logger.info("proxy.upstream_fetch.started", "proxy upstream fetch started", {
583
+ requestId,
584
+ sellerKey,
585
+ model: modelId,
586
+ endpoint,
587
+ stream: Boolean(body.stream)
588
+ });
589
+ const upstreamResponse = await fetch(`${sellerUrl}${endpoint}`, {
590
+ method: "POST",
591
+ headers: {
592
+ "Content-Type": "application/json",
593
+ "Authorization": `Bearer ${token}`,
594
+ "X-Request-Id": requestId,
595
+ "Idempotency-Key": idempotencyKey
596
+ },
597
+ body: JSON.stringify(upstreamBody)
598
+ });
599
+ if (!upstreamResponse.ok) {
600
+ const errorBody = await upstreamResponse.text();
601
+ logger.warn("proxy.upstream_fetch.failed", "proxy upstream fetch returned non-ok status", {
602
+ requestId,
603
+ sellerKey,
604
+ model: modelId,
605
+ endpoint,
606
+ status: upstreamResponse.status,
607
+ durationMs: Date.now() - startedAt
608
+ });
609
+ if (this.shouldFailoverStatus(upstreamResponse.status) && routeIndex < routes.length - 1) {
610
+ lastError = new Error(`seller ${sellerKey} returned ${upstreamResponse.status}`);
611
+ this.logFailover(route, endpoint, routeIndex, "upstream_status", upstreamResponse.status);
612
+ continue;
613
+ }
614
+ this.copyUpstreamHeaders(upstreamResponse, res);
615
+ res.status(upstreamResponse.status);
616
+ res.send(errorBody);
617
+ return;
618
+ }
619
+ this.copyUpstreamHeaders(upstreamResponse, res);
620
+ res.status(upstreamResponse.status);
621
+ logger.info("proxy.upstream_fetch.succeeded", "proxy upstream fetch succeeded", {
622
+ requestId,
623
+ sellerKey,
624
+ model: modelId,
625
+ endpoint,
626
+ status: upstreamResponse.status,
627
+ stream: Boolean(body.stream)
628
+ });
629
+ const contentType = upstreamResponse.headers.get("content-type") || "";
630
+ if (contentType.includes("text/event-stream") || Boolean(body.stream)) {
631
+ const reader = upstreamResponse.body?.getReader();
632
+ if (!reader) {
633
+ res.end();
634
+ return;
635
+ }
636
+ let bytes = 0;
637
+ while (true) {
638
+ const { done, value } = await reader.read();
639
+ if (done) {
640
+ break;
641
+ }
642
+ bytes += value.byteLength;
643
+ res.write(Buffer.from(value));
644
+ }
645
+ res.end();
646
+ const billedMicros = Math.max(1, bytes);
647
+ this.tokenStore.deductBalance(sellerKey, billedMicros);
648
+ this.tokenStore.recordInferenceLedger({
649
+ requestId,
650
+ sellerKey,
651
+ modelId,
652
+ endpoint,
653
+ status: "settled",
654
+ promptTokens: 0,
655
+ completionTokens: 0,
656
+ billedMicros,
657
+ prompt: this.inferPromptForHash(body)
658
+ });
659
+ logger.info("inference.ledger.recorded", "safe inference ledger recorded", {
660
+ requestId,
661
+ sellerKey,
662
+ model: modelId,
663
+ endpoint,
664
+ status: "settled",
665
+ billedMicros
666
+ });
667
+ return;
668
+ }
669
+ const responseBody = await upstreamResponse.text();
670
+ res.send(responseBody);
671
+ const usage = this.readUsage(responseBody);
672
+ this.tokenStore.deductBalance(sellerKey, usage.billedMicros);
673
+ this.tokenStore.recordInferenceLedger({
674
+ requestId,
675
+ sellerKey,
676
+ modelId,
677
+ endpoint,
678
+ status: "settled",
679
+ promptTokens: usage.promptTokens,
680
+ completionTokens: usage.completionTokens,
681
+ billedMicros: usage.billedMicros,
682
+ prompt: this.inferPromptForHash(body),
683
+ response: responseBody
684
+ });
685
+ logger.info("inference.ledger.recorded", "safe inference ledger recorded", {
686
+ requestId,
687
+ sellerKey,
688
+ model: modelId,
689
+ endpoint,
690
+ status: "settled",
691
+ promptTokens: usage.promptTokens,
692
+ completionTokens: usage.completionTokens,
693
+ billedMicros: usage.billedMicros
694
+ });
695
+ return;
696
+ }
697
+ catch (routeError) {
698
+ lastError = routeError;
699
+ logger.warn("proxy.route.failed", "seller route failed before response", {
700
+ requestId,
701
+ sellerKey,
702
+ model: modelId,
703
+ endpoint,
704
+ errorMessage: this.failoverErrorMessage(routeError),
705
+ durationMs: Date.now() - startedAt
706
+ });
707
+ if (!res.headersSent && routeIndex < routes.length - 1) {
708
+ this.logFailover(route, endpoint, routeIndex, "exception");
709
+ continue;
710
+ }
711
+ throw routeError;
712
+ }
713
+ }
714
+ throw lastError instanceof Error ? lastError : new Error("all seller routes failed");
715
+ }
716
+ catch (error) {
717
+ logger.error("proxy.request.failed", "proxy request failed", {
718
+ requestId,
719
+ model: modelId,
720
+ endpoint,
721
+ errorMessage: error instanceof Error ? error.message : String(error),
722
+ durationMs: Date.now() - startedAt
723
+ });
724
+ if (!res.headersSent) {
725
+ res.status(502).json({
726
+ error: {
727
+ code: "proxy_request_failed",
728
+ message: error instanceof Error ? error.message : String(error)
729
+ }
730
+ });
731
+ }
732
+ }
733
+ }
734
+ start() {
735
+ // 1. Control Plane Server (17820)
736
+ const controlApp = express();
737
+ controlApp.use(express.json());
738
+ controlApp.get("/health", (req, res) => {
739
+ logger.info("control.health.requested", "control health requested", {
740
+ controlPort: this.activeControlPort(),
741
+ proxyPort: this.activeProxyPort()
742
+ });
743
+ res.status(200).json({
744
+ status: "ok",
745
+ controlPort: this.activeControlPort(),
746
+ proxyPort: this.activeProxyPort(),
747
+ store: {
748
+ journalMode: this.tokenStore.journalMode()
749
+ }
750
+ });
751
+ });
752
+ controlApp.get("/status", (req, res) => {
753
+ logger.info("control.status.requested", "control status requested", {
754
+ controlPort: this.activeControlPort(),
755
+ proxyPort: this.activeProxyPort()
756
+ });
757
+ res.status(200).json({
758
+ ...this.runtimeSummary()
759
+ });
760
+ });
761
+ controlApp.get("/payments", (req, res) => {
762
+ logger.info("control.payments.requested", "control payments requested", {});
763
+ res.status(200).json({
764
+ payments: this.tokenStore.listPayments()
765
+ });
766
+ });
767
+ controlApp.get("/ledger/purchases", (req, res) => {
768
+ logger.info("control.ledger.requested", "control purchase ledger requested", {
769
+ ledger: "purchases"
770
+ });
771
+ res.status(200).json({
772
+ purchases: this.tokenStore.listPurchaseLedger()
773
+ });
774
+ });
775
+ controlApp.get("/ledger/inferences", (req, res) => {
776
+ logger.info("control.ledger.requested", "control inference ledger requested", {
777
+ ledger: "inferences"
778
+ });
779
+ res.status(200).json({
780
+ inferences: this.tokenStore.listInferenceLedger()
781
+ });
782
+ });
783
+ controlApp.get("/sellers", async (req, res) => {
784
+ try {
785
+ const registry = await this.fetchRegistry();
786
+ logger.info("sellers.refresh.succeeded", "seller registry refreshed", {
787
+ sellerCount: registry.sellers.length
788
+ });
789
+ res.status(200).json({
790
+ registryUrl: this.config.sellerRegistryUrl,
791
+ version: registry.version,
792
+ defaultSeller: registry.defaultSeller,
793
+ sellers: registry.sellers.map((seller) => ({
794
+ id: seller.id,
795
+ name: seller.name,
796
+ url: seller.url,
797
+ supportedProtocols: seller.supportedProtocols || [],
798
+ paymentMethods: seller.paymentMethods || [],
799
+ status: "configured"
800
+ }))
801
+ });
802
+ }
803
+ catch (error) {
804
+ const errorMessage = error instanceof Error ? error.message : String(error);
805
+ logger.warn("sellers.refresh.failed", "seller registry refresh failed", {
806
+ registryUrl: this.config.sellerRegistryUrl,
807
+ errorMessage
808
+ });
809
+ res.status(502).json({
810
+ error: {
811
+ code: "registry_unavailable",
812
+ message: errorMessage
813
+ }
814
+ });
815
+ }
816
+ });
817
+ controlApp.get("/models", async (req, res) => {
818
+ try {
819
+ const { models, sellers } = await this.listSellerBackedModels();
820
+ logger.info("models.refresh.succeeded", "seller models refreshed", {
821
+ sellerCount: sellers.length,
822
+ modelCount: models.length
823
+ });
824
+ res.status(200).json({
825
+ object: "list",
826
+ registryUrl: this.config.sellerRegistryUrl,
827
+ data: models,
828
+ sellers
829
+ });
830
+ }
831
+ catch (error) {
832
+ const errorMessage = error instanceof Error ? error.message : String(error);
833
+ logger.warn("models.refresh.failed", "model refresh failed", {
834
+ registryUrl: this.config.sellerRegistryUrl,
835
+ errorMessage
836
+ });
837
+ res.status(502).json({
838
+ error: {
839
+ code: "models_unavailable",
840
+ message: errorMessage
841
+ }
842
+ });
843
+ }
844
+ });
845
+ controlApp.post("/providers/detect", (req, res) => {
846
+ try {
847
+ const providers = detectProviders({ home: typeof req.body?.home === "string" ? req.body.home : undefined });
848
+ logger.info("provider.detect.succeeded", "provider detection succeeded", {
849
+ providerCount: providers.length,
850
+ detectedCount: providers.filter((provider) => provider.detected).length
851
+ });
852
+ res.status(200).json({ providers });
853
+ }
854
+ catch (error) {
855
+ const errorMessage = error instanceof Error ? error.message : String(error);
856
+ logger.warn("provider.detect.failed", "provider detection failed", { errorMessage });
857
+ res.status(400).json({
858
+ error: {
859
+ code: "provider_detect_failed",
860
+ message: errorMessage
861
+ }
862
+ });
863
+ }
864
+ });
865
+ controlApp.post("/providers/install/preview", (req, res) => {
866
+ try {
867
+ const changes = previewProviderInstall({
868
+ providers: Array.isArray(req.body?.providers) ? req.body.providers : [],
869
+ proxyUrl: String(req.body?.proxyUrl || ""),
870
+ model: String(req.body?.model || ""),
871
+ home: typeof req.body?.home === "string" ? req.body.home : undefined
872
+ });
873
+ logger.info("provider.install.previewed", "provider install previewed", {
874
+ providerCount: new Set(changes.map((change) => change.providerId)).size,
875
+ changeCount: changes.length
876
+ });
877
+ res.status(200).json({
878
+ changes: changes.map(({ content, ...change }) => change)
879
+ });
880
+ }
881
+ catch (error) {
882
+ const errorMessage = error instanceof Error ? error.message : String(error);
883
+ logger.warn("provider.install.preview_failed", "provider install preview failed", { errorMessage });
884
+ res.status(400).json({
885
+ error: {
886
+ code: "provider_install_preview_failed",
887
+ message: errorMessage
888
+ }
889
+ });
890
+ }
891
+ });
892
+ controlApp.post("/providers/install/apply", (req, res) => {
893
+ try {
894
+ const applied = applyProviderInstall({
895
+ providers: Array.isArray(req.body?.providers) ? req.body.providers : [],
896
+ proxyUrl: String(req.body?.proxyUrl || ""),
897
+ model: String(req.body?.model || ""),
898
+ home: typeof req.body?.home === "string" ? req.body.home : undefined
899
+ }, this.tokenStore);
900
+ logger.info("provider.install.applied", "provider install applied", {
901
+ providerCount: new Set(applied.map((entry) => entry.providerId)).size,
902
+ changeCount: applied.length
903
+ });
904
+ res.status(200).json({ applied });
905
+ }
906
+ catch (error) {
907
+ const errorMessage = error instanceof Error ? error.message : String(error);
908
+ logger.warn("provider.install.apply_failed", "provider install apply failed", { errorMessage });
909
+ res.status(400).json({
910
+ error: {
911
+ code: "provider_install_apply_failed",
912
+ message: errorMessage
913
+ }
914
+ });
915
+ }
916
+ });
917
+ controlApp.post("/providers/install/rollback", (req, res) => {
918
+ try {
919
+ const rolledBack = rollbackProviderInstall({
920
+ providers: Array.isArray(req.body?.providers) ? req.body.providers : [],
921
+ home: typeof req.body?.home === "string" ? req.body.home : undefined
922
+ }, this.tokenStore);
923
+ logger.info("provider.install.rolled_back", "provider install rolled back", {
924
+ providerCount: new Set(rolledBack.map((entry) => entry.providerId)).size,
925
+ changeCount: rolledBack.length
926
+ });
927
+ res.status(200).json({ rolledBack });
928
+ }
929
+ catch (error) {
930
+ const errorMessage = error instanceof Error ? error.message : String(error);
931
+ logger.warn("provider.install.rollback_failed", "provider install rollback failed", { errorMessage });
932
+ res.status(400).json({
933
+ error: {
934
+ code: "provider_install_rollback_failed",
935
+ message: errorMessage
936
+ }
937
+ });
938
+ }
939
+ });
940
+ this.controlServer = controlApp.listen(this.config.controlPort);
941
+ // 2. Proxy Plane Server (17821)
942
+ const proxyApp = express();
943
+ proxyApp.use(express.json({ limit: PROXY_JSON_BODY_LIMIT }));
944
+ proxyApp.get("/v1/models", async (req, res) => {
945
+ try {
946
+ const { models } = await this.listSellerBackedModels();
947
+ logger.info("models.refresh.succeeded", "proxy models refreshed", {
948
+ modelCount: models.length
949
+ });
950
+ res.status(200).json({
951
+ object: "list",
952
+ data: models.map((model) => ({
953
+ id: model.id,
954
+ object: "model",
955
+ owned_by: model.sellerId,
956
+ sellerId: model.sellerId,
957
+ supportedProtocols: model.supportedProtocols,
958
+ paymentMethods: model.paymentMethods
959
+ }))
960
+ });
961
+ }
962
+ catch (error) {
963
+ const errorMessage = error instanceof Error ? error.message : String(error);
964
+ logger.warn("models.refresh.failed", "proxy models refresh failed", {
965
+ registryUrl: this.config.sellerRegistryUrl,
966
+ errorMessage
967
+ });
968
+ res.status(502).json({
969
+ error: {
970
+ code: "models_unavailable",
971
+ message: errorMessage
972
+ }
973
+ });
974
+ }
975
+ });
976
+ for (const endpoint of ["/v1/chat/completions", "/v1/responses", "/v1/messages", "/messages"]) {
977
+ proxyApp.post(endpoint, async (req, res) => {
978
+ await this.forwardProxyRequest(endpoint, req, res);
979
+ });
980
+ }
981
+ this.proxyServer = proxyApp.listen(this.config.proxyPort);
982
+ logger.info("proxy.startup", "tb-proxyd daemon started", {
983
+ controlPort: this.config.controlPort,
984
+ proxyPort: this.config.proxyPort,
985
+ dbPath: this.config.dbPath,
986
+ sellerRegistryUrl: this.config.sellerRegistryUrl,
987
+ selectionMode: this.selectionMode
988
+ });
989
+ }
990
+ stop() {
991
+ if (this.controlServer)
992
+ this.controlServer.close();
993
+ if (this.proxyServer)
994
+ this.proxyServer.close();
995
+ this.tokenStore.close();
996
+ }
997
+ }
998
+ //# sourceMappingURL=daemon.js.map