@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/README.md +42 -457
- package/dist/index.js +135 -72
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/scripts/reinstall.sh +177 -0
- package/scripts/uninstall.sh +135 -0
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
|
-
|
|
2000
|
-
`Insufficient
|
|
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
|
-
|
|
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
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
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
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
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("
|
|
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(`
|
|
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
|
-
|
|
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;
|