@blockrun/llm 3.24.0 → 3.24.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.cjs +241 -52
  2. package/dist/index.js +241 -52
  3. package/package.json +2 -2
package/dist/index.cjs CHANGED
@@ -571,7 +571,69 @@ function withCost(body, response, bookedUsd, scheme) {
571
571
  // src/client.ts
572
572
  var import_accounts2 = require("viem/accounts");
573
573
 
574
- // node_modules/.pnpm/@blockrun+router-core@https+++codeload.github.com+BlockRunAI+router-core+tar.gz+814fd3b6bc788_xq5fkcckbb2p7dtp6dodey4yri/node_modules/@blockrun/router-core/dist/index.js
574
+ // node_modules/.pnpm/@blockrun+router-core@https+++codeload.github.com+BlockRunAI+router-core+tar.gz+65a92c85ea1f1_ulf6q3mq2pzpwvcnu73z4krxvy/node_modules/@blockrun/router-core/dist/index.js
575
+ var UNSEGMENTED_SCRIPT = /[؀-ۿ -ヿ㐀-䶿一-鿿가-힯豈-﫿＀-￯]/;
576
+ var WORD_CHAR = /[\p{L}\p{N}]/u;
577
+ var ASCII_PHRASE = /^[a-z0-9' -]+$/;
578
+ var SEGMENTED_WORD_CHAR = "\\p{Script=Latin}\\p{Script=Cyrillic}\\p{Script=Greek}\\p{N}";
579
+ var BOUNDARY_BEFORE = `(?<![${SEGMENTED_WORD_CHAR}])`;
580
+ var BOUNDARY_AFTER = `(?![${SEGMENTED_WORD_CHAR}])`;
581
+ var BOUNDED_LAST_WORD_LENGTH = 6;
582
+ var SHORT_SUFFIXES = "(?:s|ed|ing)?";
583
+ var SIBILANT_PLURAL = "es";
584
+ var MID_SUFFIXES = "(?:s|es|ed|d|ing|er|ers|ly|ment|ments|ation|ations|e|en|n|a|as|o|os|em|et|ung|ungen)?";
585
+ var DROPPED_E_SUFFIXES = "(?:ing|ed|es|er|ers|ation|ations)";
586
+ var DOUBLED_CONSONANT_SUFFIXES = "(?:ing|ed|er|ers)";
587
+ var matcherCache = /* @__PURE__ */ new Map();
588
+ function escapeRegExp(value) {
589
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
590
+ }
591
+ function keywordMatcher(keyword) {
592
+ const cached = matcherCache.get(keyword);
593
+ if (cached) return cached;
594
+ const kw = keyword.toLowerCase();
595
+ let matcher;
596
+ if (UNSEGMENTED_SCRIPT.test(kw) || kw.length === 0) {
597
+ matcher = { needle: kw, pattern: null };
598
+ } else {
599
+ const lead = WORD_CHAR.test(kw[0]) ? BOUNDARY_BEFORE : "";
600
+ const escaped = escapeRegExp(kw);
601
+ let body = escaped;
602
+ let trail = "";
603
+ let needle = kw;
604
+ const lastWord = kw.slice(kw.lastIndexOf(" ") + 1);
605
+ if (WORD_CHAR.test(kw[kw.length - 1]) && ASCII_PHRASE.test(kw) && lastWord.length <= BOUNDED_LAST_WORD_LENGTH) {
606
+ const forms = [];
607
+ if (/\d$/.test(kw)) {
608
+ forms.push(escaped);
609
+ } else if (lastWord.length <= 3) {
610
+ forms.push(`${escaped}${SHORT_SUFFIXES}`);
611
+ if (/(?:x|s|z|ch|sh)$/.test(kw)) forms.push(`${escaped}${SIBILANT_PLURAL}`);
612
+ } else {
613
+ forms.push(`${escaped}${MID_SUFFIXES}`);
614
+ if (kw.endsWith("e")) {
615
+ forms.push(`${escaped.slice(0, -1)}${DROPPED_E_SUFFIXES}`);
616
+ needle = kw.slice(0, -1);
617
+ } else if (kw.endsWith("y")) {
618
+ forms.push(`${escaped.slice(0, -1)}(?:ies|ied)`);
619
+ needle = kw.slice(0, -1);
620
+ } else if (/[aeiou][bdgmnprt]$/.test(kw)) {
621
+ forms.push(`${escaped}${kw[kw.length - 1]}${DOUBLED_CONSONANT_SUFFIXES}`);
622
+ }
623
+ }
624
+ body = `(?:${forms.join("|")})`;
625
+ trail = BOUNDARY_AFTER;
626
+ }
627
+ matcher = { needle, pattern: new RegExp(`${lead}${body}${trail}`, "u") };
628
+ }
629
+ matcherCache.set(keyword, matcher);
630
+ return matcher;
631
+ }
632
+ function matchesKeyword(text, keyword) {
633
+ const { needle, pattern } = keywordMatcher(keyword);
634
+ if (!text.includes(needle)) return false;
635
+ return pattern === null ? true : pattern.test(text);
636
+ }
575
637
  function scoreTokenCount(estimatedTokens, thresholds) {
576
638
  if (estimatedTokens < thresholds.simple) {
577
639
  return { name: "tokenCount", score: -1, signal: `short (${estimatedTokens} tokens)` };
@@ -582,7 +644,7 @@ function scoreTokenCount(estimatedTokens, thresholds) {
582
644
  return { name: "tokenCount", score: 0, signal: null };
583
645
  }
584
646
  function scoreKeywordMatch(text, keywords, name, signalLabel, thresholds, scores) {
585
- const matches = keywords.filter((kw) => text.includes(kw.toLowerCase()));
647
+ const matches = keywords.filter((kw) => matchesKeyword(text, kw));
586
648
  if (matches.length >= thresholds.high) {
587
649
  return {
588
650
  name,
@@ -618,7 +680,7 @@ function scoreAgenticTask(text, keywords) {
618
680
  let matchCount = 0;
619
681
  const signals = [];
620
682
  for (const keyword of keywords) {
621
- if (text.includes(keyword.toLowerCase())) {
683
+ if (matchesKeyword(text, keyword)) {
622
684
  matchCount++;
623
685
  if (signals.length < 3) {
624
686
  signals.push(keyword);
@@ -765,9 +827,7 @@ function classifyByRules(prompt, systemPrompt, estimatedTokens, config) {
765
827
  const w = weights[d.name] ?? 0;
766
828
  weightedScore += d.score * w;
767
829
  }
768
- const reasoningMatches = config.reasoningKeywords.filter(
769
- (kw) => userText.includes(kw.toLowerCase())
770
- );
830
+ const reasoningMatches = config.reasoningKeywords.filter((kw) => matchesKeyword(userText, kw));
771
831
  if (reasoningMatches.length >= 2) {
772
832
  const confidence2 = calibrateConfidence(
773
833
  Math.max(weightedScore, 0.3),
@@ -832,7 +892,7 @@ function selectModel(tier, confidence, method, reasoning, tierConfigs, modelPric
832
892
  const baselineInput = estimatedInputTokens / 1e6 * opusInputPrice;
833
893
  const baselineOutput = maxOutputTokens / 1e6 * opusOutputPrice;
834
894
  const baselineCost = baselineInput + baselineOutput;
835
- const savings = routingProfile === "premium" ? 0 : baselineCost > 0 ? Math.max(0, (baselineCost - costEstimate) / baselineCost) : 0;
895
+ const savings = routingProfile === "premium" || pricing === void 0 ? 0 : baselineCost > 0 ? Math.max(0, (baselineCost - costEstimate) / baselineCost) : 0;
836
896
  return {
837
897
  model,
838
898
  tier,
@@ -883,6 +943,21 @@ function filterCandidatesByCapacity(models, estimatedInputTokens, requestedOutpu
883
943
  });
884
944
  return filtered;
885
945
  }
946
+ var CJK_CHARS = /[ -ヿ㐀-䶿一-鿿가-힯豈-﫿＀-￯]/g;
947
+ function estimateTokens(text) {
948
+ if (text.length === 0) return 0;
949
+ const cjk = text.match(CJK_CHARS)?.length ?? 0;
950
+ return cjk + Math.ceil((text.length - cjk) / 4);
951
+ }
952
+ function scanLimit(promptTruncationChars) {
953
+ return Math.max(1, Math.min(8e3, promptTruncationChars));
954
+ }
955
+ function sampleForScan(value, limit) {
956
+ if (value.length <= limit) return value;
957
+ const prefixLength = Math.ceil(limit / 2);
958
+ return `${value.slice(0, prefixLength)}
959
+ ${value.slice(-(limit - prefixLength))}`;
960
+ }
886
961
  function applyPromotions(tierConfigs, promotions, profile, now = /* @__PURE__ */ new Date()) {
887
962
  if (!promotions || promotions.length === 0) return tierConfigs;
888
963
  let result = tierConfigs;
@@ -925,16 +1000,10 @@ var RulesStrategy = class {
925
1000
  route(prompt, systemPrompt, maxOutputTokens, options) {
926
1001
  const { config, modelPricing } = options;
927
1002
  const fullText = `${systemPrompt ?? ""} ${prompt}`;
928
- const estimatedTokens = Math.ceil(fullText.length / 4);
929
- const scanLimit = Math.max(1, Math.min(8e3, config.classifier.promptTruncationChars));
930
- const sample = (value) => {
931
- if (value.length <= scanLimit) return value;
932
- const prefixLength = Math.ceil(scanLimit / 2);
933
- return `${value.slice(0, prefixLength)}
934
- ${value.slice(-(scanLimit - prefixLength))}`;
935
- };
936
- const scannedPrompt = sample(prompt);
937
- const scannedSystemPrompt = systemPrompt ? sample(systemPrompt) : void 0;
1003
+ const estimatedTokens = estimateTokens(fullText);
1004
+ const limit = scanLimit(config.classifier.promptTruncationChars);
1005
+ const scannedPrompt = sampleForScan(prompt, limit);
1006
+ const scannedSystemPrompt = systemPrompt ? sampleForScan(systemPrompt, limit) : void 0;
938
1007
  const ruleResult = classifyByRules(
939
1008
  scannedPrompt,
940
1009
  scannedSystemPrompt,
@@ -988,7 +1057,7 @@ ${value.slice(-(scanLimit - prefixLength))}`;
988
1057
  );
989
1058
  return { ...decision2, tierConfigs, profile };
990
1059
  }
991
- const hasStructuredOutput = options.requiresStructuredOutput === true || (scannedSystemPrompt ? /json|structured|schema/i.test(scannedSystemPrompt) : false);
1060
+ const hasStructuredOutput = options.requiresStructuredOutput === true;
992
1061
  let tier;
993
1062
  let confidence;
994
1063
  const method = "rules";
@@ -1044,6 +1113,12 @@ var DEFAULT_MODEL_CAPABILITIES = Object.freeze({
1044
1113
  supportsTools: true,
1045
1114
  supportsVision: true
1046
1115
  },
1116
+ "anthropic/claude-fable-5.1": {
1117
+ contextWindow: 1e6,
1118
+ maxOutputTokens: 128e3,
1119
+ supportsTools: true,
1120
+ supportsVision: true
1121
+ },
1047
1122
  "anthropic/claude-haiku-4.5": {
1048
1123
  // override: The public catalog's `categories` omit "vision" for this Anthropic model even though the gateway accepts image input for it (the prior hand-maintained snapshot had it, and Anthropic's model card lists it). Without this the vision filter would silently drop it — reported against the catalog; remove once the categories carry vision.
1049
1124
  contextWindow: 2e5,
@@ -1075,6 +1150,12 @@ var DEFAULT_MODEL_CAPABILITIES = Object.freeze({
1075
1150
  supportsTools: true,
1076
1151
  supportsVision: true
1077
1152
  },
1153
+ "anthropic/claude-opus-5.5": {
1154
+ contextWindow: 1e6,
1155
+ maxOutputTokens: 128e3,
1156
+ supportsTools: true,
1157
+ supportsVision: true
1158
+ },
1078
1159
  "anthropic/claude-sonnet-4.5": {
1079
1160
  contextWindow: 2e5,
1080
1161
  maxOutputTokens: 64e3,
@@ -1094,6 +1175,12 @@ var DEFAULT_MODEL_CAPABILITIES = Object.freeze({
1094
1175
  supportsTools: true,
1095
1176
  supportsVision: true
1096
1177
  },
1178
+ "anthropic/claude-sonnet-5.5": {
1179
+ contextWindow: 1e6,
1180
+ maxOutputTokens: 128e3,
1181
+ supportsTools: true,
1182
+ supportsVision: true
1183
+ },
1097
1184
  "cohere/north-mini-code": {
1098
1185
  // supportsTools: not probed — fails closed
1099
1186
  contextWindow: 256e3,
@@ -1113,6 +1200,12 @@ var DEFAULT_MODEL_CAPABILITIES = Object.freeze({
1113
1200
  supportsTools: true,
1114
1201
  supportsVision: false
1115
1202
  },
1203
+ "deepseek/deepseek-v4-flash-vision-exp": {
1204
+ contextWindow: 1048576,
1205
+ maxOutputTokens: 65536,
1206
+ supportsTools: false,
1207
+ supportsVision: true
1208
+ },
1116
1209
  "deepseek/deepseek-v4-pro": {
1117
1210
  contextWindow: 1048576,
1118
1211
  maxOutputTokens: 65536,
@@ -1173,6 +1266,12 @@ var DEFAULT_MODEL_CAPABILITIES = Object.freeze({
1173
1266
  supportsTools: true,
1174
1267
  supportsVision: true
1175
1268
  },
1269
+ "google/gemini-3.8-flash": {
1270
+ contextWindow: 1048576,
1271
+ maxOutputTokens: 65536,
1272
+ supportsTools: true,
1273
+ supportsVision: true
1274
+ },
1176
1275
  "minimax/minimax-m2.7": {
1177
1276
  contextWindow: 204800,
1178
1277
  maxOutputTokens: 16384,
@@ -1198,7 +1297,7 @@ var DEFAULT_MODEL_CAPABILITIES = Object.freeze({
1198
1297
  supportsTools: false,
1199
1298
  supportsVision: true
1200
1299
  },
1201
- "nvidia/nemotron-3-nano-30b": {
1300
+ "nvidia/muse-glimmer-30b": {
1202
1301
  // supportsTools: not probed — fails closed
1203
1302
  contextWindow: 131072,
1204
1303
  maxOutputTokens: 16384,
@@ -1206,10 +1305,11 @@ var DEFAULT_MODEL_CAPABILITIES = Object.freeze({
1206
1305
  supportsVision: false
1207
1306
  },
1208
1307
  "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning": {
1308
+ // override: The catalog tags this model "vision", but a correctly sized probe does not hold up (ClawRouter, 2026-08-31): a 64x64 solid-red PNG was named correctly 1 of 4 times on Base, and on Solana the image was silently dropped and a text model answered "white". An HTTP 200 with a confident wrong answer gives the caller nothing to branch on, so image turns must not be routed here. It is ecoTiers.SIMPLE.fallback[0] since nemotron-3-nano-30b was delisted (2026-09-08); remove once a probe of this size comes back right on both chains.
1209
1309
  contextWindow: 256e3,
1210
1310
  maxOutputTokens: 16384,
1211
1311
  supportsTools: false,
1212
- supportsVision: true
1312
+ supportsVision: false
1213
1313
  },
1214
1314
  "nvidia/nemotron-3-ultra-550b": {
1215
1315
  // supportsTools: not probed — fails closed
@@ -1267,6 +1367,12 @@ var DEFAULT_MODEL_CAPABILITIES = Object.freeze({
1267
1367
  supportsTools: true,
1268
1368
  supportsVision: false
1269
1369
  },
1370
+ "openai/gpt-5.1": {
1371
+ contextWindow: 4e5,
1372
+ maxOutputTokens: 128e3,
1373
+ supportsTools: true,
1374
+ supportsVision: true
1375
+ },
1270
1376
  "openai/gpt-5.2": {
1271
1377
  contextWindow: 4e5,
1272
1378
  maxOutputTokens: 128e3,
@@ -1361,6 +1467,24 @@ var DEFAULT_MODEL_CAPABILITIES = Object.freeze({
1361
1467
  supportsTools: true,
1362
1468
  supportsVision: true
1363
1469
  },
1470
+ "openai/gpt-6-astra": {
1471
+ contextWindow: 105e4,
1472
+ maxOutputTokens: 128e3,
1473
+ supportsTools: true,
1474
+ supportsVision: true
1475
+ },
1476
+ "openai/gpt-6-luna": {
1477
+ contextWindow: 105e4,
1478
+ maxOutputTokens: 128e3,
1479
+ supportsTools: true,
1480
+ supportsVision: true
1481
+ },
1482
+ "openai/gpt-6-sol": {
1483
+ contextWindow: 105e4,
1484
+ maxOutputTokens: 128e3,
1485
+ supportsTools: true,
1486
+ supportsVision: true
1487
+ },
1364
1488
  "openai/o1": {
1365
1489
  contextWindow: 2e5,
1366
1490
  maxOutputTokens: 1e5,
@@ -1385,6 +1509,13 @@ var DEFAULT_MODEL_CAPABILITIES = Object.freeze({
1385
1509
  supportsTools: true,
1386
1510
  supportsVision: false
1387
1511
  },
1512
+ "poolside/laguna-s-2.1": {
1513
+ // supportsTools: not probed — fails closed
1514
+ contextWindow: 1048576,
1515
+ maxOutputTokens: 131072,
1516
+ supportsTools: false,
1517
+ supportsVision: false
1518
+ },
1388
1519
  "poolside/laguna-xs-2.1": {
1389
1520
  // supportsTools: not probed — fails closed
1390
1521
  contextWindow: 131072,
@@ -1410,10 +1541,24 @@ var DEFAULT_MODEL_CAPABILITIES = Object.freeze({
1410
1541
  supportsTools: true,
1411
1542
  supportsVision: false
1412
1543
  },
1413
- "tencent/hy3": {
1414
- contextWindow: 262144,
1415
- maxOutputTokens: 128e3,
1416
- supportsTools: true,
1544
+ "qwen/qwen3.8-flash": {
1545
+ contextWindow: 1e6,
1546
+ maxOutputTokens: 131072,
1547
+ supportsTools: false,
1548
+ supportsVision: true
1549
+ },
1550
+ "qwen/qwen3.8-max": {
1551
+ // supportsTools: not probed — fails closed
1552
+ contextWindow: 1e6,
1553
+ maxOutputTokens: 131072,
1554
+ supportsTools: false,
1555
+ supportsVision: false
1556
+ },
1557
+ "tencent/hy4-preview": {
1558
+ // supportsTools: not probed — fails closed
1559
+ contextWindow: 1048576,
1560
+ maxOutputTokens: 64e3,
1561
+ supportsTools: false,
1417
1562
  supportsVision: false
1418
1563
  },
1419
1564
  "xai/grok-4.3": {
@@ -1428,12 +1573,30 @@ var DEFAULT_MODEL_CAPABILITIES = Object.freeze({
1428
1573
  supportsTools: true,
1429
1574
  supportsVision: true
1430
1575
  },
1576
+ "xai/grok-4.6": {
1577
+ contextWindow: 5e5,
1578
+ maxOutputTokens: 16384,
1579
+ supportsTools: true,
1580
+ supportsVision: true
1581
+ },
1582
+ "xai/grok-4.7": {
1583
+ contextWindow: 5e5,
1584
+ maxOutputTokens: 16384,
1585
+ supportsTools: true,
1586
+ supportsVision: true
1587
+ },
1431
1588
  "xai/grok-build-0.1": {
1432
1589
  contextWindow: 256e3,
1433
1590
  maxOutputTokens: 16384,
1434
1591
  supportsTools: true,
1435
1592
  supportsVision: false
1436
1593
  },
1594
+ "xiaomi/mimo-v2.5": {
1595
+ contextWindow: 1048576,
1596
+ maxOutputTokens: 131072,
1597
+ supportsTools: true,
1598
+ supportsVision: true
1599
+ },
1437
1600
  "xiaomi/mimo-v2.5-pro": {
1438
1601
  contextWindow: 1048576,
1439
1602
  maxOutputTokens: 131072,
@@ -2147,16 +2310,10 @@ function likelyNeedsParallelToolCalls(prompt, needsTools, toolCount, toolNames)
2147
2310
  }
2148
2311
  function classifyTask(prompt, systemPrompt, options) {
2149
2312
  const fullText = `${systemPrompt ?? ""} ${prompt}`;
2150
- const estimatedInputTokens = Math.ceil(fullText.length / 4);
2151
- const scanLimit = Math.max(1, Math.min(8e3, options.config.classifier.promptTruncationChars));
2152
- const sample = (value) => {
2153
- if (value.length <= scanLimit) return value;
2154
- const prefixLength = Math.ceil(scanLimit / 2);
2155
- return `${value.slice(0, prefixLength)}
2156
- ${value.slice(-(scanLimit - prefixLength))}`;
2157
- };
2158
- const scannedPrompt = sample(prompt);
2159
- const scannedSystemPrompt = sample(systemPrompt ?? "");
2313
+ const estimatedInputTokens = estimateTokens(fullText);
2314
+ const limit = scanLimit(options.config.classifier.promptTruncationChars);
2315
+ const scannedPrompt = sampleForScan(prompt, limit);
2316
+ const scannedSystemPrompt = sampleForScan(systemPrompt ?? "", limit);
2160
2317
  const scannedFullText = `${scannedSystemPrompt} ${scannedPrompt}`;
2161
2318
  const text = scannedPrompt.toLowerCase();
2162
2319
  const explicitCodeSignal = /```|\b(?:typescript|javascript|python|rust|java|sql|stack trace|traceback|exception)\b|\.(?:ts|tsx|js|py|go|rs)\b/i.test(
@@ -2168,7 +2325,9 @@ ${value.slice(-(scanLimit - prefixLength))}`;
2168
2325
  const nativeCodeSignal = /\b(?:programmed|written|implemented?|code)\s+(?:in|using)\s+(?:c\+\+|c|rust|go)\b/i.test(
2169
2326
  scannedPrompt
2170
2327
  );
2171
- const hasCode = explicitCodeSignal || codeConstructSignal || nativeCodeSignal;
2328
+ const errorClassSignal = /\b[A-Z][A-Za-z]*(?:Error|Exception)\b/.test(scannedPrompt);
2329
+ const hasCode = explicitCodeSignal || codeConstructSignal || nativeCodeSignal || errorClassSignal;
2330
+ const codeArtifact = /```|\b(?:stack trace|traceback)\b|\.(?:ts|tsx|js|py|go|rs)\b/i.test(scannedPrompt) || codeConstructSignal || nativeCodeSignal || errorClassSignal;
2172
2331
  const toolsAvailable = options.hasTools ?? false;
2173
2332
  const needsTools = options.requiresTools ?? (toolsAvailable && inferToolRequirement(scannedPrompt, scannedSystemPrompt));
2174
2333
  const likelyParallelToolCalls = likelyNeedsParallelToolCalls(
@@ -2240,10 +2399,10 @@ ${value.slice(-(scanLimit - prefixLength))}`;
2240
2399
  const needsVision = options.hasVision ?? false;
2241
2400
  const needsStructuredOutput = options.requiresStructuredOutput ?? false;
2242
2401
  const latencySensitive = /\b(?:urgent|asap|fast|quick|low latency|real[- ]time)\b|尽快|马上|快速|低延迟/i.test(
2243
- scannedFullText
2402
+ scannedPrompt
2244
2403
  );
2245
2404
  const highStakes = /\b(?:production|security|payment|legal|medical|financial|audit)\b|生产|安全|支付|法律|医疗|财务|审计/i.test(
2246
- scannedFullText
2405
+ scannedPrompt
2247
2406
  );
2248
2407
  const terminalToolSignal = normalizedToolNames.some(
2249
2408
  (name) => /^(?:terminalexec|terminalinspect|terminalsendkeys)$/.test(name)
@@ -2279,7 +2438,7 @@ ${value.slice(-(scanLimit - prefixLength))}`;
2279
2438
  );
2280
2439
  const terminalSafetySensitive = terminalToolSignal && (highStakes || terminalCredentialSignal);
2281
2440
  const implicitTerminalCode = needsTools && terminalToolSignal && agentRisk === "standard" && !highStakes && !complexTerminalOperation && numberedSteps < 3 && prompt.length <= 1e3 && simpleTerminalArtifact;
2282
- const language = /[\u3400-\u9fff]/.test(scannedFullText) ? "zh" : "other";
2441
+ const language = /[\u3400-\u9fff]/.test(scannedPrompt) ? "zh" : "other";
2283
2442
  const multipleChoiceSignals = (scannedPrompt.match(/(?:^|\n)\s*[A-D][.)]\s+/gim) ?? []).length;
2284
2443
  const numericSignals = (scannedPrompt.match(/-?\d+(?:[.,]\d+)?/g) ?? []).length;
2285
2444
  const compactMathProblem = !hasCode && prompt.length < 2500 && numericSignals >= 2 && (/[+×÷=%$€£¥]|\b(?:total|each|per|times|half|twice|percent|how many|how much|calculate)\b/i.test(
@@ -2294,7 +2453,7 @@ ${value.slice(-(scanLimit - prefixLength))}`;
2294
2453
  else if (needsTools) taskType = "tool_agent";
2295
2454
  else if (multipleChoiceSignals >= 3) taskType = "reasoning_mcq";
2296
2455
  else if (compactMathProblem) taskType = "reasoning_math";
2297
- else if (/\b(?:bug|debug|error|failure|failing|regression|crash|修复|报错|错误|调试)\b/i.test(text))
2456
+ else if (errorClassSignal || /\b(?:bug|debug|error|failure|failing|regression|crash|修复|报错|错误|调试)\b/i.test(text))
2298
2457
  taskType = "debug";
2299
2458
  else if (hasCode || /\b(?:refactor|implement|patch|edit|rewrite|重构|实现|修改)\b/i.test(text))
2300
2459
  taskType = "code_edit";
@@ -2320,7 +2479,8 @@ ${value.slice(-(scanLimit - prefixLength))}`;
2320
2479
  agentRisk,
2321
2480
  terminalToolSignal,
2322
2481
  terminalSafetySensitive,
2323
- implicitTerminalCode
2482
+ implicitTerminalCode,
2483
+ codeArtifact
2324
2484
  };
2325
2485
  }
2326
2486
  function affinity(modelId, task, language = "other", agentDomain = "other", deepWebResearch = false, agentRisk = "standard", terminalToolSignal = false, terminalSafetySensitive = false) {
@@ -2676,6 +2836,19 @@ function evidenceCandidates(task) {
2676
2836
  }
2677
2837
  return [];
2678
2838
  }
2839
+ var VERIFIED_TOOL_FALLBACKS = [
2840
+ "anthropic/claude-fable-5.1",
2841
+ "anthropic/claude-opus-5.5",
2842
+ "anthropic/claude-sonnet-5.5",
2843
+ "google/gemini-3.8-flash",
2844
+ "openai/gpt-5.1",
2845
+ "openai/gpt-6-astra",
2846
+ "openai/gpt-6-luna",
2847
+ "openai/gpt-6-sol",
2848
+ "xai/grok-4.6",
2849
+ "xai/grok-4.7",
2850
+ "xiaomi/mimo-v2.5"
2851
+ ];
2679
2852
  function isEligible(modelId, features, maxOutputTokens, options) {
2680
2853
  const model = options.modelCapabilities?.[modelId] ?? DEFAULT_MODEL_CAPABILITIES[modelId];
2681
2854
  if (!model) return true;
@@ -2710,27 +2883,38 @@ function profileScore(modelId, options, now) {
2710
2883
  }
2711
2884
  var PortfolioStrategy = class {
2712
2885
  name = "portfolio";
2886
+ // Rules set the capability tier; the strategy is stateless, so one instance serves every request.
2887
+ rules = new RulesStrategy();
2713
2888
  route(prompt, systemPrompt, maxOutputTokens, options) {
2714
2889
  const features = classifyTask(prompt, systemPrompt, options);
2715
- const base = new RulesStrategy().route(prompt, systemPrompt, maxOutputTokens, {
2890
+ const base = this.rules.route(prompt, systemPrompt, maxOutputTokens, {
2716
2891
  ...options,
2717
2892
  requiresTools: features.needsTools
2718
2893
  });
2719
2894
  const tierConfigs = base.tierConfigs;
2720
2895
  if (!tierConfigs) return base;
2721
- const targetTier = (features.taskType === "reasoning_mcq" || features.taskType === "reasoning_math") && (base.tier === "SIMPLE" || base.tier === "MEDIUM") ? "REASONING" : base.tier;
2896
+ const targetTier = (features.taskType === "reasoning_mcq" || features.taskType === "reasoning_math") && (base.tier === "SIMPLE" || base.tier === "MEDIUM") ? "REASONING" : (features.taskType === "debug" || features.taskType === "code_edit") && features.codeArtifact && base.tier === "SIMPLE" ? "MEDIUM" : base.tier;
2722
2897
  const tierConfig = tierConfigs[targetTier];
2723
2898
  const configuredCandidates = tierConfig ? getFallbackChain(targetTier, tierConfigs) : [];
2724
2899
  const unavailable = new Set(options.unavailableModels ?? []);
2900
+ const verifiedToolFallbacks = features.needsTools ? VERIFIED_TOOL_FALLBACKS.filter((model2) => options.modelPricing.has(model2)) : [];
2901
+ if (options.routingProfile !== "premium") {
2902
+ const fallbackCost = (model2) => estimatedCost(model2, options, features.estimatedInputTokens, maxOutputTokens);
2903
+ verifiedToolFallbacks.sort((a, b) => fallbackCost(a) - fallbackCost(b));
2904
+ }
2725
2905
  const chain = [
2726
- .../* @__PURE__ */ new Set([...configuredCandidates, ...evidenceCandidates(features.taskType)])
2906
+ .../* @__PURE__ */ new Set([
2907
+ ...configuredCandidates,
2908
+ ...evidenceCandidates(features.taskType),
2909
+ ...verifiedToolFallbacks
2910
+ ])
2727
2911
  ].filter(
2728
2912
  (model2) => typeof model2 === "string" && model2.length > 0 && !unavailable.has(model2)
2729
2913
  );
2730
- const eligible = chain.filter(
2731
- (model2) => isEligible(model2, features, maxOutputTokens, options)
2914
+ const eligible = new Set(
2915
+ chain.filter((model2) => isEligible(model2, features, maxOutputTokens, options))
2732
2916
  );
2733
- const eligibleCandidates = eligible.length > 0 ? eligible : chain;
2917
+ const eligibleCandidates = eligible.size > 0 ? [...eligible] : chain;
2734
2918
  if (eligibleCandidates.length === 0) return base;
2735
2919
  const profileName = options.routingProfile === "eco" ? "eco" : options.routingProfile === "premium" ? "premium" : "auto";
2736
2920
  const portfolio = options.config.portfolio ?? DEFAULT_PORTFOLIO_WEIGHTS;
@@ -2765,11 +2949,11 @@ var PortfolioStrategy = class {
2765
2949
  const rankedEntries = candidates.map((model2, index) => {
2766
2950
  const cost = estimatedCost(model2, options, features.estimatedInputTokens, maxOutputTokens);
2767
2951
  const costScore = Number.isFinite(cost) && maxCost > minCost ? 1 - (cost - minCost) / (maxCost - minCost) : 0.5;
2768
- const capabilityScore = isEligible(model2, features, maxOutputTokens, options) ? 1 : 0;
2952
+ const capabilityScore = eligible.has(model2) ? 1 : 0;
2769
2953
  const profile = profileScore(model2, options, now);
2770
2954
  const observedQuality = profile?.quality === void 0 ? getAffinity(model2) : getAffinity(model2) * (1 - profile.freshness) + profile.quality * profile.freshness;
2771
- const observedSpeed = profile ? profile.speed * profile.freshness : 0.5;
2772
- const observedTailSpeed = profile ? profile.tailSpeed * profile.freshness : 0.5;
2955
+ const observedSpeed = profile ? profile.speed * profile.freshness + 0.5 * (1 - profile.freshness) : 0.5;
2956
+ const observedTailSpeed = profile ? profile.tailSpeed * profile.freshness + 0.5 * (1 - profile.freshness) : 0.5;
2773
2957
  const observedReliability = profile ? profile.reliability * profile.freshness + (1 - profile.freshness) : 1;
2774
2958
  const legacyScore = 1 - index / Math.max(1, candidates.length - 1);
2775
2959
  const qualityWeight = profileWeights.quality + (features.highStakes ? portfolio.highStakesBoost.quality : 0);
@@ -4026,8 +4210,13 @@ var DEFAULT_ROUTING_CONFIG = {
4026
4210
  primary: "nvidia/nemotron-3.5-lightning",
4027
4211
  // FREE — NVIDIA free tier flagship, 1M ctx
4028
4212
  fallback: [
4029
- "nvidia/nemotron-3-nano-30b",
4030
- // FREE — fastest free model (~121 tok/s)
4213
+ // Was nvidia/nemotron-3-nano-30b until blockrun delisted it on 2026-09-08
4214
+ // (NVIDIA deprovisioned it for the account — a structured per-account
4215
+ // 404). nano-omni is blockrun's own redirect target for that id, so this
4216
+ // follows the same rule as the head: the router and the gateway name the
4217
+ // same model.
4218
+ "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning",
4219
+ // FREE — nano-30b's successor, 256K ctx
4031
4220
  // The free head keeps rotting with NVIDIA's hosting (deepseek-v4-flash
4032
4221
  // 410 2026-08-12, seed-oss-36b 410 2026-08-03, gpt-oss-120b/20b 400
4033
4222
  // 2026-08-21, and on 2026-08-30 FOUR of the five visible free models at
@@ -4856,7 +5045,7 @@ function getCostSummary() {
4856
5045
  }
4857
5046
 
4858
5047
  // src/version.ts
4859
- var SDK_VERSION = "3.24.0";
5048
+ var SDK_VERSION = "3.24.2";
4860
5049
  var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
4861
5050
 
4862
5051
  // src/client.ts
package/dist/index.js CHANGED
@@ -440,7 +440,69 @@ function withCost(body, response, bookedUsd, scheme) {
440
440
  // src/client.ts
441
441
  import { privateKeyToAccount } from "viem/accounts";
442
442
 
443
- // node_modules/.pnpm/@blockrun+router-core@https+++codeload.github.com+BlockRunAI+router-core+tar.gz+814fd3b6bc788_xq5fkcckbb2p7dtp6dodey4yri/node_modules/@blockrun/router-core/dist/index.js
443
+ // node_modules/.pnpm/@blockrun+router-core@https+++codeload.github.com+BlockRunAI+router-core+tar.gz+65a92c85ea1f1_ulf6q3mq2pzpwvcnu73z4krxvy/node_modules/@blockrun/router-core/dist/index.js
444
+ var UNSEGMENTED_SCRIPT = /[؀-ۿ -ヿ㐀-䶿一-鿿가-힯豈-﫿＀-￯]/;
445
+ var WORD_CHAR = /[\p{L}\p{N}]/u;
446
+ var ASCII_PHRASE = /^[a-z0-9' -]+$/;
447
+ var SEGMENTED_WORD_CHAR = "\\p{Script=Latin}\\p{Script=Cyrillic}\\p{Script=Greek}\\p{N}";
448
+ var BOUNDARY_BEFORE = `(?<![${SEGMENTED_WORD_CHAR}])`;
449
+ var BOUNDARY_AFTER = `(?![${SEGMENTED_WORD_CHAR}])`;
450
+ var BOUNDED_LAST_WORD_LENGTH = 6;
451
+ var SHORT_SUFFIXES = "(?:s|ed|ing)?";
452
+ var SIBILANT_PLURAL = "es";
453
+ var MID_SUFFIXES = "(?:s|es|ed|d|ing|er|ers|ly|ment|ments|ation|ations|e|en|n|a|as|o|os|em|et|ung|ungen)?";
454
+ var DROPPED_E_SUFFIXES = "(?:ing|ed|es|er|ers|ation|ations)";
455
+ var DOUBLED_CONSONANT_SUFFIXES = "(?:ing|ed|er|ers)";
456
+ var matcherCache = /* @__PURE__ */ new Map();
457
+ function escapeRegExp(value) {
458
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
459
+ }
460
+ function keywordMatcher(keyword) {
461
+ const cached = matcherCache.get(keyword);
462
+ if (cached) return cached;
463
+ const kw = keyword.toLowerCase();
464
+ let matcher;
465
+ if (UNSEGMENTED_SCRIPT.test(kw) || kw.length === 0) {
466
+ matcher = { needle: kw, pattern: null };
467
+ } else {
468
+ const lead = WORD_CHAR.test(kw[0]) ? BOUNDARY_BEFORE : "";
469
+ const escaped = escapeRegExp(kw);
470
+ let body = escaped;
471
+ let trail = "";
472
+ let needle = kw;
473
+ const lastWord = kw.slice(kw.lastIndexOf(" ") + 1);
474
+ if (WORD_CHAR.test(kw[kw.length - 1]) && ASCII_PHRASE.test(kw) && lastWord.length <= BOUNDED_LAST_WORD_LENGTH) {
475
+ const forms = [];
476
+ if (/\d$/.test(kw)) {
477
+ forms.push(escaped);
478
+ } else if (lastWord.length <= 3) {
479
+ forms.push(`${escaped}${SHORT_SUFFIXES}`);
480
+ if (/(?:x|s|z|ch|sh)$/.test(kw)) forms.push(`${escaped}${SIBILANT_PLURAL}`);
481
+ } else {
482
+ forms.push(`${escaped}${MID_SUFFIXES}`);
483
+ if (kw.endsWith("e")) {
484
+ forms.push(`${escaped.slice(0, -1)}${DROPPED_E_SUFFIXES}`);
485
+ needle = kw.slice(0, -1);
486
+ } else if (kw.endsWith("y")) {
487
+ forms.push(`${escaped.slice(0, -1)}(?:ies|ied)`);
488
+ needle = kw.slice(0, -1);
489
+ } else if (/[aeiou][bdgmnprt]$/.test(kw)) {
490
+ forms.push(`${escaped}${kw[kw.length - 1]}${DOUBLED_CONSONANT_SUFFIXES}`);
491
+ }
492
+ }
493
+ body = `(?:${forms.join("|")})`;
494
+ trail = BOUNDARY_AFTER;
495
+ }
496
+ matcher = { needle, pattern: new RegExp(`${lead}${body}${trail}`, "u") };
497
+ }
498
+ matcherCache.set(keyword, matcher);
499
+ return matcher;
500
+ }
501
+ function matchesKeyword(text, keyword) {
502
+ const { needle, pattern } = keywordMatcher(keyword);
503
+ if (!text.includes(needle)) return false;
504
+ return pattern === null ? true : pattern.test(text);
505
+ }
444
506
  function scoreTokenCount(estimatedTokens, thresholds) {
445
507
  if (estimatedTokens < thresholds.simple) {
446
508
  return { name: "tokenCount", score: -1, signal: `short (${estimatedTokens} tokens)` };
@@ -451,7 +513,7 @@ function scoreTokenCount(estimatedTokens, thresholds) {
451
513
  return { name: "tokenCount", score: 0, signal: null };
452
514
  }
453
515
  function scoreKeywordMatch(text, keywords, name, signalLabel, thresholds, scores) {
454
- const matches = keywords.filter((kw) => text.includes(kw.toLowerCase()));
516
+ const matches = keywords.filter((kw) => matchesKeyword(text, kw));
455
517
  if (matches.length >= thresholds.high) {
456
518
  return {
457
519
  name,
@@ -487,7 +549,7 @@ function scoreAgenticTask(text, keywords) {
487
549
  let matchCount = 0;
488
550
  const signals = [];
489
551
  for (const keyword of keywords) {
490
- if (text.includes(keyword.toLowerCase())) {
552
+ if (matchesKeyword(text, keyword)) {
491
553
  matchCount++;
492
554
  if (signals.length < 3) {
493
555
  signals.push(keyword);
@@ -634,9 +696,7 @@ function classifyByRules(prompt, systemPrompt, estimatedTokens, config) {
634
696
  const w = weights[d.name] ?? 0;
635
697
  weightedScore += d.score * w;
636
698
  }
637
- const reasoningMatches = config.reasoningKeywords.filter(
638
- (kw) => userText.includes(kw.toLowerCase())
639
- );
699
+ const reasoningMatches = config.reasoningKeywords.filter((kw) => matchesKeyword(userText, kw));
640
700
  if (reasoningMatches.length >= 2) {
641
701
  const confidence2 = calibrateConfidence(
642
702
  Math.max(weightedScore, 0.3),
@@ -701,7 +761,7 @@ function selectModel(tier, confidence, method, reasoning, tierConfigs, modelPric
701
761
  const baselineInput = estimatedInputTokens / 1e6 * opusInputPrice;
702
762
  const baselineOutput = maxOutputTokens / 1e6 * opusOutputPrice;
703
763
  const baselineCost = baselineInput + baselineOutput;
704
- const savings = routingProfile === "premium" ? 0 : baselineCost > 0 ? Math.max(0, (baselineCost - costEstimate) / baselineCost) : 0;
764
+ const savings = routingProfile === "premium" || pricing === void 0 ? 0 : baselineCost > 0 ? Math.max(0, (baselineCost - costEstimate) / baselineCost) : 0;
705
765
  return {
706
766
  model,
707
767
  tier,
@@ -752,6 +812,21 @@ function filterCandidatesByCapacity(models, estimatedInputTokens, requestedOutpu
752
812
  });
753
813
  return filtered;
754
814
  }
815
+ var CJK_CHARS = /[ -ヿ㐀-䶿一-鿿가-힯豈-﫿＀-￯]/g;
816
+ function estimateTokens(text) {
817
+ if (text.length === 0) return 0;
818
+ const cjk = text.match(CJK_CHARS)?.length ?? 0;
819
+ return cjk + Math.ceil((text.length - cjk) / 4);
820
+ }
821
+ function scanLimit(promptTruncationChars) {
822
+ return Math.max(1, Math.min(8e3, promptTruncationChars));
823
+ }
824
+ function sampleForScan(value, limit) {
825
+ if (value.length <= limit) return value;
826
+ const prefixLength = Math.ceil(limit / 2);
827
+ return `${value.slice(0, prefixLength)}
828
+ ${value.slice(-(limit - prefixLength))}`;
829
+ }
755
830
  function applyPromotions(tierConfigs, promotions, profile, now = /* @__PURE__ */ new Date()) {
756
831
  if (!promotions || promotions.length === 0) return tierConfigs;
757
832
  let result = tierConfigs;
@@ -794,16 +869,10 @@ var RulesStrategy = class {
794
869
  route(prompt, systemPrompt, maxOutputTokens, options) {
795
870
  const { config, modelPricing } = options;
796
871
  const fullText = `${systemPrompt ?? ""} ${prompt}`;
797
- const estimatedTokens = Math.ceil(fullText.length / 4);
798
- const scanLimit = Math.max(1, Math.min(8e3, config.classifier.promptTruncationChars));
799
- const sample = (value) => {
800
- if (value.length <= scanLimit) return value;
801
- const prefixLength = Math.ceil(scanLimit / 2);
802
- return `${value.slice(0, prefixLength)}
803
- ${value.slice(-(scanLimit - prefixLength))}`;
804
- };
805
- const scannedPrompt = sample(prompt);
806
- const scannedSystemPrompt = systemPrompt ? sample(systemPrompt) : void 0;
872
+ const estimatedTokens = estimateTokens(fullText);
873
+ const limit = scanLimit(config.classifier.promptTruncationChars);
874
+ const scannedPrompt = sampleForScan(prompt, limit);
875
+ const scannedSystemPrompt = systemPrompt ? sampleForScan(systemPrompt, limit) : void 0;
807
876
  const ruleResult = classifyByRules(
808
877
  scannedPrompt,
809
878
  scannedSystemPrompt,
@@ -857,7 +926,7 @@ ${value.slice(-(scanLimit - prefixLength))}`;
857
926
  );
858
927
  return { ...decision2, tierConfigs, profile };
859
928
  }
860
- const hasStructuredOutput = options.requiresStructuredOutput === true || (scannedSystemPrompt ? /json|structured|schema/i.test(scannedSystemPrompt) : false);
929
+ const hasStructuredOutput = options.requiresStructuredOutput === true;
861
930
  let tier;
862
931
  let confidence;
863
932
  const method = "rules";
@@ -913,6 +982,12 @@ var DEFAULT_MODEL_CAPABILITIES = Object.freeze({
913
982
  supportsTools: true,
914
983
  supportsVision: true
915
984
  },
985
+ "anthropic/claude-fable-5.1": {
986
+ contextWindow: 1e6,
987
+ maxOutputTokens: 128e3,
988
+ supportsTools: true,
989
+ supportsVision: true
990
+ },
916
991
  "anthropic/claude-haiku-4.5": {
917
992
  // override: The public catalog's `categories` omit "vision" for this Anthropic model even though the gateway accepts image input for it (the prior hand-maintained snapshot had it, and Anthropic's model card lists it). Without this the vision filter would silently drop it — reported against the catalog; remove once the categories carry vision.
918
993
  contextWindow: 2e5,
@@ -944,6 +1019,12 @@ var DEFAULT_MODEL_CAPABILITIES = Object.freeze({
944
1019
  supportsTools: true,
945
1020
  supportsVision: true
946
1021
  },
1022
+ "anthropic/claude-opus-5.5": {
1023
+ contextWindow: 1e6,
1024
+ maxOutputTokens: 128e3,
1025
+ supportsTools: true,
1026
+ supportsVision: true
1027
+ },
947
1028
  "anthropic/claude-sonnet-4.5": {
948
1029
  contextWindow: 2e5,
949
1030
  maxOutputTokens: 64e3,
@@ -963,6 +1044,12 @@ var DEFAULT_MODEL_CAPABILITIES = Object.freeze({
963
1044
  supportsTools: true,
964
1045
  supportsVision: true
965
1046
  },
1047
+ "anthropic/claude-sonnet-5.5": {
1048
+ contextWindow: 1e6,
1049
+ maxOutputTokens: 128e3,
1050
+ supportsTools: true,
1051
+ supportsVision: true
1052
+ },
966
1053
  "cohere/north-mini-code": {
967
1054
  // supportsTools: not probed — fails closed
968
1055
  contextWindow: 256e3,
@@ -982,6 +1069,12 @@ var DEFAULT_MODEL_CAPABILITIES = Object.freeze({
982
1069
  supportsTools: true,
983
1070
  supportsVision: false
984
1071
  },
1072
+ "deepseek/deepseek-v4-flash-vision-exp": {
1073
+ contextWindow: 1048576,
1074
+ maxOutputTokens: 65536,
1075
+ supportsTools: false,
1076
+ supportsVision: true
1077
+ },
985
1078
  "deepseek/deepseek-v4-pro": {
986
1079
  contextWindow: 1048576,
987
1080
  maxOutputTokens: 65536,
@@ -1042,6 +1135,12 @@ var DEFAULT_MODEL_CAPABILITIES = Object.freeze({
1042
1135
  supportsTools: true,
1043
1136
  supportsVision: true
1044
1137
  },
1138
+ "google/gemini-3.8-flash": {
1139
+ contextWindow: 1048576,
1140
+ maxOutputTokens: 65536,
1141
+ supportsTools: true,
1142
+ supportsVision: true
1143
+ },
1045
1144
  "minimax/minimax-m2.7": {
1046
1145
  contextWindow: 204800,
1047
1146
  maxOutputTokens: 16384,
@@ -1067,7 +1166,7 @@ var DEFAULT_MODEL_CAPABILITIES = Object.freeze({
1067
1166
  supportsTools: false,
1068
1167
  supportsVision: true
1069
1168
  },
1070
- "nvidia/nemotron-3-nano-30b": {
1169
+ "nvidia/muse-glimmer-30b": {
1071
1170
  // supportsTools: not probed — fails closed
1072
1171
  contextWindow: 131072,
1073
1172
  maxOutputTokens: 16384,
@@ -1075,10 +1174,11 @@ var DEFAULT_MODEL_CAPABILITIES = Object.freeze({
1075
1174
  supportsVision: false
1076
1175
  },
1077
1176
  "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning": {
1177
+ // override: The catalog tags this model "vision", but a correctly sized probe does not hold up (ClawRouter, 2026-08-31): a 64x64 solid-red PNG was named correctly 1 of 4 times on Base, and on Solana the image was silently dropped and a text model answered "white". An HTTP 200 with a confident wrong answer gives the caller nothing to branch on, so image turns must not be routed here. It is ecoTiers.SIMPLE.fallback[0] since nemotron-3-nano-30b was delisted (2026-09-08); remove once a probe of this size comes back right on both chains.
1078
1178
  contextWindow: 256e3,
1079
1179
  maxOutputTokens: 16384,
1080
1180
  supportsTools: false,
1081
- supportsVision: true
1181
+ supportsVision: false
1082
1182
  },
1083
1183
  "nvidia/nemotron-3-ultra-550b": {
1084
1184
  // supportsTools: not probed — fails closed
@@ -1136,6 +1236,12 @@ var DEFAULT_MODEL_CAPABILITIES = Object.freeze({
1136
1236
  supportsTools: true,
1137
1237
  supportsVision: false
1138
1238
  },
1239
+ "openai/gpt-5.1": {
1240
+ contextWindow: 4e5,
1241
+ maxOutputTokens: 128e3,
1242
+ supportsTools: true,
1243
+ supportsVision: true
1244
+ },
1139
1245
  "openai/gpt-5.2": {
1140
1246
  contextWindow: 4e5,
1141
1247
  maxOutputTokens: 128e3,
@@ -1230,6 +1336,24 @@ var DEFAULT_MODEL_CAPABILITIES = Object.freeze({
1230
1336
  supportsTools: true,
1231
1337
  supportsVision: true
1232
1338
  },
1339
+ "openai/gpt-6-astra": {
1340
+ contextWindow: 105e4,
1341
+ maxOutputTokens: 128e3,
1342
+ supportsTools: true,
1343
+ supportsVision: true
1344
+ },
1345
+ "openai/gpt-6-luna": {
1346
+ contextWindow: 105e4,
1347
+ maxOutputTokens: 128e3,
1348
+ supportsTools: true,
1349
+ supportsVision: true
1350
+ },
1351
+ "openai/gpt-6-sol": {
1352
+ contextWindow: 105e4,
1353
+ maxOutputTokens: 128e3,
1354
+ supportsTools: true,
1355
+ supportsVision: true
1356
+ },
1233
1357
  "openai/o1": {
1234
1358
  contextWindow: 2e5,
1235
1359
  maxOutputTokens: 1e5,
@@ -1254,6 +1378,13 @@ var DEFAULT_MODEL_CAPABILITIES = Object.freeze({
1254
1378
  supportsTools: true,
1255
1379
  supportsVision: false
1256
1380
  },
1381
+ "poolside/laguna-s-2.1": {
1382
+ // supportsTools: not probed — fails closed
1383
+ contextWindow: 1048576,
1384
+ maxOutputTokens: 131072,
1385
+ supportsTools: false,
1386
+ supportsVision: false
1387
+ },
1257
1388
  "poolside/laguna-xs-2.1": {
1258
1389
  // supportsTools: not probed — fails closed
1259
1390
  contextWindow: 131072,
@@ -1279,10 +1410,24 @@ var DEFAULT_MODEL_CAPABILITIES = Object.freeze({
1279
1410
  supportsTools: true,
1280
1411
  supportsVision: false
1281
1412
  },
1282
- "tencent/hy3": {
1283
- contextWindow: 262144,
1284
- maxOutputTokens: 128e3,
1285
- supportsTools: true,
1413
+ "qwen/qwen3.8-flash": {
1414
+ contextWindow: 1e6,
1415
+ maxOutputTokens: 131072,
1416
+ supportsTools: false,
1417
+ supportsVision: true
1418
+ },
1419
+ "qwen/qwen3.8-max": {
1420
+ // supportsTools: not probed — fails closed
1421
+ contextWindow: 1e6,
1422
+ maxOutputTokens: 131072,
1423
+ supportsTools: false,
1424
+ supportsVision: false
1425
+ },
1426
+ "tencent/hy4-preview": {
1427
+ // supportsTools: not probed — fails closed
1428
+ contextWindow: 1048576,
1429
+ maxOutputTokens: 64e3,
1430
+ supportsTools: false,
1286
1431
  supportsVision: false
1287
1432
  },
1288
1433
  "xai/grok-4.3": {
@@ -1297,12 +1442,30 @@ var DEFAULT_MODEL_CAPABILITIES = Object.freeze({
1297
1442
  supportsTools: true,
1298
1443
  supportsVision: true
1299
1444
  },
1445
+ "xai/grok-4.6": {
1446
+ contextWindow: 5e5,
1447
+ maxOutputTokens: 16384,
1448
+ supportsTools: true,
1449
+ supportsVision: true
1450
+ },
1451
+ "xai/grok-4.7": {
1452
+ contextWindow: 5e5,
1453
+ maxOutputTokens: 16384,
1454
+ supportsTools: true,
1455
+ supportsVision: true
1456
+ },
1300
1457
  "xai/grok-build-0.1": {
1301
1458
  contextWindow: 256e3,
1302
1459
  maxOutputTokens: 16384,
1303
1460
  supportsTools: true,
1304
1461
  supportsVision: false
1305
1462
  },
1463
+ "xiaomi/mimo-v2.5": {
1464
+ contextWindow: 1048576,
1465
+ maxOutputTokens: 131072,
1466
+ supportsTools: true,
1467
+ supportsVision: true
1468
+ },
1306
1469
  "xiaomi/mimo-v2.5-pro": {
1307
1470
  contextWindow: 1048576,
1308
1471
  maxOutputTokens: 131072,
@@ -2016,16 +2179,10 @@ function likelyNeedsParallelToolCalls(prompt, needsTools, toolCount, toolNames)
2016
2179
  }
2017
2180
  function classifyTask(prompt, systemPrompt, options) {
2018
2181
  const fullText = `${systemPrompt ?? ""} ${prompt}`;
2019
- const estimatedInputTokens = Math.ceil(fullText.length / 4);
2020
- const scanLimit = Math.max(1, Math.min(8e3, options.config.classifier.promptTruncationChars));
2021
- const sample = (value) => {
2022
- if (value.length <= scanLimit) return value;
2023
- const prefixLength = Math.ceil(scanLimit / 2);
2024
- return `${value.slice(0, prefixLength)}
2025
- ${value.slice(-(scanLimit - prefixLength))}`;
2026
- };
2027
- const scannedPrompt = sample(prompt);
2028
- const scannedSystemPrompt = sample(systemPrompt ?? "");
2182
+ const estimatedInputTokens = estimateTokens(fullText);
2183
+ const limit = scanLimit(options.config.classifier.promptTruncationChars);
2184
+ const scannedPrompt = sampleForScan(prompt, limit);
2185
+ const scannedSystemPrompt = sampleForScan(systemPrompt ?? "", limit);
2029
2186
  const scannedFullText = `${scannedSystemPrompt} ${scannedPrompt}`;
2030
2187
  const text = scannedPrompt.toLowerCase();
2031
2188
  const explicitCodeSignal = /```|\b(?:typescript|javascript|python|rust|java|sql|stack trace|traceback|exception)\b|\.(?:ts|tsx|js|py|go|rs)\b/i.test(
@@ -2037,7 +2194,9 @@ ${value.slice(-(scanLimit - prefixLength))}`;
2037
2194
  const nativeCodeSignal = /\b(?:programmed|written|implemented?|code)\s+(?:in|using)\s+(?:c\+\+|c|rust|go)\b/i.test(
2038
2195
  scannedPrompt
2039
2196
  );
2040
- const hasCode = explicitCodeSignal || codeConstructSignal || nativeCodeSignal;
2197
+ const errorClassSignal = /\b[A-Z][A-Za-z]*(?:Error|Exception)\b/.test(scannedPrompt);
2198
+ const hasCode = explicitCodeSignal || codeConstructSignal || nativeCodeSignal || errorClassSignal;
2199
+ const codeArtifact = /```|\b(?:stack trace|traceback)\b|\.(?:ts|tsx|js|py|go|rs)\b/i.test(scannedPrompt) || codeConstructSignal || nativeCodeSignal || errorClassSignal;
2041
2200
  const toolsAvailable = options.hasTools ?? false;
2042
2201
  const needsTools = options.requiresTools ?? (toolsAvailable && inferToolRequirement(scannedPrompt, scannedSystemPrompt));
2043
2202
  const likelyParallelToolCalls = likelyNeedsParallelToolCalls(
@@ -2109,10 +2268,10 @@ ${value.slice(-(scanLimit - prefixLength))}`;
2109
2268
  const needsVision = options.hasVision ?? false;
2110
2269
  const needsStructuredOutput = options.requiresStructuredOutput ?? false;
2111
2270
  const latencySensitive = /\b(?:urgent|asap|fast|quick|low latency|real[- ]time)\b|尽快|马上|快速|低延迟/i.test(
2112
- scannedFullText
2271
+ scannedPrompt
2113
2272
  );
2114
2273
  const highStakes = /\b(?:production|security|payment|legal|medical|financial|audit)\b|生产|安全|支付|法律|医疗|财务|审计/i.test(
2115
- scannedFullText
2274
+ scannedPrompt
2116
2275
  );
2117
2276
  const terminalToolSignal = normalizedToolNames.some(
2118
2277
  (name) => /^(?:terminalexec|terminalinspect|terminalsendkeys)$/.test(name)
@@ -2148,7 +2307,7 @@ ${value.slice(-(scanLimit - prefixLength))}`;
2148
2307
  );
2149
2308
  const terminalSafetySensitive = terminalToolSignal && (highStakes || terminalCredentialSignal);
2150
2309
  const implicitTerminalCode = needsTools && terminalToolSignal && agentRisk === "standard" && !highStakes && !complexTerminalOperation && numberedSteps < 3 && prompt.length <= 1e3 && simpleTerminalArtifact;
2151
- const language = /[\u3400-\u9fff]/.test(scannedFullText) ? "zh" : "other";
2310
+ const language = /[\u3400-\u9fff]/.test(scannedPrompt) ? "zh" : "other";
2152
2311
  const multipleChoiceSignals = (scannedPrompt.match(/(?:^|\n)\s*[A-D][.)]\s+/gim) ?? []).length;
2153
2312
  const numericSignals = (scannedPrompt.match(/-?\d+(?:[.,]\d+)?/g) ?? []).length;
2154
2313
  const compactMathProblem = !hasCode && prompt.length < 2500 && numericSignals >= 2 && (/[+×÷=%$€£¥]|\b(?:total|each|per|times|half|twice|percent|how many|how much|calculate)\b/i.test(
@@ -2163,7 +2322,7 @@ ${value.slice(-(scanLimit - prefixLength))}`;
2163
2322
  else if (needsTools) taskType = "tool_agent";
2164
2323
  else if (multipleChoiceSignals >= 3) taskType = "reasoning_mcq";
2165
2324
  else if (compactMathProblem) taskType = "reasoning_math";
2166
- else if (/\b(?:bug|debug|error|failure|failing|regression|crash|修复|报错|错误|调试)\b/i.test(text))
2325
+ else if (errorClassSignal || /\b(?:bug|debug|error|failure|failing|regression|crash|修复|报错|错误|调试)\b/i.test(text))
2167
2326
  taskType = "debug";
2168
2327
  else if (hasCode || /\b(?:refactor|implement|patch|edit|rewrite|重构|实现|修改)\b/i.test(text))
2169
2328
  taskType = "code_edit";
@@ -2189,7 +2348,8 @@ ${value.slice(-(scanLimit - prefixLength))}`;
2189
2348
  agentRisk,
2190
2349
  terminalToolSignal,
2191
2350
  terminalSafetySensitive,
2192
- implicitTerminalCode
2351
+ implicitTerminalCode,
2352
+ codeArtifact
2193
2353
  };
2194
2354
  }
2195
2355
  function affinity(modelId, task, language = "other", agentDomain = "other", deepWebResearch = false, agentRisk = "standard", terminalToolSignal = false, terminalSafetySensitive = false) {
@@ -2545,6 +2705,19 @@ function evidenceCandidates(task) {
2545
2705
  }
2546
2706
  return [];
2547
2707
  }
2708
+ var VERIFIED_TOOL_FALLBACKS = [
2709
+ "anthropic/claude-fable-5.1",
2710
+ "anthropic/claude-opus-5.5",
2711
+ "anthropic/claude-sonnet-5.5",
2712
+ "google/gemini-3.8-flash",
2713
+ "openai/gpt-5.1",
2714
+ "openai/gpt-6-astra",
2715
+ "openai/gpt-6-luna",
2716
+ "openai/gpt-6-sol",
2717
+ "xai/grok-4.6",
2718
+ "xai/grok-4.7",
2719
+ "xiaomi/mimo-v2.5"
2720
+ ];
2548
2721
  function isEligible(modelId, features, maxOutputTokens, options) {
2549
2722
  const model = options.modelCapabilities?.[modelId] ?? DEFAULT_MODEL_CAPABILITIES[modelId];
2550
2723
  if (!model) return true;
@@ -2579,27 +2752,38 @@ function profileScore(modelId, options, now) {
2579
2752
  }
2580
2753
  var PortfolioStrategy = class {
2581
2754
  name = "portfolio";
2755
+ // Rules set the capability tier; the strategy is stateless, so one instance serves every request.
2756
+ rules = new RulesStrategy();
2582
2757
  route(prompt, systemPrompt, maxOutputTokens, options) {
2583
2758
  const features = classifyTask(prompt, systemPrompt, options);
2584
- const base = new RulesStrategy().route(prompt, systemPrompt, maxOutputTokens, {
2759
+ const base = this.rules.route(prompt, systemPrompt, maxOutputTokens, {
2585
2760
  ...options,
2586
2761
  requiresTools: features.needsTools
2587
2762
  });
2588
2763
  const tierConfigs = base.tierConfigs;
2589
2764
  if (!tierConfigs) return base;
2590
- const targetTier = (features.taskType === "reasoning_mcq" || features.taskType === "reasoning_math") && (base.tier === "SIMPLE" || base.tier === "MEDIUM") ? "REASONING" : base.tier;
2765
+ const targetTier = (features.taskType === "reasoning_mcq" || features.taskType === "reasoning_math") && (base.tier === "SIMPLE" || base.tier === "MEDIUM") ? "REASONING" : (features.taskType === "debug" || features.taskType === "code_edit") && features.codeArtifact && base.tier === "SIMPLE" ? "MEDIUM" : base.tier;
2591
2766
  const tierConfig = tierConfigs[targetTier];
2592
2767
  const configuredCandidates = tierConfig ? getFallbackChain(targetTier, tierConfigs) : [];
2593
2768
  const unavailable = new Set(options.unavailableModels ?? []);
2769
+ const verifiedToolFallbacks = features.needsTools ? VERIFIED_TOOL_FALLBACKS.filter((model2) => options.modelPricing.has(model2)) : [];
2770
+ if (options.routingProfile !== "premium") {
2771
+ const fallbackCost = (model2) => estimatedCost(model2, options, features.estimatedInputTokens, maxOutputTokens);
2772
+ verifiedToolFallbacks.sort((a, b) => fallbackCost(a) - fallbackCost(b));
2773
+ }
2594
2774
  const chain = [
2595
- .../* @__PURE__ */ new Set([...configuredCandidates, ...evidenceCandidates(features.taskType)])
2775
+ .../* @__PURE__ */ new Set([
2776
+ ...configuredCandidates,
2777
+ ...evidenceCandidates(features.taskType),
2778
+ ...verifiedToolFallbacks
2779
+ ])
2596
2780
  ].filter(
2597
2781
  (model2) => typeof model2 === "string" && model2.length > 0 && !unavailable.has(model2)
2598
2782
  );
2599
- const eligible = chain.filter(
2600
- (model2) => isEligible(model2, features, maxOutputTokens, options)
2783
+ const eligible = new Set(
2784
+ chain.filter((model2) => isEligible(model2, features, maxOutputTokens, options))
2601
2785
  );
2602
- const eligibleCandidates = eligible.length > 0 ? eligible : chain;
2786
+ const eligibleCandidates = eligible.size > 0 ? [...eligible] : chain;
2603
2787
  if (eligibleCandidates.length === 0) return base;
2604
2788
  const profileName = options.routingProfile === "eco" ? "eco" : options.routingProfile === "premium" ? "premium" : "auto";
2605
2789
  const portfolio = options.config.portfolio ?? DEFAULT_PORTFOLIO_WEIGHTS;
@@ -2634,11 +2818,11 @@ var PortfolioStrategy = class {
2634
2818
  const rankedEntries = candidates.map((model2, index) => {
2635
2819
  const cost = estimatedCost(model2, options, features.estimatedInputTokens, maxOutputTokens);
2636
2820
  const costScore = Number.isFinite(cost) && maxCost > minCost ? 1 - (cost - minCost) / (maxCost - minCost) : 0.5;
2637
- const capabilityScore = isEligible(model2, features, maxOutputTokens, options) ? 1 : 0;
2821
+ const capabilityScore = eligible.has(model2) ? 1 : 0;
2638
2822
  const profile = profileScore(model2, options, now);
2639
2823
  const observedQuality = profile?.quality === void 0 ? getAffinity(model2) : getAffinity(model2) * (1 - profile.freshness) + profile.quality * profile.freshness;
2640
- const observedSpeed = profile ? profile.speed * profile.freshness : 0.5;
2641
- const observedTailSpeed = profile ? profile.tailSpeed * profile.freshness : 0.5;
2824
+ const observedSpeed = profile ? profile.speed * profile.freshness + 0.5 * (1 - profile.freshness) : 0.5;
2825
+ const observedTailSpeed = profile ? profile.tailSpeed * profile.freshness + 0.5 * (1 - profile.freshness) : 0.5;
2642
2826
  const observedReliability = profile ? profile.reliability * profile.freshness + (1 - profile.freshness) : 1;
2643
2827
  const legacyScore = 1 - index / Math.max(1, candidates.length - 1);
2644
2828
  const qualityWeight = profileWeights.quality + (features.highStakes ? portfolio.highStakesBoost.quality : 0);
@@ -3895,8 +4079,13 @@ var DEFAULT_ROUTING_CONFIG = {
3895
4079
  primary: "nvidia/nemotron-3.5-lightning",
3896
4080
  // FREE — NVIDIA free tier flagship, 1M ctx
3897
4081
  fallback: [
3898
- "nvidia/nemotron-3-nano-30b",
3899
- // FREE — fastest free model (~121 tok/s)
4082
+ // Was nvidia/nemotron-3-nano-30b until blockrun delisted it on 2026-09-08
4083
+ // (NVIDIA deprovisioned it for the account — a structured per-account
4084
+ // 404). nano-omni is blockrun's own redirect target for that id, so this
4085
+ // follows the same rule as the head: the router and the gateway name the
4086
+ // same model.
4087
+ "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning",
4088
+ // FREE — nano-30b's successor, 256K ctx
3900
4089
  // The free head keeps rotting with NVIDIA's hosting (deepseek-v4-flash
3901
4090
  // 410 2026-08-12, seed-oss-36b 410 2026-08-03, gpt-oss-120b/20b 400
3902
4091
  // 2026-08-21, and on 2026-08-30 FOUR of the five visible free models at
@@ -4725,7 +4914,7 @@ function getCostSummary() {
4725
4914
  }
4726
4915
 
4727
4916
  // src/version.ts
4728
- var SDK_VERSION = "3.24.0";
4917
+ var SDK_VERSION = "3.24.2";
4729
4918
  var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
4730
4919
 
4731
4920
  // src/client.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/llm",
3
- "version": "3.24.0",
3
+ "version": "3.24.2",
4
4
  "type": "module",
5
5
  "description": "TypeScript SDK for BlockRun - every frontier model behind one OpenAI-compatible client. Pay with a BlockRun API key or per-call in USDC. Smart routing picks the cheapest capable model. No rate limits.",
6
6
  "main": "dist/index.cjs",
@@ -58,7 +58,7 @@
58
58
  },
59
59
  "devDependencies": {
60
60
  "@blockrun/core": "^0.1.0",
61
- "@blockrun/router-core": "https://codeload.github.com/BlockRunAI/router-core/tar.gz/814fd3b6bc7880a84bbe030bd08c98e8670031e7",
61
+ "@blockrun/router-core": "https://codeload.github.com/BlockRunAI/router-core/tar.gz/65a92c85ea1f187a64e7611302ef6add236b0cba",
62
62
  "@eslint/js": "^10.0.1",
63
63
  "@solana/kit": "^5.5.1",
64
64
  "@types/node": "^26.4.1",