@tokenbuddy/tokenbuddy 1.0.12 → 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 -19
  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 -27
  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 +446 -34
  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
package/src/daemon.ts CHANGED
@@ -23,7 +23,6 @@ import {
23
23
  type RegistrySeller,
24
24
  type SellerManifest,
25
25
  type SellerRegistryDocument,
26
- type SellerRoutingPreference,
27
26
  } from "./seller-catalog.js";
28
27
  import { ModelIndex } from "./model-index.js";
29
28
  import { PrewarmCache } from "./prewarm-cache.js";
@@ -32,22 +31,39 @@ import { SellerPool, type FailureKind } from "./seller-pool.js";
32
31
  import { RouteFailover, type FailoverDecision, type RouteCandidate } from "./route-failover.js";
33
32
  import { PrewarmScheduler, type SellerProber } from "./prewarm-scheduler.js";
34
33
  import type { PoolEntry } from "./seller-pool.js";
34
+ import { planSellerRouteSet } from "./seller-route-planner.js";
35
+ import {
36
+ mergeSellerRoutingConfig,
37
+ ROUTING_CONFIG_KEY,
38
+ type BuyerSellerRoutingConfig
39
+ } from "./seller-routing-config.js";
35
40
 
36
41
  const logger = createModuleLogger("tb-proxyd");
37
42
  const PROXY_JSON_BODY_LIMIT = "10mb";
38
43
 
44
+ /**
45
+ * buyer 端守护进程(`tb-proxyd`)的配置。
46
+ * 由 `startDaemon(config)` 注入;字段缺省时由内部补齐。
47
+ */
39
48
  export interface DaemonConfig {
49
+ /** 本地控制端口(如 healthz / 控制接口) */
40
50
  controlPort: number;
51
+ /** 对 buyer 客户端暴露的反向代理端口(OpenAI / Anthropic 协议入口) */
41
52
  proxyPort: number;
53
+ /** buyer 端 SQLite 路径(用于 store、credit tracker、prewarm cache) */
42
54
  dbPath: string;
55
+ /** seller 注册表拉取 URL(通常是 wallet-bootstrap 的 `/registry/sellers`) */
43
56
  sellerRegistryUrl: string;
44
- selectionMode?: "auto" | "manual";
45
- selectedSellerId?: string;
46
- // v1.2 §18.4: focus-set override. When omitted, the daemon derives the
47
- // focus set from the BuyerStore's historical model usage and the
48
- // `TB_BUYER_WARMUP_MODELS` env var (comma-separated).
57
+ /** 路由策略覆盖(与 `TB_SELLER_ROUTING_*` env 合并) */
58
+ sellerRouting?: BuyerSellerRoutingConfig;
59
+ /**
60
+ * v1.2 §18.4 预热 focus-set 覆盖。
61
+ * 缺省时由 BuyerStore 历史模型使用情况 + `TB_BUYER_WARMUP_MODELS` env 推导。
62
+ */
49
63
  warmupModels?: string[];
64
+ /** 预热模型目录刷新间隔(秒) */
50
65
  warmupRefreshIntervalSecs?: number;
66
+ /** 预热探测超时(毫秒) */
51
67
  warmupProbeTimeoutMs?: number;
52
68
  }
53
69
 
@@ -66,6 +82,18 @@ interface UsageSummary {
66
82
  billedMicros: number;
67
83
  }
68
84
 
85
+ interface ProxyBodySummary {
86
+ bodyType: string;
87
+ messageCount?: number;
88
+ inputItemCount?: number;
89
+ toolCount?: number;
90
+ hasMessages?: boolean;
91
+ hasInput?: boolean;
92
+ hasTools?: boolean;
93
+ maxTokensPresent?: boolean;
94
+ temperaturePresent?: boolean;
95
+ }
96
+
69
97
  interface SellerSettlementSummary {
70
98
  requestId: string;
71
99
  settledMicros: number;
@@ -105,11 +133,13 @@ class SellerSettlementStreamExtractor {
105
133
  return blocks
106
134
  .map((block) => this.processBlock(block))
107
135
  .filter((block) => block.length > 0)
108
- .join("\n\n");
136
+ .map((block) => `${block}\n\n`)
137
+ .join("");
109
138
  }
110
139
 
111
140
  public finish(): { downstream: string; settlement: SellerSettlementSummary | undefined } {
112
- const downstream = this.pending.trim() ? this.processBlock(this.pending) : "";
141
+ const processed = this.pending.trim() ? this.processBlock(this.pending) : "";
142
+ const downstream = processed ? processed : "";
113
143
  this.pending = "";
114
144
  return { downstream, settlement: this.settlement };
115
145
  }
@@ -171,6 +201,42 @@ function parseSellerSettlementObject(raw: string): SellerSettlementSummary | und
171
201
  }
172
202
  }
173
203
 
204
+ function arrayLength(value: unknown): number | undefined {
205
+ return Array.isArray(value) ? value.length : undefined;
206
+ }
207
+
208
+ function summarizeProxyBody(body: unknown): ProxyBodySummary {
209
+ if (!body || typeof body !== "object") {
210
+ return { bodyType: typeof body };
211
+ }
212
+
213
+ const data = body as Record<string, unknown>;
214
+ const messages = arrayLength(data.messages);
215
+ const input = arrayLength(data.input);
216
+ const tools = arrayLength(data.tools);
217
+ return {
218
+ bodyType: "object",
219
+ messageCount: messages,
220
+ inputItemCount: input,
221
+ toolCount: tools,
222
+ hasMessages: messages !== undefined,
223
+ hasInput: data.input !== undefined,
224
+ hasTools: tools !== undefined,
225
+ maxTokensPresent: data.max_tokens !== undefined || data.maxTokens !== undefined,
226
+ temperaturePresent: data.temperature !== undefined
227
+ };
228
+ }
229
+
230
+ function reorderDefaultSellerFirst(sellers: RegistrySeller[], defaultSellerId: string | undefined): RegistrySeller[] {
231
+ if (!defaultSellerId) {
232
+ return sellers;
233
+ }
234
+ return [
235
+ ...sellers.filter((seller) => seller.id === defaultSellerId),
236
+ ...sellers.filter((seller) => seller.id !== defaultSellerId)
237
+ ];
238
+ }
239
+
174
240
  interface PurchaseCreateResponse {
175
241
  purchaseId?: string;
176
242
  purchase_id?: string;
@@ -195,6 +261,12 @@ interface PurchaseCompleteResponse extends PurchaseCreateResponse {
195
261
  token_class?: string;
196
262
  }
197
263
 
264
+ /**
265
+ * buyer 端守护进程。
266
+ * 负责启动两个 Express 服务:控制接口(healthz + 控制路由)+ 反向代理(OpenAI / Anthropic 协议入口)。
267
+ * 同时跑后台任务:seller catalog 周期拉取、model-index 刷新、prewarm scheduler、credit tracker。
268
+ * 推荐用 `startDaemon(config)` 启动 / `stopDaemon(daemon)` 优雅关闭,避免直接管理生命周期。
269
+ */
198
270
  export class TokenbuddyDaemon {
199
271
  private config: DaemonConfig;
200
272
  private tokenStore: BuyerStore;
@@ -202,6 +274,7 @@ export class TokenbuddyDaemon {
202
274
  private proxyServer?: any;
203
275
  private selectionMode: "auto" | "manual";
204
276
  private selectedSellerId?: string;
277
+ private sellerRouting: BuyerSellerRoutingConfig;
205
278
 
206
279
  private activePurchases = new Map<string, Promise<string>>();
207
280
 
@@ -227,16 +300,15 @@ export class TokenbuddyDaemon {
227
300
 
228
301
  constructor(config: DaemonConfig) {
229
302
  this.tokenStore = new BuyerStore({ dbPath: config.dbPath });
230
- const routingPreference =
231
- this.tokenStore.getDaemonRuntimeConfig<SellerRoutingPreference>("routing")
232
- ?.config;
303
+ const storedRouting = this.tokenStore.getDaemonRuntimeConfig<unknown>(ROUTING_CONFIG_KEY)
304
+ ?.config;
233
305
  this.config = config;
234
- this.selectionMode =
235
- config.selectionMode ||
236
- (routingPreference?.mode === "fixed" ? "manual" : routingPreference?.mode) ||
237
- "auto";
238
- this.selectedSellerId =
239
- config.selectedSellerId || routingPreference?.sellerId;
306
+ this.sellerRouting = mergeSellerRoutingConfig(
307
+ storedRouting,
308
+ config.sellerRouting
309
+ );
310
+ this.selectionMode = this.sellerRouting.mode === "fullAuto" ? "auto" : "manual";
311
+ this.selectedSellerId = this.sellerRouting.mode === "fixed" ? this.sellerRouting.sellerId : undefined;
240
312
  // v1.2 §18.5: scheduler is created here (not in the field initializer)
241
313
  // because it needs the config-derived prober + idle interval.
242
314
  Object.assign(this, {
@@ -253,7 +325,7 @@ export class TokenbuddyDaemon {
253
325
  return async (seller, signal) => {
254
326
  try {
255
327
  const ac = new AbortController();
256
- const timer = setTimeout(() => ac.abort(new Error("healthz timeout")), timeoutMs);
328
+ const timer = setTimeout(() => ac.abort(new Error("health timeout")), timeoutMs);
257
329
  if (signal) {
258
330
  if (signal.aborted) {
259
331
  ac.abort(signal.reason);
@@ -262,10 +334,10 @@ export class TokenbuddyDaemon {
262
334
  }
263
335
  }
264
336
  const startedAt = Date.now();
265
- const res = await fetch(`${seller.url.replace(/\/+$/, "")}/healthz`, { signal: ac.signal });
337
+ const res = await fetch(`${seller.url.replace(/\/+$/, "")}/health`, { signal: ac.signal });
266
338
  clearTimeout(timer);
267
339
  if (!res.ok) {
268
- return { ok: false, latencyMs: Date.now() - startedAt, httpStatus: res.status, errorMessage: `healthz returned ${res.status}` };
340
+ return { ok: false, latencyMs: Date.now() - startedAt, httpStatus: res.status, errorMessage: `health returned ${res.status}` };
269
341
  }
270
342
  return { ok: true, latencyMs: Date.now() - startedAt, httpStatus: res.status };
271
343
  } catch (err) {
@@ -325,14 +397,15 @@ export class TokenbuddyDaemon {
325
397
  }
326
398
 
327
399
  private runtimeSummary() {
328
- const sellerRoutingMode = this.selectedSellerId ? "fixed" : this.selectionMode;
329
400
  return {
330
401
  status: "running",
331
402
  pid: process.pid,
332
403
  controlPort: this.activeControlPort(),
333
404
  proxyPort: this.activeProxyPort(),
334
405
  selectionMode: this.selectionMode,
335
- sellerRoutingMode,
406
+ sellerRoutingMode: this.sellerRouting.mode,
407
+ sellerRoutingScorer: this.sellerRouting.scorer,
408
+ sellerRouting: this.sellerRouting,
336
409
  selectedSellerId: this.selectedSellerId,
337
410
  dbPath: this.config.dbPath,
338
411
  sellerRegistryUrl: this.config.sellerRegistryUrl,
@@ -446,32 +519,35 @@ export class TokenbuddyDaemon {
446
519
  // pulling `models` directly off the registry entries.
447
520
  const registry = await this.fetchRegistry();
448
521
 
449
- const indexCandidates = this.modelIndex.sellersFor(modelId, { protocol, paymentMethod });
450
- let ordered = indexCandidates;
451
- if (this.selectionMode === "manual" && this.selectedSellerId) {
452
- ordered = indexCandidates.filter((seller) => seller.id === this.selectedSellerId);
453
- } else if (this.selectionMode === "manual" && registry.defaultSeller) {
454
- ordered = indexCandidates.filter((seller) => seller.id === registry.defaultSeller);
455
- } else if (registry.defaultSeller) {
456
- // auto mode: default first, then backups in registry order
457
- ordered = [
458
- ...indexCandidates.filter((seller) => seller.id === registry.defaultSeller),
459
- ...indexCandidates.filter((seller) => seller.id !== registry.defaultSeller)
460
- ];
461
- }
522
+ const routing = this.sellerRouting;
523
+ const registrySellers = reorderDefaultSellerFirst(registry.sellers, registry.defaultSeller);
524
+ const poolById = new Map(this.sellerPool.snapshot().map((entry) => [entry.sellerId, entry]));
525
+ const planned = planSellerRouteSet({
526
+ modelId,
527
+ protocol,
528
+ paymentMethod,
529
+ registrySellers,
530
+ routing,
531
+ prewarmCandidates: this.prewarmCache.get(modelId, protocol, paymentMethod)?.candidates,
532
+ sellerMetrics: Array.from(poolById.values()).map((entry) => ({
533
+ sellerId: entry.sellerId,
534
+ healthScore: entry.healthScore,
535
+ avgLatencyMs: entry.avgLatencyMs,
536
+ circuit: entry.circuit
537
+ }))
538
+ });
462
539
 
463
- if (ordered.length === 0) {
540
+ if (planned.routes.length === 0) {
464
541
  throw new Error(`no compatible seller for ${endpoint} model ${modelId}`);
465
542
  }
466
543
 
467
- const poolById = new Map(this.sellerPool.snapshot().map((entry) => [entry.sellerId, entry]));
468
- const routes: SellerRoute[] = ordered.map((seller) => ({
469
- seller,
544
+ const routes: SellerRoute[] = planned.routes.map((route) => ({
545
+ seller: route.seller,
470
546
  manifest: null,
471
547
  protocol,
472
548
  modelId,
473
549
  paymentMethod,
474
- poolEntry: poolById.get(seller.id)
550
+ poolEntry: poolById.get(route.seller.id)
475
551
  }));
476
552
 
477
553
  logger.info("route.candidates.prewarmed", "seller route candidates prewarmed", {
@@ -480,6 +556,11 @@ export class TokenbuddyDaemon {
480
556
  protocol,
481
557
  paymentMethod,
482
558
  selectionMode: this.selectionMode,
559
+ sellerRoutingMode: routing.mode,
560
+ sellerRoutingScorer: routing.scorer,
561
+ routeSource: planned.source,
562
+ routeSourceReason: planned.sourceReason,
563
+ routeReason: planned.reason,
483
564
  sellerCount: routes.length,
484
565
  sellers: routes.map((route) => route.seller.id)
485
566
  });
@@ -519,23 +600,118 @@ export class TokenbuddyDaemon {
519
600
  */
520
601
  private handleFailoverDecision(
521
602
  decision: FailoverDecision,
522
- context: { sellerKey: string; endpoint: string; routeIndex: number; status?: number; reason?: string }
603
+ context: {
604
+ requestId: string;
605
+ sellerKey: string;
606
+ model: string;
607
+ endpoint: string;
608
+ routeIndex: number;
609
+ routesRemaining: number;
610
+ attempt: number;
611
+ status?: number;
612
+ reason?: string;
613
+ }
523
614
  ): void {
524
615
  if (decision.action === "retry_same_seller") {
616
+ logger.warn("route.failover.retry_scheduled", "seller route retry scheduled", {
617
+ requestId: context.requestId,
618
+ sellerKey: context.sellerKey,
619
+ model: context.model,
620
+ endpoint: context.endpoint,
621
+ routeIndex: context.routeIndex,
622
+ routesRemaining: context.routesRemaining,
623
+ attempt: context.attempt,
624
+ nextAttempt: context.attempt + 1,
625
+ reason: decision.reason,
626
+ status: context.status,
627
+ retryDelayMs: decision.retryDelayMs
628
+ });
525
629
  return;
526
630
  }
527
631
  if (decision.action === "failover_next") {
528
632
  logger.warn("route.failover.triggered", "seller route failed over to backup candidate", {
633
+ requestId: context.requestId,
529
634
  sellerKey: context.sellerKey,
635
+ model: context.model,
530
636
  endpoint: context.endpoint,
531
637
  routeIndex: context.routeIndex,
638
+ nextRouteIndex: context.routeIndex + 1,
639
+ routesRemaining: context.routesRemaining,
640
+ attempt: context.attempt,
532
641
  reason: decision.reason,
533
642
  status: context.status,
534
643
  wastedCreditMicros: decision.wastedCreditMicros,
535
644
  freshPurchase: decision.freshPurchase,
536
645
  retryAttemptsBeforeFailover: decision.retryAttemptsBeforeFailover
537
646
  });
647
+ return;
538
648
  }
649
+ logger.warn("route.failover.terminal", "seller route failover reached terminal decision", {
650
+ requestId: context.requestId,
651
+ sellerKey: context.sellerKey,
652
+ model: context.model,
653
+ endpoint: context.endpoint,
654
+ routeIndex: context.routeIndex,
655
+ routesRemaining: context.routesRemaining,
656
+ attempt: context.attempt,
657
+ action: decision.action,
658
+ reason: decision.reason,
659
+ status: context.status
660
+ });
661
+ }
662
+
663
+ private logPaymentProofResolved(route: SellerRoute, proofSource: "command" | "file" | "env", requestId?: string): void {
664
+ logger.info("purchase.payment_proof.resolved", "payment proof resolved for purchase completion", {
665
+ requestId,
666
+ sellerKey: route.seller.id,
667
+ model: route.modelId,
668
+ paymentMethod: route.paymentMethod,
669
+ proofSource,
670
+ proofPresent: true
671
+ });
672
+ }
673
+
674
+ private logPurchaseLedgerRecorded(input: {
675
+ requestId?: string;
676
+ sellerKey: string;
677
+ modelId: string;
678
+ purchaseId: string;
679
+ paymentMethod: string;
680
+ status: string;
681
+ creditMicros: number;
682
+ currency: string;
683
+ durationMs: number;
684
+ }): void {
685
+ logger.info("purchase.ledger.recorded", "safe purchase ledger recorded", {
686
+ requestId: input.requestId,
687
+ sellerKey: input.sellerKey,
688
+ model: input.modelId,
689
+ purchaseId: input.purchaseId,
690
+ paymentMethod: input.paymentMethod,
691
+ ledgerStatus: input.status,
692
+ creditMicros: input.creditMicros,
693
+ currency: input.currency,
694
+ durationMs: input.durationMs
695
+ });
696
+ }
697
+
698
+ private logTokenBalanceReconciled(
699
+ route: SellerRoute,
700
+ requestId: string,
701
+ settlement: SellerSettlementSummary
702
+ ): void {
703
+ logger.info("token.balance.reconciled", "seller token balance reconciled from settlement", {
704
+ requestId: settlement.requestId || requestId,
705
+ sellerKey: route.seller.id,
706
+ model: route.modelId,
707
+ remainingCreditMicros: settlement.remainingCreditMicros,
708
+ reservedMicros: settlement.reservedBalanceMicros ?? 0,
709
+ spentMicros: settlement.spentMicros ?? 0,
710
+ settledMicros: settlement.settledMicros,
711
+ settledUsdMicros: settlement.settledUsdMicros,
712
+ priceVersion: settlement.priceVersion,
713
+ balanceSource: "seller_settlement_summary"
714
+ });
539
715
  }
540
716
 
541
717
  private async listSellerBackedModels(): Promise<{
@@ -604,6 +780,7 @@ export class TokenbuddyDaemon {
604
780
  spentMicros: settlement.spentMicros ?? 0,
605
781
  balanceSource: "seller_settlement_summary"
606
782
  });
783
+ this.logTokenBalanceReconciled(route, requestId, settlement);
607
784
  }
608
785
 
609
786
  const settledMicros = settlement?.settledMicros;
@@ -633,6 +810,11 @@ export class TokenbuddyDaemon {
633
810
  status: settlement ? "settled" : "estimated",
634
811
  estimatedMicros: usage.billedMicros,
635
812
  settledMicros,
813
+ settledUsdMicros: settlement?.settledUsdMicros,
814
+ billedMicros: settledMicros ?? usage.billedMicros,
815
+ promptTokens: usage.promptTokens,
816
+ completionTokens: usage.completionTokens,
817
+ balanceSnapshotMicros: settlement?.remainingCreditMicros,
636
818
  balanceSource: settlement ? "seller_authoritative" : "estimated"
637
819
  });
638
820
  }
@@ -693,19 +875,20 @@ export class TokenbuddyDaemon {
693
875
  }
694
876
  }
695
877
 
696
- private async recoverFromInsufficientFunds(route: SellerRoute, token: string): Promise<string> {
878
+ private async recoverFromInsufficientFunds(route: SellerRoute, token: string, requestId?: string): Promise<string> {
697
879
  const sellerKey = route.seller.id;
698
880
  this.tokenStore.markTokenStale(sellerKey);
699
881
  const snapshot = await this.refreshSellerBalance(route, token, "seller_402_refresh");
700
882
  const rebuyMinBalanceMicros = this.tokenRebuyMinBalanceMicros();
701
883
  if (!snapshot || snapshot.availableMicros <= rebuyMinBalanceMicros) {
702
884
  logger.info("purchase.retry_after_402.started", "seller 402 triggered one-shot auto purchase retry", {
885
+ requestId,
703
886
  sellerKey,
704
887
  model: route.modelId,
705
888
  availableMicros: snapshot?.availableMicros ?? 0,
706
889
  rebuyMinBalanceMicros
707
890
  });
708
- return await this.getOrPurchaseToken(route);
891
+ return await this.getOrPurchaseToken(route, requestId);
709
892
  }
710
893
  const cached = this.tokenStore.getToken(sellerKey);
711
894
  return cached?.token || token;
@@ -742,16 +925,16 @@ export class TokenbuddyDaemon {
742
925
  * than this for a single seller; on expiry the request is aborted and
743
926
  * the route-failover controller can either retry the same seller with
744
927
  * a smaller body or fail over. Configurable via
745
- * `TB_PROXYD_REQUEST_DEADLINE_MS` (default 30s).
928
+ * `TB_PROXYD_REQUEST_DEADLINE_MS` (default 180s).
746
929
  */
747
930
  private requestDeadlineMs(): number {
748
931
  const raw = process.env.TB_PROXYD_REQUEST_DEADLINE_MS;
749
932
  if (!raw) {
750
- return 30_000;
933
+ return 180_000;
751
934
  }
752
935
  const parsed = Number(raw);
753
936
  if (!Number.isInteger(parsed) || parsed < 1000) {
754
- return 30_000;
937
+ return 180_000;
755
938
  }
756
939
  return parsed;
757
940
  }
@@ -774,7 +957,7 @@ export class TokenbuddyDaemon {
774
957
  return parsed;
775
958
  }
776
959
 
777
- private async getOrPurchaseToken(route: SellerRoute): Promise<string> {
960
+ private async getOrPurchaseToken(route: SellerRoute, requestId?: string): Promise<string> {
778
961
  const sellerKey = route.seller.id;
779
962
  const sellerUrl = normalizeSellerUrl(route.seller);
780
963
  const { modelId, paymentMethod } = route;
@@ -792,6 +975,7 @@ export class TokenbuddyDaemon {
792
975
  const tokenStillFresh = Number.isFinite(expiresAtMs) && Date.now() + this.tokenExpirySafetyMarginMs() < expiresAtMs;
793
976
  if (cached && tokenStillFresh && cached.balanceMicros > rebuyMinBalanceMicros) {
794
977
  logger.info("token.cache.hit", "seller token cache hit", {
978
+ requestId,
795
979
  sellerKey,
796
980
  model: modelId,
797
981
  balanceMicros: cached.balanceMicros,
@@ -800,6 +984,7 @@ export class TokenbuddyDaemon {
800
984
  return cached.token;
801
985
  }
802
986
  logger.info("token.cache.miss", "seller token cache miss", {
987
+ requestId,
803
988
  sellerKey,
804
989
  model: modelId,
805
990
  balanceMicros: cached?.balanceMicros || 0,
@@ -811,6 +996,7 @@ export class TokenbuddyDaemon {
811
996
  const purchasePromise = this.activePurchases.get(purchaseKey);
812
997
  if (purchasePromise) {
813
998
  logger.info("purchase.lock.awaited", "parallel request awaiting active purchase", {
999
+ requestId,
814
1000
  sellerKey,
815
1001
  model: modelId
816
1002
  });
@@ -821,6 +1007,7 @@ export class TokenbuddyDaemon {
821
1007
  const startedAt = Date.now();
822
1008
  const amountUsdMicros = this.autoPurchaseAmountUsdMicros();
823
1009
  logger.info("purchase.token.started", "seller token purchase started", {
1010
+ requestId,
824
1011
  sellerKey,
825
1012
  model: modelId,
826
1013
  paymentMethod,
@@ -828,6 +1015,13 @@ export class TokenbuddyDaemon {
828
1015
  });
829
1016
  try {
830
1017
  // 1. purchase/create
1018
+ logger.info("purchase.create.started", "seller purchase create started", {
1019
+ requestId,
1020
+ sellerKey,
1021
+ model: modelId,
1022
+ paymentMethod,
1023
+ amountUsdMicros
1024
+ });
831
1025
  const createRes = await fetch(`${sellerUrl}/purchase/create`, {
832
1026
  method: "POST",
833
1027
  headers: { "Content-Type": "application/json" },
@@ -844,6 +1038,7 @@ export class TokenbuddyDaemon {
844
1038
  logger.warn("purchase.create.failed", "seller purchase create failed", {
845
1039
  sellerKey,
846
1040
  model: modelId,
1041
+ requestId,
847
1042
  status: createRes.status,
848
1043
  errorMessage: createData.error?.message || "purchase/create failed",
849
1044
  durationMs: Date.now() - startedAt
@@ -867,11 +1062,29 @@ export class TokenbuddyDaemon {
867
1062
  logger.info("purchase.create.succeeded", "seller purchase created", {
868
1063
  sellerKey,
869
1064
  model: modelId,
1065
+ requestId,
870
1066
  purchaseId,
871
- status: createRes.status
1067
+ paymentMethod,
1068
+ httpStatus: createRes.status,
1069
+ purchaseStatus: createData.status || "pending",
1070
+ creditMicros: createData.creditMicros ?? createData.credit_micros,
1071
+ currency: createData.currency,
1072
+ expiresAtPresent: Boolean(createData.expiresAt || createData.expires_at),
1073
+ paymentReferencePresent: Boolean(createData.paymentReference || createData.payment_reference),
1074
+ paymentInstructionsPresent: Boolean(createData.paymentInstructions || createData.payment_instructions),
1075
+ quotePresent: Boolean(createData.quote),
1076
+ durationMs: Date.now() - startedAt
872
1077
  });
873
1078
 
874
- const paymentProof = await this.resolvePaymentProof(route, createData);
1079
+ const paymentProof = await this.resolvePaymentProof(route, createData, requestId);
1080
+ logger.info("purchase.complete.started", "seller purchase complete started", {
1081
+ requestId,
1082
+ sellerKey,
1083
+ model: modelId,
1084
+ purchaseId,
1085
+ paymentMethod,
1086
+ durationMs: Date.now() - startedAt
1087
+ });
875
1088
  const completeRes = await fetch(`${sellerUrl}/purchase/complete`, {
876
1089
  method: "POST",
877
1090
  headers: { "Content-Type": "application/json" },
@@ -887,6 +1100,7 @@ export class TokenbuddyDaemon {
887
1100
  logger.warn("purchase.complete.failed", "seller purchase complete failed", {
888
1101
  sellerKey,
889
1102
  model: modelId,
1103
+ requestId,
890
1104
  purchaseId,
891
1105
  status: completeRes.status,
892
1106
  errorMessage: completeData.error?.message || "purchase/complete failed",
@@ -903,6 +1117,7 @@ export class TokenbuddyDaemon {
903
1117
  const creditMicros = completeData.creditMicros ?? completeData.credit_micros ?? createData.creditMicros ?? createData.credit_micros ?? 0;
904
1118
  const currency = completeData.currency || createData.currency || "USD";
905
1119
  const expiresAt = new Date(Date.now() + 86400 * 1000).toISOString();
1120
+ const ledgerStatus = completeData.status || "funded";
906
1121
 
907
1122
  this.tokenStore.saveToken(sellerKey, token, tokenClass, creditMicros, expiresAt);
908
1123
  this.tokenStore.recordPurchaseLedger({
@@ -910,27 +1125,44 @@ export class TokenbuddyDaemon {
910
1125
  sellerKey,
911
1126
  modelId,
912
1127
  paymentMethod,
913
- status: completeData.status || "funded",
1128
+ status: ledgerStatus,
914
1129
  creditMicros,
915
1130
  currency,
916
1131
  paymentReference: completeData.paymentReference || completeData.payment_reference,
917
1132
  completedAt: new Date().toISOString()
918
1133
  });
1134
+ this.logPurchaseLedgerRecorded({
1135
+ requestId,
1136
+ sellerKey,
1137
+ modelId,
1138
+ purchaseId,
1139
+ paymentMethod,
1140
+ status: ledgerStatus,
1141
+ creditMicros,
1142
+ currency,
1143
+ durationMs: Date.now() - startedAt
1144
+ });
919
1145
  // v1.1: feed the credit tracker so the route-failover controller
920
1146
  // knows the seller is inside the fresh-purchase window.
921
1147
  this.creditTracker.recordPurchase(sellerKey, creditMicros, creditMicros);
922
1148
  logger.info("purchase.token.succeeded", "seller token purchased", {
1149
+ requestId,
923
1150
  sellerKey,
924
1151
  model: modelId,
925
1152
  purchaseId,
1153
+ paymentMethod,
926
1154
  tokenClass,
927
1155
  creditMicros,
1156
+ currency,
1157
+ ledgerStatus,
1158
+ completeStatus: completeRes.status,
928
1159
  durationMs: Date.now() - startedAt
929
1160
  });
930
1161
 
931
1162
  return token;
932
1163
  } catch (error: unknown) {
933
1164
  logger.error("purchase.token.failed", "seller token purchase failed", {
1165
+ requestId,
934
1166
  sellerKey,
935
1167
  model: modelId,
936
1168
  errorMessage: error instanceof Error ? error.message : String(error),
@@ -946,7 +1178,7 @@ export class TokenbuddyDaemon {
946
1178
  return purchaseTask;
947
1179
  }
948
1180
 
949
- private async resolvePaymentProof(route: SellerRoute, createData: PurchaseCreateResponse): Promise<string> {
1181
+ private async resolvePaymentProof(route: SellerRoute, createData: PurchaseCreateResponse, requestId?: string): Promise<string> {
950
1182
  if (route.paymentMethod === "mock") {
951
1183
  return "mock-proof-data";
952
1184
  }
@@ -957,16 +1189,21 @@ export class TokenbuddyDaemon {
957
1189
 
958
1190
  const proofCommand = process.env.TB_PROXYD_CLAWTIP_PROOF_COMMAND;
959
1191
  if (proofCommand?.trim()) {
960
- return await this.runClawtipProofCommand(route, createData, proofCommand.trim());
1192
+ const proof = await this.runClawtipProofCommand(route, createData, proofCommand.trim(), requestId);
1193
+ this.logPaymentProofResolved(route, "command", requestId);
1194
+ return proof;
961
1195
  }
962
1196
 
963
1197
  const proofFile = process.env.TOKENBUDDY_CLAWTIP_PAYMENT_PROOF_FILE || process.env.TOKENBUDDY_CLAWTIP_PROOF_FILE;
964
1198
  if (proofFile?.trim()) {
965
- return fs.readFileSync(proofFile.trim(), "utf8").trim();
1199
+ const proof = fs.readFileSync(proofFile.trim(), "utf8").trim();
1200
+ this.logPaymentProofResolved(route, "file", requestId);
1201
+ return proof;
966
1202
  }
967
1203
 
968
1204
  const proof = process.env.TOKENBUDDY_CLAWTIP_PAYMENT_PROOF;
969
1205
  if (proof?.trim()) {
1206
+ this.logPaymentProofResolved(route, "env", requestId);
970
1207
  return proof.trim();
971
1208
  }
972
1209
 
@@ -976,7 +1213,8 @@ export class TokenbuddyDaemon {
976
1213
  private runClawtipProofCommand(
977
1214
  route: SellerRoute,
978
1215
  createData: PurchaseCreateResponse,
979
- commandPath: string
1216
+ commandPath: string,
1217
+ requestId?: string
980
1218
  ): Promise<string> {
981
1219
  const timeoutMs = this.clawtipProofTimeoutMs();
982
1220
  const payload = JSON.stringify({
@@ -989,6 +1227,7 @@ export class TokenbuddyDaemon {
989
1227
  });
990
1228
 
991
1229
  logger.info("purchase.clawtip_proof.started", "clawtip proof provider started", {
1230
+ requestId,
992
1231
  sellerKey: route.seller.id,
993
1232
  model: route.modelId,
994
1233
  timeoutMs
@@ -1045,6 +1284,7 @@ export class TokenbuddyDaemon {
1045
1284
  return;
1046
1285
  }
1047
1286
  logger.info("purchase.clawtip_proof.succeeded", "clawtip proof provider succeeded", {
1287
+ requestId,
1048
1288
  sellerKey: route.seller.id,
1049
1289
  model: route.modelId,
1050
1290
  durationMs: Date.now() - startedAt
@@ -1132,7 +1372,7 @@ export class TokenbuddyDaemon {
1132
1372
  model: modelId,
1133
1373
  endpoint,
1134
1374
  stream: Boolean((body as { stream?: unknown }).stream),
1135
- upstreamBody
1375
+ bodySummary: summarizeProxyBody(upstreamBody)
1136
1376
  });
1137
1377
  // v1.1 §17.5: refuse to auto-purchase once the session budget is
1138
1378
  // exhausted. The seller is treated as "no auto-purchase available"
@@ -1153,7 +1393,7 @@ export class TokenbuddyDaemon {
1153
1393
  // seller; transfer leftover to wasted and fail over immediately.
1154
1394
  let token: string;
1155
1395
  try {
1156
- token = await this.getOrPurchaseToken(route);
1396
+ token = await this.getOrPurchaseToken(route, requestId);
1157
1397
  } catch (purchaseError) {
1158
1398
  logger.warn("purchase.failed", "seller auto-purchase failed; failing over without retry", {
1159
1399
  requestId,
@@ -1162,7 +1402,7 @@ export class TokenbuddyDaemon {
1162
1402
  endpoint,
1163
1403
  errorMessage: this.failoverErrorMessage(purchaseError)
1164
1404
  });
1165
- this.routeFailover.decide(
1405
+ const decision = this.routeFailover.decide(
1166
1406
  {
1167
1407
  sellerId: sellerKey,
1168
1408
  errorKind: "deadline",
@@ -1171,6 +1411,19 @@ export class TokenbuddyDaemon {
1171
1411
  },
1172
1412
  routes.length - routeIndex
1173
1413
  );
1414
+ logger.warn("route.failover.triggered", "seller route failed over after purchase failure", {
1415
+ requestId,
1416
+ sellerKey,
1417
+ model: modelId,
1418
+ endpoint,
1419
+ routeIndex,
1420
+ nextRouteIndex: routeIndex + 1,
1421
+ routesRemaining: routes.length - routeIndex,
1422
+ attempt,
1423
+ reason: "purchase_failed",
1424
+ controllerReason: decision.reason,
1425
+ controllerAction: decision.action
1426
+ });
1174
1427
  lastError = purchaseError;
1175
1428
  break;
1176
1429
  }
@@ -1180,9 +1433,9 @@ export class TokenbuddyDaemon {
1180
1433
  // the `X-TokenBuddy-Deadline-Ms` header (PR-6) can propagate
1181
1434
  // it to their own upstream fetch via the same signal.
1182
1435
  const deadlineMs = this.requestDeadlineMs();
1183
- const requestAc = new AbortController();
1184
- const requestTimer = setTimeout(() => requestAc.abort(new Error("buyer deadline exceeded")), deadlineMs);
1185
1436
  const sendSellerRequest = async (token: string) => {
1437
+ const requestAc = new AbortController();
1438
+ const requestTimer = setTimeout(() => requestAc.abort(new Error("buyer deadline exceeded")), deadlineMs);
1186
1439
  const headers: Record<string, string> = {
1187
1440
  "Content-Type": "application/json",
1188
1441
  "Authorization": `Bearer ${token}`,
@@ -1190,19 +1443,23 @@ export class TokenbuddyDaemon {
1190
1443
  "Idempotency-Key": idempotencyKey
1191
1444
  };
1192
1445
  headers["X-TokenBuddy-Deadline-Ms"] = String(deadlineMs);
1193
- return fetch(`${sellerUrl}${endpoint}`, {
1194
- method: "POST",
1195
- headers,
1196
- body: JSON.stringify(upstreamBody),
1197
- signal: requestAc.signal
1198
- });
1446
+ try {
1447
+ return await fetch(`${sellerUrl}${endpoint}`, {
1448
+ method: "POST",
1449
+ headers,
1450
+ body: JSON.stringify(upstreamBody),
1451
+ signal: requestAc.signal
1452
+ });
1453
+ } finally {
1454
+ clearTimeout(requestTimer);
1455
+ }
1199
1456
  };
1200
1457
  let upstreamResponse = await sendSellerRequest(token);
1201
1458
 
1202
1459
  if (!upstreamResponse.ok) {
1203
1460
  const errorBody = await upstreamResponse.text();
1204
1461
  if (this.isInsufficientFundsResponse(upstreamResponse.status, errorBody)) {
1205
- token = await this.recoverFromInsufficientFunds(route, token);
1462
+ token = await this.recoverFromInsufficientFunds(route, token, requestId);
1206
1463
  upstreamResponse = await sendSellerRequest(token);
1207
1464
  if (upstreamResponse.ok) {
1208
1465
  logger.info("proxy.retry_after_402.succeeded", "seller request succeeded after one-shot auto purchase retry", {
@@ -1247,7 +1504,16 @@ export class TokenbuddyDaemon {
1247
1504
  },
1248
1505
  routes.length - routeIndex
1249
1506
  );
1250
- this.handleFailoverDecision(decision, { sellerKey, endpoint, routeIndex });
1507
+ this.handleFailoverDecision(decision, {
1508
+ requestId,
1509
+ sellerKey,
1510
+ model: modelId,
1511
+ endpoint,
1512
+ routeIndex,
1513
+ routesRemaining: routes.length - routeIndex,
1514
+ attempt,
1515
+ status: upstreamResponse.status
1516
+ });
1251
1517
  if (decision.action === "fail_fast" || decision.action === "abort") {
1252
1518
  this.copyUpstreamHeaders(upstreamResponse, res);
1253
1519
  res.status(upstreamResponse.status);
@@ -1357,7 +1623,16 @@ export class TokenbuddyDaemon {
1357
1623
  },
1358
1624
  routes.length - routeIndex
1359
1625
  );
1360
- this.handleFailoverDecision(decision, { sellerKey, endpoint, routeIndex, reason: "exception" });
1626
+ this.handleFailoverDecision(decision, {
1627
+ requestId,
1628
+ sellerKey,
1629
+ model: modelId,
1630
+ endpoint,
1631
+ routeIndex,
1632
+ routesRemaining: routes.length - routeIndex,
1633
+ attempt,
1634
+ reason: "exception"
1635
+ });
1361
1636
  logger.warn("proxy.route.failed", "seller route failed before response", {
1362
1637
  requestId,
1363
1638
  sellerKey,
@@ -1587,7 +1862,6 @@ export class TokenbuddyDaemon {
1587
1862
  proxyUrl: String(req.body?.proxyUrl || ""),
1588
1863
  model: typeof req.body?.model === "string" ? req.body.model : undefined,
1589
1864
  providerSelections: req.body?.providerSelections,
1590
- sellerRouting: req.body?.sellerRouting,
1591
1865
  home: typeof req.body?.home === "string" ? req.body.home : undefined
1592
1866
  });
1593
1867
  logger.info("provider.install.previewed", "provider install previewed", {
@@ -1616,7 +1890,6 @@ export class TokenbuddyDaemon {
1616
1890
  proxyUrl: String(req.body?.proxyUrl || ""),
1617
1891
  model: typeof req.body?.model === "string" ? req.body.model : undefined,
1618
1892
  providerSelections: req.body?.providerSelections,
1619
- sellerRouting: req.body?.sellerRouting,
1620
1893
  home: typeof req.body?.home === "string" ? req.body.home : undefined
1621
1894
  }, this.tokenStore);
1622
1895
  logger.info("provider.install.applied", "provider install applied", {
@@ -1709,7 +1982,10 @@ export class TokenbuddyDaemon {
1709
1982
  proxyPort: this.config.proxyPort,
1710
1983
  dbPath: this.config.dbPath,
1711
1984
  sellerRegistryUrl: this.config.sellerRegistryUrl,
1712
- selectionMode: this.selectionMode
1985
+ selectionMode: this.selectionMode,
1986
+ sellerRoutingMode: this.sellerRouting.mode,
1987
+ sellerRoutingScorer: this.sellerRouting.scorer,
1988
+ selectedSellerId: this.selectedSellerId
1713
1989
  });
1714
1990
 
1715
1991
  // v1.2 §18.5: kick off the on-demand prewarm pipeline. The startup
@@ -1749,7 +2025,13 @@ export class TokenbuddyDaemon {
1749
2025
  focusSet: focusSet.slice(0, 20)
1750
2026
  });
1751
2027
  try {
1752
- await this.prewarmScheduler.runStartupPrewarm(focusSet);
2028
+ await this.fetchRegistry();
2029
+ await this.prewarmScheduler.runStartupPrewarm(
2030
+ focusSet.map((modelId) => ({
2031
+ modelId,
2032
+ protocol: this.resolvePrewarmProtocol(modelId)
2033
+ }))
2034
+ );
1753
2035
  } catch (err) {
1754
2036
  logger.warn("prewarm.startup.failed", "startup prewarm sweep failed", {
1755
2037
  errorMessage: err instanceof Error ? err.message : String(err)
@@ -1757,6 +2039,15 @@ export class TokenbuddyDaemon {
1757
2039
  }
1758
2040
  }
1759
2041
 
2042
+ private resolvePrewarmProtocol(modelId: string): string | undefined {
2043
+ for (const protocol of ["chat_completions", "messages", "responses"]) {
2044
+ if (this.modelIndex.sellersFor(modelId, { protocol, paymentMethod: "clawtip" }).length > 0) {
2045
+ return protocol;
2046
+ }
2047
+ }
2048
+ return undefined;
2049
+ }
2050
+
1760
2051
  public stop() {
1761
2052
  if (this.controlServer) this.controlServer.close();
1762
2053
  if (this.proxyServer) this.proxyServer.close();