@blockrun/clawrouter 0.12.60 → 0.12.62

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 CHANGED
@@ -248,6 +248,10 @@ USDC stays in your wallet until spent — non-custodial. Price is visible in the
248
248
  /chain solana # Alias for /wallet solana
249
249
  /stats # View usage and savings
250
250
  /stats clear # Reset usage statistics
251
+ /exclude # Show excluded models
252
+ /exclude add <model> # Block a model from routing (aliases work: "grok-4", "free")
253
+ /exclude remove <model> # Unblock a model
254
+ /exclude clear # Remove all exclusions
251
255
  ```
252
256
 
253
257
  **Fund your wallet:**
@@ -289,6 +293,21 @@ For basic usage, no configuration needed. For advanced options:
289
293
 
290
294
  **Full reference:** [docs/configuration.md](docs/configuration.md)
291
295
 
296
+ ### Model Exclusion
297
+
298
+ Block specific models from being routed to. Useful if a model doesn't follow your agent instructions or you want to control costs.
299
+
300
+ ```bash
301
+ /exclude add nvidia/gpt-oss-120b # Block the free model
302
+ /exclude add grok-4 # Aliases work — blocks all grok-4 variants
303
+ /exclude add gpt-5.4 # Skip expensive models
304
+ /exclude # Show current exclusions
305
+ /exclude remove grok-4 # Unblock a model
306
+ /exclude clear # Remove all exclusions
307
+ ```
308
+
309
+ Exclusions persist across restarts (`~/.openclaw/blockrun/exclude-models.json`). If all models in a tier are excluded, the safety net ignores the filter so routing never breaks.
310
+
292
311
  ---
293
312
 
294
313
  ## Troubleshooting
package/dist/cli.js CHANGED
@@ -25522,10 +25522,10 @@ var init_client = __esm({
25522
25522
  // src/proxy.ts
25523
25523
  import { createServer } from "http";
25524
25524
  import { finished } from "stream";
25525
- import { homedir as homedir4 } from "os";
25526
- import { join as join7 } from "path";
25525
+ import { homedir as homedir5 } from "os";
25526
+ import { join as join8 } from "path";
25527
25527
  import { mkdir as mkdir3, writeFile as writeFile2, readFile, stat as fsStat } from "fs/promises";
25528
- import { readFileSync, existsSync } from "fs";
25528
+ import { readFileSync as readFileSync2, existsSync } from "fs";
25529
25529
 
25530
25530
  // node_modules/viem/_esm/utils/getAction.js
25531
25531
  function getAction(client, actionFn, name) {
@@ -39019,6 +39019,11 @@ function filterByVision(models, hasVision, supportsVision2) {
39019
39019
  const filtered = models.filter(supportsVision2);
39020
39020
  return filtered.length > 0 ? filtered : models;
39021
39021
  }
39022
+ function filterByExcludeList(models, excludeList) {
39023
+ if (excludeList.size === 0) return models;
39024
+ const filtered = models.filter((m) => !excludeList.has(m));
39025
+ return filtered.length > 0 ? filtered : models;
39026
+ }
39022
39027
  function getFallbackChainFiltered(tier, tierConfigs, estimatedTotalTokens, getContextWindow) {
39023
39028
  const fullChain = getFallbackChain(tier, tierConfigs);
39024
39029
  const filtered = fullChain.filter((modelId) => {
@@ -40454,7 +40459,9 @@ var MODEL_ALIASES = {
40454
40459
  nvidia: "nvidia/gpt-oss-120b",
40455
40460
  "gpt-120b": "nvidia/gpt-oss-120b",
40456
40461
  // MiniMax
40457
- minimax: "minimax/minimax-m2.5",
40462
+ minimax: "minimax/minimax-m2.7",
40463
+ "minimax-m2.7": "minimax/minimax-m2.7",
40464
+ "minimax-m2.5": "minimax/minimax-m2.5",
40458
40465
  // Z.AI GLM-5
40459
40466
  glm: "zai/glm-5",
40460
40467
  "glm-5": "zai/glm-5",
@@ -40960,6 +40967,18 @@ var BLOCKRUN_MODELS = [
40960
40967
  toolCalling: true
40961
40968
  },
40962
40969
  // MiniMax
40970
+ {
40971
+ id: "minimax/minimax-m2.7",
40972
+ name: "MiniMax M2.7",
40973
+ version: "m2.7",
40974
+ inputPrice: 0.3,
40975
+ outputPrice: 1.2,
40976
+ contextWindow: 204800,
40977
+ maxOutput: 16384,
40978
+ reasoning: true,
40979
+ agentic: true,
40980
+ toolCalling: true
40981
+ },
40963
40982
  {
40964
40983
  id: "minimax/minimax-m2.5",
40965
40984
  name: "MiniMax M2.5",
@@ -46344,6 +46363,29 @@ async function checkForUpdates() {
46344
46363
  }
46345
46364
  }
46346
46365
 
46366
+ // src/exclude-models.ts
46367
+ import { readFileSync, writeFileSync, mkdirSync } from "fs";
46368
+ import { join as join7, dirname as dirname2 } from "path";
46369
+ import { homedir as homedir4 } from "os";
46370
+ var DEFAULT_FILE_PATH = join7(
46371
+ homedir4(),
46372
+ ".openclaw",
46373
+ "blockrun",
46374
+ "exclude-models.json"
46375
+ );
46376
+ function loadExcludeList(filePath = DEFAULT_FILE_PATH) {
46377
+ try {
46378
+ const raw = readFileSync(filePath, "utf-8");
46379
+ const arr = JSON.parse(raw);
46380
+ if (Array.isArray(arr)) {
46381
+ return new Set(arr.filter((x) => typeof x === "string"));
46382
+ }
46383
+ return /* @__PURE__ */ new Set();
46384
+ } catch {
46385
+ return /* @__PURE__ */ new Set();
46386
+ }
46387
+ }
46388
+
46347
46389
  // src/config.ts
46348
46390
  var DEFAULT_PORT = 8402;
46349
46391
  var PROXY_PORT = (() => {
@@ -46528,7 +46570,7 @@ ${lines.join("\n")}`;
46528
46570
  // src/proxy.ts
46529
46571
  var BLOCKRUN_API = "https://blockrun.ai/api";
46530
46572
  var BLOCKRUN_SOLANA_API = "https://sol.blockrun.ai/api";
46531
- var IMAGE_DIR = join7(homedir4(), ".openclaw", "blockrun", "images");
46573
+ var IMAGE_DIR = join8(homedir5(), ".openclaw", "blockrun", "images");
46532
46574
  var AUTO_MODEL = "blockrun/auto";
46533
46575
  var ROUTING_PROFILES = /* @__PURE__ */ new Set([
46534
46576
  "blockrun/free",
@@ -47196,7 +47238,7 @@ async function proxyPartnerRequest(req, res, apiBase, payFetch) {
47196
47238
  });
47197
47239
  }
47198
47240
  function readImageFileAsDataUri(filePath) {
47199
- const resolved = filePath.startsWith("~/") ? join7(homedir4(), filePath.slice(2)) : filePath;
47241
+ const resolved = filePath.startsWith("~/") ? join8(homedir5(), filePath.slice(2)) : filePath;
47200
47242
  if (!existsSync(resolved)) {
47201
47243
  throw new Error(`Image file not found: ${resolved}`);
47202
47244
  }
@@ -47208,7 +47250,7 @@ function readImageFileAsDataUri(filePath) {
47208
47250
  webp: "image/webp"
47209
47251
  };
47210
47252
  const mime = mimeMap[ext] ?? "image/png";
47211
- const data = readFileSync(resolved);
47253
+ const data = readFileSync2(resolved);
47212
47254
  return `data:${mime};base64,${data.toString("base64")}`;
47213
47255
  }
47214
47256
  async function uploadDataUriToHost(dataUri) {
@@ -47326,7 +47368,9 @@ async function startProxy(options) {
47326
47368
  skipPreAuth: paymentChain === "solana"
47327
47369
  });
47328
47370
  let balanceMonitor;
47329
- if (paymentChain === "solana" && solanaAddress) {
47371
+ if (options._balanceMonitorOverride) {
47372
+ balanceMonitor = options._balanceMonitorOverride;
47373
+ } else if (paymentChain === "solana" && solanaAddress) {
47330
47374
  const { SolanaBalanceMonitor: SolanaBalanceMonitor2 } = await Promise.resolve().then(() => (init_solana_balance(), solana_balance_exports));
47331
47375
  balanceMonitor = new SolanaBalanceMonitor2(solanaAddress);
47332
47376
  } else {
@@ -47451,7 +47495,7 @@ async function startProxy(options) {
47451
47495
  res.end("Bad request");
47452
47496
  return;
47453
47497
  }
47454
- const filePath = join7(IMAGE_DIR, filename);
47498
+ const filePath = join8(IMAGE_DIR, filename);
47455
47499
  try {
47456
47500
  const s3 = await fsStat(filePath);
47457
47501
  if (!s3.isFile()) throw new Error("not a file");
@@ -47510,7 +47554,7 @@ async function startProxy(options) {
47510
47554
  const [, mimeType, b64] = dataUriMatch;
47511
47555
  const ext = mimeType === "image/jpeg" ? "jpg" : mimeType.split("/")[1] ?? "png";
47512
47556
  const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
47513
- await writeFile2(join7(IMAGE_DIR, filename), Buffer.from(b64, "base64"));
47557
+ await writeFile2(join8(IMAGE_DIR, filename), Buffer.from(b64, "base64"));
47514
47558
  img.url = `http://localhost:${port2}/images/${filename}`;
47515
47559
  console.log(`[ClawRouter] Image saved \u2192 ${img.url}`);
47516
47560
  } else if (img.url?.startsWith("https://") || img.url?.startsWith("http://")) {
@@ -47521,7 +47565,7 @@ async function startProxy(options) {
47521
47565
  const ext = contentType.includes("jpeg") || contentType.includes("jpg") ? "jpg" : contentType.includes("webp") ? "webp" : "png";
47522
47566
  const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
47523
47567
  const buf = Buffer.from(await imgResp.arrayBuffer());
47524
- await writeFile2(join7(IMAGE_DIR, filename), buf);
47568
+ await writeFile2(join8(IMAGE_DIR, filename), buf);
47525
47569
  img.url = `http://localhost:${port2}/images/${filename}`;
47526
47570
  console.log(`[ClawRouter] Image downloaded & saved \u2192 ${img.url}`);
47527
47571
  }
@@ -47610,7 +47654,7 @@ async function startProxy(options) {
47610
47654
  const [, mimeType, b64] = dataUriMatch;
47611
47655
  const ext = mimeType === "image/jpeg" ? "jpg" : mimeType.split("/")[1] ?? "png";
47612
47656
  const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
47613
- await writeFile2(join7(IMAGE_DIR, filename), Buffer.from(b64, "base64"));
47657
+ await writeFile2(join8(IMAGE_DIR, filename), Buffer.from(b64, "base64"));
47614
47658
  img.url = `http://localhost:${port2}/images/${filename}`;
47615
47659
  console.log(`[ClawRouter] Image saved \u2192 ${img.url}`);
47616
47660
  } else if (img.url?.startsWith("https://") || img.url?.startsWith("http://")) {
@@ -47621,7 +47665,7 @@ async function startProxy(options) {
47621
47665
  const ext = contentType.includes("jpeg") || contentType.includes("jpg") ? "jpg" : contentType.includes("webp") ? "webp" : "png";
47622
47666
  const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
47623
47667
  const buf = Buffer.from(await imgResp.arrayBuffer());
47624
- await writeFile2(join7(IMAGE_DIR, filename), buf);
47668
+ await writeFile2(join8(IMAGE_DIR, filename), buf);
47625
47669
  img.url = `http://localhost:${port2}/images/${filename}`;
47626
47670
  console.log(`[ClawRouter] Image downloaded & saved \u2192 ${img.url}`);
47627
47671
  }
@@ -47933,6 +47977,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
47933
47977
  let budgetDowngradeHeaderMode;
47934
47978
  let accumulatedContent = "";
47935
47979
  let responseInputTokens;
47980
+ let responseOutputTokens;
47936
47981
  const isChatCompletion = req.url?.includes("/chat/completions");
47937
47982
  const sessionId = getSessionId(req.headers);
47938
47983
  let effectiveSessionId = sessionId;
@@ -48866,6 +48911,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
48866
48911
  const timeoutId = setTimeout(() => globalController.abort(), timeoutMs);
48867
48912
  try {
48868
48913
  let modelsToTry;
48914
+ const excludeList = options.excludeModels ?? loadExcludeList();
48869
48915
  if (routingDecision) {
48870
48916
  const estimatedInputTokens = Math.ceil(body.length / 4);
48871
48917
  const estimatedTotalTokens = estimatedInputTokens + maxTokens;
@@ -48883,8 +48929,15 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
48883
48929
  `[ClawRouter] Context filter (~${estimatedTotalTokens} tokens): excluded ${contextExcluded.join(", ")}`
48884
48930
  );
48885
48931
  }
48886
- let toolFiltered = filterByToolCalling(contextFiltered, hasTools, supportsToolCalling);
48887
- const toolExcluded = contextFiltered.filter((m) => !toolFiltered.includes(m));
48932
+ const excludeFiltered = filterByExcludeList(contextFiltered, excludeList);
48933
+ const excludeExcluded = contextFiltered.filter((m) => !excludeFiltered.includes(m));
48934
+ if (excludeExcluded.length > 0) {
48935
+ console.log(
48936
+ `[ClawRouter] Exclude filter: excluded ${excludeExcluded.join(", ")} (user preference)`
48937
+ );
48938
+ }
48939
+ let toolFiltered = filterByToolCalling(excludeFiltered, hasTools, supportsToolCalling);
48940
+ const toolExcluded = excludeFiltered.filter((m) => !toolFiltered.includes(m));
48888
48941
  if (toolExcluded.length > 0) {
48889
48942
  console.log(
48890
48943
  `[ClawRouter] Tool-calling filter: excluded ${toolExcluded.join(", ")} (no structured function call support)`
@@ -48917,7 +48970,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
48917
48970
  } else {
48918
48971
  modelsToTry = modelId ? [modelId] : [];
48919
48972
  }
48920
- if (!hasTools && !modelsToTry.includes(FREE_MODEL)) {
48973
+ if (!hasTools && !modelsToTry.includes(FREE_MODEL) && !excludeList.has(FREE_MODEL)) {
48921
48974
  modelsToTry.push(FREE_MODEL);
48922
48975
  }
48923
48976
  if (options.maxCostPerRunUsd && effectiveSessionId && !isFreeModel && (options.maxCostPerRunMode ?? "graceful") === "graceful") {
@@ -49225,6 +49278,7 @@ data: [DONE]
49225
49278
  if (rsp.usage && typeof rsp.usage === "object") {
49226
49279
  const u = rsp.usage;
49227
49280
  if (typeof u.prompt_tokens === "number") responseInputTokens = u.prompt_tokens;
49281
+ if (typeof u.completion_tokens === "number") responseOutputTokens = u.completion_tokens;
49228
49282
  }
49229
49283
  const baseChunk = {
49230
49284
  id: rsp.id ?? `chatcmpl-${Date.now()}`,
@@ -49438,6 +49492,8 @@ data: [DONE]
49438
49492
  if (rspJson.usage && typeof rspJson.usage === "object") {
49439
49493
  if (typeof rspJson.usage.prompt_tokens === "number")
49440
49494
  responseInputTokens = rspJson.usage.prompt_tokens;
49495
+ if (typeof rspJson.usage.completion_tokens === "number")
49496
+ responseOutputTokens = rspJson.usage.completion_tokens;
49441
49497
  }
49442
49498
  } catch {
49443
49499
  }
@@ -49470,25 +49526,25 @@ data: [DONE]
49470
49526
  }
49471
49527
  const logModel = routingDecision?.model ?? modelId;
49472
49528
  if (logModel) {
49473
- const estimatedInputTokens = Math.ceil(body.length / 4);
49529
+ const actualInputTokens = responseInputTokens ?? Math.ceil(body.length / 4);
49530
+ const actualOutputTokens = responseOutputTokens ?? maxTokens;
49474
49531
  const accurateCosts = calculateModelCost(
49475
49532
  logModel,
49476
49533
  routerOpts.modelPricing,
49477
- estimatedInputTokens,
49478
- maxTokens,
49534
+ actualInputTokens,
49535
+ actualOutputTokens,
49479
49536
  routingProfile ?? void 0
49480
49537
  );
49481
- const costWithBuffer = accurateCosts.costEstimate * 1.2;
49482
- const baselineWithBuffer = accurateCosts.baselineCost * 1.2;
49483
49538
  const entry = {
49484
49539
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
49485
49540
  model: logModel,
49486
49541
  tier: routingDecision?.tier ?? "DIRECT",
49487
- cost: costWithBuffer,
49488
- baselineCost: baselineWithBuffer,
49542
+ cost: accurateCosts.costEstimate,
49543
+ baselineCost: accurateCosts.baselineCost,
49489
49544
  savings: accurateCosts.savings,
49490
49545
  latencyMs: Date.now() - startTime,
49491
- ...responseInputTokens !== void 0 && { inputTokens: responseInputTokens }
49546
+ ...responseInputTokens !== void 0 && { inputTokens: responseInputTokens },
49547
+ ...responseOutputTokens !== void 0 && { outputTokens: responseOutputTokens }
49492
49548
  };
49493
49549
  logUsage(entry).catch(() => {
49494
49550
  });