@blockrun/clawrouter 0.12.34 → 0.12.36

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
@@ -1,4 +1,132 @@
1
1
  #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/solana-balance.ts
13
+ var solana_balance_exports = {};
14
+ __export(solana_balance_exports, {
15
+ SolanaBalanceMonitor: () => SolanaBalanceMonitor
16
+ });
17
+ import { address as solAddress, createSolanaRpc } from "@solana/kit";
18
+ var SOLANA_USDC_MINT, SOLANA_DEFAULT_RPC, BALANCE_TIMEOUT_MS, CACHE_TTL_MS2, SolanaBalanceMonitor;
19
+ var init_solana_balance = __esm({
20
+ "src/solana-balance.ts"() {
21
+ "use strict";
22
+ SOLANA_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
23
+ SOLANA_DEFAULT_RPC = "https://api.mainnet-beta.solana.com";
24
+ BALANCE_TIMEOUT_MS = 1e4;
25
+ CACHE_TTL_MS2 = 3e4;
26
+ SolanaBalanceMonitor = class {
27
+ rpc;
28
+ walletAddress;
29
+ cachedBalance = null;
30
+ cachedAt = 0;
31
+ constructor(walletAddress, rpcUrl) {
32
+ this.walletAddress = walletAddress;
33
+ const url = rpcUrl || process["env"].CLAWROUTER_SOLANA_RPC_URL || SOLANA_DEFAULT_RPC;
34
+ this.rpc = createSolanaRpc(url);
35
+ }
36
+ async checkBalance() {
37
+ const now = Date.now();
38
+ if (this.cachedBalance !== null && this.cachedBalance > 0n && now - this.cachedAt < CACHE_TTL_MS2) {
39
+ return this.buildInfo(this.cachedBalance);
40
+ }
41
+ const balance = await this.fetchBalance();
42
+ if (balance > 0n) {
43
+ this.cachedBalance = balance;
44
+ this.cachedAt = now;
45
+ }
46
+ return this.buildInfo(balance);
47
+ }
48
+ deductEstimated(amountMicros) {
49
+ if (this.cachedBalance !== null && this.cachedBalance >= amountMicros) {
50
+ this.cachedBalance -= amountMicros;
51
+ }
52
+ }
53
+ invalidate() {
54
+ this.cachedBalance = null;
55
+ this.cachedAt = 0;
56
+ }
57
+ async refresh() {
58
+ this.invalidate();
59
+ return this.checkBalance();
60
+ }
61
+ /**
62
+ * Check if balance is sufficient for an estimated cost.
63
+ */
64
+ async checkSufficient(estimatedCostMicros) {
65
+ const info = await this.checkBalance();
66
+ if (info.balance >= estimatedCostMicros) {
67
+ return { sufficient: true, info };
68
+ }
69
+ const shortfall = estimatedCostMicros - info.balance;
70
+ return {
71
+ sufficient: false,
72
+ info,
73
+ shortfall: this.formatUSDC(shortfall)
74
+ };
75
+ }
76
+ /**
77
+ * Format USDC amount (in micros) as "$X.XX".
78
+ */
79
+ formatUSDC(amountMicros) {
80
+ const dollars = Number(amountMicros) / 1e6;
81
+ return `$${dollars.toFixed(2)}`;
82
+ }
83
+ getWalletAddress() {
84
+ return this.walletAddress;
85
+ }
86
+ async fetchBalance() {
87
+ const owner = solAddress(this.walletAddress);
88
+ const mint = solAddress(SOLANA_USDC_MINT);
89
+ for (let attempt = 0; attempt < 2; attempt++) {
90
+ const result = await this.fetchBalanceOnce(owner, mint);
91
+ if (result > 0n || attempt === 1) return result;
92
+ await new Promise((r) => setTimeout(r, 1e3));
93
+ }
94
+ return 0n;
95
+ }
96
+ async fetchBalanceOnce(owner, mint) {
97
+ const controller = new AbortController();
98
+ const timer = setTimeout(() => controller.abort(), BALANCE_TIMEOUT_MS);
99
+ try {
100
+ const response = await this.rpc.getTokenAccountsByOwner(owner, { mint }, { encoding: "jsonParsed" }).send({ abortSignal: controller.signal });
101
+ if (response.value.length === 0) return 0n;
102
+ let total = 0n;
103
+ for (const account of response.value) {
104
+ const parsed = account.account.data;
105
+ total += BigInt(parsed.parsed.info.tokenAmount.amount);
106
+ }
107
+ return total;
108
+ } catch (err) {
109
+ throw new Error(
110
+ `Failed to fetch Solana USDC balance: ${err instanceof Error ? err.message : String(err)}`,
111
+ { cause: err }
112
+ );
113
+ } finally {
114
+ clearTimeout(timer);
115
+ }
116
+ }
117
+ buildInfo(balance) {
118
+ const dollars = Number(balance) / 1e6;
119
+ return {
120
+ balance,
121
+ balanceUSD: `$${dollars.toFixed(2)}`,
122
+ isLow: balance < 1000000n,
123
+ isEmpty: balance < 100n,
124
+ walletAddress: this.walletAddress
125
+ };
126
+ }
127
+ };
128
+ }
129
+ });
2
130
 
3
131
  // src/proxy.ts
4
132
  import { createServer } from "http";
@@ -1429,11 +1557,14 @@ var DEFAULT_ROUTING_CONFIG = {
1429
1557
  primary: "moonshot/kimi-k2.5",
1430
1558
  // $0.60/$3.00 - best quality/price for simple tasks
1431
1559
  fallback: [
1560
+ "google/gemini-2.5-flash",
1561
+ // 60% retention (best), fast growth (+800%)
1432
1562
  "google/gemini-2.5-flash-lite",
1433
1563
  // 1M context, ultra cheap ($0.10/$0.40)
1434
- "nvidia/gpt-oss-120b",
1564
+ "deepseek/deepseek-chat",
1565
+ // 41% retention
1566
+ "nvidia/gpt-oss-120b"
1435
1567
  // FREE fallback
1436
- "deepseek/deepseek-chat"
1437
1568
  ]
1438
1569
  },
1439
1570
  MEDIUM: {
@@ -1441,6 +1572,9 @@ var DEFAULT_ROUTING_CONFIG = {
1441
1572
  // $0.50/$2.40 - strong tool use, proper function call format
1442
1573
  fallback: [
1443
1574
  "deepseek/deepseek-chat",
1575
+ // 41% retention
1576
+ "google/gemini-2.5-flash",
1577
+ // 60% retention, cheap fast model
1444
1578
  "google/gemini-2.5-flash-lite",
1445
1579
  // 1M context, ultra cheap ($0.10/$0.40)
1446
1580
  "xai/grok-4-1-fast-non-reasoning"
@@ -1451,6 +1585,8 @@ var DEFAULT_ROUTING_CONFIG = {
1451
1585
  primary: "google/gemini-3.1-pro",
1452
1586
  // Newest Gemini 3.1 - upgraded from 3.0
1453
1587
  fallback: [
1588
+ "google/gemini-2.5-flash",
1589
+ // 60% retention, cheap failsafe before expensive models
1454
1590
  "google/gemini-2.5-flash-lite",
1455
1591
  // CRITICAL: 1M context, ultra-cheap failsafe ($0.10/$0.40)
1456
1592
  "google/gemini-3-pro-preview",
@@ -1481,12 +1617,12 @@ var DEFAULT_ROUTING_CONFIG = {
1481
1617
  SIMPLE: {
1482
1618
  primary: "nvidia/gpt-oss-120b",
1483
1619
  // FREE! $0.00/$0.00
1484
- fallback: ["google/gemini-2.5-flash-lite", "deepseek/deepseek-chat"]
1620
+ fallback: ["google/gemini-2.5-flash-lite", "google/gemini-2.5-flash", "deepseek/deepseek-chat"]
1485
1621
  },
1486
1622
  MEDIUM: {
1487
1623
  primary: "google/gemini-2.5-flash-lite",
1488
1624
  // $0.10/$0.40 - cheapest capable with 1M context
1489
- fallback: ["deepseek/deepseek-chat", "nvidia/gpt-oss-120b"]
1625
+ fallback: ["google/gemini-2.5-flash", "deepseek/deepseek-chat", "nvidia/gpt-oss-120b"]
1490
1626
  },
1491
1627
  COMPLEX: {
1492
1628
  primary: "google/gemini-2.5-flash-lite",
@@ -1506,6 +1642,8 @@ var DEFAULT_ROUTING_CONFIG = {
1506
1642
  primary: "moonshot/kimi-k2.5",
1507
1643
  // $0.60/$3.00 - good for simple coding
1508
1644
  fallback: [
1645
+ "google/gemini-2.5-flash",
1646
+ // 60% retention, fast growth
1509
1647
  "anthropic/claude-haiku-4.5",
1510
1648
  "google/gemini-2.5-flash-lite",
1511
1649
  "deepseek/deepseek-chat"
@@ -1516,6 +1654,8 @@ var DEFAULT_ROUTING_CONFIG = {
1516
1654
  // $2.50/$10 - strong coding for medium tasks
1517
1655
  fallback: [
1518
1656
  "moonshot/kimi-k2.5",
1657
+ "google/gemini-2.5-flash",
1658
+ // 60% retention, good coding capability
1519
1659
  "google/gemini-2.5-pro",
1520
1660
  "xai/grok-4-0709",
1521
1661
  "anthropic/claude-sonnet-4.6"
@@ -1726,7 +1866,8 @@ var MODEL_ALIASES = {
1726
1866
  // xAI
1727
1867
  grok: "xai/grok-3",
1728
1868
  "grok-fast": "xai/grok-4-fast-reasoning",
1729
- "grok-code": "xai/grok-code-fast-1",
1869
+ "grok-code": "deepseek/deepseek-chat",
1870
+ // was grok-code-fast-1, delisted due to poor retention
1730
1871
  // NVIDIA
1731
1872
  nvidia: "nvidia/gpt-oss-120b",
1732
1873
  "gpt-120b": "nvidia/gpt-oss-120b",
@@ -2194,18 +2335,8 @@ var BLOCKRUN_MODELS = [
2194
2335
  maxOutput: 16384,
2195
2336
  toolCalling: true
2196
2337
  },
2197
- {
2198
- id: "xai/grok-code-fast-1",
2199
- name: "Grok Code Fast",
2200
- version: "1",
2201
- inputPrice: 0.2,
2202
- outputPrice: 1.5,
2203
- contextWindow: 131072,
2204
- maxOutput: 16384
2205
- // toolCalling intentionally omitted: outputs tool calls as plain text JSON,
2206
- // not OpenAI-compatible structured function calls. Will be skipped when
2207
- // request has tools to prevent the "talking to itself" bug.
2208
- },
2338
+ // xai/grok-code-fast-1 delisted 2026-03-12: poor retention (coding users churn),
2339
+ // no structured tool calling, alias "grok-code" redirected to deepseek-chat
2209
2340
  {
2210
2341
  id: "xai/grok-4-0709",
2211
2342
  name: "Grok 4 (0709)",
@@ -2973,115 +3104,6 @@ var BalanceMonitor = class {
2973
3104
  }
2974
3105
  };
2975
3106
 
2976
- // src/solana-balance.ts
2977
- import { address as solAddress, createSolanaRpc } from "@solana/kit";
2978
- var SOLANA_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
2979
- var SOLANA_DEFAULT_RPC = "https://api.mainnet-beta.solana.com";
2980
- var BALANCE_TIMEOUT_MS = 1e4;
2981
- var CACHE_TTL_MS2 = 3e4;
2982
- var SolanaBalanceMonitor = class {
2983
- rpc;
2984
- walletAddress;
2985
- cachedBalance = null;
2986
- cachedAt = 0;
2987
- constructor(walletAddress, rpcUrl) {
2988
- this.walletAddress = walletAddress;
2989
- const url = rpcUrl || process["env"].CLAWROUTER_SOLANA_RPC_URL || SOLANA_DEFAULT_RPC;
2990
- this.rpc = createSolanaRpc(url);
2991
- }
2992
- async checkBalance() {
2993
- const now = Date.now();
2994
- if (this.cachedBalance !== null && this.cachedBalance > 0n && now - this.cachedAt < CACHE_TTL_MS2) {
2995
- return this.buildInfo(this.cachedBalance);
2996
- }
2997
- const balance = await this.fetchBalance();
2998
- if (balance > 0n) {
2999
- this.cachedBalance = balance;
3000
- this.cachedAt = now;
3001
- }
3002
- return this.buildInfo(balance);
3003
- }
3004
- deductEstimated(amountMicros) {
3005
- if (this.cachedBalance !== null && this.cachedBalance >= amountMicros) {
3006
- this.cachedBalance -= amountMicros;
3007
- }
3008
- }
3009
- invalidate() {
3010
- this.cachedBalance = null;
3011
- this.cachedAt = 0;
3012
- }
3013
- async refresh() {
3014
- this.invalidate();
3015
- return this.checkBalance();
3016
- }
3017
- /**
3018
- * Check if balance is sufficient for an estimated cost.
3019
- */
3020
- async checkSufficient(estimatedCostMicros) {
3021
- const info = await this.checkBalance();
3022
- if (info.balance >= estimatedCostMicros) {
3023
- return { sufficient: true, info };
3024
- }
3025
- const shortfall = estimatedCostMicros - info.balance;
3026
- return {
3027
- sufficient: false,
3028
- info,
3029
- shortfall: this.formatUSDC(shortfall)
3030
- };
3031
- }
3032
- /**
3033
- * Format USDC amount (in micros) as "$X.XX".
3034
- */
3035
- formatUSDC(amountMicros) {
3036
- const dollars = Number(amountMicros) / 1e6;
3037
- return `$${dollars.toFixed(2)}`;
3038
- }
3039
- getWalletAddress() {
3040
- return this.walletAddress;
3041
- }
3042
- async fetchBalance() {
3043
- const owner = solAddress(this.walletAddress);
3044
- const mint = solAddress(SOLANA_USDC_MINT);
3045
- for (let attempt = 0; attempt < 2; attempt++) {
3046
- const result = await this.fetchBalanceOnce(owner, mint);
3047
- if (result > 0n || attempt === 1) return result;
3048
- await new Promise((r) => setTimeout(r, 1e3));
3049
- }
3050
- return 0n;
3051
- }
3052
- async fetchBalanceOnce(owner, mint) {
3053
- const controller = new AbortController();
3054
- const timer = setTimeout(() => controller.abort(), BALANCE_TIMEOUT_MS);
3055
- try {
3056
- const response = await this.rpc.getTokenAccountsByOwner(owner, { mint }, { encoding: "jsonParsed" }).send({ abortSignal: controller.signal });
3057
- if (response.value.length === 0) return 0n;
3058
- let total = 0n;
3059
- for (const account of response.value) {
3060
- const parsed = account.account.data;
3061
- total += BigInt(parsed.parsed.info.tokenAmount.amount);
3062
- }
3063
- return total;
3064
- } catch (err) {
3065
- throw new Error(
3066
- `Failed to fetch Solana USDC balance: ${err instanceof Error ? err.message : String(err)}`,
3067
- { cause: err }
3068
- );
3069
- } finally {
3070
- clearTimeout(timer);
3071
- }
3072
- }
3073
- buildInfo(balance) {
3074
- const dollars = Number(balance) / 1e6;
3075
- return {
3076
- balance,
3077
- balanceUSD: `$${dollars.toFixed(2)}`,
3078
- isLow: balance < 1000000n,
3079
- isEmpty: balance < 100n,
3080
- walletAddress: this.walletAddress
3081
- };
3082
- }
3083
- };
3084
-
3085
3107
  // src/auth.ts
3086
3108
  import { writeFile, mkdir as mkdir2 } from "fs/promises";
3087
3109
  import { join as join4 } from "path";
@@ -5004,6 +5026,7 @@ var ROUTING_PROFILES = /* @__PURE__ */ new Set([
5004
5026
  "premium"
5005
5027
  ]);
5006
5028
  var FREE_MODEL = "nvidia/gpt-oss-120b";
5029
+ var freeRequestCount = 0;
5007
5030
  var MAX_MESSAGES = 200;
5008
5031
  var CONTEXT_LIMIT_KB = 5120;
5009
5032
  var HEARTBEAT_INTERVAL_MS = 2e3;
@@ -5705,7 +5728,13 @@ async function startProxy(options) {
5705
5728
  const solanaSigner = await createKeyPairSignerFromPrivateKeyBytes(solanaPrivateKeyBytes);
5706
5729
  reuseSolanaAddress = solanaSigner.address;
5707
5730
  }
5708
- const balanceMonitor2 = paymentChain === "solana" && reuseSolanaAddress ? new SolanaBalanceMonitor(reuseSolanaAddress) : new BalanceMonitor(account2.address);
5731
+ let balanceMonitor2;
5732
+ if (paymentChain === "solana" && reuseSolanaAddress) {
5733
+ const { SolanaBalanceMonitor: SolanaBalanceMonitor2 } = await Promise.resolve().then(() => (init_solana_balance(), solana_balance_exports));
5734
+ balanceMonitor2 = new SolanaBalanceMonitor2(reuseSolanaAddress);
5735
+ } else {
5736
+ balanceMonitor2 = new BalanceMonitor(account2.address);
5737
+ }
5709
5738
  options.onReady?.(listenPort);
5710
5739
  return {
5711
5740
  port: listenPort,
@@ -5739,7 +5768,13 @@ async function startProxy(options) {
5739
5768
  const payFetch = createPayFetchWithPreAuth(fetch, x402, void 0, {
5740
5769
  skipPreAuth: paymentChain === "solana"
5741
5770
  });
5742
- const balanceMonitor = paymentChain === "solana" && solanaAddress ? new SolanaBalanceMonitor(solanaAddress) : new BalanceMonitor(account.address);
5771
+ let balanceMonitor;
5772
+ if (paymentChain === "solana" && solanaAddress) {
5773
+ const { SolanaBalanceMonitor: SolanaBalanceMonitor2 } = await Promise.resolve().then(() => (init_solana_balance(), solana_balance_exports));
5774
+ balanceMonitor = new SolanaBalanceMonitor2(solanaAddress);
5775
+ } else {
5776
+ balanceMonitor = new BalanceMonitor(account.address);
5777
+ }
5743
5778
  const routingConfig = mergeRoutingConfig(options.routingConfig);
5744
5779
  const modelPricing = buildModelPricing();
5745
5780
  const routerOpts = {
@@ -5959,11 +5994,14 @@ async function startProxy(options) {
5959
5994
  if (val.startsWith("data:")) {
5960
5995
  } else if (val.startsWith("https://") || val.startsWith("http://")) {
5961
5996
  const imgResp = await fetch(val);
5962
- if (!imgResp.ok) throw new Error(`Failed to download ${field} from ${val}: HTTP ${imgResp.status}`);
5997
+ if (!imgResp.ok)
5998
+ throw new Error(`Failed to download ${field} from ${val}: HTTP ${imgResp.status}`);
5963
5999
  const contentType = imgResp.headers.get("content-type") ?? "image/png";
5964
6000
  const buf = Buffer.from(await imgResp.arrayBuffer());
5965
6001
  parsed[field] = `data:${contentType};base64,${buf.toString("base64")}`;
5966
- console.log(`[ClawRouter] img2img: downloaded ${field} URL \u2192 data URI (${buf.length} bytes)`);
6002
+ console.log(
6003
+ `[ClawRouter] img2img: downloaded ${field} URL \u2192 data URI (${buf.length} bytes)`
6004
+ );
5967
6005
  } else {
5968
6006
  parsed[field] = readImageFileAsDataUri(val);
5969
6007
  console.log(`[ClawRouter] img2img: read ${field} file \u2192 data URI`);
@@ -6320,6 +6358,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
6320
6358
  let modelId = "";
6321
6359
  let maxTokens = 4096;
6322
6360
  let routingProfile = null;
6361
+ let balanceFallbackNotice;
6323
6362
  let accumulatedContent = "";
6324
6363
  let responseInputTokens;
6325
6364
  const isChatCompletion = req.url?.includes("/chat/completions");
@@ -6855,6 +6894,12 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
6855
6894
  parsed.model = freeModel;
6856
6895
  modelId = freeModel;
6857
6896
  bodyModified = true;
6897
+ freeRequestCount++;
6898
+ if (freeRequestCount % 5 === 0) {
6899
+ balanceFallbackNotice = `> **\u{1F4A1} Tip:** Not satisfied with free model quality? Fund your wallet to unlock deepseek-chat, gemini-flash, and 30+ premium models \u2014 starting at $0.001/request.
6900
+
6901
+ `;
6902
+ }
6858
6903
  await logUsage({
6859
6904
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
6860
6905
  model: freeModel,
@@ -7071,7 +7116,6 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
7071
7116
  }
7072
7117
  deduplicator.markInflight(dedupKey);
7073
7118
  let estimatedCostMicros;
7074
- let balanceFallbackNotice;
7075
7119
  const isFreeModel = modelId === FREE_MODEL;
7076
7120
  if (modelId && !options.skipBalanceCheck && !isFreeModel) {
7077
7121
  const estimated = estimateAmount(modelId, body.length, maxTokens);
@@ -7093,6 +7137,12 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
7093
7137
  ` : `> **\u26A0\uFE0F Insufficient balance** (${sufficiency.info.balanceUSD}) \u2014 using free model instead of ${originalModel}.
7094
7138
 
7095
7139
  `;
7140
+ freeRequestCount++;
7141
+ if (freeRequestCount % 5 === 0) {
7142
+ balanceFallbackNotice = `> **\u{1F4A1} Tip:** Not satisfied with free model quality? Fund your wallet to unlock deepseek-chat, gemini-flash, and 30+ premium models \u2014 starting at $0.001/request.
7143
+
7144
+ `;
7145
+ }
7096
7146
  options.onLowBalance?.({
7097
7147
  balanceUSD: sufficiency.info.balanceUSD,
7098
7148
  walletAddress: sufficiency.info.walletAddress
@@ -7976,7 +8026,7 @@ var PARTNER_SERVICES = [
7976
8026
  id: "x_users_lookup",
7977
8027
  name: "Twitter/X User Lookup",
7978
8028
  partner: "AttentionVC",
7979
- description: "ALWAYS use this tool to look up real-time Twitter/X user profiles. Call this when the user asks about any Twitter/X account, username, handle, follower count, verification status, bio, or profile. Do NOT answer Twitter/X user questions from memory \u2014 always fetch live data with this tool. Returns: follower count, verification badge, bio, location, join date. Accepts up to 100 usernames per request (without @ prefix).",
8029
+ description: "Look up real-time Twitter/X user profiles by username. Call this ONLY when the user explicitly asks to look up, check, or get information about a specific Twitter/X user's profile (follower count, bio, verification status, etc.). Do NOT call this for messages that merely contain x.com or twitter.com URLs \u2014 only invoke when the user is asking for profile information about a specific account. Returns: follower count, verification badge, bio, location, join date. Accepts up to 100 usernames per request (without @ prefix).",
7980
8030
  proxyPath: "/x/users/lookup",
7981
8031
  method: "POST",
7982
8032
  params: [