@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/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 = inputCost + outputCost;
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: 0.2,
40964
- outputPrice: 1.5,
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.55,
41023
- outputPrice: 2.5,
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
- async function proxyPartnerRequest(req, res, apiBase, payFetch) {
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: 0,
47237
- // Actual cost handled by x402 settlement
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
- console.log(`[ClawRouter] Payment signed on ${chain3} (${network})`);
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(async (req, res) => {
47397
- req.on("error", (err) => {
47398
- console.error(`[ClawRouter] Request stream error: ${err.message}`);
47399
- });
47400
- res.on("error", (err) => {
47401
- console.error(`[ClawRouter] Response stream error: ${err.message}`);
47402
- });
47403
- finished(res, (err) => {
47404
- if (err && err.code !== "ERR_STREAM_DESTROYED") {
47405
- console.error(`[ClawRouter] Response finished with error: ${err.message}`);
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
- res.end(JSON.stringify(stats, null, 2));
47445
- return;
47446
- }
47447
- if (req.url === "/stats" && req.method === "DELETE") {
47448
- try {
47449
- const result = await clearStats();
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({ cleared: true, deletedFiles: result.deletedFiles }));
47452
- } catch (err) {
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
- return;
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
- JSON.stringify(
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
- return;
47490
- }
47491
- if (req.url === "/v1/models" && req.method === "GET") {
47492
- const models = buildProxyModelList();
47493
- res.writeHead(200, { "Content-Type": "application/json" });
47494
- res.end(JSON.stringify({ object: "list", data: models }));
47495
- return;
47496
- }
47497
- if (req.url?.startsWith("/images/") && req.method === "GET") {
47498
- const filename = req.url.slice("/images/".length).split("?")[0].replace(/[^a-zA-Z0-9._-]/g, "");
47499
- if (!filename) {
47500
- res.writeHead(400);
47501
- res.end("Bad request");
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
- const filePath = join8(IMAGE_DIR, filename);
47505
- try {
47506
- const s3 = await fsStat(filePath);
47507
- if (!s3.isFile()) throw new Error("not a file");
47508
- const ext = filename.split(".").pop()?.toLowerCase() ?? "png";
47509
- const mime = {
47510
- png: "image/png",
47511
- jpg: "image/jpeg",
47512
- jpeg: "image/jpeg",
47513
- webp: "image/webp",
47514
- gif: "image/gif"
47515
- };
47516
- const data = await readFile(filePath);
47517
- res.writeHead(200, {
47518
- "Content-Type": mime[ext] ?? "application/octet-stream",
47519
- "Content-Length": data.length
47520
- });
47521
- res.end(data);
47522
- } catch {
47523
- res.writeHead(404, { "Content-Type": "application/json" });
47524
- res.end(JSON.stringify({ error: "Image not found" }));
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
- return;
47527
- }
47528
- if (req.url === "/v1/images/generations" && req.method === "POST") {
47529
- const chunks = [];
47530
- for await (const chunk of req) {
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
- const reqBody = Buffer.concat(chunks);
47534
- try {
47535
- const upstream = await payFetch(`${apiBase}/v1/images/generations`, {
47536
- method: "POST",
47537
- headers: { "content-type": "application/json", "user-agent": USER_AGENT },
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
- let result;
47530
+ const filePath = join8(IMAGE_DIR, filename);
47547
47531
  try {
47548
- result = JSON.parse(text);
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(200, { "Content-Type": "application/json" });
47551
- res.end(text);
47552
- return;
47549
+ res.writeHead(404, { "Content-Type": "application/json" });
47550
+ res.end(JSON.stringify({ error: "Image not found" }));
47553
47551
  }
47554
- if (result.data?.length) {
47555
- await mkdir3(IMAGE_DIR, { recursive: true });
47556
- const port2 = server.address()?.port ?? 8402;
47557
- for (const img of result.data) {
47558
- const dataUriMatch = img.url?.match(/^data:(image\/\w+);base64,(.+)$/);
47559
- if (dataUriMatch) {
47560
- const [, mimeType, b64] = dataUriMatch;
47561
- const ext = mimeType === "image/jpeg" ? "jpg" : mimeType.split("/")[1] ?? "png";
47562
- const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
47563
- await writeFile2(join8(IMAGE_DIR, filename), Buffer.from(b64, "base64"));
47564
- img.url = `http://localhost:${port2}/images/${filename}`;
47565
- console.log(`[ClawRouter] Image saved \u2192 ${img.url}`);
47566
- } else if (img.url?.startsWith("https://") || img.url?.startsWith("http://")) {
47567
- try {
47568
- const imgResp = await fetch(img.url);
47569
- if (imgResp.ok) {
47570
- const contentType = imgResp.headers.get("content-type") ?? "image/png";
47571
- const ext = contentType.includes("jpeg") || contentType.includes("jpg") ? "jpg" : contentType.includes("webp") ? "webp" : "png";
47572
- const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
47573
- const buf = Buffer.from(await imgResp.arrayBuffer());
47574
- await writeFile2(join8(IMAGE_DIR, filename), buf);
47575
- img.url = `http://localhost:${port2}/images/${filename}`;
47576
- console.log(`[ClawRouter] Image downloaded & saved \u2192 ${img.url}`);
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
- res.writeHead(200, { "Content-Type": "application/json" });
47587
- res.end(JSON.stringify(result));
47588
- } catch (err) {
47589
- const msg = err instanceof Error ? err.message : String(err);
47590
- console.error(`[ClawRouter] Image generation error: ${msg}`);
47591
- if (!res.headersSent) {
47592
- res.writeHead(502, { "Content-Type": "application/json" });
47593
- res.end(JSON.stringify({ error: "Image generation failed", details: msg }));
47594
- }
47595
- }
47596
- return;
47597
- }
47598
- if (req.url === "/v1/images/image2image" && req.method === "POST") {
47599
- const chunks = [];
47600
- for await (const chunk of req) {
47601
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
47602
- }
47603
- const rawBody = Buffer.concat(chunks);
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
- try {
47635
- const upstream = await payFetch(`${apiBase}/v1/images/image2image`, {
47636
- method: "POST",
47637
- headers: { "content-type": "application/json", "user-agent": USER_AGENT },
47638
- body: reqBody
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
- let result;
47651
+ const rawBody = Buffer.concat(chunks);
47652
+ let reqBody;
47653
+ let img2imgModel = "openai/gpt-image-1";
47654
+ let img2imgCost = 0;
47647
47655
  try {
47648
- result = JSON.parse(text);
47649
- } catch {
47650
- res.writeHead(200, { "Content-Type": "application/json" });
47651
- res.end(text);
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
- if (result.data?.length) {
47655
- await mkdir3(IMAGE_DIR, { recursive: true });
47656
- const port2 = server.address()?.port ?? 8402;
47657
- for (const img of result.data) {
47658
- const dataUriMatch = img.url?.match(/^data:(image\/\w+);base64,(.+)$/);
47659
- if (dataUriMatch) {
47660
- const [, mimeType, b64] = dataUriMatch;
47661
- const ext = mimeType === "image/jpeg" ? "jpg" : mimeType.split("/")[1] ?? "png";
47662
- const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
47663
- await writeFile2(join8(IMAGE_DIR, filename), Buffer.from(b64, "base64"));
47664
- img.url = `http://localhost:${port2}/images/${filename}`;
47665
- console.log(`[ClawRouter] Image saved \u2192 ${img.url}`);
47666
- } else if (img.url?.startsWith("https://") || img.url?.startsWith("http://")) {
47667
- try {
47668
- const imgResp = await fetch(img.url);
47669
- if (imgResp.ok) {
47670
- const contentType = imgResp.headers.get("content-type") ?? "image/png";
47671
- const ext = contentType.includes("jpeg") || contentType.includes("jpg") ? "jpg" : contentType.includes("webp") ? "webp" : "png";
47672
- const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
47673
- const buf = Buffer.from(await imgResp.arrayBuffer());
47674
- await writeFile2(join8(IMAGE_DIR, filename), buf);
47675
- img.url = `http://localhost:${port2}/images/${filename}`;
47676
- console.log(`[ClawRouter] Image downloaded & saved \u2192 ${img.url}`);
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
- res.writeHead(200, { "Content-Type": "application/json" });
47687
- res.end(JSON.stringify(result));
47688
- } catch (err) {
47689
- const msg = err instanceof Error ? err.message : String(err);
47690
- console.error(`[ClawRouter] Image editing error: ${msg}`);
47691
- if (!res.headersSent) {
47692
- res.writeHead(502, { "Content-Type": "application/json" });
47693
- res.end(JSON.stringify({ error: "Image editing failed", details: msg }));
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 proxyPartnerRequest(req, res, apiBase, payFetch);
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: `Partner proxy error: ${error.message}`, type: "partner_error" }
47804
+ error: { message: `Proxy error: ${error.message}`, type: "proxy_error" }
47709
47805
  })
47710
47806
  );
47711
- }
47712
- }
47713
- return;
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
- res.write("data: [DONE]\n\n");
47751
- res.end();
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 actualInputTokens = responseInputTokens ?? Math.ceil(body.length / 4);
49575
- const actualOutputTokens = responseOutputTokens ?? maxTokens;
49576
- const accurateCosts = calculateModelCost(
49577
- logModel,
49578
- routerOpts.modelPricing,
49579
- actualInputTokens,
49580
- actualOutputTokens,
49581
- routingProfile ?? void 0
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: accurateCosts.costEstimate,
49588
- baselineCost: accurateCosts.baselineCost,
49589
- savings: accurateCosts.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 }