@blockrun/clawrouter 0.8.10 → 0.8.12

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.d.ts CHANGED
@@ -860,6 +860,7 @@ type AggregatedStats = {
860
860
  percentage: number;
861
861
  }>;
862
862
  dailyBreakdown: DailyStats[];
863
+ entriesWithBaseline: number;
863
864
  };
864
865
  /**
865
866
  * Get aggregated statistics for the last N days.
package/dist/index.js CHANGED
@@ -1829,6 +1829,12 @@ async function getStats(days = 7) {
1829
1829
  }
1830
1830
  const totalSavings = totalBaselineCost - totalCost;
1831
1831
  const savingsPercentage = totalBaselineCost > 0 ? totalSavings / totalBaselineCost * 100 : 0;
1832
+ let entriesWithBaseline = 0;
1833
+ for (const day of dailyBreakdown) {
1834
+ if (day.totalBaselineCost !== day.totalCost) {
1835
+ entriesWithBaseline += day.totalRequests;
1836
+ }
1837
+ }
1832
1838
  return {
1833
1839
  period: days === 1 ? "today" : `last ${days} days`,
1834
1840
  totalRequests,
@@ -1840,8 +1846,10 @@ async function getStats(days = 7) {
1840
1846
  avgCostPerRequest: totalRequests > 0 ? totalCost / totalRequests : 0,
1841
1847
  byTier: byTierWithPercentage,
1842
1848
  byModel: byModelWithPercentage,
1843
- dailyBreakdown: dailyBreakdown.reverse()
1849
+ dailyBreakdown: dailyBreakdown.reverse(),
1844
1850
  // Oldest first for charts
1851
+ entriesWithBaseline
1852
+ // How many entries have valid baseline tracking
1845
1853
  };
1846
1854
  }
1847
1855
  function formatStatsAscii(stats) {
@@ -1853,20 +1861,27 @@ function formatStatsAscii(stats) {
1853
1861
  lines.push(`\u2551 Total Requests: ${stats.totalRequests.toString().padEnd(41)}\u2551`);
1854
1862
  lines.push(`\u2551 Total Cost: $${stats.totalCost.toFixed(4).padEnd(43)}\u2551`);
1855
1863
  lines.push(`\u2551 Baseline Cost (Opus): $${stats.totalBaselineCost.toFixed(4).padEnd(33)}\u2551`);
1856
- lines.push(
1857
- `\u2551 \u{1F4B0} Total Saved: $${stats.totalSavings.toFixed(4)} (${stats.savingsPercentage.toFixed(1)}%)`.padEnd(
1858
- 61
1859
- ) + "\u2551"
1860
- );
1864
+ const savingsLine = `\u2551 \u{1F4B0} Total Saved: $${stats.totalSavings.toFixed(4)} (${stats.savingsPercentage.toFixed(1)}%)`;
1865
+ if (stats.entriesWithBaseline < stats.totalRequests && stats.entriesWithBaseline > 0) {
1866
+ lines.push(savingsLine.padEnd(61) + "\u2551");
1867
+ const note = `\u2551 (based on ${stats.entriesWithBaseline}/${stats.totalRequests} tracked requests)`;
1868
+ lines.push(note.padEnd(61) + "\u2551");
1869
+ } else {
1870
+ lines.push(savingsLine.padEnd(61) + "\u2551");
1871
+ }
1861
1872
  lines.push(`\u2551 Avg Latency: ${stats.avgLatencyMs.toFixed(0)}ms`.padEnd(61) + "\u2551");
1862
1873
  lines.push("\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563");
1863
1874
  lines.push("\u2551 Routing by Tier: \u2551");
1864
- const tierOrder = ["SIMPLE", "MEDIUM", "COMPLEX", "REASONING"];
1875
+ const knownTiers = ["SIMPLE", "MEDIUM", "COMPLEX", "REASONING"];
1876
+ const allTiers = Object.keys(stats.byTier);
1877
+ const otherTiers = allTiers.filter((t) => !knownTiers.includes(t));
1878
+ const tierOrder = [...knownTiers.filter((t) => stats.byTier[t]), ...otherTiers];
1865
1879
  for (const tier of tierOrder) {
1866
1880
  const data = stats.byTier[tier];
1867
1881
  if (data) {
1868
1882
  const bar = "\u2588".repeat(Math.min(20, Math.round(data.percentage / 5)));
1869
- const line = `\u2551 ${tier.padEnd(10)} ${bar.padEnd(20)} ${data.percentage.toFixed(1).padStart(5)}% (${data.count})`;
1883
+ const displayTier = tier === "UNKNOWN" ? "OTHER" : tier;
1884
+ const line = `\u2551 ${displayTier.padEnd(10)} ${bar.padEnd(20)} ${data.percentage.toFixed(1).padStart(5)}% (${data.count})`;
1870
1885
  lines.push(line.padEnd(61) + "\u2551");
1871
1886
  }
1872
1887
  }
@@ -2347,6 +2362,42 @@ var HEALTH_CHECK_TIMEOUT_MS = 2e3;
2347
2362
  var RATE_LIMIT_COOLDOWN_MS = 6e4;
2348
2363
  var PORT_RETRY_ATTEMPTS = 5;
2349
2364
  var PORT_RETRY_DELAY_MS = 1e3;
2365
+ function transformPaymentError(errorBody) {
2366
+ try {
2367
+ const parsed = JSON.parse(errorBody);
2368
+ if (parsed.error === "Payment verification failed" && parsed.details) {
2369
+ const match = parsed.details.match(/Verification failed:\s*(\{.*\})/s);
2370
+ if (match) {
2371
+ const innerJson = JSON.parse(match[1]);
2372
+ if (innerJson.invalidReason === "insufficient_funds" && innerJson.invalidMessage) {
2373
+ const balanceMatch = innerJson.invalidMessage.match(
2374
+ /insufficient balance:\s*(\d+)\s*<\s*(\d+)/i
2375
+ );
2376
+ if (balanceMatch) {
2377
+ const currentMicros = parseInt(balanceMatch[1], 10);
2378
+ const requiredMicros = parseInt(balanceMatch[2], 10);
2379
+ const currentUSD = (currentMicros / 1e6).toFixed(6);
2380
+ const requiredUSD = (requiredMicros / 1e6).toFixed(6);
2381
+ const wallet = innerJson.payer || "unknown";
2382
+ const shortWallet = wallet.length > 12 ? `${wallet.slice(0, 6)}...${wallet.slice(-4)}` : wallet;
2383
+ return JSON.stringify({
2384
+ error: {
2385
+ message: `Insufficient USDC balance. Current: $${currentUSD}, Required: ~$${requiredUSD}`,
2386
+ type: "insufficient_funds",
2387
+ wallet,
2388
+ current_balance_usd: currentUSD,
2389
+ required_usd: requiredUSD,
2390
+ help: `Fund wallet ${shortWallet} with USDC on Base, or use free model: /model free`
2391
+ }
2392
+ });
2393
+ }
2394
+ }
2395
+ }
2396
+ }
2397
+ } catch {
2398
+ }
2399
+ return errorBody;
2400
+ }
2350
2401
  var rateLimitedModels = /* @__PURE__ */ new Map();
2351
2402
  function isRateLimited(modelId) {
2352
2403
  const hitTime = rateLimitedModels.get(modelId);
@@ -3215,10 +3266,18 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
3215
3266
  options.onRouted?.(routingDecision);
3216
3267
  }
3217
3268
  if (!upstream) {
3218
- const errBody = lastError?.body || "All models in fallback chain failed";
3269
+ const rawErrBody = lastError?.body || "All models in fallback chain failed";
3219
3270
  const errStatus = lastError?.status || 502;
3271
+ const transformedErr = transformPaymentError(rawErrBody);
3220
3272
  if (headersSentEarly) {
3221
- const errEvent = `data: ${JSON.stringify({ error: { message: errBody, type: "provider_error", status: errStatus } })}
3273
+ let errPayload;
3274
+ try {
3275
+ const parsed = JSON.parse(transformedErr);
3276
+ errPayload = JSON.stringify(parsed);
3277
+ } catch {
3278
+ errPayload = JSON.stringify({ error: { message: rawErrBody, type: "provider_error", status: errStatus } });
3279
+ }
3280
+ const errEvent = `data: ${errPayload}
3222
3281
 
3223
3282
  `;
3224
3283
  safeWrite(res, errEvent);
@@ -3233,17 +3292,11 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
3233
3292
  });
3234
3293
  } else {
3235
3294
  res.writeHead(errStatus, { "Content-Type": "application/json" });
3236
- res.end(
3237
- JSON.stringify({
3238
- error: { message: errBody, type: "provider_error" }
3239
- })
3240
- );
3295
+ res.end(transformedErr);
3241
3296
  deduplicator.complete(dedupKey, {
3242
3297
  status: errStatus,
3243
3298
  headers: { "content-type": "application/json" },
3244
- body: Buffer.from(
3245
- JSON.stringify({ error: { message: errBody, type: "provider_error" } })
3246
- ),
3299
+ body: Buffer.from(transformedErr),
3247
3300
  completedAt: Date.now()
3248
3301
  });
3249
3302
  }