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