@blockrun/clawrouter 0.12.9 → 0.12.11

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
@@ -45,7 +45,12 @@ function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS, op
45
45
  const getHeader = (name) => response.headers.get(name);
46
46
  let body;
47
47
  try {
48
- const responseText = await response.text();
48
+ const responseText = await Promise.race([
49
+ response.text(),
50
+ new Promise(
51
+ (_, reject) => setTimeout(() => reject(new Error("Body read timeout")), 6e4)
52
+ )
53
+ ]);
49
54
  if (responseText) body = JSON.parse(responseText);
50
55
  } catch {
51
56
  }
@@ -2317,7 +2322,7 @@ async function logUsage(entry) {
2317
2322
  }
2318
2323
 
2319
2324
  // src/stats.ts
2320
- import { readdir } from "fs/promises";
2325
+ import { readdir, unlink } from "fs/promises";
2321
2326
 
2322
2327
  // src/fs-read.ts
2323
2328
  import { open } from "fs/promises";
@@ -2486,6 +2491,16 @@ async function getStats(days = 7) {
2486
2491
  // How many entries have valid baseline tracking
2487
2492
  };
2488
2493
  }
2494
+ async function clearStats() {
2495
+ try {
2496
+ const files = await readdir(LOG_DIR2);
2497
+ const logFiles = files.filter((f) => f.startsWith("usage-") && f.endsWith(".jsonl"));
2498
+ await Promise.all(logFiles.map((f) => unlink(join3(LOG_DIR2, f))));
2499
+ return { deletedFiles: logFiles.length };
2500
+ } catch {
2501
+ return { deletedFiles: 0 };
2502
+ }
2503
+ }
2489
2504
 
2490
2505
  // src/dedup.ts
2491
2506
  import { createHash } from "crypto";
@@ -4972,6 +4987,27 @@ var HEALTH_CHECK_TIMEOUT_MS = 2e3;
4972
4987
  var RATE_LIMIT_COOLDOWN_MS = 6e4;
4973
4988
  var PORT_RETRY_ATTEMPTS = 5;
4974
4989
  var PORT_RETRY_DELAY_MS = 1e3;
4990
+ var BODY_READ_TIMEOUT_MS = 6e4;
4991
+ async function readBodyWithTimeout(body, timeoutMs = BODY_READ_TIMEOUT_MS) {
4992
+ if (!body) return [];
4993
+ const reader = body.getReader();
4994
+ const chunks = [];
4995
+ try {
4996
+ while (true) {
4997
+ const result = await Promise.race([
4998
+ reader.read(),
4999
+ new Promise(
5000
+ (_, reject) => setTimeout(() => reject(new Error("Body read timeout")), timeoutMs)
5001
+ )
5002
+ ]);
5003
+ if (result.done) break;
5004
+ chunks.push(result.value);
5005
+ }
5006
+ } finally {
5007
+ reader.releaseLock();
5008
+ }
5009
+ return chunks;
5010
+ }
4975
5011
  function transformPaymentError(errorBody) {
4976
5012
  try {
4977
5013
  const parsed = JSON.parse(errorBody);
@@ -5011,9 +5047,21 @@ function transformPaymentError(errorBody) {
5011
5047
  }
5012
5048
  });
5013
5049
  }
5050
+ if (innerJson.invalidReason === "transaction_simulation_failed") {
5051
+ console.error(
5052
+ `[ClawRouter] Solana transaction simulation failed: ${innerJson.invalidMessage || "unknown"}`
5053
+ );
5054
+ return JSON.stringify({
5055
+ error: {
5056
+ message: "Solana payment simulation failed. Retrying with a different model.",
5057
+ type: "transaction_simulation_failed",
5058
+ help: "This is usually temporary. If it persists, check your Solana USDC balance or try: /model free"
5059
+ }
5060
+ });
5061
+ }
5014
5062
  }
5015
5063
  }
5016
- if (parsed.error === "Settlement failed" || parsed.details?.includes("Settlement failed")) {
5064
+ if (parsed.error === "Settlement failed" || parsed.error === "Payment settlement failed" || parsed.details?.includes("Settlement failed") || parsed.details?.includes("transaction_simulation_failed")) {
5017
5065
  const details = parsed.details || "";
5018
5066
  const gasError = details.includes("unable to estimate gas");
5019
5067
  return JSON.stringify({
@@ -5459,15 +5507,9 @@ async function proxyPartnerRequest(req, res, apiBase, payFetch) {
5459
5507
  });
5460
5508
  res.writeHead(upstream.status, responseHeaders);
5461
5509
  if (upstream.body) {
5462
- const reader = upstream.body.getReader();
5463
- try {
5464
- while (true) {
5465
- const { done, value } = await reader.read();
5466
- if (done) break;
5467
- safeWrite(res, Buffer.from(value));
5468
- }
5469
- } finally {
5470
- reader.releaseLock();
5510
+ const chunks = await readBodyWithTimeout(upstream.body);
5511
+ for (const chunk of chunks) {
5512
+ safeWrite(res, Buffer.from(chunk));
5471
5513
  }
5472
5514
  }
5473
5515
  res.end();
@@ -5497,16 +5539,23 @@ async function uploadDataUriToHost(dataUri) {
5497
5539
  const form = new FormData();
5498
5540
  form.append("reqtype", "fileupload");
5499
5541
  form.append("fileToUpload", blob, `image.${ext}`);
5500
- const resp = await fetch("https://catbox.moe/user/api.php", {
5501
- method: "POST",
5502
- body: form
5503
- });
5504
- if (!resp.ok) throw new Error(`catbox.moe upload failed: HTTP ${resp.status}`);
5505
- const result = await resp.text();
5506
- if (result.startsWith("https://")) {
5507
- return result.trim();
5542
+ const uploadController = new AbortController();
5543
+ const uploadTimeout = setTimeout(() => uploadController.abort(), 3e4);
5544
+ try {
5545
+ const resp = await fetch("https://catbox.moe/user/api.php", {
5546
+ method: "POST",
5547
+ body: form,
5548
+ signal: uploadController.signal
5549
+ });
5550
+ if (!resp.ok) throw new Error(`catbox.moe upload failed: HTTP ${resp.status}`);
5551
+ const result = await resp.text();
5552
+ if (result.startsWith("https://")) {
5553
+ return result.trim();
5554
+ }
5555
+ throw new Error(`catbox.moe upload failed: ${result}`);
5556
+ } finally {
5557
+ clearTimeout(uploadTimeout);
5508
5558
  }
5509
- throw new Error(`catbox.moe upload failed: ${result}`);
5510
5559
  }
5511
5560
  async function startProxy(options) {
5512
5561
  const walletKey = typeof options.wallet === "string" ? options.wallet : options.wallet.key;
@@ -5647,6 +5696,21 @@ async function startProxy(options) {
5647
5696
  res.end(JSON.stringify(stats, null, 2));
5648
5697
  return;
5649
5698
  }
5699
+ if (req.url === "/stats" && req.method === "DELETE") {
5700
+ try {
5701
+ const result = await clearStats();
5702
+ res.writeHead(200, { "Content-Type": "application/json" });
5703
+ res.end(JSON.stringify({ cleared: true, deletedFiles: result.deletedFiles }));
5704
+ } catch (err) {
5705
+ res.writeHead(500, { "Content-Type": "application/json" });
5706
+ res.end(
5707
+ JSON.stringify({
5708
+ error: `Failed to clear stats: ${err instanceof Error ? err.message : String(err)}`
5709
+ })
5710
+ );
5711
+ }
5712
+ return;
5713
+ }
5650
5714
  if (req.url === "/stats" || req.url?.startsWith("/stats?")) {
5651
5715
  try {
5652
5716
  const url = new URL(req.url, "http://localhost");
@@ -5893,7 +5957,8 @@ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxT
5893
5957
  signal
5894
5958
  });
5895
5959
  if (response.status !== 200) {
5896
- const errorBody = await response.text();
5960
+ const errorBodyChunks = await readBodyWithTimeout(response.body);
5961
+ const errorBody = Buffer.concat(errorBodyChunks).toString();
5897
5962
  const isProviderErr = isProviderError(response.status, errorBody);
5898
5963
  return {
5899
5964
  success: false,
@@ -5905,7 +5970,8 @@ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxT
5905
5970
  const contentType = response.headers.get("content-type") || "";
5906
5971
  if (contentType.includes("json") || contentType.includes("text")) {
5907
5972
  try {
5908
- const responseBody = await response.clone().text();
5973
+ const clonedChunks = await readBodyWithTimeout(response.clone().body);
5974
+ const responseBody = Buffer.concat(clonedChunks).toString();
5909
5975
  const degradedReason = detectDegradedSuccessResponse(responseBody);
5910
5976
  if (degradedReason) {
5911
5977
  return {
@@ -6706,7 +6772,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
6706
6772
  if (result.errorStatus === 429) {
6707
6773
  markRateLimited(tryModel);
6708
6774
  }
6709
- const isPaymentErr = /payment.*verification.*failed|insufficient.*funds/i.test(
6775
+ const isPaymentErr = /payment.*verification.*failed|payment.*settlement.*failed|insufficient.*funds|transaction_simulation_failed/i.test(
6710
6776
  result.errorBody || ""
6711
6777
  );
6712
6778
  if (isPaymentErr && tryModel !== FREE_MODEL) {
@@ -6811,17 +6877,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
6811
6877
  const responseChunks = [];
6812
6878
  if (headersSentEarly) {
6813
6879
  if (upstream.body) {
6814
- const reader = upstream.body.getReader();
6815
- const chunks = [];
6816
- try {
6817
- while (true) {
6818
- const { done, value } = await reader.read();
6819
- if (done) break;
6820
- chunks.push(value);
6821
- }
6822
- } finally {
6823
- reader.releaseLock();
6824
- }
6880
+ const chunks = await readBodyWithTimeout(upstream.body);
6825
6881
  const jsonBody = Buffer.concat(chunks);
6826
6882
  const jsonStr = jsonBody.toString();
6827
6883
  try {
@@ -6960,15 +7016,9 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
6960
7016
  }
6961
7017
  const bodyParts = [];
6962
7018
  if (upstream.body) {
6963
- const reader = upstream.body.getReader();
6964
- try {
6965
- while (true) {
6966
- const { done, value } = await reader.read();
6967
- if (done) break;
6968
- bodyParts.push(Buffer.from(value));
6969
- }
6970
- } finally {
6971
- reader.releaseLock();
7019
+ const chunks = await readBodyWithTimeout(upstream.body);
7020
+ for (const chunk of chunks) {
7021
+ bodyParts.push(Buffer.from(chunk));
6972
7022
  }
6973
7023
  }
6974
7024
  let responseBody = Buffer.concat(bodyParts);