@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.
- package/dist/src/buyer-store.d.ts +61 -0
- package/dist/src/buyer-store.d.ts.map +1 -1
- package/dist/src/buyer-store.js +12 -0
- package/dist/src/buyer-store.js.map +1 -1
- package/dist/src/cli.d.ts +47 -0
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +287 -63
- package/dist/src/cli.js.map +1 -1
- package/dist/src/credit-tracker.d.ts +26 -0
- package/dist/src/credit-tracker.d.ts.map +1 -1
- package/dist/src/credit-tracker.js +8 -0
- package/dist/src/credit-tracker.js.map +1 -1
- package/dist/src/daemon.d.ts +29 -3
- package/dist/src/daemon.d.ts.map +1 -1
- package/dist/src/daemon.js +292 -65
- package/dist/src/daemon.js.map +1 -1
- package/dist/src/doctor-clawtip-wallet.d.ts +25 -0
- package/dist/src/doctor-clawtip-wallet.d.ts.map +1 -1
- package/dist/src/doctor-clawtip-wallet.js +13 -0
- package/dist/src/doctor-clawtip-wallet.js.map +1 -1
- package/dist/src/doctor-diagnostics.d.ts +63 -0
- package/dist/src/doctor-diagnostics.d.ts.map +1 -1
- package/dist/src/doctor-diagnostics.js +39 -1
- package/dist/src/doctor-diagnostics.js.map +1 -1
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/init-clawtip-activation.d.ts +103 -0
- package/dist/src/init-clawtip-activation.d.ts.map +1 -1
- package/dist/src/init-clawtip-activation.js +60 -0
- package/dist/src/init-clawtip-activation.js.map +1 -1
- package/dist/src/init-payment-options.d.ts +124 -0
- package/dist/src/init-payment-options.d.ts.map +1 -1
- package/dist/src/init-payment-options.js +68 -0
- package/dist/src/init-payment-options.js.map +1 -1
- package/dist/src/model-index.d.ts +9 -0
- package/dist/src/model-index.d.ts.map +1 -1
- package/dist/src/model-index.js.map +1 -1
- package/dist/src/prewarm-cache.d.ts +89 -0
- package/dist/src/prewarm-cache.d.ts.map +1 -1
- package/dist/src/prewarm-cache.js +14 -1
- package/dist/src/prewarm-cache.js.map +1 -1
- package/dist/src/prewarm-scheduler.d.ts +62 -3
- package/dist/src/prewarm-scheduler.d.ts.map +1 -1
- package/dist/src/prewarm-scheduler.js +39 -8
- package/dist/src/prewarm-scheduler.js.map +1 -1
- package/dist/src/provider-install.d.ts +89 -3
- package/dist/src/provider-install.d.ts.map +1 -1
- package/dist/src/provider-install.js +77 -19
- package/dist/src/provider-install.js.map +1 -1
- package/dist/src/route-failover.d.ts +48 -0
- package/dist/src/route-failover.d.ts.map +1 -1
- package/dist/src/route-failover.js.map +1 -1
- package/dist/src/seller-catalog.d.ts +158 -10
- package/dist/src/seller-catalog.d.ts.map +1 -1
- package/dist/src/seller-catalog.js +79 -5
- package/dist/src/seller-catalog.js.map +1 -1
- package/dist/src/seller-metadata-cache.d.ts +29 -0
- package/dist/src/seller-metadata-cache.d.ts.map +1 -0
- package/dist/src/seller-metadata-cache.js +71 -0
- package/dist/src/seller-metadata-cache.js.map +1 -0
- package/dist/src/seller-pool.d.ts +71 -0
- package/dist/src/seller-pool.d.ts.map +1 -1
- package/dist/src/seller-pool.js +6 -1
- package/dist/src/seller-pool.js.map +1 -1
- package/dist/src/seller-route-planner.d.ts +118 -0
- package/dist/src/seller-route-planner.d.ts.map +1 -0
- package/dist/src/seller-route-planner.js +160 -0
- package/dist/src/seller-route-planner.js.map +1 -0
- package/dist/src/seller-routing-config.d.ts +69 -0
- package/dist/src/seller-routing-config.d.ts.map +1 -0
- package/dist/src/seller-routing-config.js +164 -0
- package/dist/src/seller-routing-config.js.map +1 -0
- package/dist/src/seller-routing-strategy.d.ts +118 -0
- package/dist/src/seller-routing-strategy.d.ts.map +1 -0
- package/dist/src/seller-routing-strategy.js +183 -0
- package/dist/src/seller-routing-strategy.js.map +1 -0
- package/dist/src/stream-failover.d.ts +23 -0
- package/dist/src/stream-failover.d.ts.map +1 -1
- package/dist/src/stream-failover.js +4 -0
- package/dist/src/stream-failover.js.map +1 -1
- package/dist/src/tb-proxyd.js +7 -21
- package/dist/src/tb-proxyd.js.map +1 -1
- package/dist/src/terminal-detect.d.ts +51 -0
- package/dist/src/terminal-detect.d.ts.map +1 -1
- package/dist/src/terminal-detect.js +42 -0
- package/dist/src/terminal-detect.js.map +1 -1
- package/dist/src/terminal-image.d.ts +41 -0
- package/dist/src/terminal-image.d.ts.map +1 -1
- package/dist/src/terminal-image.js +15 -0
- package/dist/src/terminal-image.js.map +1 -1
- package/package.json +1 -1
- package/src/buyer-store.ts +61 -0
- package/src/cli.ts +330 -68
- package/src/credit-tracker.ts +26 -0
- package/src/daemon.ts +363 -72
- package/src/doctor-clawtip-wallet.ts +25 -0
- package/src/doctor-diagnostics.ts +63 -1
- package/src/index.ts +4 -0
- package/src/init-clawtip-activation.ts +103 -0
- package/src/init-payment-options.ts +124 -0
- package/src/model-index.ts +9 -0
- package/src/prewarm-cache.ts +99 -1
- package/src/prewarm-scheduler.ts +97 -12
- package/src/provider-install.ts +125 -27
- package/src/route-failover.ts +48 -0
- package/src/seller-catalog.ts +158 -12
- package/src/seller-metadata-cache.ts +91 -0
- package/src/seller-pool.ts +77 -1
- package/src/seller-route-planner.ts +323 -0
- package/src/seller-routing-config.ts +198 -0
- package/src/seller-routing-strategy.ts +316 -0
- package/src/stream-failover.ts +23 -0
- package/src/tb-proxyd.ts +7 -23
- package/src/terminal-detect.ts +51 -0
- package/src/terminal-image.ts +41 -0
- package/tests/cli-routing.test.ts +287 -0
- package/tests/daemon-classify.test.ts +431 -0
- package/tests/daemon-roles.test.ts +92 -0
- package/tests/seller-catalog-utilities.test.ts +70 -0
- package/tests/seller-metadata-cache.test.ts +89 -0
- package/tests/seller-route-planner.test.ts +150 -0
- package/tests/seller-routing-config.test.ts +111 -0
- package/tests/seller-routing-strategy.test.ts +166 -0
- package/tests/tokenbuddy.test.ts +446 -34
- /package/{src → tests}/credit-tracker.test.ts +0 -0
- /package/{src → tests}/model-index.test.ts +0 -0
- /package/{src → tests}/prewarm-cache.test.ts +0 -0
- /package/{src → tests}/prewarm-scheduler.test.ts +0 -0
- /package/{src → tests}/route-failover.test.ts +0 -0
- /package/{src → tests}/seller-catalog-413.test.ts +0 -0
- /package/{src → tests}/seller-pool.test.ts +0 -0
- /package/{src → tests}/stream-failover.test.ts +0 -0
- /package/{src → tests}/thousand-seller.test.ts +0 -0
package/dist/src/daemon.js
CHANGED
|
@@ -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
|
-
.
|
|
39
|
+
.map((block) => `${block}\n\n`)
|
|
40
|
+
.join("");
|
|
38
41
|
}
|
|
39
42
|
finish() {
|
|
40
|
-
const
|
|
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
|
|
171
|
+
const storedRouting = this.tokenStore.getDaemonRuntimeConfig(ROUTING_CONFIG_KEY)
|
|
129
172
|
?.config;
|
|
130
173
|
this.config = config;
|
|
131
|
-
this.
|
|
132
|
-
|
|
133
|
-
|
|
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("
|
|
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(/\/+$/, "")}/
|
|
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: `
|
|
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
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
|
347
|
-
|
|
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
|
|
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
|
|
716
|
+
return 180_000;
|
|
595
717
|
}
|
|
596
718
|
const parsed = Number(raw);
|
|
597
719
|
if (!Number.isInteger(parsed) || parsed < 1000) {
|
|
598
|
-
return
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
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, {
|
|
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, {
|
|
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.
|
|
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();
|