@blockrun/llm 3.24.1 → 3.24.3

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 +132 -43
  2. package/dist/index.js +132 -43
  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+27125a3dfe319_a6zjl5xcggrab43wsmb4mvkqym/node_modules/@blockrun/router-core/dist/index.js
574
+ // node_modules/.pnpm/@blockrun+router-core@https+++codeload.github.com+BlockRunAI+router-core+tar.gz+2cd5a1026f755_easf3bov3hzdfoup33nwewqudy/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";
@@ -1143,6 +1212,13 @@ var DEFAULT_MODEL_CAPABILITIES = Object.freeze({
1143
1212
  supportsTools: true,
1144
1213
  supportsVision: false
1145
1214
  },
1215
+ "deepseek/deepseek-v4.1-flash": {
1216
+ // supportsTools: not probed — fails closed
1217
+ contextWindow: 1048576,
1218
+ maxOutputTokens: 65536,
1219
+ supportsTools: false,
1220
+ supportsVision: false
1221
+ },
1146
1222
  "google/gemini-2.5-flash": {
1147
1223
  contextWindow: 1048576,
1148
1224
  maxOutputTokens: 65536,
@@ -1215,6 +1291,13 @@ var DEFAULT_MODEL_CAPABILITIES = Object.freeze({
1215
1291
  supportsTools: true,
1216
1292
  supportsVision: false
1217
1293
  },
1294
+ "mistral/mistral-large-4": {
1295
+ // supportsTools: not probed — fails closed
1296
+ contextWindow: 524288,
1297
+ maxOutputTokens: 262144,
1298
+ supportsTools: false,
1299
+ supportsVision: true
1300
+ },
1218
1301
  "moonshot/kimi-k3": {
1219
1302
  contextWindow: 1048576,
1220
1303
  maxOutputTokens: 65536,
@@ -1416,6 +1499,13 @@ var DEFAULT_MODEL_CAPABILITIES = Object.freeze({
1416
1499
  supportsTools: true,
1417
1500
  supportsVision: true
1418
1501
  },
1502
+ "openai/gpt-6.1-sol": {
1503
+ // supportsTools: not probed — fails closed
1504
+ contextWindow: 105e4,
1505
+ maxOutputTokens: 128e3,
1506
+ supportsTools: false,
1507
+ supportsVision: true
1508
+ },
1419
1509
  "openai/o1": {
1420
1510
  contextWindow: 2e5,
1421
1511
  maxOutputTokens: 1e5,
@@ -2241,16 +2331,10 @@ function likelyNeedsParallelToolCalls(prompt, needsTools, toolCount, toolNames)
2241
2331
  }
2242
2332
  function classifyTask(prompt, systemPrompt, options) {
2243
2333
  const fullText = `${systemPrompt ?? ""} ${prompt}`;
2244
- const estimatedInputTokens = Math.ceil(fullText.length / 4);
2245
- const scanLimit = Math.max(1, Math.min(8e3, options.config.classifier.promptTruncationChars));
2246
- const sample = (value) => {
2247
- if (value.length <= scanLimit) return value;
2248
- const prefixLength = Math.ceil(scanLimit / 2);
2249
- return `${value.slice(0, prefixLength)}
2250
- ${value.slice(-(scanLimit - prefixLength))}`;
2251
- };
2252
- const scannedPrompt = sample(prompt);
2253
- const scannedSystemPrompt = sample(systemPrompt ?? "");
2334
+ const estimatedInputTokens = estimateTokens(fullText);
2335
+ const limit = scanLimit(options.config.classifier.promptTruncationChars);
2336
+ const scannedPrompt = sampleForScan(prompt, limit);
2337
+ const scannedSystemPrompt = sampleForScan(systemPrompt ?? "", limit);
2254
2338
  const scannedFullText = `${scannedSystemPrompt} ${scannedPrompt}`;
2255
2339
  const text = scannedPrompt.toLowerCase();
2256
2340
  const explicitCodeSignal = /```|\b(?:typescript|javascript|python|rust|java|sql|stack trace|traceback|exception)\b|\.(?:ts|tsx|js|py|go|rs)\b/i.test(
@@ -2262,7 +2346,9 @@ ${value.slice(-(scanLimit - prefixLength))}`;
2262
2346
  const nativeCodeSignal = /\b(?:programmed|written|implemented?|code)\s+(?:in|using)\s+(?:c\+\+|c|rust|go)\b/i.test(
2263
2347
  scannedPrompt
2264
2348
  );
2265
- const hasCode = explicitCodeSignal || codeConstructSignal || nativeCodeSignal;
2349
+ const errorClassSignal = /\b[A-Z][A-Za-z]*(?:Error|Exception)\b/.test(scannedPrompt);
2350
+ const hasCode = explicitCodeSignal || codeConstructSignal || nativeCodeSignal || errorClassSignal;
2351
+ const codeArtifact = /```|\b(?:stack trace|traceback)\b|\.(?:ts|tsx|js|py|go|rs)\b/i.test(scannedPrompt) || codeConstructSignal || nativeCodeSignal || errorClassSignal;
2266
2352
  const toolsAvailable = options.hasTools ?? false;
2267
2353
  const needsTools = options.requiresTools ?? (toolsAvailable && inferToolRequirement(scannedPrompt, scannedSystemPrompt));
2268
2354
  const likelyParallelToolCalls = likelyNeedsParallelToolCalls(
@@ -2334,10 +2420,10 @@ ${value.slice(-(scanLimit - prefixLength))}`;
2334
2420
  const needsVision = options.hasVision ?? false;
2335
2421
  const needsStructuredOutput = options.requiresStructuredOutput ?? false;
2336
2422
  const latencySensitive = /\b(?:urgent|asap|fast|quick|low latency|real[- ]time)\b|尽快|马上|快速|低延迟/i.test(
2337
- scannedFullText
2423
+ scannedPrompt
2338
2424
  );
2339
2425
  const highStakes = /\b(?:production|security|payment|legal|medical|financial|audit)\b|生产|安全|支付|法律|医疗|财务|审计/i.test(
2340
- scannedFullText
2426
+ scannedPrompt
2341
2427
  );
2342
2428
  const terminalToolSignal = normalizedToolNames.some(
2343
2429
  (name) => /^(?:terminalexec|terminalinspect|terminalsendkeys)$/.test(name)
@@ -2373,7 +2459,7 @@ ${value.slice(-(scanLimit - prefixLength))}`;
2373
2459
  );
2374
2460
  const terminalSafetySensitive = terminalToolSignal && (highStakes || terminalCredentialSignal);
2375
2461
  const implicitTerminalCode = needsTools && terminalToolSignal && agentRisk === "standard" && !highStakes && !complexTerminalOperation && numberedSteps < 3 && prompt.length <= 1e3 && simpleTerminalArtifact;
2376
- const language = /[\u3400-\u9fff]/.test(scannedFullText) ? "zh" : "other";
2462
+ const language = /[\u3400-\u9fff]/.test(scannedPrompt) ? "zh" : "other";
2377
2463
  const multipleChoiceSignals = (scannedPrompt.match(/(?:^|\n)\s*[A-D][.)]\s+/gim) ?? []).length;
2378
2464
  const numericSignals = (scannedPrompt.match(/-?\d+(?:[.,]\d+)?/g) ?? []).length;
2379
2465
  const compactMathProblem = !hasCode && prompt.length < 2500 && numericSignals >= 2 && (/[+×÷=%$€£¥]|\b(?:total|each|per|times|half|twice|percent|how many|how much|calculate)\b/i.test(
@@ -2388,7 +2474,7 @@ ${value.slice(-(scanLimit - prefixLength))}`;
2388
2474
  else if (needsTools) taskType = "tool_agent";
2389
2475
  else if (multipleChoiceSignals >= 3) taskType = "reasoning_mcq";
2390
2476
  else if (compactMathProblem) taskType = "reasoning_math";
2391
- else if (/\b(?:bug|debug|error|failure|failing|regression|crash|修复|报错|错误|调试)\b/i.test(text))
2477
+ else if (errorClassSignal || /\b(?:bug|debug|error|failure|failing|regression|crash|修复|报错|错误|调试)\b/i.test(text))
2392
2478
  taskType = "debug";
2393
2479
  else if (hasCode || /\b(?:refactor|implement|patch|edit|rewrite|重构|实现|修改)\b/i.test(text))
2394
2480
  taskType = "code_edit";
@@ -2414,7 +2500,8 @@ ${value.slice(-(scanLimit - prefixLength))}`;
2414
2500
  agentRisk,
2415
2501
  terminalToolSignal,
2416
2502
  terminalSafetySensitive,
2417
- implicitTerminalCode
2503
+ implicitTerminalCode,
2504
+ codeArtifact
2418
2505
  };
2419
2506
  }
2420
2507
  function affinity(modelId, task, language = "other", agentDomain = "other", deepWebResearch = false, agentRisk = "standard", terminalToolSignal = false, terminalSafetySensitive = false) {
@@ -2817,15 +2904,17 @@ function profileScore(modelId, options, now) {
2817
2904
  }
2818
2905
  var PortfolioStrategy = class {
2819
2906
  name = "portfolio";
2907
+ // Rules set the capability tier; the strategy is stateless, so one instance serves every request.
2908
+ rules = new RulesStrategy();
2820
2909
  route(prompt, systemPrompt, maxOutputTokens, options) {
2821
2910
  const features = classifyTask(prompt, systemPrompt, options);
2822
- const base = new RulesStrategy().route(prompt, systemPrompt, maxOutputTokens, {
2911
+ const base = this.rules.route(prompt, systemPrompt, maxOutputTokens, {
2823
2912
  ...options,
2824
2913
  requiresTools: features.needsTools
2825
2914
  });
2826
2915
  const tierConfigs = base.tierConfigs;
2827
2916
  if (!tierConfigs) return base;
2828
- const targetTier = (features.taskType === "reasoning_mcq" || features.taskType === "reasoning_math") && (base.tier === "SIMPLE" || base.tier === "MEDIUM") ? "REASONING" : base.tier;
2917
+ 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;
2829
2918
  const tierConfig = tierConfigs[targetTier];
2830
2919
  const configuredCandidates = tierConfig ? getFallbackChain(targetTier, tierConfigs) : [];
2831
2920
  const unavailable = new Set(options.unavailableModels ?? []);
@@ -2843,10 +2932,10 @@ var PortfolioStrategy = class {
2843
2932
  ].filter(
2844
2933
  (model2) => typeof model2 === "string" && model2.length > 0 && !unavailable.has(model2)
2845
2934
  );
2846
- const eligible = chain.filter(
2847
- (model2) => isEligible(model2, features, maxOutputTokens, options)
2935
+ const eligible = new Set(
2936
+ chain.filter((model2) => isEligible(model2, features, maxOutputTokens, options))
2848
2937
  );
2849
- const eligibleCandidates = eligible.length > 0 ? eligible : chain;
2938
+ const eligibleCandidates = eligible.size > 0 ? [...eligible] : chain;
2850
2939
  if (eligibleCandidates.length === 0) return base;
2851
2940
  const profileName = options.routingProfile === "eco" ? "eco" : options.routingProfile === "premium" ? "premium" : "auto";
2852
2941
  const portfolio = options.config.portfolio ?? DEFAULT_PORTFOLIO_WEIGHTS;
@@ -2881,11 +2970,11 @@ var PortfolioStrategy = class {
2881
2970
  const rankedEntries = candidates.map((model2, index) => {
2882
2971
  const cost = estimatedCost(model2, options, features.estimatedInputTokens, maxOutputTokens);
2883
2972
  const costScore = Number.isFinite(cost) && maxCost > minCost ? 1 - (cost - minCost) / (maxCost - minCost) : 0.5;
2884
- const capabilityScore = isEligible(model2, features, maxOutputTokens, options) ? 1 : 0;
2973
+ const capabilityScore = eligible.has(model2) ? 1 : 0;
2885
2974
  const profile = profileScore(model2, options, now);
2886
2975
  const observedQuality = profile?.quality === void 0 ? getAffinity(model2) : getAffinity(model2) * (1 - profile.freshness) + profile.quality * profile.freshness;
2887
- const observedSpeed = profile ? profile.speed * profile.freshness : 0.5;
2888
- const observedTailSpeed = profile ? profile.tailSpeed * profile.freshness : 0.5;
2976
+ const observedSpeed = profile ? profile.speed * profile.freshness + 0.5 * (1 - profile.freshness) : 0.5;
2977
+ const observedTailSpeed = profile ? profile.tailSpeed * profile.freshness + 0.5 * (1 - profile.freshness) : 0.5;
2889
2978
  const observedReliability = profile ? profile.reliability * profile.freshness + (1 - profile.freshness) : 1;
2890
2979
  const legacyScore = 1 - index / Math.max(1, candidates.length - 1);
2891
2980
  const qualityWeight = profileWeights.quality + (features.highStakes ? portfolio.highStakesBoost.quality : 0);
@@ -4977,7 +5066,7 @@ function getCostSummary() {
4977
5066
  }
4978
5067
 
4979
5068
  // src/version.ts
4980
- var SDK_VERSION = "3.24.1";
5069
+ var SDK_VERSION = "3.24.3";
4981
5070
  var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
4982
5071
 
4983
5072
  // 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+27125a3dfe319_a6zjl5xcggrab43wsmb4mvkqym/node_modules/@blockrun/router-core/dist/index.js
443
+ // node_modules/.pnpm/@blockrun+router-core@https+++codeload.github.com+BlockRunAI+router-core+tar.gz+2cd5a1026f755_easf3bov3hzdfoup33nwewqudy/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";
@@ -1012,6 +1081,13 @@ var DEFAULT_MODEL_CAPABILITIES = Object.freeze({
1012
1081
  supportsTools: true,
1013
1082
  supportsVision: false
1014
1083
  },
1084
+ "deepseek/deepseek-v4.1-flash": {
1085
+ // supportsTools: not probed — fails closed
1086
+ contextWindow: 1048576,
1087
+ maxOutputTokens: 65536,
1088
+ supportsTools: false,
1089
+ supportsVision: false
1090
+ },
1015
1091
  "google/gemini-2.5-flash": {
1016
1092
  contextWindow: 1048576,
1017
1093
  maxOutputTokens: 65536,
@@ -1084,6 +1160,13 @@ var DEFAULT_MODEL_CAPABILITIES = Object.freeze({
1084
1160
  supportsTools: true,
1085
1161
  supportsVision: false
1086
1162
  },
1163
+ "mistral/mistral-large-4": {
1164
+ // supportsTools: not probed — fails closed
1165
+ contextWindow: 524288,
1166
+ maxOutputTokens: 262144,
1167
+ supportsTools: false,
1168
+ supportsVision: true
1169
+ },
1087
1170
  "moonshot/kimi-k3": {
1088
1171
  contextWindow: 1048576,
1089
1172
  maxOutputTokens: 65536,
@@ -1285,6 +1368,13 @@ var DEFAULT_MODEL_CAPABILITIES = Object.freeze({
1285
1368
  supportsTools: true,
1286
1369
  supportsVision: true
1287
1370
  },
1371
+ "openai/gpt-6.1-sol": {
1372
+ // supportsTools: not probed — fails closed
1373
+ contextWindow: 105e4,
1374
+ maxOutputTokens: 128e3,
1375
+ supportsTools: false,
1376
+ supportsVision: true
1377
+ },
1288
1378
  "openai/o1": {
1289
1379
  contextWindow: 2e5,
1290
1380
  maxOutputTokens: 1e5,
@@ -2110,16 +2200,10 @@ function likelyNeedsParallelToolCalls(prompt, needsTools, toolCount, toolNames)
2110
2200
  }
2111
2201
  function classifyTask(prompt, systemPrompt, options) {
2112
2202
  const fullText = `${systemPrompt ?? ""} ${prompt}`;
2113
- const estimatedInputTokens = Math.ceil(fullText.length / 4);
2114
- const scanLimit = Math.max(1, Math.min(8e3, options.config.classifier.promptTruncationChars));
2115
- const sample = (value) => {
2116
- if (value.length <= scanLimit) return value;
2117
- const prefixLength = Math.ceil(scanLimit / 2);
2118
- return `${value.slice(0, prefixLength)}
2119
- ${value.slice(-(scanLimit - prefixLength))}`;
2120
- };
2121
- const scannedPrompt = sample(prompt);
2122
- const scannedSystemPrompt = sample(systemPrompt ?? "");
2203
+ const estimatedInputTokens = estimateTokens(fullText);
2204
+ const limit = scanLimit(options.config.classifier.promptTruncationChars);
2205
+ const scannedPrompt = sampleForScan(prompt, limit);
2206
+ const scannedSystemPrompt = sampleForScan(systemPrompt ?? "", limit);
2123
2207
  const scannedFullText = `${scannedSystemPrompt} ${scannedPrompt}`;
2124
2208
  const text = scannedPrompt.toLowerCase();
2125
2209
  const explicitCodeSignal = /```|\b(?:typescript|javascript|python|rust|java|sql|stack trace|traceback|exception)\b|\.(?:ts|tsx|js|py|go|rs)\b/i.test(
@@ -2131,7 +2215,9 @@ ${value.slice(-(scanLimit - prefixLength))}`;
2131
2215
  const nativeCodeSignal = /\b(?:programmed|written|implemented?|code)\s+(?:in|using)\s+(?:c\+\+|c|rust|go)\b/i.test(
2132
2216
  scannedPrompt
2133
2217
  );
2134
- const hasCode = explicitCodeSignal || codeConstructSignal || nativeCodeSignal;
2218
+ const errorClassSignal = /\b[A-Z][A-Za-z]*(?:Error|Exception)\b/.test(scannedPrompt);
2219
+ const hasCode = explicitCodeSignal || codeConstructSignal || nativeCodeSignal || errorClassSignal;
2220
+ const codeArtifact = /```|\b(?:stack trace|traceback)\b|\.(?:ts|tsx|js|py|go|rs)\b/i.test(scannedPrompt) || codeConstructSignal || nativeCodeSignal || errorClassSignal;
2135
2221
  const toolsAvailable = options.hasTools ?? false;
2136
2222
  const needsTools = options.requiresTools ?? (toolsAvailable && inferToolRequirement(scannedPrompt, scannedSystemPrompt));
2137
2223
  const likelyParallelToolCalls = likelyNeedsParallelToolCalls(
@@ -2203,10 +2289,10 @@ ${value.slice(-(scanLimit - prefixLength))}`;
2203
2289
  const needsVision = options.hasVision ?? false;
2204
2290
  const needsStructuredOutput = options.requiresStructuredOutput ?? false;
2205
2291
  const latencySensitive = /\b(?:urgent|asap|fast|quick|low latency|real[- ]time)\b|尽快|马上|快速|低延迟/i.test(
2206
- scannedFullText
2292
+ scannedPrompt
2207
2293
  );
2208
2294
  const highStakes = /\b(?:production|security|payment|legal|medical|financial|audit)\b|生产|安全|支付|法律|医疗|财务|审计/i.test(
2209
- scannedFullText
2295
+ scannedPrompt
2210
2296
  );
2211
2297
  const terminalToolSignal = normalizedToolNames.some(
2212
2298
  (name) => /^(?:terminalexec|terminalinspect|terminalsendkeys)$/.test(name)
@@ -2242,7 +2328,7 @@ ${value.slice(-(scanLimit - prefixLength))}`;
2242
2328
  );
2243
2329
  const terminalSafetySensitive = terminalToolSignal && (highStakes || terminalCredentialSignal);
2244
2330
  const implicitTerminalCode = needsTools && terminalToolSignal && agentRisk === "standard" && !highStakes && !complexTerminalOperation && numberedSteps < 3 && prompt.length <= 1e3 && simpleTerminalArtifact;
2245
- const language = /[\u3400-\u9fff]/.test(scannedFullText) ? "zh" : "other";
2331
+ const language = /[\u3400-\u9fff]/.test(scannedPrompt) ? "zh" : "other";
2246
2332
  const multipleChoiceSignals = (scannedPrompt.match(/(?:^|\n)\s*[A-D][.)]\s+/gim) ?? []).length;
2247
2333
  const numericSignals = (scannedPrompt.match(/-?\d+(?:[.,]\d+)?/g) ?? []).length;
2248
2334
  const compactMathProblem = !hasCode && prompt.length < 2500 && numericSignals >= 2 && (/[+×÷=%$€£¥]|\b(?:total|each|per|times|half|twice|percent|how many|how much|calculate)\b/i.test(
@@ -2257,7 +2343,7 @@ ${value.slice(-(scanLimit - prefixLength))}`;
2257
2343
  else if (needsTools) taskType = "tool_agent";
2258
2344
  else if (multipleChoiceSignals >= 3) taskType = "reasoning_mcq";
2259
2345
  else if (compactMathProblem) taskType = "reasoning_math";
2260
- else if (/\b(?:bug|debug|error|failure|failing|regression|crash|修复|报错|错误|调试)\b/i.test(text))
2346
+ else if (errorClassSignal || /\b(?:bug|debug|error|failure|failing|regression|crash|修复|报错|错误|调试)\b/i.test(text))
2261
2347
  taskType = "debug";
2262
2348
  else if (hasCode || /\b(?:refactor|implement|patch|edit|rewrite|重构|实现|修改)\b/i.test(text))
2263
2349
  taskType = "code_edit";
@@ -2283,7 +2369,8 @@ ${value.slice(-(scanLimit - prefixLength))}`;
2283
2369
  agentRisk,
2284
2370
  terminalToolSignal,
2285
2371
  terminalSafetySensitive,
2286
- implicitTerminalCode
2372
+ implicitTerminalCode,
2373
+ codeArtifact
2287
2374
  };
2288
2375
  }
2289
2376
  function affinity(modelId, task, language = "other", agentDomain = "other", deepWebResearch = false, agentRisk = "standard", terminalToolSignal = false, terminalSafetySensitive = false) {
@@ -2686,15 +2773,17 @@ function profileScore(modelId, options, now) {
2686
2773
  }
2687
2774
  var PortfolioStrategy = class {
2688
2775
  name = "portfolio";
2776
+ // Rules set the capability tier; the strategy is stateless, so one instance serves every request.
2777
+ rules = new RulesStrategy();
2689
2778
  route(prompt, systemPrompt, maxOutputTokens, options) {
2690
2779
  const features = classifyTask(prompt, systemPrompt, options);
2691
- const base = new RulesStrategy().route(prompt, systemPrompt, maxOutputTokens, {
2780
+ const base = this.rules.route(prompt, systemPrompt, maxOutputTokens, {
2692
2781
  ...options,
2693
2782
  requiresTools: features.needsTools
2694
2783
  });
2695
2784
  const tierConfigs = base.tierConfigs;
2696
2785
  if (!tierConfigs) return base;
2697
- const targetTier = (features.taskType === "reasoning_mcq" || features.taskType === "reasoning_math") && (base.tier === "SIMPLE" || base.tier === "MEDIUM") ? "REASONING" : base.tier;
2786
+ 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;
2698
2787
  const tierConfig = tierConfigs[targetTier];
2699
2788
  const configuredCandidates = tierConfig ? getFallbackChain(targetTier, tierConfigs) : [];
2700
2789
  const unavailable = new Set(options.unavailableModels ?? []);
@@ -2712,10 +2801,10 @@ var PortfolioStrategy = class {
2712
2801
  ].filter(
2713
2802
  (model2) => typeof model2 === "string" && model2.length > 0 && !unavailable.has(model2)
2714
2803
  );
2715
- const eligible = chain.filter(
2716
- (model2) => isEligible(model2, features, maxOutputTokens, options)
2804
+ const eligible = new Set(
2805
+ chain.filter((model2) => isEligible(model2, features, maxOutputTokens, options))
2717
2806
  );
2718
- const eligibleCandidates = eligible.length > 0 ? eligible : chain;
2807
+ const eligibleCandidates = eligible.size > 0 ? [...eligible] : chain;
2719
2808
  if (eligibleCandidates.length === 0) return base;
2720
2809
  const profileName = options.routingProfile === "eco" ? "eco" : options.routingProfile === "premium" ? "premium" : "auto";
2721
2810
  const portfolio = options.config.portfolio ?? DEFAULT_PORTFOLIO_WEIGHTS;
@@ -2750,11 +2839,11 @@ var PortfolioStrategy = class {
2750
2839
  const rankedEntries = candidates.map((model2, index) => {
2751
2840
  const cost = estimatedCost(model2, options, features.estimatedInputTokens, maxOutputTokens);
2752
2841
  const costScore = Number.isFinite(cost) && maxCost > minCost ? 1 - (cost - minCost) / (maxCost - minCost) : 0.5;
2753
- const capabilityScore = isEligible(model2, features, maxOutputTokens, options) ? 1 : 0;
2842
+ const capabilityScore = eligible.has(model2) ? 1 : 0;
2754
2843
  const profile = profileScore(model2, options, now);
2755
2844
  const observedQuality = profile?.quality === void 0 ? getAffinity(model2) : getAffinity(model2) * (1 - profile.freshness) + profile.quality * profile.freshness;
2756
- const observedSpeed = profile ? profile.speed * profile.freshness : 0.5;
2757
- const observedTailSpeed = profile ? profile.tailSpeed * profile.freshness : 0.5;
2845
+ const observedSpeed = profile ? profile.speed * profile.freshness + 0.5 * (1 - profile.freshness) : 0.5;
2846
+ const observedTailSpeed = profile ? profile.tailSpeed * profile.freshness + 0.5 * (1 - profile.freshness) : 0.5;
2758
2847
  const observedReliability = profile ? profile.reliability * profile.freshness + (1 - profile.freshness) : 1;
2759
2848
  const legacyScore = 1 - index / Math.max(1, candidates.length - 1);
2760
2849
  const qualityWeight = profileWeights.quality + (features.highStakes ? portfolio.highStakesBoost.quality : 0);
@@ -4846,7 +4935,7 @@ function getCostSummary() {
4846
4935
  }
4847
4936
 
4848
4937
  // src/version.ts
4849
- var SDK_VERSION = "3.24.1";
4938
+ var SDK_VERSION = "3.24.3";
4850
4939
  var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
4851
4940
 
4852
4941
  // src/client.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/llm",
3
- "version": "3.24.1",
3
+ "version": "3.24.3",
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/27125a3dfe319a6f422a0c0f41bf1666b0dc61a9",
61
+ "@blockrun/router-core": "https://codeload.github.com/BlockRunAI/router-core/tar.gz/2cd5a1026f755cd39a3f0d6e61cab23a5ad391a9",
62
62
  "@eslint/js": "^10.0.1",
63
63
  "@solana/kit": "^5.5.1",
64
64
  "@types/node": "^26.4.1",