@blockrun/clawrouter 0.12.229 → 0.12.231

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
@@ -434,6 +434,7 @@ No Surf account, no API key — settles directly to Surf's Base treasury in USDC
434
434
  | google/gemini-2.5-pro | $1.25 | $10.00 | $0.0056 | 1M | reasoning, vision, tools |
435
435
  | zai/glm-5.2 | $1.40 | $4.40 | $0.0029 | 1M | reasoning, coding, tools (flagship) |
436
436
  | zai/glm-5.1 | $1.40 | $4.40 | $0.0029 | 200K | reasoning, tools (promo ended 2026-06-05) |
437
+ | qwen/qwen3.7-max | $1.475 | $4.425 | $0.0030 | 1M | reasoning, agentic, tools (Qwen flagship) |
437
438
  | xai/grok-4.3 | $1.50 | $4.00 | $0.0028 | 1M | reasoning, vision, agentic, tools |
438
439
  | xai/grok-4.5 | $2.50 | $9.00 | $0.0058 | 500K | reasoning, vision, agentic, tools |
439
440
  | xai/grok-build-0.1 | $1.50 | $3.00 | $0.0023 | 256K | agentic coding, tools |
package/dist/cli.js CHANGED
@@ -34296,6 +34296,7 @@ var init_top_models = __esm({
34296
34296
  "minimax/minimax-m3",
34297
34297
  "minimax/minimax-m2.7",
34298
34298
  "moonshot/kimi-k3",
34299
+ "qwen/qwen3.7-max",
34299
34300
  "deepseek/deepseek-v4-pro",
34300
34301
  "deepseek/deepseek-chat",
34301
34302
  "deepseek/deepseek-reasoner",
@@ -34522,6 +34523,17 @@ var init_models = __esm({
34522
34523
  "kimi-k2.6": "moonshot/kimi-k2.6",
34523
34524
  "kimi-k2.5": "moonshot/kimi-k2.5",
34524
34525
  "nvidia/kimi-k2.5": "moonshot/kimi-k2.5",
34526
+ // Qwen — Qwen3.7 Max is Alibaba's current flagship for reasoning, coding,
34527
+ // and agentic tool use. EXPLICIT PINS ONLY: bare `qwen` deliberately stays
34528
+ // unbound. Every other qwen* shorthand below points at a FREE model
34529
+ // (qwen-coder, qwen-thinking, qwen3-next, qwen3.5-122b), so binding the
34530
+ // shortest name to a $1.475/$4.425 flagship would silently charge callers
34531
+ // who typed it expecting the free tier — same rule that keeps generic
34532
+ // `kimi` on K2.7. (`grok` WAS promoted to 4.5, but only after the cost
34533
+ // tradeoff was argued explicitly; there's no such case for qwen yet.)
34534
+ "qwen3.7-max": "qwen/qwen3.7-max",
34535
+ "qwen-3.7-max": "qwen/qwen3.7-max",
34536
+ "qwen3-7-max": "qwen/qwen3.7-max",
34525
34537
  // Google
34526
34538
  // gemini-3-pro-preview delisted by Google 2026-06-06 — mirror the gateway
34527
34539
  // redirect to its successor so pinned callers land on 3.1-pro, not an error.
@@ -35331,6 +35343,27 @@ var init_models = __esm({
35331
35343
  agentic: true,
35332
35344
  toolCalling: true
35333
35345
  },
35346
+ // Qwen3.7 Max — BlockRun's paid Qwen flagship, served through its
35347
+ // OpenRouter credit pool. The gateway applies its standard 5% margin at
35348
+ // settlement, so keep the catalog rates at the upstream $1.475/$4.425 COGS.
35349
+ // NOTE: `reasoning: true` also enrolls this id in REASONING_MODEL_IDS
35350
+ // (proxy.ts), which raises its per-model timeout from 60s to 180s.
35351
+ // `toolCalling: true` is LIVE-VERIFIED (2026-07-20): a real request through
35352
+ // the gateway returned a structured tool_calls array (name + valid JSON
35353
+ // arguments, finish_reason "tool_calls"), not the textual leak that Kimi K3
35354
+ // (#213), Gemini (#189) and GPT (#193) produce. Don't downgrade on a hunch.
35355
+ {
35356
+ id: "qwen/qwen3.7-max",
35357
+ name: "Qwen3.7 Max",
35358
+ version: "3.7-max",
35359
+ inputPrice: 1.475,
35360
+ outputPrice: 4.425,
35361
+ contextWindow: 1e6,
35362
+ maxOutput: 65536,
35363
+ reasoning: true,
35364
+ agentic: true,
35365
+ toolCalling: true
35366
+ },
35334
35367
  // Kimi K2.6 — superseded by K2.7 (2026-06-13), hidden on BlockRun but still routable.
35335
35368
  {
35336
35369
  id: "moonshot/kimi-k2.6",
@@ -82181,6 +82214,65 @@ function extractGptCalls(content) {
82181
82214
  }
82182
82215
  return { calls: [], matches: [] };
82183
82216
  }
82217
+ function deriveRequestTools(tools) {
82218
+ if (!Array.isArray(tools)) return [];
82219
+ const derived = [];
82220
+ for (const raw of tools) {
82221
+ const fn = raw?.function;
82222
+ const name = fn?.name;
82223
+ if (typeof name !== "string" || name.trim() === "") continue;
82224
+ const params = fn?.parameters;
82225
+ const props = isPlainObject(params?.properties) ? Object.keys(params.properties) : [];
82226
+ const required = Array.isArray(params?.required) ? params.required.filter((r2) => typeof r2 === "string") : [];
82227
+ derived.push({ name: name.trim(), propKeys: new Set(props), required });
82228
+ }
82229
+ return derived;
82230
+ }
82231
+ function keyMatchesParam(key2, propKeys) {
82232
+ if (propKeys.has(key2)) return true;
82233
+ if (key2 === "cmd" && propKeys.has("command")) return true;
82234
+ if (key2 === "command" && propKeys.has("cmd")) return true;
82235
+ return false;
82236
+ }
82237
+ function requiredSatisfied(tool, blobKeys) {
82238
+ return tool.required.every(
82239
+ (r2) => blobKeys.has(r2) || r2 === "command" && blobKeys.has("cmd") || r2 === "cmd" && blobKeys.has("command")
82240
+ );
82241
+ }
82242
+ function resolveKimiTool(prose, blob, tools) {
82243
+ const blobKeys = Object.keys(blob);
82244
+ if (prose) {
82245
+ const named = tools.filter(
82246
+ (t) => new RegExp(`(?:^|[^A-Za-z0-9_])${escapeRegExp2(t.name)}(?:[^A-Za-z0-9_]|$)`).test(prose)
82247
+ );
82248
+ if (named.length === 1) return named[0].name;
82249
+ }
82250
+ if (blobKeys.length === 0) return null;
82251
+ const keySet = new Set(blobKeys);
82252
+ const sigMatches = tools.filter(
82253
+ (t) => blobKeys.every((k) => keyMatchesParam(k, t.propKeys)) && requiredSatisfied(t, keySet)
82254
+ );
82255
+ return sigMatches.length === 1 ? sigMatches[0].name : null;
82256
+ }
82257
+ function escapeRegExp2(s3) {
82258
+ return s3.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
82259
+ }
82260
+ function extractKimiCalls(content, tools) {
82261
+ if (tools.length === 0) return { calls: [], matches: [] };
82262
+ const trailing = findTrailingJsonObject(content);
82263
+ if (!trailing || !isPlainObject(trailing.parsed)) return { calls: [], matches: [] };
82264
+ if (typeof trailing.parsed.name === "string" && trailing.parsed.name.trim() !== "") {
82265
+ return { calls: [], matches: [] };
82266
+ }
82267
+ const prose = content.slice(0, trailing.start).trim();
82268
+ const name = resolveKimiTool(prose, trailing.parsed, tools);
82269
+ if (!name) return { calls: [], matches: [] };
82270
+ const start = prose === "" ? 0 : trailing.start;
82271
+ return {
82272
+ calls: [makeCall(name, normalizeGptArgs(trailing.parsed))],
82273
+ matches: [{ start, end: content.length }]
82274
+ };
82275
+ }
82184
82276
  function stripRanges(content, ranges) {
82185
82277
  if (ranges.length === 0) return content;
82186
82278
  const sorted = [...ranges].sort((a, b) => a.start - b.start);
@@ -82195,15 +82287,23 @@ function stripRanges(content, ranges) {
82195
82287
  cleaned += content.slice(cursor);
82196
82288
  return cleaned;
82197
82289
  }
82198
- function extractTextualToolCalls(content) {
82290
+ function extractTextualToolCalls(content, options) {
82199
82291
  if (!content) {
82200
82292
  return { toolCalls: [], cleanedContent: "" };
82201
82293
  }
82202
82294
  const openClaw = extractOpenClawCalls(content);
82203
82295
  const anthropic = extractAnthropicCalls(content);
82204
82296
  const gemini = extractGeminiCalls(content);
82205
- const gpt = openClaw.calls.length + anthropic.calls.length + gemini.calls.length === 0 ? extractGptCalls(content) : { calls: [], matches: [] };
82206
- const toolCalls = [...openClaw.calls, ...anthropic.calls, ...gemini.calls, ...gpt.calls];
82297
+ const tagBasedCount = openClaw.calls.length + anthropic.calls.length + gemini.calls.length;
82298
+ const gpt = tagBasedCount === 0 ? extractGptCalls(content) : { calls: [], matches: [] };
82299
+ const kimi = tagBasedCount + gpt.calls.length === 0 ? extractKimiCalls(content, deriveRequestTools(options?.tools)) : { calls: [], matches: [] };
82300
+ const toolCalls = [
82301
+ ...openClaw.calls,
82302
+ ...anthropic.calls,
82303
+ ...gemini.calls,
82304
+ ...gpt.calls,
82305
+ ...kimi.calls
82306
+ ];
82207
82307
  if (toolCalls.length === 0) {
82208
82308
  return { toolCalls: [], cleanedContent: content };
82209
82309
  }
@@ -82211,7 +82311,8 @@ function extractTextualToolCalls(content) {
82211
82311
  ...openClaw.matches,
82212
82312
  ...anthropic.matches,
82213
82313
  ...gemini.matches,
82214
- ...gpt.matches
82314
+ ...gpt.matches,
82315
+ ...kimi.matches
82215
82316
  ]);
82216
82317
  return { toolCalls, cleanedContent };
82217
82318
  }
@@ -85986,6 +86087,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
85986
86087
  const debugMode = debugHeadersEnabledFromEnv() && req.headers["x-clawrouter-debug"] !== "false";
85987
86088
  let routingDecision;
85988
86089
  let hasTools = false;
86090
+ let requestTools;
85989
86091
  let hasVision = false;
85990
86092
  let isStreaming = false;
85991
86093
  let modelId = "";
@@ -86023,6 +86125,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
86023
86125
  requestSummaryForStore = summarizeRequest(promptText);
86024
86126
  }
86025
86127
  hasTools = Array.isArray(parsed.tools) && parsed.tools.length > 0;
86128
+ requestTools = parsed.tools;
86026
86129
  const rawLastContent = lastUserMsg?.content;
86027
86130
  const lastContent = typeof rawLastContent === "string" ? rawLastContent : Array.isArray(rawLastContent) ? rawLastContent.filter((b) => b.type === "text").map((b) => b.text ?? "").join(" ") : "";
86028
86131
  if (sessionId && parsedMessages.length > 0) {
@@ -87566,7 +87669,7 @@ data: [DONE]
87566
87669
  let toolCalls = choice.message?.tool_calls ?? choice.delta?.tool_calls;
87567
87670
  const rawContent = choice.message?.content ?? choice.delta?.content ?? "";
87568
87671
  if (!endsWithToolCalls && (!toolCalls || toolCalls.length === 0) && rawContent) {
87569
- const extracted = extractTextualToolCalls(rawContent);
87672
+ const extracted = extractTextualToolCalls(rawContent, { tools: requestTools });
87570
87673
  if (extracted.toolCalls.length > 0) {
87571
87674
  toolCalls = extracted.toolCalls;
87572
87675
  }
@@ -87779,7 +87882,7 @@ data: [DONE]
87779
87882
  }
87780
87883
  continue;
87781
87884
  }
87782
- const extracted = extractTextualToolCalls(message.content);
87885
+ const extracted = extractTextualToolCalls(message.content, { tools: requestTools });
87783
87886
  if (extracted.toolCalls.length > 0) {
87784
87887
  message.tool_calls = extracted.toolCalls;
87785
87888
  message.content = "";
package/dist/index.js CHANGED
@@ -86,6 +86,7 @@ var init_top_models = __esm({
86
86
  "minimax/minimax-m3",
87
87
  "minimax/minimax-m2.7",
88
88
  "moonshot/kimi-k3",
89
+ "qwen/qwen3.7-max",
89
90
  "deepseek/deepseek-v4-pro",
90
91
  "deepseek/deepseek-chat",
91
92
  "deepseek/deepseek-reasoner",
@@ -312,6 +313,17 @@ var init_models = __esm({
312
313
  "kimi-k2.6": "moonshot/kimi-k2.6",
313
314
  "kimi-k2.5": "moonshot/kimi-k2.5",
314
315
  "nvidia/kimi-k2.5": "moonshot/kimi-k2.5",
316
+ // Qwen — Qwen3.7 Max is Alibaba's current flagship for reasoning, coding,
317
+ // and agentic tool use. EXPLICIT PINS ONLY: bare `qwen` deliberately stays
318
+ // unbound. Every other qwen* shorthand below points at a FREE model
319
+ // (qwen-coder, qwen-thinking, qwen3-next, qwen3.5-122b), so binding the
320
+ // shortest name to a $1.475/$4.425 flagship would silently charge callers
321
+ // who typed it expecting the free tier — same rule that keeps generic
322
+ // `kimi` on K2.7. (`grok` WAS promoted to 4.5, but only after the cost
323
+ // tradeoff was argued explicitly; there's no such case for qwen yet.)
324
+ "qwen3.7-max": "qwen/qwen3.7-max",
325
+ "qwen-3.7-max": "qwen/qwen3.7-max",
326
+ "qwen3-7-max": "qwen/qwen3.7-max",
315
327
  // Google
316
328
  // gemini-3-pro-preview delisted by Google 2026-06-06 — mirror the gateway
317
329
  // redirect to its successor so pinned callers land on 3.1-pro, not an error.
@@ -1121,6 +1133,27 @@ var init_models = __esm({
1121
1133
  agentic: true,
1122
1134
  toolCalling: true
1123
1135
  },
1136
+ // Qwen3.7 Max — BlockRun's paid Qwen flagship, served through its
1137
+ // OpenRouter credit pool. The gateway applies its standard 5% margin at
1138
+ // settlement, so keep the catalog rates at the upstream $1.475/$4.425 COGS.
1139
+ // NOTE: `reasoning: true` also enrolls this id in REASONING_MODEL_IDS
1140
+ // (proxy.ts), which raises its per-model timeout from 60s to 180s.
1141
+ // `toolCalling: true` is LIVE-VERIFIED (2026-07-20): a real request through
1142
+ // the gateway returned a structured tool_calls array (name + valid JSON
1143
+ // arguments, finish_reason "tool_calls"), not the textual leak that Kimi K3
1144
+ // (#213), Gemini (#189) and GPT (#193) produce. Don't downgrade on a hunch.
1145
+ {
1146
+ id: "qwen/qwen3.7-max",
1147
+ name: "Qwen3.7 Max",
1148
+ version: "3.7-max",
1149
+ inputPrice: 1.475,
1150
+ outputPrice: 4.425,
1151
+ contextWindow: 1e6,
1152
+ maxOutput: 65536,
1153
+ reasoning: true,
1154
+ agentic: true,
1155
+ toolCalling: true
1156
+ },
1124
1157
  // Kimi K2.6 — superseded by K2.7 (2026-06-13), hidden on BlockRun but still routable.
1125
1158
  {
1126
1159
  id: "moonshot/kimi-k2.6",
@@ -82001,6 +82034,65 @@ function extractGptCalls(content) {
82001
82034
  }
82002
82035
  return { calls: [], matches: [] };
82003
82036
  }
82037
+ function deriveRequestTools(tools) {
82038
+ if (!Array.isArray(tools)) return [];
82039
+ const derived = [];
82040
+ for (const raw of tools) {
82041
+ const fn = raw?.function;
82042
+ const name = fn?.name;
82043
+ if (typeof name !== "string" || name.trim() === "") continue;
82044
+ const params = fn?.parameters;
82045
+ const props = isPlainObject(params?.properties) ? Object.keys(params.properties) : [];
82046
+ const required = Array.isArray(params?.required) ? params.required.filter((r2) => typeof r2 === "string") : [];
82047
+ derived.push({ name: name.trim(), propKeys: new Set(props), required });
82048
+ }
82049
+ return derived;
82050
+ }
82051
+ function keyMatchesParam(key2, propKeys) {
82052
+ if (propKeys.has(key2)) return true;
82053
+ if (key2 === "cmd" && propKeys.has("command")) return true;
82054
+ if (key2 === "command" && propKeys.has("cmd")) return true;
82055
+ return false;
82056
+ }
82057
+ function requiredSatisfied(tool, blobKeys) {
82058
+ return tool.required.every(
82059
+ (r2) => blobKeys.has(r2) || r2 === "command" && blobKeys.has("cmd") || r2 === "cmd" && blobKeys.has("command")
82060
+ );
82061
+ }
82062
+ function resolveKimiTool(prose, blob, tools) {
82063
+ const blobKeys = Object.keys(blob);
82064
+ if (prose) {
82065
+ const named = tools.filter(
82066
+ (t) => new RegExp(`(?:^|[^A-Za-z0-9_])${escapeRegExp2(t.name)}(?:[^A-Za-z0-9_]|$)`).test(prose)
82067
+ );
82068
+ if (named.length === 1) return named[0].name;
82069
+ }
82070
+ if (blobKeys.length === 0) return null;
82071
+ const keySet = new Set(blobKeys);
82072
+ const sigMatches = tools.filter(
82073
+ (t) => blobKeys.every((k) => keyMatchesParam(k, t.propKeys)) && requiredSatisfied(t, keySet)
82074
+ );
82075
+ return sigMatches.length === 1 ? sigMatches[0].name : null;
82076
+ }
82077
+ function escapeRegExp2(s3) {
82078
+ return s3.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
82079
+ }
82080
+ function extractKimiCalls(content, tools) {
82081
+ if (tools.length === 0) return { calls: [], matches: [] };
82082
+ const trailing = findTrailingJsonObject(content);
82083
+ if (!trailing || !isPlainObject(trailing.parsed)) return { calls: [], matches: [] };
82084
+ if (typeof trailing.parsed.name === "string" && trailing.parsed.name.trim() !== "") {
82085
+ return { calls: [], matches: [] };
82086
+ }
82087
+ const prose = content.slice(0, trailing.start).trim();
82088
+ const name = resolveKimiTool(prose, trailing.parsed, tools);
82089
+ if (!name) return { calls: [], matches: [] };
82090
+ const start = prose === "" ? 0 : trailing.start;
82091
+ return {
82092
+ calls: [makeCall(name, normalizeGptArgs(trailing.parsed))],
82093
+ matches: [{ start, end: content.length }]
82094
+ };
82095
+ }
82004
82096
  function stripRanges(content, ranges) {
82005
82097
  if (ranges.length === 0) return content;
82006
82098
  const sorted = [...ranges].sort((a, b) => a.start - b.start);
@@ -82015,15 +82107,23 @@ function stripRanges(content, ranges) {
82015
82107
  cleaned += content.slice(cursor);
82016
82108
  return cleaned;
82017
82109
  }
82018
- function extractTextualToolCalls(content) {
82110
+ function extractTextualToolCalls(content, options) {
82019
82111
  if (!content) {
82020
82112
  return { toolCalls: [], cleanedContent: "" };
82021
82113
  }
82022
82114
  const openClaw = extractOpenClawCalls(content);
82023
82115
  const anthropic = extractAnthropicCalls(content);
82024
82116
  const gemini = extractGeminiCalls(content);
82025
- const gpt = openClaw.calls.length + anthropic.calls.length + gemini.calls.length === 0 ? extractGptCalls(content) : { calls: [], matches: [] };
82026
- const toolCalls = [...openClaw.calls, ...anthropic.calls, ...gemini.calls, ...gpt.calls];
82117
+ const tagBasedCount = openClaw.calls.length + anthropic.calls.length + gemini.calls.length;
82118
+ const gpt = tagBasedCount === 0 ? extractGptCalls(content) : { calls: [], matches: [] };
82119
+ const kimi = tagBasedCount + gpt.calls.length === 0 ? extractKimiCalls(content, deriveRequestTools(options?.tools)) : { calls: [], matches: [] };
82120
+ const toolCalls = [
82121
+ ...openClaw.calls,
82122
+ ...anthropic.calls,
82123
+ ...gemini.calls,
82124
+ ...gpt.calls,
82125
+ ...kimi.calls
82126
+ ];
82027
82127
  if (toolCalls.length === 0) {
82028
82128
  return { toolCalls: [], cleanedContent: content };
82029
82129
  }
@@ -82031,7 +82131,8 @@ function extractTextualToolCalls(content) {
82031
82131
  ...openClaw.matches,
82032
82132
  ...anthropic.matches,
82033
82133
  ...gemini.matches,
82034
- ...gpt.matches
82134
+ ...gpt.matches,
82135
+ ...kimi.matches
82035
82136
  ]);
82036
82137
  return { toolCalls, cleanedContent };
82037
82138
  }
@@ -85806,6 +85907,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
85806
85907
  const debugMode = debugHeadersEnabledFromEnv() && req.headers["x-clawrouter-debug"] !== "false";
85807
85908
  let routingDecision;
85808
85909
  let hasTools = false;
85910
+ let requestTools;
85809
85911
  let hasVision = false;
85810
85912
  let isStreaming = false;
85811
85913
  let modelId = "";
@@ -85843,6 +85945,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
85843
85945
  requestSummaryForStore = summarizeRequest(promptText);
85844
85946
  }
85845
85947
  hasTools = Array.isArray(parsed.tools) && parsed.tools.length > 0;
85948
+ requestTools = parsed.tools;
85846
85949
  const rawLastContent = lastUserMsg?.content;
85847
85950
  const lastContent = typeof rawLastContent === "string" ? rawLastContent : Array.isArray(rawLastContent) ? rawLastContent.filter((b) => b.type === "text").map((b) => b.text ?? "").join(" ") : "";
85848
85951
  if (sessionId && parsedMessages.length > 0) {
@@ -87386,7 +87489,7 @@ data: [DONE]
87386
87489
  let toolCalls = choice.message?.tool_calls ?? choice.delta?.tool_calls;
87387
87490
  const rawContent = choice.message?.content ?? choice.delta?.content ?? "";
87388
87491
  if (!endsWithToolCalls && (!toolCalls || toolCalls.length === 0) && rawContent) {
87389
- const extracted = extractTextualToolCalls(rawContent);
87492
+ const extracted = extractTextualToolCalls(rawContent, { tools: requestTools });
87390
87493
  if (extracted.toolCalls.length > 0) {
87391
87494
  toolCalls = extracted.toolCalls;
87392
87495
  }
@@ -87599,7 +87702,7 @@ data: [DONE]
87599
87702
  }
87600
87703
  continue;
87601
87704
  }
87602
- const extracted = extractTextualToolCalls(message.content);
87705
+ const extracted = extractTextualToolCalls(message.content, { tools: requestTools });
87603
87706
  if (extracted.toolCalls.length > 0) {
87604
87707
  message.tool_calls = extracted.toolCalls;
87605
87708
  message.content = "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/clawrouter",
3
- "version": "0.12.229",
3
+ "version": "0.12.231",
4
4
  "description": "Smart LLM router — save 85% on inference costs. 55+ models (8 free), one wallet, x402 micropayments.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/scripts/update.sh CHANGED
@@ -16,6 +16,96 @@ WALLET_FILE="$HOME/.openclaw/blockrun/wallet.key"
16
16
  WALLET_BACKUP=""
17
17
  PLUGIN_BACKUP=""
18
18
  CONFIG_BACKUP=""
19
+ ACTIVE_PACKAGE_DIR=""
20
+
21
+ NPM_PREFIX="$(npm config get prefix 2>/dev/null || echo "")"
22
+ if [ -n "$NPM_PREFIX" ] && [ -d "$NPM_PREFIX/bin" ]; then
23
+ PATH="$NPM_PREFIX/bin:$PATH"
24
+ export PATH
25
+ fi
26
+
27
+ find_clawrouter_package_dir() {
28
+ node <<'NODE'
29
+ const fs = require('fs');
30
+ const os = require('os');
31
+ const path = require('path');
32
+
33
+ const home = os.homedir();
34
+ const candidates = [
35
+ { packagePath: path.join(home, '.openclaw', 'extensions', 'clawrouter', 'package.json'), priority: 0 },
36
+ { packagePath: path.join(home, '.openclaw', 'npm', 'node_modules', '@blockrun', 'clawrouter', 'package.json'), priority: 2 },
37
+ ];
38
+
39
+ const projectsDir = path.join(home, '.openclaw', 'npm', 'projects');
40
+ try {
41
+ for (const name of fs.readdirSync(projectsDir)) {
42
+ candidates.push({
43
+ packagePath: path.join(projectsDir, name, 'node_modules', '@blockrun', 'clawrouter', 'package.json'),
44
+ priority: 1,
45
+ });
46
+ }
47
+ } catch {}
48
+
49
+ function versionParts(version) {
50
+ return String(version || '').split(/[.-]/).slice(0, 3).map((part) => Number.parseInt(part, 10) || 0);
51
+ }
52
+
53
+ function compareVersions(a, b) {
54
+ const left = versionParts(a);
55
+ const right = versionParts(b);
56
+ for (let i = 0; i < 3; i++) {
57
+ if (left[i] !== right[i]) return left[i] - right[i];
58
+ }
59
+ return 0;
60
+ }
61
+
62
+ let best = null;
63
+ for (const candidate of candidates) {
64
+ try {
65
+ const stat = fs.statSync(candidate.packagePath);
66
+ const pkg = JSON.parse(fs.readFileSync(candidate.packagePath, 'utf8'));
67
+ if (pkg?.name !== '@blockrun/clawrouter') continue;
68
+ const versionOrder = best ? compareVersions(pkg.version, best.pkg.version) : 1;
69
+ if (
70
+ !best ||
71
+ versionOrder > 0 ||
72
+ (versionOrder === 0 &&
73
+ (candidate.priority < best.priority ||
74
+ (candidate.priority === best.priority && stat.mtimeMs > best.mtimeMs)))
75
+ ) {
76
+ best = { ...candidate, pkg, mtimeMs: stat.mtimeMs };
77
+ }
78
+ } catch {}
79
+ }
80
+
81
+ if (best) console.log(path.dirname(best.packagePath));
82
+ NODE
83
+ }
84
+
85
+ sync_cli_shim() {
86
+ local package_dir="$1"
87
+ if [ -z "$package_dir" ] || [ ! -f "$package_dir/dist/cli.js" ]; then
88
+ echo " ⚠ Could not update CLI shim: ClawRouter CLI not found"
89
+ return 1
90
+ fi
91
+
92
+ local prefix
93
+ prefix=$(npm config get prefix 2>/dev/null || echo "")
94
+ if [ -z "$prefix" ]; then
95
+ echo " ⚠ Could not update CLI shim: npm prefix unavailable"
96
+ return 1
97
+ fi
98
+
99
+ local bin_dir="$prefix/bin"
100
+ mkdir -p "$bin_dir"
101
+ rm -f "$bin_dir/clawrouter"
102
+ cat >"$bin_dir/clawrouter" <<EOF
103
+ #!/bin/sh
104
+ exec node "$package_dir/dist/cli.js" "\$@"
105
+ EOF
106
+ chmod +x "$bin_dir/clawrouter"
107
+ echo " ✓ CLI shim updated: $bin_dir/clawrouter → $package_dir/dist/cli.js"
108
+ }
19
109
 
20
110
  cleanup_backups() {
21
111
  if [ -n "$PLUGIN_BACKUP" ] && [ -d "$PLUGIN_BACKUP" ]; then
@@ -93,6 +183,7 @@ echo ""
93
183
 
94
184
  # Pre-flight: fail fast if config is corrupt
95
185
  validate_config
186
+ ACTIVE_PACKAGE_DIR=$(find_clawrouter_package_dir)
96
187
 
97
188
  echo "→ Checking wallet..."
98
189
 
@@ -105,7 +196,7 @@ if [ -f "$WALLET_FILE" ]; then
105
196
  # Derive wallet address via node (viem is available post-install)
106
197
  WALLET_ADDRESS=$(node -e "
107
198
  try {
108
- const { privateKeyToAccount } = require('$HOME/.openclaw/extensions/clawrouter/node_modules/viem/accounts/index.js');
199
+ const { privateKeyToAccount } = require(require.resolve('viem/accounts', { paths: ['$ACTIVE_PACKAGE_DIR'] }));
109
200
  const acct = privateKeyToAccount('$WALLET_KEY');
110
201
  console.log(acct.address);
111
202
  } catch {
@@ -551,41 +642,42 @@ force_install_from_npm() {
551
642
  return 1
552
643
  }
553
644
 
554
- if [ "$OPENCLAW_INSTALL_RECOVERABLE" = "1" ]; then
555
- LATEST_VER=$(npm view @blockrun/clawrouter@latest version 2>/dev/null || echo "")
556
- if [ -z "$LATEST_VER" ]; then
557
- echo " ✗ Could not resolve latest ClawRouter version from npm"
558
- exit 1
645
+ LATEST_VER=$(npm view @blockrun/clawrouter@latest version 2>/dev/null || echo "")
646
+ if [ -z "$LATEST_VER" ]; then
647
+ echo " ✗ Could not resolve latest ClawRouter version from npm"
648
+ exit 1
649
+ fi
650
+
651
+ ACTIVE_PACKAGE_DIR=$(find_clawrouter_package_dir)
652
+ INSTALLED_VER=""
653
+ if [ -n "$ACTIVE_PACKAGE_DIR" ] && [ -f "$ACTIVE_PACKAGE_DIR/package.json" ]; then
654
+ INSTALLED_VER=$(node -e "try{const p=require('$ACTIVE_PACKAGE_DIR/package.json');console.log(p.version);}catch{console.log('');}" 2>/dev/null || echo "")
655
+ fi
656
+
657
+ if [ "$OPENCLAW_INSTALL_RECOVERABLE" = "1" ] || [ -z "$INSTALLED_VER" ] || [ "$INSTALLED_VER" != "$LATEST_VER" ]; then
658
+ if [ -n "$INSTALLED_VER" ] && [ "$INSTALLED_VER" != "$LATEST_VER" ]; then
659
+ echo " ⚠️ OpenClaw installed v${INSTALLED_VER} but latest is v${LATEST_VER}"
559
660
  fi
560
661
  force_install_from_npm "$LATEST_VER"
561
- if [ ! -f "$PLUGIN_DIR/package.json" ]; then
562
- echo " ✗ ClawRouter package.json missing after npm fallback"
563
- exit 1
564
- fi
565
- INSTALLED_VER=$(node -e "try{const p=require('$PLUGIN_DIR/package.json');console.log(p.version);}catch{console.log('');}" 2>/dev/null || echo "")
566
- if [ -z "$INSTALLED_VER" ]; then
567
- echo " ✗ Could not verify ClawRouter version after npm fallback"
568
- exit 1
569
- fi
570
- echo " ✓ ClawRouter v${INSTALLED_VER} installed"
571
- trap - EXIT INT TERM
572
- elif [ -d "$PLUGIN_DIR" ] && [ -f "$PLUGIN_DIR/package.json" ]; then
573
- INSTALLED_VER=$(node -e "try{const p=require('$PLUGIN_DIR/package.json');console.log(p.version);}catch{console.log('');}" 2>/dev/null || echo "")
574
- LATEST_VER=$(npm view @blockrun/clawrouter@latest version 2>/dev/null || echo "")
575
- if [ -n "$LATEST_VER" ] && [ -n "$INSTALLED_VER" ] && [ "$INSTALLED_VER" != "$LATEST_VER" ]; then
576
- echo " ⚠️ openclaw installed v${INSTALLED_VER} (cached) but latest is v${LATEST_VER}"
577
- force_install_from_npm "$LATEST_VER" || true
578
- fi
579
- INSTALLED_VER=$(node -e "try{const p=require('$PLUGIN_DIR/package.json');console.log(p.version);}catch{console.log('?');}" 2>/dev/null || echo "?")
580
- echo " ✓ ClawRouter v${INSTALLED_VER} installed"
662
+ ACTIVE_PACKAGE_DIR=$(find_clawrouter_package_dir)
663
+ INSTALLED_VER=$(node -e "try{const p=require('$ACTIVE_PACKAGE_DIR/package.json');console.log(p.version);}catch{console.log('');}" 2>/dev/null || echo "")
581
664
  fi
582
665
 
666
+ if [ -z "$ACTIVE_PACKAGE_DIR" ] || [ -z "$INSTALLED_VER" ]; then
667
+ echo " ✗ Could not verify ClawRouter install"
668
+ exit 1
669
+ fi
670
+
671
+ echo " ✓ ClawRouter v${INSTALLED_VER} installed at $ACTIVE_PACKAGE_DIR"
672
+ sync_cli_shim "$ACTIVE_PACKAGE_DIR"
673
+ trap - EXIT INT TERM
674
+
583
675
  # ── Step 4c: Ensure all dependencies are installed ────────────
584
676
  # openclaw's plugin installer may skip native/optional deps like @solana/kit.
585
677
  # Run npm install in the plugin directory to fill any gaps.
586
- if [ -d "$PLUGIN_DIR" ] && [ -f "$PLUGIN_DIR/package.json" ]; then
678
+ if [ -n "$ACTIVE_PACKAGE_DIR" ] && [ -f "$ACTIVE_PACKAGE_DIR/package.json" ]; then
587
679
  echo "→ Installing dependencies (Solana, x402, etc.)..."
588
- run_dependency_install "$PLUGIN_DIR"
680
+ run_dependency_install "$ACTIVE_PACKAGE_DIR"
589
681
  fi
590
682
 
591
683
  # ── Step 5: Verify wallet survived ─────────────────────────────
@@ -724,7 +816,7 @@ const os = require('os');
724
816
  const fs = require('fs');
725
817
  const path = require('path');
726
818
  const configPath = path.join(os.homedir(), '.openclaw', 'openclaw.json');
727
- const topModelsPath = '$SCRIPT_DIR/../src/top-models.json';
819
+ const topModelsPath = path.join('$ACTIVE_PACKAGE_DIR', 'src', 'top-models.json');
728
820
 
729
821
  if (!fs.existsSync(configPath)) {
730
822
  console.log(' No config file found, skipping');
@@ -1025,7 +1117,7 @@ if [ -f "$WALLET_FILE" ]; then
1025
1117
  FINAL_KEY=$(cat "$WALLET_FILE" | tr -d '[:space:]')
1026
1118
  FINAL_ADDRESS=$(node -e "
1027
1119
  try {
1028
- const { privateKeyToAccount } = require('$HOME/.openclaw/extensions/clawrouter/node_modules/viem/accounts/index.js');
1120
+ const { privateKeyToAccount } = require(require.resolve('viem/accounts', { paths: ['$ACTIVE_PACKAGE_DIR'] }));
1029
1121
  console.log(privateKeyToAccount('$FINAL_KEY').address);
1030
1122
  } catch { console.log('(run /wallet in OpenClaw to see your address)'); }
1031
1123
  " 2>/dev/null || echo "(run /wallet in OpenClaw to see your address)")
@@ -135,7 +135,7 @@ Rules handle ~~80% of requests in <1ms. Only ambiguous queries hit the LLM class
135
135
 
136
136
  ## Available Models
137
137
 
138
- 55+ models including: gpt-5.6-terra [balanced, stable default], gpt-5.6-sol [flagship], gpt-5.6-luna [cost-efficient], gpt-5.5, gpt-5.4, gpt-4o, o3, claude-fable-5, claude-opus-4.8, claude-opus-4.7, claude-opus-4.5, claude-sonnet-5, claude-sonnet-4.6, gemini-3.1-pro, gemini-2.5-flash, deepseek-v4-pro, deepseek-chat, grok-4.5, grok-4.3, grok-build-0.1, kimi-k3 [1M ctx flagship], kimi-k2.7, and 8 free NVIDIA models (mistral-large-3-675b [default, 675B flagship], deepseek-v4-flash [1M context], seed-oss-36b [coding], nemotron-3-nano-omni-30b-a3b-reasoning [vision], mistral-nemotron, step-3.7-flash, nemotron-nano-9b-v2, nemotron-nano-12b-v2-vl [vision]).
138
+ 55+ models including: gpt-5.6-terra [balanced, stable default], gpt-5.6-sol [flagship], gpt-5.6-luna [cost-efficient], gpt-5.5, gpt-5.4, gpt-4o, o3, claude-fable-5, claude-opus-4.8, claude-opus-4.7, claude-opus-4.5, claude-sonnet-5, claude-sonnet-4.6, gemini-3.1-pro, gemini-2.5-flash, deepseek-v4-pro, deepseek-chat, grok-4.5, grok-4.3, grok-build-0.1, kimi-k3 [1M ctx flagship], kimi-k2.7, qwen3.7-max [Qwen flagship, 1M ctx], and 8 free NVIDIA models (mistral-large-3-675b [default, 675B flagship], deepseek-v4-flash [1M context], seed-oss-36b [coding], nemotron-3-nano-omni-30b-a3b-reasoning [vision], mistral-nemotron, step-3.7-flash, nemotron-nano-9b-v2, nemotron-nano-12b-v2-vl [vision]).
139
139
 
140
140
  ## Built-in Agent Tools
141
141
 
@@ -32,6 +32,7 @@
32
32
  "minimax/minimax-m3",
33
33
  "minimax/minimax-m2.7",
34
34
  "moonshot/kimi-k3",
35
+ "qwen/qwen3.7-max",
35
36
  "deepseek/deepseek-v4-pro",
36
37
  "deepseek/deepseek-chat",
37
38
  "deepseek/deepseek-reasoner",