@blockrun/clawrouter 0.12.66 → 0.12.67
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/README.md +24 -0
- package/dist/cli.js +438 -330
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +0 -4
- package/dist/index.js +438 -330
- package/dist/index.js.map +1 -1
- package/docs/clawrouter-vs-openrouter-llm-routing-comparison.md +24 -0
- package/package.json +1 -1
- package/scripts/reinstall.sh +7 -7
- package/scripts/update.sh +17 -12
package/dist/cli.js
CHANGED
|
@@ -25520,6 +25520,7 @@ var init_client = __esm({
|
|
|
25520
25520
|
});
|
|
25521
25521
|
|
|
25522
25522
|
// src/proxy.ts
|
|
25523
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
25523
25524
|
import { createServer } from "http";
|
|
25524
25525
|
import { finished } from "stream";
|
|
25525
25526
|
import { homedir as homedir5 } from "os";
|
|
@@ -39005,13 +39006,18 @@ function getFallbackChain(tier, tierConfigs) {
|
|
|
39005
39006
|
const config = tierConfigs[tier];
|
|
39006
39007
|
return [config.primary, ...config.fallback];
|
|
39007
39008
|
}
|
|
39009
|
+
var SERVER_MARGIN_PERCENT = 5;
|
|
39010
|
+
var MIN_PAYMENT_USD = 1e-3;
|
|
39008
39011
|
function calculateModelCost(model, modelPricing, estimatedInputTokens, maxOutputTokens, routingProfile) {
|
|
39009
39012
|
const pricing = modelPricing.get(model);
|
|
39010
39013
|
const inputPrice = pricing?.inputPrice ?? 0;
|
|
39011
39014
|
const outputPrice = pricing?.outputPrice ?? 0;
|
|
39012
39015
|
const inputCost = estimatedInputTokens / 1e6 * inputPrice;
|
|
39013
39016
|
const outputCost = maxOutputTokens / 1e6 * outputPrice;
|
|
39014
|
-
const costEstimate =
|
|
39017
|
+
const costEstimate = Math.max(
|
|
39018
|
+
(inputCost + outputCost) * (1 + SERVER_MARGIN_PERCENT / 100),
|
|
39019
|
+
MIN_PAYMENT_USD
|
|
39020
|
+
);
|
|
39015
39021
|
const opusPricing = modelPricing.get(BASELINE_MODEL_ID);
|
|
39016
39022
|
const opusInputPrice = opusPricing?.inputPrice ?? BASELINE_INPUT_PRICE;
|
|
39017
39023
|
const opusOutputPrice = opusPricing?.outputPrice ?? BASELINE_OUTPUT_PRICE;
|
|
@@ -40960,8 +40966,8 @@ var BLOCKRUN_MODELS = [
|
|
|
40960
40966
|
id: "xai/grok-4-0709",
|
|
40961
40967
|
name: "Grok 4 (0709)",
|
|
40962
40968
|
version: "4-0709",
|
|
40963
|
-
inputPrice:
|
|
40964
|
-
outputPrice:
|
|
40969
|
+
inputPrice: 3,
|
|
40970
|
+
outputPrice: 15,
|
|
40965
40971
|
contextWindow: 131072,
|
|
40966
40972
|
maxOutput: 16384,
|
|
40967
40973
|
reasoning: true,
|
|
@@ -41019,8 +41025,8 @@ var BLOCKRUN_MODELS = [
|
|
|
41019
41025
|
id: "nvidia/kimi-k2.5",
|
|
41020
41026
|
name: "NVIDIA Kimi K2.5",
|
|
41021
41027
|
version: "k2.5",
|
|
41022
|
-
inputPrice: 0.
|
|
41023
|
-
outputPrice:
|
|
41028
|
+
inputPrice: 0.6,
|
|
41029
|
+
outputPrice: 3,
|
|
41024
41030
|
contextWindow: 262144,
|
|
41025
41031
|
maxOutput: 16384,
|
|
41026
41032
|
toolCalling: true
|
|
@@ -46575,6 +46581,7 @@ ${lines.join("\n")}`;
|
|
|
46575
46581
|
};
|
|
46576
46582
|
|
|
46577
46583
|
// src/proxy.ts
|
|
46584
|
+
var paymentStore = new AsyncLocalStorage();
|
|
46578
46585
|
var BLOCKRUN_API = "https://blockrun.ai/api";
|
|
46579
46586
|
var BLOCKRUN_SOLANA_API = "https://sol.blockrun.ai/api";
|
|
46580
46587
|
var IMAGE_DIR = join8(homedir5(), ".openclaw", "blockrun", "images");
|
|
@@ -47192,7 +47199,21 @@ function estimateAmount(modelId, bodyLength, maxTokens) {
|
|
|
47192
47199
|
const amountMicros = Math.max(1e3, Math.ceil(costUsd * 1.2 * 1e6));
|
|
47193
47200
|
return amountMicros.toString();
|
|
47194
47201
|
}
|
|
47195
|
-
|
|
47202
|
+
var IMAGE_PRICING = {
|
|
47203
|
+
"openai/dall-e-3": { default: 0.04, sizes: { "1024x1024": 0.04, "1792x1024": 0.08, "1024x1792": 0.08 } },
|
|
47204
|
+
"openai/gpt-image-1": { default: 0.02, sizes: { "1024x1024": 0.02, "1536x1024": 0.04, "1024x1536": 0.04 } },
|
|
47205
|
+
"black-forest/flux-1.1-pro": { default: 0.04 },
|
|
47206
|
+
"google/nano-banana": { default: 0.05 },
|
|
47207
|
+
"google/nano-banana-pro": { default: 0.1, sizes: { "1024x1024": 0.1, "2048x2048": 0.1, "4096x4096": 0.15 } }
|
|
47208
|
+
};
|
|
47209
|
+
function estimateImageCost(model, size5, n = 1) {
|
|
47210
|
+
const pricing = IMAGE_PRICING[model];
|
|
47211
|
+
if (!pricing) return 0.04 * n * 1.05;
|
|
47212
|
+
const sizePrice = size5 && pricing.sizes ? pricing.sizes[size5] : void 0;
|
|
47213
|
+
const pricePerImage = sizePrice ?? pricing.default;
|
|
47214
|
+
return pricePerImage * n * 1.05;
|
|
47215
|
+
}
|
|
47216
|
+
async function proxyPartnerRequest(req, res, apiBase, payFetch, getActualPaymentUsd) {
|
|
47196
47217
|
const startTime = Date.now();
|
|
47197
47218
|
const upstreamUrl = `${apiBase}${req.url}`;
|
|
47198
47219
|
const bodyChunks = [];
|
|
@@ -47229,13 +47250,13 @@ async function proxyPartnerRequest(req, res, apiBase, payFetch) {
|
|
|
47229
47250
|
res.end();
|
|
47230
47251
|
const latencyMs = Date.now() - startTime;
|
|
47231
47252
|
console.log(`[ClawRouter] Partner response: ${upstream.status} (${latencyMs}ms)`);
|
|
47253
|
+
const partnerCost = getActualPaymentUsd();
|
|
47232
47254
|
logUsage({
|
|
47233
47255
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
47234
47256
|
model: "partner",
|
|
47235
47257
|
tier: "PARTNER",
|
|
47236
|
-
cost:
|
|
47237
|
-
|
|
47238
|
-
baselineCost: 0,
|
|
47258
|
+
cost: partnerCost,
|
|
47259
|
+
baselineCost: partnerCost,
|
|
47239
47260
|
savings: 0,
|
|
47240
47261
|
latencyMs,
|
|
47241
47262
|
partnerId: (req.url?.split("?")[0] ?? "").replace(/^\/v1\//, "").replace(/\//g, "_") || "unknown",
|
|
@@ -47368,7 +47389,11 @@ async function startProxy(options) {
|
|
|
47368
47389
|
x402.onAfterPaymentCreation(async (context) => {
|
|
47369
47390
|
const network = context.selectedRequirements.network;
|
|
47370
47391
|
const chain3 = network.startsWith("eip155") ? "Base (EVM)" : network.startsWith("solana") ? "Solana" : network;
|
|
47371
|
-
|
|
47392
|
+
const amountMicros = parseInt(context.selectedRequirements.amount || "0", 10);
|
|
47393
|
+
const amountUsd = amountMicros / 1e6;
|
|
47394
|
+
const store = paymentStore.getStore();
|
|
47395
|
+
if (store) store.amountUsd = amountUsd;
|
|
47396
|
+
console.log(`[ClawRouter] Payment signed on ${chain3} (${network}) \u2014 $${amountUsd.toFixed(6)}`);
|
|
47372
47397
|
});
|
|
47373
47398
|
const payFetch = createPayFetchWithPreAuth(fetch, x402, void 0, {
|
|
47374
47399
|
skipPreAuth: paymentChain === "solana"
|
|
@@ -47393,311 +47418,382 @@ async function startProxy(options) {
|
|
|
47393
47418
|
const sessionStore = new SessionStore(options.sessionConfig);
|
|
47394
47419
|
const sessionJournal = new SessionJournal();
|
|
47395
47420
|
const connections = /* @__PURE__ */ new Set();
|
|
47396
|
-
const server = createServer(
|
|
47397
|
-
|
|
47398
|
-
|
|
47399
|
-
|
|
47400
|
-
|
|
47401
|
-
|
|
47402
|
-
|
|
47403
|
-
|
|
47404
|
-
|
|
47405
|
-
|
|
47406
|
-
|
|
47407
|
-
});
|
|
47408
|
-
finished(req, (err) => {
|
|
47409
|
-
if (err && err.code !== "ERR_STREAM_DESTROYED") {
|
|
47410
|
-
console.error(`[ClawRouter] Request finished with error: ${err.message}`);
|
|
47411
|
-
}
|
|
47412
|
-
});
|
|
47413
|
-
if (req.url === "/health" || req.url?.startsWith("/health?")) {
|
|
47414
|
-
const url = new URL(req.url, "http://localhost");
|
|
47415
|
-
const full = url.searchParams.get("full") === "true";
|
|
47416
|
-
const response = {
|
|
47417
|
-
status: "ok",
|
|
47418
|
-
wallet: account.address,
|
|
47419
|
-
paymentChain
|
|
47420
|
-
};
|
|
47421
|
-
if (solanaAddress) {
|
|
47422
|
-
response.solana = solanaAddress;
|
|
47423
|
-
}
|
|
47424
|
-
if (full) {
|
|
47425
|
-
try {
|
|
47426
|
-
const balanceInfo = await balanceMonitor.checkBalance();
|
|
47427
|
-
response.balance = balanceInfo.balanceUSD;
|
|
47428
|
-
response.isLow = balanceInfo.isLow;
|
|
47429
|
-
response.isEmpty = balanceInfo.isEmpty;
|
|
47430
|
-
} catch {
|
|
47431
|
-
response.balanceError = "Could not fetch balance";
|
|
47421
|
+
const server = createServer((req, res) => {
|
|
47422
|
+
paymentStore.run({ amountUsd: 0 }, async () => {
|
|
47423
|
+
req.on("error", (err) => {
|
|
47424
|
+
console.error(`[ClawRouter] Request stream error: ${err.message}`);
|
|
47425
|
+
});
|
|
47426
|
+
res.on("error", (err) => {
|
|
47427
|
+
console.error(`[ClawRouter] Response stream error: ${err.message}`);
|
|
47428
|
+
});
|
|
47429
|
+
finished(res, (err) => {
|
|
47430
|
+
if (err && err.code !== "ERR_STREAM_DESTROYED") {
|
|
47431
|
+
console.error(`[ClawRouter] Response finished with error: ${err.message}`);
|
|
47432
47432
|
}
|
|
47433
|
-
}
|
|
47434
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
47435
|
-
res.end(JSON.stringify(response));
|
|
47436
|
-
return;
|
|
47437
|
-
}
|
|
47438
|
-
if (req.url === "/cache" || req.url?.startsWith("/cache?")) {
|
|
47439
|
-
const stats = responseCache2.getStats();
|
|
47440
|
-
res.writeHead(200, {
|
|
47441
|
-
"Content-Type": "application/json",
|
|
47442
|
-
"Cache-Control": "no-cache"
|
|
47443
47433
|
});
|
|
47444
|
-
|
|
47445
|
-
|
|
47446
|
-
|
|
47447
|
-
|
|
47448
|
-
|
|
47449
|
-
|
|
47434
|
+
finished(req, (err) => {
|
|
47435
|
+
if (err && err.code !== "ERR_STREAM_DESTROYED") {
|
|
47436
|
+
console.error(`[ClawRouter] Request finished with error: ${err.message}`);
|
|
47437
|
+
}
|
|
47438
|
+
});
|
|
47439
|
+
if (req.url === "/health" || req.url?.startsWith("/health?")) {
|
|
47440
|
+
const url = new URL(req.url, "http://localhost");
|
|
47441
|
+
const full = url.searchParams.get("full") === "true";
|
|
47442
|
+
const response = {
|
|
47443
|
+
status: "ok",
|
|
47444
|
+
wallet: account.address,
|
|
47445
|
+
paymentChain
|
|
47446
|
+
};
|
|
47447
|
+
if (solanaAddress) {
|
|
47448
|
+
response.solana = solanaAddress;
|
|
47449
|
+
}
|
|
47450
|
+
if (full) {
|
|
47451
|
+
try {
|
|
47452
|
+
const balanceInfo = await balanceMonitor.checkBalance();
|
|
47453
|
+
response.balance = balanceInfo.balanceUSD;
|
|
47454
|
+
response.isLow = balanceInfo.isLow;
|
|
47455
|
+
response.isEmpty = balanceInfo.isEmpty;
|
|
47456
|
+
} catch {
|
|
47457
|
+
response.balanceError = "Could not fetch balance";
|
|
47458
|
+
}
|
|
47459
|
+
}
|
|
47450
47460
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
47451
|
-
res.end(JSON.stringify(
|
|
47452
|
-
|
|
47453
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
47454
|
-
res.end(
|
|
47455
|
-
JSON.stringify({
|
|
47456
|
-
error: `Failed to clear stats: ${err instanceof Error ? err.message : String(err)}`
|
|
47457
|
-
})
|
|
47458
|
-
);
|
|
47461
|
+
res.end(JSON.stringify(response));
|
|
47462
|
+
return;
|
|
47459
47463
|
}
|
|
47460
|
-
|
|
47461
|
-
|
|
47462
|
-
if (req.url === "/stats" || req.url?.startsWith("/stats?")) {
|
|
47463
|
-
try {
|
|
47464
|
-
const url = new URL(req.url, "http://localhost");
|
|
47465
|
-
const days = parseInt(url.searchParams.get("days") || "7", 10);
|
|
47466
|
-
const stats = await getStats(Math.min(days, 30));
|
|
47464
|
+
if (req.url === "/cache" || req.url?.startsWith("/cache?")) {
|
|
47465
|
+
const stats = responseCache2.getStats();
|
|
47467
47466
|
res.writeHead(200, {
|
|
47468
47467
|
"Content-Type": "application/json",
|
|
47469
47468
|
"Cache-Control": "no-cache"
|
|
47470
47469
|
});
|
|
47471
|
-
res.end(
|
|
47472
|
-
|
|
47473
|
-
{
|
|
47474
|
-
...stats,
|
|
47475
|
-
providerErrors: Object.fromEntries(perProviderErrors)
|
|
47476
|
-
},
|
|
47477
|
-
null,
|
|
47478
|
-
2
|
|
47479
|
-
)
|
|
47480
|
-
);
|
|
47481
|
-
} catch (err) {
|
|
47482
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
47483
|
-
res.end(
|
|
47484
|
-
JSON.stringify({
|
|
47485
|
-
error: `Failed to get stats: ${err instanceof Error ? err.message : String(err)}`
|
|
47486
|
-
})
|
|
47487
|
-
);
|
|
47470
|
+
res.end(JSON.stringify(stats, null, 2));
|
|
47471
|
+
return;
|
|
47488
47472
|
}
|
|
47489
|
-
|
|
47490
|
-
|
|
47491
|
-
|
|
47492
|
-
|
|
47493
|
-
|
|
47494
|
-
|
|
47495
|
-
|
|
47496
|
-
|
|
47497
|
-
|
|
47498
|
-
|
|
47499
|
-
|
|
47500
|
-
|
|
47501
|
-
|
|
47473
|
+
if (req.url === "/stats" && req.method === "DELETE") {
|
|
47474
|
+
try {
|
|
47475
|
+
const result = await clearStats();
|
|
47476
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
47477
|
+
res.end(JSON.stringify({ cleared: true, deletedFiles: result.deletedFiles }));
|
|
47478
|
+
} catch (err) {
|
|
47479
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
47480
|
+
res.end(
|
|
47481
|
+
JSON.stringify({
|
|
47482
|
+
error: `Failed to clear stats: ${err instanceof Error ? err.message : String(err)}`
|
|
47483
|
+
})
|
|
47484
|
+
);
|
|
47485
|
+
}
|
|
47502
47486
|
return;
|
|
47503
47487
|
}
|
|
47504
|
-
|
|
47505
|
-
|
|
47506
|
-
|
|
47507
|
-
|
|
47508
|
-
|
|
47509
|
-
|
|
47510
|
-
|
|
47511
|
-
|
|
47512
|
-
|
|
47513
|
-
|
|
47514
|
-
|
|
47515
|
-
|
|
47516
|
-
|
|
47517
|
-
|
|
47518
|
-
|
|
47519
|
-
|
|
47520
|
-
|
|
47521
|
-
|
|
47522
|
-
|
|
47523
|
-
|
|
47524
|
-
|
|
47488
|
+
if (req.url === "/stats" || req.url?.startsWith("/stats?")) {
|
|
47489
|
+
try {
|
|
47490
|
+
const url = new URL(req.url, "http://localhost");
|
|
47491
|
+
const days = parseInt(url.searchParams.get("days") || "7", 10);
|
|
47492
|
+
const stats = await getStats(Math.min(days, 30));
|
|
47493
|
+
res.writeHead(200, {
|
|
47494
|
+
"Content-Type": "application/json",
|
|
47495
|
+
"Cache-Control": "no-cache"
|
|
47496
|
+
});
|
|
47497
|
+
res.end(
|
|
47498
|
+
JSON.stringify(
|
|
47499
|
+
{
|
|
47500
|
+
...stats,
|
|
47501
|
+
providerErrors: Object.fromEntries(perProviderErrors)
|
|
47502
|
+
},
|
|
47503
|
+
null,
|
|
47504
|
+
2
|
|
47505
|
+
)
|
|
47506
|
+
);
|
|
47507
|
+
} catch (err) {
|
|
47508
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
47509
|
+
res.end(
|
|
47510
|
+
JSON.stringify({
|
|
47511
|
+
error: `Failed to get stats: ${err instanceof Error ? err.message : String(err)}`
|
|
47512
|
+
})
|
|
47513
|
+
);
|
|
47514
|
+
}
|
|
47515
|
+
return;
|
|
47525
47516
|
}
|
|
47526
|
-
|
|
47527
|
-
|
|
47528
|
-
|
|
47529
|
-
|
|
47530
|
-
|
|
47531
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
47517
|
+
if (req.url === "/v1/models" && req.method === "GET") {
|
|
47518
|
+
const models = buildProxyModelList();
|
|
47519
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
47520
|
+
res.end(JSON.stringify({ object: "list", data: models }));
|
|
47521
|
+
return;
|
|
47532
47522
|
}
|
|
47533
|
-
|
|
47534
|
-
|
|
47535
|
-
|
|
47536
|
-
|
|
47537
|
-
|
|
47538
|
-
body: reqBody
|
|
47539
|
-
});
|
|
47540
|
-
const text = await upstream.text();
|
|
47541
|
-
if (!upstream.ok) {
|
|
47542
|
-
res.writeHead(upstream.status, { "Content-Type": "application/json" });
|
|
47543
|
-
res.end(text);
|
|
47523
|
+
if (req.url?.startsWith("/images/") && req.method === "GET") {
|
|
47524
|
+
const filename = req.url.slice("/images/".length).split("?")[0].replace(/[^a-zA-Z0-9._-]/g, "");
|
|
47525
|
+
if (!filename) {
|
|
47526
|
+
res.writeHead(400);
|
|
47527
|
+
res.end("Bad request");
|
|
47544
47528
|
return;
|
|
47545
47529
|
}
|
|
47546
|
-
|
|
47530
|
+
const filePath = join8(IMAGE_DIR, filename);
|
|
47547
47531
|
try {
|
|
47548
|
-
|
|
47532
|
+
const s3 = await fsStat(filePath);
|
|
47533
|
+
if (!s3.isFile()) throw new Error("not a file");
|
|
47534
|
+
const ext = filename.split(".").pop()?.toLowerCase() ?? "png";
|
|
47535
|
+
const mime = {
|
|
47536
|
+
png: "image/png",
|
|
47537
|
+
jpg: "image/jpeg",
|
|
47538
|
+
jpeg: "image/jpeg",
|
|
47539
|
+
webp: "image/webp",
|
|
47540
|
+
gif: "image/gif"
|
|
47541
|
+
};
|
|
47542
|
+
const data = await readFile(filePath);
|
|
47543
|
+
res.writeHead(200, {
|
|
47544
|
+
"Content-Type": mime[ext] ?? "application/octet-stream",
|
|
47545
|
+
"Content-Length": data.length
|
|
47546
|
+
});
|
|
47547
|
+
res.end(data);
|
|
47549
47548
|
} catch {
|
|
47550
|
-
res.writeHead(
|
|
47551
|
-
res.end(
|
|
47552
|
-
return;
|
|
47549
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
47550
|
+
res.end(JSON.stringify({ error: "Image not found" }));
|
|
47553
47551
|
}
|
|
47554
|
-
|
|
47555
|
-
|
|
47556
|
-
|
|
47557
|
-
|
|
47558
|
-
|
|
47559
|
-
|
|
47560
|
-
|
|
47561
|
-
|
|
47562
|
-
|
|
47563
|
-
|
|
47564
|
-
|
|
47565
|
-
|
|
47566
|
-
|
|
47567
|
-
|
|
47568
|
-
|
|
47569
|
-
|
|
47570
|
-
|
|
47571
|
-
|
|
47572
|
-
|
|
47573
|
-
|
|
47574
|
-
|
|
47575
|
-
|
|
47576
|
-
|
|
47552
|
+
return;
|
|
47553
|
+
}
|
|
47554
|
+
if (req.url === "/v1/images/generations" && req.method === "POST") {
|
|
47555
|
+
const imgStartTime = Date.now();
|
|
47556
|
+
const chunks = [];
|
|
47557
|
+
for await (const chunk of req) {
|
|
47558
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
47559
|
+
}
|
|
47560
|
+
const reqBody = Buffer.concat(chunks);
|
|
47561
|
+
let imgModel = "unknown";
|
|
47562
|
+
let imgCost = 0;
|
|
47563
|
+
try {
|
|
47564
|
+
const parsed = JSON.parse(reqBody.toString());
|
|
47565
|
+
imgModel = parsed.model || "openai/dall-e-3";
|
|
47566
|
+
const n = parsed.n || 1;
|
|
47567
|
+
imgCost = estimateImageCost(imgModel, parsed.size, n);
|
|
47568
|
+
} catch {
|
|
47569
|
+
}
|
|
47570
|
+
try {
|
|
47571
|
+
const upstream = await payFetch(`${apiBase}/v1/images/generations`, {
|
|
47572
|
+
method: "POST",
|
|
47573
|
+
headers: { "content-type": "application/json", "user-agent": USER_AGENT },
|
|
47574
|
+
body: reqBody
|
|
47575
|
+
});
|
|
47576
|
+
const text = await upstream.text();
|
|
47577
|
+
if (!upstream.ok) {
|
|
47578
|
+
res.writeHead(upstream.status, { "Content-Type": "application/json" });
|
|
47579
|
+
res.end(text);
|
|
47580
|
+
return;
|
|
47581
|
+
}
|
|
47582
|
+
let result;
|
|
47583
|
+
try {
|
|
47584
|
+
result = JSON.parse(text);
|
|
47585
|
+
} catch {
|
|
47586
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
47587
|
+
res.end(text);
|
|
47588
|
+
return;
|
|
47589
|
+
}
|
|
47590
|
+
if (result.data?.length) {
|
|
47591
|
+
await mkdir3(IMAGE_DIR, { recursive: true });
|
|
47592
|
+
const port2 = server.address()?.port ?? 8402;
|
|
47593
|
+
for (const img of result.data) {
|
|
47594
|
+
const dataUriMatch = img.url?.match(/^data:(image\/\w+);base64,(.+)$/);
|
|
47595
|
+
if (dataUriMatch) {
|
|
47596
|
+
const [, mimeType, b64] = dataUriMatch;
|
|
47597
|
+
const ext = mimeType === "image/jpeg" ? "jpg" : mimeType.split("/")[1] ?? "png";
|
|
47598
|
+
const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
|
|
47599
|
+
await writeFile2(join8(IMAGE_DIR, filename), Buffer.from(b64, "base64"));
|
|
47600
|
+
img.url = `http://localhost:${port2}/images/${filename}`;
|
|
47601
|
+
console.log(`[ClawRouter] Image saved \u2192 ${img.url}`);
|
|
47602
|
+
} else if (img.url?.startsWith("https://") || img.url?.startsWith("http://")) {
|
|
47603
|
+
try {
|
|
47604
|
+
const imgResp = await fetch(img.url);
|
|
47605
|
+
if (imgResp.ok) {
|
|
47606
|
+
const contentType = imgResp.headers.get("content-type") ?? "image/png";
|
|
47607
|
+
const ext = contentType.includes("jpeg") || contentType.includes("jpg") ? "jpg" : contentType.includes("webp") ? "webp" : "png";
|
|
47608
|
+
const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
|
|
47609
|
+
const buf = Buffer.from(await imgResp.arrayBuffer());
|
|
47610
|
+
await writeFile2(join8(IMAGE_DIR, filename), buf);
|
|
47611
|
+
img.url = `http://localhost:${port2}/images/${filename}`;
|
|
47612
|
+
console.log(`[ClawRouter] Image downloaded & saved \u2192 ${img.url}`);
|
|
47613
|
+
}
|
|
47614
|
+
} catch (downloadErr) {
|
|
47615
|
+
console.warn(
|
|
47616
|
+
`[ClawRouter] Failed to download image, using original URL: ${downloadErr instanceof Error ? downloadErr.message : String(downloadErr)}`
|
|
47617
|
+
);
|
|
47577
47618
|
}
|
|
47578
|
-
} catch (downloadErr) {
|
|
47579
|
-
console.warn(
|
|
47580
|
-
`[ClawRouter] Failed to download image, using original URL: ${downloadErr instanceof Error ? downloadErr.message : String(downloadErr)}`
|
|
47581
|
-
);
|
|
47582
47619
|
}
|
|
47583
47620
|
}
|
|
47584
47621
|
}
|
|
47585
|
-
|
|
47586
|
-
|
|
47587
|
-
|
|
47588
|
-
|
|
47589
|
-
|
|
47590
|
-
|
|
47591
|
-
|
|
47592
|
-
|
|
47593
|
-
|
|
47594
|
-
|
|
47595
|
-
|
|
47596
|
-
|
|
47597
|
-
|
|
47598
|
-
|
|
47599
|
-
|
|
47600
|
-
|
|
47601
|
-
|
|
47602
|
-
|
|
47603
|
-
|
|
47604
|
-
let reqBody;
|
|
47605
|
-
try {
|
|
47606
|
-
const parsed = JSON.parse(rawBody.toString());
|
|
47607
|
-
for (const field of ["image", "mask"]) {
|
|
47608
|
-
const val = parsed[field];
|
|
47609
|
-
if (typeof val !== "string" || !val) continue;
|
|
47610
|
-
if (val.startsWith("data:")) {
|
|
47611
|
-
} else if (val.startsWith("https://") || val.startsWith("http://")) {
|
|
47612
|
-
const imgResp = await fetch(val);
|
|
47613
|
-
if (!imgResp.ok)
|
|
47614
|
-
throw new Error(`Failed to download ${field} from ${val}: HTTP ${imgResp.status}`);
|
|
47615
|
-
const contentType = imgResp.headers.get("content-type") ?? "image/png";
|
|
47616
|
-
const buf = Buffer.from(await imgResp.arrayBuffer());
|
|
47617
|
-
parsed[field] = `data:${contentType};base64,${buf.toString("base64")}`;
|
|
47618
|
-
console.log(
|
|
47619
|
-
`[ClawRouter] img2img: downloaded ${field} URL \u2192 data URI (${buf.length} bytes)`
|
|
47620
|
-
);
|
|
47621
|
-
} else {
|
|
47622
|
-
parsed[field] = readImageFileAsDataUri(val);
|
|
47623
|
-
console.log(`[ClawRouter] img2img: read ${field} file \u2192 data URI`);
|
|
47622
|
+
const imgActualCost = paymentStore.getStore()?.amountUsd ?? imgCost;
|
|
47623
|
+
logUsage({
|
|
47624
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
47625
|
+
model: imgModel,
|
|
47626
|
+
tier: "IMAGE",
|
|
47627
|
+
cost: imgActualCost,
|
|
47628
|
+
baselineCost: imgActualCost,
|
|
47629
|
+
savings: 0,
|
|
47630
|
+
latencyMs: Date.now() - imgStartTime
|
|
47631
|
+
}).catch(() => {
|
|
47632
|
+
});
|
|
47633
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
47634
|
+
res.end(JSON.stringify(result));
|
|
47635
|
+
} catch (err) {
|
|
47636
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
47637
|
+
console.error(`[ClawRouter] Image generation error: ${msg}`);
|
|
47638
|
+
if (!res.headersSent) {
|
|
47639
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
47640
|
+
res.end(JSON.stringify({ error: "Image generation failed", details: msg }));
|
|
47624
47641
|
}
|
|
47625
47642
|
}
|
|
47626
|
-
if (!parsed.model) parsed.model = "openai/gpt-image-1";
|
|
47627
|
-
reqBody = JSON.stringify(parsed);
|
|
47628
|
-
} catch (parseErr) {
|
|
47629
|
-
const msg = parseErr instanceof Error ? parseErr.message : String(parseErr);
|
|
47630
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
47631
|
-
res.end(JSON.stringify({ error: "Invalid request", details: msg }));
|
|
47632
47643
|
return;
|
|
47633
47644
|
}
|
|
47634
|
-
|
|
47635
|
-
const
|
|
47636
|
-
|
|
47637
|
-
|
|
47638
|
-
|
|
47639
|
-
});
|
|
47640
|
-
const text = await upstream.text();
|
|
47641
|
-
if (!upstream.ok) {
|
|
47642
|
-
res.writeHead(upstream.status, { "Content-Type": "application/json" });
|
|
47643
|
-
res.end(text);
|
|
47644
|
-
return;
|
|
47645
|
+
if (req.url === "/v1/images/image2image" && req.method === "POST") {
|
|
47646
|
+
const img2imgStartTime = Date.now();
|
|
47647
|
+
const chunks = [];
|
|
47648
|
+
for await (const chunk of req) {
|
|
47649
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
47645
47650
|
}
|
|
47646
|
-
|
|
47651
|
+
const rawBody = Buffer.concat(chunks);
|
|
47652
|
+
let reqBody;
|
|
47653
|
+
let img2imgModel = "openai/gpt-image-1";
|
|
47654
|
+
let img2imgCost = 0;
|
|
47647
47655
|
try {
|
|
47648
|
-
|
|
47649
|
-
|
|
47650
|
-
|
|
47651
|
-
|
|
47656
|
+
const parsed = JSON.parse(rawBody.toString());
|
|
47657
|
+
for (const field of ["image", "mask"]) {
|
|
47658
|
+
const val = parsed[field];
|
|
47659
|
+
if (typeof val !== "string" || !val) continue;
|
|
47660
|
+
if (val.startsWith("data:")) {
|
|
47661
|
+
} else if (val.startsWith("https://") || val.startsWith("http://")) {
|
|
47662
|
+
const imgResp = await fetch(val);
|
|
47663
|
+
if (!imgResp.ok)
|
|
47664
|
+
throw new Error(`Failed to download ${field} from ${val}: HTTP ${imgResp.status}`);
|
|
47665
|
+
const contentType = imgResp.headers.get("content-type") ?? "image/png";
|
|
47666
|
+
const buf = Buffer.from(await imgResp.arrayBuffer());
|
|
47667
|
+
parsed[field] = `data:${contentType};base64,${buf.toString("base64")}`;
|
|
47668
|
+
console.log(
|
|
47669
|
+
`[ClawRouter] img2img: downloaded ${field} URL \u2192 data URI (${buf.length} bytes)`
|
|
47670
|
+
);
|
|
47671
|
+
} else {
|
|
47672
|
+
parsed[field] = readImageFileAsDataUri(val);
|
|
47673
|
+
console.log(`[ClawRouter] img2img: read ${field} file \u2192 data URI`);
|
|
47674
|
+
}
|
|
47675
|
+
}
|
|
47676
|
+
if (!parsed.model) parsed.model = "openai/gpt-image-1";
|
|
47677
|
+
img2imgModel = parsed.model;
|
|
47678
|
+
img2imgCost = estimateImageCost(img2imgModel, parsed.size, parsed.n || 1);
|
|
47679
|
+
reqBody = JSON.stringify(parsed);
|
|
47680
|
+
} catch (parseErr) {
|
|
47681
|
+
const msg = parseErr instanceof Error ? parseErr.message : String(parseErr);
|
|
47682
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
47683
|
+
res.end(JSON.stringify({ error: "Invalid request", details: msg }));
|
|
47652
47684
|
return;
|
|
47653
47685
|
}
|
|
47654
|
-
|
|
47655
|
-
await
|
|
47656
|
-
|
|
47657
|
-
|
|
47658
|
-
|
|
47659
|
-
|
|
47660
|
-
|
|
47661
|
-
|
|
47662
|
-
|
|
47663
|
-
|
|
47664
|
-
|
|
47665
|
-
|
|
47666
|
-
|
|
47667
|
-
|
|
47668
|
-
|
|
47669
|
-
|
|
47670
|
-
|
|
47671
|
-
|
|
47672
|
-
|
|
47673
|
-
|
|
47674
|
-
|
|
47675
|
-
|
|
47676
|
-
|
|
47686
|
+
try {
|
|
47687
|
+
const upstream = await payFetch(`${apiBase}/v1/images/image2image`, {
|
|
47688
|
+
method: "POST",
|
|
47689
|
+
headers: { "content-type": "application/json", "user-agent": USER_AGENT },
|
|
47690
|
+
body: reqBody
|
|
47691
|
+
});
|
|
47692
|
+
const text = await upstream.text();
|
|
47693
|
+
if (!upstream.ok) {
|
|
47694
|
+
res.writeHead(upstream.status, { "Content-Type": "application/json" });
|
|
47695
|
+
res.end(text);
|
|
47696
|
+
return;
|
|
47697
|
+
}
|
|
47698
|
+
let result;
|
|
47699
|
+
try {
|
|
47700
|
+
result = JSON.parse(text);
|
|
47701
|
+
} catch {
|
|
47702
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
47703
|
+
res.end(text);
|
|
47704
|
+
return;
|
|
47705
|
+
}
|
|
47706
|
+
if (result.data?.length) {
|
|
47707
|
+
await mkdir3(IMAGE_DIR, { recursive: true });
|
|
47708
|
+
const port2 = server.address()?.port ?? 8402;
|
|
47709
|
+
for (const img of result.data) {
|
|
47710
|
+
const dataUriMatch = img.url?.match(/^data:(image\/\w+);base64,(.+)$/);
|
|
47711
|
+
if (dataUriMatch) {
|
|
47712
|
+
const [, mimeType, b64] = dataUriMatch;
|
|
47713
|
+
const ext = mimeType === "image/jpeg" ? "jpg" : mimeType.split("/")[1] ?? "png";
|
|
47714
|
+
const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
|
|
47715
|
+
await writeFile2(join8(IMAGE_DIR, filename), Buffer.from(b64, "base64"));
|
|
47716
|
+
img.url = `http://localhost:${port2}/images/${filename}`;
|
|
47717
|
+
console.log(`[ClawRouter] Image saved \u2192 ${img.url}`);
|
|
47718
|
+
} else if (img.url?.startsWith("https://") || img.url?.startsWith("http://")) {
|
|
47719
|
+
try {
|
|
47720
|
+
const imgResp = await fetch(img.url);
|
|
47721
|
+
if (imgResp.ok) {
|
|
47722
|
+
const contentType = imgResp.headers.get("content-type") ?? "image/png";
|
|
47723
|
+
const ext = contentType.includes("jpeg") || contentType.includes("jpg") ? "jpg" : contentType.includes("webp") ? "webp" : "png";
|
|
47724
|
+
const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
|
|
47725
|
+
const buf = Buffer.from(await imgResp.arrayBuffer());
|
|
47726
|
+
await writeFile2(join8(IMAGE_DIR, filename), buf);
|
|
47727
|
+
img.url = `http://localhost:${port2}/images/${filename}`;
|
|
47728
|
+
console.log(`[ClawRouter] Image downloaded & saved \u2192 ${img.url}`);
|
|
47729
|
+
}
|
|
47730
|
+
} catch (downloadErr) {
|
|
47731
|
+
console.warn(
|
|
47732
|
+
`[ClawRouter] Failed to download image, using original URL: ${downloadErr instanceof Error ? downloadErr.message : String(downloadErr)}`
|
|
47733
|
+
);
|
|
47677
47734
|
}
|
|
47678
|
-
} catch (downloadErr) {
|
|
47679
|
-
console.warn(
|
|
47680
|
-
`[ClawRouter] Failed to download image, using original URL: ${downloadErr instanceof Error ? downloadErr.message : String(downloadErr)}`
|
|
47681
|
-
);
|
|
47682
47735
|
}
|
|
47683
47736
|
}
|
|
47684
47737
|
}
|
|
47738
|
+
const img2imgActualCost = paymentStore.getStore()?.amountUsd ?? img2imgCost;
|
|
47739
|
+
logUsage({
|
|
47740
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
47741
|
+
model: img2imgModel,
|
|
47742
|
+
tier: "IMAGE",
|
|
47743
|
+
cost: img2imgActualCost,
|
|
47744
|
+
baselineCost: img2imgActualCost,
|
|
47745
|
+
savings: 0,
|
|
47746
|
+
latencyMs: Date.now() - img2imgStartTime
|
|
47747
|
+
}).catch(() => {
|
|
47748
|
+
});
|
|
47749
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
47750
|
+
res.end(JSON.stringify(result));
|
|
47751
|
+
} catch (err) {
|
|
47752
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
47753
|
+
console.error(`[ClawRouter] Image editing error: ${msg}`);
|
|
47754
|
+
if (!res.headersSent) {
|
|
47755
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
47756
|
+
res.end(JSON.stringify({ error: "Image editing failed", details: msg }));
|
|
47757
|
+
}
|
|
47685
47758
|
}
|
|
47686
|
-
|
|
47687
|
-
|
|
47688
|
-
|
|
47689
|
-
|
|
47690
|
-
|
|
47691
|
-
|
|
47692
|
-
|
|
47693
|
-
|
|
47759
|
+
return;
|
|
47760
|
+
}
|
|
47761
|
+
if (req.url?.match(/^\/v1\/(?:x|partner)\//)) {
|
|
47762
|
+
try {
|
|
47763
|
+
await proxyPartnerRequest(req, res, apiBase, payFetch, () => paymentStore.getStore()?.amountUsd ?? 0);
|
|
47764
|
+
} catch (err) {
|
|
47765
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
47766
|
+
options.onError?.(error);
|
|
47767
|
+
if (!res.headersSent) {
|
|
47768
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
47769
|
+
res.end(
|
|
47770
|
+
JSON.stringify({
|
|
47771
|
+
error: { message: `Partner proxy error: ${error.message}`, type: "partner_error" }
|
|
47772
|
+
})
|
|
47773
|
+
);
|
|
47774
|
+
}
|
|
47694
47775
|
}
|
|
47776
|
+
return;
|
|
47777
|
+
}
|
|
47778
|
+
if (!req.url?.startsWith("/v1")) {
|
|
47779
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
47780
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
47781
|
+
return;
|
|
47695
47782
|
}
|
|
47696
|
-
return;
|
|
47697
|
-
}
|
|
47698
|
-
if (req.url?.match(/^\/v1\/(?:x|partner)\//)) {
|
|
47699
47783
|
try {
|
|
47700
|
-
await
|
|
47784
|
+
await proxyRequest(
|
|
47785
|
+
req,
|
|
47786
|
+
res,
|
|
47787
|
+
apiBase,
|
|
47788
|
+
payFetch,
|
|
47789
|
+
options,
|
|
47790
|
+
routerOpts,
|
|
47791
|
+
deduplicator,
|
|
47792
|
+
balanceMonitor,
|
|
47793
|
+
sessionStore,
|
|
47794
|
+
responseCache2,
|
|
47795
|
+
sessionJournal
|
|
47796
|
+
);
|
|
47701
47797
|
} catch (err) {
|
|
47702
47798
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
47703
47799
|
options.onError?.(error);
|
|
@@ -47705,52 +47801,20 @@ async function startProxy(options) {
|
|
|
47705
47801
|
res.writeHead(502, { "Content-Type": "application/json" });
|
|
47706
47802
|
res.end(
|
|
47707
47803
|
JSON.stringify({
|
|
47708
|
-
error: { message: `
|
|
47804
|
+
error: { message: `Proxy error: ${error.message}`, type: "proxy_error" }
|
|
47709
47805
|
})
|
|
47710
47806
|
);
|
|
47711
|
-
}
|
|
47712
|
-
|
|
47713
|
-
|
|
47714
|
-
}
|
|
47715
|
-
if (!req.url?.startsWith("/v1")) {
|
|
47716
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
47717
|
-
res.end(JSON.stringify({ error: "Not found" }));
|
|
47718
|
-
return;
|
|
47719
|
-
}
|
|
47720
|
-
try {
|
|
47721
|
-
await proxyRequest(
|
|
47722
|
-
req,
|
|
47723
|
-
res,
|
|
47724
|
-
apiBase,
|
|
47725
|
-
payFetch,
|
|
47726
|
-
options,
|
|
47727
|
-
routerOpts,
|
|
47728
|
-
deduplicator,
|
|
47729
|
-
balanceMonitor,
|
|
47730
|
-
sessionStore,
|
|
47731
|
-
responseCache2,
|
|
47732
|
-
sessionJournal
|
|
47733
|
-
);
|
|
47734
|
-
} catch (err) {
|
|
47735
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
47736
|
-
options.onError?.(error);
|
|
47737
|
-
if (!res.headersSent) {
|
|
47738
|
-
res.writeHead(502, { "Content-Type": "application/json" });
|
|
47739
|
-
res.end(
|
|
47740
|
-
JSON.stringify({
|
|
47741
|
-
error: { message: `Proxy error: ${error.message}`, type: "proxy_error" }
|
|
47742
|
-
})
|
|
47743
|
-
);
|
|
47744
|
-
} else if (!res.writableEnded) {
|
|
47745
|
-
res.write(
|
|
47746
|
-
`data: ${JSON.stringify({ error: { message: error.message, type: "proxy_error" } })}
|
|
47807
|
+
} else if (!res.writableEnded) {
|
|
47808
|
+
res.write(
|
|
47809
|
+
`data: ${JSON.stringify({ error: { message: error.message, type: "proxy_error" } })}
|
|
47747
47810
|
|
|
47748
47811
|
`
|
|
47749
|
-
|
|
47750
|
-
|
|
47751
|
-
|
|
47812
|
+
);
|
|
47813
|
+
res.write("data: [DONE]\n\n");
|
|
47814
|
+
res.end();
|
|
47815
|
+
}
|
|
47752
47816
|
}
|
|
47753
|
-
}
|
|
47817
|
+
});
|
|
47754
47818
|
});
|
|
47755
47819
|
const tryListen = (attempt) => {
|
|
47756
47820
|
return new Promise((resolveAttempt, rejectAttempt) => {
|
|
@@ -48264,6 +48328,17 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
48264
48328
|
responseText = lines.join("\n");
|
|
48265
48329
|
}
|
|
48266
48330
|
console.log(`[ClawRouter] /imagegen success: ${images.length} image(s) generated`);
|
|
48331
|
+
const imagegenActualCost = paymentStore.getStore()?.amountUsd ?? estimateImageCost(imageModel, imageSize, 1);
|
|
48332
|
+
logUsage({
|
|
48333
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
48334
|
+
model: imageModel,
|
|
48335
|
+
tier: "IMAGE",
|
|
48336
|
+
cost: imagegenActualCost,
|
|
48337
|
+
baselineCost: imagegenActualCost,
|
|
48338
|
+
savings: 0,
|
|
48339
|
+
latencyMs: 0
|
|
48340
|
+
}).catch(() => {
|
|
48341
|
+
});
|
|
48267
48342
|
}
|
|
48268
48343
|
const completionId = `chatcmpl-image-${Date.now()}`;
|
|
48269
48344
|
const timestamp = Math.floor(Date.now() / 1e3);
|
|
@@ -48473,6 +48548,17 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
48473
48548
|
responseText = lines.join("\n");
|
|
48474
48549
|
}
|
|
48475
48550
|
console.log(`[ClawRouter] /img2img success: ${images.length} image(s)`);
|
|
48551
|
+
const img2imgActualCost2 = paymentStore.getStore()?.amountUsd ?? estimateImageCost(img2imgModel, img2imgSize, 1);
|
|
48552
|
+
logUsage({
|
|
48553
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
48554
|
+
model: img2imgModel,
|
|
48555
|
+
tier: "IMAGE",
|
|
48556
|
+
cost: img2imgActualCost2,
|
|
48557
|
+
baselineCost: img2imgActualCost2,
|
|
48558
|
+
savings: 0,
|
|
48559
|
+
latencyMs: 0
|
|
48560
|
+
}).catch(() => {
|
|
48561
|
+
});
|
|
48476
48562
|
}
|
|
48477
48563
|
sendImg2ImgText(responseText);
|
|
48478
48564
|
} catch (err) {
|
|
@@ -49571,22 +49657,44 @@ data: [DONE]
|
|
|
49571
49657
|
}
|
|
49572
49658
|
const logModel = routingDecision?.model ?? modelId;
|
|
49573
49659
|
if (logModel) {
|
|
49574
|
-
const
|
|
49575
|
-
|
|
49576
|
-
|
|
49577
|
-
|
|
49578
|
-
|
|
49579
|
-
|
|
49580
|
-
|
|
49581
|
-
|
|
49582
|
-
|
|
49660
|
+
const actualPayment = paymentStore.getStore()?.amountUsd ?? 0;
|
|
49661
|
+
let logCost;
|
|
49662
|
+
let logBaseline;
|
|
49663
|
+
let logSavings;
|
|
49664
|
+
if (actualPayment > 0) {
|
|
49665
|
+
logCost = actualPayment;
|
|
49666
|
+
const chargedInputTokens = Math.ceil(body.length / 4);
|
|
49667
|
+
const modelDef = BLOCKRUN_MODELS.find((m) => m.id === logModel);
|
|
49668
|
+
const chargedOutputTokens = modelDef ? Math.min(maxTokens, modelDef.maxOutput) : maxTokens;
|
|
49669
|
+
const baseline = calculateModelCost(
|
|
49670
|
+
logModel,
|
|
49671
|
+
routerOpts.modelPricing,
|
|
49672
|
+
chargedInputTokens,
|
|
49673
|
+
chargedOutputTokens,
|
|
49674
|
+
routingProfile ?? void 0
|
|
49675
|
+
);
|
|
49676
|
+
logBaseline = baseline.baselineCost;
|
|
49677
|
+
logSavings = logBaseline > 0 ? Math.max(0, (logBaseline - logCost) / logBaseline) : 0;
|
|
49678
|
+
} else {
|
|
49679
|
+
const chargedInputTokens = Math.ceil(body.length / 4);
|
|
49680
|
+
const costs = calculateModelCost(
|
|
49681
|
+
logModel,
|
|
49682
|
+
routerOpts.modelPricing,
|
|
49683
|
+
chargedInputTokens,
|
|
49684
|
+
maxTokens,
|
|
49685
|
+
routingProfile ?? void 0
|
|
49686
|
+
);
|
|
49687
|
+
logCost = costs.costEstimate;
|
|
49688
|
+
logBaseline = costs.baselineCost;
|
|
49689
|
+
logSavings = costs.savings;
|
|
49690
|
+
}
|
|
49583
49691
|
const entry = {
|
|
49584
49692
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
49585
49693
|
model: logModel,
|
|
49586
49694
|
tier: routingDecision?.tier ?? "DIRECT",
|
|
49587
|
-
cost:
|
|
49588
|
-
baselineCost:
|
|
49589
|
-
savings:
|
|
49695
|
+
cost: logCost,
|
|
49696
|
+
baselineCost: logBaseline,
|
|
49697
|
+
savings: logSavings,
|
|
49590
49698
|
latencyMs: Date.now() - startTime,
|
|
49591
49699
|
...responseInputTokens !== void 0 && { inputTokens: responseInputTokens },
|
|
49592
49700
|
...responseOutputTokens !== void 0 && { outputTokens: responseOutputTokens }
|