@tokenbuddy/tokenbuddy 1.0.11 → 1.0.13

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 (135) hide show
  1. package/dist/src/buyer-store.d.ts +61 -0
  2. package/dist/src/buyer-store.d.ts.map +1 -1
  3. package/dist/src/buyer-store.js +12 -0
  4. package/dist/src/buyer-store.js.map +1 -1
  5. package/dist/src/cli.d.ts +47 -0
  6. package/dist/src/cli.d.ts.map +1 -1
  7. package/dist/src/cli.js +287 -63
  8. package/dist/src/cli.js.map +1 -1
  9. package/dist/src/credit-tracker.d.ts +26 -0
  10. package/dist/src/credit-tracker.d.ts.map +1 -1
  11. package/dist/src/credit-tracker.js +8 -0
  12. package/dist/src/credit-tracker.js.map +1 -1
  13. package/dist/src/daemon.d.ts +29 -3
  14. package/dist/src/daemon.d.ts.map +1 -1
  15. package/dist/src/daemon.js +292 -65
  16. package/dist/src/daemon.js.map +1 -1
  17. package/dist/src/doctor-clawtip-wallet.d.ts +25 -0
  18. package/dist/src/doctor-clawtip-wallet.d.ts.map +1 -1
  19. package/dist/src/doctor-clawtip-wallet.js +13 -0
  20. package/dist/src/doctor-clawtip-wallet.js.map +1 -1
  21. package/dist/src/doctor-diagnostics.d.ts +63 -0
  22. package/dist/src/doctor-diagnostics.d.ts.map +1 -1
  23. package/dist/src/doctor-diagnostics.js +39 -1
  24. package/dist/src/doctor-diagnostics.js.map +1 -1
  25. package/dist/src/index.d.ts +4 -0
  26. package/dist/src/index.d.ts.map +1 -1
  27. package/dist/src/index.js +4 -0
  28. package/dist/src/index.js.map +1 -1
  29. package/dist/src/init-clawtip-activation.d.ts +103 -0
  30. package/dist/src/init-clawtip-activation.d.ts.map +1 -1
  31. package/dist/src/init-clawtip-activation.js +60 -0
  32. package/dist/src/init-clawtip-activation.js.map +1 -1
  33. package/dist/src/init-payment-options.d.ts +124 -0
  34. package/dist/src/init-payment-options.d.ts.map +1 -1
  35. package/dist/src/init-payment-options.js +68 -0
  36. package/dist/src/init-payment-options.js.map +1 -1
  37. package/dist/src/model-index.d.ts +9 -0
  38. package/dist/src/model-index.d.ts.map +1 -1
  39. package/dist/src/model-index.js.map +1 -1
  40. package/dist/src/prewarm-cache.d.ts +89 -0
  41. package/dist/src/prewarm-cache.d.ts.map +1 -1
  42. package/dist/src/prewarm-cache.js +14 -1
  43. package/dist/src/prewarm-cache.js.map +1 -1
  44. package/dist/src/prewarm-scheduler.d.ts +62 -3
  45. package/dist/src/prewarm-scheduler.d.ts.map +1 -1
  46. package/dist/src/prewarm-scheduler.js +39 -8
  47. package/dist/src/prewarm-scheduler.js.map +1 -1
  48. package/dist/src/provider-install.d.ts +89 -3
  49. package/dist/src/provider-install.d.ts.map +1 -1
  50. package/dist/src/provider-install.js +77 -17
  51. package/dist/src/provider-install.js.map +1 -1
  52. package/dist/src/route-failover.d.ts +48 -0
  53. package/dist/src/route-failover.d.ts.map +1 -1
  54. package/dist/src/route-failover.js.map +1 -1
  55. package/dist/src/seller-catalog.d.ts +158 -10
  56. package/dist/src/seller-catalog.d.ts.map +1 -1
  57. package/dist/src/seller-catalog.js +79 -5
  58. package/dist/src/seller-catalog.js.map +1 -1
  59. package/dist/src/seller-metadata-cache.d.ts +29 -0
  60. package/dist/src/seller-metadata-cache.d.ts.map +1 -0
  61. package/dist/src/seller-metadata-cache.js +71 -0
  62. package/dist/src/seller-metadata-cache.js.map +1 -0
  63. package/dist/src/seller-pool.d.ts +71 -0
  64. package/dist/src/seller-pool.d.ts.map +1 -1
  65. package/dist/src/seller-pool.js +6 -1
  66. package/dist/src/seller-pool.js.map +1 -1
  67. package/dist/src/seller-route-planner.d.ts +118 -0
  68. package/dist/src/seller-route-planner.d.ts.map +1 -0
  69. package/dist/src/seller-route-planner.js +160 -0
  70. package/dist/src/seller-route-planner.js.map +1 -0
  71. package/dist/src/seller-routing-config.d.ts +69 -0
  72. package/dist/src/seller-routing-config.d.ts.map +1 -0
  73. package/dist/src/seller-routing-config.js +164 -0
  74. package/dist/src/seller-routing-config.js.map +1 -0
  75. package/dist/src/seller-routing-strategy.d.ts +118 -0
  76. package/dist/src/seller-routing-strategy.d.ts.map +1 -0
  77. package/dist/src/seller-routing-strategy.js +183 -0
  78. package/dist/src/seller-routing-strategy.js.map +1 -0
  79. package/dist/src/stream-failover.d.ts +23 -0
  80. package/dist/src/stream-failover.d.ts.map +1 -1
  81. package/dist/src/stream-failover.js +4 -0
  82. package/dist/src/stream-failover.js.map +1 -1
  83. package/dist/src/tb-proxyd.js +7 -21
  84. package/dist/src/tb-proxyd.js.map +1 -1
  85. package/dist/src/terminal-detect.d.ts +51 -0
  86. package/dist/src/terminal-detect.d.ts.map +1 -1
  87. package/dist/src/terminal-detect.js +42 -0
  88. package/dist/src/terminal-detect.js.map +1 -1
  89. package/dist/src/terminal-image.d.ts +41 -0
  90. package/dist/src/terminal-image.d.ts.map +1 -1
  91. package/dist/src/terminal-image.js +15 -0
  92. package/dist/src/terminal-image.js.map +1 -1
  93. package/package.json +1 -1
  94. package/src/buyer-store.ts +61 -0
  95. package/src/cli.ts +330 -68
  96. package/src/credit-tracker.ts +26 -0
  97. package/src/daemon.ts +363 -72
  98. package/src/doctor-clawtip-wallet.ts +25 -0
  99. package/src/doctor-diagnostics.ts +63 -1
  100. package/src/index.ts +4 -0
  101. package/src/init-clawtip-activation.ts +103 -0
  102. package/src/init-payment-options.ts +124 -0
  103. package/src/model-index.ts +9 -0
  104. package/src/prewarm-cache.ts +99 -1
  105. package/src/prewarm-scheduler.ts +97 -12
  106. package/src/provider-install.ts +125 -25
  107. package/src/route-failover.ts +48 -0
  108. package/src/seller-catalog.ts +158 -12
  109. package/src/seller-metadata-cache.ts +91 -0
  110. package/src/seller-pool.ts +77 -1
  111. package/src/seller-route-planner.ts +323 -0
  112. package/src/seller-routing-config.ts +198 -0
  113. package/src/seller-routing-strategy.ts +316 -0
  114. package/src/stream-failover.ts +23 -0
  115. package/src/tb-proxyd.ts +7 -23
  116. package/src/terminal-detect.ts +51 -0
  117. package/src/terminal-image.ts +41 -0
  118. package/tests/cli-routing.test.ts +287 -0
  119. package/tests/daemon-classify.test.ts +431 -0
  120. package/tests/daemon-roles.test.ts +92 -0
  121. package/tests/seller-catalog-utilities.test.ts +70 -0
  122. package/tests/seller-metadata-cache.test.ts +89 -0
  123. package/tests/seller-route-planner.test.ts +150 -0
  124. package/tests/seller-routing-config.test.ts +111 -0
  125. package/tests/seller-routing-strategy.test.ts +166 -0
  126. package/tests/tokenbuddy.test.ts +447 -33
  127. /package/{src → tests}/credit-tracker.test.ts +0 -0
  128. /package/{src → tests}/model-index.test.ts +0 -0
  129. /package/{src → tests}/prewarm-cache.test.ts +0 -0
  130. /package/{src → tests}/prewarm-scheduler.test.ts +0 -0
  131. /package/{src → tests}/route-failover.test.ts +0 -0
  132. /package/{src → tests}/seller-catalog-413.test.ts +0 -0
  133. /package/{src → tests}/seller-pool.test.ts +0 -0
  134. /package/{src → tests}/stream-failover.test.ts +0 -0
  135. /package/{src → tests}/thousand-seller.test.ts +0 -0
@@ -12,6 +12,8 @@ import { CreditTracker } from "./credit-tracker.js";
12
12
  import { SellerPool } from "./seller-pool.js";
13
13
  import { RouteFailover } from "./route-failover.js";
14
14
  import { PrewarmScheduler } from "./prewarm-scheduler.js";
15
+ import { planSellerRouteSet } from "./seller-route-planner.js";
16
+ import { mergeSellerRoutingConfig, ROUTING_CONFIG_KEY } from "./seller-routing-config.js";
15
17
  const logger = createModuleLogger("tb-proxyd");
16
18
  const PROXY_JSON_BODY_LIMIT = "10mb";
17
19
  function numericHeaderField(value) {
@@ -34,10 +36,12 @@ class SellerSettlementStreamExtractor {
34
36
  return blocks
35
37
  .map((block) => this.processBlock(block))
36
38
  .filter((block) => block.length > 0)
37
- .join("\n\n");
39
+ .map((block) => `${block}\n\n`)
40
+ .join("");
38
41
  }
39
42
  finish() {
40
- const downstream = this.pending.trim() ? this.processBlock(this.pending) : "";
43
+ const processed = this.pending.trim() ? this.processBlock(this.pending) : "";
44
+ const downstream = processed ? processed : "";
41
45
  this.pending = "";
42
46
  return { downstream, settlement: this.settlement };
43
47
  }
@@ -96,6 +100,44 @@ function parseSellerSettlementObject(raw) {
96
100
  return undefined;
97
101
  }
98
102
  }
103
+ function arrayLength(value) {
104
+ return Array.isArray(value) ? value.length : undefined;
105
+ }
106
+ function summarizeProxyBody(body) {
107
+ if (!body || typeof body !== "object") {
108
+ return { bodyType: typeof body };
109
+ }
110
+ const data = body;
111
+ const messages = arrayLength(data.messages);
112
+ const input = arrayLength(data.input);
113
+ const tools = arrayLength(data.tools);
114
+ return {
115
+ bodyType: "object",
116
+ messageCount: messages,
117
+ inputItemCount: input,
118
+ toolCount: tools,
119
+ hasMessages: messages !== undefined,
120
+ hasInput: data.input !== undefined,
121
+ hasTools: tools !== undefined,
122
+ maxTokensPresent: data.max_tokens !== undefined || data.maxTokens !== undefined,
123
+ temperaturePresent: data.temperature !== undefined
124
+ };
125
+ }
126
+ function reorderDefaultSellerFirst(sellers, defaultSellerId) {
127
+ if (!defaultSellerId) {
128
+ return sellers;
129
+ }
130
+ return [
131
+ ...sellers.filter((seller) => seller.id === defaultSellerId),
132
+ ...sellers.filter((seller) => seller.id !== defaultSellerId)
133
+ ];
134
+ }
135
+ /**
136
+ * buyer 端守护进程。
137
+ * 负责启动两个 Express 服务:控制接口(healthz + 控制路由)+ 反向代理(OpenAI / Anthropic 协议入口)。
138
+ * 同时跑后台任务:seller catalog 周期拉取、model-index 刷新、prewarm scheduler、credit tracker。
139
+ * 推荐用 `startDaemon(config)` 启动 / `stopDaemon(daemon)` 优雅关闭,避免直接管理生命周期。
140
+ */
99
141
  export class TokenbuddyDaemon {
100
142
  config;
101
143
  tokenStore;
@@ -103,6 +145,7 @@ export class TokenbuddyDaemon {
103
145
  proxyServer;
104
146
  selectionMode;
105
147
  selectedSellerId;
148
+ sellerRouting;
106
149
  activePurchases = new Map();
107
150
  // v1.2 fallback pipeline: model-index, prewarm-cache, credit-tracker,
108
151
  // pool, and route-failover together replace the v1
@@ -125,15 +168,12 @@ export class TokenbuddyDaemon {
125
168
  prewarmScheduler;
126
169
  constructor(config) {
127
170
  this.tokenStore = new BuyerStore({ dbPath: config.dbPath });
128
- const routingPreference = this.tokenStore.getDaemonRuntimeConfig("routing")
171
+ const storedRouting = this.tokenStore.getDaemonRuntimeConfig(ROUTING_CONFIG_KEY)
129
172
  ?.config;
130
173
  this.config = config;
131
- this.selectionMode =
132
- config.selectionMode ||
133
- (routingPreference?.mode === "fixed" ? "manual" : routingPreference?.mode) ||
134
- "auto";
135
- this.selectedSellerId =
136
- config.selectedSellerId || routingPreference?.sellerId;
174
+ this.sellerRouting = mergeSellerRoutingConfig(storedRouting, config.sellerRouting);
175
+ this.selectionMode = this.sellerRouting.mode === "fullAuto" ? "auto" : "manual";
176
+ this.selectedSellerId = this.sellerRouting.mode === "fixed" ? this.sellerRouting.sellerId : undefined;
137
177
  // v1.2 §18.5: scheduler is created here (not in the field initializer)
138
178
  // because it needs the config-derived prober + idle interval.
139
179
  Object.assign(this, {
@@ -149,7 +189,7 @@ export class TokenbuddyDaemon {
149
189
  return async (seller, signal) => {
150
190
  try {
151
191
  const ac = new AbortController();
152
- const timer = setTimeout(() => ac.abort(new Error("healthz timeout")), timeoutMs);
192
+ const timer = setTimeout(() => ac.abort(new Error("health timeout")), timeoutMs);
153
193
  if (signal) {
154
194
  if (signal.aborted) {
155
195
  ac.abort(signal.reason);
@@ -159,10 +199,10 @@ export class TokenbuddyDaemon {
159
199
  }
160
200
  }
161
201
  const startedAt = Date.now();
162
- const res = await fetch(`${seller.url.replace(/\/+$/, "")}/healthz`, { signal: ac.signal });
202
+ const res = await fetch(`${seller.url.replace(/\/+$/, "")}/health`, { signal: ac.signal });
163
203
  clearTimeout(timer);
164
204
  if (!res.ok) {
165
- return { ok: false, latencyMs: Date.now() - startedAt, httpStatus: res.status, errorMessage: `healthz returned ${res.status}` };
205
+ return { ok: false, latencyMs: Date.now() - startedAt, httpStatus: res.status, errorMessage: `health returned ${res.status}` };
166
206
  }
167
207
  return { ok: true, latencyMs: Date.now() - startedAt, httpStatus: res.status };
168
208
  }
@@ -219,14 +259,15 @@ export class TokenbuddyDaemon {
219
259
  }
220
260
  }
221
261
  runtimeSummary() {
222
- const sellerRoutingMode = this.selectedSellerId ? "fixed" : this.selectionMode;
223
262
  return {
224
263
  status: "running",
225
264
  pid: process.pid,
226
265
  controlPort: this.activeControlPort(),
227
266
  proxyPort: this.activeProxyPort(),
228
267
  selectionMode: this.selectionMode,
229
- sellerRoutingMode,
268
+ sellerRoutingMode: this.sellerRouting.mode,
269
+ sellerRoutingScorer: this.sellerRouting.scorer,
270
+ sellerRouting: this.sellerRouting,
230
271
  selectedSellerId: this.selectedSellerId,
231
272
  dbPath: this.config.dbPath,
232
273
  sellerRegistryUrl: this.config.sellerRegistryUrl,
@@ -325,32 +366,33 @@ export class TokenbuddyDaemon {
325
366
  // "fetchSellerManifest per candidate" path is removed in favor of
326
367
  // pulling `models` directly off the registry entries.
327
368
  const registry = await this.fetchRegistry();
328
- const indexCandidates = this.modelIndex.sellersFor(modelId, { protocol, paymentMethod });
329
- let ordered = indexCandidates;
330
- if (this.selectionMode === "manual" && this.selectedSellerId) {
331
- ordered = indexCandidates.filter((seller) => seller.id === this.selectedSellerId);
332
- }
333
- else if (this.selectionMode === "manual" && registry.defaultSeller) {
334
- ordered = indexCandidates.filter((seller) => seller.id === registry.defaultSeller);
335
- }
336
- else if (registry.defaultSeller) {
337
- // auto mode: default first, then backups in registry order
338
- ordered = [
339
- ...indexCandidates.filter((seller) => seller.id === registry.defaultSeller),
340
- ...indexCandidates.filter((seller) => seller.id !== registry.defaultSeller)
341
- ];
342
- }
343
- if (ordered.length === 0) {
369
+ const routing = this.sellerRouting;
370
+ const registrySellers = reorderDefaultSellerFirst(registry.sellers, registry.defaultSeller);
371
+ const poolById = new Map(this.sellerPool.snapshot().map((entry) => [entry.sellerId, entry]));
372
+ const planned = planSellerRouteSet({
373
+ modelId,
374
+ protocol,
375
+ paymentMethod,
376
+ registrySellers,
377
+ routing,
378
+ prewarmCandidates: this.prewarmCache.get(modelId, protocol, paymentMethod)?.candidates,
379
+ sellerMetrics: Array.from(poolById.values()).map((entry) => ({
380
+ sellerId: entry.sellerId,
381
+ healthScore: entry.healthScore,
382
+ avgLatencyMs: entry.avgLatencyMs,
383
+ circuit: entry.circuit
384
+ }))
385
+ });
386
+ if (planned.routes.length === 0) {
344
387
  throw new Error(`no compatible seller for ${endpoint} model ${modelId}`);
345
388
  }
346
- const poolById = new Map(this.sellerPool.snapshot().map((entry) => [entry.sellerId, entry]));
347
- const routes = ordered.map((seller) => ({
348
- seller,
389
+ const routes = planned.routes.map((route) => ({
390
+ seller: route.seller,
349
391
  manifest: null,
350
392
  protocol,
351
393
  modelId,
352
394
  paymentMethod,
353
- poolEntry: poolById.get(seller.id)
395
+ poolEntry: poolById.get(route.seller.id)
354
396
  }));
355
397
  logger.info("route.candidates.prewarmed", "seller route candidates prewarmed", {
356
398
  model: modelId,
@@ -358,6 +400,11 @@ export class TokenbuddyDaemon {
358
400
  protocol,
359
401
  paymentMethod,
360
402
  selectionMode: this.selectionMode,
403
+ sellerRoutingMode: routing.mode,
404
+ sellerRoutingScorer: routing.scorer,
405
+ routeSource: planned.source,
406
+ routeSourceReason: planned.sourceReason,
407
+ routeReason: planned.reason,
361
408
  sellerCount: routes.length,
362
409
  sellers: routes.map((route) => route.seller.id)
363
410
  });
@@ -394,20 +441,88 @@ export class TokenbuddyDaemon {
394
441
  */
395
442
  handleFailoverDecision(decision, context) {
396
443
  if (decision.action === "retry_same_seller") {
444
+ logger.warn("route.failover.retry_scheduled", "seller route retry scheduled", {
445
+ requestId: context.requestId,
446
+ sellerKey: context.sellerKey,
447
+ model: context.model,
448
+ endpoint: context.endpoint,
449
+ routeIndex: context.routeIndex,
450
+ routesRemaining: context.routesRemaining,
451
+ attempt: context.attempt,
452
+ nextAttempt: context.attempt + 1,
453
+ reason: decision.reason,
454
+ status: context.status,
455
+ retryDelayMs: decision.retryDelayMs
456
+ });
397
457
  return;
398
458
  }
399
459
  if (decision.action === "failover_next") {
400
460
  logger.warn("route.failover.triggered", "seller route failed over to backup candidate", {
461
+ requestId: context.requestId,
401
462
  sellerKey: context.sellerKey,
463
+ model: context.model,
402
464
  endpoint: context.endpoint,
403
465
  routeIndex: context.routeIndex,
466
+ nextRouteIndex: context.routeIndex + 1,
467
+ routesRemaining: context.routesRemaining,
468
+ attempt: context.attempt,
404
469
  reason: decision.reason,
405
470
  status: context.status,
406
471
  wastedCreditMicros: decision.wastedCreditMicros,
407
472
  freshPurchase: decision.freshPurchase,
408
473
  retryAttemptsBeforeFailover: decision.retryAttemptsBeforeFailover
409
474
  });
475
+ return;
410
476
  }
477
+ logger.warn("route.failover.terminal", "seller route failover reached terminal decision", {
478
+ requestId: context.requestId,
479
+ sellerKey: context.sellerKey,
480
+ model: context.model,
481
+ endpoint: context.endpoint,
482
+ routeIndex: context.routeIndex,
483
+ routesRemaining: context.routesRemaining,
484
+ attempt: context.attempt,
485
+ action: decision.action,
486
+ reason: decision.reason,
487
+ status: context.status
488
+ });
489
+ }
490
+ logPaymentProofResolved(route, proofSource, requestId) {
491
+ logger.info("purchase.payment_proof.resolved", "payment proof resolved for purchase completion", {
492
+ requestId,
493
+ sellerKey: route.seller.id,
494
+ model: route.modelId,
495
+ paymentMethod: route.paymentMethod,
496
+ proofSource,
497
+ proofPresent: true
498
+ });
499
+ }
500
+ logPurchaseLedgerRecorded(input) {
501
+ logger.info("purchase.ledger.recorded", "safe purchase ledger recorded", {
502
+ requestId: input.requestId,
503
+ sellerKey: input.sellerKey,
504
+ model: input.modelId,
505
+ purchaseId: input.purchaseId,
506
+ paymentMethod: input.paymentMethod,
507
+ ledgerStatus: input.status,
508
+ creditMicros: input.creditMicros,
509
+ currency: input.currency,
510
+ durationMs: input.durationMs
511
+ });
512
+ }
513
+ logTokenBalanceReconciled(route, requestId, settlement) {
514
+ logger.info("token.balance.reconciled", "seller token balance reconciled from settlement", {
515
+ requestId: settlement.requestId || requestId,
516
+ sellerKey: route.seller.id,
517
+ model: route.modelId,
518
+ remainingCreditMicros: settlement.remainingCreditMicros,
519
+ reservedMicros: settlement.reservedBalanceMicros ?? 0,
520
+ spentMicros: settlement.spentMicros ?? 0,
521
+ settledMicros: settlement.settledMicros,
522
+ settledUsdMicros: settlement.settledUsdMicros,
523
+ priceVersion: settlement.priceVersion,
524
+ balanceSource: "seller_settlement_summary"
525
+ });
411
526
  }
412
527
  async listSellerBackedModels() {
413
528
  const catalog = await discoverSellerBackedModels(this.config.sellerRegistryUrl);
@@ -455,6 +570,7 @@ export class TokenbuddyDaemon {
455
570
  spentMicros: settlement.spentMicros ?? 0,
456
571
  balanceSource: "seller_settlement_summary"
457
572
  });
573
+ this.logTokenBalanceReconciled(route, requestId, settlement);
458
574
  }
459
575
  const settledMicros = settlement?.settledMicros;
460
576
  this.tokenStore.recordInferenceLedger({
@@ -483,6 +599,11 @@ export class TokenbuddyDaemon {
483
599
  status: settlement ? "settled" : "estimated",
484
600
  estimatedMicros: usage.billedMicros,
485
601
  settledMicros,
602
+ settledUsdMicros: settlement?.settledUsdMicros,
603
+ billedMicros: settledMicros ?? usage.billedMicros,
604
+ promptTokens: usage.promptTokens,
605
+ completionTokens: usage.completionTokens,
606
+ balanceSnapshotMicros: settlement?.remainingCreditMicros,
486
607
  balanceSource: settlement ? "seller_authoritative" : "estimated"
487
608
  });
488
609
  }
@@ -541,19 +662,20 @@ export class TokenbuddyDaemon {
541
662
  return /insufficient funds/i.test(bodyText);
542
663
  }
543
664
  }
544
- async recoverFromInsufficientFunds(route, token) {
665
+ async recoverFromInsufficientFunds(route, token, requestId) {
545
666
  const sellerKey = route.seller.id;
546
667
  this.tokenStore.markTokenStale(sellerKey);
547
668
  const snapshot = await this.refreshSellerBalance(route, token, "seller_402_refresh");
548
669
  const rebuyMinBalanceMicros = this.tokenRebuyMinBalanceMicros();
549
670
  if (!snapshot || snapshot.availableMicros <= rebuyMinBalanceMicros) {
550
671
  logger.info("purchase.retry_after_402.started", "seller 402 triggered one-shot auto purchase retry", {
672
+ requestId,
551
673
  sellerKey,
552
674
  model: route.modelId,
553
675
  availableMicros: snapshot?.availableMicros ?? 0,
554
676
  rebuyMinBalanceMicros
555
677
  });
556
- return await this.getOrPurchaseToken(route);
678
+ return await this.getOrPurchaseToken(route, requestId);
557
679
  }
558
680
  const cached = this.tokenStore.getToken(sellerKey);
559
681
  return cached?.token || token;
@@ -586,16 +708,16 @@ export class TokenbuddyDaemon {
586
708
  * than this for a single seller; on expiry the request is aborted and
587
709
  * the route-failover controller can either retry the same seller with
588
710
  * a smaller body or fail over. Configurable via
589
- * `TB_PROXYD_REQUEST_DEADLINE_MS` (default 30s).
711
+ * `TB_PROXYD_REQUEST_DEADLINE_MS` (default 180s).
590
712
  */
591
713
  requestDeadlineMs() {
592
714
  const raw = process.env.TB_PROXYD_REQUEST_DEADLINE_MS;
593
715
  if (!raw) {
594
- return 30_000;
716
+ return 180_000;
595
717
  }
596
718
  const parsed = Number(raw);
597
719
  if (!Number.isInteger(parsed) || parsed < 1000) {
598
- return 30_000;
720
+ return 180_000;
599
721
  }
600
722
  return parsed;
601
723
  }
@@ -616,7 +738,7 @@ export class TokenbuddyDaemon {
616
738
  }
617
739
  return parsed;
618
740
  }
619
- async getOrPurchaseToken(route) {
741
+ async getOrPurchaseToken(route, requestId) {
620
742
  const sellerKey = route.seller.id;
621
743
  const sellerUrl = normalizeSellerUrl(route.seller);
622
744
  const { modelId, paymentMethod } = route;
@@ -634,6 +756,7 @@ export class TokenbuddyDaemon {
634
756
  const tokenStillFresh = Number.isFinite(expiresAtMs) && Date.now() + this.tokenExpirySafetyMarginMs() < expiresAtMs;
635
757
  if (cached && tokenStillFresh && cached.balanceMicros > rebuyMinBalanceMicros) {
636
758
  logger.info("token.cache.hit", "seller token cache hit", {
759
+ requestId,
637
760
  sellerKey,
638
761
  model: modelId,
639
762
  balanceMicros: cached.balanceMicros,
@@ -642,6 +765,7 @@ export class TokenbuddyDaemon {
642
765
  return cached.token;
643
766
  }
644
767
  logger.info("token.cache.miss", "seller token cache miss", {
768
+ requestId,
645
769
  sellerKey,
646
770
  model: modelId,
647
771
  balanceMicros: cached?.balanceMicros || 0,
@@ -652,6 +776,7 @@ export class TokenbuddyDaemon {
652
776
  const purchasePromise = this.activePurchases.get(purchaseKey);
653
777
  if (purchasePromise) {
654
778
  logger.info("purchase.lock.awaited", "parallel request awaiting active purchase", {
779
+ requestId,
655
780
  sellerKey,
656
781
  model: modelId
657
782
  });
@@ -661,6 +786,7 @@ export class TokenbuddyDaemon {
661
786
  const startedAt = Date.now();
662
787
  const amountUsdMicros = this.autoPurchaseAmountUsdMicros();
663
788
  logger.info("purchase.token.started", "seller token purchase started", {
789
+ requestId,
664
790
  sellerKey,
665
791
  model: modelId,
666
792
  paymentMethod,
@@ -668,6 +794,13 @@ export class TokenbuddyDaemon {
668
794
  });
669
795
  try {
670
796
  // 1. purchase/create
797
+ logger.info("purchase.create.started", "seller purchase create started", {
798
+ requestId,
799
+ sellerKey,
800
+ model: modelId,
801
+ paymentMethod,
802
+ amountUsdMicros
803
+ });
671
804
  const createRes = await fetch(`${sellerUrl}/purchase/create`, {
672
805
  method: "POST",
673
806
  headers: { "Content-Type": "application/json" },
@@ -684,6 +817,7 @@ export class TokenbuddyDaemon {
684
817
  logger.warn("purchase.create.failed", "seller purchase create failed", {
685
818
  sellerKey,
686
819
  model: modelId,
820
+ requestId,
687
821
  status: createRes.status,
688
822
  errorMessage: createData.error?.message || "purchase/create failed",
689
823
  durationMs: Date.now() - startedAt
@@ -705,12 +839,30 @@ export class TokenbuddyDaemon {
705
839
  expiresAt: createData.expiresAt || createData.expires_at
706
840
  });
707
841
  logger.info("purchase.create.succeeded", "seller purchase created", {
842
+ sellerKey,
843
+ model: modelId,
844
+ requestId,
845
+ purchaseId,
846
+ paymentMethod,
847
+ httpStatus: createRes.status,
848
+ purchaseStatus: createData.status || "pending",
849
+ creditMicros: createData.creditMicros ?? createData.credit_micros,
850
+ currency: createData.currency,
851
+ expiresAtPresent: Boolean(createData.expiresAt || createData.expires_at),
852
+ paymentReferencePresent: Boolean(createData.paymentReference || createData.payment_reference),
853
+ paymentInstructionsPresent: Boolean(createData.paymentInstructions || createData.payment_instructions),
854
+ quotePresent: Boolean(createData.quote),
855
+ durationMs: Date.now() - startedAt
856
+ });
857
+ const paymentProof = await this.resolvePaymentProof(route, createData, requestId);
858
+ logger.info("purchase.complete.started", "seller purchase complete started", {
859
+ requestId,
708
860
  sellerKey,
709
861
  model: modelId,
710
862
  purchaseId,
711
- status: createRes.status
863
+ paymentMethod,
864
+ durationMs: Date.now() - startedAt
712
865
  });
713
- const paymentProof = await this.resolvePaymentProof(route, createData);
714
866
  const completeRes = await fetch(`${sellerUrl}/purchase/complete`, {
715
867
  method: "POST",
716
868
  headers: { "Content-Type": "application/json" },
@@ -726,6 +878,7 @@ export class TokenbuddyDaemon {
726
878
  logger.warn("purchase.complete.failed", "seller purchase complete failed", {
727
879
  sellerKey,
728
880
  model: modelId,
881
+ requestId,
729
882
  purchaseId,
730
883
  status: completeRes.status,
731
884
  errorMessage: completeData.error?.message || "purchase/complete failed",
@@ -741,33 +894,51 @@ export class TokenbuddyDaemon {
741
894
  const creditMicros = completeData.creditMicros ?? completeData.credit_micros ?? createData.creditMicros ?? createData.credit_micros ?? 0;
742
895
  const currency = completeData.currency || createData.currency || "USD";
743
896
  const expiresAt = new Date(Date.now() + 86400 * 1000).toISOString();
897
+ const ledgerStatus = completeData.status || "funded";
744
898
  this.tokenStore.saveToken(sellerKey, token, tokenClass, creditMicros, expiresAt);
745
899
  this.tokenStore.recordPurchaseLedger({
746
900
  purchaseId,
747
901
  sellerKey,
748
902
  modelId,
749
903
  paymentMethod,
750
- status: completeData.status || "funded",
904
+ status: ledgerStatus,
751
905
  creditMicros,
752
906
  currency,
753
907
  paymentReference: completeData.paymentReference || completeData.payment_reference,
754
908
  completedAt: new Date().toISOString()
755
909
  });
910
+ this.logPurchaseLedgerRecorded({
911
+ requestId,
912
+ sellerKey,
913
+ modelId,
914
+ purchaseId,
915
+ paymentMethod,
916
+ status: ledgerStatus,
917
+ creditMicros,
918
+ currency,
919
+ durationMs: Date.now() - startedAt
920
+ });
756
921
  // v1.1: feed the credit tracker so the route-failover controller
757
922
  // knows the seller is inside the fresh-purchase window.
758
923
  this.creditTracker.recordPurchase(sellerKey, creditMicros, creditMicros);
759
924
  logger.info("purchase.token.succeeded", "seller token purchased", {
925
+ requestId,
760
926
  sellerKey,
761
927
  model: modelId,
762
928
  purchaseId,
929
+ paymentMethod,
763
930
  tokenClass,
764
931
  creditMicros,
932
+ currency,
933
+ ledgerStatus,
934
+ completeStatus: completeRes.status,
765
935
  durationMs: Date.now() - startedAt
766
936
  });
767
937
  return token;
768
938
  }
769
939
  catch (error) {
770
940
  logger.error("purchase.token.failed", "seller token purchase failed", {
941
+ requestId,
771
942
  sellerKey,
772
943
  model: modelId,
773
944
  errorMessage: error instanceof Error ? error.message : String(error),
@@ -782,7 +953,7 @@ export class TokenbuddyDaemon {
782
953
  this.activePurchases.set(purchaseKey, purchaseTask);
783
954
  return purchaseTask;
784
955
  }
785
- async resolvePaymentProof(route, createData) {
956
+ async resolvePaymentProof(route, createData, requestId) {
786
957
  if (route.paymentMethod === "mock") {
787
958
  return "mock-proof-data";
788
959
  }
@@ -791,19 +962,24 @@ export class TokenbuddyDaemon {
791
962
  }
792
963
  const proofCommand = process.env.TB_PROXYD_CLAWTIP_PROOF_COMMAND;
793
964
  if (proofCommand?.trim()) {
794
- return await this.runClawtipProofCommand(route, createData, proofCommand.trim());
965
+ const proof = await this.runClawtipProofCommand(route, createData, proofCommand.trim(), requestId);
966
+ this.logPaymentProofResolved(route, "command", requestId);
967
+ return proof;
795
968
  }
796
969
  const proofFile = process.env.TOKENBUDDY_CLAWTIP_PAYMENT_PROOF_FILE || process.env.TOKENBUDDY_CLAWTIP_PROOF_FILE;
797
970
  if (proofFile?.trim()) {
798
- return fs.readFileSync(proofFile.trim(), "utf8").trim();
971
+ const proof = fs.readFileSync(proofFile.trim(), "utf8").trim();
972
+ this.logPaymentProofResolved(route, "file", requestId);
973
+ return proof;
799
974
  }
800
975
  const proof = process.env.TOKENBUDDY_CLAWTIP_PAYMENT_PROOF;
801
976
  if (proof?.trim()) {
977
+ this.logPaymentProofResolved(route, "env", requestId);
802
978
  return proof.trim();
803
979
  }
804
980
  throw new Error("clawtip auto purchase requires TB_PROXYD_CLAWTIP_PROOF_COMMAND or a ClawTip proof env/file");
805
981
  }
806
- runClawtipProofCommand(route, createData, commandPath) {
982
+ runClawtipProofCommand(route, createData, commandPath, requestId) {
807
983
  const timeoutMs = this.clawtipProofTimeoutMs();
808
984
  const payload = JSON.stringify({
809
985
  sellerKey: route.seller.id,
@@ -814,6 +990,7 @@ export class TokenbuddyDaemon {
814
990
  quote: createData.quote
815
991
  });
816
992
  logger.info("purchase.clawtip_proof.started", "clawtip proof provider started", {
993
+ requestId,
817
994
  sellerKey: route.seller.id,
818
995
  model: route.modelId,
819
996
  timeoutMs
@@ -868,6 +1045,7 @@ export class TokenbuddyDaemon {
868
1045
  return;
869
1046
  }
870
1047
  logger.info("purchase.clawtip_proof.succeeded", "clawtip proof provider succeeded", {
1048
+ requestId,
871
1049
  sellerKey: route.seller.id,
872
1050
  model: route.modelId,
873
1051
  durationMs: Date.now() - startedAt
@@ -948,7 +1126,7 @@ export class TokenbuddyDaemon {
948
1126
  model: modelId,
949
1127
  endpoint,
950
1128
  stream: Boolean(body.stream),
951
- upstreamBody
1129
+ bodySummary: summarizeProxyBody(upstreamBody)
952
1130
  });
953
1131
  // v1.1 §17.5: refuse to auto-purchase once the session budget is
954
1132
  // exhausted. The seller is treated as "no auto-purchase available"
@@ -969,7 +1147,7 @@ export class TokenbuddyDaemon {
969
1147
  // seller; transfer leftover to wasted and fail over immediately.
970
1148
  let token;
971
1149
  try {
972
- token = await this.getOrPurchaseToken(route);
1150
+ token = await this.getOrPurchaseToken(route, requestId);
973
1151
  }
974
1152
  catch (purchaseError) {
975
1153
  logger.warn("purchase.failed", "seller auto-purchase failed; failing over without retry", {
@@ -979,12 +1157,25 @@ export class TokenbuddyDaemon {
979
1157
  endpoint,
980
1158
  errorMessage: this.failoverErrorMessage(purchaseError)
981
1159
  });
982
- this.routeFailover.decide({
1160
+ const decision = this.routeFailover.decide({
983
1161
  sellerId: sellerKey,
984
1162
  errorKind: "deadline",
985
1163
  errorMessage: this.failoverErrorMessage(purchaseError),
986
1164
  attempt
987
1165
  }, routes.length - routeIndex);
1166
+ logger.warn("route.failover.triggered", "seller route failed over after purchase failure", {
1167
+ requestId,
1168
+ sellerKey,
1169
+ model: modelId,
1170
+ endpoint,
1171
+ routeIndex,
1172
+ nextRouteIndex: routeIndex + 1,
1173
+ routesRemaining: routes.length - routeIndex,
1174
+ attempt,
1175
+ reason: "purchase_failed",
1176
+ controllerReason: decision.reason,
1177
+ controllerAction: decision.action
1178
+ });
988
1179
  lastError = purchaseError;
989
1180
  break;
990
1181
  }
@@ -994,9 +1185,9 @@ export class TokenbuddyDaemon {
994
1185
  // the `X-TokenBuddy-Deadline-Ms` header (PR-6) can propagate
995
1186
  // it to their own upstream fetch via the same signal.
996
1187
  const deadlineMs = this.requestDeadlineMs();
997
- const requestAc = new AbortController();
998
- const requestTimer = setTimeout(() => requestAc.abort(new Error("buyer deadline exceeded")), deadlineMs);
999
1188
  const sendSellerRequest = async (token) => {
1189
+ const requestAc = new AbortController();
1190
+ const requestTimer = setTimeout(() => requestAc.abort(new Error("buyer deadline exceeded")), deadlineMs);
1000
1191
  const headers = {
1001
1192
  "Content-Type": "application/json",
1002
1193
  "Authorization": `Bearer ${token}`,
@@ -1004,18 +1195,23 @@ export class TokenbuddyDaemon {
1004
1195
  "Idempotency-Key": idempotencyKey
1005
1196
  };
1006
1197
  headers["X-TokenBuddy-Deadline-Ms"] = String(deadlineMs);
1007
- return fetch(`${sellerUrl}${endpoint}`, {
1008
- method: "POST",
1009
- headers,
1010
- body: JSON.stringify(upstreamBody),
1011
- signal: requestAc.signal
1012
- });
1198
+ try {
1199
+ return await fetch(`${sellerUrl}${endpoint}`, {
1200
+ method: "POST",
1201
+ headers,
1202
+ body: JSON.stringify(upstreamBody),
1203
+ signal: requestAc.signal
1204
+ });
1205
+ }
1206
+ finally {
1207
+ clearTimeout(requestTimer);
1208
+ }
1013
1209
  };
1014
1210
  let upstreamResponse = await sendSellerRequest(token);
1015
1211
  if (!upstreamResponse.ok) {
1016
1212
  const errorBody = await upstreamResponse.text();
1017
1213
  if (this.isInsufficientFundsResponse(upstreamResponse.status, errorBody)) {
1018
- token = await this.recoverFromInsufficientFunds(route, token);
1214
+ token = await this.recoverFromInsufficientFunds(route, token, requestId);
1019
1215
  upstreamResponse = await sendSellerRequest(token);
1020
1216
  if (upstreamResponse.ok) {
1021
1217
  logger.info("proxy.retry_after_402.succeeded", "seller request succeeded after one-shot auto purchase retry", {
@@ -1059,7 +1255,16 @@ export class TokenbuddyDaemon {
1059
1255
  errorMessage: errorBody,
1060
1256
  attempt
1061
1257
  }, routes.length - routeIndex);
1062
- this.handleFailoverDecision(decision, { sellerKey, endpoint, routeIndex });
1258
+ this.handleFailoverDecision(decision, {
1259
+ requestId,
1260
+ sellerKey,
1261
+ model: modelId,
1262
+ endpoint,
1263
+ routeIndex,
1264
+ routesRemaining: routes.length - routeIndex,
1265
+ attempt,
1266
+ status: upstreamResponse.status
1267
+ });
1063
1268
  if (decision.action === "fail_fast" || decision.action === "abort") {
1064
1269
  this.copyUpstreamHeaders(upstreamResponse, res);
1065
1270
  res.status(upstreamResponse.status);
@@ -1149,7 +1354,16 @@ export class TokenbuddyDaemon {
1149
1354
  errorMessage: this.failoverErrorMessage(routeError),
1150
1355
  attempt
1151
1356
  }, routes.length - routeIndex);
1152
- this.handleFailoverDecision(decision, { sellerKey, endpoint, routeIndex, reason: "exception" });
1357
+ this.handleFailoverDecision(decision, {
1358
+ requestId,
1359
+ sellerKey,
1360
+ model: modelId,
1361
+ endpoint,
1362
+ routeIndex,
1363
+ routesRemaining: routes.length - routeIndex,
1364
+ attempt,
1365
+ reason: "exception"
1366
+ });
1153
1367
  logger.warn("proxy.route.failed", "seller route failed before response", {
1154
1368
  requestId,
1155
1369
  sellerKey,
@@ -1371,7 +1585,6 @@ export class TokenbuddyDaemon {
1371
1585
  proxyUrl: String(req.body?.proxyUrl || ""),
1372
1586
  model: typeof req.body?.model === "string" ? req.body.model : undefined,
1373
1587
  providerSelections: req.body?.providerSelections,
1374
- sellerRouting: req.body?.sellerRouting,
1375
1588
  home: typeof req.body?.home === "string" ? req.body.home : undefined
1376
1589
  });
1377
1590
  logger.info("provider.install.previewed", "provider install previewed", {
@@ -1400,7 +1613,6 @@ export class TokenbuddyDaemon {
1400
1613
  proxyUrl: String(req.body?.proxyUrl || ""),
1401
1614
  model: typeof req.body?.model === "string" ? req.body.model : undefined,
1402
1615
  providerSelections: req.body?.providerSelections,
1403
- sellerRouting: req.body?.sellerRouting,
1404
1616
  home: typeof req.body?.home === "string" ? req.body.home : undefined
1405
1617
  }, this.tokenStore);
1406
1618
  logger.info("provider.install.applied", "provider install applied", {
@@ -1490,7 +1702,10 @@ export class TokenbuddyDaemon {
1490
1702
  proxyPort: this.config.proxyPort,
1491
1703
  dbPath: this.config.dbPath,
1492
1704
  sellerRegistryUrl: this.config.sellerRegistryUrl,
1493
- selectionMode: this.selectionMode
1705
+ selectionMode: this.selectionMode,
1706
+ sellerRoutingMode: this.sellerRouting.mode,
1707
+ sellerRoutingScorer: this.sellerRouting.scorer,
1708
+ selectedSellerId: this.selectedSellerId
1494
1709
  });
1495
1710
  // v1.2 §18.5: kick off the on-demand prewarm pipeline. The startup
1496
1711
  // sweep runs after the configured jitter window (5-10s by default);
@@ -1527,7 +1742,11 @@ export class TokenbuddyDaemon {
1527
1742
  focusSet: focusSet.slice(0, 20)
1528
1743
  });
1529
1744
  try {
1530
- await this.prewarmScheduler.runStartupPrewarm(focusSet);
1745
+ await this.fetchRegistry();
1746
+ await this.prewarmScheduler.runStartupPrewarm(focusSet.map((modelId) => ({
1747
+ modelId,
1748
+ protocol: this.resolvePrewarmProtocol(modelId)
1749
+ })));
1531
1750
  }
1532
1751
  catch (err) {
1533
1752
  logger.warn("prewarm.startup.failed", "startup prewarm sweep failed", {
@@ -1535,6 +1754,14 @@ export class TokenbuddyDaemon {
1535
1754
  });
1536
1755
  }
1537
1756
  }
1757
+ resolvePrewarmProtocol(modelId) {
1758
+ for (const protocol of ["chat_completions", "messages", "responses"]) {
1759
+ if (this.modelIndex.sellersFor(modelId, { protocol, paymentMethod: "clawtip" }).length > 0) {
1760
+ return protocol;
1761
+ }
1762
+ }
1763
+ return undefined;
1764
+ }
1538
1765
  stop() {
1539
1766
  if (this.controlServer)
1540
1767
  this.controlServer.close();