@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/index.js CHANGED
@@ -8,125 +8,6 @@ var __export = (target, all) => {
8
8
  __defProp(target, name, { get: all[name], enumerable: true });
9
9
  };
10
10
 
11
- // src/solana-balance.ts
12
- var solana_balance_exports = {};
13
- __export(solana_balance_exports, {
14
- SolanaBalanceMonitor: () => SolanaBalanceMonitor
15
- });
16
- import { address as solAddress, createSolanaRpc } from "@solana/kit";
17
- var SOLANA_USDC_MINT, SOLANA_DEFAULT_RPC, BALANCE_TIMEOUT_MS, CACHE_TTL_MS2, SolanaBalanceMonitor;
18
- var init_solana_balance = __esm({
19
- "src/solana-balance.ts"() {
20
- "use strict";
21
- SOLANA_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
22
- SOLANA_DEFAULT_RPC = "https://api.mainnet-beta.solana.com";
23
- BALANCE_TIMEOUT_MS = 1e4;
24
- CACHE_TTL_MS2 = 3e4;
25
- SolanaBalanceMonitor = class {
26
- rpc;
27
- walletAddress;
28
- cachedBalance = null;
29
- cachedAt = 0;
30
- constructor(walletAddress, rpcUrl) {
31
- this.walletAddress = walletAddress;
32
- const url = rpcUrl || process["env"].CLAWROUTER_SOLANA_RPC_URL || SOLANA_DEFAULT_RPC;
33
- this.rpc = createSolanaRpc(url);
34
- }
35
- async checkBalance() {
36
- const now = Date.now();
37
- if (this.cachedBalance !== null && this.cachedBalance > 0n && now - this.cachedAt < CACHE_TTL_MS2) {
38
- return this.buildInfo(this.cachedBalance);
39
- }
40
- const balance = await this.fetchBalance();
41
- if (balance > 0n) {
42
- this.cachedBalance = balance;
43
- this.cachedAt = now;
44
- }
45
- return this.buildInfo(balance);
46
- }
47
- deductEstimated(amountMicros) {
48
- if (this.cachedBalance !== null && this.cachedBalance >= amountMicros) {
49
- this.cachedBalance -= amountMicros;
50
- }
51
- }
52
- invalidate() {
53
- this.cachedBalance = null;
54
- this.cachedAt = 0;
55
- }
56
- async refresh() {
57
- this.invalidate();
58
- return this.checkBalance();
59
- }
60
- /**
61
- * Check if balance is sufficient for an estimated cost.
62
- */
63
- async checkSufficient(estimatedCostMicros) {
64
- const info = await this.checkBalance();
65
- if (info.balance >= estimatedCostMicros) {
66
- return { sufficient: true, info };
67
- }
68
- const shortfall = estimatedCostMicros - info.balance;
69
- return {
70
- sufficient: false,
71
- info,
72
- shortfall: this.formatUSDC(shortfall)
73
- };
74
- }
75
- /**
76
- * Format USDC amount (in micros) as "$X.XX".
77
- */
78
- formatUSDC(amountMicros) {
79
- const dollars = Number(amountMicros) / 1e6;
80
- return `$${dollars.toFixed(2)}`;
81
- }
82
- getWalletAddress() {
83
- return this.walletAddress;
84
- }
85
- async fetchBalance() {
86
- const owner = solAddress(this.walletAddress);
87
- const mint = solAddress(SOLANA_USDC_MINT);
88
- for (let attempt = 0; attempt < 2; attempt++) {
89
- const result = await this.fetchBalanceOnce(owner, mint);
90
- if (result > 0n || attempt === 1) return result;
91
- await new Promise((r) => setTimeout(r, 1e3));
92
- }
93
- return 0n;
94
- }
95
- async fetchBalanceOnce(owner, mint) {
96
- const controller = new AbortController();
97
- const timer = setTimeout(() => controller.abort(), BALANCE_TIMEOUT_MS);
98
- try {
99
- const response = await this.rpc.getTokenAccountsByOwner(owner, { mint }, { encoding: "jsonParsed" }).send({ abortSignal: controller.signal });
100
- if (response.value.length === 0) return 0n;
101
- let total = 0n;
102
- for (const account of response.value) {
103
- const parsed = account.account.data;
104
- total += BigInt(parsed.parsed.info.tokenAmount.amount);
105
- }
106
- return total;
107
- } catch (err) {
108
- throw new Error(
109
- `Failed to fetch Solana USDC balance: ${err instanceof Error ? err.message : String(err)}`,
110
- { cause: err }
111
- );
112
- } finally {
113
- clearTimeout(timer);
114
- }
115
- }
116
- buildInfo(balance) {
117
- const dollars = Number(balance) / 1e6;
118
- return {
119
- balance,
120
- balanceUSD: `$${dollars.toFixed(2)}`,
121
- isLow: balance < 1000000n,
122
- isEmpty: balance < 100n,
123
- walletAddress: this.walletAddress
124
- };
125
- }
126
- };
127
- }
128
- });
129
-
130
11
  // node_modules/@noble/hashes/esm/utils.js
131
12
  function isBytes(a) {
132
13
  return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array";
@@ -737,6 +618,125 @@ var init_wallet = __esm({
737
618
  }
738
619
  });
739
620
 
621
+ // src/solana-balance.ts
622
+ var solana_balance_exports = {};
623
+ __export(solana_balance_exports, {
624
+ SolanaBalanceMonitor: () => SolanaBalanceMonitor
625
+ });
626
+ import { address as solAddress, createSolanaRpc } from "@solana/kit";
627
+ var SOLANA_USDC_MINT, SOLANA_DEFAULT_RPC, BALANCE_TIMEOUT_MS, CACHE_TTL_MS2, SolanaBalanceMonitor;
628
+ var init_solana_balance = __esm({
629
+ "src/solana-balance.ts"() {
630
+ "use strict";
631
+ SOLANA_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
632
+ SOLANA_DEFAULT_RPC = "https://api.mainnet-beta.solana.com";
633
+ BALANCE_TIMEOUT_MS = 1e4;
634
+ CACHE_TTL_MS2 = 3e4;
635
+ SolanaBalanceMonitor = class {
636
+ rpc;
637
+ walletAddress;
638
+ cachedBalance = null;
639
+ cachedAt = 0;
640
+ constructor(walletAddress, rpcUrl) {
641
+ this.walletAddress = walletAddress;
642
+ const url = rpcUrl || process["env"].CLAWROUTER_SOLANA_RPC_URL || SOLANA_DEFAULT_RPC;
643
+ this.rpc = createSolanaRpc(url);
644
+ }
645
+ async checkBalance() {
646
+ const now = Date.now();
647
+ if (this.cachedBalance !== null && this.cachedBalance > 0n && now - this.cachedAt < CACHE_TTL_MS2) {
648
+ return this.buildInfo(this.cachedBalance);
649
+ }
650
+ const balance = await this.fetchBalance();
651
+ if (balance > 0n) {
652
+ this.cachedBalance = balance;
653
+ this.cachedAt = now;
654
+ }
655
+ return this.buildInfo(balance);
656
+ }
657
+ deductEstimated(amountMicros) {
658
+ if (this.cachedBalance !== null && this.cachedBalance >= amountMicros) {
659
+ this.cachedBalance -= amountMicros;
660
+ }
661
+ }
662
+ invalidate() {
663
+ this.cachedBalance = null;
664
+ this.cachedAt = 0;
665
+ }
666
+ async refresh() {
667
+ this.invalidate();
668
+ return this.checkBalance();
669
+ }
670
+ /**
671
+ * Check if balance is sufficient for an estimated cost.
672
+ */
673
+ async checkSufficient(estimatedCostMicros) {
674
+ const info = await this.checkBalance();
675
+ if (info.balance >= estimatedCostMicros) {
676
+ return { sufficient: true, info };
677
+ }
678
+ const shortfall = estimatedCostMicros - info.balance;
679
+ return {
680
+ sufficient: false,
681
+ info,
682
+ shortfall: this.formatUSDC(shortfall)
683
+ };
684
+ }
685
+ /**
686
+ * Format USDC amount (in micros) as "$X.XX".
687
+ */
688
+ formatUSDC(amountMicros) {
689
+ const dollars = Number(amountMicros) / 1e6;
690
+ return `$${dollars.toFixed(2)}`;
691
+ }
692
+ getWalletAddress() {
693
+ return this.walletAddress;
694
+ }
695
+ async fetchBalance() {
696
+ const owner = solAddress(this.walletAddress);
697
+ const mint = solAddress(SOLANA_USDC_MINT);
698
+ for (let attempt = 0; attempt < 2; attempt++) {
699
+ const result = await this.fetchBalanceOnce(owner, mint);
700
+ if (result > 0n || attempt === 1) return result;
701
+ await new Promise((r) => setTimeout(r, 1e3));
702
+ }
703
+ return 0n;
704
+ }
705
+ async fetchBalanceOnce(owner, mint) {
706
+ const controller = new AbortController();
707
+ const timer = setTimeout(() => controller.abort(), BALANCE_TIMEOUT_MS);
708
+ try {
709
+ const response = await this.rpc.getTokenAccountsByOwner(owner, { mint }, { encoding: "jsonParsed" }).send({ abortSignal: controller.signal });
710
+ if (response.value.length === 0) return 0n;
711
+ let total = 0n;
712
+ for (const account of response.value) {
713
+ const parsed = account.account.data;
714
+ total += BigInt(parsed.parsed.info.tokenAmount.amount);
715
+ }
716
+ return total;
717
+ } catch (err) {
718
+ throw new Error(
719
+ `Failed to fetch Solana USDC balance: ${err instanceof Error ? err.message : String(err)}`,
720
+ { cause: err }
721
+ );
722
+ } finally {
723
+ clearTimeout(timer);
724
+ }
725
+ }
726
+ buildInfo(balance) {
727
+ const dollars = Number(balance) / 1e6;
728
+ return {
729
+ balance,
730
+ balanceUSD: `$${dollars.toFixed(2)}`,
731
+ isLow: balance < 1000000n,
732
+ isEmpty: balance < 100n,
733
+ walletAddress: this.walletAddress
734
+ };
735
+ }
736
+ };
737
+ }
738
+ });
739
+
740
740
  // src/solana-sweep.ts
741
741
  var solana_sweep_exports = {};
742
742
  __export(solana_sweep_exports, {
@@ -1009,7 +1009,8 @@ var MODEL_ALIASES = {
1009
1009
  // xAI
1010
1010
  grok: "xai/grok-3",
1011
1011
  "grok-fast": "xai/grok-4-fast-reasoning",
1012
- "grok-code": "xai/grok-code-fast-1",
1012
+ "grok-code": "deepseek/deepseek-chat",
1013
+ // was grok-code-fast-1, delisted due to poor retention
1013
1014
  // NVIDIA
1014
1015
  nvidia: "nvidia/gpt-oss-120b",
1015
1016
  "gpt-120b": "nvidia/gpt-oss-120b",
@@ -1477,18 +1478,8 @@ var BLOCKRUN_MODELS = [
1477
1478
  maxOutput: 16384,
1478
1479
  toolCalling: true
1479
1480
  },
1480
- {
1481
- id: "xai/grok-code-fast-1",
1482
- name: "Grok Code Fast",
1483
- version: "1",
1484
- inputPrice: 0.2,
1485
- outputPrice: 1.5,
1486
- contextWindow: 131072,
1487
- maxOutput: 16384
1488
- // toolCalling intentionally omitted: outputs tool calls as plain text JSON,
1489
- // not OpenAI-compatible structured function calls. Will be skipped when
1490
- // request has tools to prevent the "talking to itself" bug.
1491
- },
1481
+ // xai/grok-code-fast-1 delisted 2026-03-12: poor retention (coding users churn),
1482
+ // no structured tool calling, alias "grok-code" redirected to deepseek-chat
1492
1483
  {
1493
1484
  id: "xai/grok-4-0709",
1494
1485
  name: "Grok 4 (0709)",
@@ -3063,11 +3054,14 @@ var DEFAULT_ROUTING_CONFIG = {
3063
3054
  primary: "moonshot/kimi-k2.5",
3064
3055
  // $0.60/$3.00 - best quality/price for simple tasks
3065
3056
  fallback: [
3057
+ "google/gemini-2.5-flash",
3058
+ // 60% retention (best), fast growth (+800%)
3066
3059
  "google/gemini-2.5-flash-lite",
3067
3060
  // 1M context, ultra cheap ($0.10/$0.40)
3068
- "nvidia/gpt-oss-120b",
3061
+ "deepseek/deepseek-chat",
3062
+ // 41% retention
3063
+ "nvidia/gpt-oss-120b"
3069
3064
  // FREE fallback
3070
- "deepseek/deepseek-chat"
3071
3065
  ]
3072
3066
  },
3073
3067
  MEDIUM: {
@@ -3075,6 +3069,9 @@ var DEFAULT_ROUTING_CONFIG = {
3075
3069
  // $0.50/$2.40 - strong tool use, proper function call format
3076
3070
  fallback: [
3077
3071
  "deepseek/deepseek-chat",
3072
+ // 41% retention
3073
+ "google/gemini-2.5-flash",
3074
+ // 60% retention, cheap fast model
3078
3075
  "google/gemini-2.5-flash-lite",
3079
3076
  // 1M context, ultra cheap ($0.10/$0.40)
3080
3077
  "xai/grok-4-1-fast-non-reasoning"
@@ -3085,6 +3082,8 @@ var DEFAULT_ROUTING_CONFIG = {
3085
3082
  primary: "google/gemini-3.1-pro",
3086
3083
  // Newest Gemini 3.1 - upgraded from 3.0
3087
3084
  fallback: [
3085
+ "google/gemini-2.5-flash",
3086
+ // 60% retention, cheap failsafe before expensive models
3088
3087
  "google/gemini-2.5-flash-lite",
3089
3088
  // CRITICAL: 1M context, ultra-cheap failsafe ($0.10/$0.40)
3090
3089
  "google/gemini-3-pro-preview",
@@ -3115,12 +3114,12 @@ var DEFAULT_ROUTING_CONFIG = {
3115
3114
  SIMPLE: {
3116
3115
  primary: "nvidia/gpt-oss-120b",
3117
3116
  // FREE! $0.00/$0.00
3118
- fallback: ["google/gemini-2.5-flash-lite", "deepseek/deepseek-chat"]
3117
+ fallback: ["google/gemini-2.5-flash-lite", "google/gemini-2.5-flash", "deepseek/deepseek-chat"]
3119
3118
  },
3120
3119
  MEDIUM: {
3121
3120
  primary: "google/gemini-2.5-flash-lite",
3122
3121
  // $0.10/$0.40 - cheapest capable with 1M context
3123
- fallback: ["deepseek/deepseek-chat", "nvidia/gpt-oss-120b"]
3122
+ fallback: ["google/gemini-2.5-flash", "deepseek/deepseek-chat", "nvidia/gpt-oss-120b"]
3124
3123
  },
3125
3124
  COMPLEX: {
3126
3125
  primary: "google/gemini-2.5-flash-lite",
@@ -3140,6 +3139,8 @@ var DEFAULT_ROUTING_CONFIG = {
3140
3139
  primary: "moonshot/kimi-k2.5",
3141
3140
  // $0.60/$3.00 - good for simple coding
3142
3141
  fallback: [
3142
+ "google/gemini-2.5-flash",
3143
+ // 60% retention, fast growth
3143
3144
  "anthropic/claude-haiku-4.5",
3144
3145
  "google/gemini-2.5-flash-lite",
3145
3146
  "deepseek/deepseek-chat"
@@ -3150,6 +3151,8 @@ var DEFAULT_ROUTING_CONFIG = {
3150
3151
  // $2.50/$10 - strong coding for medium tasks
3151
3152
  fallback: [
3152
3153
  "moonshot/kimi-k2.5",
3154
+ "google/gemini-2.5-flash",
3155
+ // 60% retention, good coding capability
3153
3156
  "google/gemini-2.5-pro",
3154
3157
  "xai/grok-4-0709",
3155
3158
  "anthropic/claude-sonnet-4.6"
@@ -4089,9 +4092,6 @@ var BalanceMonitor = class {
4089
4092
  }
4090
4093
  };
4091
4094
 
4092
- // src/proxy.ts
4093
- init_solana_balance();
4094
-
4095
4095
  // src/auth.ts
4096
4096
  import { writeFile, mkdir as mkdir2 } from "fs/promises";
4097
4097
  init_wallet();
@@ -5454,6 +5454,7 @@ var ROUTING_PROFILES = /* @__PURE__ */ new Set([
5454
5454
  "premium"
5455
5455
  ]);
5456
5456
  var FREE_MODEL = "nvidia/gpt-oss-120b";
5457
+ var freeRequestCount = 0;
5457
5458
  var MAX_MESSAGES = 200;
5458
5459
  var CONTEXT_LIMIT_KB = 5120;
5459
5460
  var HEARTBEAT_INTERVAL_MS = 2e3;
@@ -6155,7 +6156,13 @@ async function startProxy(options) {
6155
6156
  const solanaSigner = await createKeyPairSignerFromPrivateKeyBytes2(solanaPrivateKeyBytes);
6156
6157
  reuseSolanaAddress = solanaSigner.address;
6157
6158
  }
6158
- const balanceMonitor2 = paymentChain === "solana" && reuseSolanaAddress ? new SolanaBalanceMonitor(reuseSolanaAddress) : new BalanceMonitor(account2.address);
6159
+ let balanceMonitor2;
6160
+ if (paymentChain === "solana" && reuseSolanaAddress) {
6161
+ const { SolanaBalanceMonitor: SolanaBalanceMonitor2 } = await Promise.resolve().then(() => (init_solana_balance(), solana_balance_exports));
6162
+ balanceMonitor2 = new SolanaBalanceMonitor2(reuseSolanaAddress);
6163
+ } else {
6164
+ balanceMonitor2 = new BalanceMonitor(account2.address);
6165
+ }
6159
6166
  options.onReady?.(listenPort);
6160
6167
  return {
6161
6168
  port: listenPort,
@@ -6189,7 +6196,13 @@ async function startProxy(options) {
6189
6196
  const payFetch = createPayFetchWithPreAuth(fetch, x402, void 0, {
6190
6197
  skipPreAuth: paymentChain === "solana"
6191
6198
  });
6192
- const balanceMonitor = paymentChain === "solana" && solanaAddress ? new SolanaBalanceMonitor(solanaAddress) : new BalanceMonitor(account.address);
6199
+ let balanceMonitor;
6200
+ if (paymentChain === "solana" && solanaAddress) {
6201
+ const { SolanaBalanceMonitor: SolanaBalanceMonitor2 } = await Promise.resolve().then(() => (init_solana_balance(), solana_balance_exports));
6202
+ balanceMonitor = new SolanaBalanceMonitor2(solanaAddress);
6203
+ } else {
6204
+ balanceMonitor = new BalanceMonitor(account.address);
6205
+ }
6193
6206
  const routingConfig = mergeRoutingConfig(options.routingConfig);
6194
6207
  const modelPricing = buildModelPricing();
6195
6208
  const routerOpts = {
@@ -6409,11 +6422,14 @@ async function startProxy(options) {
6409
6422
  if (val.startsWith("data:")) {
6410
6423
  } else if (val.startsWith("https://") || val.startsWith("http://")) {
6411
6424
  const imgResp = await fetch(val);
6412
- if (!imgResp.ok) throw new Error(`Failed to download ${field} from ${val}: HTTP ${imgResp.status}`);
6425
+ if (!imgResp.ok)
6426
+ throw new Error(`Failed to download ${field} from ${val}: HTTP ${imgResp.status}`);
6413
6427
  const contentType = imgResp.headers.get("content-type") ?? "image/png";
6414
6428
  const buf = Buffer.from(await imgResp.arrayBuffer());
6415
6429
  parsed[field] = `data:${contentType};base64,${buf.toString("base64")}`;
6416
- console.log(`[ClawRouter] img2img: downloaded ${field} URL \u2192 data URI (${buf.length} bytes)`);
6430
+ console.log(
6431
+ `[ClawRouter] img2img: downloaded ${field} URL \u2192 data URI (${buf.length} bytes)`
6432
+ );
6417
6433
  } else {
6418
6434
  parsed[field] = readImageFileAsDataUri(val);
6419
6435
  console.log(`[ClawRouter] img2img: read ${field} file \u2192 data URI`);
@@ -6770,6 +6786,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
6770
6786
  let modelId = "";
6771
6787
  let maxTokens = 4096;
6772
6788
  let routingProfile = null;
6789
+ let balanceFallbackNotice;
6773
6790
  let accumulatedContent = "";
6774
6791
  let responseInputTokens;
6775
6792
  const isChatCompletion = req.url?.includes("/chat/completions");
@@ -7305,6 +7322,12 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
7305
7322
  parsed.model = freeModel;
7306
7323
  modelId = freeModel;
7307
7324
  bodyModified = true;
7325
+ freeRequestCount++;
7326
+ if (freeRequestCount % 5 === 0) {
7327
+ 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.
7328
+
7329
+ `;
7330
+ }
7308
7331
  await logUsage({
7309
7332
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7310
7333
  model: freeModel,
@@ -7521,7 +7544,6 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
7521
7544
  }
7522
7545
  deduplicator.markInflight(dedupKey);
7523
7546
  let estimatedCostMicros;
7524
- let balanceFallbackNotice;
7525
7547
  const isFreeModel = modelId === FREE_MODEL;
7526
7548
  if (modelId && !options.skipBalanceCheck && !isFreeModel) {
7527
7549
  const estimated = estimateAmount(modelId, body.length, maxTokens);
@@ -7543,6 +7565,12 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
7543
7565
  ` : `> **\u26A0\uFE0F Insufficient balance** (${sufficiency.info.balanceUSD}) \u2014 using free model instead of ${originalModel}.
7544
7566
 
7545
7567
  `;
7568
+ freeRequestCount++;
7569
+ if (freeRequestCount % 5 === 0) {
7570
+ 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.
7571
+
7572
+ `;
7573
+ }
7546
7574
  options.onLowBalance?.({
7547
7575
  balanceUSD: sufficiency.info.balanceUSD,
7548
7576
  walletAddress: sufficiency.info.walletAddress
@@ -8081,7 +8109,7 @@ var PARTNER_SERVICES = [
8081
8109
  id: "x_users_lookup",
8082
8110
  name: "Twitter/X User Lookup",
8083
8111
  partner: "AttentionVC",
8084
- 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).",
8112
+ 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).",
8085
8113
  proxyPath: "/x/users/lookup",
8086
8114
  method: "POST",
8087
8115
  params: [