@blockrun/llm 3.24.1 → 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 +111 -43
  2. package/dist/index.js +111 -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+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";
@@ -2241,16 +2310,10 @@ function likelyNeedsParallelToolCalls(prompt, needsTools, toolCount, toolNames)
2241
2310
  }
2242
2311
  function classifyTask(prompt, systemPrompt, options) {
2243
2312
  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 ?? "");
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);
2254
2317
  const scannedFullText = `${scannedSystemPrompt} ${scannedPrompt}`;
2255
2318
  const text = scannedPrompt.toLowerCase();
2256
2319
  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 +2325,9 @@ ${value.slice(-(scanLimit - prefixLength))}`;
2262
2325
  const nativeCodeSignal = /\b(?:programmed|written|implemented?|code)\s+(?:in|using)\s+(?:c\+\+|c|rust|go)\b/i.test(
2263
2326
  scannedPrompt
2264
2327
  );
2265
- 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;
2266
2331
  const toolsAvailable = options.hasTools ?? false;
2267
2332
  const needsTools = options.requiresTools ?? (toolsAvailable && inferToolRequirement(scannedPrompt, scannedSystemPrompt));
2268
2333
  const likelyParallelToolCalls = likelyNeedsParallelToolCalls(
@@ -2334,10 +2399,10 @@ ${value.slice(-(scanLimit - prefixLength))}`;
2334
2399
  const needsVision = options.hasVision ?? false;
2335
2400
  const needsStructuredOutput = options.requiresStructuredOutput ?? false;
2336
2401
  const latencySensitive = /\b(?:urgent|asap|fast|quick|low latency|real[- ]time)\b|尽快|马上|快速|低延迟/i.test(
2337
- scannedFullText
2402
+ scannedPrompt
2338
2403
  );
2339
2404
  const highStakes = /\b(?:production|security|payment|legal|medical|financial|audit)\b|生产|安全|支付|法律|医疗|财务|审计/i.test(
2340
- scannedFullText
2405
+ scannedPrompt
2341
2406
  );
2342
2407
  const terminalToolSignal = normalizedToolNames.some(
2343
2408
  (name) => /^(?:terminalexec|terminalinspect|terminalsendkeys)$/.test(name)
@@ -2373,7 +2438,7 @@ ${value.slice(-(scanLimit - prefixLength))}`;
2373
2438
  );
2374
2439
  const terminalSafetySensitive = terminalToolSignal && (highStakes || terminalCredentialSignal);
2375
2440
  const implicitTerminalCode = needsTools && terminalToolSignal && agentRisk === "standard" && !highStakes && !complexTerminalOperation && numberedSteps < 3 && prompt.length <= 1e3 && simpleTerminalArtifact;
2376
- const language = /[\u3400-\u9fff]/.test(scannedFullText) ? "zh" : "other";
2441
+ const language = /[\u3400-\u9fff]/.test(scannedPrompt) ? "zh" : "other";
2377
2442
  const multipleChoiceSignals = (scannedPrompt.match(/(?:^|\n)\s*[A-D][.)]\s+/gim) ?? []).length;
2378
2443
  const numericSignals = (scannedPrompt.match(/-?\d+(?:[.,]\d+)?/g) ?? []).length;
2379
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(
@@ -2388,7 +2453,7 @@ ${value.slice(-(scanLimit - prefixLength))}`;
2388
2453
  else if (needsTools) taskType = "tool_agent";
2389
2454
  else if (multipleChoiceSignals >= 3) taskType = "reasoning_mcq";
2390
2455
  else if (compactMathProblem) taskType = "reasoning_math";
2391
- 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))
2392
2457
  taskType = "debug";
2393
2458
  else if (hasCode || /\b(?:refactor|implement|patch|edit|rewrite|重构|实现|修改)\b/i.test(text))
2394
2459
  taskType = "code_edit";
@@ -2414,7 +2479,8 @@ ${value.slice(-(scanLimit - prefixLength))}`;
2414
2479
  agentRisk,
2415
2480
  terminalToolSignal,
2416
2481
  terminalSafetySensitive,
2417
- implicitTerminalCode
2482
+ implicitTerminalCode,
2483
+ codeArtifact
2418
2484
  };
2419
2485
  }
2420
2486
  function affinity(modelId, task, language = "other", agentDomain = "other", deepWebResearch = false, agentRisk = "standard", terminalToolSignal = false, terminalSafetySensitive = false) {
@@ -2817,15 +2883,17 @@ function profileScore(modelId, options, now) {
2817
2883
  }
2818
2884
  var PortfolioStrategy = class {
2819
2885
  name = "portfolio";
2886
+ // Rules set the capability tier; the strategy is stateless, so one instance serves every request.
2887
+ rules = new RulesStrategy();
2820
2888
  route(prompt, systemPrompt, maxOutputTokens, options) {
2821
2889
  const features = classifyTask(prompt, systemPrompt, options);
2822
- const base = new RulesStrategy().route(prompt, systemPrompt, maxOutputTokens, {
2890
+ const base = this.rules.route(prompt, systemPrompt, maxOutputTokens, {
2823
2891
  ...options,
2824
2892
  requiresTools: features.needsTools
2825
2893
  });
2826
2894
  const tierConfigs = base.tierConfigs;
2827
2895
  if (!tierConfigs) return base;
2828
- 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;
2829
2897
  const tierConfig = tierConfigs[targetTier];
2830
2898
  const configuredCandidates = tierConfig ? getFallbackChain(targetTier, tierConfigs) : [];
2831
2899
  const unavailable = new Set(options.unavailableModels ?? []);
@@ -2843,10 +2911,10 @@ var PortfolioStrategy = class {
2843
2911
  ].filter(
2844
2912
  (model2) => typeof model2 === "string" && model2.length > 0 && !unavailable.has(model2)
2845
2913
  );
2846
- const eligible = chain.filter(
2847
- (model2) => isEligible(model2, features, maxOutputTokens, options)
2914
+ const eligible = new Set(
2915
+ chain.filter((model2) => isEligible(model2, features, maxOutputTokens, options))
2848
2916
  );
2849
- const eligibleCandidates = eligible.length > 0 ? eligible : chain;
2917
+ const eligibleCandidates = eligible.size > 0 ? [...eligible] : chain;
2850
2918
  if (eligibleCandidates.length === 0) return base;
2851
2919
  const profileName = options.routingProfile === "eco" ? "eco" : options.routingProfile === "premium" ? "premium" : "auto";
2852
2920
  const portfolio = options.config.portfolio ?? DEFAULT_PORTFOLIO_WEIGHTS;
@@ -2881,11 +2949,11 @@ var PortfolioStrategy = class {
2881
2949
  const rankedEntries = candidates.map((model2, index) => {
2882
2950
  const cost = estimatedCost(model2, options, features.estimatedInputTokens, maxOutputTokens);
2883
2951
  const costScore = Number.isFinite(cost) && maxCost > minCost ? 1 - (cost - minCost) / (maxCost - minCost) : 0.5;
2884
- const capabilityScore = isEligible(model2, features, maxOutputTokens, options) ? 1 : 0;
2952
+ const capabilityScore = eligible.has(model2) ? 1 : 0;
2885
2953
  const profile = profileScore(model2, options, now);
2886
2954
  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;
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;
2889
2957
  const observedReliability = profile ? profile.reliability * profile.freshness + (1 - profile.freshness) : 1;
2890
2958
  const legacyScore = 1 - index / Math.max(1, candidates.length - 1);
2891
2959
  const qualityWeight = profileWeights.quality + (features.highStakes ? portfolio.highStakesBoost.quality : 0);
@@ -4977,7 +5045,7 @@ function getCostSummary() {
4977
5045
  }
4978
5046
 
4979
5047
  // src/version.ts
4980
- var SDK_VERSION = "3.24.1";
5048
+ var SDK_VERSION = "3.24.2";
4981
5049
  var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
4982
5050
 
4983
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+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+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";
@@ -2110,16 +2179,10 @@ function likelyNeedsParallelToolCalls(prompt, needsTools, toolCount, toolNames)
2110
2179
  }
2111
2180
  function classifyTask(prompt, systemPrompt, options) {
2112
2181
  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 ?? "");
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);
2123
2186
  const scannedFullText = `${scannedSystemPrompt} ${scannedPrompt}`;
2124
2187
  const text = scannedPrompt.toLowerCase();
2125
2188
  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 +2194,9 @@ ${value.slice(-(scanLimit - prefixLength))}`;
2131
2194
  const nativeCodeSignal = /\b(?:programmed|written|implemented?|code)\s+(?:in|using)\s+(?:c\+\+|c|rust|go)\b/i.test(
2132
2195
  scannedPrompt
2133
2196
  );
2134
- 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;
2135
2200
  const toolsAvailable = options.hasTools ?? false;
2136
2201
  const needsTools = options.requiresTools ?? (toolsAvailable && inferToolRequirement(scannedPrompt, scannedSystemPrompt));
2137
2202
  const likelyParallelToolCalls = likelyNeedsParallelToolCalls(
@@ -2203,10 +2268,10 @@ ${value.slice(-(scanLimit - prefixLength))}`;
2203
2268
  const needsVision = options.hasVision ?? false;
2204
2269
  const needsStructuredOutput = options.requiresStructuredOutput ?? false;
2205
2270
  const latencySensitive = /\b(?:urgent|asap|fast|quick|low latency|real[- ]time)\b|尽快|马上|快速|低延迟/i.test(
2206
- scannedFullText
2271
+ scannedPrompt
2207
2272
  );
2208
2273
  const highStakes = /\b(?:production|security|payment|legal|medical|financial|audit)\b|生产|安全|支付|法律|医疗|财务|审计/i.test(
2209
- scannedFullText
2274
+ scannedPrompt
2210
2275
  );
2211
2276
  const terminalToolSignal = normalizedToolNames.some(
2212
2277
  (name) => /^(?:terminalexec|terminalinspect|terminalsendkeys)$/.test(name)
@@ -2242,7 +2307,7 @@ ${value.slice(-(scanLimit - prefixLength))}`;
2242
2307
  );
2243
2308
  const terminalSafetySensitive = terminalToolSignal && (highStakes || terminalCredentialSignal);
2244
2309
  const implicitTerminalCode = needsTools && terminalToolSignal && agentRisk === "standard" && !highStakes && !complexTerminalOperation && numberedSteps < 3 && prompt.length <= 1e3 && simpleTerminalArtifact;
2245
- const language = /[\u3400-\u9fff]/.test(scannedFullText) ? "zh" : "other";
2310
+ const language = /[\u3400-\u9fff]/.test(scannedPrompt) ? "zh" : "other";
2246
2311
  const multipleChoiceSignals = (scannedPrompt.match(/(?:^|\n)\s*[A-D][.)]\s+/gim) ?? []).length;
2247
2312
  const numericSignals = (scannedPrompt.match(/-?\d+(?:[.,]\d+)?/g) ?? []).length;
2248
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(
@@ -2257,7 +2322,7 @@ ${value.slice(-(scanLimit - prefixLength))}`;
2257
2322
  else if (needsTools) taskType = "tool_agent";
2258
2323
  else if (multipleChoiceSignals >= 3) taskType = "reasoning_mcq";
2259
2324
  else if (compactMathProblem) taskType = "reasoning_math";
2260
- 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))
2261
2326
  taskType = "debug";
2262
2327
  else if (hasCode || /\b(?:refactor|implement|patch|edit|rewrite|重构|实现|修改)\b/i.test(text))
2263
2328
  taskType = "code_edit";
@@ -2283,7 +2348,8 @@ ${value.slice(-(scanLimit - prefixLength))}`;
2283
2348
  agentRisk,
2284
2349
  terminalToolSignal,
2285
2350
  terminalSafetySensitive,
2286
- implicitTerminalCode
2351
+ implicitTerminalCode,
2352
+ codeArtifact
2287
2353
  };
2288
2354
  }
2289
2355
  function affinity(modelId, task, language = "other", agentDomain = "other", deepWebResearch = false, agentRisk = "standard", terminalToolSignal = false, terminalSafetySensitive = false) {
@@ -2686,15 +2752,17 @@ function profileScore(modelId, options, now) {
2686
2752
  }
2687
2753
  var PortfolioStrategy = class {
2688
2754
  name = "portfolio";
2755
+ // Rules set the capability tier; the strategy is stateless, so one instance serves every request.
2756
+ rules = new RulesStrategy();
2689
2757
  route(prompt, systemPrompt, maxOutputTokens, options) {
2690
2758
  const features = classifyTask(prompt, systemPrompt, options);
2691
- const base = new RulesStrategy().route(prompt, systemPrompt, maxOutputTokens, {
2759
+ const base = this.rules.route(prompt, systemPrompt, maxOutputTokens, {
2692
2760
  ...options,
2693
2761
  requiresTools: features.needsTools
2694
2762
  });
2695
2763
  const tierConfigs = base.tierConfigs;
2696
2764
  if (!tierConfigs) return base;
2697
- 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;
2698
2766
  const tierConfig = tierConfigs[targetTier];
2699
2767
  const configuredCandidates = tierConfig ? getFallbackChain(targetTier, tierConfigs) : [];
2700
2768
  const unavailable = new Set(options.unavailableModels ?? []);
@@ -2712,10 +2780,10 @@ var PortfolioStrategy = class {
2712
2780
  ].filter(
2713
2781
  (model2) => typeof model2 === "string" && model2.length > 0 && !unavailable.has(model2)
2714
2782
  );
2715
- const eligible = chain.filter(
2716
- (model2) => isEligible(model2, features, maxOutputTokens, options)
2783
+ const eligible = new Set(
2784
+ chain.filter((model2) => isEligible(model2, features, maxOutputTokens, options))
2717
2785
  );
2718
- const eligibleCandidates = eligible.length > 0 ? eligible : chain;
2786
+ const eligibleCandidates = eligible.size > 0 ? [...eligible] : chain;
2719
2787
  if (eligibleCandidates.length === 0) return base;
2720
2788
  const profileName = options.routingProfile === "eco" ? "eco" : options.routingProfile === "premium" ? "premium" : "auto";
2721
2789
  const portfolio = options.config.portfolio ?? DEFAULT_PORTFOLIO_WEIGHTS;
@@ -2750,11 +2818,11 @@ var PortfolioStrategy = class {
2750
2818
  const rankedEntries = candidates.map((model2, index) => {
2751
2819
  const cost = estimatedCost(model2, options, features.estimatedInputTokens, maxOutputTokens);
2752
2820
  const costScore = Number.isFinite(cost) && maxCost > minCost ? 1 - (cost - minCost) / (maxCost - minCost) : 0.5;
2753
- const capabilityScore = isEligible(model2, features, maxOutputTokens, options) ? 1 : 0;
2821
+ const capabilityScore = eligible.has(model2) ? 1 : 0;
2754
2822
  const profile = profileScore(model2, options, now);
2755
2823
  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;
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;
2758
2826
  const observedReliability = profile ? profile.reliability * profile.freshness + (1 - profile.freshness) : 1;
2759
2827
  const legacyScore = 1 - index / Math.max(1, candidates.length - 1);
2760
2828
  const qualityWeight = profileWeights.quality + (features.highStakes ? portfolio.highStakesBoost.quality : 0);
@@ -4846,7 +4914,7 @@ function getCostSummary() {
4846
4914
  }
4847
4915
 
4848
4916
  // src/version.ts
4849
- var SDK_VERSION = "3.24.1";
4917
+ var SDK_VERSION = "3.24.2";
4850
4918
  var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
4851
4919
 
4852
4920
  // 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.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/27125a3dfe319a6f422a0c0f41bf1666b0dc61a9",
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",