@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/index.js CHANGED
@@ -33378,8 +33378,8 @@ var BLOCKRUN_MODELS = [
33378
33378
  id: "xai/grok-4-0709",
33379
33379
  name: "Grok 4 (0709)",
33380
33380
  version: "4-0709",
33381
- inputPrice: 0.2,
33382
- outputPrice: 1.5,
33381
+ inputPrice: 3,
33382
+ outputPrice: 15,
33383
33383
  contextWindow: 131072,
33384
33384
  maxOutput: 16384,
33385
33385
  reasoning: true,
@@ -33437,8 +33437,8 @@ var BLOCKRUN_MODELS = [
33437
33437
  id: "nvidia/kimi-k2.5",
33438
33438
  name: "NVIDIA Kimi K2.5",
33439
33439
  version: "k2.5",
33440
- inputPrice: 0.55,
33441
- outputPrice: 2.5,
33440
+ inputPrice: 0.6,
33441
+ outputPrice: 3,
33442
33442
  contextWindow: 262144,
33443
33443
  maxOutput: 16384,
33444
33444
  toolCalling: true
@@ -33553,6 +33553,7 @@ var blockrunProvider = {
33553
33553
  };
33554
33554
 
33555
33555
  // src/proxy.ts
33556
+ import { AsyncLocalStorage } from "async_hooks";
33556
33557
  import { createServer } from "http";
33557
33558
  import { finished } from "stream";
33558
33559
  import { homedir as homedir5 } from "os";
@@ -43494,13 +43495,18 @@ function getFallbackChain(tier, tierConfigs) {
43494
43495
  const config = tierConfigs[tier];
43495
43496
  return [config.primary, ...config.fallback];
43496
43497
  }
43498
+ var SERVER_MARGIN_PERCENT = 5;
43499
+ var MIN_PAYMENT_USD = 1e-3;
43497
43500
  function calculateModelCost(model, modelPricing, estimatedInputTokens, maxOutputTokens, routingProfile) {
43498
43501
  const pricing = modelPricing.get(model);
43499
43502
  const inputPrice = pricing?.inputPrice ?? 0;
43500
43503
  const outputPrice = pricing?.outputPrice ?? 0;
43501
43504
  const inputCost = estimatedInputTokens / 1e6 * inputPrice;
43502
43505
  const outputCost = maxOutputTokens / 1e6 * outputPrice;
43503
- const costEstimate = inputCost + outputCost;
43506
+ const costEstimate = Math.max(
43507
+ (inputCost + outputCost) * (1 + SERVER_MARGIN_PERCENT / 100),
43508
+ MIN_PAYMENT_USD
43509
+ );
43504
43510
  const opusPricing = modelPricing.get(BASELINE_MODEL_ID);
43505
43511
  const opusInputPrice = opusPricing?.inputPrice ?? BASELINE_INPUT_PRICE;
43506
43512
  const opusOutputPrice = opusPricing?.outputPrice ?? BASELINE_OUTPUT_PRICE;
@@ -47095,6 +47101,7 @@ ${lines.join("\n")}`;
47095
47101
  };
47096
47102
 
47097
47103
  // src/proxy.ts
47104
+ var paymentStore = new AsyncLocalStorage();
47098
47105
  var BLOCKRUN_API = "https://blockrun.ai/api";
47099
47106
  var BLOCKRUN_SOLANA_API = "https://sol.blockrun.ai/api";
47100
47107
  var IMAGE_DIR = join8(homedir5(), ".openclaw", "blockrun", "images");
@@ -47712,7 +47719,21 @@ function estimateAmount(modelId, bodyLength, maxTokens) {
47712
47719
  const amountMicros = Math.max(1e3, Math.ceil(costUsd * 1.2 * 1e6));
47713
47720
  return amountMicros.toString();
47714
47721
  }
47715
- async function proxyPartnerRequest(req, res, apiBase, payFetch) {
47722
+ var IMAGE_PRICING = {
47723
+ "openai/dall-e-3": { default: 0.04, sizes: { "1024x1024": 0.04, "1792x1024": 0.08, "1024x1792": 0.08 } },
47724
+ "openai/gpt-image-1": { default: 0.02, sizes: { "1024x1024": 0.02, "1536x1024": 0.04, "1024x1536": 0.04 } },
47725
+ "black-forest/flux-1.1-pro": { default: 0.04 },
47726
+ "google/nano-banana": { default: 0.05 },
47727
+ "google/nano-banana-pro": { default: 0.1, sizes: { "1024x1024": 0.1, "2048x2048": 0.1, "4096x4096": 0.15 } }
47728
+ };
47729
+ function estimateImageCost(model, size5, n = 1) {
47730
+ const pricing = IMAGE_PRICING[model];
47731
+ if (!pricing) return 0.04 * n * 1.05;
47732
+ const sizePrice = size5 && pricing.sizes ? pricing.sizes[size5] : void 0;
47733
+ const pricePerImage = sizePrice ?? pricing.default;
47734
+ return pricePerImage * n * 1.05;
47735
+ }
47736
+ async function proxyPartnerRequest(req, res, apiBase, payFetch, getActualPaymentUsd) {
47716
47737
  const startTime = Date.now();
47717
47738
  const upstreamUrl = `${apiBase}${req.url}`;
47718
47739
  const bodyChunks = [];
@@ -47749,13 +47770,13 @@ async function proxyPartnerRequest(req, res, apiBase, payFetch) {
47749
47770
  res.end();
47750
47771
  const latencyMs = Date.now() - startTime;
47751
47772
  console.log(`[ClawRouter] Partner response: ${upstream.status} (${latencyMs}ms)`);
47773
+ const partnerCost = getActualPaymentUsd();
47752
47774
  logUsage({
47753
47775
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
47754
47776
  model: "partner",
47755
47777
  tier: "PARTNER",
47756
- cost: 0,
47757
- // Actual cost handled by x402 settlement
47758
- baselineCost: 0,
47778
+ cost: partnerCost,
47779
+ baselineCost: partnerCost,
47759
47780
  savings: 0,
47760
47781
  latencyMs,
47761
47782
  partnerId: (req.url?.split("?")[0] ?? "").replace(/^\/v1\//, "").replace(/\//g, "_") || "unknown",
@@ -47888,7 +47909,11 @@ async function startProxy(options) {
47888
47909
  x402.onAfterPaymentCreation(async (context) => {
47889
47910
  const network = context.selectedRequirements.network;
47890
47911
  const chain3 = network.startsWith("eip155") ? "Base (EVM)" : network.startsWith("solana") ? "Solana" : network;
47891
- console.log(`[ClawRouter] Payment signed on ${chain3} (${network})`);
47912
+ const amountMicros = parseInt(context.selectedRequirements.amount || "0", 10);
47913
+ const amountUsd = amountMicros / 1e6;
47914
+ const store = paymentStore.getStore();
47915
+ if (store) store.amountUsd = amountUsd;
47916
+ console.log(`[ClawRouter] Payment signed on ${chain3} (${network}) \u2014 $${amountUsd.toFixed(6)}`);
47892
47917
  });
47893
47918
  const payFetch = createPayFetchWithPreAuth(fetch, x402, void 0, {
47894
47919
  skipPreAuth: paymentChain === "solana"
@@ -47913,311 +47938,382 @@ async function startProxy(options) {
47913
47938
  const sessionStore = new SessionStore(options.sessionConfig);
47914
47939
  const sessionJournal = new SessionJournal();
47915
47940
  const connections = /* @__PURE__ */ new Set();
47916
- const server = createServer(async (req, res) => {
47917
- req.on("error", (err) => {
47918
- console.error(`[ClawRouter] Request stream error: ${err.message}`);
47919
- });
47920
- res.on("error", (err) => {
47921
- console.error(`[ClawRouter] Response stream error: ${err.message}`);
47922
- });
47923
- finished(res, (err) => {
47924
- if (err && err.code !== "ERR_STREAM_DESTROYED") {
47925
- console.error(`[ClawRouter] Response finished with error: ${err.message}`);
47926
- }
47927
- });
47928
- finished(req, (err) => {
47929
- if (err && err.code !== "ERR_STREAM_DESTROYED") {
47930
- console.error(`[ClawRouter] Request finished with error: ${err.message}`);
47931
- }
47932
- });
47933
- if (req.url === "/health" || req.url?.startsWith("/health?")) {
47934
- const url = new URL(req.url, "http://localhost");
47935
- const full = url.searchParams.get("full") === "true";
47936
- const response = {
47937
- status: "ok",
47938
- wallet: account.address,
47939
- paymentChain
47940
- };
47941
- if (solanaAddress) {
47942
- response.solana = solanaAddress;
47943
- }
47944
- if (full) {
47945
- try {
47946
- const balanceInfo = await balanceMonitor.checkBalance();
47947
- response.balance = balanceInfo.balanceUSD;
47948
- response.isLow = balanceInfo.isLow;
47949
- response.isEmpty = balanceInfo.isEmpty;
47950
- } catch {
47951
- response.balanceError = "Could not fetch balance";
47941
+ const server = createServer((req, res) => {
47942
+ paymentStore.run({ amountUsd: 0 }, async () => {
47943
+ req.on("error", (err) => {
47944
+ console.error(`[ClawRouter] Request stream error: ${err.message}`);
47945
+ });
47946
+ res.on("error", (err) => {
47947
+ console.error(`[ClawRouter] Response stream error: ${err.message}`);
47948
+ });
47949
+ finished(res, (err) => {
47950
+ if (err && err.code !== "ERR_STREAM_DESTROYED") {
47951
+ console.error(`[ClawRouter] Response finished with error: ${err.message}`);
47952
47952
  }
47953
- }
47954
- res.writeHead(200, { "Content-Type": "application/json" });
47955
- res.end(JSON.stringify(response));
47956
- return;
47957
- }
47958
- if (req.url === "/cache" || req.url?.startsWith("/cache?")) {
47959
- const stats = responseCache2.getStats();
47960
- res.writeHead(200, {
47961
- "Content-Type": "application/json",
47962
- "Cache-Control": "no-cache"
47963
47953
  });
47964
- res.end(JSON.stringify(stats, null, 2));
47965
- return;
47966
- }
47967
- if (req.url === "/stats" && req.method === "DELETE") {
47968
- try {
47969
- const result = await clearStats();
47954
+ finished(req, (err) => {
47955
+ if (err && err.code !== "ERR_STREAM_DESTROYED") {
47956
+ console.error(`[ClawRouter] Request finished with error: ${err.message}`);
47957
+ }
47958
+ });
47959
+ if (req.url === "/health" || req.url?.startsWith("/health?")) {
47960
+ const url = new URL(req.url, "http://localhost");
47961
+ const full = url.searchParams.get("full") === "true";
47962
+ const response = {
47963
+ status: "ok",
47964
+ wallet: account.address,
47965
+ paymentChain
47966
+ };
47967
+ if (solanaAddress) {
47968
+ response.solana = solanaAddress;
47969
+ }
47970
+ if (full) {
47971
+ try {
47972
+ const balanceInfo = await balanceMonitor.checkBalance();
47973
+ response.balance = balanceInfo.balanceUSD;
47974
+ response.isLow = balanceInfo.isLow;
47975
+ response.isEmpty = balanceInfo.isEmpty;
47976
+ } catch {
47977
+ response.balanceError = "Could not fetch balance";
47978
+ }
47979
+ }
47970
47980
  res.writeHead(200, { "Content-Type": "application/json" });
47971
- res.end(JSON.stringify({ cleared: true, deletedFiles: result.deletedFiles }));
47972
- } catch (err) {
47973
- res.writeHead(500, { "Content-Type": "application/json" });
47974
- res.end(
47975
- JSON.stringify({
47976
- error: `Failed to clear stats: ${err instanceof Error ? err.message : String(err)}`
47977
- })
47978
- );
47981
+ res.end(JSON.stringify(response));
47982
+ return;
47979
47983
  }
47980
- return;
47981
- }
47982
- if (req.url === "/stats" || req.url?.startsWith("/stats?")) {
47983
- try {
47984
- const url = new URL(req.url, "http://localhost");
47985
- const days = parseInt(url.searchParams.get("days") || "7", 10);
47986
- const stats = await getStats(Math.min(days, 30));
47984
+ if (req.url === "/cache" || req.url?.startsWith("/cache?")) {
47985
+ const stats = responseCache2.getStats();
47987
47986
  res.writeHead(200, {
47988
47987
  "Content-Type": "application/json",
47989
47988
  "Cache-Control": "no-cache"
47990
47989
  });
47991
- res.end(
47992
- JSON.stringify(
47993
- {
47994
- ...stats,
47995
- providerErrors: Object.fromEntries(perProviderErrors)
47996
- },
47997
- null,
47998
- 2
47999
- )
48000
- );
48001
- } catch (err) {
48002
- res.writeHead(500, { "Content-Type": "application/json" });
48003
- res.end(
48004
- JSON.stringify({
48005
- error: `Failed to get stats: ${err instanceof Error ? err.message : String(err)}`
48006
- })
48007
- );
47990
+ res.end(JSON.stringify(stats, null, 2));
47991
+ return;
48008
47992
  }
48009
- return;
48010
- }
48011
- if (req.url === "/v1/models" && req.method === "GET") {
48012
- const models = buildProxyModelList();
48013
- res.writeHead(200, { "Content-Type": "application/json" });
48014
- res.end(JSON.stringify({ object: "list", data: models }));
48015
- return;
48016
- }
48017
- if (req.url?.startsWith("/images/") && req.method === "GET") {
48018
- const filename = req.url.slice("/images/".length).split("?")[0].replace(/[^a-zA-Z0-9._-]/g, "");
48019
- if (!filename) {
48020
- res.writeHead(400);
48021
- res.end("Bad request");
47993
+ if (req.url === "/stats" && req.method === "DELETE") {
47994
+ try {
47995
+ const result = await clearStats();
47996
+ res.writeHead(200, { "Content-Type": "application/json" });
47997
+ res.end(JSON.stringify({ cleared: true, deletedFiles: result.deletedFiles }));
47998
+ } catch (err) {
47999
+ res.writeHead(500, { "Content-Type": "application/json" });
48000
+ res.end(
48001
+ JSON.stringify({
48002
+ error: `Failed to clear stats: ${err instanceof Error ? err.message : String(err)}`
48003
+ })
48004
+ );
48005
+ }
48022
48006
  return;
48023
48007
  }
48024
- const filePath = join8(IMAGE_DIR, filename);
48025
- try {
48026
- const s3 = await fsStat(filePath);
48027
- if (!s3.isFile()) throw new Error("not a file");
48028
- const ext = filename.split(".").pop()?.toLowerCase() ?? "png";
48029
- const mime = {
48030
- png: "image/png",
48031
- jpg: "image/jpeg",
48032
- jpeg: "image/jpeg",
48033
- webp: "image/webp",
48034
- gif: "image/gif"
48035
- };
48036
- const data = await readFile(filePath);
48037
- res.writeHead(200, {
48038
- "Content-Type": mime[ext] ?? "application/octet-stream",
48039
- "Content-Length": data.length
48040
- });
48041
- res.end(data);
48042
- } catch {
48043
- res.writeHead(404, { "Content-Type": "application/json" });
48044
- res.end(JSON.stringify({ error: "Image not found" }));
48008
+ if (req.url === "/stats" || req.url?.startsWith("/stats?")) {
48009
+ try {
48010
+ const url = new URL(req.url, "http://localhost");
48011
+ const days = parseInt(url.searchParams.get("days") || "7", 10);
48012
+ const stats = await getStats(Math.min(days, 30));
48013
+ res.writeHead(200, {
48014
+ "Content-Type": "application/json",
48015
+ "Cache-Control": "no-cache"
48016
+ });
48017
+ res.end(
48018
+ JSON.stringify(
48019
+ {
48020
+ ...stats,
48021
+ providerErrors: Object.fromEntries(perProviderErrors)
48022
+ },
48023
+ null,
48024
+ 2
48025
+ )
48026
+ );
48027
+ } catch (err) {
48028
+ res.writeHead(500, { "Content-Type": "application/json" });
48029
+ res.end(
48030
+ JSON.stringify({
48031
+ error: `Failed to get stats: ${err instanceof Error ? err.message : String(err)}`
48032
+ })
48033
+ );
48034
+ }
48035
+ return;
48045
48036
  }
48046
- return;
48047
- }
48048
- if (req.url === "/v1/images/generations" && req.method === "POST") {
48049
- const chunks = [];
48050
- for await (const chunk of req) {
48051
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
48037
+ if (req.url === "/v1/models" && req.method === "GET") {
48038
+ const models = buildProxyModelList();
48039
+ res.writeHead(200, { "Content-Type": "application/json" });
48040
+ res.end(JSON.stringify({ object: "list", data: models }));
48041
+ return;
48052
48042
  }
48053
- const reqBody = Buffer.concat(chunks);
48054
- try {
48055
- const upstream = await payFetch(`${apiBase}/v1/images/generations`, {
48056
- method: "POST",
48057
- headers: { "content-type": "application/json", "user-agent": USER_AGENT },
48058
- body: reqBody
48059
- });
48060
- const text = await upstream.text();
48061
- if (!upstream.ok) {
48062
- res.writeHead(upstream.status, { "Content-Type": "application/json" });
48063
- res.end(text);
48043
+ if (req.url?.startsWith("/images/") && req.method === "GET") {
48044
+ const filename = req.url.slice("/images/".length).split("?")[0].replace(/[^a-zA-Z0-9._-]/g, "");
48045
+ if (!filename) {
48046
+ res.writeHead(400);
48047
+ res.end("Bad request");
48064
48048
  return;
48065
48049
  }
48066
- let result;
48050
+ const filePath = join8(IMAGE_DIR, filename);
48067
48051
  try {
48068
- result = JSON.parse(text);
48052
+ const s3 = await fsStat(filePath);
48053
+ if (!s3.isFile()) throw new Error("not a file");
48054
+ const ext = filename.split(".").pop()?.toLowerCase() ?? "png";
48055
+ const mime = {
48056
+ png: "image/png",
48057
+ jpg: "image/jpeg",
48058
+ jpeg: "image/jpeg",
48059
+ webp: "image/webp",
48060
+ gif: "image/gif"
48061
+ };
48062
+ const data = await readFile(filePath);
48063
+ res.writeHead(200, {
48064
+ "Content-Type": mime[ext] ?? "application/octet-stream",
48065
+ "Content-Length": data.length
48066
+ });
48067
+ res.end(data);
48069
48068
  } catch {
48070
- res.writeHead(200, { "Content-Type": "application/json" });
48071
- res.end(text);
48072
- return;
48069
+ res.writeHead(404, { "Content-Type": "application/json" });
48070
+ res.end(JSON.stringify({ error: "Image not found" }));
48073
48071
  }
48074
- if (result.data?.length) {
48075
- await mkdir3(IMAGE_DIR, { recursive: true });
48076
- const port2 = server.address()?.port ?? 8402;
48077
- for (const img of result.data) {
48078
- const dataUriMatch = img.url?.match(/^data:(image\/\w+);base64,(.+)$/);
48079
- if (dataUriMatch) {
48080
- const [, mimeType, b64] = dataUriMatch;
48081
- const ext = mimeType === "image/jpeg" ? "jpg" : mimeType.split("/")[1] ?? "png";
48082
- const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
48083
- await writeFile2(join8(IMAGE_DIR, filename), Buffer.from(b64, "base64"));
48084
- img.url = `http://localhost:${port2}/images/${filename}`;
48085
- console.log(`[ClawRouter] Image saved \u2192 ${img.url}`);
48086
- } else if (img.url?.startsWith("https://") || img.url?.startsWith("http://")) {
48087
- try {
48088
- const imgResp = await fetch(img.url);
48089
- if (imgResp.ok) {
48090
- const contentType = imgResp.headers.get("content-type") ?? "image/png";
48091
- const ext = contentType.includes("jpeg") || contentType.includes("jpg") ? "jpg" : contentType.includes("webp") ? "webp" : "png";
48092
- const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
48093
- const buf = Buffer.from(await imgResp.arrayBuffer());
48094
- await writeFile2(join8(IMAGE_DIR, filename), buf);
48095
- img.url = `http://localhost:${port2}/images/${filename}`;
48096
- console.log(`[ClawRouter] Image downloaded & saved \u2192 ${img.url}`);
48072
+ return;
48073
+ }
48074
+ if (req.url === "/v1/images/generations" && req.method === "POST") {
48075
+ const imgStartTime = Date.now();
48076
+ const chunks = [];
48077
+ for await (const chunk of req) {
48078
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
48079
+ }
48080
+ const reqBody = Buffer.concat(chunks);
48081
+ let imgModel = "unknown";
48082
+ let imgCost = 0;
48083
+ try {
48084
+ const parsed = JSON.parse(reqBody.toString());
48085
+ imgModel = parsed.model || "openai/dall-e-3";
48086
+ const n = parsed.n || 1;
48087
+ imgCost = estimateImageCost(imgModel, parsed.size, n);
48088
+ } catch {
48089
+ }
48090
+ try {
48091
+ const upstream = await payFetch(`${apiBase}/v1/images/generations`, {
48092
+ method: "POST",
48093
+ headers: { "content-type": "application/json", "user-agent": USER_AGENT },
48094
+ body: reqBody
48095
+ });
48096
+ const text = await upstream.text();
48097
+ if (!upstream.ok) {
48098
+ res.writeHead(upstream.status, { "Content-Type": "application/json" });
48099
+ res.end(text);
48100
+ return;
48101
+ }
48102
+ let result;
48103
+ try {
48104
+ result = JSON.parse(text);
48105
+ } catch {
48106
+ res.writeHead(200, { "Content-Type": "application/json" });
48107
+ res.end(text);
48108
+ return;
48109
+ }
48110
+ if (result.data?.length) {
48111
+ await mkdir3(IMAGE_DIR, { recursive: true });
48112
+ const port2 = server.address()?.port ?? 8402;
48113
+ for (const img of result.data) {
48114
+ const dataUriMatch = img.url?.match(/^data:(image\/\w+);base64,(.+)$/);
48115
+ if (dataUriMatch) {
48116
+ const [, mimeType, b64] = dataUriMatch;
48117
+ const ext = mimeType === "image/jpeg" ? "jpg" : mimeType.split("/")[1] ?? "png";
48118
+ const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
48119
+ await writeFile2(join8(IMAGE_DIR, filename), Buffer.from(b64, "base64"));
48120
+ img.url = `http://localhost:${port2}/images/${filename}`;
48121
+ console.log(`[ClawRouter] Image saved \u2192 ${img.url}`);
48122
+ } else if (img.url?.startsWith("https://") || img.url?.startsWith("http://")) {
48123
+ try {
48124
+ const imgResp = await fetch(img.url);
48125
+ if (imgResp.ok) {
48126
+ const contentType = imgResp.headers.get("content-type") ?? "image/png";
48127
+ const ext = contentType.includes("jpeg") || contentType.includes("jpg") ? "jpg" : contentType.includes("webp") ? "webp" : "png";
48128
+ const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
48129
+ const buf = Buffer.from(await imgResp.arrayBuffer());
48130
+ await writeFile2(join8(IMAGE_DIR, filename), buf);
48131
+ img.url = `http://localhost:${port2}/images/${filename}`;
48132
+ console.log(`[ClawRouter] Image downloaded & saved \u2192 ${img.url}`);
48133
+ }
48134
+ } catch (downloadErr) {
48135
+ console.warn(
48136
+ `[ClawRouter] Failed to download image, using original URL: ${downloadErr instanceof Error ? downloadErr.message : String(downloadErr)}`
48137
+ );
48097
48138
  }
48098
- } catch (downloadErr) {
48099
- console.warn(
48100
- `[ClawRouter] Failed to download image, using original URL: ${downloadErr instanceof Error ? downloadErr.message : String(downloadErr)}`
48101
- );
48102
48139
  }
48103
48140
  }
48104
48141
  }
48105
- }
48106
- res.writeHead(200, { "Content-Type": "application/json" });
48107
- res.end(JSON.stringify(result));
48108
- } catch (err) {
48109
- const msg = err instanceof Error ? err.message : String(err);
48110
- console.error(`[ClawRouter] Image generation error: ${msg}`);
48111
- if (!res.headersSent) {
48112
- res.writeHead(502, { "Content-Type": "application/json" });
48113
- res.end(JSON.stringify({ error: "Image generation failed", details: msg }));
48114
- }
48115
- }
48116
- return;
48117
- }
48118
- if (req.url === "/v1/images/image2image" && req.method === "POST") {
48119
- const chunks = [];
48120
- for await (const chunk of req) {
48121
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
48122
- }
48123
- const rawBody = Buffer.concat(chunks);
48124
- let reqBody;
48125
- try {
48126
- const parsed = JSON.parse(rawBody.toString());
48127
- for (const field of ["image", "mask"]) {
48128
- const val = parsed[field];
48129
- if (typeof val !== "string" || !val) continue;
48130
- if (val.startsWith("data:")) {
48131
- } else if (val.startsWith("https://") || val.startsWith("http://")) {
48132
- const imgResp = await fetch(val);
48133
- if (!imgResp.ok)
48134
- throw new Error(`Failed to download ${field} from ${val}: HTTP ${imgResp.status}`);
48135
- const contentType = imgResp.headers.get("content-type") ?? "image/png";
48136
- const buf = Buffer.from(await imgResp.arrayBuffer());
48137
- parsed[field] = `data:${contentType};base64,${buf.toString("base64")}`;
48138
- console.log(
48139
- `[ClawRouter] img2img: downloaded ${field} URL \u2192 data URI (${buf.length} bytes)`
48140
- );
48141
- } else {
48142
- parsed[field] = readImageFileAsDataUri(val);
48143
- console.log(`[ClawRouter] img2img: read ${field} file \u2192 data URI`);
48142
+ const imgActualCost = paymentStore.getStore()?.amountUsd ?? imgCost;
48143
+ logUsage({
48144
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
48145
+ model: imgModel,
48146
+ tier: "IMAGE",
48147
+ cost: imgActualCost,
48148
+ baselineCost: imgActualCost,
48149
+ savings: 0,
48150
+ latencyMs: Date.now() - imgStartTime
48151
+ }).catch(() => {
48152
+ });
48153
+ res.writeHead(200, { "Content-Type": "application/json" });
48154
+ res.end(JSON.stringify(result));
48155
+ } catch (err) {
48156
+ const msg = err instanceof Error ? err.message : String(err);
48157
+ console.error(`[ClawRouter] Image generation error: ${msg}`);
48158
+ if (!res.headersSent) {
48159
+ res.writeHead(502, { "Content-Type": "application/json" });
48160
+ res.end(JSON.stringify({ error: "Image generation failed", details: msg }));
48144
48161
  }
48145
48162
  }
48146
- if (!parsed.model) parsed.model = "openai/gpt-image-1";
48147
- reqBody = JSON.stringify(parsed);
48148
- } catch (parseErr) {
48149
- const msg = parseErr instanceof Error ? parseErr.message : String(parseErr);
48150
- res.writeHead(400, { "Content-Type": "application/json" });
48151
- res.end(JSON.stringify({ error: "Invalid request", details: msg }));
48152
48163
  return;
48153
48164
  }
48154
- try {
48155
- const upstream = await payFetch(`${apiBase}/v1/images/image2image`, {
48156
- method: "POST",
48157
- headers: { "content-type": "application/json", "user-agent": USER_AGENT },
48158
- body: reqBody
48159
- });
48160
- const text = await upstream.text();
48161
- if (!upstream.ok) {
48162
- res.writeHead(upstream.status, { "Content-Type": "application/json" });
48163
- res.end(text);
48164
- return;
48165
+ if (req.url === "/v1/images/image2image" && req.method === "POST") {
48166
+ const img2imgStartTime = Date.now();
48167
+ const chunks = [];
48168
+ for await (const chunk of req) {
48169
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
48165
48170
  }
48166
- let result;
48171
+ const rawBody = Buffer.concat(chunks);
48172
+ let reqBody;
48173
+ let img2imgModel = "openai/gpt-image-1";
48174
+ let img2imgCost = 0;
48167
48175
  try {
48168
- result = JSON.parse(text);
48169
- } catch {
48170
- res.writeHead(200, { "Content-Type": "application/json" });
48171
- res.end(text);
48176
+ const parsed = JSON.parse(rawBody.toString());
48177
+ for (const field of ["image", "mask"]) {
48178
+ const val = parsed[field];
48179
+ if (typeof val !== "string" || !val) continue;
48180
+ if (val.startsWith("data:")) {
48181
+ } else if (val.startsWith("https://") || val.startsWith("http://")) {
48182
+ const imgResp = await fetch(val);
48183
+ if (!imgResp.ok)
48184
+ throw new Error(`Failed to download ${field} from ${val}: HTTP ${imgResp.status}`);
48185
+ const contentType = imgResp.headers.get("content-type") ?? "image/png";
48186
+ const buf = Buffer.from(await imgResp.arrayBuffer());
48187
+ parsed[field] = `data:${contentType};base64,${buf.toString("base64")}`;
48188
+ console.log(
48189
+ `[ClawRouter] img2img: downloaded ${field} URL \u2192 data URI (${buf.length} bytes)`
48190
+ );
48191
+ } else {
48192
+ parsed[field] = readImageFileAsDataUri(val);
48193
+ console.log(`[ClawRouter] img2img: read ${field} file \u2192 data URI`);
48194
+ }
48195
+ }
48196
+ if (!parsed.model) parsed.model = "openai/gpt-image-1";
48197
+ img2imgModel = parsed.model;
48198
+ img2imgCost = estimateImageCost(img2imgModel, parsed.size, parsed.n || 1);
48199
+ reqBody = JSON.stringify(parsed);
48200
+ } catch (parseErr) {
48201
+ const msg = parseErr instanceof Error ? parseErr.message : String(parseErr);
48202
+ res.writeHead(400, { "Content-Type": "application/json" });
48203
+ res.end(JSON.stringify({ error: "Invalid request", details: msg }));
48172
48204
  return;
48173
48205
  }
48174
- if (result.data?.length) {
48175
- await mkdir3(IMAGE_DIR, { recursive: true });
48176
- const port2 = server.address()?.port ?? 8402;
48177
- for (const img of result.data) {
48178
- const dataUriMatch = img.url?.match(/^data:(image\/\w+);base64,(.+)$/);
48179
- if (dataUriMatch) {
48180
- const [, mimeType, b64] = dataUriMatch;
48181
- const ext = mimeType === "image/jpeg" ? "jpg" : mimeType.split("/")[1] ?? "png";
48182
- const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
48183
- await writeFile2(join8(IMAGE_DIR, filename), Buffer.from(b64, "base64"));
48184
- img.url = `http://localhost:${port2}/images/${filename}`;
48185
- console.log(`[ClawRouter] Image saved \u2192 ${img.url}`);
48186
- } else if (img.url?.startsWith("https://") || img.url?.startsWith("http://")) {
48187
- try {
48188
- const imgResp = await fetch(img.url);
48189
- if (imgResp.ok) {
48190
- const contentType = imgResp.headers.get("content-type") ?? "image/png";
48191
- const ext = contentType.includes("jpeg") || contentType.includes("jpg") ? "jpg" : contentType.includes("webp") ? "webp" : "png";
48192
- const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
48193
- const buf = Buffer.from(await imgResp.arrayBuffer());
48194
- await writeFile2(join8(IMAGE_DIR, filename), buf);
48195
- img.url = `http://localhost:${port2}/images/${filename}`;
48196
- console.log(`[ClawRouter] Image downloaded & saved \u2192 ${img.url}`);
48206
+ try {
48207
+ const upstream = await payFetch(`${apiBase}/v1/images/image2image`, {
48208
+ method: "POST",
48209
+ headers: { "content-type": "application/json", "user-agent": USER_AGENT },
48210
+ body: reqBody
48211
+ });
48212
+ const text = await upstream.text();
48213
+ if (!upstream.ok) {
48214
+ res.writeHead(upstream.status, { "Content-Type": "application/json" });
48215
+ res.end(text);
48216
+ return;
48217
+ }
48218
+ let result;
48219
+ try {
48220
+ result = JSON.parse(text);
48221
+ } catch {
48222
+ res.writeHead(200, { "Content-Type": "application/json" });
48223
+ res.end(text);
48224
+ return;
48225
+ }
48226
+ if (result.data?.length) {
48227
+ await mkdir3(IMAGE_DIR, { recursive: true });
48228
+ const port2 = server.address()?.port ?? 8402;
48229
+ for (const img of result.data) {
48230
+ const dataUriMatch = img.url?.match(/^data:(image\/\w+);base64,(.+)$/);
48231
+ if (dataUriMatch) {
48232
+ const [, mimeType, b64] = dataUriMatch;
48233
+ const ext = mimeType === "image/jpeg" ? "jpg" : mimeType.split("/")[1] ?? "png";
48234
+ const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
48235
+ await writeFile2(join8(IMAGE_DIR, filename), Buffer.from(b64, "base64"));
48236
+ img.url = `http://localhost:${port2}/images/${filename}`;
48237
+ console.log(`[ClawRouter] Image saved \u2192 ${img.url}`);
48238
+ } else if (img.url?.startsWith("https://") || img.url?.startsWith("http://")) {
48239
+ try {
48240
+ const imgResp = await fetch(img.url);
48241
+ if (imgResp.ok) {
48242
+ const contentType = imgResp.headers.get("content-type") ?? "image/png";
48243
+ const ext = contentType.includes("jpeg") || contentType.includes("jpg") ? "jpg" : contentType.includes("webp") ? "webp" : "png";
48244
+ const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
48245
+ const buf = Buffer.from(await imgResp.arrayBuffer());
48246
+ await writeFile2(join8(IMAGE_DIR, filename), buf);
48247
+ img.url = `http://localhost:${port2}/images/${filename}`;
48248
+ console.log(`[ClawRouter] Image downloaded & saved \u2192 ${img.url}`);
48249
+ }
48250
+ } catch (downloadErr) {
48251
+ console.warn(
48252
+ `[ClawRouter] Failed to download image, using original URL: ${downloadErr instanceof Error ? downloadErr.message : String(downloadErr)}`
48253
+ );
48197
48254
  }
48198
- } catch (downloadErr) {
48199
- console.warn(
48200
- `[ClawRouter] Failed to download image, using original URL: ${downloadErr instanceof Error ? downloadErr.message : String(downloadErr)}`
48201
- );
48202
48255
  }
48203
48256
  }
48204
48257
  }
48258
+ const img2imgActualCost = paymentStore.getStore()?.amountUsd ?? img2imgCost;
48259
+ logUsage({
48260
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
48261
+ model: img2imgModel,
48262
+ tier: "IMAGE",
48263
+ cost: img2imgActualCost,
48264
+ baselineCost: img2imgActualCost,
48265
+ savings: 0,
48266
+ latencyMs: Date.now() - img2imgStartTime
48267
+ }).catch(() => {
48268
+ });
48269
+ res.writeHead(200, { "Content-Type": "application/json" });
48270
+ res.end(JSON.stringify(result));
48271
+ } catch (err) {
48272
+ const msg = err instanceof Error ? err.message : String(err);
48273
+ console.error(`[ClawRouter] Image editing error: ${msg}`);
48274
+ if (!res.headersSent) {
48275
+ res.writeHead(502, { "Content-Type": "application/json" });
48276
+ res.end(JSON.stringify({ error: "Image editing failed", details: msg }));
48277
+ }
48205
48278
  }
48206
- res.writeHead(200, { "Content-Type": "application/json" });
48207
- res.end(JSON.stringify(result));
48208
- } catch (err) {
48209
- const msg = err instanceof Error ? err.message : String(err);
48210
- console.error(`[ClawRouter] Image editing error: ${msg}`);
48211
- if (!res.headersSent) {
48212
- res.writeHead(502, { "Content-Type": "application/json" });
48213
- res.end(JSON.stringify({ error: "Image editing failed", details: msg }));
48279
+ return;
48280
+ }
48281
+ if (req.url?.match(/^\/v1\/(?:x|partner)\//)) {
48282
+ try {
48283
+ await proxyPartnerRequest(req, res, apiBase, payFetch, () => paymentStore.getStore()?.amountUsd ?? 0);
48284
+ } catch (err) {
48285
+ const error = err instanceof Error ? err : new Error(String(err));
48286
+ options.onError?.(error);
48287
+ if (!res.headersSent) {
48288
+ res.writeHead(502, { "Content-Type": "application/json" });
48289
+ res.end(
48290
+ JSON.stringify({
48291
+ error: { message: `Partner proxy error: ${error.message}`, type: "partner_error" }
48292
+ })
48293
+ );
48294
+ }
48214
48295
  }
48296
+ return;
48297
+ }
48298
+ if (!req.url?.startsWith("/v1")) {
48299
+ res.writeHead(404, { "Content-Type": "application/json" });
48300
+ res.end(JSON.stringify({ error: "Not found" }));
48301
+ return;
48215
48302
  }
48216
- return;
48217
- }
48218
- if (req.url?.match(/^\/v1\/(?:x|partner)\//)) {
48219
48303
  try {
48220
- await proxyPartnerRequest(req, res, apiBase, payFetch);
48304
+ await proxyRequest(
48305
+ req,
48306
+ res,
48307
+ apiBase,
48308
+ payFetch,
48309
+ options,
48310
+ routerOpts,
48311
+ deduplicator,
48312
+ balanceMonitor,
48313
+ sessionStore,
48314
+ responseCache2,
48315
+ sessionJournal
48316
+ );
48221
48317
  } catch (err) {
48222
48318
  const error = err instanceof Error ? err : new Error(String(err));
48223
48319
  options.onError?.(error);
@@ -48225,52 +48321,20 @@ async function startProxy(options) {
48225
48321
  res.writeHead(502, { "Content-Type": "application/json" });
48226
48322
  res.end(
48227
48323
  JSON.stringify({
48228
- error: { message: `Partner proxy error: ${error.message}`, type: "partner_error" }
48324
+ error: { message: `Proxy error: ${error.message}`, type: "proxy_error" }
48229
48325
  })
48230
48326
  );
48231
- }
48232
- }
48233
- return;
48234
- }
48235
- if (!req.url?.startsWith("/v1")) {
48236
- res.writeHead(404, { "Content-Type": "application/json" });
48237
- res.end(JSON.stringify({ error: "Not found" }));
48238
- return;
48239
- }
48240
- try {
48241
- await proxyRequest(
48242
- req,
48243
- res,
48244
- apiBase,
48245
- payFetch,
48246
- options,
48247
- routerOpts,
48248
- deduplicator,
48249
- balanceMonitor,
48250
- sessionStore,
48251
- responseCache2,
48252
- sessionJournal
48253
- );
48254
- } catch (err) {
48255
- const error = err instanceof Error ? err : new Error(String(err));
48256
- options.onError?.(error);
48257
- if (!res.headersSent) {
48258
- res.writeHead(502, { "Content-Type": "application/json" });
48259
- res.end(
48260
- JSON.stringify({
48261
- error: { message: `Proxy error: ${error.message}`, type: "proxy_error" }
48262
- })
48263
- );
48264
- } else if (!res.writableEnded) {
48265
- res.write(
48266
- `data: ${JSON.stringify({ error: { message: error.message, type: "proxy_error" } })}
48327
+ } else if (!res.writableEnded) {
48328
+ res.write(
48329
+ `data: ${JSON.stringify({ error: { message: error.message, type: "proxy_error" } })}
48267
48330
 
48268
48331
  `
48269
- );
48270
- res.write("data: [DONE]\n\n");
48271
- res.end();
48332
+ );
48333
+ res.write("data: [DONE]\n\n");
48334
+ res.end();
48335
+ }
48272
48336
  }
48273
- }
48337
+ });
48274
48338
  });
48275
48339
  const tryListen = (attempt) => {
48276
48340
  return new Promise((resolveAttempt, rejectAttempt) => {
@@ -48784,6 +48848,17 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
48784
48848
  responseText = lines.join("\n");
48785
48849
  }
48786
48850
  console.log(`[ClawRouter] /imagegen success: ${images.length} image(s) generated`);
48851
+ const imagegenActualCost = paymentStore.getStore()?.amountUsd ?? estimateImageCost(imageModel, imageSize, 1);
48852
+ logUsage({
48853
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
48854
+ model: imageModel,
48855
+ tier: "IMAGE",
48856
+ cost: imagegenActualCost,
48857
+ baselineCost: imagegenActualCost,
48858
+ savings: 0,
48859
+ latencyMs: 0
48860
+ }).catch(() => {
48861
+ });
48787
48862
  }
48788
48863
  const completionId = `chatcmpl-image-${Date.now()}`;
48789
48864
  const timestamp = Math.floor(Date.now() / 1e3);
@@ -48993,6 +49068,17 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
48993
49068
  responseText = lines.join("\n");
48994
49069
  }
48995
49070
  console.log(`[ClawRouter] /img2img success: ${images.length} image(s)`);
49071
+ const img2imgActualCost2 = paymentStore.getStore()?.amountUsd ?? estimateImageCost(img2imgModel, img2imgSize, 1);
49072
+ logUsage({
49073
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
49074
+ model: img2imgModel,
49075
+ tier: "IMAGE",
49076
+ cost: img2imgActualCost2,
49077
+ baselineCost: img2imgActualCost2,
49078
+ savings: 0,
49079
+ latencyMs: 0
49080
+ }).catch(() => {
49081
+ });
48996
49082
  }
48997
49083
  sendImg2ImgText(responseText);
48998
49084
  } catch (err) {
@@ -50091,22 +50177,44 @@ data: [DONE]
50091
50177
  }
50092
50178
  const logModel = routingDecision?.model ?? modelId;
50093
50179
  if (logModel) {
50094
- const actualInputTokens = responseInputTokens ?? Math.ceil(body.length / 4);
50095
- const actualOutputTokens = responseOutputTokens ?? maxTokens;
50096
- const accurateCosts = calculateModelCost(
50097
- logModel,
50098
- routerOpts.modelPricing,
50099
- actualInputTokens,
50100
- actualOutputTokens,
50101
- routingProfile ?? void 0
50102
- );
50180
+ const actualPayment = paymentStore.getStore()?.amountUsd ?? 0;
50181
+ let logCost;
50182
+ let logBaseline;
50183
+ let logSavings;
50184
+ if (actualPayment > 0) {
50185
+ logCost = actualPayment;
50186
+ const chargedInputTokens = Math.ceil(body.length / 4);
50187
+ const modelDef = BLOCKRUN_MODELS.find((m) => m.id === logModel);
50188
+ const chargedOutputTokens = modelDef ? Math.min(maxTokens, modelDef.maxOutput) : maxTokens;
50189
+ const baseline = calculateModelCost(
50190
+ logModel,
50191
+ routerOpts.modelPricing,
50192
+ chargedInputTokens,
50193
+ chargedOutputTokens,
50194
+ routingProfile ?? void 0
50195
+ );
50196
+ logBaseline = baseline.baselineCost;
50197
+ logSavings = logBaseline > 0 ? Math.max(0, (logBaseline - logCost) / logBaseline) : 0;
50198
+ } else {
50199
+ const chargedInputTokens = Math.ceil(body.length / 4);
50200
+ const costs = calculateModelCost(
50201
+ logModel,
50202
+ routerOpts.modelPricing,
50203
+ chargedInputTokens,
50204
+ maxTokens,
50205
+ routingProfile ?? void 0
50206
+ );
50207
+ logCost = costs.costEstimate;
50208
+ logBaseline = costs.baselineCost;
50209
+ logSavings = costs.savings;
50210
+ }
50103
50211
  const entry = {
50104
50212
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
50105
50213
  model: logModel,
50106
50214
  tier: routingDecision?.tier ?? "DIRECT",
50107
- cost: accurateCosts.costEstimate,
50108
- baselineCost: accurateCosts.baselineCost,
50109
- savings: accurateCosts.savings,
50215
+ cost: logCost,
50216
+ baselineCost: logBaseline,
50217
+ savings: logSavings,
50110
50218
  latencyMs: Date.now() - startTime,
50111
50219
  ...responseInputTokens !== void 0 && { inputTokens: responseInputTokens },
50112
50220
  ...responseOutputTokens !== void 0 && { outputTokens: responseOutputTokens }