@tokenbuddy/tokenbuddy 1.0.12 → 1.0.14
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/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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
.
|
|
136
|
+
.map((block) => `${block}\n\n`)
|
|
137
|
+
.join("");
|
|
109
138
|
}
|
|
110
139
|
|
|
111
140
|
public finish(): { downstream: string; settlement: SellerSettlementSummary | undefined } {
|
|
112
|
-
const
|
|
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
|
|
231
|
-
|
|
232
|
-
?.config;
|
|
303
|
+
const storedRouting = this.tokenStore.getDaemonRuntimeConfig<unknown>(ROUTING_CONFIG_KEY)
|
|
304
|
+
?.config;
|
|
233
305
|
this.config = config;
|
|
234
|
-
this.
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
this.
|
|
239
|
-
|
|
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("
|
|
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(/\/+$/, "")}/
|
|
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: `
|
|
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
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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 (
|
|
540
|
+
if (planned.routes.length === 0) {
|
|
464
541
|
throw new Error(`no compatible seller for ${endpoint} model ${modelId}`);
|
|
465
542
|
}
|
|
466
543
|
|
|
467
|
-
const
|
|
468
|
-
|
|
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: {
|
|
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
|
|
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
|
|
933
|
+
return 180_000;
|
|
751
934
|
}
|
|
752
935
|
const parsed = Number(raw);
|
|
753
936
|
if (!Number.isInteger(parsed) || parsed < 1000) {
|
|
754
|
-
return
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
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, {
|
|
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, {
|
|
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.
|
|
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();
|