@blockrun/clawrouter 0.4.7 → 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/LICENSE +21 -0
- package/README.md +139 -17
- package/dist/index.d.ts +188 -1
- package/dist/index.js +865 -88
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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,49 @@ 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"]
|
|
1284
1560
|
},
|
|
1285
1561
|
MEDIUM: {
|
|
1286
|
-
primary: "
|
|
1287
|
-
|
|
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"]
|
|
1288
1565
|
},
|
|
1289
1566
|
COMPLEX: {
|
|
1290
|
-
primary: "
|
|
1291
|
-
fallback: ["anthropic/claude-sonnet-4", "openai/gpt-4o"]
|
|
1567
|
+
primary: "google/gemini-2.5-pro",
|
|
1568
|
+
fallback: ["anthropic/claude-sonnet-4", "xai/grok-4-0709", "openai/gpt-4o"]
|
|
1292
1569
|
},
|
|
1293
1570
|
REASONING: {
|
|
1294
|
-
primary: "
|
|
1295
|
-
|
|
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"]
|
|
1581
|
+
},
|
|
1582
|
+
MEDIUM: {
|
|
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"]
|
|
1586
|
+
},
|
|
1587
|
+
COMPLEX: {
|
|
1588
|
+
primary: "anthropic/claude-sonnet-4",
|
|
1589
|
+
fallback: ["anthropic/claude-opus-4", "xai/grok-4-0709", "openai/gpt-4o"]
|
|
1590
|
+
},
|
|
1591
|
+
REASONING: {
|
|
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"]
|
|
1296
1595
|
}
|
|
1297
1596
|
},
|
|
1298
1597
|
overrides: {
|
|
1299
1598
|
maxTokensForceComplex: 1e5,
|
|
1300
1599
|
structuredOutputMinTier: "MEDIUM",
|
|
1301
|
-
ambiguousDefaultTier: "MEDIUM"
|
|
1600
|
+
ambiguousDefaultTier: "MEDIUM",
|
|
1601
|
+
agenticMode: false
|
|
1302
1602
|
}
|
|
1303
1603
|
};
|
|
1304
1604
|
|
|
@@ -1307,24 +1607,29 @@ function route(prompt, systemPrompt, maxOutputTokens, options) {
|
|
|
1307
1607
|
const { config, modelPricing } = options;
|
|
1308
1608
|
const fullText = `${systemPrompt ?? ""} ${prompt}`;
|
|
1309
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;
|
|
1310
1616
|
if (estimatedTokens > config.overrides.maxTokensForceComplex) {
|
|
1311
1617
|
return selectModel(
|
|
1312
1618
|
"COMPLEX",
|
|
1313
1619
|
0.95,
|
|
1314
1620
|
"rules",
|
|
1315
|
-
`Input exceeds ${config.overrides.maxTokensForceComplex} tokens`,
|
|
1316
|
-
|
|
1621
|
+
`Input exceeds ${config.overrides.maxTokensForceComplex} tokens${useAgenticTiers ? " | agentic" : ""}`,
|
|
1622
|
+
tierConfigs,
|
|
1317
1623
|
modelPricing,
|
|
1318
1624
|
estimatedTokens,
|
|
1319
1625
|
maxOutputTokens
|
|
1320
1626
|
);
|
|
1321
1627
|
}
|
|
1322
1628
|
const hasStructuredOutput = systemPrompt ? /json|structured|schema/i.test(systemPrompt) : false;
|
|
1323
|
-
const ruleResult = classifyByRules(prompt, systemPrompt, estimatedTokens, config.scoring);
|
|
1324
1629
|
let tier;
|
|
1325
1630
|
let confidence;
|
|
1326
1631
|
const method = "rules";
|
|
1327
|
-
let reasoning = `score=${ruleResult.score} | ${ruleResult.signals.join(", ")}`;
|
|
1632
|
+
let reasoning = `score=${ruleResult.score.toFixed(2)} | ${ruleResult.signals.join(", ")}`;
|
|
1328
1633
|
if (ruleResult.tier !== null) {
|
|
1329
1634
|
tier = ruleResult.tier;
|
|
1330
1635
|
confidence = ruleResult.confidence;
|
|
@@ -1341,12 +1646,17 @@ function route(prompt, systemPrompt, maxOutputTokens, options) {
|
|
|
1341
1646
|
tier = minTier;
|
|
1342
1647
|
}
|
|
1343
1648
|
}
|
|
1649
|
+
if (isAutoAgentic) {
|
|
1650
|
+
reasoning += " | auto-agentic";
|
|
1651
|
+
} else if (isExplicitAgentic) {
|
|
1652
|
+
reasoning += " | agentic";
|
|
1653
|
+
}
|
|
1344
1654
|
return selectModel(
|
|
1345
1655
|
tier,
|
|
1346
1656
|
confidence,
|
|
1347
1657
|
method,
|
|
1348
1658
|
reasoning,
|
|
1349
|
-
|
|
1659
|
+
tierConfigs,
|
|
1350
1660
|
modelPricing,
|
|
1351
1661
|
estimatedTokens,
|
|
1352
1662
|
maxOutputTokens
|
|
@@ -1374,6 +1684,176 @@ async function logUsage(entry) {
|
|
|
1374
1684
|
}
|
|
1375
1685
|
}
|
|
1376
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
|
+
|
|
1377
1857
|
// src/dedup.ts
|
|
1378
1858
|
import { createHash } from "crypto";
|
|
1379
1859
|
var DEFAULT_TTL_MS2 = 3e4;
|
|
@@ -1667,18 +2147,152 @@ var BalanceMonitor = class {
|
|
|
1667
2147
|
// src/version.ts
|
|
1668
2148
|
import { createRequire } from "module";
|
|
1669
2149
|
import { fileURLToPath } from "url";
|
|
1670
|
-
import { dirname, join as
|
|
2150
|
+
import { dirname, join as join3 } from "path";
|
|
1671
2151
|
var __filename = fileURLToPath(import.meta.url);
|
|
1672
2152
|
var __dirname = dirname(__filename);
|
|
1673
2153
|
var require2 = createRequire(import.meta.url);
|
|
1674
|
-
var pkg = require2(
|
|
2154
|
+
var pkg = require2(join3(__dirname, "..", "package.json"));
|
|
1675
2155
|
var VERSION = pkg.version;
|
|
1676
2156
|
var USER_AGENT = `clawrouter/${VERSION}`;
|
|
1677
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
|
+
|
|
1678
2291
|
// src/proxy.ts
|
|
1679
2292
|
var BLOCKRUN_API = "https://blockrun.ai/api";
|
|
1680
2293
|
var AUTO_MODEL = "blockrun/auto";
|
|
1681
2294
|
var AUTO_MODEL_SHORT = "auto";
|
|
2295
|
+
var FREE_MODEL = "nvidia/gpt-oss-120b";
|
|
1682
2296
|
var HEARTBEAT_INTERVAL_MS = 2e3;
|
|
1683
2297
|
var DEFAULT_REQUEST_TIMEOUT_MS = 18e4;
|
|
1684
2298
|
var DEFAULT_PORT = 8402;
|
|
@@ -1857,6 +2471,7 @@ async function startProxy(options) {
|
|
|
1857
2471
|
modelPricing
|
|
1858
2472
|
};
|
|
1859
2473
|
const deduplicator = new RequestDeduplicator();
|
|
2474
|
+
const sessionStore = new SessionStore(options.sessionConfig);
|
|
1860
2475
|
const server = createServer(async (req, res) => {
|
|
1861
2476
|
if (req.url === "/health" || req.url?.startsWith("/health?")) {
|
|
1862
2477
|
const url = new URL(req.url, "http://localhost");
|
|
@@ -1879,6 +2494,37 @@ async function startProxy(options) {
|
|
|
1879
2494
|
res.end(JSON.stringify(response));
|
|
1880
2495
|
return;
|
|
1881
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
|
+
}
|
|
1882
2528
|
if (!req.url?.startsWith("/v1")) {
|
|
1883
2529
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1884
2530
|
res.end(JSON.stringify({ error: "Not found" }));
|
|
@@ -1893,7 +2539,8 @@ async function startProxy(options) {
|
|
|
1893
2539
|
options,
|
|
1894
2540
|
routerOpts,
|
|
1895
2541
|
deduplicator,
|
|
1896
|
-
balanceMonitor
|
|
2542
|
+
balanceMonitor,
|
|
2543
|
+
sessionStore
|
|
1897
2544
|
);
|
|
1898
2545
|
} catch (err) {
|
|
1899
2546
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
@@ -1944,6 +2591,7 @@ async function startProxy(options) {
|
|
|
1944
2591
|
walletAddress: account.address,
|
|
1945
2592
|
balanceMonitor,
|
|
1946
2593
|
close: () => new Promise((res, rej) => {
|
|
2594
|
+
sessionStore.close();
|
|
1947
2595
|
server.close((err) => err ? rej(err) : res());
|
|
1948
2596
|
})
|
|
1949
2597
|
});
|
|
@@ -1996,7 +2644,7 @@ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxT
|
|
|
1996
2644
|
};
|
|
1997
2645
|
}
|
|
1998
2646
|
}
|
|
1999
|
-
async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, deduplicator, balanceMonitor) {
|
|
2647
|
+
async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, deduplicator, balanceMonitor, sessionStore) {
|
|
2000
2648
|
const startTime = Date.now();
|
|
2001
2649
|
const upstreamUrl = `${apiBase}${req.url}`;
|
|
2002
2650
|
const bodyChunks = [];
|
|
@@ -2021,29 +2669,66 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
2021
2669
|
bodyModified = true;
|
|
2022
2670
|
}
|
|
2023
2671
|
const normalizedModel = typeof parsed.model === "string" ? parsed.model.trim().toLowerCase() : "";
|
|
2672
|
+
const resolvedModel = resolveModelAlias(normalizedModel);
|
|
2673
|
+
const wasAlias = resolvedModel !== normalizedModel;
|
|
2024
2674
|
const isAutoModel = normalizedModel === AUTO_MODEL.toLowerCase() || normalizedModel === AUTO_MODEL_SHORT.toLowerCase();
|
|
2025
2675
|
console.log(
|
|
2026
|
-
`[ClawRouter] Received model: "${parsed.model}" -> normalized: "${normalizedModel}", isAuto: ${isAutoModel}`
|
|
2676
|
+
`[ClawRouter] Received model: "${parsed.model}" -> normalized: "${normalizedModel}"${wasAlias ? ` -> alias: "${resolvedModel}"` : ""}, isAuto: ${isAutoModel}`
|
|
2027
2677
|
);
|
|
2678
|
+
if (wasAlias && !isAutoModel) {
|
|
2679
|
+
parsed.model = resolvedModel;
|
|
2680
|
+
modelId = resolvedModel;
|
|
2681
|
+
bodyModified = true;
|
|
2682
|
+
}
|
|
2028
2683
|
if (isAutoModel) {
|
|
2029
|
-
const
|
|
2030
|
-
|
|
2031
|
-
if (
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
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 }
|
|
2036
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
|
+
);
|
|
2037
2729
|
}
|
|
2730
|
+
options.onRouted?.(routingDecision);
|
|
2038
2731
|
}
|
|
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
2732
|
}
|
|
2048
2733
|
if (bodyModified) {
|
|
2049
2734
|
body = Buffer.from(JSON.stringify(parsed));
|
|
@@ -2070,37 +2755,51 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
2070
2755
|
}
|
|
2071
2756
|
deduplicator.markInflight(dedupKey);
|
|
2072
2757
|
let estimatedCostMicros;
|
|
2073
|
-
|
|
2758
|
+
const isFreeModel = modelId === FREE_MODEL;
|
|
2759
|
+
if (modelId && !options.skipBalanceCheck && !isFreeModel) {
|
|
2074
2760
|
const estimated = estimateAmount(modelId, body.length, maxTokens);
|
|
2075
2761
|
if (estimated) {
|
|
2076
2762
|
estimatedCostMicros = BigInt(estimated);
|
|
2077
2763
|
const bufferedCostMicros = estimatedCostMicros * BigInt(Math.ceil(BALANCE_CHECK_BUFFER * 100)) / 100n;
|
|
2078
2764
|
const sufficiency = await balanceMonitor.checkSufficient(bufferedCostMicros);
|
|
2079
|
-
if (sufficiency.info.isEmpty) {
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
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) {
|
|
2104
2803
|
options.onLowBalance?.({
|
|
2105
2804
|
balanceUSD: sufficiency.info.balanceUSD,
|
|
2106
2805
|
walletAddress: sufficiency.info.walletAddress
|
|
@@ -2152,8 +2851,24 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
2152
2851
|
try {
|
|
2153
2852
|
let modelsToTry;
|
|
2154
2853
|
if (routingDecision) {
|
|
2155
|
-
|
|
2156
|
-
|
|
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);
|
|
2157
2872
|
} else {
|
|
2158
2873
|
modelsToTry = modelId ? [modelId] : [];
|
|
2159
2874
|
}
|
|
@@ -2296,6 +3011,18 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
2296
3011
|
res.write(contentData);
|
|
2297
3012
|
responseChunks.push(Buffer.from(contentData));
|
|
2298
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
|
+
}
|
|
2299
3026
|
const finishChunk = {
|
|
2300
3027
|
...baseChunk,
|
|
2301
3028
|
choices: [{ index, delta: {}, finish_reason: choice.finish_reason ?? "stop" }]
|
|
@@ -2327,7 +3054,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
2327
3054
|
} else {
|
|
2328
3055
|
const responseHeaders = {};
|
|
2329
3056
|
upstream.headers.forEach((value, key) => {
|
|
2330
|
-
if (key === "transfer-encoding" || key === "connection") return;
|
|
3057
|
+
if (key === "transfer-encoding" || key === "connection" || key === "content-encoding") return;
|
|
2331
3058
|
responseHeaders[key] = value;
|
|
2332
3059
|
});
|
|
2333
3060
|
res.writeHead(upstream.status, responseHeaders);
|
|
@@ -2373,7 +3100,10 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
2373
3100
|
const entry = {
|
|
2374
3101
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2375
3102
|
model: routingDecision.model,
|
|
3103
|
+
tier: routingDecision.tier,
|
|
2376
3104
|
cost: routingDecision.costEstimate,
|
|
3105
|
+
baselineCost: routingDecision.baselineCost,
|
|
3106
|
+
savings: routingDecision.savings,
|
|
2377
3107
|
latencyMs: Date.now() - startTime
|
|
2378
3108
|
};
|
|
2379
3109
|
logUsage(entry).catch(() => {
|
|
@@ -2382,15 +3112,15 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
2382
3112
|
}
|
|
2383
3113
|
|
|
2384
3114
|
// src/auth.ts
|
|
2385
|
-
import { writeFile, readFile, mkdir as mkdir2 } from "fs/promises";
|
|
2386
|
-
import { join as
|
|
2387
|
-
import { homedir as
|
|
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";
|
|
2388
3118
|
import { generatePrivateKey, privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
|
|
2389
|
-
var WALLET_DIR =
|
|
2390
|
-
var WALLET_FILE =
|
|
3119
|
+
var WALLET_DIR = join4(homedir3(), ".openclaw", "blockrun");
|
|
3120
|
+
var WALLET_FILE = join4(WALLET_DIR, "wallet.key");
|
|
2391
3121
|
async function loadSavedWallet() {
|
|
2392
3122
|
try {
|
|
2393
|
-
const key = (await
|
|
3123
|
+
const key = (await readFile2(WALLET_FILE, "utf-8")).trim();
|
|
2394
3124
|
if (key.startsWith("0x") && key.length === 66) return key;
|
|
2395
3125
|
} catch {
|
|
2396
3126
|
}
|
|
@@ -2420,8 +3150,8 @@ async function resolveOrGenerateWalletKey() {
|
|
|
2420
3150
|
|
|
2421
3151
|
// src/index.ts
|
|
2422
3152
|
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from "fs";
|
|
2423
|
-
import { homedir as
|
|
2424
|
-
import { join as
|
|
3153
|
+
import { homedir as homedir4 } from "os";
|
|
3154
|
+
import { join as join5 } from "path";
|
|
2425
3155
|
import { privateKeyToAccount as privateKeyToAccount4 } from "viem/accounts";
|
|
2426
3156
|
|
|
2427
3157
|
// src/retry.ts
|
|
@@ -2486,7 +3216,7 @@ function isCompletionMode() {
|
|
|
2486
3216
|
return args.some((arg, i) => arg === "completion" && i >= 1 && i <= 3);
|
|
2487
3217
|
}
|
|
2488
3218
|
function injectModelsConfig(logger) {
|
|
2489
|
-
const configPath =
|
|
3219
|
+
const configPath = join5(homedir4(), ".openclaw", "openclaw.json");
|
|
2490
3220
|
if (!existsSync(configPath)) {
|
|
2491
3221
|
logger.info("OpenClaw config not found, skipping models injection");
|
|
2492
3222
|
return;
|
|
@@ -2533,7 +3263,7 @@ function injectModelsConfig(logger) {
|
|
|
2533
3263
|
}
|
|
2534
3264
|
}
|
|
2535
3265
|
function injectAuthProfile(logger) {
|
|
2536
|
-
const agentsDir =
|
|
3266
|
+
const agentsDir = join5(homedir4(), ".openclaw", "agents");
|
|
2537
3267
|
if (!existsSync(agentsDir)) {
|
|
2538
3268
|
try {
|
|
2539
3269
|
mkdirSync(agentsDir, { recursive: true });
|
|
@@ -2550,8 +3280,8 @@ function injectAuthProfile(logger) {
|
|
|
2550
3280
|
agents = ["main", ...agents];
|
|
2551
3281
|
}
|
|
2552
3282
|
for (const agentId of agents) {
|
|
2553
|
-
const authDir =
|
|
2554
|
-
const authPath =
|
|
3283
|
+
const authDir = join5(agentsDir, agentId, "agent");
|
|
3284
|
+
const authPath = join5(authDir, "auth-profiles.json");
|
|
2555
3285
|
if (!existsSync(authDir)) {
|
|
2556
3286
|
try {
|
|
2557
3287
|
mkdirSync(authDir, { recursive: true });
|
|
@@ -2652,6 +3382,34 @@ async function startProxyInBackground(api) {
|
|
|
2652
3382
|
activeProxyHandle = proxy;
|
|
2653
3383
|
api.logger.info(`BlockRun provider active \u2014 ${proxy.baseUrl}/v1 (smart routing enabled)`);
|
|
2654
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
|
+
}
|
|
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
|
};
|