@blockrun/clawrouter 0.5.9 → 0.6.2

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
@@ -1996,9 +1996,13 @@ var InsufficientFundsError = class extends Error {
1996
1996
  requiredUSD;
1997
1997
  walletAddress;
1998
1998
  constructor(opts) {
1999
- super(
2000
- `Insufficient USDC balance. Current: ${opts.currentBalanceUSD}, Required: ${opts.requiredUSD}. Fund wallet: ${opts.walletAddress}`
2001
- );
1999
+ const msg = [
2000
+ `Insufficient balance. Current: ${opts.currentBalanceUSD}, Required: ${opts.requiredUSD}`,
2001
+ `Options:`,
2002
+ ` 1. Fund wallet: ${opts.walletAddress}`,
2003
+ ` 2. Use free model: /model free`
2004
+ ].join("\n");
2005
+ super(msg);
2002
2006
  this.name = "InsufficientFundsError";
2003
2007
  this.currentBalanceUSD = opts.currentBalanceUSD;
2004
2008
  this.requiredUSD = opts.requiredUSD;
@@ -2009,7 +2013,14 @@ var EmptyWalletError = class extends Error {
2009
2013
  code = "EMPTY_WALLET";
2010
2014
  walletAddress;
2011
2015
  constructor(walletAddress) {
2012
- super(`No USDC balance. Fund wallet to use ClawRouter: ${walletAddress}`);
2016
+ const msg = [
2017
+ `No USDC balance.`,
2018
+ `Options:`,
2019
+ ` 1. Fund wallet: ${walletAddress}`,
2020
+ ` 2. Use free model: /model free`,
2021
+ ` 3. Uninstall: bash ~/.openclaw/extensions/clawrouter/scripts/uninstall.sh`
2022
+ ].join("\n");
2023
+ super(msg);
2013
2024
  this.name = "EmptyWalletError";
2014
2025
  this.walletAddress = walletAddress;
2015
2026
  }
@@ -2309,6 +2320,34 @@ var DEFAULT_REQUEST_TIMEOUT_MS = 18e4;
2309
2320
  var DEFAULT_PORT = 8402;
2310
2321
  var MAX_FALLBACK_ATTEMPTS = 3;
2311
2322
  var HEALTH_CHECK_TIMEOUT_MS = 2e3;
2323
+ var RATE_LIMIT_COOLDOWN_MS = 6e4;
2324
+ var rateLimitedModels = /* @__PURE__ */ new Map();
2325
+ function isRateLimited(modelId) {
2326
+ const hitTime = rateLimitedModels.get(modelId);
2327
+ if (!hitTime) return false;
2328
+ const elapsed = Date.now() - hitTime;
2329
+ if (elapsed >= RATE_LIMIT_COOLDOWN_MS) {
2330
+ rateLimitedModels.delete(modelId);
2331
+ return false;
2332
+ }
2333
+ return true;
2334
+ }
2335
+ function markRateLimited(modelId) {
2336
+ rateLimitedModels.set(modelId, Date.now());
2337
+ console.log(`[ClawRouter] Model ${modelId} rate-limited, will deprioritize for 60s`);
2338
+ }
2339
+ function prioritizeNonRateLimited(models) {
2340
+ const available = [];
2341
+ const rateLimited = [];
2342
+ for (const model of models) {
2343
+ if (isRateLimited(model)) {
2344
+ rateLimited.push(model);
2345
+ } else {
2346
+ available.push(model);
2347
+ }
2348
+ }
2349
+ return [...available, ...rateLimited];
2350
+ }
2312
2351
  var BALANCE_CHECK_BUFFER = 1.5;
2313
2352
  function getProxyPort() {
2314
2353
  const envPort = process.env.BLOCKRUN_PROXY_PORT;
@@ -2431,6 +2470,18 @@ function normalizeMessagesForGoogle(messages) {
2431
2470
  function isGoogleModel(modelId) {
2432
2471
  return modelId.startsWith("google/") || modelId.startsWith("gemini");
2433
2472
  }
2473
+ function normalizeMessagesForThinking(messages) {
2474
+ if (!messages || messages.length === 0) return messages;
2475
+ let hasChanges = false;
2476
+ const normalized = messages.map((msg) => {
2477
+ if (msg.role === "assistant" && msg.tool_calls && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0 && msg.reasoning_content === void 0) {
2478
+ hasChanges = true;
2479
+ return { ...msg, reasoning_content: "" };
2480
+ }
2481
+ return msg;
2482
+ });
2483
+ return hasChanges ? normalized : messages;
2484
+ }
2434
2485
  var KIMI_BLOCK_RE = /<[||][^<>]*begin[^<>]*[||]>[\s\S]*?<[||][^<>]*end[^<>]*[||]>/gi;
2435
2486
  var KIMI_TOKEN_RE = /<[||][^<>]*[||]>/g;
2436
2487
  var THINKING_TAG_RE = /<\s*\/?\s*(?:think(?:ing)?|thought|antthinking)\b[^>]*>/gi;
@@ -2642,6 +2693,9 @@ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxT
2642
2693
  if (isGoogleModel(modelId) && Array.isArray(parsed.messages)) {
2643
2694
  parsed.messages = normalizeMessagesForGoogle(parsed.messages);
2644
2695
  }
2696
+ if (parsed.thinking && Array.isArray(parsed.messages)) {
2697
+ parsed.messages = normalizeMessagesForThinking(parsed.messages);
2698
+ }
2645
2699
  requestBody = Buffer.from(JSON.stringify(parsed));
2646
2700
  } catch {
2647
2701
  }
@@ -2801,42 +2855,18 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
2801
2855
  const bufferedCostMicros = estimatedCostMicros * BigInt(Math.ceil(BALANCE_CHECK_BUFFER * 100)) / 100n;
2802
2856
  const sufficiency = await balanceMonitor.checkSufficient(bufferedCostMicros);
2803
2857
  if (sufficiency.info.isEmpty || !sufficiency.sufficient) {
2804
- if (routingDecision) {
2805
- console.log(
2806
- `[ClawRouter] Wallet ${sufficiency.info.isEmpty ? "empty" : "insufficient"} ($${sufficiency.info.balanceUSD}), falling back to free model: ${FREE_MODEL}`
2807
- );
2808
- modelId = FREE_MODEL;
2809
- const parsed = JSON.parse(body.toString());
2810
- parsed.model = FREE_MODEL;
2811
- body = Buffer.from(JSON.stringify(parsed));
2812
- options.onLowBalance?.({
2813
- balanceUSD: sufficiency.info.balanceUSD,
2814
- walletAddress: sufficiency.info.walletAddress
2815
- });
2816
- } else {
2817
- deduplicator.removeInflight(dedupKey);
2818
- if (sufficiency.info.isEmpty) {
2819
- const error = new EmptyWalletError(sufficiency.info.walletAddress);
2820
- options.onInsufficientFunds?.({
2821
- balanceUSD: sufficiency.info.balanceUSD,
2822
- requiredUSD: balanceMonitor.formatUSDC(bufferedCostMicros),
2823
- walletAddress: sufficiency.info.walletAddress
2824
- });
2825
- throw error;
2826
- } else {
2827
- const error = new InsufficientFundsError({
2828
- currentBalanceUSD: sufficiency.info.balanceUSD,
2829
- requiredUSD: balanceMonitor.formatUSDC(bufferedCostMicros),
2830
- walletAddress: sufficiency.info.walletAddress
2831
- });
2832
- options.onInsufficientFunds?.({
2833
- balanceUSD: sufficiency.info.balanceUSD,
2834
- requiredUSD: balanceMonitor.formatUSDC(bufferedCostMicros),
2835
- walletAddress: sufficiency.info.walletAddress
2836
- });
2837
- throw error;
2838
- }
2839
- }
2858
+ const originalModel = modelId;
2859
+ console.log(
2860
+ `[ClawRouter] Wallet ${sufficiency.info.isEmpty ? "empty" : "insufficient"} ($${sufficiency.info.balanceUSD}), falling back to free model: ${FREE_MODEL} (requested: ${originalModel})`
2861
+ );
2862
+ modelId = FREE_MODEL;
2863
+ const parsed = JSON.parse(body.toString());
2864
+ parsed.model = FREE_MODEL;
2865
+ body = Buffer.from(JSON.stringify(parsed));
2866
+ options.onLowBalance?.({
2867
+ balanceUSD: sufficiency.info.balanceUSD,
2868
+ walletAddress: sufficiency.info.walletAddress
2869
+ });
2840
2870
  } else if (sufficiency.info.isLow) {
2841
2871
  options.onLowBalance?.({
2842
2872
  balanceUSD: sufficiency.info.balanceUSD,
@@ -2907,6 +2937,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
2907
2937
  );
2908
2938
  }
2909
2939
  modelsToTry = contextFiltered.slice(0, MAX_FALLBACK_ATTEMPTS);
2940
+ modelsToTry = prioritizeNonRateLimited(modelsToTry);
2910
2941
  } else {
2911
2942
  modelsToTry = modelId ? [modelId] : [];
2912
2943
  }
@@ -2939,6 +2970,9 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
2939
2970
  status: result.errorStatus || 500
2940
2971
  };
2941
2972
  if (result.isProviderError && !isLastAttempt) {
2973
+ if (result.errorStatus === 429) {
2974
+ markRateLimited(tryModel);
2975
+ }
2942
2976
  console.log(
2943
2977
  `[ClawRouter] Provider error from ${tryModel}, trying fallback: ${result.errorBody?.slice(0, 100)}`
2944
2978
  );
@@ -3265,6 +3299,18 @@ function isRetryable(errorOrResponse, config) {
3265
3299
  }
3266
3300
 
3267
3301
  // src/index.ts
3302
+ async function waitForProxyHealth(port, timeoutMs = 3e3) {
3303
+ const start = Date.now();
3304
+ while (Date.now() - start < timeoutMs) {
3305
+ try {
3306
+ const res = await fetch(`http://127.0.0.1:${port}/health`);
3307
+ if (res.ok) return true;
3308
+ } catch {
3309
+ }
3310
+ await new Promise((r) => setTimeout(r, 100));
3311
+ }
3312
+ return false;
3313
+ }
3268
3314
  function isCompletionMode() {
3269
3315
  const args = process.argv;
3270
3316
  return args.some((arg, i) => arg === "completion" && i >= 1 && i <= 3);
@@ -3314,20 +3360,34 @@ function injectModelsConfig(logger) {
3314
3360
  config.agents.defaults.model.primary = "blockrun/auto";
3315
3361
  needsWrite = true;
3316
3362
  }
3317
- if (config.agents.defaults.models && typeof config.agents.defaults.models === "object") {
3318
- const allowlist = config.agents.defaults.models;
3319
- for (const model of OPENCLAW_MODELS) {
3320
- const fullId = `blockrun/${model.id}`;
3321
- if (!allowlist[fullId]) {
3322
- const alias = model.id.includes("/") ? model.id.split("/").pop() : model.id;
3323
- allowlist[fullId] = { alias };
3324
- needsWrite = true;
3325
- }
3363
+ const KEY_MODEL_ALIASES = [
3364
+ { id: "auto", alias: "auto" },
3365
+ { id: "free", alias: "free" },
3366
+ { id: "sonnet", alias: "sonnet" },
3367
+ { id: "opus", alias: "opus" },
3368
+ { id: "haiku", alias: "haiku" },
3369
+ { id: "grok", alias: "grok" },
3370
+ { id: "deepseek", alias: "deepseek" },
3371
+ { id: "kimi", alias: "kimi" },
3372
+ { id: "gemini", alias: "gemini" },
3373
+ { id: "flash", alias: "flash" },
3374
+ { id: "gpt", alias: "gpt" },
3375
+ { id: "reasoner", alias: "reasoner" }
3376
+ ];
3377
+ if (!config.agents) config.agents = {};
3378
+ if (!config.agents.defaults) config.agents.defaults = {};
3379
+ if (!config.agents.defaults.models) config.agents.defaults.models = {};
3380
+ const allowlist = config.agents.defaults.models;
3381
+ for (const m of KEY_MODEL_ALIASES) {
3382
+ const fullId = `blockrun/${m.id}`;
3383
+ if (!allowlist[fullId]) {
3384
+ allowlist[fullId] = { alias: m.alias };
3385
+ needsWrite = true;
3326
3386
  }
3327
3387
  }
3328
3388
  if (needsWrite) {
3329
3389
  writeFileSync(configPath, JSON.stringify(config, null, 2));
3330
- logger.info("Set default model to blockrun/auto (smart routing enabled)");
3390
+ logger.info("Smart routing enabled (blockrun/auto)");
3331
3391
  }
3332
3392
  } catch {
3333
3393
  }
@@ -3399,29 +3459,11 @@ async function startProxyInBackground(api) {
3399
3459
  const { key: walletKey, address, source } = await resolveOrGenerateWalletKey();
3400
3460
  if (source === "generated") {
3401
3461
  api.logger.info(`Generated new wallet: ${address}`);
3402
- api.logger.info(`Fund with USDC on Base to start using ClawRouter.`);
3403
3462
  } else if (source === "saved") {
3404
3463
  api.logger.info(`Using saved wallet: ${address}`);
3405
3464
  } else {
3406
3465
  api.logger.info(`Using wallet from BLOCKRUN_WALLET_KEY: ${address}`);
3407
3466
  }
3408
- const startupMonitor = new BalanceMonitor(address);
3409
- try {
3410
- const startupBalance = await startupMonitor.checkBalance();
3411
- if (startupBalance.isEmpty) {
3412
- api.logger.warn(`[!] No USDC balance. Fund wallet to use ClawRouter: ${address}`);
3413
- } else if (startupBalance.isLow) {
3414
- api.logger.warn(
3415
- `[!] Low balance: ${startupBalance.balanceUSD} remaining. Fund wallet: ${address}`
3416
- );
3417
- } else {
3418
- api.logger.info(`Wallet balance: ${startupBalance.balanceUSD}`);
3419
- }
3420
- } catch (err) {
3421
- api.logger.warn(
3422
- `Could not check wallet balance: ${err instanceof Error ? err.message : String(err)}`
3423
- );
3424
- }
3425
3467
  const routingConfig = api.pluginConfig?.routing;
3426
3468
  const proxy = await startProxy({
3427
3469
  walletKey,
@@ -3450,7 +3492,21 @@ async function startProxyInBackground(api) {
3450
3492
  });
3451
3493
  setActiveProxy(proxy);
3452
3494
  activeProxyHandle = proxy;
3453
- api.logger.info(`BlockRun provider active \u2014 ${proxy.baseUrl}/v1 (smart routing enabled)`);
3495
+ api.logger.info(`ClawRouter ready \u2014 smart routing enabled`);
3496
+ api.logger.info(`Pricing: Simple ~$0.001 | Code ~$0.01 | Complex ~$0.05 | Free: $0`);
3497
+ const startupMonitor = new BalanceMonitor(address);
3498
+ startupMonitor.checkBalance().then((balance) => {
3499
+ if (balance.isEmpty) {
3500
+ api.logger.info(`Wallet: ${address} | Balance: $0.00`);
3501
+ api.logger.info(`Using FREE model. Fund wallet for premium models.`);
3502
+ } else if (balance.isLow) {
3503
+ api.logger.info(`Wallet: ${address} | Balance: ${balance.balanceUSD} (low)`);
3504
+ } else {
3505
+ api.logger.info(`Wallet: ${address} | Balance: ${balance.balanceUSD}`);
3506
+ }
3507
+ }).catch(() => {
3508
+ api.logger.info(`Wallet: ${address} | Balance: (checking...)`);
3509
+ });
3454
3510
  }
3455
3511
  async function createStatsCommand() {
3456
3512
  return {
@@ -3556,7 +3612,7 @@ var plugin = {
3556
3612
  name: "ClawRouter",
3557
3613
  description: "Smart LLM router \u2014 30+ models, x402 micropayments, 78% cost savings",
3558
3614
  version: VERSION,
3559
- register(api) {
3615
+ async register(api) {
3560
3616
  const isDisabled = process.env.CLAWROUTER_DISABLED === "true" || process.env.CLAWROUTER_DISABLED === "1";
3561
3617
  if (isDisabled) {
3562
3618
  api.logger.info("ClawRouter disabled (CLAWROUTER_DISABLED=true). Using default routing.");
@@ -3622,11 +3678,18 @@ var plugin = {
3622
3678
  }
3623
3679
  }
3624
3680
  });
3625
- startProxyInBackground(api).catch((err) => {
3681
+ try {
3682
+ await startProxyInBackground(api);
3683
+ const port = getProxyPort();
3684
+ const healthy = await waitForProxyHealth(port);
3685
+ if (!healthy) {
3686
+ api.logger.warn(`Proxy health check timed out, commands may not work immediately`);
3687
+ }
3688
+ } catch (err) {
3626
3689
  api.logger.error(
3627
3690
  `Failed to start BlockRun proxy: ${err instanceof Error ? err.message : String(err)}`
3628
3691
  );
3629
- });
3692
+ }
3630
3693
  }
3631
3694
  };
3632
3695
  var index_default = plugin;