@blockrun/clawrouter 0.12.56 → 0.12.61

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
@@ -32878,11 +32878,20 @@ var MODEL_ALIASES = {
32878
32878
  "grok-fast": "xai/grok-4-fast-reasoning",
32879
32879
  "grok-code": "deepseek/deepseek-chat",
32880
32880
  // was grok-code-fast-1, delisted due to poor retention
32881
+ // Delisted model redirects — full model IDs that were previously valid but removed
32882
+ "grok-code-fast-1": "deepseek/deepseek-chat",
32883
+ // bare alias
32884
+ "xai/grok-code-fast-1": "deepseek/deepseek-chat",
32885
+ // delisted 2026-03-12
32886
+ "xai/grok-3-fast": "xai/grok-4-fast-reasoning",
32887
+ // delisted (too expensive)
32881
32888
  // NVIDIA
32882
32889
  nvidia: "nvidia/gpt-oss-120b",
32883
32890
  "gpt-120b": "nvidia/gpt-oss-120b",
32884
32891
  // MiniMax
32885
- minimax: "minimax/minimax-m2.5",
32892
+ minimax: "minimax/minimax-m2.7",
32893
+ "minimax-m2.7": "minimax/minimax-m2.7",
32894
+ "minimax-m2.5": "minimax/minimax-m2.5",
32886
32895
  // Z.AI GLM-5
32887
32896
  glm: "zai/glm-5",
32888
32897
  "glm-5": "zai/glm-5",
@@ -33388,6 +33397,18 @@ var BLOCKRUN_MODELS = [
33388
33397
  toolCalling: true
33389
33398
  },
33390
33399
  // MiniMax
33400
+ {
33401
+ id: "minimax/minimax-m2.7",
33402
+ name: "MiniMax M2.7",
33403
+ version: "m2.7",
33404
+ inputPrice: 0.3,
33405
+ outputPrice: 1.2,
33406
+ contextWindow: 204800,
33407
+ maxOutput: 16384,
33408
+ reasoning: true,
33409
+ agentic: true,
33410
+ toolCalling: true
33411
+ },
33391
33412
  {
33392
33413
  id: "minimax/minimax-m2.5",
33393
33414
  name: "MiniMax M2.5",
@@ -33534,10 +33555,10 @@ var blockrunProvider = {
33534
33555
  // src/proxy.ts
33535
33556
  import { createServer } from "http";
33536
33557
  import { finished } from "stream";
33537
- import { homedir as homedir4 } from "os";
33538
- import { join as join7 } from "path";
33558
+ import { homedir as homedir5 } from "os";
33559
+ import { join as join8 } from "path";
33539
33560
  import { mkdir as mkdir3, writeFile as writeFile2, readFile, stat as fsStat } from "fs/promises";
33540
- import { readFileSync, existsSync } from "fs";
33561
+ import { readFileSync as readFileSync2, existsSync } from "fs";
33541
33562
 
33542
33563
  // node_modules/viem/_esm/utils/getAction.js
33543
33564
  function getAction(client, actionFn, name) {
@@ -43487,6 +43508,11 @@ function filterByVision(models, hasVision, supportsVision2) {
43487
43508
  const filtered = models.filter(supportsVision2);
43488
43509
  return filtered.length > 0 ? filtered : models;
43489
43510
  }
43511
+ function filterByExcludeList(models, excludeList) {
43512
+ if (excludeList.size === 0) return models;
43513
+ const filtered = models.filter((m) => !excludeList.has(m));
43514
+ return filtered.length > 0 ? filtered : models;
43515
+ }
43490
43516
  function getFallbackChainFiltered(tier, tierConfigs, estimatedTotalTokens, getContextWindow) {
43491
43517
  const fullChain = getFallbackChain(tier, tierConfigs);
43492
43518
  const filtered = fullChain.filter((modelId) => {
@@ -46642,7 +46668,8 @@ var SessionStore = class {
46642
46668
  requestCount: 1,
46643
46669
  recentHashes: [],
46644
46670
  strikes: 0,
46645
- escalated: false
46671
+ escalated: false,
46672
+ sessionCostMicros: 0n
46646
46673
  });
46647
46674
  }
46648
46675
  }
@@ -46731,6 +46758,39 @@ var SessionStore = class {
46731
46758
  entry.escalated = true;
46732
46759
  return { model: nextConfig.primary, tier: nextTier };
46733
46760
  }
46761
+ /**
46762
+ * Add cost to a session's running total for maxCostPerRun tracking.
46763
+ * Cost is in USDC 6-decimal units (micros).
46764
+ * Creates a cost-tracking-only entry if none exists (e.g., explicit model requests
46765
+ * that never go through the routing path).
46766
+ */
46767
+ addSessionCost(sessionId, additionalMicros) {
46768
+ let entry = this.sessions.get(sessionId);
46769
+ if (!entry) {
46770
+ const now = Date.now();
46771
+ entry = {
46772
+ model: "",
46773
+ tier: "DIRECT",
46774
+ createdAt: now,
46775
+ lastUsedAt: now,
46776
+ requestCount: 0,
46777
+ recentHashes: [],
46778
+ strikes: 0,
46779
+ escalated: false,
46780
+ sessionCostMicros: 0n
46781
+ };
46782
+ this.sessions.set(sessionId, entry);
46783
+ }
46784
+ entry.sessionCostMicros += additionalMicros;
46785
+ }
46786
+ /**
46787
+ * Get the total accumulated cost for a session in USD.
46788
+ */
46789
+ getSessionCostUsd(sessionId) {
46790
+ const entry = this.sessions.get(sessionId);
46791
+ if (!entry) return 0;
46792
+ return Number(entry.sessionCostMicros) / 1e6;
46793
+ }
46734
46794
  /**
46735
46795
  * Stop the cleanup interval.
46736
46796
  */
@@ -46798,6 +46858,54 @@ async function checkForUpdates() {
46798
46858
  }
46799
46859
  }
46800
46860
 
46861
+ // src/exclude-models.ts
46862
+ import { readFileSync, writeFileSync, mkdirSync } from "fs";
46863
+ import { join as join7, dirname as dirname2 } from "path";
46864
+ import { homedir as homedir4 } from "os";
46865
+ var DEFAULT_FILE_PATH = join7(
46866
+ homedir4(),
46867
+ ".openclaw",
46868
+ "blockrun",
46869
+ "exclude-models.json"
46870
+ );
46871
+ function loadExcludeList(filePath = DEFAULT_FILE_PATH) {
46872
+ try {
46873
+ const raw = readFileSync(filePath, "utf-8");
46874
+ const arr = JSON.parse(raw);
46875
+ if (Array.isArray(arr)) {
46876
+ return new Set(arr.filter((x) => typeof x === "string"));
46877
+ }
46878
+ return /* @__PURE__ */ new Set();
46879
+ } catch {
46880
+ return /* @__PURE__ */ new Set();
46881
+ }
46882
+ }
46883
+ function saveExcludeList(set, filePath) {
46884
+ const sorted = [...set].sort();
46885
+ const dir = dirname2(filePath);
46886
+ mkdirSync(dir, { recursive: true });
46887
+ writeFileSync(filePath, JSON.stringify(sorted, null, 2) + "\n", "utf-8");
46888
+ }
46889
+ function addExclusion(model, filePath = DEFAULT_FILE_PATH) {
46890
+ const resolved = resolveModelAlias(model);
46891
+ const set = loadExcludeList(filePath);
46892
+ set.add(resolved);
46893
+ saveExcludeList(set, filePath);
46894
+ return resolved;
46895
+ }
46896
+ function removeExclusion(model, filePath = DEFAULT_FILE_PATH) {
46897
+ const resolved = resolveModelAlias(model);
46898
+ const set = loadExcludeList(filePath);
46899
+ const had = set.delete(resolved);
46900
+ if (had) {
46901
+ saveExcludeList(set, filePath);
46902
+ }
46903
+ return had;
46904
+ }
46905
+ function clearExclusions(filePath = DEFAULT_FILE_PATH) {
46906
+ saveExcludeList(/* @__PURE__ */ new Set(), filePath);
46907
+ }
46908
+
46801
46909
  // src/config.ts
46802
46910
  var DEFAULT_PORT = 8402;
46803
46911
  var PROXY_PORT = (() => {
@@ -46982,7 +47090,7 @@ ${lines.join("\n")}`;
46982
47090
  // src/proxy.ts
46983
47091
  var BLOCKRUN_API = "https://blockrun.ai/api";
46984
47092
  var BLOCKRUN_SOLANA_API = "https://sol.blockrun.ai/api";
46985
- var IMAGE_DIR = join7(homedir4(), ".openclaw", "blockrun", "images");
47093
+ var IMAGE_DIR = join8(homedir5(), ".openclaw", "blockrun", "images");
46986
47094
  var AUTO_MODEL = "blockrun/auto";
46987
47095
  var ROUTING_PROFILES = /* @__PURE__ */ new Set([
46988
47096
  "blockrun/free",
@@ -47006,9 +47114,11 @@ var MAX_MESSAGES = 200;
47006
47114
  var CONTEXT_LIMIT_KB = 5120;
47007
47115
  var HEARTBEAT_INTERVAL_MS = 2e3;
47008
47116
  var DEFAULT_REQUEST_TIMEOUT_MS = 18e4;
47117
+ var PER_MODEL_TIMEOUT_MS = 6e4;
47009
47118
  var MAX_FALLBACK_ATTEMPTS = 5;
47010
47119
  var HEALTH_CHECK_TIMEOUT_MS = 2e3;
47011
47120
  var RATE_LIMIT_COOLDOWN_MS = 6e4;
47121
+ var OVERLOAD_COOLDOWN_MS = 15e3;
47012
47122
  var PORT_RETRY_ATTEMPTS = 5;
47013
47123
  var PORT_RETRY_DELAY_MS = 1e3;
47014
47124
  var MODEL_BODY_READ_TIMEOUT_MS = 3e5;
@@ -47158,7 +47268,41 @@ function transformPaymentError(errorBody) {
47158
47268
  }
47159
47269
  return errorBody;
47160
47270
  }
47271
+ function categorizeError(status, body) {
47272
+ if (status === 401) return "auth_failure";
47273
+ if (status === 402) return "payment_error";
47274
+ if (status === 403) {
47275
+ if (/plan.*limit|quota.*exceeded|subscription|allowance/i.test(body))
47276
+ return "quota_exceeded";
47277
+ return "auth_failure";
47278
+ }
47279
+ if (status === 429) return "rate_limited";
47280
+ if (status === 529) return "overloaded";
47281
+ if (status === 503 && /overload|capacity|too.*many.*request/i.test(body)) return "overloaded";
47282
+ if (status >= 500) return "server_error";
47283
+ if (status === 400 || status === 413) {
47284
+ if (PROVIDER_ERROR_PATTERNS.some((p) => p.test(body))) return "config_error";
47285
+ return null;
47286
+ }
47287
+ return null;
47288
+ }
47161
47289
  var rateLimitedModels = /* @__PURE__ */ new Map();
47290
+ var overloadedModels = /* @__PURE__ */ new Map();
47291
+ var perProviderErrors = /* @__PURE__ */ new Map();
47292
+ function recordProviderError(modelId, category) {
47293
+ if (!perProviderErrors.has(modelId)) {
47294
+ perProviderErrors.set(modelId, {
47295
+ auth_failure: 0,
47296
+ quota_exceeded: 0,
47297
+ rate_limited: 0,
47298
+ overloaded: 0,
47299
+ server_error: 0,
47300
+ payment_error: 0,
47301
+ config_error: 0
47302
+ });
47303
+ }
47304
+ perProviderErrors.get(modelId)[category]++;
47305
+ }
47162
47306
  function isRateLimited(modelId) {
47163
47307
  const hitTime = rateLimitedModels.get(modelId);
47164
47308
  if (!hitTime) return false;
@@ -47173,17 +47317,30 @@ function markRateLimited(modelId) {
47173
47317
  rateLimitedModels.set(modelId, Date.now());
47174
47318
  console.log(`[ClawRouter] Model ${modelId} rate-limited, will deprioritize for 60s`);
47175
47319
  }
47320
+ function markOverloaded(modelId) {
47321
+ overloadedModels.set(modelId, Date.now());
47322
+ console.log(`[ClawRouter] Model ${modelId} overloaded, will deprioritize for 15s`);
47323
+ }
47324
+ function isOverloaded(modelId) {
47325
+ const hitTime = overloadedModels.get(modelId);
47326
+ if (!hitTime) return false;
47327
+ if (Date.now() - hitTime >= OVERLOAD_COOLDOWN_MS) {
47328
+ overloadedModels.delete(modelId);
47329
+ return false;
47330
+ }
47331
+ return true;
47332
+ }
47176
47333
  function prioritizeNonRateLimited(models) {
47177
47334
  const available = [];
47178
- const rateLimited = [];
47335
+ const degraded = [];
47179
47336
  for (const model of models) {
47180
- if (isRateLimited(model)) {
47181
- rateLimited.push(model);
47337
+ if (isRateLimited(model) || isOverloaded(model)) {
47338
+ degraded.push(model);
47182
47339
  } else {
47183
47340
  available.push(model);
47184
47341
  }
47185
47342
  }
47186
- return [...available, ...rateLimited];
47343
+ return [...available, ...degraded];
47187
47344
  }
47188
47345
  function canWrite(res) {
47189
47346
  return !res.writableEnded && !res.destroyed && res.socket !== null && !res.socket.destroyed && res.socket.writable;
@@ -47318,37 +47475,6 @@ function detectDegradedSuccessResponse(body) {
47318
47475
  }
47319
47476
  return void 0;
47320
47477
  }
47321
- var FALLBACK_STATUS_CODES = [
47322
- 400,
47323
- // Bad request - sometimes used for billing errors
47324
- 401,
47325
- // Unauthorized - provider API key issues
47326
- 402,
47327
- // Payment required - but from upstream, not x402
47328
- 403,
47329
- // Forbidden - provider restrictions
47330
- 413,
47331
- // Payload too large - request exceeds model's context limit
47332
- 429,
47333
- // Rate limited
47334
- 500,
47335
- // Internal server error
47336
- 502,
47337
- // Bad gateway
47338
- 503,
47339
- // Service unavailable
47340
- 504
47341
- // Gateway timeout
47342
- ];
47343
- function isProviderError(status, body) {
47344
- if (!FALLBACK_STATUS_CODES.includes(status)) {
47345
- return false;
47346
- }
47347
- if (status >= 500) {
47348
- return true;
47349
- }
47350
- return PROVIDER_ERROR_PATTERNS.some((pattern) => pattern.test(body));
47351
- }
47352
47478
  var VALID_ROLES = /* @__PURE__ */ new Set(["system", "user", "assistant", "tool", "function"]);
47353
47479
  var ROLE_MAPPINGS = {
47354
47480
  developer: "system",
@@ -47632,7 +47758,7 @@ async function proxyPartnerRequest(req, res, apiBase, payFetch) {
47632
47758
  });
47633
47759
  }
47634
47760
  function readImageFileAsDataUri(filePath) {
47635
- const resolved = filePath.startsWith("~/") ? join7(homedir4(), filePath.slice(2)) : filePath;
47761
+ const resolved = filePath.startsWith("~/") ? join8(homedir5(), filePath.slice(2)) : filePath;
47636
47762
  if (!existsSync(resolved)) {
47637
47763
  throw new Error(`Image file not found: ${resolved}`);
47638
47764
  }
@@ -47644,7 +47770,7 @@ function readImageFileAsDataUri(filePath) {
47644
47770
  webp: "image/webp"
47645
47771
  };
47646
47772
  const mime = mimeMap[ext] ?? "image/png";
47647
- const data = readFileSync(resolved);
47773
+ const data = readFileSync2(resolved);
47648
47774
  return `data:${mime};base64,${data.toString("base64")}`;
47649
47775
  }
47650
47776
  async function uploadDataUriToHost(dataUri) {
@@ -47762,7 +47888,9 @@ async function startProxy(options) {
47762
47888
  skipPreAuth: paymentChain === "solana"
47763
47889
  });
47764
47890
  let balanceMonitor;
47765
- if (paymentChain === "solana" && solanaAddress) {
47891
+ if (options._balanceMonitorOverride) {
47892
+ balanceMonitor = options._balanceMonitorOverride;
47893
+ } else if (paymentChain === "solana" && solanaAddress) {
47766
47894
  const { SolanaBalanceMonitor: SolanaBalanceMonitor2 } = await Promise.resolve().then(() => (init_solana_balance(), solana_balance_exports));
47767
47895
  balanceMonitor = new SolanaBalanceMonitor2(solanaAddress);
47768
47896
  } else {
@@ -47854,7 +47982,16 @@ async function startProxy(options) {
47854
47982
  "Content-Type": "application/json",
47855
47983
  "Cache-Control": "no-cache"
47856
47984
  });
47857
- res.end(JSON.stringify(stats, null, 2));
47985
+ res.end(
47986
+ JSON.stringify(
47987
+ {
47988
+ ...stats,
47989
+ providerErrors: Object.fromEntries(perProviderErrors)
47990
+ },
47991
+ null,
47992
+ 2
47993
+ )
47994
+ );
47858
47995
  } catch (err) {
47859
47996
  res.writeHead(500, { "Content-Type": "application/json" });
47860
47997
  res.end(
@@ -47878,7 +48015,7 @@ async function startProxy(options) {
47878
48015
  res.end("Bad request");
47879
48016
  return;
47880
48017
  }
47881
- const filePath = join7(IMAGE_DIR, filename);
48018
+ const filePath = join8(IMAGE_DIR, filename);
47882
48019
  try {
47883
48020
  const s3 = await fsStat(filePath);
47884
48021
  if (!s3.isFile()) throw new Error("not a file");
@@ -47937,7 +48074,7 @@ async function startProxy(options) {
47937
48074
  const [, mimeType, b64] = dataUriMatch;
47938
48075
  const ext = mimeType === "image/jpeg" ? "jpg" : mimeType.split("/")[1] ?? "png";
47939
48076
  const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
47940
- await writeFile2(join7(IMAGE_DIR, filename), Buffer.from(b64, "base64"));
48077
+ await writeFile2(join8(IMAGE_DIR, filename), Buffer.from(b64, "base64"));
47941
48078
  img.url = `http://localhost:${port2}/images/${filename}`;
47942
48079
  console.log(`[ClawRouter] Image saved \u2192 ${img.url}`);
47943
48080
  } else if (img.url?.startsWith("https://") || img.url?.startsWith("http://")) {
@@ -47948,7 +48085,7 @@ async function startProxy(options) {
47948
48085
  const ext = contentType.includes("jpeg") || contentType.includes("jpg") ? "jpg" : contentType.includes("webp") ? "webp" : "png";
47949
48086
  const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
47950
48087
  const buf = Buffer.from(await imgResp.arrayBuffer());
47951
- await writeFile2(join7(IMAGE_DIR, filename), buf);
48088
+ await writeFile2(join8(IMAGE_DIR, filename), buf);
47952
48089
  img.url = `http://localhost:${port2}/images/${filename}`;
47953
48090
  console.log(`[ClawRouter] Image downloaded & saved \u2192 ${img.url}`);
47954
48091
  }
@@ -48037,7 +48174,7 @@ async function startProxy(options) {
48037
48174
  const [, mimeType, b64] = dataUriMatch;
48038
48175
  const ext = mimeType === "image/jpeg" ? "jpg" : mimeType.split("/")[1] ?? "png";
48039
48176
  const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
48040
- await writeFile2(join7(IMAGE_DIR, filename), Buffer.from(b64, "base64"));
48177
+ await writeFile2(join8(IMAGE_DIR, filename), Buffer.from(b64, "base64"));
48041
48178
  img.url = `http://localhost:${port2}/images/${filename}`;
48042
48179
  console.log(`[ClawRouter] Image saved \u2192 ${img.url}`);
48043
48180
  } else if (img.url?.startsWith("https://") || img.url?.startsWith("http://")) {
@@ -48048,7 +48185,7 @@ async function startProxy(options) {
48048
48185
  const ext = contentType.includes("jpeg") || contentType.includes("jpg") ? "jpg" : contentType.includes("webp") ? "webp" : "png";
48049
48186
  const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
48050
48187
  const buf = Buffer.from(await imgResp.arrayBuffer());
48051
- await writeFile2(join7(IMAGE_DIR, filename), buf);
48188
+ await writeFile2(join8(IMAGE_DIR, filename), buf);
48052
48189
  img.url = `http://localhost:${port2}/images/${filename}`;
48053
48190
  console.log(`[ClawRouter] Image downloaded & saved \u2192 ${img.url}`);
48054
48191
  }
@@ -48297,12 +48434,13 @@ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxT
48297
48434
  if (response.status !== 200) {
48298
48435
  const errorBodyChunks = await readBodyWithTimeout(response.body, ERROR_BODY_READ_TIMEOUT_MS);
48299
48436
  const errorBody = Buffer.concat(errorBodyChunks).toString();
48300
- const isProviderErr = isProviderError(response.status, errorBody);
48437
+ const category = categorizeError(response.status, errorBody);
48301
48438
  return {
48302
48439
  success: false,
48303
48440
  errorBody,
48304
48441
  errorStatus: response.status,
48305
- isProviderError: isProviderErr
48442
+ isProviderError: category !== null,
48443
+ errorCategory: category ?? void 0
48306
48444
  };
48307
48445
  }
48308
48446
  const contentType = response.headers.get("content-type") || "";
@@ -48355,8 +48493,11 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
48355
48493
  let maxTokens = 4096;
48356
48494
  let routingProfile = null;
48357
48495
  let balanceFallbackNotice;
48496
+ let budgetDowngradeNotice;
48497
+ let budgetDowngradeHeaderMode;
48358
48498
  let accumulatedContent = "";
48359
48499
  let responseInputTokens;
48500
+ let responseOutputTokens;
48360
48501
  const isChatCompletion = req.url?.includes("/chat/completions");
48361
48502
  const sessionId = getSessionId(req.headers);
48362
48503
  let effectiveSessionId = sessionId;
@@ -48369,6 +48510,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
48369
48510
  let bodyModified = false;
48370
48511
  const parsedMessages = Array.isArray(parsed.messages) ? parsed.messages : [];
48371
48512
  const lastUserMsg = [...parsedMessages].reverse().find((m) => m.role === "user");
48513
+ hasTools = Array.isArray(parsed.tools) && parsed.tools.length > 0;
48372
48514
  const rawLastContent = lastUserMsg?.content;
48373
48515
  const lastContent = typeof rawLastContent === "string" ? rawLastContent : Array.isArray(rawLastContent) ? rawLastContent.filter((b) => b.type === "text").map((b) => b.text ?? "").join(" ") : "";
48374
48516
  if (sessionId && parsedMessages.length > 0) {
@@ -49025,6 +49167,9 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
49025
49167
  options.onRouted?.(routingDecision);
49026
49168
  }
49027
49169
  }
49170
+ if (!effectiveSessionId && parsedMessages.length > 0) {
49171
+ effectiveSessionId = deriveSessionId(parsedMessages);
49172
+ }
49028
49173
  if (bodyModified) {
49029
49174
  body = Buffer.from(JSON.stringify(parsed));
49030
49175
  }
@@ -49115,7 +49260,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
49115
49260
  }
49116
49261
  deduplicator.markInflight(dedupKey);
49117
49262
  let estimatedCostMicros;
49118
- const isFreeModel = modelId === FREE_MODEL;
49263
+ let isFreeModel = modelId === FREE_MODEL;
49119
49264
  if (modelId && !options.skipBalanceCheck && !isFreeModel) {
49120
49265
  const estimated = estimateAmount(modelId, body.length, maxTokens);
49121
49266
  if (estimated) {
@@ -49128,6 +49273,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
49128
49273
  `[ClawRouter] Wallet ${sufficiency.info.isEmpty ? "empty" : "insufficient"} (${sufficiency.info.balanceUSD}), falling back to free model: ${FREE_MODEL} (requested: ${originalModel})`
49129
49274
  );
49130
49275
  modelId = FREE_MODEL;
49276
+ isFreeModel = true;
49131
49277
  const parsed = JSON.parse(body.toString());
49132
49278
  parsed.model = FREE_MODEL;
49133
49279
  body = Buffer.from(JSON.stringify(parsed));
@@ -49154,6 +49300,89 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
49154
49300
  }
49155
49301
  }
49156
49302
  }
49303
+ if (options.maxCostPerRunUsd && effectiveSessionId && !isFreeModel && (options.maxCostPerRunMode ?? "graceful") === "strict") {
49304
+ const runCostUsd = sessionStore.getSessionCostUsd(effectiveSessionId);
49305
+ const thisReqEstStr = estimatedCostMicros !== void 0 ? estimatedCostMicros.toString() : modelId ? estimateAmount(modelId, body.length, maxTokens) : void 0;
49306
+ const thisReqEstUsd = thisReqEstStr ? Number(thisReqEstStr) / 1e6 : 0;
49307
+ const projectedCostUsd = runCostUsd + thisReqEstUsd;
49308
+ if (projectedCostUsd > options.maxCostPerRunUsd) {
49309
+ console.log(
49310
+ `[ClawRouter] Cost cap exceeded for session ${effectiveSessionId.slice(0, 8)}...: projected $${projectedCostUsd.toFixed(4)} (spent $${runCostUsd.toFixed(4)} + est $${thisReqEstUsd.toFixed(4)}) > $${options.maxCostPerRunUsd} limit`
49311
+ );
49312
+ res.writeHead(429, {
49313
+ "Content-Type": "application/json",
49314
+ "X-ClawRouter-Cost-Cap-Exceeded": "1"
49315
+ });
49316
+ res.end(
49317
+ JSON.stringify({
49318
+ error: {
49319
+ message: `ClawRouter cost cap exceeded: projected spend $${projectedCostUsd.toFixed(4)} (spent $${runCostUsd.toFixed(4)} + est $${thisReqEstUsd.toFixed(4)}) would exceed limit $${options.maxCostPerRunUsd}`,
49320
+ type: "cost_cap_exceeded",
49321
+ code: "cost_cap_exceeded"
49322
+ }
49323
+ })
49324
+ );
49325
+ deduplicator.removeInflight(dedupKey);
49326
+ return;
49327
+ }
49328
+ }
49329
+ if (options.maxCostPerRunUsd && effectiveSessionId && !isFreeModel && (options.maxCostPerRunMode ?? "graceful") === "graceful") {
49330
+ const runCostUsd = sessionStore.getSessionCostUsd(effectiveSessionId);
49331
+ const remainingUsd = options.maxCostPerRunUsd - runCostUsd;
49332
+ const isComplexOrAgentic = hasTools || routingDecision?.tier === "COMPLEX" || routingDecision?.tier === "REASONING";
49333
+ if (isComplexOrAgentic) {
49334
+ const canAffordAnyNonFreeModel = BLOCKRUN_MODELS.some((m) => {
49335
+ if (m.id === FREE_MODEL) return false;
49336
+ const est = estimateAmount(m.id, body.length, maxTokens);
49337
+ return est !== void 0 && Number(est) / 1e6 <= remainingUsd;
49338
+ });
49339
+ if (!canAffordAnyNonFreeModel) {
49340
+ console.log(
49341
+ `[ClawRouter] Budget insufficient for agentic/complex session ${effectiveSessionId.slice(0, 8)}...: $${Math.max(0, remainingUsd).toFixed(4)} remaining \u2014 blocking (silent downgrade would corrupt tool/complex responses)`
49342
+ );
49343
+ res.writeHead(429, {
49344
+ "Content-Type": "application/json",
49345
+ "X-ClawRouter-Cost-Cap-Exceeded": "1",
49346
+ "X-ClawRouter-Budget-Mode": "blocked"
49347
+ });
49348
+ res.end(
49349
+ JSON.stringify({
49350
+ error: {
49351
+ message: `ClawRouter budget exhausted: $${Math.max(0, remainingUsd).toFixed(4)} remaining (limit: $${options.maxCostPerRunUsd}). Increase maxCostPerRun to continue.`,
49352
+ type: "cost_cap_exceeded",
49353
+ code: "budget_exhausted"
49354
+ }
49355
+ })
49356
+ );
49357
+ deduplicator.removeInflight(dedupKey);
49358
+ return;
49359
+ }
49360
+ } else if (!routingDecision && modelId && modelId !== FREE_MODEL) {
49361
+ const est = estimateAmount(modelId, body.length, maxTokens);
49362
+ const canAfford = !est || Number(est) / 1e6 <= remainingUsd;
49363
+ if (!canAfford) {
49364
+ console.log(
49365
+ `[ClawRouter] Budget insufficient for explicit model ${modelId} in session ${effectiveSessionId.slice(0, 8)}...: $${Math.max(0, remainingUsd).toFixed(4)} remaining \u2014 blocking (user explicitly chose ${modelId})`
49366
+ );
49367
+ res.writeHead(429, {
49368
+ "Content-Type": "application/json",
49369
+ "X-ClawRouter-Cost-Cap-Exceeded": "1",
49370
+ "X-ClawRouter-Budget-Mode": "blocked"
49371
+ });
49372
+ res.end(
49373
+ JSON.stringify({
49374
+ error: {
49375
+ message: `ClawRouter budget exhausted: $${Math.max(0, remainingUsd).toFixed(4)} remaining (limit: $${options.maxCostPerRunUsd}). Increase maxCostPerRun to continue using ${modelId}.`,
49376
+ type: "cost_cap_exceeded",
49377
+ code: "budget_exhausted"
49378
+ }
49379
+ })
49380
+ );
49381
+ deduplicator.removeInflight(dedupKey);
49382
+ return;
49383
+ }
49384
+ }
49385
+ }
49157
49386
  let heartbeatInterval;
49158
49387
  let headersSentEarly = false;
49159
49388
  if (isStreaming) {
@@ -49198,10 +49427,11 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
49198
49427
  }
49199
49428
  });
49200
49429
  const timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
49201
- const controller = new AbortController();
49202
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
49430
+ const globalController = new AbortController();
49431
+ const timeoutId = setTimeout(() => globalController.abort(), timeoutMs);
49203
49432
  try {
49204
49433
  let modelsToTry;
49434
+ const excludeList = options.excludeModels ?? loadExcludeList();
49205
49435
  if (routingDecision) {
49206
49436
  const estimatedInputTokens = Math.ceil(body.length / 4);
49207
49437
  const estimatedTotalTokens = estimatedInputTokens + maxTokens;
@@ -49219,8 +49449,15 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
49219
49449
  `[ClawRouter] Context filter (~${estimatedTotalTokens} tokens): excluded ${contextExcluded.join(", ")}`
49220
49450
  );
49221
49451
  }
49222
- let toolFiltered = filterByToolCalling(contextFiltered, hasTools, supportsToolCalling);
49223
- const toolExcluded = contextFiltered.filter((m) => !toolFiltered.includes(m));
49452
+ const excludeFiltered = filterByExcludeList(contextFiltered, excludeList);
49453
+ const excludeExcluded = contextFiltered.filter((m) => !excludeFiltered.includes(m));
49454
+ if (excludeExcluded.length > 0) {
49455
+ console.log(
49456
+ `[ClawRouter] Exclude filter: excluded ${excludeExcluded.join(", ")} (user preference)`
49457
+ );
49458
+ }
49459
+ let toolFiltered = filterByToolCalling(excludeFiltered, hasTools, supportsToolCalling);
49460
+ const toolExcluded = excludeFiltered.filter((m) => !toolFiltered.includes(m));
49224
49461
  if (toolExcluded.length > 0) {
49225
49462
  console.log(
49226
49463
  `[ClawRouter] Tool-calling filter: excluded ${toolExcluded.join(", ")} (no structured function call support)`
@@ -49253,16 +49490,86 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
49253
49490
  } else {
49254
49491
  modelsToTry = modelId ? [modelId] : [];
49255
49492
  }
49256
- if (!hasTools && !modelsToTry.includes(FREE_MODEL)) {
49493
+ if (!hasTools && !modelsToTry.includes(FREE_MODEL) && !excludeList.has(FREE_MODEL)) {
49257
49494
  modelsToTry.push(FREE_MODEL);
49258
49495
  }
49496
+ if (options.maxCostPerRunUsd && effectiveSessionId && !isFreeModel && (options.maxCostPerRunMode ?? "graceful") === "graceful") {
49497
+ const runCostUsd = sessionStore.getSessionCostUsd(effectiveSessionId);
49498
+ const remainingUsd = options.maxCostPerRunUsd - runCostUsd;
49499
+ const beforeFilter = [...modelsToTry];
49500
+ modelsToTry = modelsToTry.filter((m) => {
49501
+ if (m === FREE_MODEL) return true;
49502
+ const est = estimateAmount(m, body.length, maxTokens);
49503
+ if (!est) return true;
49504
+ return Number(est) / 1e6 <= remainingUsd;
49505
+ });
49506
+ const excluded = beforeFilter.filter((m) => !modelsToTry.includes(m));
49507
+ const isComplexOrAgenticFilter = hasTools || routingDecision?.tier === "COMPLEX" || routingDecision?.tier === "REASONING" || routingDecision === void 0;
49508
+ const filteredToFreeOnly = modelsToTry.length > 0 && modelsToTry.every((m) => m === FREE_MODEL);
49509
+ if (isComplexOrAgenticFilter && filteredToFreeOnly) {
49510
+ const budgetSummary = `$${Math.max(0, remainingUsd).toFixed(4)} remaining (limit: $${options.maxCostPerRunUsd})`;
49511
+ console.log(
49512
+ `[ClawRouter] Budget filter left only free model for complex/agentic session \u2014 blocking (${budgetSummary})`
49513
+ );
49514
+ const errPayload = JSON.stringify({
49515
+ error: {
49516
+ message: `ClawRouter budget exhausted: remaining budget (${budgetSummary}) cannot support a complex/tool request. Increase maxCostPerRun to continue.`,
49517
+ type: "cost_cap_exceeded",
49518
+ code: "budget_exhausted"
49519
+ }
49520
+ });
49521
+ if (heartbeatInterval) clearInterval(heartbeatInterval);
49522
+ if (headersSentEarly) {
49523
+ safeWrite(res, `data: ${errPayload}
49524
+
49525
+ data: [DONE]
49526
+
49527
+ `);
49528
+ res.end();
49529
+ } else {
49530
+ res.writeHead(429, {
49531
+ "Content-Type": "application/json",
49532
+ "X-ClawRouter-Cost-Cap-Exceeded": "1",
49533
+ "X-ClawRouter-Budget-Mode": "blocked"
49534
+ });
49535
+ res.end(errPayload);
49536
+ }
49537
+ deduplicator.removeInflight(dedupKey);
49538
+ return;
49539
+ }
49540
+ if (excluded.length > 0) {
49541
+ const budgetSummary = remainingUsd > 0 ? `$${remainingUsd.toFixed(4)} remaining` : `budget exhausted ($${runCostUsd.toFixed(4)}/$${options.maxCostPerRunUsd})`;
49542
+ console.log(
49543
+ `[ClawRouter] Budget downgrade (${budgetSummary}): excluded ${excluded.join(", ")}`
49544
+ );
49545
+ const fromModel = excluded[0];
49546
+ const usingFree = modelsToTry.length === 1 && modelsToTry[0] === FREE_MODEL;
49547
+ if (usingFree) {
49548
+ budgetDowngradeNotice = `> **\u26A0\uFE0F Budget cap reached** ($${runCostUsd.toFixed(4)}/$${options.maxCostPerRunUsd}) \u2014 downgraded to free model. Quality may be reduced. Increase \`maxCostPerRun\` to continue with ${fromModel}.
49549
+
49550
+ `;
49551
+ } else {
49552
+ const toModel = modelsToTry[0] ?? FREE_MODEL;
49553
+ budgetDowngradeNotice = `> **\u26A0\uFE0F Budget low** ($${remainingUsd > 0 ? remainingUsd.toFixed(4) : "0.0000"} remaining) \u2014 using ${toModel} instead of ${fromModel}.
49554
+
49555
+ `;
49556
+ }
49557
+ budgetDowngradeHeaderMode = "downgraded";
49558
+ }
49559
+ }
49259
49560
  let upstream;
49260
49561
  let lastError;
49261
49562
  let actualModelUsed = modelId;
49262
49563
  for (let i = 0; i < modelsToTry.length; i++) {
49263
49564
  const tryModel = modelsToTry[i];
49264
49565
  const isLastAttempt = i === modelsToTry.length - 1;
49566
+ if (globalController.signal.aborted) {
49567
+ throw new Error(`Request timed out after ${timeoutMs}ms`);
49568
+ }
49265
49569
  console.log(`[ClawRouter] Trying model ${i + 1}/${modelsToTry.length}: ${tryModel}`);
49570
+ const modelController = new AbortController();
49571
+ const modelTimeoutId = setTimeout(() => modelController.abort(), PER_MODEL_TIMEOUT_MS);
49572
+ const combinedSignal = AbortSignal.any([globalController.signal, modelController.signal]);
49266
49573
  const result = await tryModelRequest(
49267
49574
  upstreamUrl,
49268
49575
  req.method ?? "POST",
@@ -49272,12 +49579,29 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
49272
49579
  maxTokens,
49273
49580
  payFetch,
49274
49581
  balanceMonitor,
49275
- controller.signal
49582
+ combinedSignal
49276
49583
  );
49584
+ clearTimeout(modelTimeoutId);
49585
+ if (globalController.signal.aborted) {
49586
+ throw new Error(`Request timed out after ${timeoutMs}ms`);
49587
+ }
49588
+ if (!result.success && modelController.signal.aborted && !isLastAttempt) {
49589
+ console.log(
49590
+ `[ClawRouter] Model ${tryModel} timed out after ${PER_MODEL_TIMEOUT_MS}ms, trying fallback`
49591
+ );
49592
+ recordProviderError(tryModel, "server_error");
49593
+ continue;
49594
+ }
49277
49595
  if (result.success && result.response) {
49278
49596
  upstream = result.response;
49279
49597
  actualModelUsed = tryModel;
49280
49598
  console.log(`[ClawRouter] Success with model: ${tryModel}`);
49599
+ if (options.maxCostPerRunUsd && effectiveSessionId && tryModel !== FREE_MODEL) {
49600
+ const costEst = estimateAmount(tryModel, body.length, maxTokens);
49601
+ if (costEst) {
49602
+ sessionStore.addSessionCost(effectiveSessionId, BigInt(costEst));
49603
+ }
49604
+ }
49281
49605
  break;
49282
49606
  }
49283
49607
  lastError = {
@@ -49293,7 +49617,52 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
49293
49617
  );
49294
49618
  break;
49295
49619
  }
49296
- if (result.errorStatus === 429) {
49620
+ const errorCat = result.errorCategory;
49621
+ if (errorCat) {
49622
+ recordProviderError(tryModel, errorCat);
49623
+ }
49624
+ if (errorCat === "rate_limited") {
49625
+ if (!isLastAttempt && !globalController.signal.aborted) {
49626
+ console.log(
49627
+ `[ClawRouter] Rate-limited on ${tryModel}, retrying in 200ms before failover`
49628
+ );
49629
+ await new Promise((resolve) => setTimeout(resolve, 200));
49630
+ if (!globalController.signal.aborted) {
49631
+ const retryController = new AbortController();
49632
+ const retryTimeoutId = setTimeout(
49633
+ () => retryController.abort(),
49634
+ PER_MODEL_TIMEOUT_MS
49635
+ );
49636
+ const retrySignal = AbortSignal.any([
49637
+ globalController.signal,
49638
+ retryController.signal
49639
+ ]);
49640
+ const retryResult = await tryModelRequest(
49641
+ upstreamUrl,
49642
+ req.method ?? "POST",
49643
+ headers,
49644
+ body,
49645
+ tryModel,
49646
+ maxTokens,
49647
+ payFetch,
49648
+ balanceMonitor,
49649
+ retrySignal
49650
+ );
49651
+ clearTimeout(retryTimeoutId);
49652
+ if (retryResult.success && retryResult.response) {
49653
+ upstream = retryResult.response;
49654
+ actualModelUsed = tryModel;
49655
+ console.log(`[ClawRouter] Rate-limit retry succeeded for: ${tryModel}`);
49656
+ if (options.maxCostPerRunUsd && effectiveSessionId && tryModel !== FREE_MODEL) {
49657
+ const costEst = estimateAmount(tryModel, body.length, maxTokens);
49658
+ if (costEst) {
49659
+ sessionStore.addSessionCost(effectiveSessionId, BigInt(costEst));
49660
+ }
49661
+ }
49662
+ break;
49663
+ }
49664
+ }
49665
+ }
49297
49666
  markRateLimited(tryModel);
49298
49667
  try {
49299
49668
  const parsed = JSON.parse(result.errorBody || "{}");
@@ -49309,6 +49678,12 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
49309
49678
  }
49310
49679
  } catch {
49311
49680
  }
49681
+ } else if (errorCat === "overloaded") {
49682
+ markOverloaded(tryModel);
49683
+ } else if (errorCat === "auth_failure" || errorCat === "quota_exceeded") {
49684
+ console.log(
49685
+ `[ClawRouter] \u{1F511} ${errorCat === "auth_failure" ? "Auth failure" : "Quota exceeded"} for ${tryModel} \u2014 check provider config`
49686
+ );
49312
49687
  }
49313
49688
  const isPaymentErr = /payment.*verification.*failed|payment.*settlement.*failed|insufficient.*funds|transaction_simulation_failed/i.test(
49314
49689
  result.errorBody || ""
@@ -49423,6 +49798,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
49423
49798
  if (rsp.usage && typeof rsp.usage === "object") {
49424
49799
  const u = rsp.usage;
49425
49800
  if (typeof u.prompt_tokens === "number") responseInputTokens = u.prompt_tokens;
49801
+ if (typeof u.completion_tokens === "number") responseOutputTokens = u.completion_tokens;
49426
49802
  }
49427
49803
  const baseChunk = {
49428
49804
  id: rsp.id ?? `chatcmpl-${Date.now()}`,
@@ -49468,6 +49844,25 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
49468
49844
  responseChunks.push(Buffer.from(noticeData));
49469
49845
  balanceFallbackNotice = void 0;
49470
49846
  }
49847
+ if (budgetDowngradeNotice) {
49848
+ const noticeChunk = {
49849
+ ...baseChunk,
49850
+ choices: [
49851
+ {
49852
+ index: index2,
49853
+ delta: { content: budgetDowngradeNotice },
49854
+ logprobs: null,
49855
+ finish_reason: null
49856
+ }
49857
+ ]
49858
+ };
49859
+ const noticeData = `data: ${JSON.stringify(noticeChunk)}
49860
+
49861
+ `;
49862
+ safeWrite(res, noticeData);
49863
+ responseChunks.push(Buffer.from(noticeData));
49864
+ budgetDowngradeNotice = void 0;
49865
+ }
49471
49866
  if (content) {
49472
49867
  const contentChunk = {
49473
49868
  ...baseChunk,
@@ -49571,6 +49966,22 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
49571
49966
  }
49572
49967
  balanceFallbackNotice = void 0;
49573
49968
  }
49969
+ if (budgetDowngradeNotice && responseBody.length > 0) {
49970
+ try {
49971
+ const parsed = JSON.parse(responseBody.toString());
49972
+ if (parsed.choices?.[0]?.message?.content !== void 0) {
49973
+ parsed.choices[0].message.content = budgetDowngradeNotice + parsed.choices[0].message.content;
49974
+ responseBody = Buffer.from(JSON.stringify(parsed));
49975
+ }
49976
+ } catch {
49977
+ }
49978
+ budgetDowngradeNotice = void 0;
49979
+ }
49980
+ if (budgetDowngradeHeaderMode) {
49981
+ responseHeaders["x-clawrouter-budget-downgrade"] = "1";
49982
+ responseHeaders["x-clawrouter-budget-mode"] = budgetDowngradeHeaderMode;
49983
+ budgetDowngradeHeaderMode = void 0;
49984
+ }
49574
49985
  responseHeaders["content-length"] = String(responseBody.length);
49575
49986
  res.writeHead(upstream.status, responseHeaders);
49576
49987
  safeWrite(res, responseBody);
@@ -49601,6 +50012,8 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
49601
50012
  if (rspJson.usage && typeof rspJson.usage === "object") {
49602
50013
  if (typeof rspJson.usage.prompt_tokens === "number")
49603
50014
  responseInputTokens = rspJson.usage.prompt_tokens;
50015
+ if (typeof rspJson.usage.completion_tokens === "number")
50016
+ responseOutputTokens = rspJson.usage.completion_tokens;
49604
50017
  }
49605
50018
  } catch {
49606
50019
  }
@@ -49633,25 +50046,25 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
49633
50046
  }
49634
50047
  const logModel = routingDecision?.model ?? modelId;
49635
50048
  if (logModel) {
49636
- const estimatedInputTokens = Math.ceil(body.length / 4);
50049
+ const actualInputTokens = responseInputTokens ?? Math.ceil(body.length / 4);
50050
+ const actualOutputTokens = responseOutputTokens ?? maxTokens;
49637
50051
  const accurateCosts = calculateModelCost(
49638
50052
  logModel,
49639
50053
  routerOpts.modelPricing,
49640
- estimatedInputTokens,
49641
- maxTokens,
50054
+ actualInputTokens,
50055
+ actualOutputTokens,
49642
50056
  routingProfile ?? void 0
49643
50057
  );
49644
- const costWithBuffer = accurateCosts.costEstimate * 1.2;
49645
- const baselineWithBuffer = accurateCosts.baselineCost * 1.2;
49646
50058
  const entry = {
49647
50059
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
49648
50060
  model: logModel,
49649
50061
  tier: routingDecision?.tier ?? "DIRECT",
49650
- cost: costWithBuffer,
49651
- baselineCost: baselineWithBuffer,
50062
+ cost: accurateCosts.costEstimate,
50063
+ baselineCost: accurateCosts.baselineCost,
49652
50064
  savings: accurateCosts.savings,
49653
50065
  latencyMs: Date.now() - startTime,
49654
- ...responseInputTokens !== void 0 && { inputTokens: responseInputTokens }
50066
+ ...responseInputTokens !== void 0 && { inputTokens: responseInputTokens },
50067
+ ...responseOutputTokens !== void 0 && { outputTokens: responseOutputTokens }
49655
50068
  };
49656
50069
  logUsage(entry).catch(() => {
49657
50070
  });
@@ -49660,15 +50073,15 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
49660
50073
 
49661
50074
  // src/index.ts
49662
50075
  import {
49663
- writeFileSync as writeFileSync2,
50076
+ writeFileSync as writeFileSync3,
49664
50077
  existsSync as existsSync3,
49665
50078
  readdirSync,
49666
- mkdirSync as mkdirSync2,
50079
+ mkdirSync as mkdirSync3,
49667
50080
  copyFileSync,
49668
50081
  renameSync
49669
50082
  } from "fs";
49670
- import { homedir as homedir6 } from "os";
49671
- import { join as join9 } from "path";
50083
+ import { homedir as homedir7 } from "os";
50084
+ import { join as join10 } from "path";
49672
50085
  init_accounts();
49673
50086
 
49674
50087
  // src/partners/registry.ts
@@ -49772,8 +50185,8 @@ init_solana_balance();
49772
50185
  // src/spend-control.ts
49773
50186
  import * as fs from "fs";
49774
50187
  import * as path from "path";
49775
- import { homedir as homedir5 } from "os";
49776
- var WALLET_DIR2 = path.join(homedir5(), ".openclaw", "blockrun");
50188
+ import { homedir as homedir6 } from "os";
50189
+ var WALLET_DIR2 = path.join(homedir6(), ".openclaw", "blockrun");
49777
50190
  var HOUR_MS = 60 * 60 * 1e3;
49778
50191
  var DAY_MS = 24 * HOUR_MS;
49779
50192
  var FileSpendControlStorage = class {
@@ -50098,13 +50511,13 @@ function isGatewayMode() {
50098
50511
  return args.includes("gateway");
50099
50512
  }
50100
50513
  function injectModelsConfig(logger) {
50101
- const configDir = join9(homedir6(), ".openclaw");
50102
- const configPath = join9(configDir, "openclaw.json");
50514
+ const configDir = join10(homedir7(), ".openclaw");
50515
+ const configPath = join10(configDir, "openclaw.json");
50103
50516
  let config = {};
50104
50517
  let needsWrite = false;
50105
50518
  if (!existsSync3(configDir)) {
50106
50519
  try {
50107
- mkdirSync2(configDir, { recursive: true });
50520
+ mkdirSync3(configDir, { recursive: true });
50108
50521
  logger.info("Created OpenClaw config directory");
50109
50522
  } catch (err) {
50110
50523
  logger.info(
@@ -50238,6 +50651,18 @@ function injectModelsConfig(logger) {
50238
50651
  needsWrite = true;
50239
50652
  }
50240
50653
  const allowlist = defaults.models;
50654
+ const DEPRECATED_BLOCKRUN_MODELS = ["blockrun/xai/grok-code-fast-1"];
50655
+ let removedDeprecatedCount = 0;
50656
+ for (const key of DEPRECATED_BLOCKRUN_MODELS) {
50657
+ if (allowlist[key]) {
50658
+ delete allowlist[key];
50659
+ removedDeprecatedCount++;
50660
+ }
50661
+ }
50662
+ if (removedDeprecatedCount > 0) {
50663
+ needsWrite = true;
50664
+ logger.info(`Removed ${removedDeprecatedCount} deprecated model entries from allowlist`);
50665
+ }
50241
50666
  let addedCount = 0;
50242
50667
  for (const id of TOP_MODELS) {
50243
50668
  const key = `blockrun/${id}`;
@@ -50253,7 +50678,7 @@ function injectModelsConfig(logger) {
50253
50678
  if (needsWrite) {
50254
50679
  try {
50255
50680
  const tmpPath = `${configPath}.tmp.${process.pid}`;
50256
- writeFileSync2(tmpPath, JSON.stringify(config, null, 2));
50681
+ writeFileSync3(tmpPath, JSON.stringify(config, null, 2));
50257
50682
  renameSync(tmpPath, configPath);
50258
50683
  logger.info("Smart routing enabled (blockrun/auto)");
50259
50684
  } catch (err) {
@@ -50262,10 +50687,10 @@ function injectModelsConfig(logger) {
50262
50687
  }
50263
50688
  }
50264
50689
  function injectAuthProfile(logger) {
50265
- const agentsDir = join9(homedir6(), ".openclaw", "agents");
50690
+ const agentsDir = join10(homedir7(), ".openclaw", "agents");
50266
50691
  if (!existsSync3(agentsDir)) {
50267
50692
  try {
50268
- mkdirSync2(agentsDir, { recursive: true });
50693
+ mkdirSync3(agentsDir, { recursive: true });
50269
50694
  } catch (err) {
50270
50695
  logger.info(
50271
50696
  `Could not create agents dir: ${err instanceof Error ? err.message : String(err)}`
@@ -50279,11 +50704,11 @@ function injectAuthProfile(logger) {
50279
50704
  agents = ["main", ...agents];
50280
50705
  }
50281
50706
  for (const agentId of agents) {
50282
- const authDir = join9(agentsDir, agentId, "agent");
50283
- const authPath = join9(authDir, "auth-profiles.json");
50707
+ const authDir = join10(agentsDir, agentId, "agent");
50708
+ const authPath = join10(authDir, "auth-profiles.json");
50284
50709
  if (!existsSync3(authDir)) {
50285
50710
  try {
50286
- mkdirSync2(authDir, { recursive: true });
50711
+ mkdirSync3(authDir, { recursive: true });
50287
50712
  } catch {
50288
50713
  continue;
50289
50714
  }
@@ -50311,7 +50736,7 @@ function injectAuthProfile(logger) {
50311
50736
  key: "x402-proxy-handles-auth"
50312
50737
  };
50313
50738
  try {
50314
- writeFileSync2(authPath, JSON.stringify(store, null, 2));
50739
+ writeFileSync3(authPath, JSON.stringify(store, null, 2));
50315
50740
  logger.info(`Injected BlockRun auth profile for agent: ${agentId}`);
50316
50741
  } catch (err) {
50317
50742
  logger.info(
@@ -50339,9 +50764,18 @@ async function startProxyInBackground(api) {
50339
50764
  api.logger.info(`Using wallet from BLOCKRUN_WALLET_KEY: ${wallet.address}`);
50340
50765
  }
50341
50766
  const routingConfig = api.pluginConfig?.routing;
50767
+ const maxCostPerRunUsd = typeof api.pluginConfig?.maxCostPerRun === "number" ? api.pluginConfig.maxCostPerRun : void 0;
50768
+ const maxCostPerRunMode = api.pluginConfig?.maxCostPerRunMode === "strict" ? "strict" : "graceful";
50769
+ if (maxCostPerRunUsd !== void 0) {
50770
+ api.logger.info(
50771
+ `Cost cap: $${maxCostPerRunUsd.toFixed(2)} per session (mode: ${maxCostPerRunMode})`
50772
+ );
50773
+ }
50342
50774
  const proxy = await startProxy({
50343
50775
  wallet,
50344
50776
  routingConfig,
50777
+ maxCostPerRunUsd,
50778
+ maxCostPerRunMode,
50345
50779
  onReady: (port) => {
50346
50780
  api.logger.info(`BlockRun x402 proxy listening on port ${port}`);
50347
50781
  },
@@ -50366,6 +50800,10 @@ async function startProxyInBackground(api) {
50366
50800
  });
50367
50801
  setActiveProxy(proxy);
50368
50802
  activeProxyHandle = proxy;
50803
+ const startupExclusions = loadExcludeList();
50804
+ if (startupExclusions.size > 0) {
50805
+ api.logger.info(`Model exclusions active (${startupExclusions.size}): ${[...startupExclusions].join(", ")}`);
50806
+ }
50369
50807
  api.logger.info(`ClawRouter ready \u2014 smart routing enabled`);
50370
50808
  api.logger.info(`Pricing: Simple ~$0.001 | Code ~$0.01 | Complex ~$0.05 | Free: $0`);
50371
50809
  const currentChain = await resolvePaymentChain();
@@ -50425,6 +50863,78 @@ async function createStatsCommand() {
50425
50863
  }
50426
50864
  };
50427
50865
  }
50866
+ async function createExcludeCommand() {
50867
+ return {
50868
+ name: "exclude",
50869
+ description: "Manage excluded models \u2014 /exclude add|remove|clear <model>",
50870
+ acceptsArgs: true,
50871
+ requireAuth: true,
50872
+ handler: async (ctx) => {
50873
+ const args = ctx.args?.trim() || "";
50874
+ const parts = args.split(/\s+/);
50875
+ const subcommand = parts[0]?.toLowerCase() || "";
50876
+ const modelArg = parts.slice(1).join(" ").trim();
50877
+ if (!subcommand) {
50878
+ const list = loadExcludeList();
50879
+ if (list.size === 0) {
50880
+ return {
50881
+ text: "No models excluded.\n\nUsage:\n /exclude add <model> \u2014 block a model\n /exclude remove <model> \u2014 unblock\n /exclude clear \u2014 remove all"
50882
+ };
50883
+ }
50884
+ const models = [...list].sort().map((m) => ` \u2022 ${m}`).join("\n");
50885
+ return {
50886
+ text: `Excluded models (${list.size}):
50887
+ ${models}
50888
+
50889
+ Use /exclude remove <model> to unblock.`
50890
+ };
50891
+ }
50892
+ if (subcommand === "add") {
50893
+ if (!modelArg) {
50894
+ return { text: "Usage: /exclude add <model>\nExample: /exclude add nvidia/gpt-oss-120b", isError: true };
50895
+ }
50896
+ const resolved = addExclusion(modelArg);
50897
+ const list = loadExcludeList();
50898
+ return {
50899
+ text: `Excluded: ${resolved}
50900
+
50901
+ Active exclusions (${list.size}):
50902
+ ${[...list].sort().map((m) => ` \u2022 ${m}`).join("\n")}`
50903
+ };
50904
+ }
50905
+ if (subcommand === "remove") {
50906
+ if (!modelArg) {
50907
+ return { text: "Usage: /exclude remove <model>", isError: true };
50908
+ }
50909
+ const removed = removeExclusion(modelArg);
50910
+ if (!removed) {
50911
+ return { text: `Model "${modelArg}" was not in the exclude list.` };
50912
+ }
50913
+ const list = loadExcludeList();
50914
+ return {
50915
+ text: `Unblocked: ${modelArg}
50916
+
50917
+ Active exclusions (${list.size}):
50918
+ ${list.size > 0 ? [...list].sort().map((m) => ` \u2022 ${m}`).join("\n") : " (none)"}`
50919
+ };
50920
+ }
50921
+ if (subcommand === "clear") {
50922
+ clearExclusions();
50923
+ return { text: "All model exclusions cleared." };
50924
+ }
50925
+ return {
50926
+ text: `Unknown subcommand: ${subcommand}
50927
+
50928
+ Usage:
50929
+ /exclude \u2014 show list
50930
+ /exclude add <model>
50931
+ /exclude remove <model>
50932
+ /exclude clear`,
50933
+ isError: true
50934
+ };
50935
+ }
50936
+ };
50937
+ }
50428
50938
  async function createWalletCommand() {
50429
50939
  return {
50430
50940
  name: "wallet",
@@ -50737,6 +51247,13 @@ var plugin = {
50737
51247
  `Failed to register /stats command: ${err instanceof Error ? err.message : String(err)}`
50738
51248
  );
50739
51249
  });
51250
+ createExcludeCommand().then((excludeCommand) => {
51251
+ api.registerCommand(excludeCommand);
51252
+ }).catch((err) => {
51253
+ api.logger.warn(
51254
+ `Failed to register /exclude command: ${err instanceof Error ? err.message : String(err)}`
51255
+ );
51256
+ });
50740
51257
  api.registerService({
50741
51258
  id: "clawrouter-proxy",
50742
51259
  start: () => {