@blockrun/clawrouter 0.9.11 → 0.9.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,5029 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/proxy.ts
4
+ import { createServer } from "http";
5
+ import { finished } from "stream";
6
+ import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
7
+
8
+ // src/x402.ts
9
+ import { signTypedData, privateKeyToAccount } from "viem/accounts";
10
+
11
+ // src/payment-cache.ts
12
+ var DEFAULT_TTL_MS = 36e5;
13
+ var PaymentCache = class {
14
+ cache = /* @__PURE__ */ new Map();
15
+ ttlMs;
16
+ constructor(ttlMs = DEFAULT_TTL_MS) {
17
+ this.ttlMs = ttlMs;
18
+ }
19
+ /** Get cached payment params for an endpoint path. */
20
+ get(endpointPath) {
21
+ const entry = this.cache.get(endpointPath);
22
+ if (!entry) return void 0;
23
+ if (Date.now() - entry.cachedAt > this.ttlMs) {
24
+ this.cache.delete(endpointPath);
25
+ return void 0;
26
+ }
27
+ return entry;
28
+ }
29
+ /** Cache payment params from a 402 response. */
30
+ set(endpointPath, params) {
31
+ this.cache.set(endpointPath, { ...params, cachedAt: Date.now() });
32
+ }
33
+ /** Invalidate cache for an endpoint (e.g., if payTo changed). */
34
+ invalidate(endpointPath) {
35
+ this.cache.delete(endpointPath);
36
+ }
37
+ };
38
+
39
+ // src/x402.ts
40
+ var BASE_CHAIN_ID = 8453;
41
+ var USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
42
+ var USDC_DOMAIN = {
43
+ name: "USD Coin",
44
+ version: "2",
45
+ chainId: BASE_CHAIN_ID,
46
+ verifyingContract: USDC_BASE
47
+ };
48
+ var TRANSFER_TYPES = {
49
+ TransferWithAuthorization: [
50
+ { name: "from", type: "address" },
51
+ { name: "to", type: "address" },
52
+ { name: "value", type: "uint256" },
53
+ { name: "validAfter", type: "uint256" },
54
+ { name: "validBefore", type: "uint256" },
55
+ { name: "nonce", type: "bytes32" }
56
+ ]
57
+ };
58
+ function createNonce() {
59
+ const bytes = new Uint8Array(32);
60
+ crypto.getRandomValues(bytes);
61
+ return `0x${Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
62
+ }
63
+ function parsePaymentRequired(headerValue) {
64
+ const decoded = atob(headerValue);
65
+ return JSON.parse(decoded);
66
+ }
67
+ async function createPaymentPayload(privateKey, fromAddress, recipient, amount, resourceUrl) {
68
+ const now = Math.floor(Date.now() / 1e3);
69
+ const validAfter = now - 600;
70
+ const validBefore = now + 300;
71
+ const nonce = createNonce();
72
+ const signature = await signTypedData({
73
+ privateKey,
74
+ domain: USDC_DOMAIN,
75
+ types: TRANSFER_TYPES,
76
+ primaryType: "TransferWithAuthorization",
77
+ message: {
78
+ from: fromAddress,
79
+ to: recipient,
80
+ value: BigInt(amount),
81
+ validAfter: BigInt(validAfter),
82
+ validBefore: BigInt(validBefore),
83
+ nonce
84
+ }
85
+ });
86
+ const paymentData = {
87
+ x402Version: 2,
88
+ resource: {
89
+ url: resourceUrl,
90
+ description: "BlockRun AI API call",
91
+ mimeType: "application/json"
92
+ },
93
+ accepted: {
94
+ scheme: "exact",
95
+ network: "eip155:8453",
96
+ amount,
97
+ asset: USDC_BASE,
98
+ payTo: recipient,
99
+ maxTimeoutSeconds: 300,
100
+ extra: { name: "USD Coin", version: "2" }
101
+ },
102
+ payload: {
103
+ signature,
104
+ authorization: {
105
+ from: fromAddress,
106
+ to: recipient,
107
+ value: amount,
108
+ validAfter: validAfter.toString(),
109
+ validBefore: validBefore.toString(),
110
+ nonce
111
+ }
112
+ },
113
+ extensions: {}
114
+ };
115
+ return btoa(JSON.stringify(paymentData));
116
+ }
117
+ function createPaymentFetch(privateKey) {
118
+ const account = privateKeyToAccount(privateKey);
119
+ const walletAddress = account.address;
120
+ const paymentCache = new PaymentCache();
121
+ const payFetch = async (input, init, preAuth) => {
122
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
123
+ const endpointPath = new URL(url).pathname;
124
+ const cached = paymentCache.get(endpointPath);
125
+ if (cached && preAuth?.estimatedAmount) {
126
+ const paymentPayload = await createPaymentPayload(
127
+ privateKey,
128
+ walletAddress,
129
+ cached.payTo,
130
+ preAuth.estimatedAmount,
131
+ url
132
+ );
133
+ const preAuthHeaders = new Headers(init?.headers);
134
+ preAuthHeaders.set("payment-signature", paymentPayload);
135
+ const response2 = await fetch(input, { ...init, headers: preAuthHeaders });
136
+ if (response2.status !== 402) {
137
+ return response2;
138
+ }
139
+ const paymentHeader2 = response2.headers.get("x-payment-required");
140
+ if (paymentHeader2) {
141
+ return handle402(input, init, url, endpointPath, paymentHeader2);
142
+ }
143
+ paymentCache.invalidate(endpointPath);
144
+ const cleanResponse = await fetch(input, init);
145
+ if (cleanResponse.status !== 402) {
146
+ return cleanResponse;
147
+ }
148
+ const cleanHeader = cleanResponse.headers.get("x-payment-required");
149
+ if (!cleanHeader) {
150
+ throw new Error("402 response missing x-payment-required header");
151
+ }
152
+ return handle402(input, init, url, endpointPath, cleanHeader);
153
+ }
154
+ const response = await fetch(input, init);
155
+ if (response.status !== 402) {
156
+ return response;
157
+ }
158
+ const paymentHeader = response.headers.get("x-payment-required");
159
+ if (!paymentHeader) {
160
+ throw new Error("402 response missing x-payment-required header");
161
+ }
162
+ return handle402(input, init, url, endpointPath, paymentHeader);
163
+ };
164
+ async function handle402(input, init, url, endpointPath, paymentHeader) {
165
+ const paymentRequired = parsePaymentRequired(paymentHeader);
166
+ const option = paymentRequired.accepts?.[0];
167
+ if (!option) {
168
+ throw new Error("No payment options in 402 response");
169
+ }
170
+ const amount = option.amount || option.maxAmountRequired;
171
+ if (!amount) {
172
+ throw new Error("No amount in payment requirements");
173
+ }
174
+ paymentCache.set(endpointPath, {
175
+ payTo: option.payTo,
176
+ asset: option.asset,
177
+ scheme: option.scheme,
178
+ network: option.network,
179
+ extra: option.extra,
180
+ maxTimeoutSeconds: option.maxTimeoutSeconds
181
+ });
182
+ const paymentPayload = await createPaymentPayload(
183
+ privateKey,
184
+ walletAddress,
185
+ option.payTo,
186
+ amount,
187
+ url
188
+ );
189
+ const retryHeaders = new Headers(init?.headers);
190
+ retryHeaders.set("payment-signature", paymentPayload);
191
+ return fetch(input, {
192
+ ...init,
193
+ headers: retryHeaders
194
+ });
195
+ }
196
+ return { fetch: payFetch, cache: paymentCache };
197
+ }
198
+
199
+ // src/router/rules.ts
200
+ function scoreTokenCount(estimatedTokens, thresholds) {
201
+ if (estimatedTokens < thresholds.simple) {
202
+ return { name: "tokenCount", score: -1, signal: `short (${estimatedTokens} tokens)` };
203
+ }
204
+ if (estimatedTokens > thresholds.complex) {
205
+ return { name: "tokenCount", score: 1, signal: `long (${estimatedTokens} tokens)` };
206
+ }
207
+ return { name: "tokenCount", score: 0, signal: null };
208
+ }
209
+ function scoreKeywordMatch(text, keywords, name, signalLabel, thresholds, scores) {
210
+ const matches = keywords.filter((kw) => text.includes(kw.toLowerCase()));
211
+ if (matches.length >= thresholds.high) {
212
+ return {
213
+ name,
214
+ score: scores.high,
215
+ signal: `${signalLabel} (${matches.slice(0, 3).join(", ")})`
216
+ };
217
+ }
218
+ if (matches.length >= thresholds.low) {
219
+ return {
220
+ name,
221
+ score: scores.low,
222
+ signal: `${signalLabel} (${matches.slice(0, 3).join(", ")})`
223
+ };
224
+ }
225
+ return { name, score: scores.none, signal: null };
226
+ }
227
+ function scoreMultiStep(text) {
228
+ const patterns = [/first.*then/i, /step \d/i, /\d\.\s/];
229
+ const hits = patterns.filter((p) => p.test(text));
230
+ if (hits.length > 0) {
231
+ return { name: "multiStepPatterns", score: 0.5, signal: "multi-step" };
232
+ }
233
+ return { name: "multiStepPatterns", score: 0, signal: null };
234
+ }
235
+ function scoreQuestionComplexity(prompt) {
236
+ const count = (prompt.match(/\?/g) || []).length;
237
+ if (count > 3) {
238
+ return { name: "questionComplexity", score: 0.5, signal: `${count} questions` };
239
+ }
240
+ return { name: "questionComplexity", score: 0, signal: null };
241
+ }
242
+ function scoreAgenticTask(text, keywords) {
243
+ let matchCount = 0;
244
+ const signals = [];
245
+ for (const keyword of keywords) {
246
+ if (text.includes(keyword.toLowerCase())) {
247
+ matchCount++;
248
+ if (signals.length < 3) {
249
+ signals.push(keyword);
250
+ }
251
+ }
252
+ }
253
+ if (matchCount >= 4) {
254
+ return {
255
+ dimensionScore: {
256
+ name: "agenticTask",
257
+ score: 1,
258
+ signal: `agentic (${signals.join(", ")})`
259
+ },
260
+ agenticScore: 1
261
+ };
262
+ } else if (matchCount >= 3) {
263
+ return {
264
+ dimensionScore: {
265
+ name: "agenticTask",
266
+ score: 0.6,
267
+ signal: `agentic (${signals.join(", ")})`
268
+ },
269
+ agenticScore: 0.6
270
+ };
271
+ } else if (matchCount >= 1) {
272
+ return {
273
+ dimensionScore: {
274
+ name: "agenticTask",
275
+ score: 0.2,
276
+ signal: `agentic-light (${signals.join(", ")})`
277
+ },
278
+ agenticScore: 0.2
279
+ };
280
+ }
281
+ return {
282
+ dimensionScore: { name: "agenticTask", score: 0, signal: null },
283
+ agenticScore: 0
284
+ };
285
+ }
286
+ function classifyByRules(prompt, systemPrompt, estimatedTokens, config) {
287
+ const text = `${systemPrompt ?? ""} ${prompt}`.toLowerCase();
288
+ const userText = prompt.toLowerCase();
289
+ const dimensions = [
290
+ // Original 8 dimensions
291
+ scoreTokenCount(estimatedTokens, config.tokenCountThresholds),
292
+ scoreKeywordMatch(
293
+ text,
294
+ config.codeKeywords,
295
+ "codePresence",
296
+ "code",
297
+ { low: 1, high: 2 },
298
+ { none: 0, low: 0.5, high: 1 }
299
+ ),
300
+ // Reasoning markers use USER prompt only — system prompt "step by step" shouldn't trigger reasoning
301
+ scoreKeywordMatch(
302
+ userText,
303
+ config.reasoningKeywords,
304
+ "reasoningMarkers",
305
+ "reasoning",
306
+ { low: 1, high: 2 },
307
+ { none: 0, low: 0.7, high: 1 }
308
+ ),
309
+ scoreKeywordMatch(
310
+ text,
311
+ config.technicalKeywords,
312
+ "technicalTerms",
313
+ "technical",
314
+ { low: 2, high: 4 },
315
+ { none: 0, low: 0.5, high: 1 }
316
+ ),
317
+ scoreKeywordMatch(
318
+ text,
319
+ config.creativeKeywords,
320
+ "creativeMarkers",
321
+ "creative",
322
+ { low: 1, high: 2 },
323
+ { none: 0, low: 0.5, high: 0.7 }
324
+ ),
325
+ scoreKeywordMatch(
326
+ text,
327
+ config.simpleKeywords,
328
+ "simpleIndicators",
329
+ "simple",
330
+ { low: 1, high: 2 },
331
+ { none: 0, low: -1, high: -1 }
332
+ ),
333
+ scoreMultiStep(text),
334
+ scoreQuestionComplexity(prompt),
335
+ // 6 new dimensions
336
+ scoreKeywordMatch(
337
+ text,
338
+ config.imperativeVerbs,
339
+ "imperativeVerbs",
340
+ "imperative",
341
+ { low: 1, high: 2 },
342
+ { none: 0, low: 0.3, high: 0.5 }
343
+ ),
344
+ scoreKeywordMatch(
345
+ text,
346
+ config.constraintIndicators,
347
+ "constraintCount",
348
+ "constraints",
349
+ { low: 1, high: 3 },
350
+ { none: 0, low: 0.3, high: 0.7 }
351
+ ),
352
+ scoreKeywordMatch(
353
+ text,
354
+ config.outputFormatKeywords,
355
+ "outputFormat",
356
+ "format",
357
+ { low: 1, high: 2 },
358
+ { none: 0, low: 0.4, high: 0.7 }
359
+ ),
360
+ scoreKeywordMatch(
361
+ text,
362
+ config.referenceKeywords,
363
+ "referenceComplexity",
364
+ "references",
365
+ { low: 1, high: 2 },
366
+ { none: 0, low: 0.3, high: 0.5 }
367
+ ),
368
+ scoreKeywordMatch(
369
+ text,
370
+ config.negationKeywords,
371
+ "negationComplexity",
372
+ "negation",
373
+ { low: 2, high: 3 },
374
+ { none: 0, low: 0.3, high: 0.5 }
375
+ ),
376
+ scoreKeywordMatch(
377
+ text,
378
+ config.domainSpecificKeywords,
379
+ "domainSpecificity",
380
+ "domain-specific",
381
+ { low: 1, high: 2 },
382
+ { none: 0, low: 0.5, high: 0.8 }
383
+ )
384
+ ];
385
+ const agenticResult = scoreAgenticTask(text, config.agenticTaskKeywords);
386
+ dimensions.push(agenticResult.dimensionScore);
387
+ const agenticScore = agenticResult.agenticScore;
388
+ const signals = dimensions.filter((d) => d.signal !== null).map((d) => d.signal);
389
+ const weights = config.dimensionWeights;
390
+ let weightedScore = 0;
391
+ for (const d of dimensions) {
392
+ const w = weights[d.name] ?? 0;
393
+ weightedScore += d.score * w;
394
+ }
395
+ const reasoningMatches = config.reasoningKeywords.filter(
396
+ (kw) => userText.includes(kw.toLowerCase())
397
+ );
398
+ if (reasoningMatches.length >= 2) {
399
+ const confidence2 = calibrateConfidence(
400
+ Math.max(weightedScore, 0.3),
401
+ // ensure positive for confidence calc
402
+ config.confidenceSteepness
403
+ );
404
+ return {
405
+ score: weightedScore,
406
+ tier: "REASONING",
407
+ confidence: Math.max(confidence2, 0.85),
408
+ signals,
409
+ agenticScore
410
+ };
411
+ }
412
+ const { simpleMedium, mediumComplex, complexReasoning } = config.tierBoundaries;
413
+ let tier;
414
+ let distanceFromBoundary;
415
+ if (weightedScore < simpleMedium) {
416
+ tier = "SIMPLE";
417
+ distanceFromBoundary = simpleMedium - weightedScore;
418
+ } else if (weightedScore < mediumComplex) {
419
+ tier = "MEDIUM";
420
+ distanceFromBoundary = Math.min(weightedScore - simpleMedium, mediumComplex - weightedScore);
421
+ } else if (weightedScore < complexReasoning) {
422
+ tier = "COMPLEX";
423
+ distanceFromBoundary = Math.min(
424
+ weightedScore - mediumComplex,
425
+ complexReasoning - weightedScore
426
+ );
427
+ } else {
428
+ tier = "REASONING";
429
+ distanceFromBoundary = weightedScore - complexReasoning;
430
+ }
431
+ const confidence = calibrateConfidence(distanceFromBoundary, config.confidenceSteepness);
432
+ if (confidence < config.confidenceThreshold) {
433
+ return { score: weightedScore, tier: null, confidence, signals, agenticScore };
434
+ }
435
+ return { score: weightedScore, tier, confidence, signals, agenticScore };
436
+ }
437
+ function calibrateConfidence(distance, steepness) {
438
+ return 1 / (1 + Math.exp(-steepness * distance));
439
+ }
440
+
441
+ // src/router/selector.ts
442
+ function selectModel(tier, confidence, method, reasoning, tierConfigs, modelPricing, estimatedInputTokens, maxOutputTokens, routingProfile) {
443
+ const tierConfig = tierConfigs[tier];
444
+ const model = tierConfig.primary;
445
+ const pricing = modelPricing.get(model);
446
+ const inputPrice = pricing?.inputPrice ?? 0;
447
+ const outputPrice = pricing?.outputPrice ?? 0;
448
+ const inputCost = estimatedInputTokens / 1e6 * inputPrice;
449
+ const outputCost = maxOutputTokens / 1e6 * outputPrice;
450
+ const costEstimate = inputCost + outputCost;
451
+ const opusPricing = modelPricing.get("anthropic/claude-opus-4.5");
452
+ const opusInputPrice = opusPricing?.inputPrice ?? 0;
453
+ const opusOutputPrice = opusPricing?.outputPrice ?? 0;
454
+ const baselineInput = estimatedInputTokens / 1e6 * opusInputPrice;
455
+ const baselineOutput = maxOutputTokens / 1e6 * opusOutputPrice;
456
+ const baselineCost = baselineInput + baselineOutput;
457
+ const savings = routingProfile === "premium" ? 0 : baselineCost > 0 ? Math.max(0, (baselineCost - costEstimate) / baselineCost) : 0;
458
+ return {
459
+ model,
460
+ tier,
461
+ confidence,
462
+ method,
463
+ reasoning,
464
+ costEstimate,
465
+ baselineCost,
466
+ savings
467
+ };
468
+ }
469
+ function getFallbackChain(tier, tierConfigs) {
470
+ const config = tierConfigs[tier];
471
+ return [config.primary, ...config.fallback];
472
+ }
473
+ function calculateModelCost(model, modelPricing, estimatedInputTokens, maxOutputTokens, routingProfile) {
474
+ const pricing = modelPricing.get(model);
475
+ const inputPrice = pricing?.inputPrice ?? 0;
476
+ const outputPrice = pricing?.outputPrice ?? 0;
477
+ const inputCost = estimatedInputTokens / 1e6 * inputPrice;
478
+ const outputCost = maxOutputTokens / 1e6 * outputPrice;
479
+ const costEstimate = inputCost + outputCost;
480
+ const opusPricing = modelPricing.get("anthropic/claude-opus-4.5");
481
+ const opusInputPrice = opusPricing?.inputPrice ?? 0;
482
+ const opusOutputPrice = opusPricing?.outputPrice ?? 0;
483
+ const baselineInput = estimatedInputTokens / 1e6 * opusInputPrice;
484
+ const baselineOutput = maxOutputTokens / 1e6 * opusOutputPrice;
485
+ const baselineCost = baselineInput + baselineOutput;
486
+ const savings = routingProfile === "premium" ? 0 : baselineCost > 0 ? Math.max(0, (baselineCost - costEstimate) / baselineCost) : 0;
487
+ return { costEstimate, baselineCost, savings };
488
+ }
489
+ function getFallbackChainFiltered(tier, tierConfigs, estimatedTotalTokens, getContextWindow) {
490
+ const fullChain = getFallbackChain(tier, tierConfigs);
491
+ const filtered = fullChain.filter((modelId) => {
492
+ const contextWindow = getContextWindow(modelId);
493
+ if (contextWindow === void 0) {
494
+ return true;
495
+ }
496
+ return contextWindow >= estimatedTotalTokens * 1.1;
497
+ });
498
+ if (filtered.length === 0) {
499
+ return fullChain;
500
+ }
501
+ return filtered;
502
+ }
503
+
504
+ // src/router/config.ts
505
+ var DEFAULT_ROUTING_CONFIG = {
506
+ version: "2.0",
507
+ classifier: {
508
+ llmModel: "google/gemini-2.5-flash",
509
+ llmMaxTokens: 10,
510
+ llmTemperature: 0,
511
+ promptTruncationChars: 500,
512
+ cacheTtlMs: 36e5
513
+ // 1 hour
514
+ },
515
+ scoring: {
516
+ tokenCountThresholds: { simple: 50, complex: 500 },
517
+ // Multilingual keywords: English + Chinese (中文) + Japanese (日本語) + Russian (Русский) + German (Deutsch)
518
+ codeKeywords: [
519
+ // English
520
+ "function",
521
+ "class",
522
+ "import",
523
+ "def",
524
+ "SELECT",
525
+ "async",
526
+ "await",
527
+ "const",
528
+ "let",
529
+ "var",
530
+ "return",
531
+ "```",
532
+ // Chinese
533
+ "\u51FD\u6570",
534
+ "\u7C7B",
535
+ "\u5BFC\u5165",
536
+ "\u5B9A\u4E49",
537
+ "\u67E5\u8BE2",
538
+ "\u5F02\u6B65",
539
+ "\u7B49\u5F85",
540
+ "\u5E38\u91CF",
541
+ "\u53D8\u91CF",
542
+ "\u8FD4\u56DE",
543
+ // Japanese
544
+ "\u95A2\u6570",
545
+ "\u30AF\u30E9\u30B9",
546
+ "\u30A4\u30F3\u30DD\u30FC\u30C8",
547
+ "\u975E\u540C\u671F",
548
+ "\u5B9A\u6570",
549
+ "\u5909\u6570",
550
+ // Russian
551
+ "\u0444\u0443\u043D\u043A\u0446\u0438\u044F",
552
+ "\u043A\u043B\u0430\u0441\u0441",
553
+ "\u0438\u043C\u043F\u043E\u0440\u0442",
554
+ "\u043E\u043F\u0440\u0435\u0434\u0435\u043B",
555
+ "\u0437\u0430\u043F\u0440\u043E\u0441",
556
+ "\u0430\u0441\u0438\u043D\u0445\u0440\u043E\u043D\u043D\u044B\u0439",
557
+ "\u043E\u0436\u0438\u0434\u0430\u0442\u044C",
558
+ "\u043A\u043E\u043D\u0441\u0442\u0430\u043D\u0442\u0430",
559
+ "\u043F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u0430\u044F",
560
+ "\u0432\u0435\u0440\u043D\u0443\u0442\u044C",
561
+ // German
562
+ "funktion",
563
+ "klasse",
564
+ "importieren",
565
+ "definieren",
566
+ "abfrage",
567
+ "asynchron",
568
+ "erwarten",
569
+ "konstante",
570
+ "variable",
571
+ "zur\xFCckgeben"
572
+ ],
573
+ reasoningKeywords: [
574
+ // English
575
+ "prove",
576
+ "theorem",
577
+ "derive",
578
+ "step by step",
579
+ "chain of thought",
580
+ "formally",
581
+ "mathematical",
582
+ "proof",
583
+ "logically",
584
+ // Chinese
585
+ "\u8BC1\u660E",
586
+ "\u5B9A\u7406",
587
+ "\u63A8\u5BFC",
588
+ "\u9010\u6B65",
589
+ "\u601D\u7EF4\u94FE",
590
+ "\u5F62\u5F0F\u5316",
591
+ "\u6570\u5B66",
592
+ "\u903B\u8F91",
593
+ // Japanese
594
+ "\u8A3C\u660E",
595
+ "\u5B9A\u7406",
596
+ "\u5C0E\u51FA",
597
+ "\u30B9\u30C6\u30C3\u30D7\u30D0\u30A4\u30B9\u30C6\u30C3\u30D7",
598
+ "\u8AD6\u7406\u7684",
599
+ // Russian
600
+ "\u0434\u043E\u043A\u0430\u0437\u0430\u0442\u044C",
601
+ "\u0434\u043E\u043A\u0430\u0436\u0438",
602
+ "\u0434\u043E\u043A\u0430\u0437\u0430\u0442\u0435\u043B\u044C\u0441\u0442\u0432",
603
+ "\u0442\u0435\u043E\u0440\u0435\u043C\u0430",
604
+ "\u0432\u044B\u0432\u0435\u0441\u0442\u0438",
605
+ "\u0448\u0430\u0433 \u0437\u0430 \u0448\u0430\u0433\u043E\u043C",
606
+ "\u043F\u043E\u0448\u0430\u0433\u043E\u0432\u043E",
607
+ "\u043F\u043E\u044D\u0442\u0430\u043F\u043D\u043E",
608
+ "\u0446\u0435\u043F\u043E\u0447\u043A\u0430 \u0440\u0430\u0441\u0441\u0443\u0436\u0434\u0435\u043D\u0438\u0439",
609
+ "\u0440\u0430\u0441\u0441\u0443\u0436\u0434\u0435\u043D\u0438",
610
+ "\u0444\u043E\u0440\u043C\u0430\u043B\u044C\u043D\u043E",
611
+ "\u043C\u0430\u0442\u0435\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438",
612
+ "\u043B\u043E\u0433\u0438\u0447\u0435\u0441\u043A\u0438",
613
+ // German
614
+ "beweisen",
615
+ "beweis",
616
+ "theorem",
617
+ "ableiten",
618
+ "schritt f\xFCr schritt",
619
+ "gedankenkette",
620
+ "formal",
621
+ "mathematisch",
622
+ "logisch"
623
+ ],
624
+ simpleKeywords: [
625
+ // English
626
+ "what is",
627
+ "define",
628
+ "translate",
629
+ "hello",
630
+ "yes or no",
631
+ "capital of",
632
+ "how old",
633
+ "who is",
634
+ "when was",
635
+ // Chinese
636
+ "\u4EC0\u4E48\u662F",
637
+ "\u5B9A\u4E49",
638
+ "\u7FFB\u8BD1",
639
+ "\u4F60\u597D",
640
+ "\u662F\u5426",
641
+ "\u9996\u90FD",
642
+ "\u591A\u5927",
643
+ "\u8C01\u662F",
644
+ "\u4F55\u65F6",
645
+ // Japanese
646
+ "\u3068\u306F",
647
+ "\u5B9A\u7FA9",
648
+ "\u7FFB\u8A33",
649
+ "\u3053\u3093\u306B\u3061\u306F",
650
+ "\u306F\u3044\u304B\u3044\u3044\u3048",
651
+ "\u9996\u90FD",
652
+ "\u8AB0",
653
+ // Russian
654
+ "\u0447\u0442\u043E \u0442\u0430\u043A\u043E\u0435",
655
+ "\u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u0435",
656
+ "\u043F\u0435\u0440\u0435\u0432\u0435\u0441\u0442\u0438",
657
+ "\u043F\u0435\u0440\u0435\u0432\u0435\u0434\u0438",
658
+ "\u043F\u0440\u0438\u0432\u0435\u0442",
659
+ "\u0434\u0430 \u0438\u043B\u0438 \u043D\u0435\u0442",
660
+ "\u0441\u0442\u043E\u043B\u0438\u0446\u0430",
661
+ "\u0441\u043A\u043E\u043B\u044C\u043A\u043E \u043B\u0435\u0442",
662
+ "\u043A\u0442\u043E \u0442\u0430\u043A\u043E\u0439",
663
+ "\u043A\u043E\u0433\u0434\u0430",
664
+ "\u043E\u0431\u044A\u044F\u0441\u043D\u0438",
665
+ // German
666
+ "was ist",
667
+ "definiere",
668
+ "\xFCbersetze",
669
+ "hallo",
670
+ "ja oder nein",
671
+ "hauptstadt",
672
+ "wie alt",
673
+ "wer ist",
674
+ "wann",
675
+ "erkl\xE4re"
676
+ ],
677
+ technicalKeywords: [
678
+ // English
679
+ "algorithm",
680
+ "optimize",
681
+ "architecture",
682
+ "distributed",
683
+ "kubernetes",
684
+ "microservice",
685
+ "database",
686
+ "infrastructure",
687
+ // Chinese
688
+ "\u7B97\u6CD5",
689
+ "\u4F18\u5316",
690
+ "\u67B6\u6784",
691
+ "\u5206\u5E03\u5F0F",
692
+ "\u5FAE\u670D\u52A1",
693
+ "\u6570\u636E\u5E93",
694
+ "\u57FA\u7840\u8BBE\u65BD",
695
+ // Japanese
696
+ "\u30A2\u30EB\u30B4\u30EA\u30BA\u30E0",
697
+ "\u6700\u9069\u5316",
698
+ "\u30A2\u30FC\u30AD\u30C6\u30AF\u30C1\u30E3",
699
+ "\u5206\u6563",
700
+ "\u30DE\u30A4\u30AF\u30ED\u30B5\u30FC\u30D3\u30B9",
701
+ "\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9",
702
+ // Russian
703
+ "\u0430\u043B\u0433\u043E\u0440\u0438\u0442\u043C",
704
+ "\u043E\u043F\u0442\u0438\u043C\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u0442\u044C",
705
+ "\u043E\u043F\u0442\u0438\u043C\u0438\u0437\u0430\u0446\u0438",
706
+ "\u043E\u043F\u0442\u0438\u043C\u0438\u0437\u0438\u0440\u0443\u0439",
707
+ "\u0430\u0440\u0445\u0438\u0442\u0435\u043A\u0442\u0443\u0440\u0430",
708
+ "\u0440\u0430\u0441\u043F\u0440\u0435\u0434\u0435\u043B\u0451\u043D\u043D\u044B\u0439",
709
+ "\u043C\u0438\u043A\u0440\u043E\u0441\u0435\u0440\u0432\u0438\u0441",
710
+ "\u0431\u0430\u0437\u0430 \u0434\u0430\u043D\u043D\u044B\u0445",
711
+ "\u0438\u043D\u0444\u0440\u0430\u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0430",
712
+ // German
713
+ "algorithmus",
714
+ "optimieren",
715
+ "architektur",
716
+ "verteilt",
717
+ "kubernetes",
718
+ "mikroservice",
719
+ "datenbank",
720
+ "infrastruktur"
721
+ ],
722
+ creativeKeywords: [
723
+ // English
724
+ "story",
725
+ "poem",
726
+ "compose",
727
+ "brainstorm",
728
+ "creative",
729
+ "imagine",
730
+ "write a",
731
+ // Chinese
732
+ "\u6545\u4E8B",
733
+ "\u8BD7",
734
+ "\u521B\u4F5C",
735
+ "\u5934\u8111\u98CE\u66B4",
736
+ "\u521B\u610F",
737
+ "\u60F3\u8C61",
738
+ "\u5199\u4E00\u4E2A",
739
+ // Japanese
740
+ "\u7269\u8A9E",
741
+ "\u8A69",
742
+ "\u4F5C\u66F2",
743
+ "\u30D6\u30EC\u30A4\u30F3\u30B9\u30C8\u30FC\u30E0",
744
+ "\u5275\u9020\u7684",
745
+ "\u60F3\u50CF",
746
+ // Russian
747
+ "\u0438\u0441\u0442\u043E\u0440\u0438\u044F",
748
+ "\u0440\u0430\u0441\u0441\u043A\u0430\u0437",
749
+ "\u0441\u0442\u0438\u0445\u043E\u0442\u0432\u043E\u0440\u0435\u043D\u0438\u0435",
750
+ "\u0441\u043E\u0447\u0438\u043D\u0438\u0442\u044C",
751
+ "\u0441\u043E\u0447\u0438\u043D\u0438",
752
+ "\u043C\u043E\u0437\u0433\u043E\u0432\u043E\u0439 \u0448\u0442\u0443\u0440\u043C",
753
+ "\u0442\u0432\u043E\u0440\u0447\u0435\u0441\u043A\u0438\u0439",
754
+ "\u043F\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u0438\u0442\u044C",
755
+ "\u043F\u0440\u0438\u0434\u0443\u043C\u0430\u0439",
756
+ "\u043D\u0430\u043F\u0438\u0448\u0438",
757
+ // German
758
+ "geschichte",
759
+ "gedicht",
760
+ "komponieren",
761
+ "brainstorming",
762
+ "kreativ",
763
+ "vorstellen",
764
+ "schreibe",
765
+ "erz\xE4hlung"
766
+ ],
767
+ // New dimension keyword lists (multilingual)
768
+ imperativeVerbs: [
769
+ // English
770
+ "build",
771
+ "create",
772
+ "implement",
773
+ "design",
774
+ "develop",
775
+ "construct",
776
+ "generate",
777
+ "deploy",
778
+ "configure",
779
+ "set up",
780
+ // Chinese
781
+ "\u6784\u5EFA",
782
+ "\u521B\u5EFA",
783
+ "\u5B9E\u73B0",
784
+ "\u8BBE\u8BA1",
785
+ "\u5F00\u53D1",
786
+ "\u751F\u6210",
787
+ "\u90E8\u7F72",
788
+ "\u914D\u7F6E",
789
+ "\u8BBE\u7F6E",
790
+ // Japanese
791
+ "\u69CB\u7BC9",
792
+ "\u4F5C\u6210",
793
+ "\u5B9F\u88C5",
794
+ "\u8A2D\u8A08",
795
+ "\u958B\u767A",
796
+ "\u751F\u6210",
797
+ "\u30C7\u30D7\u30ED\u30A4",
798
+ "\u8A2D\u5B9A",
799
+ // Russian
800
+ "\u043F\u043E\u0441\u0442\u0440\u043E\u0438\u0442\u044C",
801
+ "\u043F\u043E\u0441\u0442\u0440\u043E\u0439",
802
+ "\u0441\u043E\u0437\u0434\u0430\u0442\u044C",
803
+ "\u0441\u043E\u0437\u0434\u0430\u0439",
804
+ "\u0440\u0435\u0430\u043B\u0438\u0437\u043E\u0432\u0430\u0442\u044C",
805
+ "\u0440\u0435\u0430\u043B\u0438\u0437\u0443\u0439",
806
+ "\u0441\u043F\u0440\u043E\u0435\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C",
807
+ "\u0440\u0430\u0437\u0440\u0430\u0431\u043E\u0442\u0430\u0442\u044C",
808
+ "\u0440\u0430\u0437\u0440\u0430\u0431\u043E\u0442\u0430\u0439",
809
+ "\u0441\u043A\u043E\u043D\u0441\u0442\u0440\u0443\u0438\u0440\u043E\u0432\u0430\u0442\u044C",
810
+ "\u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C",
811
+ "\u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0439",
812
+ "\u0440\u0430\u0437\u0432\u0435\u0440\u043D\u0443\u0442\u044C",
813
+ "\u0440\u0430\u0437\u0432\u0435\u0440\u043D\u0438",
814
+ "\u043D\u0430\u0441\u0442\u0440\u043E\u0438\u0442\u044C",
815
+ "\u043D\u0430\u0441\u0442\u0440\u043E\u0439",
816
+ // German
817
+ "erstellen",
818
+ "bauen",
819
+ "implementieren",
820
+ "entwerfen",
821
+ "entwickeln",
822
+ "konstruieren",
823
+ "generieren",
824
+ "bereitstellen",
825
+ "konfigurieren",
826
+ "einrichten"
827
+ ],
828
+ constraintIndicators: [
829
+ // English
830
+ "under",
831
+ "at most",
832
+ "at least",
833
+ "within",
834
+ "no more than",
835
+ "o(",
836
+ "maximum",
837
+ "minimum",
838
+ "limit",
839
+ "budget",
840
+ // Chinese
841
+ "\u4E0D\u8D85\u8FC7",
842
+ "\u81F3\u5C11",
843
+ "\u6700\u591A",
844
+ "\u5728\u5185",
845
+ "\u6700\u5927",
846
+ "\u6700\u5C0F",
847
+ "\u9650\u5236",
848
+ "\u9884\u7B97",
849
+ // Japanese
850
+ "\u4EE5\u4E0B",
851
+ "\u6700\u5927",
852
+ "\u6700\u5C0F",
853
+ "\u5236\u9650",
854
+ "\u4E88\u7B97",
855
+ // Russian
856
+ "\u043D\u0435 \u0431\u043E\u043B\u0435\u0435",
857
+ "\u043D\u0435 \u043C\u0435\u043D\u0435\u0435",
858
+ "\u043A\u0430\u043A \u043C\u0438\u043D\u0438\u043C\u0443\u043C",
859
+ "\u0432 \u043F\u0440\u0435\u0434\u0435\u043B\u0430\u0445",
860
+ "\u043C\u0430\u043A\u0441\u0438\u043C\u0443\u043C",
861
+ "\u043C\u0438\u043D\u0438\u043C\u0443\u043C",
862
+ "\u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0435\u043D\u0438\u0435",
863
+ "\u0431\u044E\u0434\u0436\u0435\u0442",
864
+ // German
865
+ "h\xF6chstens",
866
+ "mindestens",
867
+ "innerhalb",
868
+ "nicht mehr als",
869
+ "maximal",
870
+ "minimal",
871
+ "grenze",
872
+ "budget"
873
+ ],
874
+ outputFormatKeywords: [
875
+ // English
876
+ "json",
877
+ "yaml",
878
+ "xml",
879
+ "table",
880
+ "csv",
881
+ "markdown",
882
+ "schema",
883
+ "format as",
884
+ "structured",
885
+ // Chinese
886
+ "\u8868\u683C",
887
+ "\u683C\u5F0F\u5316\u4E3A",
888
+ "\u7ED3\u6784\u5316",
889
+ // Japanese
890
+ "\u30C6\u30FC\u30D6\u30EB",
891
+ "\u30D5\u30A9\u30FC\u30DE\u30C3\u30C8",
892
+ "\u69CB\u9020\u5316",
893
+ // Russian
894
+ "\u0442\u0430\u0431\u043B\u0438\u0446\u0430",
895
+ "\u0444\u043E\u0440\u043C\u0430\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043A\u0430\u043A",
896
+ "\u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0439",
897
+ // German
898
+ "tabelle",
899
+ "formatieren als",
900
+ "strukturiert"
901
+ ],
902
+ referenceKeywords: [
903
+ // English
904
+ "above",
905
+ "below",
906
+ "previous",
907
+ "following",
908
+ "the docs",
909
+ "the api",
910
+ "the code",
911
+ "earlier",
912
+ "attached",
913
+ // Chinese
914
+ "\u4E0A\u9762",
915
+ "\u4E0B\u9762",
916
+ "\u4E4B\u524D",
917
+ "\u63A5\u4E0B\u6765",
918
+ "\u6587\u6863",
919
+ "\u4EE3\u7801",
920
+ "\u9644\u4EF6",
921
+ // Japanese
922
+ "\u4E0A\u8A18",
923
+ "\u4E0B\u8A18",
924
+ "\u524D\u306E",
925
+ "\u6B21\u306E",
926
+ "\u30C9\u30AD\u30E5\u30E1\u30F3\u30C8",
927
+ "\u30B3\u30FC\u30C9",
928
+ // Russian
929
+ "\u0432\u044B\u0448\u0435",
930
+ "\u043D\u0438\u0436\u0435",
931
+ "\u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0438\u0439",
932
+ "\u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439",
933
+ "\u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0430\u0446\u0438\u044F",
934
+ "\u043A\u043E\u0434",
935
+ "\u0440\u0430\u043D\u0435\u0435",
936
+ "\u0432\u043B\u043E\u0436\u0435\u043D\u0438\u0435",
937
+ // German
938
+ "oben",
939
+ "unten",
940
+ "vorherige",
941
+ "folgende",
942
+ "dokumentation",
943
+ "der code",
944
+ "fr\xFCher",
945
+ "anhang"
946
+ ],
947
+ negationKeywords: [
948
+ // English
949
+ "don't",
950
+ "do not",
951
+ "avoid",
952
+ "never",
953
+ "without",
954
+ "except",
955
+ "exclude",
956
+ "no longer",
957
+ // Chinese
958
+ "\u4E0D\u8981",
959
+ "\u907F\u514D",
960
+ "\u4ECE\u4E0D",
961
+ "\u6CA1\u6709",
962
+ "\u9664\u4E86",
963
+ "\u6392\u9664",
964
+ // Japanese
965
+ "\u3057\u306A\u3044\u3067",
966
+ "\u907F\u3051\u308B",
967
+ "\u6C7A\u3057\u3066",
968
+ "\u306A\u3057\u3067",
969
+ "\u9664\u304F",
970
+ // Russian
971
+ "\u043D\u0435 \u0434\u0435\u043B\u0430\u0439",
972
+ "\u043D\u0435 \u043D\u0430\u0434\u043E",
973
+ "\u043D\u0435\u043B\u044C\u0437\u044F",
974
+ "\u0438\u0437\u0431\u0435\u0433\u0430\u0442\u044C",
975
+ "\u043D\u0438\u043A\u043E\u0433\u0434\u0430",
976
+ "\u0431\u0435\u0437",
977
+ "\u043A\u0440\u043E\u043C\u0435",
978
+ "\u0438\u0441\u043A\u043B\u044E\u0447\u0438\u0442\u044C",
979
+ "\u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435",
980
+ // German
981
+ "nicht",
982
+ "vermeide",
983
+ "niemals",
984
+ "ohne",
985
+ "au\xDFer",
986
+ "ausschlie\xDFen",
987
+ "nicht mehr"
988
+ ],
989
+ domainSpecificKeywords: [
990
+ // English
991
+ "quantum",
992
+ "fpga",
993
+ "vlsi",
994
+ "risc-v",
995
+ "asic",
996
+ "photonics",
997
+ "genomics",
998
+ "proteomics",
999
+ "topological",
1000
+ "homomorphic",
1001
+ "zero-knowledge",
1002
+ "lattice-based",
1003
+ // Chinese
1004
+ "\u91CF\u5B50",
1005
+ "\u5149\u5B50\u5B66",
1006
+ "\u57FA\u56E0\u7EC4\u5B66",
1007
+ "\u86CB\u767D\u8D28\u7EC4\u5B66",
1008
+ "\u62D3\u6251",
1009
+ "\u540C\u6001",
1010
+ "\u96F6\u77E5\u8BC6",
1011
+ "\u683C\u5BC6\u7801",
1012
+ // Japanese
1013
+ "\u91CF\u5B50",
1014
+ "\u30D5\u30A9\u30C8\u30CB\u30AF\u30B9",
1015
+ "\u30B2\u30CE\u30DF\u30AF\u30B9",
1016
+ "\u30C8\u30DD\u30ED\u30B8\u30AB\u30EB",
1017
+ // Russian
1018
+ "\u043A\u0432\u0430\u043D\u0442\u043E\u0432\u044B\u0439",
1019
+ "\u0444\u043E\u0442\u043E\u043D\u0438\u043A\u0430",
1020
+ "\u0433\u0435\u043D\u043E\u043C\u0438\u043A\u0430",
1021
+ "\u043F\u0440\u043E\u0442\u0435\u043E\u043C\u0438\u043A\u0430",
1022
+ "\u0442\u043E\u043F\u043E\u043B\u043E\u0433\u0438\u0447\u0435\u0441\u043A\u0438\u0439",
1023
+ "\u0433\u043E\u043C\u043E\u043C\u043E\u0440\u0444\u043D\u044B\u0439",
1024
+ "\u0441 \u043D\u0443\u043B\u0435\u0432\u044B\u043C \u0440\u0430\u0437\u0433\u043B\u0430\u0448\u0435\u043D\u0438\u0435\u043C",
1025
+ "\u043D\u0430 \u043E\u0441\u043D\u043E\u0432\u0435 \u0440\u0435\u0448\u0451\u0442\u043E\u043A",
1026
+ // German
1027
+ "quanten",
1028
+ "photonik",
1029
+ "genomik",
1030
+ "proteomik",
1031
+ "topologisch",
1032
+ "homomorph",
1033
+ "zero-knowledge",
1034
+ "gitterbasiert"
1035
+ ],
1036
+ // Agentic task keywords - file ops, execution, multi-step, iterative work
1037
+ // Pruned: removed overly common words like "then", "first", "run", "test", "build"
1038
+ agenticTaskKeywords: [
1039
+ // English - File operations (clearly agentic)
1040
+ "read file",
1041
+ "read the file",
1042
+ "look at",
1043
+ "check the",
1044
+ "open the",
1045
+ "edit",
1046
+ "modify",
1047
+ "update the",
1048
+ "change the",
1049
+ "write to",
1050
+ "create file",
1051
+ // English - Execution (specific commands only)
1052
+ "execute",
1053
+ "deploy",
1054
+ "install",
1055
+ "npm",
1056
+ "pip",
1057
+ "compile",
1058
+ // English - Multi-step patterns (specific only)
1059
+ "after that",
1060
+ "and also",
1061
+ "once done",
1062
+ "step 1",
1063
+ "step 2",
1064
+ // English - Iterative work
1065
+ "fix",
1066
+ "debug",
1067
+ "until it works",
1068
+ "keep trying",
1069
+ "iterate",
1070
+ "make sure",
1071
+ "verify",
1072
+ "confirm",
1073
+ // Chinese (keep specific ones)
1074
+ "\u8BFB\u53D6\u6587\u4EF6",
1075
+ "\u67E5\u770B",
1076
+ "\u6253\u5F00",
1077
+ "\u7F16\u8F91",
1078
+ "\u4FEE\u6539",
1079
+ "\u66F4\u65B0",
1080
+ "\u521B\u5EFA",
1081
+ "\u6267\u884C",
1082
+ "\u90E8\u7F72",
1083
+ "\u5B89\u88C5",
1084
+ "\u7B2C\u4E00\u6B65",
1085
+ "\u7B2C\u4E8C\u6B65",
1086
+ "\u4FEE\u590D",
1087
+ "\u8C03\u8BD5",
1088
+ "\u76F4\u5230",
1089
+ "\u786E\u8BA4",
1090
+ "\u9A8C\u8BC1"
1091
+ ],
1092
+ // Dimension weights (sum to 1.0)
1093
+ dimensionWeights: {
1094
+ tokenCount: 0.08,
1095
+ codePresence: 0.15,
1096
+ reasoningMarkers: 0.18,
1097
+ technicalTerms: 0.1,
1098
+ creativeMarkers: 0.05,
1099
+ simpleIndicators: 0.02,
1100
+ // Reduced from 0.12 to make room for agenticTask
1101
+ multiStepPatterns: 0.12,
1102
+ questionComplexity: 0.05,
1103
+ imperativeVerbs: 0.03,
1104
+ constraintCount: 0.04,
1105
+ outputFormat: 0.03,
1106
+ referenceComplexity: 0.02,
1107
+ negationComplexity: 0.01,
1108
+ domainSpecificity: 0.02,
1109
+ agenticTask: 0.04
1110
+ // Reduced - agentic signals influence tier selection, not dominate it
1111
+ },
1112
+ // Tier boundaries on weighted score axis
1113
+ tierBoundaries: {
1114
+ simpleMedium: 0,
1115
+ mediumComplex: 0.3,
1116
+ // Raised from 0.18 - prevent simple tasks from reaching expensive COMPLEX tier
1117
+ complexReasoning: 0.5
1118
+ // Raised from 0.4 - reserve for true reasoning tasks
1119
+ },
1120
+ // Sigmoid steepness for confidence calibration
1121
+ confidenceSteepness: 12,
1122
+ // Below this confidence → ambiguous (null tier)
1123
+ confidenceThreshold: 0.7
1124
+ },
1125
+ // Auto (balanced) tier configs - current default smart routing
1126
+ tiers: {
1127
+ SIMPLE: {
1128
+ primary: "moonshot/kimi-k2.5",
1129
+ // $0.50/$2.40 - best quality/price for simple tasks
1130
+ fallback: [
1131
+ "google/gemini-2.5-flash",
1132
+ // 1M context, cost-effective
1133
+ "nvidia/gpt-oss-120b",
1134
+ // FREE fallback
1135
+ "deepseek/deepseek-chat"
1136
+ ]
1137
+ },
1138
+ MEDIUM: {
1139
+ primary: "xai/grok-code-fast-1",
1140
+ // Code specialist, $0.20/$1.50
1141
+ fallback: [
1142
+ "google/gemini-2.5-flash",
1143
+ // 1M context, cost-effective
1144
+ "deepseek/deepseek-chat",
1145
+ "xai/grok-4-1-fast-non-reasoning"
1146
+ // Upgraded Grok 4.1
1147
+ ]
1148
+ },
1149
+ COMPLEX: {
1150
+ primary: "google/gemini-3-pro-preview",
1151
+ // Latest Gemini - upgraded from 2.5
1152
+ fallback: [
1153
+ "google/gemini-2.5-flash",
1154
+ // CRITICAL: 1M context, cheap failsafe before expensive models
1155
+ "google/gemini-2.5-pro",
1156
+ "deepseek/deepseek-chat",
1157
+ // Another cheap option
1158
+ "xai/grok-4-0709",
1159
+ "openai/gpt-4o",
1160
+ "openai/gpt-5.2",
1161
+ "anthropic/claude-sonnet-4"
1162
+ ]
1163
+ },
1164
+ REASONING: {
1165
+ primary: "xai/grok-4-1-fast-reasoning",
1166
+ // Upgraded Grok 4.1 reasoning $0.20/$0.50
1167
+ fallback: [
1168
+ "deepseek/deepseek-reasoner",
1169
+ // Cheap reasoning model as first fallback
1170
+ "xai/grok-4-fast-reasoning",
1171
+ "openai/o3",
1172
+ "openai/o4-mini",
1173
+ // Latest o-series mini
1174
+ "moonshot/kimi-k2.5"
1175
+ ]
1176
+ }
1177
+ },
1178
+ // Eco tier configs - ultra cost-optimized (blockrun/eco)
1179
+ ecoTiers: {
1180
+ SIMPLE: {
1181
+ primary: "moonshot/kimi-k2.5",
1182
+ // $0.50/$2.40
1183
+ fallback: ["nvidia/gpt-oss-120b", "deepseek/deepseek-chat", "google/gemini-2.5-flash"]
1184
+ },
1185
+ MEDIUM: {
1186
+ primary: "deepseek/deepseek-chat",
1187
+ // $0.14/$0.28
1188
+ fallback: ["xai/grok-code-fast-1", "google/gemini-2.5-flash", "moonshot/kimi-k2.5"]
1189
+ },
1190
+ COMPLEX: {
1191
+ primary: "xai/grok-4-0709",
1192
+ // $0.20/$1.50
1193
+ fallback: ["deepseek/deepseek-chat", "google/gemini-2.5-flash", "openai/gpt-4o-mini"]
1194
+ },
1195
+ REASONING: {
1196
+ primary: "deepseek/deepseek-reasoner",
1197
+ // $0.55/$2.19
1198
+ fallback: ["xai/grok-4-fast-reasoning", "moonshot/kimi-k2.5"]
1199
+ }
1200
+ },
1201
+ // Premium tier configs - best quality (blockrun/premium)
1202
+ // codex=complex coding, kimi=simple coding, sonnet=reasoning/instructions, opus=architecture/PM/audits
1203
+ premiumTiers: {
1204
+ SIMPLE: {
1205
+ primary: "moonshot/kimi-k2.5",
1206
+ // $0.50/$2.40 - good for simple coding
1207
+ fallback: ["anthropic/claude-haiku-4.5", "google/gemini-2.5-flash", "xai/grok-code-fast-1"]
1208
+ },
1209
+ MEDIUM: {
1210
+ primary: "anthropic/claude-sonnet-4",
1211
+ // $3/$15 - reasoning/instructions
1212
+ fallback: [
1213
+ "openai/gpt-5.2-codex",
1214
+ "moonshot/kimi-k2.5",
1215
+ "google/gemini-2.5-pro",
1216
+ "xai/grok-4-0709"
1217
+ ]
1218
+ },
1219
+ COMPLEX: {
1220
+ primary: "openai/gpt-5.2-codex",
1221
+ // $2.50/$10 - complex coding (78% cost savings vs Opus)
1222
+ fallback: [
1223
+ "anthropic/claude-opus-4.6",
1224
+ "anthropic/claude-opus-4.5",
1225
+ "anthropic/claude-sonnet-4",
1226
+ "google/gemini-3-pro-preview",
1227
+ "moonshot/kimi-k2.5"
1228
+ ]
1229
+ },
1230
+ REASONING: {
1231
+ primary: "anthropic/claude-sonnet-4",
1232
+ // $3/$15 - best for reasoning/instructions
1233
+ fallback: [
1234
+ "anthropic/claude-opus-4.6",
1235
+ "anthropic/claude-opus-4.5",
1236
+ "openai/o3",
1237
+ "xai/grok-4-1-fast-reasoning"
1238
+ ]
1239
+ }
1240
+ },
1241
+ // Agentic tier configs - models that excel at multi-step autonomous tasks
1242
+ agenticTiers: {
1243
+ SIMPLE: {
1244
+ primary: "moonshot/kimi-k2.5",
1245
+ // Cheaper than Haiku ($0.5/$2.4 vs $1/$5), larger context
1246
+ fallback: [
1247
+ "anthropic/claude-haiku-4.5",
1248
+ "xai/grok-4-fast-non-reasoning",
1249
+ "openai/gpt-4o-mini"
1250
+ ]
1251
+ },
1252
+ MEDIUM: {
1253
+ primary: "xai/grok-code-fast-1",
1254
+ // Code specialist for agentic coding
1255
+ fallback: ["moonshot/kimi-k2.5", "anthropic/claude-haiku-4.5", "anthropic/claude-sonnet-4"]
1256
+ },
1257
+ COMPLEX: {
1258
+ primary: "anthropic/claude-sonnet-4",
1259
+ fallback: [
1260
+ "anthropic/claude-opus-4.6",
1261
+ // Latest Opus - best agentic
1262
+ "openai/gpt-5.2",
1263
+ "google/gemini-3-pro-preview",
1264
+ "xai/grok-4-0709"
1265
+ ]
1266
+ },
1267
+ REASONING: {
1268
+ primary: "anthropic/claude-sonnet-4",
1269
+ // Strong tool use + reasoning for agentic tasks
1270
+ fallback: [
1271
+ "anthropic/claude-opus-4.6",
1272
+ "xai/grok-4-fast-reasoning",
1273
+ "moonshot/kimi-k2.5",
1274
+ "deepseek/deepseek-reasoner"
1275
+ ]
1276
+ }
1277
+ },
1278
+ overrides: {
1279
+ maxTokensForceComplex: 1e5,
1280
+ structuredOutputMinTier: "MEDIUM",
1281
+ ambiguousDefaultTier: "MEDIUM",
1282
+ agenticMode: false
1283
+ }
1284
+ };
1285
+
1286
+ // src/router/index.ts
1287
+ function route(prompt, systemPrompt, maxOutputTokens, options) {
1288
+ const { config, modelPricing } = options;
1289
+ const fullText = `${systemPrompt ?? ""} ${prompt}`;
1290
+ const estimatedTokens = Math.ceil(fullText.length / 4);
1291
+ const ruleResult = classifyByRules(prompt, systemPrompt, estimatedTokens, config.scoring);
1292
+ const { routingProfile } = options;
1293
+ let tierConfigs;
1294
+ let profileSuffix = "";
1295
+ if (routingProfile === "eco" && config.ecoTiers) {
1296
+ tierConfigs = config.ecoTiers;
1297
+ profileSuffix = " | eco";
1298
+ } else if (routingProfile === "premium" && config.premiumTiers) {
1299
+ tierConfigs = config.premiumTiers;
1300
+ profileSuffix = " | premium";
1301
+ } else {
1302
+ const agenticScore = ruleResult.agenticScore ?? 0;
1303
+ const isAutoAgentic = agenticScore >= 0.5;
1304
+ const isExplicitAgentic = config.overrides.agenticMode ?? false;
1305
+ const useAgenticTiers = (isAutoAgentic || isExplicitAgentic) && config.agenticTiers != null;
1306
+ tierConfigs = useAgenticTiers ? config.agenticTiers : config.tiers;
1307
+ profileSuffix = useAgenticTiers ? " | agentic" : "";
1308
+ }
1309
+ if (estimatedTokens > config.overrides.maxTokensForceComplex) {
1310
+ return selectModel(
1311
+ "COMPLEX",
1312
+ 0.95,
1313
+ "rules",
1314
+ `Input exceeds ${config.overrides.maxTokensForceComplex} tokens${profileSuffix}`,
1315
+ tierConfigs,
1316
+ modelPricing,
1317
+ estimatedTokens,
1318
+ maxOutputTokens,
1319
+ routingProfile
1320
+ );
1321
+ }
1322
+ const hasStructuredOutput = systemPrompt ? /json|structured|schema/i.test(systemPrompt) : false;
1323
+ let tier;
1324
+ let confidence;
1325
+ const method = "rules";
1326
+ let reasoning = `score=${ruleResult.score.toFixed(2)} | ${ruleResult.signals.join(", ")}`;
1327
+ if (ruleResult.tier !== null) {
1328
+ tier = ruleResult.tier;
1329
+ confidence = ruleResult.confidence;
1330
+ } else {
1331
+ tier = config.overrides.ambiguousDefaultTier;
1332
+ confidence = 0.5;
1333
+ reasoning += ` | ambiguous -> default: ${tier}`;
1334
+ }
1335
+ if (hasStructuredOutput) {
1336
+ const tierRank = { SIMPLE: 0, MEDIUM: 1, COMPLEX: 2, REASONING: 3 };
1337
+ const minTier = config.overrides.structuredOutputMinTier;
1338
+ if (tierRank[tier] < tierRank[minTier]) {
1339
+ reasoning += ` | upgraded to ${minTier} (structured output)`;
1340
+ tier = minTier;
1341
+ }
1342
+ }
1343
+ reasoning += profileSuffix;
1344
+ return selectModel(
1345
+ tier,
1346
+ confidence,
1347
+ method,
1348
+ reasoning,
1349
+ tierConfigs,
1350
+ modelPricing,
1351
+ estimatedTokens,
1352
+ maxOutputTokens,
1353
+ routingProfile
1354
+ );
1355
+ }
1356
+
1357
+ // src/models.ts
1358
+ var MODEL_ALIASES = {
1359
+ // Claude
1360
+ claude: "anthropic/claude-sonnet-4",
1361
+ sonnet: "anthropic/claude-sonnet-4",
1362
+ opus: "anthropic/claude-opus-4.6",
1363
+ // Updated to latest Opus 4.6
1364
+ "opus-46": "anthropic/claude-opus-4.6",
1365
+ "opus-45": "anthropic/claude-opus-4.5",
1366
+ haiku: "anthropic/claude-haiku-4.5",
1367
+ // OpenAI
1368
+ gpt: "openai/gpt-4o",
1369
+ gpt4: "openai/gpt-4o",
1370
+ gpt5: "openai/gpt-5.2",
1371
+ codex: "openai/gpt-5.2-codex",
1372
+ mini: "openai/gpt-4o-mini",
1373
+ o3: "openai/o3",
1374
+ // DeepSeek
1375
+ deepseek: "deepseek/deepseek-chat",
1376
+ reasoner: "deepseek/deepseek-reasoner",
1377
+ // Kimi / Moonshot
1378
+ kimi: "moonshot/kimi-k2.5",
1379
+ // Google
1380
+ gemini: "google/gemini-2.5-pro",
1381
+ flash: "google/gemini-2.5-flash",
1382
+ // xAI
1383
+ grok: "xai/grok-3",
1384
+ "grok-fast": "xai/grok-4-fast-reasoning",
1385
+ "grok-code": "xai/grok-code-fast-1",
1386
+ // NVIDIA
1387
+ nvidia: "nvidia/gpt-oss-120b",
1388
+ "gpt-120b": "nvidia/gpt-oss-120b"
1389
+ // Note: auto, free, eco, premium are virtual routing profiles registered in BLOCKRUN_MODELS
1390
+ // They don't need aliases since they're already top-level model IDs
1391
+ };
1392
+ function resolveModelAlias(model) {
1393
+ const normalized = model.trim().toLowerCase();
1394
+ const resolved = MODEL_ALIASES[normalized];
1395
+ if (resolved) return resolved;
1396
+ if (normalized.startsWith("blockrun/")) {
1397
+ const withoutPrefix = normalized.slice("blockrun/".length);
1398
+ const resolvedWithoutPrefix = MODEL_ALIASES[withoutPrefix];
1399
+ if (resolvedWithoutPrefix) return resolvedWithoutPrefix;
1400
+ return withoutPrefix;
1401
+ }
1402
+ return model;
1403
+ }
1404
+ var BLOCKRUN_MODELS = [
1405
+ // Smart routing meta-models — proxy replaces with actual model
1406
+ // NOTE: Model IDs are WITHOUT provider prefix (OpenClaw adds "blockrun/" automatically)
1407
+ {
1408
+ id: "auto",
1409
+ name: "Auto (Smart Router - Balanced)",
1410
+ inputPrice: 0,
1411
+ outputPrice: 0,
1412
+ contextWindow: 105e4,
1413
+ maxOutput: 128e3
1414
+ },
1415
+ {
1416
+ id: "free",
1417
+ name: "Free (NVIDIA GPT-OSS-120B only)",
1418
+ inputPrice: 0,
1419
+ outputPrice: 0,
1420
+ contextWindow: 128e3,
1421
+ maxOutput: 4096
1422
+ },
1423
+ {
1424
+ id: "eco",
1425
+ name: "Eco (Smart Router - Cost Optimized)",
1426
+ inputPrice: 0,
1427
+ outputPrice: 0,
1428
+ contextWindow: 105e4,
1429
+ maxOutput: 128e3
1430
+ },
1431
+ {
1432
+ id: "premium",
1433
+ name: "Premium (Smart Router - Best Quality)",
1434
+ inputPrice: 0,
1435
+ outputPrice: 0,
1436
+ contextWindow: 2e6,
1437
+ maxOutput: 2e5
1438
+ },
1439
+ // OpenAI GPT-5 Family
1440
+ {
1441
+ id: "openai/gpt-5.2",
1442
+ name: "GPT-5.2",
1443
+ inputPrice: 1.75,
1444
+ outputPrice: 14,
1445
+ contextWindow: 4e5,
1446
+ maxOutput: 128e3,
1447
+ reasoning: true,
1448
+ vision: true,
1449
+ agentic: true
1450
+ },
1451
+ {
1452
+ id: "openai/gpt-5-mini",
1453
+ name: "GPT-5 Mini",
1454
+ inputPrice: 0.25,
1455
+ outputPrice: 2,
1456
+ contextWindow: 2e5,
1457
+ maxOutput: 65536
1458
+ },
1459
+ {
1460
+ id: "openai/gpt-5-nano",
1461
+ name: "GPT-5 Nano",
1462
+ inputPrice: 0.05,
1463
+ outputPrice: 0.4,
1464
+ contextWindow: 128e3,
1465
+ maxOutput: 32768
1466
+ },
1467
+ {
1468
+ id: "openai/gpt-5.2-pro",
1469
+ name: "GPT-5.2 Pro",
1470
+ inputPrice: 21,
1471
+ outputPrice: 168,
1472
+ contextWindow: 4e5,
1473
+ maxOutput: 128e3,
1474
+ reasoning: true
1475
+ },
1476
+ // OpenAI Codex Family
1477
+ {
1478
+ id: "openai/gpt-5.2-codex",
1479
+ name: "GPT-5.2 Codex",
1480
+ inputPrice: 2.5,
1481
+ outputPrice: 12,
1482
+ contextWindow: 128e3,
1483
+ maxOutput: 32e3,
1484
+ agentic: true
1485
+ },
1486
+ // OpenAI GPT-4 Family
1487
+ {
1488
+ id: "openai/gpt-4.1",
1489
+ name: "GPT-4.1",
1490
+ inputPrice: 2,
1491
+ outputPrice: 8,
1492
+ contextWindow: 128e3,
1493
+ maxOutput: 16384,
1494
+ vision: true
1495
+ },
1496
+ {
1497
+ id: "openai/gpt-4.1-mini",
1498
+ name: "GPT-4.1 Mini",
1499
+ inputPrice: 0.4,
1500
+ outputPrice: 1.6,
1501
+ contextWindow: 128e3,
1502
+ maxOutput: 16384
1503
+ },
1504
+ // gpt-4.1-nano removed - replaced by gpt-5-nano
1505
+ {
1506
+ id: "openai/gpt-4o",
1507
+ name: "GPT-4o",
1508
+ inputPrice: 2.5,
1509
+ outputPrice: 10,
1510
+ contextWindow: 128e3,
1511
+ maxOutput: 16384,
1512
+ vision: true,
1513
+ agentic: true
1514
+ },
1515
+ {
1516
+ id: "openai/gpt-4o-mini",
1517
+ name: "GPT-4o Mini",
1518
+ inputPrice: 0.15,
1519
+ outputPrice: 0.6,
1520
+ contextWindow: 128e3,
1521
+ maxOutput: 16384
1522
+ },
1523
+ // OpenAI O-series (Reasoning) - o1/o1-mini removed, replaced by o3/o4
1524
+ {
1525
+ id: "openai/o3",
1526
+ name: "o3",
1527
+ inputPrice: 2,
1528
+ outputPrice: 8,
1529
+ contextWindow: 2e5,
1530
+ maxOutput: 1e5,
1531
+ reasoning: true
1532
+ },
1533
+ {
1534
+ id: "openai/o3-mini",
1535
+ name: "o3-mini",
1536
+ inputPrice: 1.1,
1537
+ outputPrice: 4.4,
1538
+ contextWindow: 128e3,
1539
+ maxOutput: 65536,
1540
+ reasoning: true
1541
+ },
1542
+ {
1543
+ id: "openai/o4-mini",
1544
+ name: "o4-mini",
1545
+ inputPrice: 1.1,
1546
+ outputPrice: 4.4,
1547
+ contextWindow: 128e3,
1548
+ maxOutput: 65536,
1549
+ reasoning: true
1550
+ },
1551
+ // Anthropic - all Claude models excel at agentic workflows
1552
+ {
1553
+ id: "anthropic/claude-haiku-4.5",
1554
+ name: "Claude Haiku 4.5",
1555
+ inputPrice: 1,
1556
+ outputPrice: 5,
1557
+ contextWindow: 2e5,
1558
+ maxOutput: 8192,
1559
+ agentic: true
1560
+ },
1561
+ {
1562
+ id: "anthropic/claude-sonnet-4",
1563
+ name: "Claude Sonnet 4",
1564
+ inputPrice: 3,
1565
+ outputPrice: 15,
1566
+ contextWindow: 2e5,
1567
+ maxOutput: 64e3,
1568
+ reasoning: true,
1569
+ agentic: true
1570
+ },
1571
+ {
1572
+ id: "anthropic/claude-opus-4",
1573
+ name: "Claude Opus 4",
1574
+ inputPrice: 15,
1575
+ outputPrice: 75,
1576
+ contextWindow: 2e5,
1577
+ maxOutput: 32e3,
1578
+ reasoning: true,
1579
+ agentic: true
1580
+ },
1581
+ {
1582
+ id: "anthropic/claude-opus-4.5",
1583
+ name: "Claude Opus 4.5",
1584
+ inputPrice: 5,
1585
+ outputPrice: 25,
1586
+ contextWindow: 2e5,
1587
+ maxOutput: 32e3,
1588
+ reasoning: true,
1589
+ agentic: true
1590
+ },
1591
+ {
1592
+ id: "anthropic/claude-opus-4.6",
1593
+ name: "Claude Opus 4.6",
1594
+ inputPrice: 5,
1595
+ outputPrice: 25,
1596
+ contextWindow: 2e5,
1597
+ maxOutput: 64e3,
1598
+ reasoning: true,
1599
+ vision: true,
1600
+ agentic: true
1601
+ },
1602
+ // Google
1603
+ {
1604
+ id: "google/gemini-3-pro-preview",
1605
+ name: "Gemini 3 Pro Preview",
1606
+ inputPrice: 2,
1607
+ outputPrice: 12,
1608
+ contextWindow: 105e4,
1609
+ maxOutput: 65536,
1610
+ reasoning: true,
1611
+ vision: true
1612
+ },
1613
+ {
1614
+ id: "google/gemini-2.5-pro",
1615
+ name: "Gemini 2.5 Pro",
1616
+ inputPrice: 1.25,
1617
+ outputPrice: 10,
1618
+ contextWindow: 105e4,
1619
+ maxOutput: 65536,
1620
+ reasoning: true,
1621
+ vision: true
1622
+ },
1623
+ {
1624
+ id: "google/gemini-2.5-flash",
1625
+ name: "Gemini 2.5 Flash",
1626
+ inputPrice: 0.15,
1627
+ outputPrice: 0.6,
1628
+ contextWindow: 1e6,
1629
+ maxOutput: 65536
1630
+ },
1631
+ // DeepSeek
1632
+ {
1633
+ id: "deepseek/deepseek-chat",
1634
+ name: "DeepSeek V3.2 Chat",
1635
+ inputPrice: 0.28,
1636
+ outputPrice: 0.42,
1637
+ contextWindow: 128e3,
1638
+ maxOutput: 8192
1639
+ },
1640
+ {
1641
+ id: "deepseek/deepseek-reasoner",
1642
+ name: "DeepSeek V3.2 Reasoner",
1643
+ inputPrice: 0.28,
1644
+ outputPrice: 0.42,
1645
+ contextWindow: 128e3,
1646
+ maxOutput: 8192,
1647
+ reasoning: true
1648
+ },
1649
+ // Moonshot / Kimi - optimized for agentic workflows
1650
+ {
1651
+ id: "moonshot/kimi-k2.5",
1652
+ name: "Kimi K2.5",
1653
+ inputPrice: 0.5,
1654
+ outputPrice: 2.4,
1655
+ contextWindow: 262144,
1656
+ maxOutput: 8192,
1657
+ reasoning: true,
1658
+ vision: true,
1659
+ agentic: true
1660
+ },
1661
+ // xAI / Grok
1662
+ {
1663
+ id: "xai/grok-3",
1664
+ name: "Grok 3",
1665
+ inputPrice: 3,
1666
+ outputPrice: 15,
1667
+ contextWindow: 131072,
1668
+ maxOutput: 16384,
1669
+ reasoning: true
1670
+ },
1671
+ // grok-3-fast removed - too expensive ($5/$25), use grok-4-fast instead
1672
+ {
1673
+ id: "xai/grok-3-mini",
1674
+ name: "Grok 3 Mini",
1675
+ inputPrice: 0.3,
1676
+ outputPrice: 0.5,
1677
+ contextWindow: 131072,
1678
+ maxOutput: 16384
1679
+ },
1680
+ // xAI Grok 4 Family - Ultra-cheap fast models
1681
+ {
1682
+ id: "xai/grok-4-fast-reasoning",
1683
+ name: "Grok 4 Fast Reasoning",
1684
+ inputPrice: 0.2,
1685
+ outputPrice: 0.5,
1686
+ contextWindow: 131072,
1687
+ maxOutput: 16384,
1688
+ reasoning: true
1689
+ },
1690
+ {
1691
+ id: "xai/grok-4-fast-non-reasoning",
1692
+ name: "Grok 4 Fast",
1693
+ inputPrice: 0.2,
1694
+ outputPrice: 0.5,
1695
+ contextWindow: 131072,
1696
+ maxOutput: 16384
1697
+ },
1698
+ {
1699
+ id: "xai/grok-4-1-fast-reasoning",
1700
+ name: "Grok 4.1 Fast Reasoning",
1701
+ inputPrice: 0.2,
1702
+ outputPrice: 0.5,
1703
+ contextWindow: 131072,
1704
+ maxOutput: 16384,
1705
+ reasoning: true
1706
+ },
1707
+ {
1708
+ id: "xai/grok-4-1-fast-non-reasoning",
1709
+ name: "Grok 4.1 Fast",
1710
+ inputPrice: 0.2,
1711
+ outputPrice: 0.5,
1712
+ contextWindow: 131072,
1713
+ maxOutput: 16384
1714
+ },
1715
+ {
1716
+ id: "xai/grok-code-fast-1",
1717
+ name: "Grok Code Fast",
1718
+ inputPrice: 0.2,
1719
+ outputPrice: 1.5,
1720
+ contextWindow: 131072,
1721
+ maxOutput: 16384,
1722
+ agentic: true
1723
+ // Good for coding tasks
1724
+ },
1725
+ {
1726
+ id: "xai/grok-4-0709",
1727
+ name: "Grok 4 (0709)",
1728
+ inputPrice: 0.2,
1729
+ outputPrice: 1.5,
1730
+ contextWindow: 131072,
1731
+ maxOutput: 16384,
1732
+ reasoning: true
1733
+ },
1734
+ // grok-2-vision removed - old, 0 transactions
1735
+ // NVIDIA - Free/cheap models
1736
+ {
1737
+ id: "nvidia/gpt-oss-120b",
1738
+ name: "NVIDIA GPT-OSS 120B",
1739
+ inputPrice: 0,
1740
+ outputPrice: 0,
1741
+ contextWindow: 128e3,
1742
+ maxOutput: 16384
1743
+ },
1744
+ {
1745
+ id: "nvidia/kimi-k2.5",
1746
+ name: "NVIDIA Kimi K2.5",
1747
+ inputPrice: 0.55,
1748
+ outputPrice: 2.5,
1749
+ contextWindow: 262144,
1750
+ maxOutput: 16384
1751
+ }
1752
+ ];
1753
+ function toOpenClawModel(m) {
1754
+ return {
1755
+ id: m.id,
1756
+ name: m.name,
1757
+ api: "openai-completions",
1758
+ reasoning: m.reasoning ?? false,
1759
+ input: m.vision ? ["text", "image"] : ["text"],
1760
+ cost: {
1761
+ input: m.inputPrice,
1762
+ output: m.outputPrice,
1763
+ cacheRead: 0,
1764
+ cacheWrite: 0
1765
+ },
1766
+ contextWindow: m.contextWindow,
1767
+ maxTokens: m.maxOutput
1768
+ };
1769
+ }
1770
+ var ALIAS_MODELS = Object.entries(MODEL_ALIASES).map(([alias, targetId]) => {
1771
+ const target = BLOCKRUN_MODELS.find((m) => m.id === targetId);
1772
+ if (!target) return null;
1773
+ return toOpenClawModel({ ...target, id: alias, name: `${alias} \u2192 ${target.name}` });
1774
+ }).filter((m) => m !== null);
1775
+ var OPENCLAW_MODELS = [
1776
+ ...BLOCKRUN_MODELS.map(toOpenClawModel),
1777
+ ...ALIAS_MODELS
1778
+ ];
1779
+ function getModelContextWindow(modelId) {
1780
+ const normalized = modelId.replace("blockrun/", "");
1781
+ const model = BLOCKRUN_MODELS.find((m) => m.id === normalized);
1782
+ return model?.contextWindow;
1783
+ }
1784
+ function isReasoningModel(modelId) {
1785
+ const normalized = modelId.replace("blockrun/", "");
1786
+ const model = BLOCKRUN_MODELS.find((m) => m.id === normalized);
1787
+ return model?.reasoning ?? false;
1788
+ }
1789
+
1790
+ // src/logger.ts
1791
+ import { appendFile, mkdir } from "fs/promises";
1792
+ import { join } from "path";
1793
+ import { homedir } from "os";
1794
+ var LOG_DIR = join(homedir(), ".openclaw", "blockrun", "logs");
1795
+ var dirReady = false;
1796
+ async function ensureDir() {
1797
+ if (dirReady) return;
1798
+ await mkdir(LOG_DIR, { recursive: true });
1799
+ dirReady = true;
1800
+ }
1801
+ async function logUsage(entry) {
1802
+ try {
1803
+ await ensureDir();
1804
+ const date = entry.timestamp.slice(0, 10);
1805
+ const file = join(LOG_DIR, `usage-${date}.jsonl`);
1806
+ await appendFile(file, JSON.stringify(entry) + "\n");
1807
+ } catch {
1808
+ }
1809
+ }
1810
+
1811
+ // src/stats.ts
1812
+ import { readFile, readdir } from "fs/promises";
1813
+ import { join as join3 } from "path";
1814
+ import { homedir as homedir2 } from "os";
1815
+
1816
+ // src/version.ts
1817
+ import { createRequire } from "module";
1818
+ import { fileURLToPath } from "url";
1819
+ import { dirname, join as join2 } from "path";
1820
+ var __filename = fileURLToPath(import.meta.url);
1821
+ var __dirname = dirname(__filename);
1822
+ var require2 = createRequire(import.meta.url);
1823
+ var pkg = require2(join2(__dirname, "..", "package.json"));
1824
+ var VERSION = pkg.version;
1825
+ var USER_AGENT = `clawrouter/${VERSION}`;
1826
+
1827
+ // src/stats.ts
1828
+ var LOG_DIR2 = join3(homedir2(), ".openclaw", "blockrun", "logs");
1829
+ async function parseLogFile(filePath) {
1830
+ try {
1831
+ const content = await readFile(filePath, "utf-8");
1832
+ const lines = content.trim().split("\n").filter(Boolean);
1833
+ return lines.map((line) => {
1834
+ const entry = JSON.parse(line);
1835
+ return {
1836
+ timestamp: entry.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
1837
+ model: entry.model || "unknown",
1838
+ tier: entry.tier || "UNKNOWN",
1839
+ cost: entry.cost || 0,
1840
+ baselineCost: entry.baselineCost || entry.cost || 0,
1841
+ savings: entry.savings || 0,
1842
+ latencyMs: entry.latencyMs || 0
1843
+ };
1844
+ });
1845
+ } catch {
1846
+ return [];
1847
+ }
1848
+ }
1849
+ async function getLogFiles() {
1850
+ try {
1851
+ const files = await readdir(LOG_DIR2);
1852
+ return files.filter((f) => f.startsWith("usage-") && f.endsWith(".jsonl")).sort().reverse();
1853
+ } catch {
1854
+ return [];
1855
+ }
1856
+ }
1857
+ function aggregateDay(date, entries) {
1858
+ const byTier = {};
1859
+ const byModel = {};
1860
+ let totalLatency = 0;
1861
+ for (const entry of entries) {
1862
+ if (!byTier[entry.tier]) byTier[entry.tier] = { count: 0, cost: 0 };
1863
+ byTier[entry.tier].count++;
1864
+ byTier[entry.tier].cost += entry.cost;
1865
+ if (!byModel[entry.model]) byModel[entry.model] = { count: 0, cost: 0 };
1866
+ byModel[entry.model].count++;
1867
+ byModel[entry.model].cost += entry.cost;
1868
+ totalLatency += entry.latencyMs;
1869
+ }
1870
+ const totalCost = entries.reduce((sum, e) => sum + e.cost, 0);
1871
+ const totalBaselineCost = entries.reduce((sum, e) => sum + e.baselineCost, 0);
1872
+ return {
1873
+ date,
1874
+ totalRequests: entries.length,
1875
+ totalCost,
1876
+ totalBaselineCost,
1877
+ totalSavings: totalBaselineCost - totalCost,
1878
+ avgLatencyMs: entries.length > 0 ? totalLatency / entries.length : 0,
1879
+ byTier,
1880
+ byModel
1881
+ };
1882
+ }
1883
+ async function getStats(days = 7) {
1884
+ const logFiles = await getLogFiles();
1885
+ const filesToRead = logFiles.slice(0, days);
1886
+ const dailyBreakdown = [];
1887
+ const allByTier = {};
1888
+ const allByModel = {};
1889
+ let totalRequests = 0;
1890
+ let totalCost = 0;
1891
+ let totalBaselineCost = 0;
1892
+ let totalLatency = 0;
1893
+ for (const file of filesToRead) {
1894
+ const date = file.replace("usage-", "").replace(".jsonl", "");
1895
+ const filePath = join3(LOG_DIR2, file);
1896
+ const entries = await parseLogFile(filePath);
1897
+ if (entries.length === 0) continue;
1898
+ const dayStats = aggregateDay(date, entries);
1899
+ dailyBreakdown.push(dayStats);
1900
+ totalRequests += dayStats.totalRequests;
1901
+ totalCost += dayStats.totalCost;
1902
+ totalBaselineCost += dayStats.totalBaselineCost;
1903
+ totalLatency += dayStats.avgLatencyMs * dayStats.totalRequests;
1904
+ for (const [tier, stats] of Object.entries(dayStats.byTier)) {
1905
+ if (!allByTier[tier]) allByTier[tier] = { count: 0, cost: 0 };
1906
+ allByTier[tier].count += stats.count;
1907
+ allByTier[tier].cost += stats.cost;
1908
+ }
1909
+ for (const [model, stats] of Object.entries(dayStats.byModel)) {
1910
+ if (!allByModel[model]) allByModel[model] = { count: 0, cost: 0 };
1911
+ allByModel[model].count += stats.count;
1912
+ allByModel[model].cost += stats.cost;
1913
+ }
1914
+ }
1915
+ const byTierWithPercentage = {};
1916
+ for (const [tier, stats] of Object.entries(allByTier)) {
1917
+ byTierWithPercentage[tier] = {
1918
+ ...stats,
1919
+ percentage: totalRequests > 0 ? stats.count / totalRequests * 100 : 0
1920
+ };
1921
+ }
1922
+ const byModelWithPercentage = {};
1923
+ for (const [model, stats] of Object.entries(allByModel)) {
1924
+ byModelWithPercentage[model] = {
1925
+ ...stats,
1926
+ percentage: totalRequests > 0 ? stats.count / totalRequests * 100 : 0
1927
+ };
1928
+ }
1929
+ const totalSavings = totalBaselineCost - totalCost;
1930
+ const savingsPercentage = totalBaselineCost > 0 ? totalSavings / totalBaselineCost * 100 : 0;
1931
+ let entriesWithBaseline = 0;
1932
+ for (const day of dailyBreakdown) {
1933
+ if (day.totalBaselineCost !== day.totalCost) {
1934
+ entriesWithBaseline += day.totalRequests;
1935
+ }
1936
+ }
1937
+ return {
1938
+ period: days === 1 ? "today" : `last ${days} days`,
1939
+ totalRequests,
1940
+ totalCost,
1941
+ totalBaselineCost,
1942
+ totalSavings,
1943
+ savingsPercentage,
1944
+ avgLatencyMs: totalRequests > 0 ? totalLatency / totalRequests : 0,
1945
+ avgCostPerRequest: totalRequests > 0 ? totalCost / totalRequests : 0,
1946
+ byTier: byTierWithPercentage,
1947
+ byModel: byModelWithPercentage,
1948
+ dailyBreakdown: dailyBreakdown.reverse(),
1949
+ // Oldest first for charts
1950
+ entriesWithBaseline
1951
+ // How many entries have valid baseline tracking
1952
+ };
1953
+ }
1954
+
1955
+ // src/dedup.ts
1956
+ import { createHash } from "crypto";
1957
+ var DEFAULT_TTL_MS2 = 3e4;
1958
+ var MAX_BODY_SIZE = 1048576;
1959
+ function canonicalize(obj) {
1960
+ if (obj === null || typeof obj !== "object") {
1961
+ return obj;
1962
+ }
1963
+ if (Array.isArray(obj)) {
1964
+ return obj.map(canonicalize);
1965
+ }
1966
+ const sorted = {};
1967
+ for (const key of Object.keys(obj).sort()) {
1968
+ sorted[key] = canonicalize(obj[key]);
1969
+ }
1970
+ return sorted;
1971
+ }
1972
+ var TIMESTAMP_PATTERN = /^\[\w{3}\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}\s+\w+\]\s*/;
1973
+ function stripTimestamps(obj) {
1974
+ if (obj === null || typeof obj !== "object") {
1975
+ return obj;
1976
+ }
1977
+ if (Array.isArray(obj)) {
1978
+ return obj.map(stripTimestamps);
1979
+ }
1980
+ const result = {};
1981
+ for (const [key, value] of Object.entries(obj)) {
1982
+ if (key === "content" && typeof value === "string") {
1983
+ result[key] = value.replace(TIMESTAMP_PATTERN, "");
1984
+ } else {
1985
+ result[key] = stripTimestamps(value);
1986
+ }
1987
+ }
1988
+ return result;
1989
+ }
1990
+ var RequestDeduplicator = class {
1991
+ inflight = /* @__PURE__ */ new Map();
1992
+ completed = /* @__PURE__ */ new Map();
1993
+ ttlMs;
1994
+ constructor(ttlMs = DEFAULT_TTL_MS2) {
1995
+ this.ttlMs = ttlMs;
1996
+ }
1997
+ /** Hash request body to create a dedup key. */
1998
+ static hash(body) {
1999
+ let content = body;
2000
+ try {
2001
+ const parsed = JSON.parse(body.toString());
2002
+ const stripped = stripTimestamps(parsed);
2003
+ const canonical = canonicalize(stripped);
2004
+ content = Buffer.from(JSON.stringify(canonical));
2005
+ } catch {
2006
+ }
2007
+ return createHash("sha256").update(content).digest("hex").slice(0, 16);
2008
+ }
2009
+ /** Check if a response is cached for this key. */
2010
+ getCached(key) {
2011
+ const entry = this.completed.get(key);
2012
+ if (!entry) return void 0;
2013
+ if (Date.now() - entry.completedAt > this.ttlMs) {
2014
+ this.completed.delete(key);
2015
+ return void 0;
2016
+ }
2017
+ return entry;
2018
+ }
2019
+ /** Check if a request with this key is currently in-flight. Returns a promise to wait on. */
2020
+ getInflight(key) {
2021
+ const entry = this.inflight.get(key);
2022
+ if (!entry) return void 0;
2023
+ return new Promise((resolve) => {
2024
+ entry.resolvers.push(resolve);
2025
+ });
2026
+ }
2027
+ /** Mark a request as in-flight. */
2028
+ markInflight(key) {
2029
+ this.inflight.set(key, {
2030
+ resolvers: []
2031
+ });
2032
+ }
2033
+ /** Complete an in-flight request — cache result and notify waiters. */
2034
+ complete(key, result) {
2035
+ if (result.body.length <= MAX_BODY_SIZE) {
2036
+ this.completed.set(key, result);
2037
+ }
2038
+ const entry = this.inflight.get(key);
2039
+ if (entry) {
2040
+ for (const resolve of entry.resolvers) {
2041
+ resolve(result);
2042
+ }
2043
+ this.inflight.delete(key);
2044
+ }
2045
+ this.prune();
2046
+ }
2047
+ /** Remove an in-flight entry on error (don't cache failures).
2048
+ * Also rejects any waiters so they can retry independently. */
2049
+ removeInflight(key) {
2050
+ const entry = this.inflight.get(key);
2051
+ if (entry) {
2052
+ const errorBody = Buffer.from(
2053
+ JSON.stringify({
2054
+ error: { message: "Original request failed, please retry", type: "dedup_origin_failed" }
2055
+ })
2056
+ );
2057
+ for (const resolve of entry.resolvers) {
2058
+ resolve({
2059
+ status: 503,
2060
+ headers: { "content-type": "application/json" },
2061
+ body: errorBody,
2062
+ completedAt: Date.now()
2063
+ });
2064
+ }
2065
+ this.inflight.delete(key);
2066
+ }
2067
+ }
2068
+ /** Prune expired completed entries. */
2069
+ prune() {
2070
+ const now = Date.now();
2071
+ for (const [key, entry] of this.completed) {
2072
+ if (now - entry.completedAt > this.ttlMs) {
2073
+ this.completed.delete(key);
2074
+ }
2075
+ }
2076
+ }
2077
+ };
2078
+
2079
+ // src/response-cache.ts
2080
+ import { createHash as createHash2 } from "crypto";
2081
+ var DEFAULT_CONFIG = {
2082
+ maxSize: 200,
2083
+ defaultTTL: 600,
2084
+ maxItemSize: 1048576,
2085
+ // 1MB
2086
+ enabled: true
2087
+ };
2088
+ function canonicalize2(obj) {
2089
+ if (obj === null || typeof obj !== "object") {
2090
+ return obj;
2091
+ }
2092
+ if (Array.isArray(obj)) {
2093
+ return obj.map(canonicalize2);
2094
+ }
2095
+ const sorted = {};
2096
+ for (const key of Object.keys(obj).sort()) {
2097
+ sorted[key] = canonicalize2(obj[key]);
2098
+ }
2099
+ return sorted;
2100
+ }
2101
+ var TIMESTAMP_PATTERN2 = /^\[\w{3}\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}\s+\w+\]\s*/;
2102
+ function normalizeForCache(obj) {
2103
+ const result = {};
2104
+ for (const [key, value] of Object.entries(obj)) {
2105
+ if (["stream", "user", "request_id", "x-request-id"].includes(key)) {
2106
+ continue;
2107
+ }
2108
+ if (key === "messages" && Array.isArray(value)) {
2109
+ result[key] = value.map((msg) => {
2110
+ if (typeof msg === "object" && msg !== null) {
2111
+ const m = msg;
2112
+ if (typeof m.content === "string") {
2113
+ return { ...m, content: m.content.replace(TIMESTAMP_PATTERN2, "") };
2114
+ }
2115
+ }
2116
+ return msg;
2117
+ });
2118
+ } else {
2119
+ result[key] = value;
2120
+ }
2121
+ }
2122
+ return result;
2123
+ }
2124
+ var ResponseCache = class {
2125
+ cache = /* @__PURE__ */ new Map();
2126
+ expirationHeap = [];
2127
+ config;
2128
+ // Stats for monitoring
2129
+ stats = {
2130
+ hits: 0,
2131
+ misses: 0,
2132
+ evictions: 0
2133
+ };
2134
+ constructor(config = {}) {
2135
+ const filtered = Object.fromEntries(
2136
+ Object.entries(config).filter(([, v]) => v !== void 0)
2137
+ );
2138
+ this.config = { ...DEFAULT_CONFIG, ...filtered };
2139
+ }
2140
+ /**
2141
+ * Generate cache key from request body.
2142
+ * Hashes: model + messages + temperature + max_tokens + other params
2143
+ */
2144
+ static generateKey(body) {
2145
+ try {
2146
+ const parsed = JSON.parse(typeof body === "string" ? body : body.toString());
2147
+ const normalized = normalizeForCache(parsed);
2148
+ const canonical = canonicalize2(normalized);
2149
+ const keyContent = JSON.stringify(canonical);
2150
+ return createHash2("sha256").update(keyContent).digest("hex").slice(0, 32);
2151
+ } catch {
2152
+ const content = typeof body === "string" ? body : body.toString();
2153
+ return createHash2("sha256").update(content).digest("hex").slice(0, 32);
2154
+ }
2155
+ }
2156
+ /**
2157
+ * Check if caching is enabled for this request.
2158
+ * Respects cache control headers and request params.
2159
+ */
2160
+ shouldCache(body, headers) {
2161
+ if (!this.config.enabled) return false;
2162
+ if (headers?.["cache-control"]?.includes("no-cache")) {
2163
+ return false;
2164
+ }
2165
+ try {
2166
+ const parsed = JSON.parse(typeof body === "string" ? body : body.toString());
2167
+ if (parsed.cache === false || parsed.no_cache === true) {
2168
+ return false;
2169
+ }
2170
+ } catch {
2171
+ }
2172
+ return true;
2173
+ }
2174
+ /**
2175
+ * Get cached response if available and not expired.
2176
+ */
2177
+ get(key) {
2178
+ const entry = this.cache.get(key);
2179
+ if (!entry) {
2180
+ this.stats.misses++;
2181
+ return void 0;
2182
+ }
2183
+ if (Date.now() > entry.expiresAt) {
2184
+ this.cache.delete(key);
2185
+ this.stats.misses++;
2186
+ return void 0;
2187
+ }
2188
+ this.stats.hits++;
2189
+ return entry;
2190
+ }
2191
+ /**
2192
+ * Cache a response with optional custom TTL.
2193
+ */
2194
+ set(key, response, ttlSeconds) {
2195
+ if (!this.config.enabled || this.config.maxSize <= 0) return;
2196
+ if (response.body.length > this.config.maxItemSize) {
2197
+ console.log(`[ResponseCache] Skipping cache - item too large: ${response.body.length} bytes`);
2198
+ return;
2199
+ }
2200
+ if (response.status >= 400) {
2201
+ return;
2202
+ }
2203
+ if (this.cache.size >= this.config.maxSize) {
2204
+ this.evict();
2205
+ }
2206
+ const now = Date.now();
2207
+ const ttl = ttlSeconds ?? this.config.defaultTTL;
2208
+ const expiresAt = now + ttl * 1e3;
2209
+ const entry = {
2210
+ ...response,
2211
+ cachedAt: now,
2212
+ expiresAt
2213
+ };
2214
+ this.cache.set(key, entry);
2215
+ this.expirationHeap.push({ expiresAt, key });
2216
+ }
2217
+ /**
2218
+ * Evict expired and oldest entries to make room.
2219
+ */
2220
+ evict() {
2221
+ const now = Date.now();
2222
+ this.expirationHeap.sort((a, b) => a.expiresAt - b.expiresAt);
2223
+ while (this.expirationHeap.length > 0) {
2224
+ const oldest = this.expirationHeap[0];
2225
+ const entry = this.cache.get(oldest.key);
2226
+ if (!entry || entry.expiresAt !== oldest.expiresAt) {
2227
+ this.expirationHeap.shift();
2228
+ continue;
2229
+ }
2230
+ if (oldest.expiresAt <= now) {
2231
+ this.cache.delete(oldest.key);
2232
+ this.expirationHeap.shift();
2233
+ this.stats.evictions++;
2234
+ } else {
2235
+ break;
2236
+ }
2237
+ }
2238
+ while (this.cache.size >= this.config.maxSize && this.expirationHeap.length > 0) {
2239
+ const oldest = this.expirationHeap.shift();
2240
+ if (this.cache.has(oldest.key)) {
2241
+ this.cache.delete(oldest.key);
2242
+ this.stats.evictions++;
2243
+ }
2244
+ }
2245
+ }
2246
+ /**
2247
+ * Get cache statistics.
2248
+ */
2249
+ getStats() {
2250
+ const total = this.stats.hits + this.stats.misses;
2251
+ const hitRate = total > 0 ? (this.stats.hits / total * 100).toFixed(1) + "%" : "0%";
2252
+ return {
2253
+ size: this.cache.size,
2254
+ maxSize: this.config.maxSize,
2255
+ hits: this.stats.hits,
2256
+ misses: this.stats.misses,
2257
+ evictions: this.stats.evictions,
2258
+ hitRate
2259
+ };
2260
+ }
2261
+ /**
2262
+ * Clear all cached entries.
2263
+ */
2264
+ clear() {
2265
+ this.cache.clear();
2266
+ this.expirationHeap = [];
2267
+ }
2268
+ /**
2269
+ * Check if cache is enabled.
2270
+ */
2271
+ isEnabled() {
2272
+ return this.config.enabled;
2273
+ }
2274
+ };
2275
+
2276
+ // src/balance.ts
2277
+ import { createPublicClient, http, erc20Abi } from "viem";
2278
+ import { base } from "viem/chains";
2279
+
2280
+ // src/errors.ts
2281
+ var RpcError = class extends Error {
2282
+ code = "RPC_ERROR";
2283
+ originalError;
2284
+ constructor(message, originalError) {
2285
+ super(`RPC error: ${message}. Check network connectivity.`);
2286
+ this.name = "RpcError";
2287
+ this.originalError = originalError;
2288
+ }
2289
+ };
2290
+
2291
+ // src/balance.ts
2292
+ var USDC_BASE2 = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
2293
+ var CACHE_TTL_MS = 3e4;
2294
+ var BALANCE_THRESHOLDS = {
2295
+ /** Low balance warning threshold: $1.00 */
2296
+ LOW_BALANCE_MICROS: 1000000n,
2297
+ /** Effectively zero threshold: $0.0001 (covers dust/rounding) */
2298
+ ZERO_THRESHOLD: 100n
2299
+ };
2300
+ var BalanceMonitor = class {
2301
+ client;
2302
+ walletAddress;
2303
+ /** Cached balance (null = not yet fetched) */
2304
+ cachedBalance = null;
2305
+ /** Timestamp when cache was last updated */
2306
+ cachedAt = 0;
2307
+ constructor(walletAddress) {
2308
+ this.walletAddress = walletAddress;
2309
+ this.client = createPublicClient({
2310
+ chain: base,
2311
+ transport: http(void 0, {
2312
+ timeout: 1e4
2313
+ // 10 second timeout to prevent hanging on slow RPC
2314
+ })
2315
+ });
2316
+ }
2317
+ /**
2318
+ * Check current USDC balance.
2319
+ * Uses cache if valid, otherwise fetches from RPC.
2320
+ */
2321
+ async checkBalance() {
2322
+ const now = Date.now();
2323
+ if (this.cachedBalance !== null && now - this.cachedAt < CACHE_TTL_MS) {
2324
+ return this.buildInfo(this.cachedBalance);
2325
+ }
2326
+ const balance = await this.fetchBalance();
2327
+ this.cachedBalance = balance;
2328
+ this.cachedAt = now;
2329
+ return this.buildInfo(balance);
2330
+ }
2331
+ /**
2332
+ * Check if balance is sufficient for an estimated cost.
2333
+ *
2334
+ * @param estimatedCostMicros - Estimated cost in USDC smallest unit (6 decimals)
2335
+ */
2336
+ async checkSufficient(estimatedCostMicros) {
2337
+ const info = await this.checkBalance();
2338
+ if (info.balance >= estimatedCostMicros) {
2339
+ return { sufficient: true, info };
2340
+ }
2341
+ const shortfall = estimatedCostMicros - info.balance;
2342
+ return {
2343
+ sufficient: false,
2344
+ info,
2345
+ shortfall: this.formatUSDC(shortfall)
2346
+ };
2347
+ }
2348
+ /**
2349
+ * Optimistically deduct estimated cost from cached balance.
2350
+ * Call this after a successful payment to keep cache accurate.
2351
+ *
2352
+ * @param amountMicros - Amount to deduct in USDC smallest unit
2353
+ */
2354
+ deductEstimated(amountMicros) {
2355
+ if (this.cachedBalance !== null && this.cachedBalance >= amountMicros) {
2356
+ this.cachedBalance -= amountMicros;
2357
+ }
2358
+ }
2359
+ /**
2360
+ * Invalidate cache, forcing next checkBalance() to fetch from RPC.
2361
+ * Call this after a payment failure to get accurate balance.
2362
+ */
2363
+ invalidate() {
2364
+ this.cachedBalance = null;
2365
+ this.cachedAt = 0;
2366
+ }
2367
+ /**
2368
+ * Force refresh balance from RPC (ignores cache).
2369
+ */
2370
+ async refresh() {
2371
+ this.invalidate();
2372
+ return this.checkBalance();
2373
+ }
2374
+ /**
2375
+ * Format USDC amount (in micros) as "$X.XX".
2376
+ */
2377
+ formatUSDC(amountMicros) {
2378
+ const dollars = Number(amountMicros) / 1e6;
2379
+ return `$${dollars.toFixed(2)}`;
2380
+ }
2381
+ /**
2382
+ * Get the wallet address being monitored.
2383
+ */
2384
+ getWalletAddress() {
2385
+ return this.walletAddress;
2386
+ }
2387
+ /** Fetch balance from RPC */
2388
+ async fetchBalance() {
2389
+ try {
2390
+ const balance = await this.client.readContract({
2391
+ address: USDC_BASE2,
2392
+ abi: erc20Abi,
2393
+ functionName: "balanceOf",
2394
+ args: [this.walletAddress]
2395
+ });
2396
+ return balance;
2397
+ } catch (error) {
2398
+ throw new RpcError(error instanceof Error ? error.message : "Unknown error", error);
2399
+ }
2400
+ }
2401
+ /** Build BalanceInfo from raw balance */
2402
+ buildInfo(balance) {
2403
+ return {
2404
+ balance,
2405
+ balanceUSD: this.formatUSDC(balance),
2406
+ isLow: balance < BALANCE_THRESHOLDS.LOW_BALANCE_MICROS,
2407
+ isEmpty: balance < BALANCE_THRESHOLDS.ZERO_THRESHOLD,
2408
+ walletAddress: this.walletAddress
2409
+ };
2410
+ }
2411
+ };
2412
+
2413
+ // src/compression/types.ts
2414
+ var DEFAULT_COMPRESSION_CONFIG = {
2415
+ enabled: true,
2416
+ preserveRaw: true,
2417
+ layers: {
2418
+ deduplication: true,
2419
+ // Safe: removes duplicate messages
2420
+ whitespace: true,
2421
+ // Safe: normalizes whitespace
2422
+ dictionary: false,
2423
+ // DISABLED: requires model to understand codebook
2424
+ paths: false,
2425
+ // DISABLED: requires model to understand path codes
2426
+ jsonCompact: true,
2427
+ // Safe: just removes JSON whitespace
2428
+ observation: false,
2429
+ // DISABLED: may lose important context
2430
+ dynamicCodebook: false
2431
+ // DISABLED: requires model to understand codes
2432
+ },
2433
+ dictionary: {
2434
+ maxEntries: 50,
2435
+ minPhraseLength: 15,
2436
+ includeCodebookHeader: false
2437
+ // No codebook header needed
2438
+ }
2439
+ };
2440
+
2441
+ // src/compression/layers/deduplication.ts
2442
+ import crypto2 from "crypto";
2443
+ function hashMessage(message) {
2444
+ let contentStr = "";
2445
+ if (typeof message.content === "string") {
2446
+ contentStr = message.content;
2447
+ } else if (Array.isArray(message.content)) {
2448
+ contentStr = JSON.stringify(message.content);
2449
+ }
2450
+ const parts = [message.role, contentStr, message.tool_call_id || "", message.name || ""];
2451
+ if (message.tool_calls) {
2452
+ parts.push(
2453
+ JSON.stringify(
2454
+ message.tool_calls.map((tc) => ({
2455
+ name: tc.function.name,
2456
+ args: tc.function.arguments
2457
+ }))
2458
+ )
2459
+ );
2460
+ }
2461
+ const content = parts.join("|");
2462
+ return crypto2.createHash("md5").update(content).digest("hex");
2463
+ }
2464
+ function deduplicateMessages(messages) {
2465
+ const seen = /* @__PURE__ */ new Set();
2466
+ const result = [];
2467
+ let duplicatesRemoved = 0;
2468
+ const referencedToolCallIds = /* @__PURE__ */ new Set();
2469
+ for (const message of messages) {
2470
+ if (message.role === "tool" && message.tool_call_id) {
2471
+ referencedToolCallIds.add(message.tool_call_id);
2472
+ }
2473
+ }
2474
+ for (const message of messages) {
2475
+ if (message.role === "system") {
2476
+ result.push(message);
2477
+ continue;
2478
+ }
2479
+ if (message.role === "user") {
2480
+ result.push(message);
2481
+ continue;
2482
+ }
2483
+ if (message.role === "tool") {
2484
+ result.push(message);
2485
+ continue;
2486
+ }
2487
+ if (message.role === "assistant" && message.tool_calls) {
2488
+ const hasReferencedToolCall = message.tool_calls.some(
2489
+ (tc) => referencedToolCallIds.has(tc.id)
2490
+ );
2491
+ if (hasReferencedToolCall) {
2492
+ result.push(message);
2493
+ continue;
2494
+ }
2495
+ }
2496
+ const hash = hashMessage(message);
2497
+ if (!seen.has(hash)) {
2498
+ seen.add(hash);
2499
+ result.push(message);
2500
+ } else {
2501
+ duplicatesRemoved++;
2502
+ }
2503
+ }
2504
+ return {
2505
+ messages: result,
2506
+ duplicatesRemoved,
2507
+ originalCount: messages.length
2508
+ };
2509
+ }
2510
+
2511
+ // src/compression/layers/whitespace.ts
2512
+ function normalizeWhitespace(content) {
2513
+ if (!content || typeof content !== "string") return content;
2514
+ return content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n{3,}/g, "\n\n").replace(/[ \t]+$/gm, "").replace(/([^\n]) {2,}/g, "$1 ").replace(/^[ ]{8,}/gm, (match) => " ".repeat(Math.ceil(match.length / 4))).replace(/\t/g, " ").trim();
2515
+ }
2516
+ function normalizeMessagesWhitespace(messages) {
2517
+ let charsSaved = 0;
2518
+ const result = messages.map((message) => {
2519
+ if (!message.content || typeof message.content !== "string") return message;
2520
+ const originalLength = message.content.length;
2521
+ const normalizedContent = normalizeWhitespace(message.content);
2522
+ charsSaved += originalLength - normalizedContent.length;
2523
+ return {
2524
+ ...message,
2525
+ content: normalizedContent
2526
+ };
2527
+ });
2528
+ return {
2529
+ messages: result,
2530
+ charsSaved
2531
+ };
2532
+ }
2533
+
2534
+ // src/compression/codebook.ts
2535
+ var STATIC_CODEBOOK = {
2536
+ // High-impact: OpenClaw/Agent system prompt patterns (very common)
2537
+ $OC01: "unbrowse_",
2538
+ // Common prefix in tool names
2539
+ $OC02: "<location>",
2540
+ $OC03: "</location>",
2541
+ $OC04: "<name>",
2542
+ $OC05: "</name>",
2543
+ $OC06: "<description>",
2544
+ $OC07: "</description>",
2545
+ $OC08: "(may need login)",
2546
+ $OC09: "API skill for OpenClaw",
2547
+ $OC10: "endpoints",
2548
+ // Skill/tool markers
2549
+ $SK01: "<available_skills>",
2550
+ $SK02: "</available_skills>",
2551
+ $SK03: "<skill>",
2552
+ $SK04: "</skill>",
2553
+ // Schema patterns (very common in tool definitions)
2554
+ $T01: 'type: "function"',
2555
+ $T02: '"type": "function"',
2556
+ $T03: '"type": "string"',
2557
+ $T04: '"type": "object"',
2558
+ $T05: '"type": "array"',
2559
+ $T06: '"type": "boolean"',
2560
+ $T07: '"type": "number"',
2561
+ // Common descriptions
2562
+ $D01: "description:",
2563
+ $D02: '"description":',
2564
+ // Common instructions
2565
+ $I01: "You are a personal assistant",
2566
+ $I02: "Tool names are case-sensitive",
2567
+ $I03: "Call tools exactly as listed",
2568
+ $I04: "Use when",
2569
+ $I05: "without asking",
2570
+ // Safety phrases
2571
+ $S01: "Do not manipulate or persuade",
2572
+ $S02: "Prioritize safety and human oversight",
2573
+ $S03: "unless explicitly requested",
2574
+ // JSON patterns
2575
+ $J01: '"required": ["',
2576
+ $J02: '"properties": {',
2577
+ $J03: '"additionalProperties": false',
2578
+ // Heartbeat patterns
2579
+ $H01: "HEARTBEAT_OK",
2580
+ $H02: "Read HEARTBEAT.md if it exists",
2581
+ // Role markers
2582
+ $R01: '"role": "system"',
2583
+ $R02: '"role": "user"',
2584
+ $R03: '"role": "assistant"',
2585
+ $R04: '"role": "tool"',
2586
+ // Common endings/phrases
2587
+ $E01: "would you like to",
2588
+ $E02: "Let me know if you",
2589
+ $E03: "internal APIs",
2590
+ $E04: "session cookies",
2591
+ // BlockRun model aliases (common in prompts)
2592
+ $M01: "blockrun/",
2593
+ $M02: "openai/",
2594
+ $M03: "anthropic/",
2595
+ $M04: "google/",
2596
+ $M05: "xai/"
2597
+ };
2598
+ function getInverseCodebook() {
2599
+ const inverse = {};
2600
+ for (const [code, phrase] of Object.entries(STATIC_CODEBOOK)) {
2601
+ inverse[phrase] = code;
2602
+ }
2603
+ return inverse;
2604
+ }
2605
+ function generateCodebookHeader(usedCodes, pathMap = {}) {
2606
+ if (usedCodes.size === 0 && Object.keys(pathMap).length === 0) {
2607
+ return "";
2608
+ }
2609
+ const parts = [];
2610
+ if (usedCodes.size > 0) {
2611
+ const codeEntries = Array.from(usedCodes).map((code) => `${code}=${STATIC_CODEBOOK[code]}`).join(", ");
2612
+ parts.push(`[Dict: ${codeEntries}]`);
2613
+ }
2614
+ if (Object.keys(pathMap).length > 0) {
2615
+ const pathEntries = Object.entries(pathMap).map(([code, path]) => `${code}=${path}`).join(", ");
2616
+ parts.push(`[Paths: ${pathEntries}]`);
2617
+ }
2618
+ return parts.join("\n");
2619
+ }
2620
+
2621
+ // src/compression/layers/dictionary.ts
2622
+ function encodeContent(content, inverseCodebook) {
2623
+ if (!content || typeof content !== "string") {
2624
+ return { encoded: content, substitutions: 0, codes: /* @__PURE__ */ new Set(), charsSaved: 0 };
2625
+ }
2626
+ let encoded = content;
2627
+ let substitutions = 0;
2628
+ let charsSaved = 0;
2629
+ const codes = /* @__PURE__ */ new Set();
2630
+ const phrases = Object.keys(inverseCodebook).sort((a, b) => b.length - a.length);
2631
+ for (const phrase of phrases) {
2632
+ const code = inverseCodebook[phrase];
2633
+ const regex = new RegExp(escapeRegex(phrase), "g");
2634
+ const matches = encoded.match(regex);
2635
+ if (matches && matches.length > 0) {
2636
+ encoded = encoded.replace(regex, code);
2637
+ substitutions += matches.length;
2638
+ charsSaved += matches.length * (phrase.length - code.length);
2639
+ codes.add(code);
2640
+ }
2641
+ }
2642
+ return { encoded, substitutions, codes, charsSaved };
2643
+ }
2644
+ function escapeRegex(str) {
2645
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2646
+ }
2647
+ function encodeMessages(messages) {
2648
+ const inverseCodebook = getInverseCodebook();
2649
+ let totalSubstitutions = 0;
2650
+ let totalCharsSaved = 0;
2651
+ const allUsedCodes = /* @__PURE__ */ new Set();
2652
+ const result = messages.map((message) => {
2653
+ if (!message.content || typeof message.content !== "string") return message;
2654
+ const { encoded, substitutions, codes, charsSaved } = encodeContent(
2655
+ message.content,
2656
+ inverseCodebook
2657
+ );
2658
+ totalSubstitutions += substitutions;
2659
+ totalCharsSaved += charsSaved;
2660
+ codes.forEach((code) => allUsedCodes.add(code));
2661
+ return {
2662
+ ...message,
2663
+ content: encoded
2664
+ };
2665
+ });
2666
+ return {
2667
+ messages: result,
2668
+ substitutionCount: totalSubstitutions,
2669
+ usedCodes: allUsedCodes,
2670
+ charsSaved: totalCharsSaved
2671
+ };
2672
+ }
2673
+
2674
+ // src/compression/layers/paths.ts
2675
+ var PATH_REGEX = /(?:\/[\w.-]+){3,}/g;
2676
+ function extractPaths(messages) {
2677
+ const paths = [];
2678
+ for (const message of messages) {
2679
+ if (!message.content || typeof message.content !== "string") continue;
2680
+ const matches = message.content.match(PATH_REGEX);
2681
+ if (matches) {
2682
+ paths.push(...matches);
2683
+ }
2684
+ }
2685
+ return paths;
2686
+ }
2687
+ function findFrequentPrefixes(paths) {
2688
+ const prefixCounts = /* @__PURE__ */ new Map();
2689
+ for (const path of paths) {
2690
+ const parts = path.split("/").filter(Boolean);
2691
+ for (let i = 2; i < parts.length; i++) {
2692
+ const prefix = "/" + parts.slice(0, i).join("/") + "/";
2693
+ prefixCounts.set(prefix, (prefixCounts.get(prefix) || 0) + 1);
2694
+ }
2695
+ }
2696
+ return Array.from(prefixCounts.entries()).filter(([, count]) => count >= 3).sort((a, b) => b[0].length - a[0].length).slice(0, 5).map(([prefix]) => prefix);
2697
+ }
2698
+ function shortenPaths(messages) {
2699
+ const allPaths = extractPaths(messages);
2700
+ if (allPaths.length < 5) {
2701
+ return {
2702
+ messages,
2703
+ pathMap: {},
2704
+ charsSaved: 0
2705
+ };
2706
+ }
2707
+ const prefixes = findFrequentPrefixes(allPaths);
2708
+ if (prefixes.length === 0) {
2709
+ return {
2710
+ messages,
2711
+ pathMap: {},
2712
+ charsSaved: 0
2713
+ };
2714
+ }
2715
+ const pathMap = {};
2716
+ prefixes.forEach((prefix, i) => {
2717
+ pathMap[`$P${i + 1}`] = prefix;
2718
+ });
2719
+ let charsSaved = 0;
2720
+ const result = messages.map((message) => {
2721
+ if (!message.content || typeof message.content !== "string") return message;
2722
+ let content = message.content;
2723
+ const originalLength = content.length;
2724
+ for (const [code, prefix] of Object.entries(pathMap)) {
2725
+ content = content.split(prefix).join(code + "/");
2726
+ }
2727
+ charsSaved += originalLength - content.length;
2728
+ return {
2729
+ ...message,
2730
+ content
2731
+ };
2732
+ });
2733
+ return {
2734
+ messages: result,
2735
+ pathMap,
2736
+ charsSaved
2737
+ };
2738
+ }
2739
+
2740
+ // src/compression/layers/json-compact.ts
2741
+ function compactJson(jsonString) {
2742
+ try {
2743
+ const parsed = JSON.parse(jsonString);
2744
+ return JSON.stringify(parsed);
2745
+ } catch {
2746
+ return jsonString;
2747
+ }
2748
+ }
2749
+ function looksLikeJson(str) {
2750
+ const trimmed = str.trim();
2751
+ return trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]");
2752
+ }
2753
+ function compactToolCalls(toolCalls) {
2754
+ return toolCalls.map((tc) => ({
2755
+ ...tc,
2756
+ function: {
2757
+ ...tc.function,
2758
+ arguments: compactJson(tc.function.arguments)
2759
+ }
2760
+ }));
2761
+ }
2762
+ function compactMessagesJson(messages) {
2763
+ let charsSaved = 0;
2764
+ const result = messages.map((message) => {
2765
+ const newMessage = { ...message };
2766
+ if (message.tool_calls && message.tool_calls.length > 0) {
2767
+ const originalLength = JSON.stringify(message.tool_calls).length;
2768
+ newMessage.tool_calls = compactToolCalls(message.tool_calls);
2769
+ const newLength = JSON.stringify(newMessage.tool_calls).length;
2770
+ charsSaved += originalLength - newLength;
2771
+ }
2772
+ if (message.role === "tool" && message.content && typeof message.content === "string" && looksLikeJson(message.content)) {
2773
+ const originalLength = message.content.length;
2774
+ const compacted = compactJson(message.content);
2775
+ charsSaved += originalLength - compacted.length;
2776
+ newMessage.content = compacted;
2777
+ }
2778
+ return newMessage;
2779
+ });
2780
+ return {
2781
+ messages: result,
2782
+ charsSaved
2783
+ };
2784
+ }
2785
+
2786
+ // src/compression/layers/observation.ts
2787
+ var TOOL_RESULT_THRESHOLD = 500;
2788
+ var COMPRESSED_RESULT_MAX = 300;
2789
+ function compressToolResult(content) {
2790
+ if (!content || content.length <= TOOL_RESULT_THRESHOLD) {
2791
+ return content;
2792
+ }
2793
+ const lines = content.split("\n").map((l) => l.trim()).filter(Boolean);
2794
+ const errorLines = lines.filter(
2795
+ (l) => /error|exception|failed|denied|refused|timeout|invalid/i.test(l) && l.length < 200
2796
+ );
2797
+ const statusLines = lines.filter(
2798
+ (l) => /success|complete|created|updated|found|result|status|total|count/i.test(l) && l.length < 150
2799
+ );
2800
+ const jsonMatches = [];
2801
+ const jsonPattern = /"(id|name|status|error|message|count|total|url|path)":\s*"?([^",}\n]+)"?/gi;
2802
+ let match;
2803
+ while ((match = jsonPattern.exec(content)) !== null) {
2804
+ jsonMatches.push(`${match[1]}: ${match[2].slice(0, 50)}`);
2805
+ }
2806
+ const firstLine = lines[0]?.slice(0, 100);
2807
+ const lastLine = lines.length > 1 ? lines[lines.length - 1]?.slice(0, 100) : "";
2808
+ const parts = [];
2809
+ if (errorLines.length > 0) {
2810
+ parts.push("[ERR] " + errorLines.slice(0, 3).join(" | "));
2811
+ }
2812
+ if (statusLines.length > 0) {
2813
+ parts.push(statusLines.slice(0, 3).join(" | "));
2814
+ }
2815
+ if (jsonMatches.length > 0) {
2816
+ parts.push(jsonMatches.slice(0, 5).join(", "));
2817
+ }
2818
+ if (parts.length === 0) {
2819
+ parts.push(firstLine || "");
2820
+ if (lines.length > 2) {
2821
+ parts.push(`[...${lines.length - 2} lines...]`);
2822
+ }
2823
+ if (lastLine && lastLine !== firstLine) {
2824
+ parts.push(lastLine);
2825
+ }
2826
+ }
2827
+ let result = parts.join("\n");
2828
+ if (result.length > COMPRESSED_RESULT_MAX) {
2829
+ result = result.slice(0, COMPRESSED_RESULT_MAX - 20) + "\n[...truncated]";
2830
+ }
2831
+ return result;
2832
+ }
2833
+ function deduplicateLargeBlocks(messages) {
2834
+ const blockHashes = /* @__PURE__ */ new Map();
2835
+ let charsSaved = 0;
2836
+ const result = messages.map((msg, idx) => {
2837
+ if (!msg.content || typeof msg.content !== "string" || msg.content.length < 500) {
2838
+ return msg;
2839
+ }
2840
+ const blockKey = msg.content.slice(0, 200);
2841
+ if (blockHashes.has(blockKey)) {
2842
+ const firstIdx = blockHashes.get(blockKey);
2843
+ const original = msg.content;
2844
+ const compressed = `[See message #${firstIdx + 1} - same content]`;
2845
+ charsSaved += original.length - compressed.length;
2846
+ return { ...msg, content: compressed };
2847
+ }
2848
+ blockHashes.set(blockKey, idx);
2849
+ return msg;
2850
+ });
2851
+ return { messages: result, charsSaved };
2852
+ }
2853
+ function compressObservations(messages) {
2854
+ let charsSaved = 0;
2855
+ let observationsCompressed = 0;
2856
+ let result = messages.map((msg) => {
2857
+ if (msg.role !== "tool" || !msg.content || typeof msg.content !== "string") {
2858
+ return msg;
2859
+ }
2860
+ const original = msg.content;
2861
+ if (original.length <= TOOL_RESULT_THRESHOLD) {
2862
+ return msg;
2863
+ }
2864
+ const compressed = compressToolResult(original);
2865
+ const saved = original.length - compressed.length;
2866
+ if (saved > 50) {
2867
+ charsSaved += saved;
2868
+ observationsCompressed++;
2869
+ return { ...msg, content: compressed };
2870
+ }
2871
+ return msg;
2872
+ });
2873
+ const dedupResult = deduplicateLargeBlocks(result);
2874
+ result = dedupResult.messages;
2875
+ charsSaved += dedupResult.charsSaved;
2876
+ return {
2877
+ messages: result,
2878
+ charsSaved,
2879
+ observationsCompressed
2880
+ };
2881
+ }
2882
+
2883
+ // src/compression/layers/dynamic-codebook.ts
2884
+ var MIN_PHRASE_LENGTH = 20;
2885
+ var MAX_PHRASE_LENGTH = 200;
2886
+ var MIN_FREQUENCY = 3;
2887
+ var MAX_ENTRIES = 100;
2888
+ var CODE_PREFIX = "$D";
2889
+ function findRepeatedPhrases(allContent) {
2890
+ const phrases = /* @__PURE__ */ new Map();
2891
+ const segments = allContent.split(/(?<=[.!?\n])\s+/);
2892
+ for (const segment of segments) {
2893
+ const trimmed = segment.trim();
2894
+ if (trimmed.length >= MIN_PHRASE_LENGTH && trimmed.length <= MAX_PHRASE_LENGTH) {
2895
+ phrases.set(trimmed, (phrases.get(trimmed) || 0) + 1);
2896
+ }
2897
+ }
2898
+ const lines = allContent.split("\n");
2899
+ for (const line of lines) {
2900
+ const trimmed = line.trim();
2901
+ if (trimmed.length >= MIN_PHRASE_LENGTH && trimmed.length <= MAX_PHRASE_LENGTH) {
2902
+ phrases.set(trimmed, (phrases.get(trimmed) || 0) + 1);
2903
+ }
2904
+ }
2905
+ return phrases;
2906
+ }
2907
+ function buildDynamicCodebook(messages) {
2908
+ let allContent = "";
2909
+ for (const msg of messages) {
2910
+ if (msg.content && typeof msg.content === "string") {
2911
+ allContent += msg.content + "\n";
2912
+ }
2913
+ }
2914
+ const phrases = findRepeatedPhrases(allContent);
2915
+ const candidates = [];
2916
+ for (const [phrase, count] of phrases.entries()) {
2917
+ if (count >= MIN_FREQUENCY) {
2918
+ const codeLength = 4;
2919
+ const savings = (phrase.length - codeLength) * count;
2920
+ if (savings > 50) {
2921
+ candidates.push({ phrase, count, savings });
2922
+ }
2923
+ }
2924
+ }
2925
+ candidates.sort((a, b) => b.savings - a.savings);
2926
+ const topCandidates = candidates.slice(0, MAX_ENTRIES);
2927
+ const codebook = {};
2928
+ topCandidates.forEach((c, i) => {
2929
+ const code = `${CODE_PREFIX}${String(i + 1).padStart(2, "0")}`;
2930
+ codebook[code] = c.phrase;
2931
+ });
2932
+ return codebook;
2933
+ }
2934
+ function escapeRegex2(str) {
2935
+ if (!str || typeof str !== "string") return "";
2936
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2937
+ }
2938
+ function applyDynamicCodebook(messages) {
2939
+ const codebook = buildDynamicCodebook(messages);
2940
+ if (Object.keys(codebook).length === 0) {
2941
+ return {
2942
+ messages,
2943
+ charsSaved: 0,
2944
+ dynamicCodes: {},
2945
+ substitutions: 0
2946
+ };
2947
+ }
2948
+ const phraseToCode = {};
2949
+ for (const [code, phrase] of Object.entries(codebook)) {
2950
+ phraseToCode[phrase] = code;
2951
+ }
2952
+ const sortedPhrases = Object.keys(phraseToCode).sort((a, b) => b.length - a.length);
2953
+ let charsSaved = 0;
2954
+ let substitutions = 0;
2955
+ const result = messages.map((msg) => {
2956
+ if (!msg.content || typeof msg.content !== "string") return msg;
2957
+ let content = msg.content;
2958
+ for (const phrase of sortedPhrases) {
2959
+ const code = phraseToCode[phrase];
2960
+ const regex = new RegExp(escapeRegex2(phrase), "g");
2961
+ const matches = content.match(regex);
2962
+ if (matches) {
2963
+ content = content.replace(regex, code);
2964
+ charsSaved += (phrase.length - code.length) * matches.length;
2965
+ substitutions += matches.length;
2966
+ }
2967
+ }
2968
+ return { ...msg, content };
2969
+ });
2970
+ return {
2971
+ messages: result,
2972
+ charsSaved,
2973
+ dynamicCodes: codebook,
2974
+ substitutions
2975
+ };
2976
+ }
2977
+ function generateDynamicCodebookHeader(codebook) {
2978
+ if (Object.keys(codebook).length === 0) return "";
2979
+ const entries = Object.entries(codebook).slice(0, 20).map(([code, phrase]) => {
2980
+ const displayPhrase = phrase.length > 40 ? phrase.slice(0, 37) + "..." : phrase;
2981
+ return `${code}=${displayPhrase}`;
2982
+ }).join(", ");
2983
+ return `[DynDict: ${entries}]`;
2984
+ }
2985
+
2986
+ // src/compression/index.ts
2987
+ function calculateTotalChars(messages) {
2988
+ return messages.reduce((total, msg) => {
2989
+ let chars = 0;
2990
+ if (typeof msg.content === "string") {
2991
+ chars = msg.content.length;
2992
+ } else if (Array.isArray(msg.content)) {
2993
+ chars = JSON.stringify(msg.content).length;
2994
+ }
2995
+ if (msg.tool_calls) {
2996
+ chars += JSON.stringify(msg.tool_calls).length;
2997
+ }
2998
+ return total + chars;
2999
+ }, 0);
3000
+ }
3001
+ function cloneMessages(messages) {
3002
+ return JSON.parse(JSON.stringify(messages));
3003
+ }
3004
+ function prependCodebookHeader(messages, usedCodes, pathMap) {
3005
+ const header = generateCodebookHeader(usedCodes, pathMap);
3006
+ if (!header) return messages;
3007
+ const userIndex = messages.findIndex((m) => m.role === "user");
3008
+ if (userIndex === -1) {
3009
+ return [{ role: "system", content: header }, ...messages];
3010
+ }
3011
+ return messages.map((msg, i) => {
3012
+ if (i === userIndex) {
3013
+ if (typeof msg.content === "string") {
3014
+ return {
3015
+ ...msg,
3016
+ content: `${header}
3017
+
3018
+ ${msg.content}`
3019
+ };
3020
+ }
3021
+ }
3022
+ return msg;
3023
+ });
3024
+ }
3025
+ async function compressContext(messages, config = {}) {
3026
+ const fullConfig = {
3027
+ ...DEFAULT_COMPRESSION_CONFIG,
3028
+ ...config,
3029
+ layers: {
3030
+ ...DEFAULT_COMPRESSION_CONFIG.layers,
3031
+ ...config.layers
3032
+ },
3033
+ dictionary: {
3034
+ ...DEFAULT_COMPRESSION_CONFIG.dictionary,
3035
+ ...config.dictionary
3036
+ }
3037
+ };
3038
+ if (!fullConfig.enabled) {
3039
+ const originalChars2 = calculateTotalChars(messages);
3040
+ return {
3041
+ messages,
3042
+ originalMessages: messages,
3043
+ originalChars: originalChars2,
3044
+ compressedChars: originalChars2,
3045
+ compressionRatio: 1,
3046
+ stats: {
3047
+ duplicatesRemoved: 0,
3048
+ whitespaceSavedChars: 0,
3049
+ dictionarySubstitutions: 0,
3050
+ pathsShortened: 0,
3051
+ jsonCompactedChars: 0,
3052
+ observationsCompressed: 0,
3053
+ observationCharsSaved: 0,
3054
+ dynamicSubstitutions: 0,
3055
+ dynamicCharsSaved: 0
3056
+ },
3057
+ codebook: {},
3058
+ pathMap: {},
3059
+ dynamicCodes: {}
3060
+ };
3061
+ }
3062
+ const originalMessages = fullConfig.preserveRaw ? cloneMessages(messages) : messages;
3063
+ const originalChars = calculateTotalChars(messages);
3064
+ const stats = {
3065
+ duplicatesRemoved: 0,
3066
+ whitespaceSavedChars: 0,
3067
+ dictionarySubstitutions: 0,
3068
+ pathsShortened: 0,
3069
+ jsonCompactedChars: 0,
3070
+ observationsCompressed: 0,
3071
+ observationCharsSaved: 0,
3072
+ dynamicSubstitutions: 0,
3073
+ dynamicCharsSaved: 0
3074
+ };
3075
+ let result = cloneMessages(messages);
3076
+ let usedCodes = /* @__PURE__ */ new Set();
3077
+ let pathMap = {};
3078
+ let dynamicCodes = {};
3079
+ if (fullConfig.layers.deduplication) {
3080
+ const dedupResult = deduplicateMessages(result);
3081
+ result = dedupResult.messages;
3082
+ stats.duplicatesRemoved = dedupResult.duplicatesRemoved;
3083
+ }
3084
+ if (fullConfig.layers.whitespace) {
3085
+ const wsResult = normalizeMessagesWhitespace(result);
3086
+ result = wsResult.messages;
3087
+ stats.whitespaceSavedChars = wsResult.charsSaved;
3088
+ }
3089
+ if (fullConfig.layers.dictionary) {
3090
+ const dictResult = encodeMessages(result);
3091
+ result = dictResult.messages;
3092
+ stats.dictionarySubstitutions = dictResult.substitutionCount;
3093
+ usedCodes = dictResult.usedCodes;
3094
+ }
3095
+ if (fullConfig.layers.paths) {
3096
+ const pathResult = shortenPaths(result);
3097
+ result = pathResult.messages;
3098
+ pathMap = pathResult.pathMap;
3099
+ stats.pathsShortened = Object.keys(pathMap).length;
3100
+ }
3101
+ if (fullConfig.layers.jsonCompact) {
3102
+ const jsonResult = compactMessagesJson(result);
3103
+ result = jsonResult.messages;
3104
+ stats.jsonCompactedChars = jsonResult.charsSaved;
3105
+ }
3106
+ if (fullConfig.layers.observation) {
3107
+ const obsResult = compressObservations(result);
3108
+ result = obsResult.messages;
3109
+ stats.observationsCompressed = obsResult.observationsCompressed;
3110
+ stats.observationCharsSaved = obsResult.charsSaved;
3111
+ }
3112
+ if (fullConfig.layers.dynamicCodebook) {
3113
+ const dynResult = applyDynamicCodebook(result);
3114
+ result = dynResult.messages;
3115
+ stats.dynamicSubstitutions = dynResult.substitutions;
3116
+ stats.dynamicCharsSaved = dynResult.charsSaved;
3117
+ dynamicCodes = dynResult.dynamicCodes;
3118
+ }
3119
+ if (fullConfig.dictionary.includeCodebookHeader && (usedCodes.size > 0 || Object.keys(pathMap).length > 0 || Object.keys(dynamicCodes).length > 0)) {
3120
+ result = prependCodebookHeader(result, usedCodes, pathMap);
3121
+ if (Object.keys(dynamicCodes).length > 0) {
3122
+ const dynHeader = generateDynamicCodebookHeader(dynamicCodes);
3123
+ if (dynHeader) {
3124
+ const systemIndex = result.findIndex((m) => m.role === "system");
3125
+ if (systemIndex >= 0 && typeof result[systemIndex].content === "string") {
3126
+ result[systemIndex] = {
3127
+ ...result[systemIndex],
3128
+ content: `${dynHeader}
3129
+ ${result[systemIndex].content}`
3130
+ };
3131
+ }
3132
+ }
3133
+ }
3134
+ }
3135
+ const compressedChars = calculateTotalChars(result);
3136
+ const compressionRatio = compressedChars / originalChars;
3137
+ const usedCodebook = {};
3138
+ usedCodes.forEach((code) => {
3139
+ usedCodebook[code] = STATIC_CODEBOOK[code];
3140
+ });
3141
+ return {
3142
+ messages: result,
3143
+ originalMessages,
3144
+ originalChars,
3145
+ compressedChars,
3146
+ compressionRatio,
3147
+ stats,
3148
+ codebook: usedCodebook,
3149
+ pathMap,
3150
+ dynamicCodes
3151
+ };
3152
+ }
3153
+ function shouldCompress(messages) {
3154
+ const chars = calculateTotalChars(messages);
3155
+ return chars > 5e3;
3156
+ }
3157
+
3158
+ // src/session.ts
3159
+ var DEFAULT_SESSION_CONFIG = {
3160
+ enabled: false,
3161
+ timeoutMs: 30 * 60 * 1e3,
3162
+ // 30 minutes
3163
+ headerName: "x-session-id"
3164
+ };
3165
+ var SessionStore = class {
3166
+ sessions = /* @__PURE__ */ new Map();
3167
+ config;
3168
+ cleanupInterval = null;
3169
+ constructor(config = {}) {
3170
+ this.config = { ...DEFAULT_SESSION_CONFIG, ...config };
3171
+ if (this.config.enabled) {
3172
+ this.cleanupInterval = setInterval(() => this.cleanup(), 5 * 60 * 1e3);
3173
+ }
3174
+ }
3175
+ /**
3176
+ * Get the pinned model for a session, if any.
3177
+ */
3178
+ getSession(sessionId) {
3179
+ if (!this.config.enabled || !sessionId) {
3180
+ return void 0;
3181
+ }
3182
+ const entry = this.sessions.get(sessionId);
3183
+ if (!entry) {
3184
+ return void 0;
3185
+ }
3186
+ const now = Date.now();
3187
+ if (now - entry.lastUsedAt > this.config.timeoutMs) {
3188
+ this.sessions.delete(sessionId);
3189
+ return void 0;
3190
+ }
3191
+ return entry;
3192
+ }
3193
+ /**
3194
+ * Pin a model to a session.
3195
+ */
3196
+ setSession(sessionId, model, tier) {
3197
+ if (!this.config.enabled || !sessionId) {
3198
+ return;
3199
+ }
3200
+ const existing = this.sessions.get(sessionId);
3201
+ const now = Date.now();
3202
+ if (existing) {
3203
+ existing.lastUsedAt = now;
3204
+ existing.requestCount++;
3205
+ if (existing.model !== model) {
3206
+ existing.model = model;
3207
+ existing.tier = tier;
3208
+ }
3209
+ } else {
3210
+ this.sessions.set(sessionId, {
3211
+ model,
3212
+ tier,
3213
+ createdAt: now,
3214
+ lastUsedAt: now,
3215
+ requestCount: 1
3216
+ });
3217
+ }
3218
+ }
3219
+ /**
3220
+ * Touch a session to extend its timeout.
3221
+ */
3222
+ touchSession(sessionId) {
3223
+ if (!this.config.enabled || !sessionId) {
3224
+ return;
3225
+ }
3226
+ const entry = this.sessions.get(sessionId);
3227
+ if (entry) {
3228
+ entry.lastUsedAt = Date.now();
3229
+ entry.requestCount++;
3230
+ }
3231
+ }
3232
+ /**
3233
+ * Clear a specific session.
3234
+ */
3235
+ clearSession(sessionId) {
3236
+ this.sessions.delete(sessionId);
3237
+ }
3238
+ /**
3239
+ * Clear all sessions.
3240
+ */
3241
+ clearAll() {
3242
+ this.sessions.clear();
3243
+ }
3244
+ /**
3245
+ * Get session stats for debugging.
3246
+ */
3247
+ getStats() {
3248
+ const now = Date.now();
3249
+ const sessions = Array.from(this.sessions.entries()).map(([id, entry]) => ({
3250
+ id: id.slice(0, 8) + "...",
3251
+ model: entry.model,
3252
+ age: Math.round((now - entry.createdAt) / 1e3)
3253
+ }));
3254
+ return { count: this.sessions.size, sessions };
3255
+ }
3256
+ /**
3257
+ * Clean up expired sessions.
3258
+ */
3259
+ cleanup() {
3260
+ const now = Date.now();
3261
+ for (const [id, entry] of this.sessions) {
3262
+ if (now - entry.lastUsedAt > this.config.timeoutMs) {
3263
+ this.sessions.delete(id);
3264
+ }
3265
+ }
3266
+ }
3267
+ /**
3268
+ * Stop the cleanup interval.
3269
+ */
3270
+ close() {
3271
+ if (this.cleanupInterval) {
3272
+ clearInterval(this.cleanupInterval);
3273
+ this.cleanupInterval = null;
3274
+ }
3275
+ }
3276
+ };
3277
+ function getSessionId(headers, headerName = DEFAULT_SESSION_CONFIG.headerName) {
3278
+ const value = headers[headerName] || headers[headerName.toLowerCase()];
3279
+ if (typeof value === "string" && value.length > 0) {
3280
+ return value;
3281
+ }
3282
+ if (Array.isArray(value) && value.length > 0) {
3283
+ return value[0];
3284
+ }
3285
+ return void 0;
3286
+ }
3287
+
3288
+ // src/updater.ts
3289
+ var NPM_REGISTRY = "https://registry.npmjs.org/@blockrun/clawrouter/latest";
3290
+ var UPDATE_URL = "https://blockrun.ai/ClawRouter-update";
3291
+ var CHECK_TIMEOUT_MS = 5e3;
3292
+ function compareSemver(a, b) {
3293
+ const pa = a.split(".").map(Number);
3294
+ const pb = b.split(".").map(Number);
3295
+ for (let i = 0; i < 3; i++) {
3296
+ if ((pa[i] || 0) > (pb[i] || 0)) return 1;
3297
+ if ((pa[i] || 0) < (pb[i] || 0)) return -1;
3298
+ }
3299
+ return 0;
3300
+ }
3301
+ async function checkForUpdates() {
3302
+ try {
3303
+ const controller = new AbortController();
3304
+ const timeout = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS);
3305
+ const res = await fetch(NPM_REGISTRY, {
3306
+ signal: controller.signal,
3307
+ headers: { Accept: "application/json" }
3308
+ });
3309
+ clearTimeout(timeout);
3310
+ if (!res.ok) return;
3311
+ const data = await res.json();
3312
+ const latest = data.version;
3313
+ if (!latest) return;
3314
+ if (compareSemver(latest, VERSION) > 0) {
3315
+ console.log("");
3316
+ console.log(`\x1B[33m\u2B06\uFE0F ClawRouter ${latest} available (you have ${VERSION})\x1B[0m`);
3317
+ console.log(` Run: \x1B[36mcurl -fsSL ${UPDATE_URL} | bash\x1B[0m`);
3318
+ console.log("");
3319
+ }
3320
+ } catch {
3321
+ }
3322
+ }
3323
+
3324
+ // src/config.ts
3325
+ var DEFAULT_PORT = 8402;
3326
+ var PROXY_PORT = (() => {
3327
+ const envPort = process.env.BLOCKRUN_PROXY_PORT;
3328
+ if (envPort) {
3329
+ const parsed = parseInt(envPort, 10);
3330
+ if (!isNaN(parsed) && parsed > 0 && parsed < 65536) {
3331
+ return parsed;
3332
+ }
3333
+ }
3334
+ return DEFAULT_PORT;
3335
+ })();
3336
+
3337
+ // src/journal.ts
3338
+ var DEFAULT_CONFIG2 = {
3339
+ maxEntries: 100,
3340
+ maxAgeMs: 24 * 60 * 60 * 1e3,
3341
+ // 24 hours
3342
+ maxEventsPerResponse: 5
3343
+ };
3344
+ var SessionJournal = class {
3345
+ journals = /* @__PURE__ */ new Map();
3346
+ config;
3347
+ constructor(config) {
3348
+ this.config = { ...DEFAULT_CONFIG2, ...config };
3349
+ }
3350
+ /**
3351
+ * Extract key events from assistant response content.
3352
+ * Looks for patterns like "I created...", "I fixed...", "Successfully..."
3353
+ */
3354
+ extractEvents(content) {
3355
+ if (!content || typeof content !== "string") {
3356
+ return [];
3357
+ }
3358
+ const events = [];
3359
+ const seen = /* @__PURE__ */ new Set();
3360
+ const patterns = [
3361
+ // Creation patterns
3362
+ /I (?:also |then |have |)?(?:created|implemented|added|wrote|built|generated|set up|initialized) ([^.!?\n]{10,150})/gi,
3363
+ // Fix patterns
3364
+ /I (?:also |then |have |)?(?:fixed|resolved|solved|patched|corrected|addressed|debugged) ([^.!?\n]{10,150})/gi,
3365
+ // Completion patterns
3366
+ /I (?:also |then |have |)?(?:completed|finished|done with|wrapped up) ([^.!?\n]{10,150})/gi,
3367
+ // Update patterns
3368
+ /I (?:also |then |have |)?(?:updated|modified|changed|refactored|improved|enhanced|optimized) ([^.!?\n]{10,150})/gi,
3369
+ // Success patterns
3370
+ /Successfully ([^.!?\n]{10,150})/gi,
3371
+ // Tool usage patterns (when agent uses tools)
3372
+ /I (?:also |then |have |)?(?:ran|executed|called|invoked) ([^.!?\n]{10,100})/gi
3373
+ ];
3374
+ for (const pattern of patterns) {
3375
+ pattern.lastIndex = 0;
3376
+ let match;
3377
+ while ((match = pattern.exec(content)) !== null) {
3378
+ const action = match[0].trim();
3379
+ const normalized = action.toLowerCase();
3380
+ if (seen.has(normalized)) {
3381
+ continue;
3382
+ }
3383
+ if (action.length >= 15 && action.length <= 200) {
3384
+ events.push(action);
3385
+ seen.add(normalized);
3386
+ }
3387
+ if (events.length >= this.config.maxEventsPerResponse) {
3388
+ break;
3389
+ }
3390
+ }
3391
+ if (events.length >= this.config.maxEventsPerResponse) {
3392
+ break;
3393
+ }
3394
+ }
3395
+ return events;
3396
+ }
3397
+ /**
3398
+ * Record events to the session journal.
3399
+ */
3400
+ record(sessionId, events, model) {
3401
+ if (!sessionId || !events.length) {
3402
+ return;
3403
+ }
3404
+ const journal = this.journals.get(sessionId) || [];
3405
+ const now = Date.now();
3406
+ for (const action of events) {
3407
+ journal.push({
3408
+ timestamp: now,
3409
+ action,
3410
+ model
3411
+ });
3412
+ }
3413
+ const cutoff = now - this.config.maxAgeMs;
3414
+ const trimmed = journal.filter((e) => e.timestamp > cutoff).slice(-this.config.maxEntries);
3415
+ this.journals.set(sessionId, trimmed);
3416
+ }
3417
+ /**
3418
+ * Check if the user message indicates a need for historical context.
3419
+ */
3420
+ needsContext(lastUserMessage) {
3421
+ if (!lastUserMessage || typeof lastUserMessage !== "string") {
3422
+ return false;
3423
+ }
3424
+ const lower = lastUserMessage.toLowerCase();
3425
+ const triggers = [
3426
+ // Direct questions about past work
3427
+ "what did you do",
3428
+ "what have you done",
3429
+ "what did we do",
3430
+ "what have we done",
3431
+ // Temporal references
3432
+ "earlier",
3433
+ "before",
3434
+ "previously",
3435
+ "this session",
3436
+ "today",
3437
+ "so far",
3438
+ // Summary requests
3439
+ "remind me",
3440
+ "summarize",
3441
+ "summary of",
3442
+ "recap",
3443
+ // Progress inquiries
3444
+ "your work",
3445
+ "your progress",
3446
+ "accomplished",
3447
+ "achievements",
3448
+ "completed tasks"
3449
+ ];
3450
+ return triggers.some((t) => lower.includes(t));
3451
+ }
3452
+ /**
3453
+ * Format the journal for injection into system message.
3454
+ * Returns null if journal is empty.
3455
+ */
3456
+ format(sessionId) {
3457
+ const journal = this.journals.get(sessionId);
3458
+ if (!journal?.length) {
3459
+ return null;
3460
+ }
3461
+ const lines = journal.map((e) => {
3462
+ const time = new Date(e.timestamp).toLocaleTimeString("en-US", {
3463
+ hour: "2-digit",
3464
+ minute: "2-digit",
3465
+ hour12: true
3466
+ });
3467
+ return `- ${time}: ${e.action}`;
3468
+ });
3469
+ return `[Session Memory - Key Actions]
3470
+ ${lines.join("\n")}`;
3471
+ }
3472
+ /**
3473
+ * Get the raw journal entries for a session (for debugging/testing).
3474
+ */
3475
+ getEntries(sessionId) {
3476
+ return this.journals.get(sessionId) || [];
3477
+ }
3478
+ /**
3479
+ * Clear journal for a specific session.
3480
+ */
3481
+ clear(sessionId) {
3482
+ this.journals.delete(sessionId);
3483
+ }
3484
+ /**
3485
+ * Clear all journals.
3486
+ */
3487
+ clearAll() {
3488
+ this.journals.clear();
3489
+ }
3490
+ /**
3491
+ * Get stats about the journal.
3492
+ */
3493
+ getStats() {
3494
+ let totalEntries = 0;
3495
+ for (const entries of this.journals.values()) {
3496
+ totalEntries += entries.length;
3497
+ }
3498
+ return {
3499
+ sessions: this.journals.size,
3500
+ totalEntries
3501
+ };
3502
+ }
3503
+ };
3504
+
3505
+ // src/proxy.ts
3506
+ var BLOCKRUN_API = "https://blockrun.ai/api";
3507
+ var AUTO_MODEL = "blockrun/auto";
3508
+ var ROUTING_PROFILES = /* @__PURE__ */ new Set([
3509
+ "blockrun/free",
3510
+ "free",
3511
+ "blockrun/eco",
3512
+ "eco",
3513
+ "blockrun/auto",
3514
+ "auto",
3515
+ "blockrun/premium",
3516
+ "premium"
3517
+ ]);
3518
+ var FREE_MODEL = "nvidia/gpt-oss-120b";
3519
+ var MAX_MESSAGES = 200;
3520
+ var HEARTBEAT_INTERVAL_MS = 2e3;
3521
+ var DEFAULT_REQUEST_TIMEOUT_MS = 18e4;
3522
+ var MAX_FALLBACK_ATTEMPTS = 5;
3523
+ var HEALTH_CHECK_TIMEOUT_MS = 2e3;
3524
+ var RATE_LIMIT_COOLDOWN_MS = 6e4;
3525
+ var PORT_RETRY_ATTEMPTS = 5;
3526
+ var PORT_RETRY_DELAY_MS = 1e3;
3527
+ function transformPaymentError(errorBody) {
3528
+ try {
3529
+ const parsed = JSON.parse(errorBody);
3530
+ if (parsed.error === "Payment verification failed" && parsed.details) {
3531
+ const match = parsed.details.match(/Verification failed:\s*(\{.*\})/s);
3532
+ if (match) {
3533
+ const innerJson = JSON.parse(match[1]);
3534
+ if (innerJson.invalidReason === "insufficient_funds" && innerJson.invalidMessage) {
3535
+ const balanceMatch = innerJson.invalidMessage.match(
3536
+ /insufficient balance:\s*(\d+)\s*<\s*(\d+)/i
3537
+ );
3538
+ if (balanceMatch) {
3539
+ const currentMicros = parseInt(balanceMatch[1], 10);
3540
+ const requiredMicros = parseInt(balanceMatch[2], 10);
3541
+ const currentUSD = (currentMicros / 1e6).toFixed(6);
3542
+ const requiredUSD = (requiredMicros / 1e6).toFixed(6);
3543
+ const wallet = innerJson.payer || "unknown";
3544
+ const shortWallet = wallet.length > 12 ? `${wallet.slice(0, 6)}...${wallet.slice(-4)}` : wallet;
3545
+ return JSON.stringify({
3546
+ error: {
3547
+ message: `Insufficient USDC balance. Current: $${currentUSD}, Required: ~$${requiredUSD}`,
3548
+ type: "insufficient_funds",
3549
+ wallet,
3550
+ current_balance_usd: currentUSD,
3551
+ required_usd: requiredUSD,
3552
+ help: `Fund wallet ${shortWallet} with USDC on Base, or use free model: /model free`
3553
+ }
3554
+ });
3555
+ }
3556
+ }
3557
+ if (innerJson.invalidReason === "invalid_payload") {
3558
+ return JSON.stringify({
3559
+ error: {
3560
+ message: "Payment signature invalid. This may be a temporary issue.",
3561
+ type: "invalid_payload",
3562
+ help: "Try again. If this persists, reinstall ClawRouter: curl -fsSL https://blockrun.ai/ClawRouter-update | bash"
3563
+ }
3564
+ });
3565
+ }
3566
+ }
3567
+ }
3568
+ if (parsed.error === "Settlement failed" || parsed.details?.includes("Settlement failed")) {
3569
+ const details = parsed.details || "";
3570
+ const gasError = details.includes("unable to estimate gas");
3571
+ return JSON.stringify({
3572
+ error: {
3573
+ message: gasError ? "Payment failed: network congestion or gas issue. Try again." : "Payment settlement failed. Try again in a moment.",
3574
+ type: "settlement_failed",
3575
+ help: "This is usually temporary. If it persists, try: /model free"
3576
+ }
3577
+ });
3578
+ }
3579
+ } catch {
3580
+ }
3581
+ return errorBody;
3582
+ }
3583
+ var rateLimitedModels = /* @__PURE__ */ new Map();
3584
+ function isRateLimited(modelId) {
3585
+ const hitTime = rateLimitedModels.get(modelId);
3586
+ if (!hitTime) return false;
3587
+ const elapsed = Date.now() - hitTime;
3588
+ if (elapsed >= RATE_LIMIT_COOLDOWN_MS) {
3589
+ rateLimitedModels.delete(modelId);
3590
+ return false;
3591
+ }
3592
+ return true;
3593
+ }
3594
+ function markRateLimited(modelId) {
3595
+ rateLimitedModels.set(modelId, Date.now());
3596
+ console.log(`[ClawRouter] Model ${modelId} rate-limited, will deprioritize for 60s`);
3597
+ }
3598
+ function prioritizeNonRateLimited(models) {
3599
+ const available = [];
3600
+ const rateLimited = [];
3601
+ for (const model of models) {
3602
+ if (isRateLimited(model)) {
3603
+ rateLimited.push(model);
3604
+ } else {
3605
+ available.push(model);
3606
+ }
3607
+ }
3608
+ return [...available, ...rateLimited];
3609
+ }
3610
+ function canWrite(res) {
3611
+ return !res.writableEnded && !res.destroyed && res.socket !== null && !res.socket.destroyed && res.socket.writable;
3612
+ }
3613
+ function safeWrite(res, data) {
3614
+ if (!canWrite(res)) {
3615
+ return false;
3616
+ }
3617
+ return res.write(data);
3618
+ }
3619
+ var BALANCE_CHECK_BUFFER = 1.5;
3620
+ function getProxyPort() {
3621
+ return PROXY_PORT;
3622
+ }
3623
+ async function checkExistingProxy(port) {
3624
+ const controller = new AbortController();
3625
+ const timeoutId = setTimeout(() => controller.abort(), HEALTH_CHECK_TIMEOUT_MS);
3626
+ try {
3627
+ const response = await fetch(`http://127.0.0.1:${port}/health`, {
3628
+ signal: controller.signal
3629
+ });
3630
+ clearTimeout(timeoutId);
3631
+ if (response.ok) {
3632
+ const data = await response.json();
3633
+ if (data.status === "ok" && data.wallet) {
3634
+ return data.wallet;
3635
+ }
3636
+ }
3637
+ return void 0;
3638
+ } catch {
3639
+ clearTimeout(timeoutId);
3640
+ return void 0;
3641
+ }
3642
+ }
3643
+ var PROVIDER_ERROR_PATTERNS = [
3644
+ /billing/i,
3645
+ /insufficient.*balance/i,
3646
+ /credits/i,
3647
+ /quota.*exceeded/i,
3648
+ /rate.*limit/i,
3649
+ /model.*unavailable/i,
3650
+ /model.*not.*available/i,
3651
+ /service.*unavailable/i,
3652
+ /capacity/i,
3653
+ /overloaded/i,
3654
+ /temporarily.*unavailable/i,
3655
+ /api.*key.*invalid/i,
3656
+ /authentication.*failed/i,
3657
+ /request too large/i,
3658
+ /request.*size.*exceeds/i,
3659
+ /payload too large/i
3660
+ ];
3661
+ var FALLBACK_STATUS_CODES = [
3662
+ 400,
3663
+ // Bad request - sometimes used for billing errors
3664
+ 401,
3665
+ // Unauthorized - provider API key issues
3666
+ 402,
3667
+ // Payment required - but from upstream, not x402
3668
+ 403,
3669
+ // Forbidden - provider restrictions
3670
+ 413,
3671
+ // Payload too large - request exceeds model's context limit
3672
+ 429,
3673
+ // Rate limited
3674
+ 500,
3675
+ // Internal server error
3676
+ 502,
3677
+ // Bad gateway
3678
+ 503,
3679
+ // Service unavailable
3680
+ 504
3681
+ // Gateway timeout
3682
+ ];
3683
+ function isProviderError(status, body) {
3684
+ if (!FALLBACK_STATUS_CODES.includes(status)) {
3685
+ return false;
3686
+ }
3687
+ if (status >= 500) {
3688
+ return true;
3689
+ }
3690
+ return PROVIDER_ERROR_PATTERNS.some((pattern) => pattern.test(body));
3691
+ }
3692
+ var VALID_ROLES = /* @__PURE__ */ new Set(["system", "user", "assistant", "tool", "function"]);
3693
+ var ROLE_MAPPINGS = {
3694
+ developer: "system",
3695
+ // OpenAI's newer API uses "developer" for system messages
3696
+ model: "assistant"
3697
+ // Some APIs use "model" instead of "assistant"
3698
+ };
3699
+ var VALID_TOOL_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
3700
+ function sanitizeToolId(id) {
3701
+ if (!id || typeof id !== "string") return id;
3702
+ if (VALID_TOOL_ID_PATTERN.test(id)) return id;
3703
+ return id.replace(/[^a-zA-Z0-9_-]/g, "_");
3704
+ }
3705
+ function sanitizeToolIds(messages) {
3706
+ if (!messages || messages.length === 0) return messages;
3707
+ let hasChanges = false;
3708
+ const sanitized = messages.map((msg) => {
3709
+ const typedMsg = msg;
3710
+ let msgChanged = false;
3711
+ let newMsg = { ...msg };
3712
+ if (typedMsg.tool_calls && Array.isArray(typedMsg.tool_calls)) {
3713
+ const newToolCalls = typedMsg.tool_calls.map((tc) => {
3714
+ if (tc.id && typeof tc.id === "string") {
3715
+ const sanitized2 = sanitizeToolId(tc.id);
3716
+ if (sanitized2 !== tc.id) {
3717
+ msgChanged = true;
3718
+ return { ...tc, id: sanitized2 };
3719
+ }
3720
+ }
3721
+ return tc;
3722
+ });
3723
+ if (msgChanged) {
3724
+ newMsg = { ...newMsg, tool_calls: newToolCalls };
3725
+ }
3726
+ }
3727
+ if (typedMsg.tool_call_id && typeof typedMsg.tool_call_id === "string") {
3728
+ const sanitized2 = sanitizeToolId(typedMsg.tool_call_id);
3729
+ if (sanitized2 !== typedMsg.tool_call_id) {
3730
+ msgChanged = true;
3731
+ newMsg = { ...newMsg, tool_call_id: sanitized2 };
3732
+ }
3733
+ }
3734
+ if (Array.isArray(typedMsg.content)) {
3735
+ const newContent = typedMsg.content.map((block) => {
3736
+ if (!block || typeof block !== "object") return block;
3737
+ let blockChanged = false;
3738
+ let newBlock = { ...block };
3739
+ if (block.type === "tool_use" && block.id && typeof block.id === "string") {
3740
+ const sanitized2 = sanitizeToolId(block.id);
3741
+ if (sanitized2 !== block.id) {
3742
+ blockChanged = true;
3743
+ newBlock = { ...newBlock, id: sanitized2 };
3744
+ }
3745
+ }
3746
+ if (block.type === "tool_result" && block.tool_use_id && typeof block.tool_use_id === "string") {
3747
+ const sanitized2 = sanitizeToolId(block.tool_use_id);
3748
+ if (sanitized2 !== block.tool_use_id) {
3749
+ blockChanged = true;
3750
+ newBlock = { ...newBlock, tool_use_id: sanitized2 };
3751
+ }
3752
+ }
3753
+ if (blockChanged) {
3754
+ msgChanged = true;
3755
+ return newBlock;
3756
+ }
3757
+ return block;
3758
+ });
3759
+ if (msgChanged) {
3760
+ newMsg = { ...newMsg, content: newContent };
3761
+ }
3762
+ }
3763
+ if (msgChanged) {
3764
+ hasChanges = true;
3765
+ return newMsg;
3766
+ }
3767
+ return msg;
3768
+ });
3769
+ return hasChanges ? sanitized : messages;
3770
+ }
3771
+ function normalizeMessageRoles(messages) {
3772
+ if (!messages || messages.length === 0) return messages;
3773
+ let hasChanges = false;
3774
+ const normalized = messages.map((msg) => {
3775
+ if (VALID_ROLES.has(msg.role)) return msg;
3776
+ const mappedRole = ROLE_MAPPINGS[msg.role];
3777
+ if (mappedRole) {
3778
+ hasChanges = true;
3779
+ return { ...msg, role: mappedRole };
3780
+ }
3781
+ hasChanges = true;
3782
+ return { ...msg, role: "user" };
3783
+ });
3784
+ return hasChanges ? normalized : messages;
3785
+ }
3786
+ function normalizeMessagesForGoogle(messages) {
3787
+ if (!messages || messages.length === 0) return messages;
3788
+ let firstNonSystemIdx = -1;
3789
+ for (let i = 0; i < messages.length; i++) {
3790
+ if (messages[i].role !== "system") {
3791
+ firstNonSystemIdx = i;
3792
+ break;
3793
+ }
3794
+ }
3795
+ if (firstNonSystemIdx === -1) return messages;
3796
+ const firstRole = messages[firstNonSystemIdx].role;
3797
+ if (firstRole === "user") return messages;
3798
+ if (firstRole === "assistant" || firstRole === "model") {
3799
+ const normalized = [...messages];
3800
+ normalized.splice(firstNonSystemIdx, 0, {
3801
+ role: "user",
3802
+ content: "(continuing conversation)"
3803
+ });
3804
+ return normalized;
3805
+ }
3806
+ return messages;
3807
+ }
3808
+ function isGoogleModel(modelId) {
3809
+ return modelId.startsWith("google/") || modelId.startsWith("gemini");
3810
+ }
3811
+ function normalizeMessagesForThinking(messages) {
3812
+ if (!messages || messages.length === 0) return messages;
3813
+ let hasChanges = false;
3814
+ const normalized = messages.map((msg) => {
3815
+ if (msg.role !== "assistant" || msg.reasoning_content !== void 0) {
3816
+ return msg;
3817
+ }
3818
+ const hasOpenAIToolCalls = msg.tool_calls && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0;
3819
+ const hasAnthropicToolUse = Array.isArray(msg.content) && msg.content.some((block) => block?.type === "tool_use");
3820
+ if (hasOpenAIToolCalls || hasAnthropicToolUse) {
3821
+ hasChanges = true;
3822
+ return { ...msg, reasoning_content: "" };
3823
+ }
3824
+ return msg;
3825
+ });
3826
+ return hasChanges ? normalized : messages;
3827
+ }
3828
+ function truncateMessages(messages) {
3829
+ if (!messages || messages.length <= MAX_MESSAGES) return messages;
3830
+ const systemMsgs = messages.filter((m) => m.role === "system");
3831
+ const conversationMsgs = messages.filter((m) => m.role !== "system");
3832
+ const maxConversation = MAX_MESSAGES - systemMsgs.length;
3833
+ const truncatedConversation = conversationMsgs.slice(-maxConversation);
3834
+ console.log(
3835
+ `[ClawRouter] Truncated messages: ${messages.length} \u2192 ${systemMsgs.length + truncatedConversation.length} (kept ${systemMsgs.length} system + ${truncatedConversation.length} recent)`
3836
+ );
3837
+ return [...systemMsgs, ...truncatedConversation];
3838
+ }
3839
+ var KIMI_BLOCK_RE = /<[||][^<>]*begin[^<>]*[||]>[\s\S]*?<[||][^<>]*end[^<>]*[||]>/gi;
3840
+ var KIMI_TOKEN_RE = /<[||][^<>]*[||]>/g;
3841
+ var THINKING_TAG_RE = /<\s*\/?\s*(?:think(?:ing)?|thought|antthinking)\b[^>]*>/gi;
3842
+ var THINKING_BLOCK_RE = /<\s*(?:think(?:ing)?|thought|antthinking)\b[^>]*>[\s\S]*?<\s*\/\s*(?:think(?:ing)?|thought|antthinking)\s*>/gi;
3843
+ function stripThinkingTokens(content) {
3844
+ if (!content) return content;
3845
+ let cleaned = content.replace(KIMI_BLOCK_RE, "");
3846
+ cleaned = cleaned.replace(KIMI_TOKEN_RE, "");
3847
+ cleaned = cleaned.replace(THINKING_BLOCK_RE, "");
3848
+ cleaned = cleaned.replace(THINKING_TAG_RE, "");
3849
+ return cleaned;
3850
+ }
3851
+ function buildModelPricing() {
3852
+ const map = /* @__PURE__ */ new Map();
3853
+ for (const m of BLOCKRUN_MODELS) {
3854
+ if (m.id === AUTO_MODEL) continue;
3855
+ map.set(m.id, { inputPrice: m.inputPrice, outputPrice: m.outputPrice });
3856
+ }
3857
+ return map;
3858
+ }
3859
+ function mergeRoutingConfig(overrides) {
3860
+ if (!overrides) return DEFAULT_ROUTING_CONFIG;
3861
+ return {
3862
+ ...DEFAULT_ROUTING_CONFIG,
3863
+ ...overrides,
3864
+ classifier: { ...DEFAULT_ROUTING_CONFIG.classifier, ...overrides.classifier },
3865
+ scoring: { ...DEFAULT_ROUTING_CONFIG.scoring, ...overrides.scoring },
3866
+ tiers: { ...DEFAULT_ROUTING_CONFIG.tiers, ...overrides.tiers },
3867
+ overrides: { ...DEFAULT_ROUTING_CONFIG.overrides, ...overrides.overrides }
3868
+ };
3869
+ }
3870
+ function estimateAmount(modelId, bodyLength, maxTokens) {
3871
+ const model = BLOCKRUN_MODELS.find((m) => m.id === modelId);
3872
+ if (!model) return void 0;
3873
+ const estimatedInputTokens = Math.ceil(bodyLength / 4);
3874
+ const estimatedOutputTokens = maxTokens || model.maxOutput || 4096;
3875
+ const costUsd = estimatedInputTokens / 1e6 * model.inputPrice + estimatedOutputTokens / 1e6 * model.outputPrice;
3876
+ const amountMicros = Math.max(100, Math.ceil(costUsd * 1.2 * 1e6));
3877
+ return amountMicros.toString();
3878
+ }
3879
+ async function startProxy(options) {
3880
+ const apiBase = options.apiBase ?? BLOCKRUN_API;
3881
+ const listenPort = options.port ?? getProxyPort();
3882
+ const existingWallet = await checkExistingProxy(listenPort);
3883
+ if (existingWallet) {
3884
+ const account2 = privateKeyToAccount2(options.walletKey);
3885
+ const balanceMonitor2 = new BalanceMonitor(account2.address);
3886
+ const baseUrl2 = `http://127.0.0.1:${listenPort}`;
3887
+ if (existingWallet !== account2.address) {
3888
+ console.warn(
3889
+ `[ClawRouter] Existing proxy on port ${listenPort} uses wallet ${existingWallet}, but current config uses ${account2.address}. Reusing existing proxy.`
3890
+ );
3891
+ }
3892
+ options.onReady?.(listenPort);
3893
+ return {
3894
+ port: listenPort,
3895
+ baseUrl: baseUrl2,
3896
+ walletAddress: existingWallet,
3897
+ balanceMonitor: balanceMonitor2,
3898
+ close: async () => {
3899
+ }
3900
+ };
3901
+ }
3902
+ const account = privateKeyToAccount2(options.walletKey);
3903
+ const { fetch: payFetch } = createPaymentFetch(options.walletKey);
3904
+ const balanceMonitor = new BalanceMonitor(account.address);
3905
+ const routingConfig = mergeRoutingConfig(options.routingConfig);
3906
+ const modelPricing = buildModelPricing();
3907
+ const routerOpts = {
3908
+ config: routingConfig,
3909
+ modelPricing
3910
+ };
3911
+ const deduplicator = new RequestDeduplicator();
3912
+ const responseCache = new ResponseCache(options.cacheConfig);
3913
+ const sessionStore = new SessionStore(options.sessionConfig);
3914
+ const sessionJournal = new SessionJournal();
3915
+ const connections = /* @__PURE__ */ new Set();
3916
+ const server = createServer(async (req, res) => {
3917
+ req.on("error", (err) => {
3918
+ console.error(`[ClawRouter] Request stream error: ${err.message}`);
3919
+ });
3920
+ res.on("error", (err) => {
3921
+ console.error(`[ClawRouter] Response stream error: ${err.message}`);
3922
+ });
3923
+ finished(res, (err) => {
3924
+ if (err && err.code !== "ERR_STREAM_DESTROYED") {
3925
+ console.error(`[ClawRouter] Response finished with error: ${err.message}`);
3926
+ }
3927
+ });
3928
+ finished(req, (err) => {
3929
+ if (err && err.code !== "ERR_STREAM_DESTROYED") {
3930
+ console.error(`[ClawRouter] Request finished with error: ${err.message}`);
3931
+ }
3932
+ });
3933
+ if (req.url === "/health" || req.url?.startsWith("/health?")) {
3934
+ const url = new URL(req.url, "http://localhost");
3935
+ const full = url.searchParams.get("full") === "true";
3936
+ const response = {
3937
+ status: "ok",
3938
+ wallet: account.address
3939
+ };
3940
+ if (full) {
3941
+ try {
3942
+ const balanceInfo = await balanceMonitor.checkBalance();
3943
+ response.balance = balanceInfo.balanceUSD;
3944
+ response.isLow = balanceInfo.isLow;
3945
+ response.isEmpty = balanceInfo.isEmpty;
3946
+ } catch {
3947
+ response.balanceError = "Could not fetch balance";
3948
+ }
3949
+ }
3950
+ res.writeHead(200, { "Content-Type": "application/json" });
3951
+ res.end(JSON.stringify(response));
3952
+ return;
3953
+ }
3954
+ if (req.url === "/cache" || req.url?.startsWith("/cache?")) {
3955
+ const stats = responseCache.getStats();
3956
+ res.writeHead(200, {
3957
+ "Content-Type": "application/json",
3958
+ "Cache-Control": "no-cache"
3959
+ });
3960
+ res.end(JSON.stringify(stats, null, 2));
3961
+ return;
3962
+ }
3963
+ if (req.url === "/stats" || req.url?.startsWith("/stats?")) {
3964
+ try {
3965
+ const url = new URL(req.url, "http://localhost");
3966
+ const days = parseInt(url.searchParams.get("days") || "7", 10);
3967
+ const stats = await getStats(Math.min(days, 30));
3968
+ res.writeHead(200, {
3969
+ "Content-Type": "application/json",
3970
+ "Cache-Control": "no-cache"
3971
+ });
3972
+ res.end(JSON.stringify(stats, null, 2));
3973
+ } catch (err) {
3974
+ res.writeHead(500, { "Content-Type": "application/json" });
3975
+ res.end(
3976
+ JSON.stringify({
3977
+ error: `Failed to get stats: ${err instanceof Error ? err.message : String(err)}`
3978
+ })
3979
+ );
3980
+ }
3981
+ return;
3982
+ }
3983
+ if (req.url === "/v1/models" && req.method === "GET") {
3984
+ const models = BLOCKRUN_MODELS.filter((m) => m.id !== "blockrun/auto").map((m) => ({
3985
+ id: m.id,
3986
+ object: "model",
3987
+ created: Math.floor(Date.now() / 1e3),
3988
+ owned_by: m.id.split("/")[0] || "unknown"
3989
+ }));
3990
+ res.writeHead(200, { "Content-Type": "application/json" });
3991
+ res.end(JSON.stringify({ object: "list", data: models }));
3992
+ return;
3993
+ }
3994
+ if (!req.url?.startsWith("/v1")) {
3995
+ res.writeHead(404, { "Content-Type": "application/json" });
3996
+ res.end(JSON.stringify({ error: "Not found" }));
3997
+ return;
3998
+ }
3999
+ try {
4000
+ await proxyRequest(
4001
+ req,
4002
+ res,
4003
+ apiBase,
4004
+ payFetch,
4005
+ options,
4006
+ routerOpts,
4007
+ deduplicator,
4008
+ balanceMonitor,
4009
+ sessionStore,
4010
+ responseCache,
4011
+ sessionJournal
4012
+ );
4013
+ } catch (err) {
4014
+ const error = err instanceof Error ? err : new Error(String(err));
4015
+ options.onError?.(error);
4016
+ if (!res.headersSent) {
4017
+ res.writeHead(502, { "Content-Type": "application/json" });
4018
+ res.end(
4019
+ JSON.stringify({
4020
+ error: { message: `Proxy error: ${error.message}`, type: "proxy_error" }
4021
+ })
4022
+ );
4023
+ } else if (!res.writableEnded) {
4024
+ res.write(
4025
+ `data: ${JSON.stringify({ error: { message: error.message, type: "proxy_error" } })}
4026
+
4027
+ `
4028
+ );
4029
+ res.write("data: [DONE]\n\n");
4030
+ res.end();
4031
+ }
4032
+ }
4033
+ });
4034
+ const tryListen = (attempt) => {
4035
+ return new Promise((resolveAttempt, rejectAttempt) => {
4036
+ const onError = async (err) => {
4037
+ server.removeListener("error", onError);
4038
+ if (err.code === "EADDRINUSE") {
4039
+ const existingWallet2 = await checkExistingProxy(listenPort);
4040
+ if (existingWallet2) {
4041
+ console.log(`[ClawRouter] Existing proxy detected on port ${listenPort}, reusing`);
4042
+ rejectAttempt({ code: "REUSE_EXISTING", wallet: existingWallet2 });
4043
+ return;
4044
+ }
4045
+ if (attempt < PORT_RETRY_ATTEMPTS) {
4046
+ console.log(
4047
+ `[ClawRouter] Port ${listenPort} in TIME_WAIT, retrying in ${PORT_RETRY_DELAY_MS}ms (attempt ${attempt}/${PORT_RETRY_ATTEMPTS})`
4048
+ );
4049
+ rejectAttempt({ code: "RETRY", attempt });
4050
+ return;
4051
+ }
4052
+ console.error(
4053
+ `[ClawRouter] Port ${listenPort} still in use after ${PORT_RETRY_ATTEMPTS} attempts`
4054
+ );
4055
+ rejectAttempt(err);
4056
+ return;
4057
+ }
4058
+ rejectAttempt(err);
4059
+ };
4060
+ server.once("error", onError);
4061
+ server.listen(listenPort, "127.0.0.1", () => {
4062
+ server.removeListener("error", onError);
4063
+ resolveAttempt();
4064
+ });
4065
+ });
4066
+ };
4067
+ let lastError;
4068
+ for (let attempt = 1; attempt <= PORT_RETRY_ATTEMPTS; attempt++) {
4069
+ try {
4070
+ await tryListen(attempt);
4071
+ break;
4072
+ } catch (err) {
4073
+ const error = err;
4074
+ if (error.code === "REUSE_EXISTING" && error.wallet) {
4075
+ const baseUrl2 = `http://127.0.0.1:${listenPort}`;
4076
+ options.onReady?.(listenPort);
4077
+ return {
4078
+ port: listenPort,
4079
+ baseUrl: baseUrl2,
4080
+ walletAddress: error.wallet,
4081
+ balanceMonitor,
4082
+ close: async () => {
4083
+ }
4084
+ };
4085
+ }
4086
+ if (error.code === "RETRY") {
4087
+ await new Promise((r) => setTimeout(r, PORT_RETRY_DELAY_MS));
4088
+ continue;
4089
+ }
4090
+ lastError = err;
4091
+ break;
4092
+ }
4093
+ }
4094
+ if (lastError) {
4095
+ throw lastError;
4096
+ }
4097
+ const addr = server.address();
4098
+ const port = addr.port;
4099
+ const baseUrl = `http://127.0.0.1:${port}`;
4100
+ options.onReady?.(port);
4101
+ checkForUpdates();
4102
+ server.on("error", (err) => {
4103
+ console.error(`[ClawRouter] Server runtime error: ${err.message}`);
4104
+ options.onError?.(err);
4105
+ });
4106
+ server.on("clientError", (err, socket) => {
4107
+ console.error(`[ClawRouter] Client error: ${err.message}`);
4108
+ if (socket.writable && !socket.destroyed) {
4109
+ socket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
4110
+ }
4111
+ });
4112
+ server.on("connection", (socket) => {
4113
+ connections.add(socket);
4114
+ socket.setTimeout(3e5);
4115
+ socket.on("timeout", () => {
4116
+ console.error(`[ClawRouter] Socket timeout, destroying connection`);
4117
+ socket.destroy();
4118
+ });
4119
+ socket.on("end", () => {
4120
+ });
4121
+ socket.on("error", (err) => {
4122
+ console.error(`[ClawRouter] Socket error: ${err.message}`);
4123
+ });
4124
+ socket.on("close", () => {
4125
+ connections.delete(socket);
4126
+ });
4127
+ });
4128
+ return {
4129
+ port,
4130
+ baseUrl,
4131
+ walletAddress: account.address,
4132
+ balanceMonitor,
4133
+ close: () => new Promise((res, rej) => {
4134
+ const timeout = setTimeout(() => {
4135
+ rej(new Error("[ClawRouter] Close timeout after 4s"));
4136
+ }, 4e3);
4137
+ sessionStore.close();
4138
+ for (const socket of connections) {
4139
+ socket.destroy();
4140
+ }
4141
+ connections.clear();
4142
+ server.close((err) => {
4143
+ clearTimeout(timeout);
4144
+ if (err) {
4145
+ rej(err);
4146
+ } else {
4147
+ res();
4148
+ }
4149
+ });
4150
+ })
4151
+ };
4152
+ }
4153
+ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxTokens, payFetch, balanceMonitor, signal) {
4154
+ let requestBody = body;
4155
+ try {
4156
+ const parsed = JSON.parse(body.toString());
4157
+ parsed.model = modelId;
4158
+ if (Array.isArray(parsed.messages)) {
4159
+ parsed.messages = normalizeMessageRoles(parsed.messages);
4160
+ }
4161
+ if (Array.isArray(parsed.messages)) {
4162
+ parsed.messages = truncateMessages(parsed.messages);
4163
+ }
4164
+ if (Array.isArray(parsed.messages)) {
4165
+ parsed.messages = sanitizeToolIds(parsed.messages);
4166
+ }
4167
+ if (isGoogleModel(modelId) && Array.isArray(parsed.messages)) {
4168
+ parsed.messages = normalizeMessagesForGoogle(parsed.messages);
4169
+ }
4170
+ const hasThinkingEnabled = !!(parsed.thinking || parsed.extended_thinking || isReasoningModel(modelId));
4171
+ if (hasThinkingEnabled && Array.isArray(parsed.messages)) {
4172
+ parsed.messages = normalizeMessagesForThinking(parsed.messages);
4173
+ }
4174
+ requestBody = Buffer.from(JSON.stringify(parsed));
4175
+ } catch {
4176
+ }
4177
+ const estimated = estimateAmount(modelId, requestBody.length, maxTokens);
4178
+ const preAuth = estimated ? { estimatedAmount: estimated } : void 0;
4179
+ try {
4180
+ const response = await payFetch(
4181
+ upstreamUrl,
4182
+ {
4183
+ method,
4184
+ headers,
4185
+ body: requestBody.length > 0 ? new Uint8Array(requestBody) : void 0,
4186
+ signal
4187
+ },
4188
+ preAuth
4189
+ );
4190
+ if (response.status !== 200) {
4191
+ const errorBody = await response.text();
4192
+ const isProviderErr = isProviderError(response.status, errorBody);
4193
+ return {
4194
+ success: false,
4195
+ errorBody,
4196
+ errorStatus: response.status,
4197
+ isProviderError: isProviderErr
4198
+ };
4199
+ }
4200
+ return { success: true, response };
4201
+ } catch (err) {
4202
+ const errorMsg = err instanceof Error ? err.message : String(err);
4203
+ return {
4204
+ success: false,
4205
+ errorBody: errorMsg,
4206
+ errorStatus: 500,
4207
+ isProviderError: true
4208
+ // Network errors are retryable
4209
+ };
4210
+ }
4211
+ }
4212
+ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, deduplicator, balanceMonitor, sessionStore, responseCache, sessionJournal) {
4213
+ const startTime = Date.now();
4214
+ const upstreamUrl = `${apiBase}${req.url}`;
4215
+ const bodyChunks = [];
4216
+ for await (const chunk of req) {
4217
+ bodyChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
4218
+ }
4219
+ let body = Buffer.concat(bodyChunks);
4220
+ let routingDecision;
4221
+ let isStreaming = false;
4222
+ let modelId = "";
4223
+ let maxTokens = 4096;
4224
+ let routingProfile = null;
4225
+ let accumulatedContent = "";
4226
+ const isChatCompletion = req.url?.includes("/chat/completions");
4227
+ const sessionId = getSessionId(req.headers);
4228
+ if (isChatCompletion && body.length > 0) {
4229
+ try {
4230
+ const parsed = JSON.parse(body.toString());
4231
+ isStreaming = parsed.stream === true;
4232
+ modelId = parsed.model || "";
4233
+ maxTokens = parsed.max_tokens || 4096;
4234
+ if (sessionId && Array.isArray(parsed.messages)) {
4235
+ const messages = parsed.messages;
4236
+ const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
4237
+ const lastContent = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : "";
4238
+ if (sessionJournal.needsContext(lastContent)) {
4239
+ const journalText = sessionJournal.format(sessionId);
4240
+ if (journalText) {
4241
+ const sysIdx = messages.findIndex((m) => m.role === "system");
4242
+ if (sysIdx >= 0 && typeof messages[sysIdx].content === "string") {
4243
+ messages[sysIdx] = {
4244
+ ...messages[sysIdx],
4245
+ content: journalText + "\n\n" + messages[sysIdx].content
4246
+ };
4247
+ } else {
4248
+ messages.unshift({ role: "system", content: journalText });
4249
+ }
4250
+ parsed.messages = messages;
4251
+ console.log(
4252
+ `[ClawRouter] Injected session journal (${journalText.length} chars) for session ${sessionId.slice(0, 8)}...`
4253
+ );
4254
+ }
4255
+ }
4256
+ }
4257
+ let bodyModified = false;
4258
+ if (parsed.stream === true) {
4259
+ parsed.stream = false;
4260
+ bodyModified = true;
4261
+ }
4262
+ const normalizedModel = typeof parsed.model === "string" ? parsed.model.trim().toLowerCase() : "";
4263
+ const resolvedModel = resolveModelAlias(normalizedModel);
4264
+ const wasAlias = resolvedModel !== normalizedModel;
4265
+ const isRoutingProfile = ROUTING_PROFILES.has(normalizedModel);
4266
+ if (isRoutingProfile) {
4267
+ const profileName = normalizedModel.replace("blockrun/", "");
4268
+ routingProfile = profileName;
4269
+ }
4270
+ console.log(
4271
+ `[ClawRouter] Received model: "${parsed.model}" -> normalized: "${normalizedModel}"${wasAlias ? ` -> alias: "${resolvedModel}"` : ""}${routingProfile ? `, profile: ${routingProfile}` : ""}`
4272
+ );
4273
+ if (wasAlias && !isRoutingProfile) {
4274
+ parsed.model = resolvedModel;
4275
+ modelId = resolvedModel;
4276
+ bodyModified = true;
4277
+ }
4278
+ if (isRoutingProfile) {
4279
+ if (routingProfile === "free") {
4280
+ const freeModel = "nvidia/gpt-oss-120b";
4281
+ console.log(`[ClawRouter] Free profile - using ${freeModel} directly`);
4282
+ parsed.model = freeModel;
4283
+ modelId = freeModel;
4284
+ bodyModified = true;
4285
+ await logUsage({
4286
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4287
+ model: freeModel,
4288
+ tier: "SIMPLE",
4289
+ cost: 0,
4290
+ baselineCost: 0,
4291
+ savings: 1,
4292
+ // 100% savings
4293
+ latencyMs: 0
4294
+ });
4295
+ } else {
4296
+ const sessionId2 = getSessionId(
4297
+ req.headers
4298
+ );
4299
+ const existingSession = sessionId2 ? sessionStore.getSession(sessionId2) : void 0;
4300
+ if (existingSession) {
4301
+ console.log(
4302
+ `[ClawRouter] Session ${sessionId2?.slice(0, 8)}... using pinned model: ${existingSession.model}`
4303
+ );
4304
+ parsed.model = existingSession.model;
4305
+ modelId = existingSession.model;
4306
+ bodyModified = true;
4307
+ sessionStore.touchSession(sessionId2);
4308
+ } else {
4309
+ const messages = parsed.messages;
4310
+ let lastUserMsg;
4311
+ if (messages) {
4312
+ for (let i = messages.length - 1; i >= 0; i--) {
4313
+ if (messages[i].role === "user") {
4314
+ lastUserMsg = messages[i];
4315
+ break;
4316
+ }
4317
+ }
4318
+ }
4319
+ const systemMsg = messages?.find((m) => m.role === "system");
4320
+ const prompt = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : "";
4321
+ const systemPrompt = typeof systemMsg?.content === "string" ? systemMsg.content : void 0;
4322
+ const tools = parsed.tools;
4323
+ const hasTools = Array.isArray(tools) && tools.length > 0;
4324
+ if (hasTools) {
4325
+ console.log(
4326
+ `[ClawRouter] Tools detected (${tools.length}), agentic mode via keywords`
4327
+ );
4328
+ }
4329
+ routingDecision = route(prompt, systemPrompt, maxTokens, {
4330
+ ...routerOpts,
4331
+ routingProfile: routingProfile ?? void 0
4332
+ });
4333
+ parsed.model = routingDecision.model;
4334
+ modelId = routingDecision.model;
4335
+ bodyModified = true;
4336
+ if (sessionId2) {
4337
+ sessionStore.setSession(sessionId2, routingDecision.model, routingDecision.tier);
4338
+ console.log(
4339
+ `[ClawRouter] Session ${sessionId2.slice(0, 8)}... pinned to model: ${routingDecision.model}`
4340
+ );
4341
+ }
4342
+ options.onRouted?.(routingDecision);
4343
+ }
4344
+ }
4345
+ }
4346
+ if (bodyModified) {
4347
+ body = Buffer.from(JSON.stringify(parsed));
4348
+ }
4349
+ } catch (err) {
4350
+ const errorMsg = err instanceof Error ? err.message : String(err);
4351
+ console.error(`[ClawRouter] Routing error: ${errorMsg}`);
4352
+ options.onError?.(new Error(`Routing failed: ${errorMsg}`));
4353
+ }
4354
+ }
4355
+ const autoCompress = options.autoCompressRequests ?? true;
4356
+ const compressionThreshold = options.compressionThresholdKB ?? 180;
4357
+ const requestSizeKB = Math.ceil(body.length / 1024);
4358
+ if (autoCompress && requestSizeKB > compressionThreshold) {
4359
+ try {
4360
+ console.log(
4361
+ `[ClawRouter] Request size ${requestSizeKB}KB exceeds threshold ${compressionThreshold}KB, applying compression...`
4362
+ );
4363
+ const parsed = JSON.parse(body.toString());
4364
+ if (parsed.messages && parsed.messages.length > 0 && shouldCompress(parsed.messages)) {
4365
+ const compressionResult = await compressContext(parsed.messages, {
4366
+ enabled: true,
4367
+ preserveRaw: false,
4368
+ // Don't need originals in proxy
4369
+ layers: {
4370
+ deduplication: true,
4371
+ // Safe: removes duplicate messages
4372
+ whitespace: true,
4373
+ // Safe: normalizes whitespace
4374
+ dictionary: false,
4375
+ // Disabled: requires model to understand codebook
4376
+ paths: false,
4377
+ // Disabled: requires model to understand path codes
4378
+ jsonCompact: true,
4379
+ // Safe: just removes JSON whitespace
4380
+ observation: false,
4381
+ // Disabled: may lose important context
4382
+ dynamicCodebook: false
4383
+ // Disabled: requires model to understand codes
4384
+ },
4385
+ dictionary: {
4386
+ maxEntries: 50,
4387
+ minPhraseLength: 15,
4388
+ includeCodebookHeader: false
4389
+ }
4390
+ });
4391
+ const compressedSizeKB = Math.ceil(compressionResult.compressedChars / 1024);
4392
+ const savings = ((requestSizeKB - compressedSizeKB) / requestSizeKB * 100).toFixed(1);
4393
+ console.log(
4394
+ `[ClawRouter] Compressed ${requestSizeKB}KB \u2192 ${compressedSizeKB}KB (${savings}% reduction)`
4395
+ );
4396
+ parsed.messages = compressionResult.messages;
4397
+ body = Buffer.from(JSON.stringify(parsed));
4398
+ }
4399
+ } catch (err) {
4400
+ console.warn(
4401
+ `[ClawRouter] Compression failed: ${err instanceof Error ? err.message : String(err)}`
4402
+ );
4403
+ }
4404
+ }
4405
+ const cacheKey = ResponseCache.generateKey(body);
4406
+ const reqHeaders = {};
4407
+ for (const [key, value] of Object.entries(req.headers)) {
4408
+ if (typeof value === "string") reqHeaders[key] = value;
4409
+ }
4410
+ if (responseCache.shouldCache(body, reqHeaders)) {
4411
+ const cachedResponse = responseCache.get(cacheKey);
4412
+ if (cachedResponse) {
4413
+ console.log(`[ClawRouter] Cache HIT for ${cachedResponse.model} (saved API call)`);
4414
+ res.writeHead(cachedResponse.status, cachedResponse.headers);
4415
+ res.end(cachedResponse.body);
4416
+ return;
4417
+ }
4418
+ }
4419
+ const dedupKey = RequestDeduplicator.hash(body);
4420
+ const cached = deduplicator.getCached(dedupKey);
4421
+ if (cached) {
4422
+ res.writeHead(cached.status, cached.headers);
4423
+ res.end(cached.body);
4424
+ return;
4425
+ }
4426
+ const inflight = deduplicator.getInflight(dedupKey);
4427
+ if (inflight) {
4428
+ const result = await inflight;
4429
+ res.writeHead(result.status, result.headers);
4430
+ res.end(result.body);
4431
+ return;
4432
+ }
4433
+ deduplicator.markInflight(dedupKey);
4434
+ let estimatedCostMicros;
4435
+ const isFreeModel = modelId === FREE_MODEL;
4436
+ if (modelId && !options.skipBalanceCheck && !isFreeModel) {
4437
+ const estimated = estimateAmount(modelId, body.length, maxTokens);
4438
+ if (estimated) {
4439
+ estimatedCostMicros = BigInt(estimated);
4440
+ const bufferedCostMicros = estimatedCostMicros * BigInt(Math.ceil(BALANCE_CHECK_BUFFER * 100)) / 100n;
4441
+ const sufficiency = await balanceMonitor.checkSufficient(bufferedCostMicros);
4442
+ if (sufficiency.info.isEmpty || !sufficiency.sufficient) {
4443
+ const originalModel = modelId;
4444
+ console.log(
4445
+ `[ClawRouter] Wallet ${sufficiency.info.isEmpty ? "empty" : "insufficient"} ($${sufficiency.info.balanceUSD}), falling back to free model: ${FREE_MODEL} (requested: ${originalModel})`
4446
+ );
4447
+ modelId = FREE_MODEL;
4448
+ const parsed = JSON.parse(body.toString());
4449
+ parsed.model = FREE_MODEL;
4450
+ body = Buffer.from(JSON.stringify(parsed));
4451
+ options.onLowBalance?.({
4452
+ balanceUSD: sufficiency.info.balanceUSD,
4453
+ walletAddress: sufficiency.info.walletAddress
4454
+ });
4455
+ } else if (sufficiency.info.isLow) {
4456
+ options.onLowBalance?.({
4457
+ balanceUSD: sufficiency.info.balanceUSD,
4458
+ walletAddress: sufficiency.info.walletAddress
4459
+ });
4460
+ }
4461
+ }
4462
+ }
4463
+ let heartbeatInterval;
4464
+ let headersSentEarly = false;
4465
+ if (isStreaming) {
4466
+ res.writeHead(200, {
4467
+ "content-type": "text/event-stream",
4468
+ "cache-control": "no-cache",
4469
+ connection: "keep-alive"
4470
+ });
4471
+ headersSentEarly = true;
4472
+ safeWrite(res, ": heartbeat\n\n");
4473
+ heartbeatInterval = setInterval(() => {
4474
+ if (canWrite(res)) {
4475
+ safeWrite(res, ": heartbeat\n\n");
4476
+ } else {
4477
+ clearInterval(heartbeatInterval);
4478
+ heartbeatInterval = void 0;
4479
+ }
4480
+ }, HEARTBEAT_INTERVAL_MS);
4481
+ }
4482
+ const headers = {};
4483
+ for (const [key, value] of Object.entries(req.headers)) {
4484
+ if (key === "host" || key === "connection" || key === "transfer-encoding" || key === "content-length")
4485
+ continue;
4486
+ if (typeof value === "string") {
4487
+ headers[key] = value;
4488
+ }
4489
+ }
4490
+ if (!headers["content-type"]) {
4491
+ headers["content-type"] = "application/json";
4492
+ }
4493
+ headers["user-agent"] = USER_AGENT;
4494
+ let completed = false;
4495
+ res.on("close", () => {
4496
+ if (heartbeatInterval) {
4497
+ clearInterval(heartbeatInterval);
4498
+ heartbeatInterval = void 0;
4499
+ }
4500
+ if (!completed) {
4501
+ deduplicator.removeInflight(dedupKey);
4502
+ }
4503
+ });
4504
+ const timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
4505
+ const controller = new AbortController();
4506
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
4507
+ try {
4508
+ let modelsToTry;
4509
+ if (routingDecision) {
4510
+ const estimatedInputTokens = Math.ceil(body.length / 4);
4511
+ const estimatedTotalTokens = estimatedInputTokens + maxTokens;
4512
+ const useAgenticTiers = routingDecision.reasoning?.includes("agentic") && routerOpts.config.agenticTiers;
4513
+ const tierConfigs = useAgenticTiers ? routerOpts.config.agenticTiers : routerOpts.config.tiers;
4514
+ const fullChain = getFallbackChain(routingDecision.tier, tierConfigs);
4515
+ const contextFiltered = getFallbackChainFiltered(
4516
+ routingDecision.tier,
4517
+ tierConfigs,
4518
+ estimatedTotalTokens,
4519
+ getModelContextWindow
4520
+ );
4521
+ const contextExcluded = fullChain.filter((m) => !contextFiltered.includes(m));
4522
+ if (contextExcluded.length > 0) {
4523
+ console.log(
4524
+ `[ClawRouter] Context filter (~${estimatedTotalTokens} tokens): excluded ${contextExcluded.join(", ")}`
4525
+ );
4526
+ }
4527
+ modelsToTry = contextFiltered.slice(0, MAX_FALLBACK_ATTEMPTS);
4528
+ modelsToTry = prioritizeNonRateLimited(modelsToTry);
4529
+ } else {
4530
+ if (modelId && modelId !== FREE_MODEL) {
4531
+ modelsToTry = [modelId, FREE_MODEL];
4532
+ } else {
4533
+ modelsToTry = modelId ? [modelId] : [];
4534
+ }
4535
+ }
4536
+ let upstream;
4537
+ let lastError;
4538
+ let actualModelUsed = modelId;
4539
+ for (let i = 0; i < modelsToTry.length; i++) {
4540
+ const tryModel = modelsToTry[i];
4541
+ const isLastAttempt = i === modelsToTry.length - 1;
4542
+ console.log(`[ClawRouter] Trying model ${i + 1}/${modelsToTry.length}: ${tryModel}`);
4543
+ const result = await tryModelRequest(
4544
+ upstreamUrl,
4545
+ req.method ?? "POST",
4546
+ headers,
4547
+ body,
4548
+ tryModel,
4549
+ maxTokens,
4550
+ payFetch,
4551
+ balanceMonitor,
4552
+ controller.signal
4553
+ );
4554
+ if (result.success && result.response) {
4555
+ upstream = result.response;
4556
+ actualModelUsed = tryModel;
4557
+ console.log(`[ClawRouter] Success with model: ${tryModel}`);
4558
+ break;
4559
+ }
4560
+ lastError = {
4561
+ body: result.errorBody || "Unknown error",
4562
+ status: result.errorStatus || 500
4563
+ };
4564
+ if (result.isProviderError && !isLastAttempt) {
4565
+ if (result.errorStatus === 429) {
4566
+ markRateLimited(tryModel);
4567
+ }
4568
+ console.log(
4569
+ `[ClawRouter] Provider error from ${tryModel}, trying fallback: ${result.errorBody?.slice(0, 100)}`
4570
+ );
4571
+ continue;
4572
+ }
4573
+ if (!result.isProviderError) {
4574
+ console.log(
4575
+ `[ClawRouter] Non-provider error from ${tryModel}, not retrying: ${result.errorBody?.slice(0, 100)}`
4576
+ );
4577
+ }
4578
+ break;
4579
+ }
4580
+ clearTimeout(timeoutId);
4581
+ if (heartbeatInterval) {
4582
+ clearInterval(heartbeatInterval);
4583
+ heartbeatInterval = void 0;
4584
+ }
4585
+ if (routingDecision && actualModelUsed !== routingDecision.model) {
4586
+ const estimatedInputTokens = Math.ceil(body.length / 4);
4587
+ const newCosts = calculateModelCost(
4588
+ actualModelUsed,
4589
+ routerOpts.modelPricing,
4590
+ estimatedInputTokens,
4591
+ maxTokens,
4592
+ routingProfile ?? void 0
4593
+ );
4594
+ routingDecision = {
4595
+ ...routingDecision,
4596
+ model: actualModelUsed,
4597
+ reasoning: `${routingDecision.reasoning} | fallback to ${actualModelUsed}`,
4598
+ costEstimate: newCosts.costEstimate,
4599
+ baselineCost: newCosts.baselineCost,
4600
+ savings: newCosts.savings
4601
+ };
4602
+ options.onRouted?.(routingDecision);
4603
+ }
4604
+ if (!upstream) {
4605
+ const rawErrBody = lastError?.body || "All models in fallback chain failed";
4606
+ const errStatus = lastError?.status || 502;
4607
+ const transformedErr = transformPaymentError(rawErrBody);
4608
+ if (headersSentEarly) {
4609
+ let errPayload;
4610
+ try {
4611
+ const parsed = JSON.parse(transformedErr);
4612
+ errPayload = JSON.stringify(parsed);
4613
+ } catch {
4614
+ errPayload = JSON.stringify({
4615
+ error: { message: rawErrBody, type: "provider_error", status: errStatus }
4616
+ });
4617
+ }
4618
+ const errEvent = `data: ${errPayload}
4619
+
4620
+ `;
4621
+ safeWrite(res, errEvent);
4622
+ safeWrite(res, "data: [DONE]\n\n");
4623
+ res.end();
4624
+ const errBuf = Buffer.from(errEvent + "data: [DONE]\n\n");
4625
+ deduplicator.complete(dedupKey, {
4626
+ status: 200,
4627
+ headers: { "content-type": "text/event-stream" },
4628
+ body: errBuf,
4629
+ completedAt: Date.now()
4630
+ });
4631
+ } else {
4632
+ res.writeHead(errStatus, { "Content-Type": "application/json" });
4633
+ res.end(transformedErr);
4634
+ deduplicator.complete(dedupKey, {
4635
+ status: errStatus,
4636
+ headers: { "content-type": "application/json" },
4637
+ body: Buffer.from(transformedErr),
4638
+ completedAt: Date.now()
4639
+ });
4640
+ }
4641
+ return;
4642
+ }
4643
+ const responseChunks = [];
4644
+ if (headersSentEarly) {
4645
+ if (upstream.body) {
4646
+ const reader = upstream.body.getReader();
4647
+ const chunks = [];
4648
+ try {
4649
+ while (true) {
4650
+ const { done, value } = await reader.read();
4651
+ if (done) break;
4652
+ chunks.push(value);
4653
+ }
4654
+ } finally {
4655
+ reader.releaseLock();
4656
+ }
4657
+ const jsonBody = Buffer.concat(chunks);
4658
+ const jsonStr = jsonBody.toString();
4659
+ try {
4660
+ const rsp = JSON.parse(jsonStr);
4661
+ const baseChunk = {
4662
+ id: rsp.id ?? `chatcmpl-${Date.now()}`,
4663
+ object: "chat.completion.chunk",
4664
+ created: rsp.created ?? Math.floor(Date.now() / 1e3),
4665
+ model: rsp.model ?? "unknown",
4666
+ system_fingerprint: null
4667
+ };
4668
+ if (rsp.choices && Array.isArray(rsp.choices)) {
4669
+ for (const choice of rsp.choices) {
4670
+ const rawContent = choice.message?.content ?? choice.delta?.content ?? "";
4671
+ const content = stripThinkingTokens(rawContent);
4672
+ const role = choice.message?.role ?? choice.delta?.role ?? "assistant";
4673
+ const index = choice.index ?? 0;
4674
+ if (content) {
4675
+ accumulatedContent += content;
4676
+ }
4677
+ const roleChunk = {
4678
+ ...baseChunk,
4679
+ choices: [{ index, delta: { role }, logprobs: null, finish_reason: null }]
4680
+ };
4681
+ const roleData = `data: ${JSON.stringify(roleChunk)}
4682
+
4683
+ `;
4684
+ safeWrite(res, roleData);
4685
+ responseChunks.push(Buffer.from(roleData));
4686
+ if (content) {
4687
+ const contentChunk = {
4688
+ ...baseChunk,
4689
+ choices: [{ index, delta: { content }, logprobs: null, finish_reason: null }]
4690
+ };
4691
+ const contentData = `data: ${JSON.stringify(contentChunk)}
4692
+
4693
+ `;
4694
+ safeWrite(res, contentData);
4695
+ responseChunks.push(Buffer.from(contentData));
4696
+ }
4697
+ const toolCalls = choice.message?.tool_calls ?? choice.delta?.tool_calls;
4698
+ if (toolCalls && toolCalls.length > 0) {
4699
+ const toolCallChunk = {
4700
+ ...baseChunk,
4701
+ choices: [
4702
+ {
4703
+ index,
4704
+ delta: { tool_calls: toolCalls },
4705
+ logprobs: null,
4706
+ finish_reason: null
4707
+ }
4708
+ ]
4709
+ };
4710
+ const toolCallData = `data: ${JSON.stringify(toolCallChunk)}
4711
+
4712
+ `;
4713
+ safeWrite(res, toolCallData);
4714
+ responseChunks.push(Buffer.from(toolCallData));
4715
+ }
4716
+ const finishChunk = {
4717
+ ...baseChunk,
4718
+ choices: [
4719
+ {
4720
+ index,
4721
+ delta: {},
4722
+ logprobs: null,
4723
+ finish_reason: toolCalls && toolCalls.length > 0 ? "tool_calls" : choice.finish_reason ?? "stop"
4724
+ }
4725
+ ]
4726
+ };
4727
+ const finishData = `data: ${JSON.stringify(finishChunk)}
4728
+
4729
+ `;
4730
+ safeWrite(res, finishData);
4731
+ responseChunks.push(Buffer.from(finishData));
4732
+ }
4733
+ }
4734
+ } catch {
4735
+ const sseData = `data: ${jsonStr}
4736
+
4737
+ `;
4738
+ safeWrite(res, sseData);
4739
+ responseChunks.push(Buffer.from(sseData));
4740
+ }
4741
+ }
4742
+ safeWrite(res, "data: [DONE]\n\n");
4743
+ responseChunks.push(Buffer.from("data: [DONE]\n\n"));
4744
+ res.end();
4745
+ deduplicator.complete(dedupKey, {
4746
+ status: 200,
4747
+ headers: { "content-type": "text/event-stream" },
4748
+ body: Buffer.concat(responseChunks),
4749
+ completedAt: Date.now()
4750
+ });
4751
+ } else {
4752
+ const responseHeaders = {};
4753
+ upstream.headers.forEach((value, key) => {
4754
+ if (key === "transfer-encoding" || key === "connection" || key === "content-encoding")
4755
+ return;
4756
+ responseHeaders[key] = value;
4757
+ });
4758
+ res.writeHead(upstream.status, responseHeaders);
4759
+ if (upstream.body) {
4760
+ const reader = upstream.body.getReader();
4761
+ try {
4762
+ while (true) {
4763
+ const { done, value } = await reader.read();
4764
+ if (done) break;
4765
+ const chunk = Buffer.from(value);
4766
+ safeWrite(res, chunk);
4767
+ responseChunks.push(chunk);
4768
+ }
4769
+ } finally {
4770
+ reader.releaseLock();
4771
+ }
4772
+ }
4773
+ res.end();
4774
+ const responseBody = Buffer.concat(responseChunks);
4775
+ deduplicator.complete(dedupKey, {
4776
+ status: upstream.status,
4777
+ headers: responseHeaders,
4778
+ body: responseBody,
4779
+ completedAt: Date.now()
4780
+ });
4781
+ if (upstream.status === 200 && responseCache.shouldCache(body)) {
4782
+ responseCache.set(cacheKey, {
4783
+ body: responseBody,
4784
+ status: upstream.status,
4785
+ headers: responseHeaders,
4786
+ model: modelId
4787
+ });
4788
+ console.log(`[ClawRouter] Cached response for ${modelId} (${responseBody.length} bytes)`);
4789
+ }
4790
+ try {
4791
+ const rspJson = JSON.parse(responseBody.toString());
4792
+ if (rspJson.choices?.[0]?.message?.content) {
4793
+ accumulatedContent = rspJson.choices[0].message.content;
4794
+ }
4795
+ } catch {
4796
+ }
4797
+ }
4798
+ if (sessionId && accumulatedContent) {
4799
+ const events = sessionJournal.extractEvents(accumulatedContent);
4800
+ if (events.length > 0) {
4801
+ sessionJournal.record(sessionId, events, actualModelUsed);
4802
+ console.log(
4803
+ `[ClawRouter] Recorded ${events.length} events to session journal for session ${sessionId.slice(0, 8)}...`
4804
+ );
4805
+ }
4806
+ }
4807
+ if (estimatedCostMicros !== void 0) {
4808
+ balanceMonitor.deductEstimated(estimatedCostMicros);
4809
+ }
4810
+ completed = true;
4811
+ } catch (err) {
4812
+ clearTimeout(timeoutId);
4813
+ if (heartbeatInterval) {
4814
+ clearInterval(heartbeatInterval);
4815
+ heartbeatInterval = void 0;
4816
+ }
4817
+ deduplicator.removeInflight(dedupKey);
4818
+ balanceMonitor.invalidate();
4819
+ if (err instanceof Error && err.name === "AbortError") {
4820
+ throw new Error(`Request timed out after ${timeoutMs}ms`);
4821
+ }
4822
+ throw err;
4823
+ }
4824
+ if (routingDecision) {
4825
+ const estimatedInputTokens = Math.ceil(body.length / 4);
4826
+ const accurateCosts = calculateModelCost(
4827
+ routingDecision.model,
4828
+ routerOpts.modelPricing,
4829
+ estimatedInputTokens,
4830
+ maxTokens,
4831
+ routingProfile ?? void 0
4832
+ );
4833
+ const costWithBuffer = accurateCosts.costEstimate * 1.2;
4834
+ const baselineWithBuffer = accurateCosts.baselineCost * 1.2;
4835
+ const entry = {
4836
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4837
+ model: routingDecision.model,
4838
+ tier: routingDecision.tier,
4839
+ cost: costWithBuffer,
4840
+ baselineCost: baselineWithBuffer,
4841
+ savings: accurateCosts.savings,
4842
+ latencyMs: Date.now() - startTime
4843
+ };
4844
+ logUsage(entry).catch(() => {
4845
+ });
4846
+ }
4847
+ }
4848
+
4849
+ // src/auth.ts
4850
+ import { writeFile, readFile as readFile2, mkdir as mkdir2 } from "fs/promises";
4851
+ import { join as join4 } from "path";
4852
+ import { homedir as homedir3 } from "os";
4853
+ import { generatePrivateKey, privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
4854
+ var WALLET_DIR = join4(homedir3(), ".openclaw", "blockrun");
4855
+ var WALLET_FILE = join4(WALLET_DIR, "wallet.key");
4856
+ async function loadSavedWallet() {
4857
+ try {
4858
+ const key = (await readFile2(WALLET_FILE, "utf-8")).trim();
4859
+ if (key.startsWith("0x") && key.length === 66) {
4860
+ console.log(`[ClawRouter] \u2713 Loaded existing wallet from ${WALLET_FILE}`);
4861
+ return key;
4862
+ }
4863
+ console.warn(`[ClawRouter] \u26A0 Wallet file exists but is invalid (wrong format)`);
4864
+ } catch (err) {
4865
+ if (err.code !== "ENOENT") {
4866
+ console.error(
4867
+ `[ClawRouter] \u2717 Failed to read wallet file: ${err instanceof Error ? err.message : String(err)}`
4868
+ );
4869
+ }
4870
+ }
4871
+ return void 0;
4872
+ }
4873
+ async function generateAndSaveWallet() {
4874
+ const key = generatePrivateKey();
4875
+ const account = privateKeyToAccount3(key);
4876
+ await mkdir2(WALLET_DIR, { recursive: true });
4877
+ await writeFile(WALLET_FILE, key + "\n", { mode: 384 });
4878
+ try {
4879
+ const verification = (await readFile2(WALLET_FILE, "utf-8")).trim();
4880
+ if (verification !== key) {
4881
+ throw new Error("Wallet file verification failed - content mismatch");
4882
+ }
4883
+ console.log(`[ClawRouter] \u2713 Wallet saved and verified at ${WALLET_FILE}`);
4884
+ } catch (err) {
4885
+ throw new Error(
4886
+ `Failed to verify wallet file after creation: ${err instanceof Error ? err.message : String(err)}`
4887
+ );
4888
+ }
4889
+ return { key, address: account.address };
4890
+ }
4891
+ async function resolveOrGenerateWalletKey() {
4892
+ const saved = await loadSavedWallet();
4893
+ if (saved) {
4894
+ const account = privateKeyToAccount3(saved);
4895
+ return { key: saved, address: account.address, source: "saved" };
4896
+ }
4897
+ const envKey = process.env.BLOCKRUN_WALLET_KEY;
4898
+ if (typeof envKey === "string" && envKey.startsWith("0x") && envKey.length === 66) {
4899
+ const account = privateKeyToAccount3(envKey);
4900
+ return { key: envKey, address: account.address, source: "env" };
4901
+ }
4902
+ const { key, address } = await generateAndSaveWallet();
4903
+ return { key, address, source: "generated" };
4904
+ }
4905
+
4906
+ // src/cli.ts
4907
+ function printHelp() {
4908
+ console.log(`
4909
+ ClawRouter v${VERSION} - Smart LLM Router
4910
+
4911
+ Usage:
4912
+ clawrouter [options]
4913
+
4914
+ Options:
4915
+ --version, -v Show version number
4916
+ --help, -h Show this help message
4917
+ --port <number> Port to listen on (default: ${getProxyPort()})
4918
+
4919
+ Examples:
4920
+ # Start standalone proxy (survives gateway restarts)
4921
+ npx @blockrun/clawrouter
4922
+
4923
+ # Start on custom port
4924
+ npx @blockrun/clawrouter --port 9000
4925
+
4926
+ # Production deployment with PM2
4927
+ pm2 start "npx @blockrun/clawrouter" --name clawrouter
4928
+
4929
+ Environment Variables:
4930
+ BLOCKRUN_WALLET_KEY Private key for x402 payments (auto-generated if not set)
4931
+ BLOCKRUN_PROXY_PORT Default proxy port (default: 8402)
4932
+
4933
+ For more info: https://github.com/BlockRunAI/ClawRouter
4934
+ `);
4935
+ }
4936
+ function parseArgs(args) {
4937
+ const result = { version: false, help: false, port: void 0 };
4938
+ for (let i = 0; i < args.length; i++) {
4939
+ const arg = args[i];
4940
+ if (arg === "--version" || arg === "-v") {
4941
+ result.version = true;
4942
+ } else if (arg === "--help" || arg === "-h") {
4943
+ result.help = true;
4944
+ } else if (arg === "--port" && args[i + 1]) {
4945
+ result.port = parseInt(args[i + 1], 10);
4946
+ i++;
4947
+ }
4948
+ }
4949
+ return result;
4950
+ }
4951
+ async function main() {
4952
+ const args = parseArgs(process.argv.slice(2));
4953
+ if (args.version) {
4954
+ console.log(VERSION);
4955
+ process.exit(0);
4956
+ }
4957
+ if (args.help) {
4958
+ printHelp();
4959
+ process.exit(0);
4960
+ }
4961
+ const { key: walletKey, address, source } = await resolveOrGenerateWalletKey();
4962
+ if (source === "generated") {
4963
+ console.log(`[ClawRouter] Generated new wallet: ${address}`);
4964
+ } else if (source === "saved") {
4965
+ console.log(`[ClawRouter] Using saved wallet: ${address}`);
4966
+ } else {
4967
+ console.log(`[ClawRouter] Using wallet from BLOCKRUN_WALLET_KEY: ${address}`);
4968
+ }
4969
+ const proxy = await startProxy({
4970
+ walletKey,
4971
+ port: args.port,
4972
+ onReady: (port) => {
4973
+ console.log(`[ClawRouter] Proxy listening on http://127.0.0.1:${port}`);
4974
+ console.log(`[ClawRouter] Health check: http://127.0.0.1:${port}/health`);
4975
+ },
4976
+ onError: (error) => {
4977
+ console.error(`[ClawRouter] Error: ${error.message}`);
4978
+ },
4979
+ onRouted: (decision) => {
4980
+ const cost = decision.costEstimate.toFixed(4);
4981
+ const saved = (decision.savings * 100).toFixed(0);
4982
+ console.log(`[ClawRouter] [${decision.tier}] ${decision.model} $${cost} (saved ${saved}%)`);
4983
+ },
4984
+ onLowBalance: (info) => {
4985
+ console.warn(`[ClawRouter] Low balance: ${info.balanceUSD}. Fund: ${info.walletAddress}`);
4986
+ },
4987
+ onInsufficientFunds: (info) => {
4988
+ console.error(
4989
+ `[ClawRouter] Insufficient funds. Balance: ${info.balanceUSD}, Need: ${info.requiredUSD}`
4990
+ );
4991
+ }
4992
+ });
4993
+ const monitor = new BalanceMonitor(address);
4994
+ try {
4995
+ const balance = await monitor.checkBalance();
4996
+ if (balance.isEmpty) {
4997
+ console.log(`[ClawRouter] Wallet balance: $0.00 (using FREE model)`);
4998
+ console.log(`[ClawRouter] Fund wallet for premium models: ${address}`);
4999
+ } else if (balance.isLow) {
5000
+ console.log(`[ClawRouter] Wallet balance: ${balance.balanceUSD} (low)`);
5001
+ } else {
5002
+ console.log(`[ClawRouter] Wallet balance: ${balance.balanceUSD}`);
5003
+ }
5004
+ } catch {
5005
+ console.log(`[ClawRouter] Wallet: ${address} (balance check pending)`);
5006
+ }
5007
+ console.log(`[ClawRouter] Ready - Ctrl+C to stop`);
5008
+ const shutdown = async (signal) => {
5009
+ console.log(`
5010
+ [ClawRouter] Received ${signal}, shutting down...`);
5011
+ try {
5012
+ await proxy.close();
5013
+ console.log(`[ClawRouter] Proxy closed`);
5014
+ process.exit(0);
5015
+ } catch (err) {
5016
+ console.error(`[ClawRouter] Error during shutdown: ${err}`);
5017
+ process.exit(1);
5018
+ }
5019
+ };
5020
+ process.on("SIGINT", () => shutdown("SIGINT"));
5021
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
5022
+ await new Promise(() => {
5023
+ });
5024
+ }
5025
+ main().catch((err) => {
5026
+ console.error(`[ClawRouter] Fatal error: ${err.message}`);
5027
+ process.exit(1);
5028
+ });
5029
+ //# sourceMappingURL=cli.js.map