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