@blockrun/clawrouter 0.4.7 → 0.5.1

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 = {
@@ -1249,6 +1450,78 @@ var DEFAULT_ROUTING_CONFIG = {
1249
1450
  "zero-knowledge",
1250
1451
  "gitterbasiert"
1251
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"
1524
+ ],
1252
1525
  // Dimension weights (sum to 1.0)
1253
1526
  dimensionWeights: {
1254
1527
  tokenCount: 0.08,
@@ -1256,7 +1529,8 @@ var DEFAULT_ROUTING_CONFIG = {
1256
1529
  reasoningMarkers: 0.18,
1257
1530
  technicalTerms: 0.1,
1258
1531
  creativeMarkers: 0.05,
1259
- simpleIndicators: 0.12,
1532
+ simpleIndicators: 0.02,
1533
+ // Reduced from 0.12 to make room for agenticTask
1260
1534
  multiStepPatterns: 0.12,
1261
1535
  questionComplexity: 0.05,
1262
1536
  imperativeVerbs: 0.03,
@@ -1264,7 +1538,9 @@ var DEFAULT_ROUTING_CONFIG = {
1264
1538
  outputFormat: 0.03,
1265
1539
  referenceComplexity: 0.02,
1266
1540
  negationComplexity: 0.01,
1267
- domainSpecificity: 0.02
1541
+ domainSpecificity: 0.02,
1542
+ agenticTask: 0.1
1543
+ // Significant weight for agentic detection
1268
1544
  },
1269
1545
  // Tier boundaries on weighted score axis
1270
1546
  tierBoundaries: {
@@ -1280,25 +1556,53 @@ var DEFAULT_ROUTING_CONFIG = {
1280
1556
  tiers: {
1281
1557
  SIMPLE: {
1282
1558
  primary: "google/gemini-2.5-flash",
1283
- 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: [
1565
+ "deepseek/deepseek-chat",
1566
+ "xai/grok-4-fast-non-reasoning",
1567
+ "google/gemini-2.5-flash"
1568
+ ]
1569
+ },
1570
+ COMPLEX: {
1571
+ primary: "google/gemini-2.5-pro",
1572
+ fallback: ["anthropic/claude-sonnet-4", "xai/grok-4-0709", "openai/gpt-4o"]
1573
+ },
1574
+ REASONING: {
1575
+ primary: "xai/grok-4-fast-reasoning",
1576
+ // Ultra-cheap reasoning $0.20/$0.50
1577
+ fallback: ["deepseek/deepseek-reasoner", "moonshot/kimi-k2.5", "google/gemini-2.5-pro"]
1578
+ }
1579
+ },
1580
+ // Agentic tier configs - models that excel at multi-step autonomous tasks
1581
+ agenticTiers: {
1582
+ SIMPLE: {
1583
+ primary: "anthropic/claude-haiku-4.5",
1584
+ fallback: ["moonshot/kimi-k2.5", "xai/grok-4-fast-non-reasoning", "openai/gpt-4o-mini"]
1284
1585
  },
1285
1586
  MEDIUM: {
1286
- primary: "deepseek/deepseek-chat",
1287
- fallback: ["google/gemini-2.5-flash", "openai/gpt-4o-mini"]
1587
+ primary: "xai/grok-code-fast-1",
1588
+ // Code specialist for agentic coding
1589
+ fallback: ["moonshot/kimi-k2.5", "anthropic/claude-haiku-4.5", "anthropic/claude-sonnet-4"]
1288
1590
  },
1289
1591
  COMPLEX: {
1290
- primary: "anthropic/claude-opus-4",
1291
- fallback: ["anthropic/claude-sonnet-4", "openai/gpt-4o"]
1592
+ primary: "anthropic/claude-sonnet-4",
1593
+ fallback: ["anthropic/claude-opus-4", "xai/grok-4-0709", "openai/gpt-4o"]
1292
1594
  },
1293
1595
  REASONING: {
1294
- primary: "deepseek/deepseek-reasoner",
1295
- fallback: ["moonshot/kimi-k2.5", "google/gemini-2.5-pro"]
1596
+ primary: "xai/grok-4-fast-reasoning",
1597
+ // Cheap reasoning for agentic tasks
1598
+ fallback: ["moonshot/kimi-k2.5", "anthropic/claude-sonnet-4", "deepseek/deepseek-reasoner"]
1296
1599
  }
1297
1600
  },
1298
1601
  overrides: {
1299
1602
  maxTokensForceComplex: 1e5,
1300
1603
  structuredOutputMinTier: "MEDIUM",
1301
- ambiguousDefaultTier: "MEDIUM"
1604
+ ambiguousDefaultTier: "MEDIUM",
1605
+ agenticMode: false
1302
1606
  }
1303
1607
  };
1304
1608
 
@@ -1307,24 +1611,29 @@ function route(prompt, systemPrompt, maxOutputTokens, options) {
1307
1611
  const { config, modelPricing } = options;
1308
1612
  const fullText = `${systemPrompt ?? ""} ${prompt}`;
1309
1613
  const estimatedTokens = Math.ceil(fullText.length / 4);
1614
+ const ruleResult = classifyByRules(prompt, systemPrompt, estimatedTokens, config.scoring);
1615
+ const agenticScore = ruleResult.agenticScore ?? 0;
1616
+ const isAutoAgentic = agenticScore >= 0.6;
1617
+ const isExplicitAgentic = config.overrides.agenticMode ?? false;
1618
+ const useAgenticTiers = (isAutoAgentic || isExplicitAgentic) && config.agenticTiers != null;
1619
+ const tierConfigs = useAgenticTiers ? config.agenticTiers : config.tiers;
1310
1620
  if (estimatedTokens > config.overrides.maxTokensForceComplex) {
1311
1621
  return selectModel(
1312
1622
  "COMPLEX",
1313
1623
  0.95,
1314
1624
  "rules",
1315
- `Input exceeds ${config.overrides.maxTokensForceComplex} tokens`,
1316
- config.tiers,
1625
+ `Input exceeds ${config.overrides.maxTokensForceComplex} tokens${useAgenticTiers ? " | agentic" : ""}`,
1626
+ tierConfigs,
1317
1627
  modelPricing,
1318
1628
  estimatedTokens,
1319
1629
  maxOutputTokens
1320
1630
  );
1321
1631
  }
1322
1632
  const hasStructuredOutput = systemPrompt ? /json|structured|schema/i.test(systemPrompt) : false;
1323
- const ruleResult = classifyByRules(prompt, systemPrompt, estimatedTokens, config.scoring);
1324
1633
  let tier;
1325
1634
  let confidence;
1326
1635
  const method = "rules";
1327
- let reasoning = `score=${ruleResult.score} | ${ruleResult.signals.join(", ")}`;
1636
+ let reasoning = `score=${ruleResult.score.toFixed(2)} | ${ruleResult.signals.join(", ")}`;
1328
1637
  if (ruleResult.tier !== null) {
1329
1638
  tier = ruleResult.tier;
1330
1639
  confidence = ruleResult.confidence;
@@ -1341,12 +1650,17 @@ function route(prompt, systemPrompt, maxOutputTokens, options) {
1341
1650
  tier = minTier;
1342
1651
  }
1343
1652
  }
1653
+ if (isAutoAgentic) {
1654
+ reasoning += " | auto-agentic";
1655
+ } else if (isExplicitAgentic) {
1656
+ reasoning += " | agentic";
1657
+ }
1344
1658
  return selectModel(
1345
1659
  tier,
1346
1660
  confidence,
1347
1661
  method,
1348
1662
  reasoning,
1349
- config.tiers,
1663
+ tierConfigs,
1350
1664
  modelPricing,
1351
1665
  estimatedTokens,
1352
1666
  maxOutputTokens
@@ -1374,6 +1688,176 @@ async function logUsage(entry) {
1374
1688
  }
1375
1689
  }
1376
1690
 
1691
+ // src/stats.ts
1692
+ import { readFile, readdir } from "fs/promises";
1693
+ import { join as join2 } from "path";
1694
+ import { homedir as homedir2 } from "os";
1695
+ var LOG_DIR2 = join2(homedir2(), ".openclaw", "blockrun", "logs");
1696
+ async function parseLogFile(filePath) {
1697
+ try {
1698
+ const content = await readFile(filePath, "utf-8");
1699
+ const lines = content.trim().split("\n").filter(Boolean);
1700
+ return lines.map((line) => {
1701
+ const entry = JSON.parse(line);
1702
+ return {
1703
+ timestamp: entry.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
1704
+ model: entry.model || "unknown",
1705
+ tier: entry.tier || "UNKNOWN",
1706
+ cost: entry.cost || 0,
1707
+ baselineCost: entry.baselineCost || entry.cost || 0,
1708
+ savings: entry.savings || 0,
1709
+ latencyMs: entry.latencyMs || 0
1710
+ };
1711
+ });
1712
+ } catch {
1713
+ return [];
1714
+ }
1715
+ }
1716
+ async function getLogFiles() {
1717
+ try {
1718
+ const files = await readdir(LOG_DIR2);
1719
+ return files.filter((f) => f.startsWith("usage-") && f.endsWith(".jsonl")).sort().reverse();
1720
+ } catch {
1721
+ return [];
1722
+ }
1723
+ }
1724
+ function aggregateDay(date, entries) {
1725
+ const byTier = {};
1726
+ const byModel = {};
1727
+ let totalLatency = 0;
1728
+ for (const entry of entries) {
1729
+ if (!byTier[entry.tier]) byTier[entry.tier] = { count: 0, cost: 0 };
1730
+ byTier[entry.tier].count++;
1731
+ byTier[entry.tier].cost += entry.cost;
1732
+ if (!byModel[entry.model]) byModel[entry.model] = { count: 0, cost: 0 };
1733
+ byModel[entry.model].count++;
1734
+ byModel[entry.model].cost += entry.cost;
1735
+ totalLatency += entry.latencyMs;
1736
+ }
1737
+ const totalCost = entries.reduce((sum, e) => sum + e.cost, 0);
1738
+ const totalBaselineCost = entries.reduce((sum, e) => sum + e.baselineCost, 0);
1739
+ return {
1740
+ date,
1741
+ totalRequests: entries.length,
1742
+ totalCost,
1743
+ totalBaselineCost,
1744
+ totalSavings: totalBaselineCost - totalCost,
1745
+ avgLatencyMs: entries.length > 0 ? totalLatency / entries.length : 0,
1746
+ byTier,
1747
+ byModel
1748
+ };
1749
+ }
1750
+ async function getStats(days = 7) {
1751
+ const logFiles = await getLogFiles();
1752
+ const filesToRead = logFiles.slice(0, days);
1753
+ const dailyBreakdown = [];
1754
+ const allByTier = {};
1755
+ const allByModel = {};
1756
+ let totalRequests = 0;
1757
+ let totalCost = 0;
1758
+ let totalBaselineCost = 0;
1759
+ let totalLatency = 0;
1760
+ for (const file of filesToRead) {
1761
+ const date = file.replace("usage-", "").replace(".jsonl", "");
1762
+ const filePath = join2(LOG_DIR2, file);
1763
+ const entries = await parseLogFile(filePath);
1764
+ if (entries.length === 0) continue;
1765
+ const dayStats = aggregateDay(date, entries);
1766
+ dailyBreakdown.push(dayStats);
1767
+ totalRequests += dayStats.totalRequests;
1768
+ totalCost += dayStats.totalCost;
1769
+ totalBaselineCost += dayStats.totalBaselineCost;
1770
+ totalLatency += dayStats.avgLatencyMs * dayStats.totalRequests;
1771
+ for (const [tier, stats] of Object.entries(dayStats.byTier)) {
1772
+ if (!allByTier[tier]) allByTier[tier] = { count: 0, cost: 0 };
1773
+ allByTier[tier].count += stats.count;
1774
+ allByTier[tier].cost += stats.cost;
1775
+ }
1776
+ for (const [model, stats] of Object.entries(dayStats.byModel)) {
1777
+ if (!allByModel[model]) allByModel[model] = { count: 0, cost: 0 };
1778
+ allByModel[model].count += stats.count;
1779
+ allByModel[model].cost += stats.cost;
1780
+ }
1781
+ }
1782
+ const byTierWithPercentage = {};
1783
+ for (const [tier, stats] of Object.entries(allByTier)) {
1784
+ byTierWithPercentage[tier] = {
1785
+ ...stats,
1786
+ percentage: totalRequests > 0 ? stats.count / totalRequests * 100 : 0
1787
+ };
1788
+ }
1789
+ const byModelWithPercentage = {};
1790
+ for (const [model, stats] of Object.entries(allByModel)) {
1791
+ byModelWithPercentage[model] = {
1792
+ ...stats,
1793
+ percentage: totalRequests > 0 ? stats.count / totalRequests * 100 : 0
1794
+ };
1795
+ }
1796
+ const totalSavings = totalBaselineCost - totalCost;
1797
+ const savingsPercentage = totalBaselineCost > 0 ? totalSavings / totalBaselineCost * 100 : 0;
1798
+ return {
1799
+ period: days === 1 ? "today" : `last ${days} days`,
1800
+ totalRequests,
1801
+ totalCost,
1802
+ totalBaselineCost,
1803
+ totalSavings,
1804
+ savingsPercentage,
1805
+ avgLatencyMs: totalRequests > 0 ? totalLatency / totalRequests : 0,
1806
+ avgCostPerRequest: totalRequests > 0 ? totalCost / totalRequests : 0,
1807
+ byTier: byTierWithPercentage,
1808
+ byModel: byModelWithPercentage,
1809
+ dailyBreakdown: dailyBreakdown.reverse()
1810
+ // Oldest first for charts
1811
+ };
1812
+ }
1813
+ function formatStatsAscii(stats) {
1814
+ const lines = [];
1815
+ 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");
1816
+ lines.push("\u2551 ClawRouter Usage Statistics \u2551");
1817
+ 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");
1818
+ lines.push(`\u2551 Period: ${stats.period.padEnd(49)}\u2551`);
1819
+ lines.push(`\u2551 Total Requests: ${stats.totalRequests.toString().padEnd(41)}\u2551`);
1820
+ lines.push(`\u2551 Total Cost: $${stats.totalCost.toFixed(4).padEnd(43)}\u2551`);
1821
+ lines.push(`\u2551 Baseline Cost (Opus): $${stats.totalBaselineCost.toFixed(4).padEnd(33)}\u2551`);
1822
+ lines.push(
1823
+ `\u2551 \u{1F4B0} Total Saved: $${stats.totalSavings.toFixed(4)} (${stats.savingsPercentage.toFixed(1)}%)`.padEnd(
1824
+ 61
1825
+ ) + "\u2551"
1826
+ );
1827
+ lines.push(`\u2551 Avg Latency: ${stats.avgLatencyMs.toFixed(0)}ms`.padEnd(61) + "\u2551");
1828
+ 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");
1829
+ lines.push("\u2551 Routing by Tier: \u2551");
1830
+ const tierOrder = ["SIMPLE", "MEDIUM", "COMPLEX", "REASONING"];
1831
+ for (const tier of tierOrder) {
1832
+ const data = stats.byTier[tier];
1833
+ if (data) {
1834
+ const bar = "\u2588".repeat(Math.min(20, Math.round(data.percentage / 5)));
1835
+ const line = `\u2551 ${tier.padEnd(10)} ${bar.padEnd(20)} ${data.percentage.toFixed(1).padStart(5)}% (${data.count})`;
1836
+ lines.push(line.padEnd(61) + "\u2551");
1837
+ }
1838
+ }
1839
+ 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");
1840
+ lines.push("\u2551 Top Models: \u2551");
1841
+ const sortedModels = Object.entries(stats.byModel).sort((a, b) => b[1].count - a[1].count).slice(0, 5);
1842
+ for (const [model, data] of sortedModels) {
1843
+ const shortModel = model.length > 25 ? model.slice(0, 22) + "..." : model;
1844
+ const line = `\u2551 ${shortModel.padEnd(25)} ${data.count.toString().padStart(5)} reqs $${data.cost.toFixed(4)}`;
1845
+ lines.push(line.padEnd(61) + "\u2551");
1846
+ }
1847
+ if (stats.dailyBreakdown.length > 0) {
1848
+ 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");
1849
+ lines.push("\u2551 Daily Breakdown: \u2551");
1850
+ lines.push("\u2551 Date Requests Cost Saved \u2551");
1851
+ for (const day of stats.dailyBreakdown.slice(-7)) {
1852
+ const saved = day.totalBaselineCost - day.totalCost;
1853
+ const line = `\u2551 ${day.date} ${day.totalRequests.toString().padStart(6)} $${day.totalCost.toFixed(4).padStart(8)} $${saved.toFixed(4)}`;
1854
+ lines.push(line.padEnd(61) + "\u2551");
1855
+ }
1856
+ }
1857
+ 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");
1858
+ return lines.join("\n");
1859
+ }
1860
+
1377
1861
  // src/dedup.ts
1378
1862
  import { createHash } from "crypto";
1379
1863
  var DEFAULT_TTL_MS2 = 3e4;
@@ -1667,18 +2151,149 @@ var BalanceMonitor = class {
1667
2151
  // src/version.ts
1668
2152
  import { createRequire } from "module";
1669
2153
  import { fileURLToPath } from "url";
1670
- import { dirname, join as join2 } from "path";
2154
+ import { dirname, join as join3 } from "path";
1671
2155
  var __filename = fileURLToPath(import.meta.url);
1672
2156
  var __dirname = dirname(__filename);
1673
2157
  var require2 = createRequire(import.meta.url);
1674
- var pkg = require2(join2(__dirname, "..", "package.json"));
2158
+ var pkg = require2(join3(__dirname, "..", "package.json"));
1675
2159
  var VERSION = pkg.version;
1676
2160
  var USER_AGENT = `clawrouter/${VERSION}`;
1677
2161
 
2162
+ // src/session.ts
2163
+ var DEFAULT_SESSION_CONFIG = {
2164
+ enabled: false,
2165
+ timeoutMs: 30 * 60 * 1e3,
2166
+ // 30 minutes
2167
+ headerName: "x-session-id"
2168
+ };
2169
+ var SessionStore = class {
2170
+ sessions = /* @__PURE__ */ new Map();
2171
+ config;
2172
+ cleanupInterval = null;
2173
+ constructor(config = {}) {
2174
+ this.config = { ...DEFAULT_SESSION_CONFIG, ...config };
2175
+ if (this.config.enabled) {
2176
+ this.cleanupInterval = setInterval(() => this.cleanup(), 5 * 60 * 1e3);
2177
+ }
2178
+ }
2179
+ /**
2180
+ * Get the pinned model for a session, if any.
2181
+ */
2182
+ getSession(sessionId) {
2183
+ if (!this.config.enabled || !sessionId) {
2184
+ return void 0;
2185
+ }
2186
+ const entry = this.sessions.get(sessionId);
2187
+ if (!entry) {
2188
+ return void 0;
2189
+ }
2190
+ const now = Date.now();
2191
+ if (now - entry.lastUsedAt > this.config.timeoutMs) {
2192
+ this.sessions.delete(sessionId);
2193
+ return void 0;
2194
+ }
2195
+ return entry;
2196
+ }
2197
+ /**
2198
+ * Pin a model to a session.
2199
+ */
2200
+ setSession(sessionId, model, tier) {
2201
+ if (!this.config.enabled || !sessionId) {
2202
+ return;
2203
+ }
2204
+ const existing = this.sessions.get(sessionId);
2205
+ const now = Date.now();
2206
+ if (existing) {
2207
+ existing.lastUsedAt = now;
2208
+ existing.requestCount++;
2209
+ if (existing.model !== model) {
2210
+ existing.model = model;
2211
+ existing.tier = tier;
2212
+ }
2213
+ } else {
2214
+ this.sessions.set(sessionId, {
2215
+ model,
2216
+ tier,
2217
+ createdAt: now,
2218
+ lastUsedAt: now,
2219
+ requestCount: 1
2220
+ });
2221
+ }
2222
+ }
2223
+ /**
2224
+ * Touch a session to extend its timeout.
2225
+ */
2226
+ touchSession(sessionId) {
2227
+ if (!this.config.enabled || !sessionId) {
2228
+ return;
2229
+ }
2230
+ const entry = this.sessions.get(sessionId);
2231
+ if (entry) {
2232
+ entry.lastUsedAt = Date.now();
2233
+ entry.requestCount++;
2234
+ }
2235
+ }
2236
+ /**
2237
+ * Clear a specific session.
2238
+ */
2239
+ clearSession(sessionId) {
2240
+ this.sessions.delete(sessionId);
2241
+ }
2242
+ /**
2243
+ * Clear all sessions.
2244
+ */
2245
+ clearAll() {
2246
+ this.sessions.clear();
2247
+ }
2248
+ /**
2249
+ * Get session stats for debugging.
2250
+ */
2251
+ getStats() {
2252
+ const now = Date.now();
2253
+ const sessions = Array.from(this.sessions.entries()).map(([id, entry]) => ({
2254
+ id: id.slice(0, 8) + "...",
2255
+ model: entry.model,
2256
+ age: Math.round((now - entry.createdAt) / 1e3)
2257
+ }));
2258
+ return { count: this.sessions.size, sessions };
2259
+ }
2260
+ /**
2261
+ * Clean up expired sessions.
2262
+ */
2263
+ cleanup() {
2264
+ const now = Date.now();
2265
+ for (const [id, entry] of this.sessions) {
2266
+ if (now - entry.lastUsedAt > this.config.timeoutMs) {
2267
+ this.sessions.delete(id);
2268
+ }
2269
+ }
2270
+ }
2271
+ /**
2272
+ * Stop the cleanup interval.
2273
+ */
2274
+ close() {
2275
+ if (this.cleanupInterval) {
2276
+ clearInterval(this.cleanupInterval);
2277
+ this.cleanupInterval = null;
2278
+ }
2279
+ }
2280
+ };
2281
+ function getSessionId(headers, headerName = DEFAULT_SESSION_CONFIG.headerName) {
2282
+ const value = headers[headerName] || headers[headerName.toLowerCase()];
2283
+ if (typeof value === "string" && value.length > 0) {
2284
+ return value;
2285
+ }
2286
+ if (Array.isArray(value) && value.length > 0) {
2287
+ return value[0];
2288
+ }
2289
+ return void 0;
2290
+ }
2291
+
1678
2292
  // src/proxy.ts
1679
2293
  var BLOCKRUN_API = "https://blockrun.ai/api";
1680
2294
  var AUTO_MODEL = "blockrun/auto";
1681
2295
  var AUTO_MODEL_SHORT = "auto";
2296
+ var FREE_MODEL = "nvidia/gpt-oss-120b";
1682
2297
  var HEARTBEAT_INTERVAL_MS = 2e3;
1683
2298
  var DEFAULT_REQUEST_TIMEOUT_MS = 18e4;
1684
2299
  var DEFAULT_PORT = 8402;
@@ -1857,6 +2472,7 @@ async function startProxy(options) {
1857
2472
  modelPricing
1858
2473
  };
1859
2474
  const deduplicator = new RequestDeduplicator();
2475
+ const sessionStore = new SessionStore(options.sessionConfig);
1860
2476
  const server = createServer(async (req, res) => {
1861
2477
  if (req.url === "/health" || req.url?.startsWith("/health?")) {
1862
2478
  const url = new URL(req.url, "http://localhost");
@@ -1879,6 +2495,37 @@ async function startProxy(options) {
1879
2495
  res.end(JSON.stringify(response));
1880
2496
  return;
1881
2497
  }
2498
+ if (req.url === "/stats" || req.url?.startsWith("/stats?")) {
2499
+ try {
2500
+ const url = new URL(req.url, "http://localhost");
2501
+ const days = parseInt(url.searchParams.get("days") || "7", 10);
2502
+ const stats = await getStats(Math.min(days, 30));
2503
+ res.writeHead(200, {
2504
+ "Content-Type": "application/json",
2505
+ "Cache-Control": "no-cache"
2506
+ });
2507
+ res.end(JSON.stringify(stats, null, 2));
2508
+ } catch (err) {
2509
+ res.writeHead(500, { "Content-Type": "application/json" });
2510
+ res.end(
2511
+ JSON.stringify({
2512
+ error: `Failed to get stats: ${err instanceof Error ? err.message : String(err)}`
2513
+ })
2514
+ );
2515
+ }
2516
+ return;
2517
+ }
2518
+ if (req.url === "/v1/models" && req.method === "GET") {
2519
+ const models = BLOCKRUN_MODELS.filter((m) => m.id !== "blockrun/auto").map((m) => ({
2520
+ id: m.id,
2521
+ object: "model",
2522
+ created: Math.floor(Date.now() / 1e3),
2523
+ owned_by: m.id.split("/")[0] || "unknown"
2524
+ }));
2525
+ res.writeHead(200, { "Content-Type": "application/json" });
2526
+ res.end(JSON.stringify({ object: "list", data: models }));
2527
+ return;
2528
+ }
1882
2529
  if (!req.url?.startsWith("/v1")) {
1883
2530
  res.writeHead(404, { "Content-Type": "application/json" });
1884
2531
  res.end(JSON.stringify({ error: "Not found" }));
@@ -1893,7 +2540,8 @@ async function startProxy(options) {
1893
2540
  options,
1894
2541
  routerOpts,
1895
2542
  deduplicator,
1896
- balanceMonitor
2543
+ balanceMonitor,
2544
+ sessionStore
1897
2545
  );
1898
2546
  } catch (err) {
1899
2547
  const error = err instanceof Error ? err : new Error(String(err));
@@ -1944,6 +2592,7 @@ async function startProxy(options) {
1944
2592
  walletAddress: account.address,
1945
2593
  balanceMonitor,
1946
2594
  close: () => new Promise((res, rej) => {
2595
+ sessionStore.close();
1947
2596
  server.close((err) => err ? rej(err) : res());
1948
2597
  })
1949
2598
  });
@@ -1996,7 +2645,7 @@ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxT
1996
2645
  };
1997
2646
  }
1998
2647
  }
1999
- async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, deduplicator, balanceMonitor) {
2648
+ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, deduplicator, balanceMonitor, sessionStore) {
2000
2649
  const startTime = Date.now();
2001
2650
  const upstreamUrl = `${apiBase}${req.url}`;
2002
2651
  const bodyChunks = [];
@@ -2021,29 +2670,68 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
2021
2670
  bodyModified = true;
2022
2671
  }
2023
2672
  const normalizedModel = typeof parsed.model === "string" ? parsed.model.trim().toLowerCase() : "";
2673
+ const resolvedModel = resolveModelAlias(normalizedModel);
2674
+ const wasAlias = resolvedModel !== normalizedModel;
2024
2675
  const isAutoModel = normalizedModel === AUTO_MODEL.toLowerCase() || normalizedModel === AUTO_MODEL_SHORT.toLowerCase();
2025
2676
  console.log(
2026
- `[ClawRouter] Received model: "${parsed.model}" -> normalized: "${normalizedModel}", isAuto: ${isAutoModel}`
2677
+ `[ClawRouter] Received model: "${parsed.model}" -> normalized: "${normalizedModel}"${wasAlias ? ` -> alias: "${resolvedModel}"` : ""}, isAuto: ${isAutoModel}`
2027
2678
  );
2679
+ if (wasAlias && !isAutoModel) {
2680
+ parsed.model = resolvedModel;
2681
+ modelId = resolvedModel;
2682
+ bodyModified = true;
2683
+ }
2028
2684
  if (isAutoModel) {
2029
- const messages = parsed.messages;
2030
- let lastUserMsg;
2031
- if (messages) {
2032
- for (let i = messages.length - 1; i >= 0; i--) {
2033
- if (messages[i].role === "user") {
2034
- lastUserMsg = messages[i];
2035
- break;
2685
+ const sessionId = getSessionId(
2686
+ req.headers
2687
+ );
2688
+ const existingSession = sessionId ? sessionStore.getSession(sessionId) : void 0;
2689
+ if (existingSession) {
2690
+ console.log(
2691
+ `[ClawRouter] Session ${sessionId?.slice(0, 8)}... using pinned model: ${existingSession.model}`
2692
+ );
2693
+ parsed.model = existingSession.model;
2694
+ modelId = existingSession.model;
2695
+ bodyModified = true;
2696
+ sessionStore.touchSession(sessionId);
2697
+ } else {
2698
+ const messages = parsed.messages;
2699
+ let lastUserMsg;
2700
+ if (messages) {
2701
+ for (let i = messages.length - 1; i >= 0; i--) {
2702
+ if (messages[i].role === "user") {
2703
+ lastUserMsg = messages[i];
2704
+ break;
2705
+ }
2036
2706
  }
2037
2707
  }
2708
+ const systemMsg = messages?.find((m) => m.role === "system");
2709
+ const prompt = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : "";
2710
+ const systemPrompt = typeof systemMsg?.content === "string" ? systemMsg.content : void 0;
2711
+ const tools = parsed.tools;
2712
+ const hasTools = Array.isArray(tools) && tools.length > 0;
2713
+ const effectiveRouterOpts = hasTools ? {
2714
+ ...routerOpts,
2715
+ config: {
2716
+ ...routerOpts.config,
2717
+ overrides: { ...routerOpts.config.overrides, agenticMode: true }
2718
+ }
2719
+ } : routerOpts;
2720
+ if (hasTools) {
2721
+ console.log(`[ClawRouter] Tools detected (${tools.length}), forcing agentic mode`);
2722
+ }
2723
+ routingDecision = route(prompt, systemPrompt, maxTokens, effectiveRouterOpts);
2724
+ parsed.model = routingDecision.model;
2725
+ modelId = routingDecision.model;
2726
+ bodyModified = true;
2727
+ if (sessionId) {
2728
+ sessionStore.setSession(sessionId, routingDecision.model, routingDecision.tier);
2729
+ console.log(
2730
+ `[ClawRouter] Session ${sessionId.slice(0, 8)}... pinned to model: ${routingDecision.model}`
2731
+ );
2732
+ }
2733
+ options.onRouted?.(routingDecision);
2038
2734
  }
2039
- const systemMsg = messages?.find((m) => m.role === "system");
2040
- const prompt = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : "";
2041
- const systemPrompt = typeof systemMsg?.content === "string" ? systemMsg.content : void 0;
2042
- routingDecision = route(prompt, systemPrompt, maxTokens, routerOpts);
2043
- parsed.model = routingDecision.model;
2044
- modelId = routingDecision.model;
2045
- bodyModified = true;
2046
- options.onRouted?.(routingDecision);
2047
2735
  }
2048
2736
  if (bodyModified) {
2049
2737
  body = Buffer.from(JSON.stringify(parsed));
@@ -2070,37 +2758,51 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
2070
2758
  }
2071
2759
  deduplicator.markInflight(dedupKey);
2072
2760
  let estimatedCostMicros;
2073
- if (modelId && !options.skipBalanceCheck) {
2761
+ const isFreeModel = modelId === FREE_MODEL;
2762
+ if (modelId && !options.skipBalanceCheck && !isFreeModel) {
2074
2763
  const estimated = estimateAmount(modelId, body.length, maxTokens);
2075
2764
  if (estimated) {
2076
2765
  estimatedCostMicros = BigInt(estimated);
2077
2766
  const bufferedCostMicros = estimatedCostMicros * BigInt(Math.ceil(BALANCE_CHECK_BUFFER * 100)) / 100n;
2078
2767
  const sufficiency = await balanceMonitor.checkSufficient(bufferedCostMicros);
2079
- if (sufficiency.info.isEmpty) {
2080
- deduplicator.removeInflight(dedupKey);
2081
- const error = new EmptyWalletError(sufficiency.info.walletAddress);
2082
- options.onInsufficientFunds?.({
2083
- balanceUSD: sufficiency.info.balanceUSD,
2084
- requiredUSD: balanceMonitor.formatUSDC(bufferedCostMicros),
2085
- walletAddress: sufficiency.info.walletAddress
2086
- });
2087
- throw error;
2088
- }
2089
- if (!sufficiency.sufficient) {
2090
- deduplicator.removeInflight(dedupKey);
2091
- const error = new InsufficientFundsError({
2092
- currentBalanceUSD: sufficiency.info.balanceUSD,
2093
- requiredUSD: balanceMonitor.formatUSDC(bufferedCostMicros),
2094
- walletAddress: sufficiency.info.walletAddress
2095
- });
2096
- options.onInsufficientFunds?.({
2097
- balanceUSD: sufficiency.info.balanceUSD,
2098
- requiredUSD: balanceMonitor.formatUSDC(bufferedCostMicros),
2099
- walletAddress: sufficiency.info.walletAddress
2100
- });
2101
- throw error;
2102
- }
2103
- if (sufficiency.info.isLow) {
2768
+ if (sufficiency.info.isEmpty || !sufficiency.sufficient) {
2769
+ if (routingDecision) {
2770
+ console.log(
2771
+ `[ClawRouter] Wallet ${sufficiency.info.isEmpty ? "empty" : "insufficient"} ($${sufficiency.info.balanceUSD}), falling back to free model: ${FREE_MODEL}`
2772
+ );
2773
+ modelId = FREE_MODEL;
2774
+ const parsed = JSON.parse(body.toString());
2775
+ parsed.model = FREE_MODEL;
2776
+ body = Buffer.from(JSON.stringify(parsed));
2777
+ options.onLowBalance?.({
2778
+ balanceUSD: sufficiency.info.balanceUSD,
2779
+ walletAddress: sufficiency.info.walletAddress
2780
+ });
2781
+ } else {
2782
+ deduplicator.removeInflight(dedupKey);
2783
+ if (sufficiency.info.isEmpty) {
2784
+ const error = new EmptyWalletError(sufficiency.info.walletAddress);
2785
+ options.onInsufficientFunds?.({
2786
+ balanceUSD: sufficiency.info.balanceUSD,
2787
+ requiredUSD: balanceMonitor.formatUSDC(bufferedCostMicros),
2788
+ walletAddress: sufficiency.info.walletAddress
2789
+ });
2790
+ throw error;
2791
+ } else {
2792
+ const error = new InsufficientFundsError({
2793
+ currentBalanceUSD: sufficiency.info.balanceUSD,
2794
+ requiredUSD: balanceMonitor.formatUSDC(bufferedCostMicros),
2795
+ walletAddress: sufficiency.info.walletAddress
2796
+ });
2797
+ options.onInsufficientFunds?.({
2798
+ balanceUSD: sufficiency.info.balanceUSD,
2799
+ requiredUSD: balanceMonitor.formatUSDC(bufferedCostMicros),
2800
+ walletAddress: sufficiency.info.walletAddress
2801
+ });
2802
+ throw error;
2803
+ }
2804
+ }
2805
+ } else if (sufficiency.info.isLow) {
2104
2806
  options.onLowBalance?.({
2105
2807
  balanceUSD: sufficiency.info.balanceUSD,
2106
2808
  walletAddress: sufficiency.info.walletAddress
@@ -2152,8 +2854,24 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
2152
2854
  try {
2153
2855
  let modelsToTry;
2154
2856
  if (routingDecision) {
2155
- modelsToTry = getFallbackChain(routingDecision.tier, routerOpts.config.tiers);
2156
- modelsToTry = modelsToTry.slice(0, MAX_FALLBACK_ATTEMPTS);
2857
+ const estimatedInputTokens = Math.ceil(body.length / 4);
2858
+ const estimatedTotalTokens = estimatedInputTokens + maxTokens;
2859
+ const useAgenticTiers = routingDecision.reasoning?.includes("agentic") && routerOpts.config.agenticTiers;
2860
+ const tierConfigs = useAgenticTiers ? routerOpts.config.agenticTiers : routerOpts.config.tiers;
2861
+ const fullChain = getFallbackChain(routingDecision.tier, tierConfigs);
2862
+ const contextFiltered = getFallbackChainFiltered(
2863
+ routingDecision.tier,
2864
+ tierConfigs,
2865
+ estimatedTotalTokens,
2866
+ getModelContextWindow
2867
+ );
2868
+ const contextExcluded = fullChain.filter((m) => !contextFiltered.includes(m));
2869
+ if (contextExcluded.length > 0) {
2870
+ console.log(
2871
+ `[ClawRouter] Context filter (~${estimatedTotalTokens} tokens): excluded ${contextExcluded.join(", ")}`
2872
+ );
2873
+ }
2874
+ modelsToTry = contextFiltered.slice(0, MAX_FALLBACK_ATTEMPTS);
2157
2875
  } else {
2158
2876
  modelsToTry = modelId ? [modelId] : [];
2159
2877
  }
@@ -2296,6 +3014,18 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
2296
3014
  res.write(contentData);
2297
3015
  responseChunks.push(Buffer.from(contentData));
2298
3016
  }
3017
+ const toolCalls = choice.message?.tool_calls ?? choice.delta?.tool_calls;
3018
+ if (toolCalls && toolCalls.length > 0) {
3019
+ const toolCallChunk = {
3020
+ ...baseChunk,
3021
+ choices: [{ index, delta: { tool_calls: toolCalls }, finish_reason: null }]
3022
+ };
3023
+ const toolCallData = `data: ${JSON.stringify(toolCallChunk)}
3024
+
3025
+ `;
3026
+ res.write(toolCallData);
3027
+ responseChunks.push(Buffer.from(toolCallData));
3028
+ }
2299
3029
  const finishChunk = {
2300
3030
  ...baseChunk,
2301
3031
  choices: [{ index, delta: {}, finish_reason: choice.finish_reason ?? "stop" }]
@@ -2327,7 +3057,8 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
2327
3057
  } else {
2328
3058
  const responseHeaders = {};
2329
3059
  upstream.headers.forEach((value, key) => {
2330
- if (key === "transfer-encoding" || key === "connection") return;
3060
+ if (key === "transfer-encoding" || key === "connection" || key === "content-encoding")
3061
+ return;
2331
3062
  responseHeaders[key] = value;
2332
3063
  });
2333
3064
  res.writeHead(upstream.status, responseHeaders);
@@ -2373,7 +3104,10 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
2373
3104
  const entry = {
2374
3105
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2375
3106
  model: routingDecision.model,
3107
+ tier: routingDecision.tier,
2376
3108
  cost: routingDecision.costEstimate,
3109
+ baselineCost: routingDecision.baselineCost,
3110
+ savings: routingDecision.savings,
2377
3111
  latencyMs: Date.now() - startTime
2378
3112
  };
2379
3113
  logUsage(entry).catch(() => {
@@ -2382,15 +3116,15 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
2382
3116
  }
2383
3117
 
2384
3118
  // src/auth.ts
2385
- import { writeFile, readFile, mkdir as mkdir2 } from "fs/promises";
2386
- import { join as join3 } from "path";
2387
- import { homedir as homedir2 } from "os";
3119
+ import { writeFile, readFile as readFile2, mkdir as mkdir2 } from "fs/promises";
3120
+ import { join as join4 } from "path";
3121
+ import { homedir as homedir3 } from "os";
2388
3122
  import { generatePrivateKey, privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
2389
- var WALLET_DIR = join3(homedir2(), ".openclaw", "blockrun");
2390
- var WALLET_FILE = join3(WALLET_DIR, "wallet.key");
3123
+ var WALLET_DIR = join4(homedir3(), ".openclaw", "blockrun");
3124
+ var WALLET_FILE = join4(WALLET_DIR, "wallet.key");
2391
3125
  async function loadSavedWallet() {
2392
3126
  try {
2393
- const key = (await readFile(WALLET_FILE, "utf-8")).trim();
3127
+ const key = (await readFile2(WALLET_FILE, "utf-8")).trim();
2394
3128
  if (key.startsWith("0x") && key.length === 66) return key;
2395
3129
  } catch {
2396
3130
  }
@@ -2420,8 +3154,8 @@ async function resolveOrGenerateWalletKey() {
2420
3154
 
2421
3155
  // src/index.ts
2422
3156
  import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from "fs";
2423
- import { homedir as homedir3 } from "os";
2424
- import { join as join4 } from "path";
3157
+ import { homedir as homedir4 } from "os";
3158
+ import { join as join5 } from "path";
2425
3159
  import { privateKeyToAccount as privateKeyToAccount4 } from "viem/accounts";
2426
3160
 
2427
3161
  // src/retry.ts
@@ -2486,7 +3220,7 @@ function isCompletionMode() {
2486
3220
  return args.some((arg, i) => arg === "completion" && i >= 1 && i <= 3);
2487
3221
  }
2488
3222
  function injectModelsConfig(logger) {
2489
- const configPath = join4(homedir3(), ".openclaw", "openclaw.json");
3223
+ const configPath = join5(homedir4(), ".openclaw", "openclaw.json");
2490
3224
  if (!existsSync(configPath)) {
2491
3225
  logger.info("OpenClaw config not found, skipping models injection");
2492
3226
  return;
@@ -2533,7 +3267,7 @@ function injectModelsConfig(logger) {
2533
3267
  }
2534
3268
  }
2535
3269
  function injectAuthProfile(logger) {
2536
- const agentsDir = join4(homedir3(), ".openclaw", "agents");
3270
+ const agentsDir = join5(homedir4(), ".openclaw", "agents");
2537
3271
  if (!existsSync(agentsDir)) {
2538
3272
  try {
2539
3273
  mkdirSync(agentsDir, { recursive: true });
@@ -2550,8 +3284,8 @@ function injectAuthProfile(logger) {
2550
3284
  agents = ["main", ...agents];
2551
3285
  }
2552
3286
  for (const agentId of agents) {
2553
- const authDir = join4(agentsDir, agentId, "agent");
2554
- const authPath = join4(authDir, "auth-profiles.json");
3287
+ const authDir = join5(agentsDir, agentId, "agent");
3288
+ const authPath = join5(authDir, "auth-profiles.json");
2555
3289
  if (!existsSync(authDir)) {
2556
3290
  try {
2557
3291
  mkdirSync(authDir, { recursive: true });
@@ -2652,6 +3386,30 @@ async function startProxyInBackground(api) {
2652
3386
  activeProxyHandle = proxy;
2653
3387
  api.logger.info(`BlockRun provider active \u2014 ${proxy.baseUrl}/v1 (smart routing enabled)`);
2654
3388
  }
3389
+ async function createStatsCommand() {
3390
+ return {
3391
+ name: "stats",
3392
+ description: "Show ClawRouter usage statistics and cost savings",
3393
+ acceptsArgs: true,
3394
+ requireAuth: false,
3395
+ handler: async (ctx) => {
3396
+ const arg = ctx.args?.trim().toLowerCase() || "7";
3397
+ const days = parseInt(arg, 10) || 7;
3398
+ try {
3399
+ const stats = await getStats(Math.min(days, 30));
3400
+ const ascii = formatStatsAscii(stats);
3401
+ return {
3402
+ text: ["```", ascii, "```"].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
+ }
2655
3413
  async function createWalletCommand() {
2656
3414
  return {
2657
3415
  name: "wallet",
@@ -2773,6 +3531,13 @@ var plugin = {
2773
3531
  `Failed to register /wallet command: ${err instanceof Error ? err.message : String(err)}`
2774
3532
  );
2775
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
+ });
2776
3541
  api.registerService({
2777
3542
  id: "clawrouter-proxy",
2778
3543
  start: () => {
@@ -2805,24 +3570,36 @@ export {
2805
3570
  BalanceMonitor,
2806
3571
  DEFAULT_RETRY_CONFIG,
2807
3572
  DEFAULT_ROUTING_CONFIG,
3573
+ DEFAULT_SESSION_CONFIG,
2808
3574
  EmptyWalletError,
2809
3575
  InsufficientFundsError,
3576
+ MODEL_ALIASES,
2810
3577
  OPENCLAW_MODELS,
2811
3578
  PaymentCache,
2812
3579
  RequestDeduplicator,
2813
3580
  RpcError,
3581
+ SessionStore,
2814
3582
  blockrunProvider,
2815
3583
  buildProviderModels,
2816
3584
  createPaymentFetch,
2817
3585
  index_default as default,
2818
3586
  fetchWithRetry,
3587
+ formatStatsAscii,
3588
+ getAgenticModels,
3589
+ getFallbackChain,
3590
+ getFallbackChainFiltered,
3591
+ getModelContextWindow,
2819
3592
  getProxyPort,
3593
+ getSessionId,
3594
+ getStats,
3595
+ isAgenticModel,
2820
3596
  isBalanceError,
2821
3597
  isEmptyWalletError,
2822
3598
  isInsufficientFundsError,
2823
3599
  isRetryable,
2824
3600
  isRpcError,
2825
3601
  logUsage,
3602
+ resolveModelAlias,
2826
3603
  route,
2827
3604
  startProxy
2828
3605
  };