@blockrun/clawrouter 0.4.6 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,42 @@
1
1
  // src/models.ts
2
+ var MODEL_ALIASES = {
3
+ // Claude
4
+ claude: "anthropic/claude-sonnet-4",
5
+ sonnet: "anthropic/claude-sonnet-4",
6
+ opus: "anthropic/claude-opus-4",
7
+ haiku: "anthropic/claude-haiku-4.5",
8
+ // OpenAI
9
+ gpt: "openai/gpt-4o",
10
+ gpt4: "openai/gpt-4o",
11
+ gpt5: "openai/gpt-5.2",
12
+ mini: "openai/gpt-4o-mini",
13
+ o3: "openai/o3",
14
+ // DeepSeek
15
+ deepseek: "deepseek/deepseek-chat",
16
+ reasoner: "deepseek/deepseek-reasoner",
17
+ // Kimi / Moonshot
18
+ kimi: "moonshot/kimi-k2.5",
19
+ // Google
20
+ gemini: "google/gemini-2.5-pro",
21
+ flash: "google/gemini-2.5-flash",
22
+ // xAI
23
+ grok: "xai/grok-3",
24
+ "grok-fast": "xai/grok-4-fast-reasoning",
25
+ "grok-code": "xai/grok-code-fast-1",
26
+ // NVIDIA
27
+ "nvidia": "nvidia/gpt-oss-120b"
28
+ };
29
+ function resolveModelAlias(model) {
30
+ const normalized = model.trim().toLowerCase();
31
+ const resolved = MODEL_ALIASES[normalized];
32
+ if (resolved) return resolved;
33
+ if (normalized.startsWith("blockrun/")) {
34
+ const withoutPrefix = normalized.slice("blockrun/".length);
35
+ const resolvedWithoutPrefix = MODEL_ALIASES[withoutPrefix];
36
+ if (resolvedWithoutPrefix) return resolvedWithoutPrefix;
37
+ }
38
+ return model;
39
+ }
2
40
  var BLOCKRUN_MODELS = [
3
41
  // Smart routing meta-model — proxy replaces with actual model
4
42
  // NOTE: Model IDs are WITHOUT provider prefix (OpenClaw adds "blockrun/" automatically)
@@ -19,7 +57,8 @@ var BLOCKRUN_MODELS = [
19
57
  contextWindow: 4e5,
20
58
  maxOutput: 128e3,
21
59
  reasoning: true,
22
- vision: true
60
+ vision: true,
61
+ agentic: true
23
62
  },
24
63
  {
25
64
  id: "openai/gpt-5-mini",
@@ -79,7 +118,8 @@ var BLOCKRUN_MODELS = [
79
118
  outputPrice: 10,
80
119
  contextWindow: 128e3,
81
120
  maxOutput: 16384,
82
- vision: true
121
+ vision: true,
122
+ agentic: true
83
123
  },
84
124
  {
85
125
  id: "openai/gpt-4o-mini",
@@ -127,14 +167,15 @@ var BLOCKRUN_MODELS = [
127
167
  reasoning: true
128
168
  },
129
169
  // o4-mini: Placeholder removed - model not yet released by OpenAI
130
- // Anthropic
170
+ // Anthropic - all Claude models excel at agentic workflows
131
171
  {
132
172
  id: "anthropic/claude-haiku-4.5",
133
173
  name: "Claude Haiku 4.5",
134
174
  inputPrice: 1,
135
175
  outputPrice: 5,
136
176
  contextWindow: 2e5,
137
- maxOutput: 8192
177
+ maxOutput: 8192,
178
+ agentic: true
138
179
  },
139
180
  {
140
181
  id: "anthropic/claude-sonnet-4",
@@ -143,7 +184,8 @@ var BLOCKRUN_MODELS = [
143
184
  outputPrice: 15,
144
185
  contextWindow: 2e5,
145
186
  maxOutput: 64e3,
146
- reasoning: true
187
+ reasoning: true,
188
+ agentic: true
147
189
  },
148
190
  {
149
191
  id: "anthropic/claude-opus-4",
@@ -152,7 +194,8 @@ var BLOCKRUN_MODELS = [
152
194
  outputPrice: 75,
153
195
  contextWindow: 2e5,
154
196
  maxOutput: 32e3,
155
- reasoning: true
197
+ reasoning: true,
198
+ agentic: true
156
199
  },
157
200
  {
158
201
  id: "anthropic/claude-opus-4.5",
@@ -161,7 +204,8 @@ var BLOCKRUN_MODELS = [
161
204
  outputPrice: 25,
162
205
  contextWindow: 2e5,
163
206
  maxOutput: 32e3,
164
- reasoning: true
207
+ reasoning: true,
208
+ agentic: true
165
209
  },
166
210
  // Google
167
211
  {
@@ -210,7 +254,7 @@ var BLOCKRUN_MODELS = [
210
254
  maxOutput: 8192,
211
255
  reasoning: true
212
256
  },
213
- // Moonshot / Kimi
257
+ // Moonshot / Kimi - optimized for agentic workflows
214
258
  {
215
259
  id: "moonshot/kimi-k2.5",
216
260
  name: "Kimi K2.5",
@@ -219,7 +263,8 @@ var BLOCKRUN_MODELS = [
219
263
  contextWindow: 262144,
220
264
  maxOutput: 8192,
221
265
  reasoning: true,
222
- vision: true
266
+ vision: true,
267
+ agentic: true
223
268
  },
224
269
  // xAI / Grok
225
270
  {
@@ -247,6 +292,86 @@ var BLOCKRUN_MODELS = [
247
292
  outputPrice: 0.5,
248
293
  contextWindow: 131072,
249
294
  maxOutput: 16384
295
+ },
296
+ // xAI Grok 4 Family - Ultra-cheap fast models
297
+ {
298
+ id: "xai/grok-4-fast-reasoning",
299
+ name: "Grok 4 Fast Reasoning",
300
+ inputPrice: 0.2,
301
+ outputPrice: 0.5,
302
+ contextWindow: 131072,
303
+ maxOutput: 16384,
304
+ reasoning: true
305
+ },
306
+ {
307
+ id: "xai/grok-4-fast-non-reasoning",
308
+ name: "Grok 4 Fast",
309
+ inputPrice: 0.2,
310
+ outputPrice: 0.5,
311
+ contextWindow: 131072,
312
+ maxOutput: 16384
313
+ },
314
+ {
315
+ id: "xai/grok-4-1-fast-reasoning",
316
+ name: "Grok 4.1 Fast Reasoning",
317
+ inputPrice: 0.2,
318
+ outputPrice: 0.5,
319
+ contextWindow: 131072,
320
+ maxOutput: 16384,
321
+ reasoning: true
322
+ },
323
+ {
324
+ id: "xai/grok-4-1-fast-non-reasoning",
325
+ name: "Grok 4.1 Fast",
326
+ inputPrice: 0.2,
327
+ outputPrice: 0.5,
328
+ contextWindow: 131072,
329
+ maxOutput: 16384
330
+ },
331
+ {
332
+ id: "xai/grok-code-fast-1",
333
+ name: "Grok Code Fast",
334
+ inputPrice: 0.2,
335
+ outputPrice: 1.5,
336
+ contextWindow: 131072,
337
+ maxOutput: 16384,
338
+ agentic: true
339
+ // Good for coding tasks
340
+ },
341
+ {
342
+ id: "xai/grok-4-0709",
343
+ name: "Grok 4 (0709)",
344
+ inputPrice: 3,
345
+ outputPrice: 15,
346
+ contextWindow: 131072,
347
+ maxOutput: 16384,
348
+ reasoning: true
349
+ },
350
+ {
351
+ id: "xai/grok-2-vision",
352
+ name: "Grok 2 Vision",
353
+ inputPrice: 2,
354
+ outputPrice: 10,
355
+ contextWindow: 131072,
356
+ maxOutput: 16384,
357
+ vision: true
358
+ },
359
+ // NVIDIA - Free/cheap models
360
+ {
361
+ id: "nvidia/gpt-oss-120b",
362
+ name: "NVIDIA GPT-OSS 120B",
363
+ inputPrice: 0,
364
+ outputPrice: 0,
365
+ contextWindow: 128e3,
366
+ maxOutput: 8192
367
+ },
368
+ {
369
+ id: "nvidia/kimi-k2.5",
370
+ name: "NVIDIA Kimi K2.5",
371
+ inputPrice: 1e-3,
372
+ outputPrice: 1e-3,
373
+ contextWindow: 262144,
374
+ maxOutput: 8192
250
375
  }
251
376
  ];
252
377
  function toOpenClawModel(m) {
@@ -274,6 +399,20 @@ function buildProviderModels(baseUrl) {
274
399
  models: OPENCLAW_MODELS
275
400
  };
276
401
  }
402
+ function isAgenticModel(modelId) {
403
+ const model = BLOCKRUN_MODELS.find(
404
+ (m) => m.id === modelId || m.id === modelId.replace("blockrun/", "")
405
+ );
406
+ return model?.agentic ?? false;
407
+ }
408
+ function getAgenticModels() {
409
+ return BLOCKRUN_MODELS.filter((m) => m.agentic).map((m) => m.id);
410
+ }
411
+ function getModelContextWindow(modelId) {
412
+ const normalized = modelId.replace("blockrun/", "");
413
+ const model = BLOCKRUN_MODELS.find((m) => m.id === normalized);
414
+ return model?.contextWindow;
415
+ }
277
416
 
278
417
  // src/provider.ts
279
418
  var activeProxy = null;
@@ -537,6 +676,50 @@ function scoreQuestionComplexity(prompt) {
537
676
  }
538
677
  return { name: "questionComplexity", score: 0, signal: null };
539
678
  }
679
+ function scoreAgenticTask(text, keywords) {
680
+ let matchCount = 0;
681
+ const signals = [];
682
+ for (const keyword of keywords) {
683
+ if (text.includes(keyword.toLowerCase())) {
684
+ matchCount++;
685
+ if (signals.length < 3) {
686
+ signals.push(keyword);
687
+ }
688
+ }
689
+ }
690
+ if (matchCount >= 3) {
691
+ return {
692
+ dimensionScore: {
693
+ name: "agenticTask",
694
+ score: 1,
695
+ signal: `agentic (${signals.join(", ")})`
696
+ },
697
+ agenticScore: 1
698
+ };
699
+ } else if (matchCount >= 2) {
700
+ return {
701
+ dimensionScore: {
702
+ name: "agenticTask",
703
+ score: 0.6,
704
+ signal: `agentic (${signals.join(", ")})`
705
+ },
706
+ agenticScore: 0.6
707
+ };
708
+ } else if (matchCount >= 1) {
709
+ return {
710
+ dimensionScore: {
711
+ name: "agenticTask",
712
+ score: 0.3,
713
+ signal: `agentic (${signals.join(", ")})`
714
+ },
715
+ agenticScore: 0.3
716
+ };
717
+ }
718
+ return {
719
+ dimensionScore: { name: "agenticTask", score: 0, signal: null },
720
+ agenticScore: 0
721
+ };
722
+ }
540
723
  function classifyByRules(prompt, systemPrompt, estimatedTokens, config) {
541
724
  const text = `${systemPrompt ?? ""} ${prompt}`.toLowerCase();
542
725
  const userText = prompt.toLowerCase();
@@ -636,6 +819,9 @@ function classifyByRules(prompt, systemPrompt, estimatedTokens, config) {
636
819
  { none: 0, low: 0.5, high: 0.8 }
637
820
  )
638
821
  ];
822
+ const agenticResult = scoreAgenticTask(text, config.agenticTaskKeywords);
823
+ dimensions.push(agenticResult.dimensionScore);
824
+ const agenticScore = agenticResult.agenticScore;
639
825
  const signals = dimensions.filter((d) => d.signal !== null).map((d) => d.signal);
640
826
  const weights = config.dimensionWeights;
641
827
  let weightedScore = 0;
@@ -656,7 +842,8 @@ function classifyByRules(prompt, systemPrompt, estimatedTokens, config) {
656
842
  score: weightedScore,
657
843
  tier: "REASONING",
658
844
  confidence: Math.max(confidence2, 0.85),
659
- signals
845
+ signals,
846
+ agenticScore
660
847
  };
661
848
  }
662
849
  const { simpleMedium, mediumComplex, complexReasoning } = config.tierBoundaries;
@@ -680,9 +867,9 @@ function classifyByRules(prompt, systemPrompt, estimatedTokens, config) {
680
867
  }
681
868
  const confidence = calibrateConfidence(distanceFromBoundary, config.confidenceSteepness);
682
869
  if (confidence < config.confidenceThreshold) {
683
- return { score: weightedScore, tier: null, confidence, signals };
870
+ return { score: weightedScore, tier: null, confidence, signals, agenticScore };
684
871
  }
685
- return { score: weightedScore, tier, confidence, signals };
872
+ return { score: weightedScore, tier, confidence, signals, agenticScore };
686
873
  }
687
874
  function calibrateConfidence(distance, steepness) {
688
875
  return 1 / (1 + Math.exp(-steepness * distance));
@@ -716,6 +903,20 @@ function getFallbackChain(tier, tierConfigs) {
716
903
  const config = tierConfigs[tier];
717
904
  return [config.primary, ...config.fallback];
718
905
  }
906
+ function getFallbackChainFiltered(tier, tierConfigs, estimatedTotalTokens, getContextWindow) {
907
+ const fullChain = getFallbackChain(tier, tierConfigs);
908
+ const filtered = fullChain.filter((modelId) => {
909
+ const contextWindow = getContextWindow(modelId);
910
+ if (contextWindow === void 0) {
911
+ return true;
912
+ }
913
+ return contextWindow >= estimatedTotalTokens * 1.1;
914
+ });
915
+ if (filtered.length === 0) {
916
+ return fullChain;
917
+ }
918
+ return filtered;
919
+ }
719
920
 
720
921
  // src/router/config.ts
721
922
  var DEFAULT_ROUTING_CONFIG = {
@@ -730,7 +931,7 @@ var DEFAULT_ROUTING_CONFIG = {
730
931
  },
731
932
  scoring: {
732
933
  tokenCountThresholds: { simple: 50, complex: 500 },
733
- // Multilingual keywords: English + Chinese (中文) + Japanese (日本語) + Russian (Русский)
934
+ // Multilingual keywords: English + Chinese (中文) + Japanese (日本語) + Russian (Русский) + German (Deutsch)
734
935
  codeKeywords: [
735
936
  // English
736
937
  "function",
@@ -773,7 +974,18 @@ var DEFAULT_ROUTING_CONFIG = {
773
974
  "\u043E\u0436\u0438\u0434\u0430\u0442\u044C",
774
975
  "\u043A\u043E\u043D\u0441\u0442\u0430\u043D\u0442\u0430",
775
976
  "\u043F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u0430\u044F",
776
- "\u0432\u0435\u0440\u043D\u0443\u0442\u044C"
977
+ "\u0432\u0435\u0440\u043D\u0443\u0442\u044C",
978
+ // German
979
+ "funktion",
980
+ "klasse",
981
+ "importieren",
982
+ "definieren",
983
+ "abfrage",
984
+ "asynchron",
985
+ "erwarten",
986
+ "konstante",
987
+ "variable",
988
+ "zur\xFCckgeben"
777
989
  ],
778
990
  reasoningKeywords: [
779
991
  // English
@@ -814,7 +1026,17 @@ var DEFAULT_ROUTING_CONFIG = {
814
1026
  "\u0440\u0430\u0441\u0441\u0443\u0436\u0434\u0435\u043D\u0438",
815
1027
  "\u0444\u043E\u0440\u043C\u0430\u043B\u044C\u043D\u043E",
816
1028
  "\u043C\u0430\u0442\u0435\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438",
817
- "\u043B\u043E\u0433\u0438\u0447\u0435\u0441\u043A\u0438"
1029
+ "\u043B\u043E\u0433\u0438\u0447\u0435\u0441\u043A\u0438",
1030
+ // German
1031
+ "beweisen",
1032
+ "beweis",
1033
+ "theorem",
1034
+ "ableiten",
1035
+ "schritt f\xFCr schritt",
1036
+ "gedankenkette",
1037
+ "formal",
1038
+ "mathematisch",
1039
+ "logisch"
818
1040
  ],
819
1041
  simpleKeywords: [
820
1042
  // English
@@ -856,7 +1078,18 @@ var DEFAULT_ROUTING_CONFIG = {
856
1078
  "\u0441\u043A\u043E\u043B\u044C\u043A\u043E \u043B\u0435\u0442",
857
1079
  "\u043A\u0442\u043E \u0442\u0430\u043A\u043E\u0439",
858
1080
  "\u043A\u043E\u0433\u0434\u0430",
859
- "\u043E\u0431\u044A\u044F\u0441\u043D\u0438"
1081
+ "\u043E\u0431\u044A\u044F\u0441\u043D\u0438",
1082
+ // German
1083
+ "was ist",
1084
+ "definiere",
1085
+ "\xFCbersetze",
1086
+ "hallo",
1087
+ "ja oder nein",
1088
+ "hauptstadt",
1089
+ "wie alt",
1090
+ "wer ist",
1091
+ "wann",
1092
+ "erkl\xE4re"
860
1093
  ],
861
1094
  technicalKeywords: [
862
1095
  // English
@@ -892,7 +1125,16 @@ var DEFAULT_ROUTING_CONFIG = {
892
1125
  "\u0440\u0430\u0441\u043F\u0440\u0435\u0434\u0435\u043B\u0451\u043D\u043D\u044B\u0439",
893
1126
  "\u043C\u0438\u043A\u0440\u043E\u0441\u0435\u0440\u0432\u0438\u0441",
894
1127
  "\u0431\u0430\u0437\u0430 \u0434\u0430\u043D\u043D\u044B\u0445",
895
- "\u0438\u043D\u0444\u0440\u0430\u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0430"
1128
+ "\u0438\u043D\u0444\u0440\u0430\u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0430",
1129
+ // German
1130
+ "algorithmus",
1131
+ "optimieren",
1132
+ "architektur",
1133
+ "verteilt",
1134
+ "kubernetes",
1135
+ "mikroservice",
1136
+ "datenbank",
1137
+ "infrastruktur"
896
1138
  ],
897
1139
  creativeKeywords: [
898
1140
  // English
@@ -928,7 +1170,16 @@ var DEFAULT_ROUTING_CONFIG = {
928
1170
  "\u0442\u0432\u043E\u0440\u0447\u0435\u0441\u043A\u0438\u0439",
929
1171
  "\u043F\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u0438\u0442\u044C",
930
1172
  "\u043F\u0440\u0438\u0434\u0443\u043C\u0430\u0439",
931
- "\u043D\u0430\u043F\u0438\u0448\u0438"
1173
+ "\u043D\u0430\u043F\u0438\u0448\u0438",
1174
+ // German
1175
+ "geschichte",
1176
+ "gedicht",
1177
+ "komponieren",
1178
+ "brainstorming",
1179
+ "kreativ",
1180
+ "vorstellen",
1181
+ "schreibe",
1182
+ "erz\xE4hlung"
932
1183
  ],
933
1184
  // New dimension keyword lists (multilingual)
934
1185
  imperativeVerbs: [
@@ -978,7 +1229,18 @@ var DEFAULT_ROUTING_CONFIG = {
978
1229
  "\u0440\u0430\u0437\u0432\u0435\u0440\u043D\u0443\u0442\u044C",
979
1230
  "\u0440\u0430\u0437\u0432\u0435\u0440\u043D\u0438",
980
1231
  "\u043D\u0430\u0441\u0442\u0440\u043E\u0438\u0442\u044C",
981
- "\u043D\u0430\u0441\u0442\u0440\u043E\u0439"
1232
+ "\u043D\u0430\u0441\u0442\u0440\u043E\u0439",
1233
+ // German
1234
+ "erstellen",
1235
+ "bauen",
1236
+ "implementieren",
1237
+ "entwerfen",
1238
+ "entwickeln",
1239
+ "konstruieren",
1240
+ "generieren",
1241
+ "bereitstellen",
1242
+ "konfigurieren",
1243
+ "einrichten"
982
1244
  ],
983
1245
  constraintIndicators: [
984
1246
  // English
@@ -1015,7 +1277,16 @@ var DEFAULT_ROUTING_CONFIG = {
1015
1277
  "\u043C\u0430\u043A\u0441\u0438\u043C\u0443\u043C",
1016
1278
  "\u043C\u0438\u043D\u0438\u043C\u0443\u043C",
1017
1279
  "\u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0435\u043D\u0438\u0435",
1018
- "\u0431\u044E\u0434\u0436\u0435\u0442"
1280
+ "\u0431\u044E\u0434\u0436\u0435\u0442",
1281
+ // German
1282
+ "h\xF6chstens",
1283
+ "mindestens",
1284
+ "innerhalb",
1285
+ "nicht mehr als",
1286
+ "maximal",
1287
+ "minimal",
1288
+ "grenze",
1289
+ "budget"
1019
1290
  ],
1020
1291
  outputFormatKeywords: [
1021
1292
  // English
@@ -1039,7 +1310,11 @@ var DEFAULT_ROUTING_CONFIG = {
1039
1310
  // Russian
1040
1311
  "\u0442\u0430\u0431\u043B\u0438\u0446\u0430",
1041
1312
  "\u0444\u043E\u0440\u043C\u0430\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043A\u0430\u043A",
1042
- "\u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0439"
1313
+ "\u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0439",
1314
+ // German
1315
+ "tabelle",
1316
+ "formatieren als",
1317
+ "strukturiert"
1043
1318
  ],
1044
1319
  referenceKeywords: [
1045
1320
  // English
@@ -1075,7 +1350,16 @@ var DEFAULT_ROUTING_CONFIG = {
1075
1350
  "\u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0430\u0446\u0438\u044F",
1076
1351
  "\u043A\u043E\u0434",
1077
1352
  "\u0440\u0430\u043D\u0435\u0435",
1078
- "\u0432\u043B\u043E\u0436\u0435\u043D\u0438\u0435"
1353
+ "\u0432\u043B\u043E\u0436\u0435\u043D\u0438\u0435",
1354
+ // German
1355
+ "oben",
1356
+ "unten",
1357
+ "vorherige",
1358
+ "folgende",
1359
+ "dokumentation",
1360
+ "der code",
1361
+ "fr\xFCher",
1362
+ "anhang"
1079
1363
  ],
1080
1364
  negationKeywords: [
1081
1365
  // English
@@ -1109,7 +1393,15 @@ var DEFAULT_ROUTING_CONFIG = {
1109
1393
  "\u0431\u0435\u0437",
1110
1394
  "\u043A\u0440\u043E\u043C\u0435",
1111
1395
  "\u0438\u0441\u043A\u043B\u044E\u0447\u0438\u0442\u044C",
1112
- "\u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435"
1396
+ "\u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435",
1397
+ // German
1398
+ "nicht",
1399
+ "vermeide",
1400
+ "niemals",
1401
+ "ohne",
1402
+ "au\xDFer",
1403
+ "ausschlie\xDFen",
1404
+ "nicht mehr"
1113
1405
  ],
1114
1406
  domainSpecificKeywords: [
1115
1407
  // English
@@ -1147,7 +1439,88 @@ var DEFAULT_ROUTING_CONFIG = {
1147
1439
  "\u0442\u043E\u043F\u043E\u043B\u043E\u0433\u0438\u0447\u0435\u0441\u043A\u0438\u0439",
1148
1440
  "\u0433\u043E\u043C\u043E\u043C\u043E\u0440\u0444\u043D\u044B\u0439",
1149
1441
  "\u0441 \u043D\u0443\u043B\u0435\u0432\u044B\u043C \u0440\u0430\u0437\u0433\u043B\u0430\u0448\u0435\u043D\u0438\u0435\u043C",
1150
- "\u043D\u0430 \u043E\u0441\u043D\u043E\u0432\u0435 \u0440\u0435\u0448\u0451\u0442\u043E\u043A"
1442
+ "\u043D\u0430 \u043E\u0441\u043D\u043E\u0432\u0435 \u0440\u0435\u0448\u0451\u0442\u043E\u043A",
1443
+ // German
1444
+ "quanten",
1445
+ "photonik",
1446
+ "genomik",
1447
+ "proteomik",
1448
+ "topologisch",
1449
+ "homomorph",
1450
+ "zero-knowledge",
1451
+ "gitterbasiert"
1452
+ ],
1453
+ // Agentic task keywords - file ops, execution, multi-step, iterative work
1454
+ agenticTaskKeywords: [
1455
+ // English - File operations
1456
+ "read file",
1457
+ "read the file",
1458
+ "look at",
1459
+ "check the",
1460
+ "open the",
1461
+ "edit",
1462
+ "modify",
1463
+ "update the",
1464
+ "change the",
1465
+ "write to",
1466
+ "create file",
1467
+ // English - Execution
1468
+ "run",
1469
+ "execute",
1470
+ "test",
1471
+ "build",
1472
+ "deploy",
1473
+ "install",
1474
+ "npm",
1475
+ "pip",
1476
+ "compile",
1477
+ "start",
1478
+ "launch",
1479
+ // English - Multi-step patterns
1480
+ "then",
1481
+ "after that",
1482
+ "next",
1483
+ "and also",
1484
+ "finally",
1485
+ "once done",
1486
+ "step 1",
1487
+ "step 2",
1488
+ "first",
1489
+ "second",
1490
+ "lastly",
1491
+ // English - Iterative work
1492
+ "fix",
1493
+ "debug",
1494
+ "until it works",
1495
+ "keep trying",
1496
+ "iterate",
1497
+ "make sure",
1498
+ "verify",
1499
+ "confirm",
1500
+ // Chinese
1501
+ "\u8BFB\u53D6\u6587\u4EF6",
1502
+ "\u67E5\u770B",
1503
+ "\u6253\u5F00",
1504
+ "\u7F16\u8F91",
1505
+ "\u4FEE\u6539",
1506
+ "\u66F4\u65B0",
1507
+ "\u521B\u5EFA",
1508
+ "\u8FD0\u884C",
1509
+ "\u6267\u884C",
1510
+ "\u6D4B\u8BD5",
1511
+ "\u6784\u5EFA",
1512
+ "\u90E8\u7F72",
1513
+ "\u5B89\u88C5",
1514
+ "\u7136\u540E",
1515
+ "\u63A5\u4E0B\u6765",
1516
+ "\u6700\u540E",
1517
+ "\u7B2C\u4E00\u6B65",
1518
+ "\u7B2C\u4E8C\u6B65",
1519
+ "\u4FEE\u590D",
1520
+ "\u8C03\u8BD5",
1521
+ "\u76F4\u5230",
1522
+ "\u786E\u8BA4",
1523
+ "\u9A8C\u8BC1"
1151
1524
  ],
1152
1525
  // Dimension weights (sum to 1.0)
1153
1526
  dimensionWeights: {
@@ -1156,7 +1529,8 @@ var DEFAULT_ROUTING_CONFIG = {
1156
1529
  reasoningMarkers: 0.18,
1157
1530
  technicalTerms: 0.1,
1158
1531
  creativeMarkers: 0.05,
1159
- simpleIndicators: 0.12,
1532
+ simpleIndicators: 0.02,
1533
+ // Reduced from 0.12 to make room for agenticTask
1160
1534
  multiStepPatterns: 0.12,
1161
1535
  questionComplexity: 0.05,
1162
1536
  imperativeVerbs: 0.03,
@@ -1164,7 +1538,9 @@ var DEFAULT_ROUTING_CONFIG = {
1164
1538
  outputFormat: 0.03,
1165
1539
  referenceComplexity: 0.02,
1166
1540
  negationComplexity: 0.01,
1167
- domainSpecificity: 0.02
1541
+ domainSpecificity: 0.02,
1542
+ agenticTask: 0.1
1543
+ // Significant weight for agentic detection
1168
1544
  },
1169
1545
  // Tier boundaries on weighted score axis
1170
1546
  tierBoundaries: {
@@ -1180,25 +1556,49 @@ var DEFAULT_ROUTING_CONFIG = {
1180
1556
  tiers: {
1181
1557
  SIMPLE: {
1182
1558
  primary: "google/gemini-2.5-flash",
1183
- fallback: ["deepseek/deepseek-chat", "openai/gpt-4o-mini"]
1559
+ fallback: ["nvidia/gpt-oss-120b", "deepseek/deepseek-chat", "openai/gpt-4o-mini"]
1560
+ },
1561
+ MEDIUM: {
1562
+ primary: "xai/grok-code-fast-1",
1563
+ // Code specialist, $0.20/$1.50
1564
+ fallback: ["deepseek/deepseek-chat", "xai/grok-4-fast-non-reasoning", "google/gemini-2.5-flash"]
1565
+ },
1566
+ COMPLEX: {
1567
+ primary: "google/gemini-2.5-pro",
1568
+ fallback: ["anthropic/claude-sonnet-4", "xai/grok-4-0709", "openai/gpt-4o"]
1569
+ },
1570
+ REASONING: {
1571
+ primary: "xai/grok-4-fast-reasoning",
1572
+ // Ultra-cheap reasoning $0.20/$0.50
1573
+ fallback: ["deepseek/deepseek-reasoner", "moonshot/kimi-k2.5", "google/gemini-2.5-pro"]
1574
+ }
1575
+ },
1576
+ // Agentic tier configs - models that excel at multi-step autonomous tasks
1577
+ agenticTiers: {
1578
+ SIMPLE: {
1579
+ primary: "anthropic/claude-haiku-4.5",
1580
+ fallback: ["moonshot/kimi-k2.5", "xai/grok-4-fast-non-reasoning", "openai/gpt-4o-mini"]
1184
1581
  },
1185
1582
  MEDIUM: {
1186
- primary: "deepseek/deepseek-chat",
1187
- fallback: ["google/gemini-2.5-flash", "openai/gpt-4o-mini"]
1583
+ primary: "xai/grok-code-fast-1",
1584
+ // Code specialist for agentic coding
1585
+ fallback: ["moonshot/kimi-k2.5", "anthropic/claude-haiku-4.5", "anthropic/claude-sonnet-4"]
1188
1586
  },
1189
1587
  COMPLEX: {
1190
- primary: "anthropic/claude-opus-4",
1191
- fallback: ["anthropic/claude-sonnet-4", "openai/gpt-4o"]
1588
+ primary: "anthropic/claude-sonnet-4",
1589
+ fallback: ["anthropic/claude-opus-4", "xai/grok-4-0709", "openai/gpt-4o"]
1192
1590
  },
1193
1591
  REASONING: {
1194
- primary: "deepseek/deepseek-reasoner",
1195
- fallback: ["moonshot/kimi-k2.5", "google/gemini-2.5-pro"]
1592
+ primary: "xai/grok-4-fast-reasoning",
1593
+ // Cheap reasoning for agentic tasks
1594
+ fallback: ["moonshot/kimi-k2.5", "anthropic/claude-sonnet-4", "deepseek/deepseek-reasoner"]
1196
1595
  }
1197
1596
  },
1198
1597
  overrides: {
1199
1598
  maxTokensForceComplex: 1e5,
1200
1599
  structuredOutputMinTier: "MEDIUM",
1201
- ambiguousDefaultTier: "MEDIUM"
1600
+ ambiguousDefaultTier: "MEDIUM",
1601
+ agenticMode: false
1202
1602
  }
1203
1603
  };
1204
1604
 
@@ -1207,24 +1607,29 @@ function route(prompt, systemPrompt, maxOutputTokens, options) {
1207
1607
  const { config, modelPricing } = options;
1208
1608
  const fullText = `${systemPrompt ?? ""} ${prompt}`;
1209
1609
  const estimatedTokens = Math.ceil(fullText.length / 4);
1610
+ const ruleResult = classifyByRules(prompt, systemPrompt, estimatedTokens, config.scoring);
1611
+ const agenticScore = ruleResult.agenticScore ?? 0;
1612
+ const isAutoAgentic = agenticScore >= 0.6;
1613
+ const isExplicitAgentic = config.overrides.agenticMode ?? false;
1614
+ const useAgenticTiers = (isAutoAgentic || isExplicitAgentic) && config.agenticTiers != null;
1615
+ const tierConfigs = useAgenticTiers ? config.agenticTiers : config.tiers;
1210
1616
  if (estimatedTokens > config.overrides.maxTokensForceComplex) {
1211
1617
  return selectModel(
1212
1618
  "COMPLEX",
1213
1619
  0.95,
1214
1620
  "rules",
1215
- `Input exceeds ${config.overrides.maxTokensForceComplex} tokens`,
1216
- config.tiers,
1621
+ `Input exceeds ${config.overrides.maxTokensForceComplex} tokens${useAgenticTiers ? " | agentic" : ""}`,
1622
+ tierConfigs,
1217
1623
  modelPricing,
1218
1624
  estimatedTokens,
1219
1625
  maxOutputTokens
1220
1626
  );
1221
1627
  }
1222
1628
  const hasStructuredOutput = systemPrompt ? /json|structured|schema/i.test(systemPrompt) : false;
1223
- const ruleResult = classifyByRules(prompt, systemPrompt, estimatedTokens, config.scoring);
1224
1629
  let tier;
1225
1630
  let confidence;
1226
1631
  const method = "rules";
1227
- let reasoning = `score=${ruleResult.score} | ${ruleResult.signals.join(", ")}`;
1632
+ let reasoning = `score=${ruleResult.score.toFixed(2)} | ${ruleResult.signals.join(", ")}`;
1228
1633
  if (ruleResult.tier !== null) {
1229
1634
  tier = ruleResult.tier;
1230
1635
  confidence = ruleResult.confidence;
@@ -1241,12 +1646,17 @@ function route(prompt, systemPrompt, maxOutputTokens, options) {
1241
1646
  tier = minTier;
1242
1647
  }
1243
1648
  }
1649
+ if (isAutoAgentic) {
1650
+ reasoning += " | auto-agentic";
1651
+ } else if (isExplicitAgentic) {
1652
+ reasoning += " | agentic";
1653
+ }
1244
1654
  return selectModel(
1245
1655
  tier,
1246
1656
  confidence,
1247
1657
  method,
1248
1658
  reasoning,
1249
- config.tiers,
1659
+ tierConfigs,
1250
1660
  modelPricing,
1251
1661
  estimatedTokens,
1252
1662
  maxOutputTokens
@@ -1274,6 +1684,176 @@ async function logUsage(entry) {
1274
1684
  }
1275
1685
  }
1276
1686
 
1687
+ // src/stats.ts
1688
+ import { readFile, readdir } from "fs/promises";
1689
+ import { join as join2 } from "path";
1690
+ import { homedir as homedir2 } from "os";
1691
+ var LOG_DIR2 = join2(homedir2(), ".openclaw", "blockrun", "logs");
1692
+ async function parseLogFile(filePath) {
1693
+ try {
1694
+ const content = await readFile(filePath, "utf-8");
1695
+ const lines = content.trim().split("\n").filter(Boolean);
1696
+ return lines.map((line) => {
1697
+ const entry = JSON.parse(line);
1698
+ return {
1699
+ timestamp: entry.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
1700
+ model: entry.model || "unknown",
1701
+ tier: entry.tier || "UNKNOWN",
1702
+ cost: entry.cost || 0,
1703
+ baselineCost: entry.baselineCost || entry.cost || 0,
1704
+ savings: entry.savings || 0,
1705
+ latencyMs: entry.latencyMs || 0
1706
+ };
1707
+ });
1708
+ } catch {
1709
+ return [];
1710
+ }
1711
+ }
1712
+ async function getLogFiles() {
1713
+ try {
1714
+ const files = await readdir(LOG_DIR2);
1715
+ return files.filter((f) => f.startsWith("usage-") && f.endsWith(".jsonl")).sort().reverse();
1716
+ } catch {
1717
+ return [];
1718
+ }
1719
+ }
1720
+ function aggregateDay(date, entries) {
1721
+ const byTier = {};
1722
+ const byModel = {};
1723
+ let totalLatency = 0;
1724
+ for (const entry of entries) {
1725
+ if (!byTier[entry.tier]) byTier[entry.tier] = { count: 0, cost: 0 };
1726
+ byTier[entry.tier].count++;
1727
+ byTier[entry.tier].cost += entry.cost;
1728
+ if (!byModel[entry.model]) byModel[entry.model] = { count: 0, cost: 0 };
1729
+ byModel[entry.model].count++;
1730
+ byModel[entry.model].cost += entry.cost;
1731
+ totalLatency += entry.latencyMs;
1732
+ }
1733
+ const totalCost = entries.reduce((sum, e) => sum + e.cost, 0);
1734
+ const totalBaselineCost = entries.reduce((sum, e) => sum + e.baselineCost, 0);
1735
+ return {
1736
+ date,
1737
+ totalRequests: entries.length,
1738
+ totalCost,
1739
+ totalBaselineCost,
1740
+ totalSavings: totalBaselineCost - totalCost,
1741
+ avgLatencyMs: entries.length > 0 ? totalLatency / entries.length : 0,
1742
+ byTier,
1743
+ byModel
1744
+ };
1745
+ }
1746
+ async function getStats(days = 7) {
1747
+ const logFiles = await getLogFiles();
1748
+ const filesToRead = logFiles.slice(0, days);
1749
+ const dailyBreakdown = [];
1750
+ const allByTier = {};
1751
+ const allByModel = {};
1752
+ let totalRequests = 0;
1753
+ let totalCost = 0;
1754
+ let totalBaselineCost = 0;
1755
+ let totalLatency = 0;
1756
+ for (const file of filesToRead) {
1757
+ const date = file.replace("usage-", "").replace(".jsonl", "");
1758
+ const filePath = join2(LOG_DIR2, file);
1759
+ const entries = await parseLogFile(filePath);
1760
+ if (entries.length === 0) continue;
1761
+ const dayStats = aggregateDay(date, entries);
1762
+ dailyBreakdown.push(dayStats);
1763
+ totalRequests += dayStats.totalRequests;
1764
+ totalCost += dayStats.totalCost;
1765
+ totalBaselineCost += dayStats.totalBaselineCost;
1766
+ totalLatency += dayStats.avgLatencyMs * dayStats.totalRequests;
1767
+ for (const [tier, stats] of Object.entries(dayStats.byTier)) {
1768
+ if (!allByTier[tier]) allByTier[tier] = { count: 0, cost: 0 };
1769
+ allByTier[tier].count += stats.count;
1770
+ allByTier[tier].cost += stats.cost;
1771
+ }
1772
+ for (const [model, stats] of Object.entries(dayStats.byModel)) {
1773
+ if (!allByModel[model]) allByModel[model] = { count: 0, cost: 0 };
1774
+ allByModel[model].count += stats.count;
1775
+ allByModel[model].cost += stats.cost;
1776
+ }
1777
+ }
1778
+ const byTierWithPercentage = {};
1779
+ for (const [tier, stats] of Object.entries(allByTier)) {
1780
+ byTierWithPercentage[tier] = {
1781
+ ...stats,
1782
+ percentage: totalRequests > 0 ? stats.count / totalRequests * 100 : 0
1783
+ };
1784
+ }
1785
+ const byModelWithPercentage = {};
1786
+ for (const [model, stats] of Object.entries(allByModel)) {
1787
+ byModelWithPercentage[model] = {
1788
+ ...stats,
1789
+ percentage: totalRequests > 0 ? stats.count / totalRequests * 100 : 0
1790
+ };
1791
+ }
1792
+ const totalSavings = totalBaselineCost - totalCost;
1793
+ const savingsPercentage = totalBaselineCost > 0 ? totalSavings / totalBaselineCost * 100 : 0;
1794
+ return {
1795
+ period: days === 1 ? "today" : `last ${days} days`,
1796
+ totalRequests,
1797
+ totalCost,
1798
+ totalBaselineCost,
1799
+ totalSavings,
1800
+ savingsPercentage,
1801
+ avgLatencyMs: totalRequests > 0 ? totalLatency / totalRequests : 0,
1802
+ avgCostPerRequest: totalRequests > 0 ? totalCost / totalRequests : 0,
1803
+ byTier: byTierWithPercentage,
1804
+ byModel: byModelWithPercentage,
1805
+ dailyBreakdown: dailyBreakdown.reverse()
1806
+ // Oldest first for charts
1807
+ };
1808
+ }
1809
+ function formatStatsAscii(stats) {
1810
+ const lines = [];
1811
+ lines.push("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
1812
+ lines.push("\u2551 ClawRouter Usage Statistics \u2551");
1813
+ lines.push("\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563");
1814
+ lines.push(`\u2551 Period: ${stats.period.padEnd(49)}\u2551`);
1815
+ lines.push(`\u2551 Total Requests: ${stats.totalRequests.toString().padEnd(41)}\u2551`);
1816
+ lines.push(`\u2551 Total Cost: $${stats.totalCost.toFixed(4).padEnd(43)}\u2551`);
1817
+ lines.push(
1818
+ `\u2551 Baseline Cost (Opus): $${stats.totalBaselineCost.toFixed(4).padEnd(33)}\u2551`
1819
+ );
1820
+ lines.push(
1821
+ `\u2551 \u{1F4B0} Total Saved: $${stats.totalSavings.toFixed(4)} (${stats.savingsPercentage.toFixed(1)}%)`.padEnd(61) + "\u2551"
1822
+ );
1823
+ lines.push(`\u2551 Avg Latency: ${stats.avgLatencyMs.toFixed(0)}ms`.padEnd(61) + "\u2551");
1824
+ lines.push("\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563");
1825
+ lines.push("\u2551 Routing by Tier: \u2551");
1826
+ const tierOrder = ["SIMPLE", "MEDIUM", "COMPLEX", "REASONING"];
1827
+ for (const tier of tierOrder) {
1828
+ const data = stats.byTier[tier];
1829
+ if (data) {
1830
+ const bar = "\u2588".repeat(Math.min(20, Math.round(data.percentage / 5)));
1831
+ const line = `\u2551 ${tier.padEnd(10)} ${bar.padEnd(20)} ${data.percentage.toFixed(1).padStart(5)}% (${data.count})`;
1832
+ lines.push(line.padEnd(61) + "\u2551");
1833
+ }
1834
+ }
1835
+ lines.push("\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563");
1836
+ lines.push("\u2551 Top Models: \u2551");
1837
+ const sortedModels = Object.entries(stats.byModel).sort((a, b) => b[1].count - a[1].count).slice(0, 5);
1838
+ for (const [model, data] of sortedModels) {
1839
+ const shortModel = model.length > 25 ? model.slice(0, 22) + "..." : model;
1840
+ const line = `\u2551 ${shortModel.padEnd(25)} ${data.count.toString().padStart(5)} reqs $${data.cost.toFixed(4)}`;
1841
+ lines.push(line.padEnd(61) + "\u2551");
1842
+ }
1843
+ if (stats.dailyBreakdown.length > 0) {
1844
+ lines.push("\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563");
1845
+ lines.push("\u2551 Daily Breakdown: \u2551");
1846
+ lines.push("\u2551 Date Requests Cost Saved \u2551");
1847
+ for (const day of stats.dailyBreakdown.slice(-7)) {
1848
+ const saved = day.totalBaselineCost - day.totalCost;
1849
+ const line = `\u2551 ${day.date} ${day.totalRequests.toString().padStart(6)} $${day.totalCost.toFixed(4).padStart(8)} $${saved.toFixed(4)}`;
1850
+ lines.push(line.padEnd(61) + "\u2551");
1851
+ }
1852
+ }
1853
+ lines.push("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
1854
+ return lines.join("\n");
1855
+ }
1856
+
1277
1857
  // src/dedup.ts
1278
1858
  import { createHash } from "crypto";
1279
1859
  var DEFAULT_TTL_MS2 = 3e4;
@@ -1567,18 +2147,152 @@ var BalanceMonitor = class {
1567
2147
  // src/version.ts
1568
2148
  import { createRequire } from "module";
1569
2149
  import { fileURLToPath } from "url";
1570
- import { dirname, join as join2 } from "path";
2150
+ import { dirname, join as join3 } from "path";
1571
2151
  var __filename = fileURLToPath(import.meta.url);
1572
2152
  var __dirname = dirname(__filename);
1573
2153
  var require2 = createRequire(import.meta.url);
1574
- var pkg = require2(join2(__dirname, "..", "package.json"));
2154
+ var pkg = require2(join3(__dirname, "..", "package.json"));
1575
2155
  var VERSION = pkg.version;
1576
2156
  var USER_AGENT = `clawrouter/${VERSION}`;
1577
2157
 
2158
+ // src/session.ts
2159
+ var DEFAULT_SESSION_CONFIG = {
2160
+ enabled: false,
2161
+ timeoutMs: 30 * 60 * 1e3,
2162
+ // 30 minutes
2163
+ headerName: "x-session-id"
2164
+ };
2165
+ var SessionStore = class {
2166
+ sessions = /* @__PURE__ */ new Map();
2167
+ config;
2168
+ cleanupInterval = null;
2169
+ constructor(config = {}) {
2170
+ this.config = { ...DEFAULT_SESSION_CONFIG, ...config };
2171
+ if (this.config.enabled) {
2172
+ this.cleanupInterval = setInterval(
2173
+ () => this.cleanup(),
2174
+ 5 * 60 * 1e3
2175
+ );
2176
+ }
2177
+ }
2178
+ /**
2179
+ * Get the pinned model for a session, if any.
2180
+ */
2181
+ getSession(sessionId) {
2182
+ if (!this.config.enabled || !sessionId) {
2183
+ return void 0;
2184
+ }
2185
+ const entry = this.sessions.get(sessionId);
2186
+ if (!entry) {
2187
+ return void 0;
2188
+ }
2189
+ const now = Date.now();
2190
+ if (now - entry.lastUsedAt > this.config.timeoutMs) {
2191
+ this.sessions.delete(sessionId);
2192
+ return void 0;
2193
+ }
2194
+ return entry;
2195
+ }
2196
+ /**
2197
+ * Pin a model to a session.
2198
+ */
2199
+ setSession(sessionId, model, tier) {
2200
+ if (!this.config.enabled || !sessionId) {
2201
+ return;
2202
+ }
2203
+ const existing = this.sessions.get(sessionId);
2204
+ const now = Date.now();
2205
+ if (existing) {
2206
+ existing.lastUsedAt = now;
2207
+ existing.requestCount++;
2208
+ if (existing.model !== model) {
2209
+ existing.model = model;
2210
+ existing.tier = tier;
2211
+ }
2212
+ } else {
2213
+ this.sessions.set(sessionId, {
2214
+ model,
2215
+ tier,
2216
+ createdAt: now,
2217
+ lastUsedAt: now,
2218
+ requestCount: 1
2219
+ });
2220
+ }
2221
+ }
2222
+ /**
2223
+ * Touch a session to extend its timeout.
2224
+ */
2225
+ touchSession(sessionId) {
2226
+ if (!this.config.enabled || !sessionId) {
2227
+ return;
2228
+ }
2229
+ const entry = this.sessions.get(sessionId);
2230
+ if (entry) {
2231
+ entry.lastUsedAt = Date.now();
2232
+ entry.requestCount++;
2233
+ }
2234
+ }
2235
+ /**
2236
+ * Clear a specific session.
2237
+ */
2238
+ clearSession(sessionId) {
2239
+ this.sessions.delete(sessionId);
2240
+ }
2241
+ /**
2242
+ * Clear all sessions.
2243
+ */
2244
+ clearAll() {
2245
+ this.sessions.clear();
2246
+ }
2247
+ /**
2248
+ * Get session stats for debugging.
2249
+ */
2250
+ getStats() {
2251
+ const now = Date.now();
2252
+ const sessions = Array.from(this.sessions.entries()).map(([id, entry]) => ({
2253
+ id: id.slice(0, 8) + "...",
2254
+ model: entry.model,
2255
+ age: Math.round((now - entry.createdAt) / 1e3)
2256
+ }));
2257
+ return { count: this.sessions.size, sessions };
2258
+ }
2259
+ /**
2260
+ * Clean up expired sessions.
2261
+ */
2262
+ cleanup() {
2263
+ const now = Date.now();
2264
+ for (const [id, entry] of this.sessions) {
2265
+ if (now - entry.lastUsedAt > this.config.timeoutMs) {
2266
+ this.sessions.delete(id);
2267
+ }
2268
+ }
2269
+ }
2270
+ /**
2271
+ * Stop the cleanup interval.
2272
+ */
2273
+ close() {
2274
+ if (this.cleanupInterval) {
2275
+ clearInterval(this.cleanupInterval);
2276
+ this.cleanupInterval = null;
2277
+ }
2278
+ }
2279
+ };
2280
+ function getSessionId(headers, headerName = DEFAULT_SESSION_CONFIG.headerName) {
2281
+ const value = headers[headerName] || headers[headerName.toLowerCase()];
2282
+ if (typeof value === "string" && value.length > 0) {
2283
+ return value;
2284
+ }
2285
+ if (Array.isArray(value) && value.length > 0) {
2286
+ return value[0];
2287
+ }
2288
+ return void 0;
2289
+ }
2290
+
1578
2291
  // src/proxy.ts
1579
2292
  var BLOCKRUN_API = "https://blockrun.ai/api";
1580
2293
  var AUTO_MODEL = "blockrun/auto";
1581
2294
  var AUTO_MODEL_SHORT = "auto";
2295
+ var FREE_MODEL = "nvidia/gpt-oss-120b";
1582
2296
  var HEARTBEAT_INTERVAL_MS = 2e3;
1583
2297
  var DEFAULT_REQUEST_TIMEOUT_MS = 18e4;
1584
2298
  var DEFAULT_PORT = 8402;
@@ -1757,6 +2471,7 @@ async function startProxy(options) {
1757
2471
  modelPricing
1758
2472
  };
1759
2473
  const deduplicator = new RequestDeduplicator();
2474
+ const sessionStore = new SessionStore(options.sessionConfig);
1760
2475
  const server = createServer(async (req, res) => {
1761
2476
  if (req.url === "/health" || req.url?.startsWith("/health?")) {
1762
2477
  const url = new URL(req.url, "http://localhost");
@@ -1779,6 +2494,37 @@ async function startProxy(options) {
1779
2494
  res.end(JSON.stringify(response));
1780
2495
  return;
1781
2496
  }
2497
+ if (req.url === "/stats" || req.url?.startsWith("/stats?")) {
2498
+ try {
2499
+ const url = new URL(req.url, "http://localhost");
2500
+ const days = parseInt(url.searchParams.get("days") || "7", 10);
2501
+ const stats = await getStats(Math.min(days, 30));
2502
+ res.writeHead(200, {
2503
+ "Content-Type": "application/json",
2504
+ "Cache-Control": "no-cache"
2505
+ });
2506
+ res.end(JSON.stringify(stats, null, 2));
2507
+ } catch (err) {
2508
+ res.writeHead(500, { "Content-Type": "application/json" });
2509
+ res.end(
2510
+ JSON.stringify({
2511
+ error: `Failed to get stats: ${err instanceof Error ? err.message : String(err)}`
2512
+ })
2513
+ );
2514
+ }
2515
+ return;
2516
+ }
2517
+ if (req.url === "/v1/models" && req.method === "GET") {
2518
+ const models = BLOCKRUN_MODELS.filter((m) => m.id !== "blockrun/auto").map((m) => ({
2519
+ id: m.id,
2520
+ object: "model",
2521
+ created: Math.floor(Date.now() / 1e3),
2522
+ owned_by: m.id.split("/")[0] || "unknown"
2523
+ }));
2524
+ res.writeHead(200, { "Content-Type": "application/json" });
2525
+ res.end(JSON.stringify({ object: "list", data: models }));
2526
+ return;
2527
+ }
1782
2528
  if (!req.url?.startsWith("/v1")) {
1783
2529
  res.writeHead(404, { "Content-Type": "application/json" });
1784
2530
  res.end(JSON.stringify({ error: "Not found" }));
@@ -1793,7 +2539,8 @@ async function startProxy(options) {
1793
2539
  options,
1794
2540
  routerOpts,
1795
2541
  deduplicator,
1796
- balanceMonitor
2542
+ balanceMonitor,
2543
+ sessionStore
1797
2544
  );
1798
2545
  } catch (err) {
1799
2546
  const error = err instanceof Error ? err : new Error(String(err));
@@ -1844,6 +2591,7 @@ async function startProxy(options) {
1844
2591
  walletAddress: account.address,
1845
2592
  balanceMonitor,
1846
2593
  close: () => new Promise((res, rej) => {
2594
+ sessionStore.close();
1847
2595
  server.close((err) => err ? rej(err) : res());
1848
2596
  })
1849
2597
  });
@@ -1896,7 +2644,7 @@ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxT
1896
2644
  };
1897
2645
  }
1898
2646
  }
1899
- async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, deduplicator, balanceMonitor) {
2647
+ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, deduplicator, balanceMonitor, sessionStore) {
1900
2648
  const startTime = Date.now();
1901
2649
  const upstreamUrl = `${apiBase}${req.url}`;
1902
2650
  const bodyChunks = [];
@@ -1921,29 +2669,66 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
1921
2669
  bodyModified = true;
1922
2670
  }
1923
2671
  const normalizedModel = typeof parsed.model === "string" ? parsed.model.trim().toLowerCase() : "";
2672
+ const resolvedModel = resolveModelAlias(normalizedModel);
2673
+ const wasAlias = resolvedModel !== normalizedModel;
1924
2674
  const isAutoModel = normalizedModel === AUTO_MODEL.toLowerCase() || normalizedModel === AUTO_MODEL_SHORT.toLowerCase();
1925
2675
  console.log(
1926
- `[ClawRouter] Received model: "${parsed.model}" -> normalized: "${normalizedModel}", isAuto: ${isAutoModel}`
2676
+ `[ClawRouter] Received model: "${parsed.model}" -> normalized: "${normalizedModel}"${wasAlias ? ` -> alias: "${resolvedModel}"` : ""}, isAuto: ${isAutoModel}`
1927
2677
  );
2678
+ if (wasAlias && !isAutoModel) {
2679
+ parsed.model = resolvedModel;
2680
+ modelId = resolvedModel;
2681
+ bodyModified = true;
2682
+ }
1928
2683
  if (isAutoModel) {
1929
- const messages = parsed.messages;
1930
- let lastUserMsg;
1931
- if (messages) {
1932
- for (let i = messages.length - 1; i >= 0; i--) {
1933
- if (messages[i].role === "user") {
1934
- lastUserMsg = messages[i];
1935
- break;
2684
+ const sessionId = getSessionId(req.headers);
2685
+ const existingSession = sessionId ? sessionStore.getSession(sessionId) : void 0;
2686
+ if (existingSession) {
2687
+ console.log(
2688
+ `[ClawRouter] Session ${sessionId?.slice(0, 8)}... using pinned model: ${existingSession.model}`
2689
+ );
2690
+ parsed.model = existingSession.model;
2691
+ modelId = existingSession.model;
2692
+ bodyModified = true;
2693
+ sessionStore.touchSession(sessionId);
2694
+ } else {
2695
+ const messages = parsed.messages;
2696
+ let lastUserMsg;
2697
+ if (messages) {
2698
+ for (let i = messages.length - 1; i >= 0; i--) {
2699
+ if (messages[i].role === "user") {
2700
+ lastUserMsg = messages[i];
2701
+ break;
2702
+ }
2703
+ }
2704
+ }
2705
+ const systemMsg = messages?.find((m) => m.role === "system");
2706
+ const prompt = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : "";
2707
+ const systemPrompt = typeof systemMsg?.content === "string" ? systemMsg.content : void 0;
2708
+ const tools = parsed.tools;
2709
+ const hasTools = Array.isArray(tools) && tools.length > 0;
2710
+ const effectiveRouterOpts = hasTools ? {
2711
+ ...routerOpts,
2712
+ config: {
2713
+ ...routerOpts.config,
2714
+ overrides: { ...routerOpts.config.overrides, agenticMode: true }
1936
2715
  }
2716
+ } : routerOpts;
2717
+ if (hasTools) {
2718
+ console.log(`[ClawRouter] Tools detected (${tools.length}), forcing agentic mode`);
2719
+ }
2720
+ routingDecision = route(prompt, systemPrompt, maxTokens, effectiveRouterOpts);
2721
+ parsed.model = routingDecision.model;
2722
+ modelId = routingDecision.model;
2723
+ bodyModified = true;
2724
+ if (sessionId) {
2725
+ sessionStore.setSession(sessionId, routingDecision.model, routingDecision.tier);
2726
+ console.log(
2727
+ `[ClawRouter] Session ${sessionId.slice(0, 8)}... pinned to model: ${routingDecision.model}`
2728
+ );
1937
2729
  }
2730
+ options.onRouted?.(routingDecision);
1938
2731
  }
1939
- const systemMsg = messages?.find((m) => m.role === "system");
1940
- const prompt = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : "";
1941
- const systemPrompt = typeof systemMsg?.content === "string" ? systemMsg.content : void 0;
1942
- routingDecision = route(prompt, systemPrompt, maxTokens, routerOpts);
1943
- parsed.model = routingDecision.model;
1944
- modelId = routingDecision.model;
1945
- bodyModified = true;
1946
- options.onRouted?.(routingDecision);
1947
2732
  }
1948
2733
  if (bodyModified) {
1949
2734
  body = Buffer.from(JSON.stringify(parsed));
@@ -1970,37 +2755,51 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
1970
2755
  }
1971
2756
  deduplicator.markInflight(dedupKey);
1972
2757
  let estimatedCostMicros;
1973
- if (modelId && !options.skipBalanceCheck) {
2758
+ const isFreeModel = modelId === FREE_MODEL;
2759
+ if (modelId && !options.skipBalanceCheck && !isFreeModel) {
1974
2760
  const estimated = estimateAmount(modelId, body.length, maxTokens);
1975
2761
  if (estimated) {
1976
2762
  estimatedCostMicros = BigInt(estimated);
1977
2763
  const bufferedCostMicros = estimatedCostMicros * BigInt(Math.ceil(BALANCE_CHECK_BUFFER * 100)) / 100n;
1978
2764
  const sufficiency = await balanceMonitor.checkSufficient(bufferedCostMicros);
1979
- if (sufficiency.info.isEmpty) {
1980
- deduplicator.removeInflight(dedupKey);
1981
- const error = new EmptyWalletError(sufficiency.info.walletAddress);
1982
- options.onInsufficientFunds?.({
1983
- balanceUSD: sufficiency.info.balanceUSD,
1984
- requiredUSD: balanceMonitor.formatUSDC(bufferedCostMicros),
1985
- walletAddress: sufficiency.info.walletAddress
1986
- });
1987
- throw error;
1988
- }
1989
- if (!sufficiency.sufficient) {
1990
- deduplicator.removeInflight(dedupKey);
1991
- const error = new InsufficientFundsError({
1992
- currentBalanceUSD: sufficiency.info.balanceUSD,
1993
- requiredUSD: balanceMonitor.formatUSDC(bufferedCostMicros),
1994
- walletAddress: sufficiency.info.walletAddress
1995
- });
1996
- options.onInsufficientFunds?.({
1997
- balanceUSD: sufficiency.info.balanceUSD,
1998
- requiredUSD: balanceMonitor.formatUSDC(bufferedCostMicros),
1999
- walletAddress: sufficiency.info.walletAddress
2000
- });
2001
- throw error;
2002
- }
2003
- if (sufficiency.info.isLow) {
2765
+ if (sufficiency.info.isEmpty || !sufficiency.sufficient) {
2766
+ if (routingDecision) {
2767
+ console.log(
2768
+ `[ClawRouter] Wallet ${sufficiency.info.isEmpty ? "empty" : "insufficient"} ($${sufficiency.info.balanceUSD}), falling back to free model: ${FREE_MODEL}`
2769
+ );
2770
+ modelId = FREE_MODEL;
2771
+ const parsed = JSON.parse(body.toString());
2772
+ parsed.model = FREE_MODEL;
2773
+ body = Buffer.from(JSON.stringify(parsed));
2774
+ options.onLowBalance?.({
2775
+ balanceUSD: sufficiency.info.balanceUSD,
2776
+ walletAddress: sufficiency.info.walletAddress
2777
+ });
2778
+ } else {
2779
+ deduplicator.removeInflight(dedupKey);
2780
+ if (sufficiency.info.isEmpty) {
2781
+ const error = new EmptyWalletError(sufficiency.info.walletAddress);
2782
+ options.onInsufficientFunds?.({
2783
+ balanceUSD: sufficiency.info.balanceUSD,
2784
+ requiredUSD: balanceMonitor.formatUSDC(bufferedCostMicros),
2785
+ walletAddress: sufficiency.info.walletAddress
2786
+ });
2787
+ throw error;
2788
+ } else {
2789
+ const error = new InsufficientFundsError({
2790
+ currentBalanceUSD: sufficiency.info.balanceUSD,
2791
+ requiredUSD: balanceMonitor.formatUSDC(bufferedCostMicros),
2792
+ walletAddress: sufficiency.info.walletAddress
2793
+ });
2794
+ options.onInsufficientFunds?.({
2795
+ balanceUSD: sufficiency.info.balanceUSD,
2796
+ requiredUSD: balanceMonitor.formatUSDC(bufferedCostMicros),
2797
+ walletAddress: sufficiency.info.walletAddress
2798
+ });
2799
+ throw error;
2800
+ }
2801
+ }
2802
+ } else if (sufficiency.info.isLow) {
2004
2803
  options.onLowBalance?.({
2005
2804
  balanceUSD: sufficiency.info.balanceUSD,
2006
2805
  walletAddress: sufficiency.info.walletAddress
@@ -2052,8 +2851,24 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
2052
2851
  try {
2053
2852
  let modelsToTry;
2054
2853
  if (routingDecision) {
2055
- modelsToTry = getFallbackChain(routingDecision.tier, routerOpts.config.tiers);
2056
- modelsToTry = modelsToTry.slice(0, MAX_FALLBACK_ATTEMPTS);
2854
+ const estimatedInputTokens = Math.ceil(body.length / 4);
2855
+ const estimatedTotalTokens = estimatedInputTokens + maxTokens;
2856
+ const useAgenticTiers = routingDecision.reasoning?.includes("agentic") && routerOpts.config.agenticTiers;
2857
+ const tierConfigs = useAgenticTiers ? routerOpts.config.agenticTiers : routerOpts.config.tiers;
2858
+ const fullChain = getFallbackChain(routingDecision.tier, tierConfigs);
2859
+ const contextFiltered = getFallbackChainFiltered(
2860
+ routingDecision.tier,
2861
+ tierConfigs,
2862
+ estimatedTotalTokens,
2863
+ getModelContextWindow
2864
+ );
2865
+ const contextExcluded = fullChain.filter((m) => !contextFiltered.includes(m));
2866
+ if (contextExcluded.length > 0) {
2867
+ console.log(
2868
+ `[ClawRouter] Context filter (~${estimatedTotalTokens} tokens): excluded ${contextExcluded.join(", ")}`
2869
+ );
2870
+ }
2871
+ modelsToTry = contextFiltered.slice(0, MAX_FALLBACK_ATTEMPTS);
2057
2872
  } else {
2058
2873
  modelsToTry = modelId ? [modelId] : [];
2059
2874
  }
@@ -2196,6 +3011,18 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
2196
3011
  res.write(contentData);
2197
3012
  responseChunks.push(Buffer.from(contentData));
2198
3013
  }
3014
+ const toolCalls = choice.message?.tool_calls ?? choice.delta?.tool_calls;
3015
+ if (toolCalls && toolCalls.length > 0) {
3016
+ const toolCallChunk = {
3017
+ ...baseChunk,
3018
+ choices: [{ index, delta: { tool_calls: toolCalls }, finish_reason: null }]
3019
+ };
3020
+ const toolCallData = `data: ${JSON.stringify(toolCallChunk)}
3021
+
3022
+ `;
3023
+ res.write(toolCallData);
3024
+ responseChunks.push(Buffer.from(toolCallData));
3025
+ }
2199
3026
  const finishChunk = {
2200
3027
  ...baseChunk,
2201
3028
  choices: [{ index, delta: {}, finish_reason: choice.finish_reason ?? "stop" }]
@@ -2227,7 +3054,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
2227
3054
  } else {
2228
3055
  const responseHeaders = {};
2229
3056
  upstream.headers.forEach((value, key) => {
2230
- if (key === "transfer-encoding" || key === "connection") return;
3057
+ if (key === "transfer-encoding" || key === "connection" || key === "content-encoding") return;
2231
3058
  responseHeaders[key] = value;
2232
3059
  });
2233
3060
  res.writeHead(upstream.status, responseHeaders);
@@ -2273,7 +3100,10 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
2273
3100
  const entry = {
2274
3101
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2275
3102
  model: routingDecision.model,
3103
+ tier: routingDecision.tier,
2276
3104
  cost: routingDecision.costEstimate,
3105
+ baselineCost: routingDecision.baselineCost,
3106
+ savings: routingDecision.savings,
2277
3107
  latencyMs: Date.now() - startTime
2278
3108
  };
2279
3109
  logUsage(entry).catch(() => {
@@ -2282,15 +3112,15 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
2282
3112
  }
2283
3113
 
2284
3114
  // src/auth.ts
2285
- import { writeFile, readFile, mkdir as mkdir2 } from "fs/promises";
2286
- import { join as join3 } from "path";
2287
- import { homedir as homedir2 } from "os";
3115
+ import { writeFile, readFile as readFile2, mkdir as mkdir2 } from "fs/promises";
3116
+ import { join as join4 } from "path";
3117
+ import { homedir as homedir3 } from "os";
2288
3118
  import { generatePrivateKey, privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
2289
- var WALLET_DIR = join3(homedir2(), ".openclaw", "blockrun");
2290
- var WALLET_FILE = join3(WALLET_DIR, "wallet.key");
3119
+ var WALLET_DIR = join4(homedir3(), ".openclaw", "blockrun");
3120
+ var WALLET_FILE = join4(WALLET_DIR, "wallet.key");
2291
3121
  async function loadSavedWallet() {
2292
3122
  try {
2293
- const key = (await readFile(WALLET_FILE, "utf-8")).trim();
3123
+ const key = (await readFile2(WALLET_FILE, "utf-8")).trim();
2294
3124
  if (key.startsWith("0x") && key.length === 66) return key;
2295
3125
  } catch {
2296
3126
  }
@@ -2320,8 +3150,8 @@ async function resolveOrGenerateWalletKey() {
2320
3150
 
2321
3151
  // src/index.ts
2322
3152
  import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from "fs";
2323
- import { homedir as homedir3 } from "os";
2324
- import { join as join4 } from "path";
3153
+ import { homedir as homedir4 } from "os";
3154
+ import { join as join5 } from "path";
2325
3155
  import { privateKeyToAccount as privateKeyToAccount4 } from "viem/accounts";
2326
3156
 
2327
3157
  // src/retry.ts
@@ -2386,7 +3216,7 @@ function isCompletionMode() {
2386
3216
  return args.some((arg, i) => arg === "completion" && i >= 1 && i <= 3);
2387
3217
  }
2388
3218
  function injectModelsConfig(logger) {
2389
- const configPath = join4(homedir3(), ".openclaw", "openclaw.json");
3219
+ const configPath = join5(homedir4(), ".openclaw", "openclaw.json");
2390
3220
  if (!existsSync(configPath)) {
2391
3221
  logger.info("OpenClaw config not found, skipping models injection");
2392
3222
  return;
@@ -2433,7 +3263,7 @@ function injectModelsConfig(logger) {
2433
3263
  }
2434
3264
  }
2435
3265
  function injectAuthProfile(logger) {
2436
- const agentsDir = join4(homedir3(), ".openclaw", "agents");
3266
+ const agentsDir = join5(homedir4(), ".openclaw", "agents");
2437
3267
  if (!existsSync(agentsDir)) {
2438
3268
  try {
2439
3269
  mkdirSync(agentsDir, { recursive: true });
@@ -2450,8 +3280,8 @@ function injectAuthProfile(logger) {
2450
3280
  agents = ["main", ...agents];
2451
3281
  }
2452
3282
  for (const agentId of agents) {
2453
- const authDir = join4(agentsDir, agentId, "agent");
2454
- const authPath = join4(authDir, "auth-profiles.json");
3283
+ const authDir = join5(agentsDir, agentId, "agent");
3284
+ const authPath = join5(authDir, "auth-profiles.json");
2455
3285
  if (!existsSync(authDir)) {
2456
3286
  try {
2457
3287
  mkdirSync(authDir, { recursive: true });
@@ -2552,6 +3382,34 @@ async function startProxyInBackground(api) {
2552
3382
  activeProxyHandle = proxy;
2553
3383
  api.logger.info(`BlockRun provider active \u2014 ${proxy.baseUrl}/v1 (smart routing enabled)`);
2554
3384
  }
3385
+ async function createStatsCommand() {
3386
+ return {
3387
+ name: "stats",
3388
+ description: "Show ClawRouter usage statistics and cost savings",
3389
+ acceptsArgs: true,
3390
+ requireAuth: false,
3391
+ handler: async (ctx) => {
3392
+ const arg = ctx.args?.trim().toLowerCase() || "7";
3393
+ const days = parseInt(arg, 10) || 7;
3394
+ try {
3395
+ const stats = await getStats(Math.min(days, 30));
3396
+ const ascii = formatStatsAscii(stats);
3397
+ return {
3398
+ text: [
3399
+ "```",
3400
+ ascii,
3401
+ "```"
3402
+ ].join("\n")
3403
+ };
3404
+ } catch (err) {
3405
+ return {
3406
+ text: `Failed to load stats: ${err instanceof Error ? err.message : String(err)}`,
3407
+ isError: true
3408
+ };
3409
+ }
3410
+ }
3411
+ };
3412
+ }
2555
3413
  async function createWalletCommand() {
2556
3414
  return {
2557
3415
  name: "wallet",
@@ -2673,6 +3531,13 @@ var plugin = {
2673
3531
  `Failed to register /wallet command: ${err instanceof Error ? err.message : String(err)}`
2674
3532
  );
2675
3533
  });
3534
+ createStatsCommand().then((statsCommand) => {
3535
+ api.registerCommand(statsCommand);
3536
+ }).catch((err) => {
3537
+ api.logger.warn(
3538
+ `Failed to register /stats command: ${err instanceof Error ? err.message : String(err)}`
3539
+ );
3540
+ });
2676
3541
  api.registerService({
2677
3542
  id: "clawrouter-proxy",
2678
3543
  start: () => {
@@ -2705,24 +3570,36 @@ export {
2705
3570
  BalanceMonitor,
2706
3571
  DEFAULT_RETRY_CONFIG,
2707
3572
  DEFAULT_ROUTING_CONFIG,
3573
+ DEFAULT_SESSION_CONFIG,
2708
3574
  EmptyWalletError,
2709
3575
  InsufficientFundsError,
3576
+ MODEL_ALIASES,
2710
3577
  OPENCLAW_MODELS,
2711
3578
  PaymentCache,
2712
3579
  RequestDeduplicator,
2713
3580
  RpcError,
3581
+ SessionStore,
2714
3582
  blockrunProvider,
2715
3583
  buildProviderModels,
2716
3584
  createPaymentFetch,
2717
3585
  index_default as default,
2718
3586
  fetchWithRetry,
3587
+ formatStatsAscii,
3588
+ getAgenticModels,
3589
+ getFallbackChain,
3590
+ getFallbackChainFiltered,
3591
+ getModelContextWindow,
2719
3592
  getProxyPort,
3593
+ getSessionId,
3594
+ getStats,
3595
+ isAgenticModel,
2720
3596
  isBalanceError,
2721
3597
  isEmptyWalletError,
2722
3598
  isInsufficientFundsError,
2723
3599
  isRetryable,
2724
3600
  isRpcError,
2725
3601
  logUsage,
3602
+ resolveModelAlias,
2726
3603
  route,
2727
3604
  startProxy
2728
3605
  };