@blockrun/clawrouter 0.11.13 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -11,13 +11,13 @@ import { x402Client } from "@x402/fetch";
11
11
  // src/payment-preauth.ts
12
12
  import { x402HTTPClient } from "@x402/fetch";
13
13
  var DEFAULT_TTL_MS = 36e5;
14
- function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS) {
14
+ function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS, options) {
15
15
  const httpClient = new x402HTTPClient(client);
16
16
  const cache = /* @__PURE__ */ new Map();
17
17
  return async (input, init) => {
18
18
  const request = new Request(input, init);
19
19
  const urlPath = new URL(request.url).pathname;
20
- const cached = cache.get(urlPath);
20
+ const cached = !options?.skipPreAuth ? cache.get(urlPath) : void 0;
21
21
  if (cached && Date.now() - cached.cachedAt < ttlMs) {
22
22
  try {
23
23
  const payload2 = await client.createPaymentPayload(cached.paymentRequired);
@@ -53,7 +53,8 @@ function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS) {
53
53
  cache.set(urlPath, { paymentRequired, cachedAt: Date.now() });
54
54
  } catch (error) {
55
55
  throw new Error(
56
- `Failed to parse payment requirements: ${error instanceof Error ? error.message : "Unknown error"}`
56
+ `Failed to parse payment requirements: ${error instanceof Error ? error.message : "Unknown error"}`,
57
+ { cause: error }
57
58
  );
58
59
  }
59
60
  const payload = await client.createPaymentPayload(paymentRequired);
@@ -157,20 +158,18 @@ function scoreAgenticTask(text, keywords) {
157
158
  };
158
159
  }
159
160
  function classifyByRules(prompt, systemPrompt, estimatedTokens, config) {
160
- const text = `${systemPrompt ?? ""} ${prompt}`.toLowerCase();
161
161
  const userText = prompt.toLowerCase();
162
162
  const dimensions = [
163
- // Original 8 dimensions
163
+ // Token count uses total estimated tokens (system + user) — context size matters for model selection
164
164
  scoreTokenCount(estimatedTokens, config.tokenCountThresholds),
165
165
  scoreKeywordMatch(
166
- text,
166
+ userText,
167
167
  config.codeKeywords,
168
168
  "codePresence",
169
169
  "code",
170
170
  { low: 1, high: 2 },
171
171
  { none: 0, low: 0.5, high: 1 }
172
172
  ),
173
- // Reasoning markers use USER prompt only — system prompt "step by step" shouldn't trigger reasoning
174
173
  scoreKeywordMatch(
175
174
  userText,
176
175
  config.reasoningKeywords,
@@ -180,7 +179,7 @@ function classifyByRules(prompt, systemPrompt, estimatedTokens, config) {
180
179
  { none: 0, low: 0.7, high: 1 }
181
180
  ),
182
181
  scoreKeywordMatch(
183
- text,
182
+ userText,
184
183
  config.technicalKeywords,
185
184
  "technicalTerms",
186
185
  "technical",
@@ -188,7 +187,7 @@ function classifyByRules(prompt, systemPrompt, estimatedTokens, config) {
188
187
  { none: 0, low: 0.5, high: 1 }
189
188
  ),
190
189
  scoreKeywordMatch(
191
- text,
190
+ userText,
192
191
  config.creativeKeywords,
193
192
  "creativeMarkers",
194
193
  "creative",
@@ -196,18 +195,18 @@ function classifyByRules(prompt, systemPrompt, estimatedTokens, config) {
196
195
  { none: 0, low: 0.5, high: 0.7 }
197
196
  ),
198
197
  scoreKeywordMatch(
199
- text,
198
+ userText,
200
199
  config.simpleKeywords,
201
200
  "simpleIndicators",
202
201
  "simple",
203
202
  { low: 1, high: 2 },
204
203
  { none: 0, low: -1, high: -1 }
205
204
  ),
206
- scoreMultiStep(text),
205
+ scoreMultiStep(userText),
207
206
  scoreQuestionComplexity(prompt),
208
207
  // 6 new dimensions
209
208
  scoreKeywordMatch(
210
- text,
209
+ userText,
211
210
  config.imperativeVerbs,
212
211
  "imperativeVerbs",
213
212
  "imperative",
@@ -215,7 +214,7 @@ function classifyByRules(prompt, systemPrompt, estimatedTokens, config) {
215
214
  { none: 0, low: 0.3, high: 0.5 }
216
215
  ),
217
216
  scoreKeywordMatch(
218
- text,
217
+ userText,
219
218
  config.constraintIndicators,
220
219
  "constraintCount",
221
220
  "constraints",
@@ -223,7 +222,7 @@ function classifyByRules(prompt, systemPrompt, estimatedTokens, config) {
223
222
  { none: 0, low: 0.3, high: 0.7 }
224
223
  ),
225
224
  scoreKeywordMatch(
226
- text,
225
+ userText,
227
226
  config.outputFormatKeywords,
228
227
  "outputFormat",
229
228
  "format",
@@ -231,7 +230,7 @@ function classifyByRules(prompt, systemPrompt, estimatedTokens, config) {
231
230
  { none: 0, low: 0.4, high: 0.7 }
232
231
  ),
233
232
  scoreKeywordMatch(
234
- text,
233
+ userText,
235
234
  config.referenceKeywords,
236
235
  "referenceComplexity",
237
236
  "references",
@@ -239,7 +238,7 @@ function classifyByRules(prompt, systemPrompt, estimatedTokens, config) {
239
238
  { none: 0, low: 0.3, high: 0.5 }
240
239
  ),
241
240
  scoreKeywordMatch(
242
- text,
241
+ userText,
243
242
  config.negationKeywords,
244
243
  "negationComplexity",
245
244
  "negation",
@@ -247,7 +246,7 @@ function classifyByRules(prompt, systemPrompt, estimatedTokens, config) {
247
246
  { none: 0, low: 0.3, high: 0.5 }
248
247
  ),
249
248
  scoreKeywordMatch(
250
- text,
249
+ userText,
251
250
  config.domainSpecificKeywords,
252
251
  "domainSpecificity",
253
252
  "domain-specific",
@@ -279,7 +278,8 @@ function classifyByRules(prompt, systemPrompt, estimatedTokens, config) {
279
278
  tier: "REASONING",
280
279
  confidence: Math.max(confidence2, 0.85),
281
280
  signals,
282
- agenticScore
281
+ agenticScore,
282
+ dimensions
283
283
  };
284
284
  }
285
285
  const { simpleMedium, mediumComplex, complexReasoning } = config.tierBoundaries;
@@ -303,9 +303,9 @@ function classifyByRules(prompt, systemPrompt, estimatedTokens, config) {
303
303
  }
304
304
  const confidence = calibrateConfidence(distanceFromBoundary, config.confidenceSteepness);
305
305
  if (confidence < config.confidenceThreshold) {
306
- return { score: weightedScore, tier: null, confidence, signals, agenticScore };
306
+ return { score: weightedScore, tier: null, confidence, signals, agenticScore, dimensions };
307
307
  }
308
- return { score: weightedScore, tier, confidence, signals, agenticScore };
308
+ return { score: weightedScore, tier, confidence, signals, agenticScore, dimensions };
309
309
  }
310
310
  function calibrateConfidence(distance, steepness) {
311
311
  return 1 / (1 + Math.exp(-steepness * distance));
@@ -313,7 +313,9 @@ function calibrateConfidence(distance, steepness) {
313
313
 
314
314
  // src/router/selector.ts
315
315
  var BASELINE_MODEL_ID = "anthropic/claude-opus-4.6";
316
- function selectModel(tier, confidence, method, reasoning, tierConfigs, modelPricing, estimatedInputTokens, maxOutputTokens, routingProfile) {
316
+ var BASELINE_INPUT_PRICE = 5;
317
+ var BASELINE_OUTPUT_PRICE = 25;
318
+ function selectModel(tier, confidence, method, reasoning, tierConfigs, modelPricing, estimatedInputTokens, maxOutputTokens, routingProfile, agenticScore) {
317
319
  const tierConfig = tierConfigs[tier];
318
320
  const model = tierConfig.primary;
319
321
  const pricing = modelPricing.get(model);
@@ -323,8 +325,8 @@ function selectModel(tier, confidence, method, reasoning, tierConfigs, modelPric
323
325
  const outputCost = maxOutputTokens / 1e6 * outputPrice;
324
326
  const costEstimate = inputCost + outputCost;
325
327
  const opusPricing = modelPricing.get(BASELINE_MODEL_ID);
326
- const opusInputPrice = opusPricing?.inputPrice ?? 0;
327
- const opusOutputPrice = opusPricing?.outputPrice ?? 0;
328
+ const opusInputPrice = opusPricing?.inputPrice ?? BASELINE_INPUT_PRICE;
329
+ const opusOutputPrice = opusPricing?.outputPrice ?? BASELINE_OUTPUT_PRICE;
328
330
  const baselineInput = estimatedInputTokens / 1e6 * opusInputPrice;
329
331
  const baselineOutput = maxOutputTokens / 1e6 * opusOutputPrice;
330
332
  const baselineCost = baselineInput + baselineOutput;
@@ -337,7 +339,8 @@ function selectModel(tier, confidence, method, reasoning, tierConfigs, modelPric
337
339
  reasoning,
338
340
  costEstimate,
339
341
  baselineCost,
340
- savings
342
+ savings,
343
+ ...agenticScore !== void 0 && { agenticScore }
341
344
  };
342
345
  }
343
346
  function getFallbackChain(tier, tierConfigs) {
@@ -352,14 +355,24 @@ function calculateModelCost(model, modelPricing, estimatedInputTokens, maxOutput
352
355
  const outputCost = maxOutputTokens / 1e6 * outputPrice;
353
356
  const costEstimate = inputCost + outputCost;
354
357
  const opusPricing = modelPricing.get(BASELINE_MODEL_ID);
355
- const opusInputPrice = opusPricing?.inputPrice ?? 0;
356
- const opusOutputPrice = opusPricing?.outputPrice ?? 0;
358
+ const opusInputPrice = opusPricing?.inputPrice ?? BASELINE_INPUT_PRICE;
359
+ const opusOutputPrice = opusPricing?.outputPrice ?? BASELINE_OUTPUT_PRICE;
357
360
  const baselineInput = estimatedInputTokens / 1e6 * opusInputPrice;
358
361
  const baselineOutput = maxOutputTokens / 1e6 * opusOutputPrice;
359
362
  const baselineCost = baselineInput + baselineOutput;
360
363
  const savings = routingProfile === "premium" ? 0 : baselineCost > 0 ? Math.max(0, (baselineCost - costEstimate) / baselineCost) : 0;
361
364
  return { costEstimate, baselineCost, savings };
362
365
  }
366
+ function filterByToolCalling(models, hasTools, supportsToolCalling2) {
367
+ if (!hasTools) return models;
368
+ const filtered = models.filter(supportsToolCalling2);
369
+ return filtered.length > 0 ? filtered : models;
370
+ }
371
+ function filterByVision(models, hasVision, supportsVision2) {
372
+ if (!hasVision) return models;
373
+ const filtered = models.filter(supportsVision2);
374
+ return filtered.length > 0 ? filtered : models;
375
+ }
363
376
  function getFallbackChainFiltered(tier, tierConfigs, estimatedTotalTokens, getContextWindow) {
364
377
  const fullChain = getFallbackChain(tier, tierConfigs);
365
378
  const filtered = fullChain.filter((modelId) => {
@@ -1415,18 +1428,18 @@ var DEFAULT_ROUTING_CONFIG = {
1415
1428
  ]
1416
1429
  },
1417
1430
  MEDIUM: {
1418
- primary: "xai/grok-code-fast-1",
1419
- // Code specialist, $0.20/$1.50
1431
+ primary: "moonshot/kimi-k2.5",
1432
+ // $0.50/$2.40 - strong tool use, proper function call format
1420
1433
  fallback: [
1434
+ "deepseek/deepseek-chat",
1421
1435
  "google/gemini-2.5-flash-lite",
1422
1436
  // 1M context, ultra cheap ($0.10/$0.40)
1423
- "deepseek/deepseek-chat",
1424
1437
  "xai/grok-4-1-fast-non-reasoning"
1425
1438
  // Upgraded Grok 4.1
1426
1439
  ]
1427
1440
  },
1428
1441
  COMPLEX: {
1429
- primary: "google/gemini-3.1-pro-preview",
1442
+ primary: "google/gemini-3.1-pro",
1430
1443
  // Newest Gemini 3.1 - upgraded from 3.0
1431
1444
  fallback: [
1432
1445
  "google/gemini-2.5-flash-lite",
@@ -1486,7 +1499,7 @@ var DEFAULT_ROUTING_CONFIG = {
1486
1499
  fallback: [
1487
1500
  "anthropic/claude-haiku-4.5",
1488
1501
  "google/gemini-2.5-flash-lite",
1489
- "xai/grok-code-fast-1"
1502
+ "deepseek/deepseek-chat"
1490
1503
  ]
1491
1504
  },
1492
1505
  MEDIUM: {
@@ -1506,7 +1519,7 @@ var DEFAULT_ROUTING_CONFIG = {
1506
1519
  "openai/gpt-5.2-codex",
1507
1520
  "anthropic/claude-opus-4.6",
1508
1521
  "anthropic/claude-sonnet-4.6",
1509
- "google/gemini-3.1-pro-preview",
1522
+ "google/gemini-3.1-pro",
1510
1523
  // Newest Gemini
1511
1524
  "google/gemini-3-pro-preview",
1512
1525
  "moonshot/kimi-k2.5"
@@ -1537,9 +1550,13 @@ var DEFAULT_ROUTING_CONFIG = {
1537
1550
  ]
1538
1551
  },
1539
1552
  MEDIUM: {
1540
- primary: "xai/grok-code-fast-1",
1541
- // Code specialist for agentic coding
1542
- fallback: ["moonshot/kimi-k2.5", "anthropic/claude-haiku-4.5", "claude-sonnet-4"]
1553
+ primary: "moonshot/kimi-k2.5",
1554
+ // $0.50/$2.40 - strong tool use, handles function calls correctly
1555
+ fallback: [
1556
+ "anthropic/claude-haiku-4.5",
1557
+ "deepseek/deepseek-chat",
1558
+ "xai/grok-4-1-fast-non-reasoning"
1559
+ ]
1543
1560
  },
1544
1561
  COMPLEX: {
1545
1562
  primary: "anthropic/claude-sonnet-4.6",
@@ -1547,7 +1564,7 @@ var DEFAULT_ROUTING_CONFIG = {
1547
1564
  "anthropic/claude-opus-4.6",
1548
1565
  // Latest Opus - best agentic
1549
1566
  "openai/gpt-5.2",
1550
- "google/gemini-3.1-pro-preview",
1567
+ "google/gemini-3.1-pro",
1551
1568
  // Newest Gemini
1552
1569
  "google/gemini-3-pro-preview",
1553
1570
  "xai/grok-4-0709"
@@ -1579,7 +1596,7 @@ function route(prompt, systemPrompt, maxOutputTokens, options) {
1579
1596
  const ruleResult = classifyByRules(prompt, systemPrompt, estimatedTokens, config.scoring);
1580
1597
  const { routingProfile } = options;
1581
1598
  let tierConfigs;
1582
- let profileSuffix = "";
1599
+ let profileSuffix;
1583
1600
  if (routingProfile === "eco" && config.ecoTiers) {
1584
1601
  tierConfigs = config.ecoTiers;
1585
1602
  profileSuffix = " | eco";
@@ -1594,6 +1611,7 @@ function route(prompt, systemPrompt, maxOutputTokens, options) {
1594
1611
  tierConfigs = useAgenticTiers ? config.agenticTiers : config.tiers;
1595
1612
  profileSuffix = useAgenticTiers ? " | agentic" : "";
1596
1613
  }
1614
+ const agenticScoreValue = ruleResult.agenticScore;
1597
1615
  if (estimatedTokens > config.overrides.maxTokensForceComplex) {
1598
1616
  return selectModel(
1599
1617
  "COMPLEX",
@@ -1604,7 +1622,8 @@ function route(prompt, systemPrompt, maxOutputTokens, options) {
1604
1622
  modelPricing,
1605
1623
  estimatedTokens,
1606
1624
  maxOutputTokens,
1607
- routingProfile
1625
+ routingProfile,
1626
+ agenticScoreValue
1608
1627
  );
1609
1628
  }
1610
1629
  const hasStructuredOutput = systemPrompt ? /json|structured|schema/i.test(systemPrompt) : false;
@@ -1638,7 +1657,8 @@ function route(prompt, systemPrompt, maxOutputTokens, options) {
1638
1657
  modelPricing,
1639
1658
  estimatedTokens,
1640
1659
  maxOutputTokens,
1641
- routingProfile
1660
+ routingProfile,
1661
+ agenticScoreValue
1642
1662
  );
1643
1663
  }
1644
1664
 
@@ -1686,6 +1706,8 @@ var MODEL_ALIASES = {
1686
1706
  // Google
1687
1707
  gemini: "google/gemini-2.5-pro",
1688
1708
  flash: "google/gemini-2.5-flash",
1709
+ "gemini-3.1-pro-preview": "google/gemini-3.1-pro",
1710
+ "google/gemini-3.1-pro-preview": "google/gemini-3.1-pro",
1689
1711
  // xAI
1690
1712
  grok: "xai/grok-3",
1691
1713
  "grok-fast": "xai/grok-4-fast-reasoning",
@@ -1759,7 +1781,8 @@ var BLOCKRUN_MODELS = [
1759
1781
  maxOutput: 128e3,
1760
1782
  reasoning: true,
1761
1783
  vision: true,
1762
- agentic: true
1784
+ agentic: true,
1785
+ toolCalling: true
1763
1786
  },
1764
1787
  {
1765
1788
  id: "openai/gpt-5-mini",
@@ -1768,7 +1791,8 @@ var BLOCKRUN_MODELS = [
1768
1791
  inputPrice: 0.25,
1769
1792
  outputPrice: 2,
1770
1793
  contextWindow: 2e5,
1771
- maxOutput: 65536
1794
+ maxOutput: 65536,
1795
+ toolCalling: true
1772
1796
  },
1773
1797
  {
1774
1798
  id: "openai/gpt-5-nano",
@@ -1777,7 +1801,8 @@ var BLOCKRUN_MODELS = [
1777
1801
  inputPrice: 0.05,
1778
1802
  outputPrice: 0.4,
1779
1803
  contextWindow: 128e3,
1780
- maxOutput: 32768
1804
+ maxOutput: 32768,
1805
+ toolCalling: true
1781
1806
  },
1782
1807
  {
1783
1808
  id: "openai/gpt-5.2-pro",
@@ -1787,7 +1812,8 @@ var BLOCKRUN_MODELS = [
1787
1812
  outputPrice: 168,
1788
1813
  contextWindow: 4e5,
1789
1814
  maxOutput: 128e3,
1790
- reasoning: true
1815
+ reasoning: true,
1816
+ toolCalling: true
1791
1817
  },
1792
1818
  // OpenAI Codex Family
1793
1819
  {
@@ -1798,7 +1824,8 @@ var BLOCKRUN_MODELS = [
1798
1824
  outputPrice: 14,
1799
1825
  contextWindow: 128e3,
1800
1826
  maxOutput: 32e3,
1801
- agentic: true
1827
+ agentic: true,
1828
+ toolCalling: true
1802
1829
  },
1803
1830
  // OpenAI GPT-4 Family
1804
1831
  {
@@ -1809,7 +1836,8 @@ var BLOCKRUN_MODELS = [
1809
1836
  outputPrice: 8,
1810
1837
  contextWindow: 128e3,
1811
1838
  maxOutput: 16384,
1812
- vision: true
1839
+ vision: true,
1840
+ toolCalling: true
1813
1841
  },
1814
1842
  {
1815
1843
  id: "openai/gpt-4.1-mini",
@@ -1818,7 +1846,8 @@ var BLOCKRUN_MODELS = [
1818
1846
  inputPrice: 0.4,
1819
1847
  outputPrice: 1.6,
1820
1848
  contextWindow: 128e3,
1821
- maxOutput: 16384
1849
+ maxOutput: 16384,
1850
+ toolCalling: true
1822
1851
  },
1823
1852
  {
1824
1853
  id: "openai/gpt-4.1-nano",
@@ -1827,7 +1856,8 @@ var BLOCKRUN_MODELS = [
1827
1856
  inputPrice: 0.1,
1828
1857
  outputPrice: 0.4,
1829
1858
  contextWindow: 128e3,
1830
- maxOutput: 16384
1859
+ maxOutput: 16384,
1860
+ toolCalling: true
1831
1861
  },
1832
1862
  {
1833
1863
  id: "openai/gpt-4o",
@@ -1838,7 +1868,8 @@ var BLOCKRUN_MODELS = [
1838
1868
  contextWindow: 128e3,
1839
1869
  maxOutput: 16384,
1840
1870
  vision: true,
1841
- agentic: true
1871
+ agentic: true,
1872
+ toolCalling: true
1842
1873
  },
1843
1874
  {
1844
1875
  id: "openai/gpt-4o-mini",
@@ -1847,7 +1878,8 @@ var BLOCKRUN_MODELS = [
1847
1878
  inputPrice: 0.15,
1848
1879
  outputPrice: 0.6,
1849
1880
  contextWindow: 128e3,
1850
- maxOutput: 16384
1881
+ maxOutput: 16384,
1882
+ toolCalling: true
1851
1883
  },
1852
1884
  // OpenAI O-series (Reasoning)
1853
1885
  {
@@ -1858,7 +1890,8 @@ var BLOCKRUN_MODELS = [
1858
1890
  outputPrice: 60,
1859
1891
  contextWindow: 2e5,
1860
1892
  maxOutput: 1e5,
1861
- reasoning: true
1893
+ reasoning: true,
1894
+ toolCalling: true
1862
1895
  },
1863
1896
  {
1864
1897
  id: "openai/o1-mini",
@@ -1868,7 +1901,8 @@ var BLOCKRUN_MODELS = [
1868
1901
  outputPrice: 4.4,
1869
1902
  contextWindow: 128e3,
1870
1903
  maxOutput: 65536,
1871
- reasoning: true
1904
+ reasoning: true,
1905
+ toolCalling: true
1872
1906
  },
1873
1907
  {
1874
1908
  id: "openai/o3",
@@ -1878,7 +1912,8 @@ var BLOCKRUN_MODELS = [
1878
1912
  outputPrice: 8,
1879
1913
  contextWindow: 2e5,
1880
1914
  maxOutput: 1e5,
1881
- reasoning: true
1915
+ reasoning: true,
1916
+ toolCalling: true
1882
1917
  },
1883
1918
  {
1884
1919
  id: "openai/o3-mini",
@@ -1888,7 +1923,8 @@ var BLOCKRUN_MODELS = [
1888
1923
  outputPrice: 4.4,
1889
1924
  contextWindow: 128e3,
1890
1925
  maxOutput: 65536,
1891
- reasoning: true
1926
+ reasoning: true,
1927
+ toolCalling: true
1892
1928
  },
1893
1929
  {
1894
1930
  id: "openai/o4-mini",
@@ -1898,7 +1934,8 @@ var BLOCKRUN_MODELS = [
1898
1934
  outputPrice: 4.4,
1899
1935
  contextWindow: 128e3,
1900
1936
  maxOutput: 65536,
1901
- reasoning: true
1937
+ reasoning: true,
1938
+ toolCalling: true
1902
1939
  },
1903
1940
  // Anthropic - all Claude models excel at agentic workflows
1904
1941
  // Use newest versions (4.6) with full provider prefix
@@ -1910,7 +1947,9 @@ var BLOCKRUN_MODELS = [
1910
1947
  outputPrice: 5,
1911
1948
  contextWindow: 2e5,
1912
1949
  maxOutput: 8192,
1913
- agentic: true
1950
+ vision: true,
1951
+ agentic: true,
1952
+ toolCalling: true
1914
1953
  },
1915
1954
  {
1916
1955
  id: "anthropic/claude-sonnet-4.6",
@@ -1921,7 +1960,9 @@ var BLOCKRUN_MODELS = [
1921
1960
  contextWindow: 2e5,
1922
1961
  maxOutput: 64e3,
1923
1962
  reasoning: true,
1924
- agentic: true
1963
+ vision: true,
1964
+ agentic: true,
1965
+ toolCalling: true
1925
1966
  },
1926
1967
  {
1927
1968
  id: "anthropic/claude-opus-4.6",
@@ -1932,19 +1973,22 @@ var BLOCKRUN_MODELS = [
1932
1973
  contextWindow: 2e5,
1933
1974
  maxOutput: 32e3,
1934
1975
  reasoning: true,
1935
- agentic: true
1976
+ vision: true,
1977
+ agentic: true,
1978
+ toolCalling: true
1936
1979
  },
1937
1980
  // Google
1938
1981
  {
1939
- id: "google/gemini-3.1-pro-preview",
1940
- name: "Gemini 3.1 Pro Preview",
1982
+ id: "google/gemini-3.1-pro",
1983
+ name: "Gemini 3.1 Pro",
1941
1984
  version: "3.1",
1942
1985
  inputPrice: 2,
1943
1986
  outputPrice: 12,
1944
1987
  contextWindow: 105e4,
1945
1988
  maxOutput: 65536,
1946
1989
  reasoning: true,
1947
- vision: true
1990
+ vision: true,
1991
+ toolCalling: true
1948
1992
  },
1949
1993
  {
1950
1994
  id: "google/gemini-3-pro-preview",
@@ -1955,7 +1999,8 @@ var BLOCKRUN_MODELS = [
1955
1999
  contextWindow: 105e4,
1956
2000
  maxOutput: 65536,
1957
2001
  reasoning: true,
1958
- vision: true
2002
+ vision: true,
2003
+ toolCalling: true
1959
2004
  },
1960
2005
  {
1961
2006
  id: "google/gemini-3-flash-preview",
@@ -1965,7 +2010,8 @@ var BLOCKRUN_MODELS = [
1965
2010
  outputPrice: 3,
1966
2011
  contextWindow: 1e6,
1967
2012
  maxOutput: 65536,
1968
- vision: true
2013
+ vision: true,
2014
+ toolCalling: true
1969
2015
  },
1970
2016
  {
1971
2017
  id: "google/gemini-2.5-pro",
@@ -1976,7 +2022,8 @@ var BLOCKRUN_MODELS = [
1976
2022
  contextWindow: 105e4,
1977
2023
  maxOutput: 65536,
1978
2024
  reasoning: true,
1979
- vision: true
2025
+ vision: true,
2026
+ toolCalling: true
1980
2027
  },
1981
2028
  {
1982
2029
  id: "google/gemini-2.5-flash",
@@ -1985,7 +2032,9 @@ var BLOCKRUN_MODELS = [
1985
2032
  inputPrice: 0.3,
1986
2033
  outputPrice: 2.5,
1987
2034
  contextWindow: 1e6,
1988
- maxOutput: 65536
2035
+ maxOutput: 65536,
2036
+ vision: true,
2037
+ toolCalling: true
1989
2038
  },
1990
2039
  {
1991
2040
  id: "google/gemini-2.5-flash-lite",
@@ -1994,7 +2043,8 @@ var BLOCKRUN_MODELS = [
1994
2043
  inputPrice: 0.1,
1995
2044
  outputPrice: 0.4,
1996
2045
  contextWindow: 1e6,
1997
- maxOutput: 65536
2046
+ maxOutput: 65536,
2047
+ toolCalling: true
1998
2048
  },
1999
2049
  // DeepSeek
2000
2050
  {
@@ -2004,7 +2054,8 @@ var BLOCKRUN_MODELS = [
2004
2054
  inputPrice: 0.28,
2005
2055
  outputPrice: 0.42,
2006
2056
  contextWindow: 128e3,
2007
- maxOutput: 8192
2057
+ maxOutput: 8192,
2058
+ toolCalling: true
2008
2059
  },
2009
2060
  {
2010
2061
  id: "deepseek/deepseek-reasoner",
@@ -2014,7 +2065,8 @@ var BLOCKRUN_MODELS = [
2014
2065
  outputPrice: 0.42,
2015
2066
  contextWindow: 128e3,
2016
2067
  maxOutput: 8192,
2017
- reasoning: true
2068
+ reasoning: true,
2069
+ toolCalling: true
2018
2070
  },
2019
2071
  // Moonshot / Kimi - optimized for agentic workflows
2020
2072
  {
@@ -2027,7 +2079,8 @@ var BLOCKRUN_MODELS = [
2027
2079
  maxOutput: 8192,
2028
2080
  reasoning: true,
2029
2081
  vision: true,
2030
- agentic: true
2082
+ agentic: true,
2083
+ toolCalling: true
2031
2084
  },
2032
2085
  // xAI / Grok
2033
2086
  {
@@ -2038,7 +2091,8 @@ var BLOCKRUN_MODELS = [
2038
2091
  outputPrice: 15,
2039
2092
  contextWindow: 131072,
2040
2093
  maxOutput: 16384,
2041
- reasoning: true
2094
+ reasoning: true,
2095
+ toolCalling: true
2042
2096
  },
2043
2097
  // grok-3-fast removed - too expensive ($5/$25), use grok-4-fast instead
2044
2098
  {
@@ -2048,7 +2102,8 @@ var BLOCKRUN_MODELS = [
2048
2102
  inputPrice: 0.3,
2049
2103
  outputPrice: 0.5,
2050
2104
  contextWindow: 131072,
2051
- maxOutput: 16384
2105
+ maxOutput: 16384,
2106
+ toolCalling: true
2052
2107
  },
2053
2108
  // xAI Grok 4 Family - Ultra-cheap fast models
2054
2109
  {
@@ -2059,7 +2114,8 @@ var BLOCKRUN_MODELS = [
2059
2114
  outputPrice: 0.5,
2060
2115
  contextWindow: 131072,
2061
2116
  maxOutput: 16384,
2062
- reasoning: true
2117
+ reasoning: true,
2118
+ toolCalling: true
2063
2119
  },
2064
2120
  {
2065
2121
  id: "xai/grok-4-fast-non-reasoning",
@@ -2068,7 +2124,8 @@ var BLOCKRUN_MODELS = [
2068
2124
  inputPrice: 0.2,
2069
2125
  outputPrice: 0.5,
2070
2126
  contextWindow: 131072,
2071
- maxOutput: 16384
2127
+ maxOutput: 16384,
2128
+ toolCalling: true
2072
2129
  },
2073
2130
  {
2074
2131
  id: "xai/grok-4-1-fast-reasoning",
@@ -2078,7 +2135,8 @@ var BLOCKRUN_MODELS = [
2078
2135
  outputPrice: 0.5,
2079
2136
  contextWindow: 131072,
2080
2137
  maxOutput: 16384,
2081
- reasoning: true
2138
+ reasoning: true,
2139
+ toolCalling: true
2082
2140
  },
2083
2141
  {
2084
2142
  id: "xai/grok-4-1-fast-non-reasoning",
@@ -2087,7 +2145,8 @@ var BLOCKRUN_MODELS = [
2087
2145
  inputPrice: 0.2,
2088
2146
  outputPrice: 0.5,
2089
2147
  contextWindow: 131072,
2090
- maxOutput: 16384
2148
+ maxOutput: 16384,
2149
+ toolCalling: true
2091
2150
  },
2092
2151
  {
2093
2152
  id: "xai/grok-code-fast-1",
@@ -2096,9 +2155,10 @@ var BLOCKRUN_MODELS = [
2096
2155
  inputPrice: 0.2,
2097
2156
  outputPrice: 1.5,
2098
2157
  contextWindow: 131072,
2099
- maxOutput: 16384,
2100
- agentic: true
2101
- // Good for coding tasks
2158
+ maxOutput: 16384
2159
+ // toolCalling intentionally omitted: outputs tool calls as plain text JSON,
2160
+ // not OpenAI-compatible structured function calls. Will be skipped when
2161
+ // request has tools to prevent the "talking to itself" bug.
2102
2162
  },
2103
2163
  {
2104
2164
  id: "xai/grok-4-0709",
@@ -2108,7 +2168,8 @@ var BLOCKRUN_MODELS = [
2108
2168
  outputPrice: 1.5,
2109
2169
  contextWindow: 131072,
2110
2170
  maxOutput: 16384,
2111
- reasoning: true
2171
+ reasoning: true,
2172
+ toolCalling: true
2112
2173
  },
2113
2174
  {
2114
2175
  id: "xai/grok-2-vision",
@@ -2118,7 +2179,8 @@ var BLOCKRUN_MODELS = [
2118
2179
  outputPrice: 10,
2119
2180
  contextWindow: 131072,
2120
2181
  maxOutput: 16384,
2121
- vision: true
2182
+ vision: true,
2183
+ toolCalling: true
2122
2184
  },
2123
2185
  // MiniMax
2124
2186
  {
@@ -2130,7 +2192,8 @@ var BLOCKRUN_MODELS = [
2130
2192
  contextWindow: 204800,
2131
2193
  maxOutput: 16384,
2132
2194
  reasoning: true,
2133
- agentic: true
2195
+ agentic: true,
2196
+ toolCalling: true
2134
2197
  },
2135
2198
  // NVIDIA - Free/cheap models
2136
2199
  {
@@ -2141,6 +2204,8 @@ var BLOCKRUN_MODELS = [
2141
2204
  outputPrice: 0,
2142
2205
  contextWindow: 128e3,
2143
2206
  maxOutput: 16384
2207
+ // toolCalling intentionally omitted: free model, structured function
2208
+ // calling support unverified. Excluded from tool-heavy routing paths.
2144
2209
  },
2145
2210
  {
2146
2211
  id: "nvidia/kimi-k2.5",
@@ -2149,7 +2214,8 @@ var BLOCKRUN_MODELS = [
2149
2214
  inputPrice: 0.55,
2150
2215
  outputPrice: 2.5,
2151
2216
  contextWindow: 262144,
2152
- maxOutput: 16384
2217
+ maxOutput: 16384,
2218
+ toolCalling: true
2153
2219
  }
2154
2220
  ];
2155
2221
  function toOpenClawModel(m) {
@@ -2178,6 +2244,16 @@ var OPENCLAW_MODELS = [
2178
2244
  ...BLOCKRUN_MODELS.map(toOpenClawModel),
2179
2245
  ...ALIAS_MODELS
2180
2246
  ];
2247
+ function supportsToolCalling(modelId) {
2248
+ const normalized = modelId.replace("blockrun/", "");
2249
+ const model = BLOCKRUN_MODELS.find((m) => m.id === normalized);
2250
+ return model?.toolCalling ?? false;
2251
+ }
2252
+ function supportsVision(modelId) {
2253
+ const normalized = modelId.replace("blockrun/", "");
2254
+ const model = BLOCKRUN_MODELS.find((m) => m.id === normalized);
2255
+ return model?.vision ?? false;
2256
+ }
2181
2257
  function getModelContextWindow(modelId) {
2182
2258
  const normalized = modelId.replace("blockrun/", "");
2183
2259
  const model = BLOCKRUN_MODELS.find((m) => m.id === normalized);
@@ -2828,6 +2904,105 @@ var BalanceMonitor = class {
2828
2904
  }
2829
2905
  };
2830
2906
 
2907
+ // src/solana-balance.ts
2908
+ import { address as solAddress, createSolanaRpc } from "@solana/kit";
2909
+ var SOLANA_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
2910
+ var SOLANA_DEFAULT_RPC = "https://api.mainnet-beta.solana.com";
2911
+ var BALANCE_TIMEOUT_MS = 1e4;
2912
+ var CACHE_TTL_MS2 = 3e4;
2913
+ var SolanaBalanceMonitor = class {
2914
+ rpc;
2915
+ walletAddress;
2916
+ cachedBalance = null;
2917
+ cachedAt = 0;
2918
+ constructor(walletAddress, rpcUrl) {
2919
+ this.walletAddress = walletAddress;
2920
+ const url = rpcUrl || process["env"].CLAWROUTER_SOLANA_RPC_URL || SOLANA_DEFAULT_RPC;
2921
+ this.rpc = createSolanaRpc(url);
2922
+ }
2923
+ async checkBalance() {
2924
+ const now = Date.now();
2925
+ if (this.cachedBalance !== null && now - this.cachedAt < CACHE_TTL_MS2) {
2926
+ return this.buildInfo(this.cachedBalance);
2927
+ }
2928
+ const balance = await this.fetchBalance();
2929
+ this.cachedBalance = balance;
2930
+ this.cachedAt = now;
2931
+ return this.buildInfo(balance);
2932
+ }
2933
+ deductEstimated(amountMicros) {
2934
+ if (this.cachedBalance !== null && this.cachedBalance >= amountMicros) {
2935
+ this.cachedBalance -= amountMicros;
2936
+ }
2937
+ }
2938
+ invalidate() {
2939
+ this.cachedBalance = null;
2940
+ this.cachedAt = 0;
2941
+ }
2942
+ async refresh() {
2943
+ this.invalidate();
2944
+ return this.checkBalance();
2945
+ }
2946
+ /**
2947
+ * Check if balance is sufficient for an estimated cost.
2948
+ */
2949
+ async checkSufficient(estimatedCostMicros) {
2950
+ const info = await this.checkBalance();
2951
+ if (info.balance >= estimatedCostMicros) {
2952
+ return { sufficient: true, info };
2953
+ }
2954
+ const shortfall = estimatedCostMicros - info.balance;
2955
+ return {
2956
+ sufficient: false,
2957
+ info,
2958
+ shortfall: this.formatUSDC(shortfall)
2959
+ };
2960
+ }
2961
+ /**
2962
+ * Format USDC amount (in micros) as "$X.XX".
2963
+ */
2964
+ formatUSDC(amountMicros) {
2965
+ const dollars = Number(amountMicros) / 1e6;
2966
+ return `$${dollars.toFixed(2)}`;
2967
+ }
2968
+ getWalletAddress() {
2969
+ return this.walletAddress;
2970
+ }
2971
+ async fetchBalance() {
2972
+ const owner = solAddress(this.walletAddress);
2973
+ const mint = solAddress(SOLANA_USDC_MINT);
2974
+ const controller = new AbortController();
2975
+ const timer = setTimeout(() => controller.abort(), BALANCE_TIMEOUT_MS);
2976
+ try {
2977
+ const response = await this.rpc.getTokenAccountsByOwner(owner, { mint }, { encoding: "jsonParsed" }).send({ abortSignal: controller.signal });
2978
+ if (response.value.length === 0) return 0n;
2979
+ let total = 0n;
2980
+ for (const account of response.value) {
2981
+ const parsed = account.account.data;
2982
+ total += BigInt(parsed.parsed.info.tokenAmount.amount);
2983
+ }
2984
+ return total;
2985
+ } catch (err) {
2986
+ throw new Error(
2987
+ `Failed to fetch Solana USDC balance: ${err instanceof Error ? err.message : String(err)}`,
2988
+ { cause: err }
2989
+ );
2990
+ } finally {
2991
+ clearTimeout(timer);
2992
+ }
2993
+ }
2994
+ buildInfo(balance) {
2995
+ const dollars = Number(balance) / 1e6;
2996
+ return {
2997
+ balance,
2998
+ balanceUSD: `$${dollars.toFixed(2)}`,
2999
+ isLow: balance < 1000000n,
3000
+ isEmpty: balance < 100n,
3001
+ walletAddress: this.walletAddress
3002
+ };
3003
+ }
3004
+ };
3005
+
2831
3006
  // src/auth.ts
2832
3007
  import { writeFile, mkdir as mkdir2 } from "fs/promises";
2833
3008
  import { join as join4 } from "path";
@@ -2838,9 +3013,507 @@ import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
2838
3013
  import { HDKey } from "@scure/bip32";
2839
3014
  import { generateMnemonic, mnemonicToSeedSync, validateMnemonic } from "@scure/bip39";
2840
3015
  import { wordlist as english } from "@scure/bip39/wordlists/english";
3016
+
3017
+ // node_modules/@noble/hashes/esm/utils.js
3018
+ function isBytes(a) {
3019
+ return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array";
3020
+ }
3021
+ function anumber(n) {
3022
+ if (!Number.isSafeInteger(n) || n < 0)
3023
+ throw new Error("positive integer expected, got " + n);
3024
+ }
3025
+ function abytes(b, ...lengths) {
3026
+ if (!isBytes(b))
3027
+ throw new Error("Uint8Array expected");
3028
+ if (lengths.length > 0 && !lengths.includes(b.length))
3029
+ throw new Error("Uint8Array expected of length " + lengths + ", got length=" + b.length);
3030
+ }
3031
+ function ahash(h) {
3032
+ if (typeof h !== "function" || typeof h.create !== "function")
3033
+ throw new Error("Hash should be wrapped by utils.createHasher");
3034
+ anumber(h.outputLen);
3035
+ anumber(h.blockLen);
3036
+ }
3037
+ function aexists(instance, checkFinished = true) {
3038
+ if (instance.destroyed)
3039
+ throw new Error("Hash instance has been destroyed");
3040
+ if (checkFinished && instance.finished)
3041
+ throw new Error("Hash#digest() has already been called");
3042
+ }
3043
+ function aoutput(out, instance) {
3044
+ abytes(out);
3045
+ const min = instance.outputLen;
3046
+ if (out.length < min) {
3047
+ throw new Error("digestInto() expects output buffer of length at least " + min);
3048
+ }
3049
+ }
3050
+ function clean(...arrays) {
3051
+ for (let i = 0; i < arrays.length; i++) {
3052
+ arrays[i].fill(0);
3053
+ }
3054
+ }
3055
+ function createView(arr) {
3056
+ return new DataView(arr.buffer, arr.byteOffset, arr.byteLength);
3057
+ }
3058
+ function utf8ToBytes(str) {
3059
+ if (typeof str !== "string")
3060
+ throw new Error("string expected");
3061
+ return new Uint8Array(new TextEncoder().encode(str));
3062
+ }
3063
+ function toBytes(data) {
3064
+ if (typeof data === "string")
3065
+ data = utf8ToBytes(data);
3066
+ abytes(data);
3067
+ return data;
3068
+ }
3069
+ var Hash = class {
3070
+ };
3071
+ function createHasher(hashCons) {
3072
+ const hashC = (msg) => hashCons().update(toBytes(msg)).digest();
3073
+ const tmp = hashCons();
3074
+ hashC.outputLen = tmp.outputLen;
3075
+ hashC.blockLen = tmp.blockLen;
3076
+ hashC.create = () => hashCons();
3077
+ return hashC;
3078
+ }
3079
+
3080
+ // node_modules/@noble/hashes/esm/hmac.js
3081
+ var HMAC = class extends Hash {
3082
+ constructor(hash, _key) {
3083
+ super();
3084
+ this.finished = false;
3085
+ this.destroyed = false;
3086
+ ahash(hash);
3087
+ const key = toBytes(_key);
3088
+ this.iHash = hash.create();
3089
+ if (typeof this.iHash.update !== "function")
3090
+ throw new Error("Expected instance of class which extends utils.Hash");
3091
+ this.blockLen = this.iHash.blockLen;
3092
+ this.outputLen = this.iHash.outputLen;
3093
+ const blockLen = this.blockLen;
3094
+ const pad = new Uint8Array(blockLen);
3095
+ pad.set(key.length > blockLen ? hash.create().update(key).digest() : key);
3096
+ for (let i = 0; i < pad.length; i++)
3097
+ pad[i] ^= 54;
3098
+ this.iHash.update(pad);
3099
+ this.oHash = hash.create();
3100
+ for (let i = 0; i < pad.length; i++)
3101
+ pad[i] ^= 54 ^ 92;
3102
+ this.oHash.update(pad);
3103
+ clean(pad);
3104
+ }
3105
+ update(buf) {
3106
+ aexists(this);
3107
+ this.iHash.update(buf);
3108
+ return this;
3109
+ }
3110
+ digestInto(out) {
3111
+ aexists(this);
3112
+ abytes(out, this.outputLen);
3113
+ this.finished = true;
3114
+ this.iHash.digestInto(out);
3115
+ this.oHash.update(out);
3116
+ this.oHash.digestInto(out);
3117
+ this.destroy();
3118
+ }
3119
+ digest() {
3120
+ const out = new Uint8Array(this.oHash.outputLen);
3121
+ this.digestInto(out);
3122
+ return out;
3123
+ }
3124
+ _cloneInto(to) {
3125
+ to || (to = Object.create(Object.getPrototypeOf(this), {}));
3126
+ const { oHash, iHash, finished: finished2, destroyed, blockLen, outputLen } = this;
3127
+ to = to;
3128
+ to.finished = finished2;
3129
+ to.destroyed = destroyed;
3130
+ to.blockLen = blockLen;
3131
+ to.outputLen = outputLen;
3132
+ to.oHash = oHash._cloneInto(to.oHash);
3133
+ to.iHash = iHash._cloneInto(to.iHash);
3134
+ return to;
3135
+ }
3136
+ clone() {
3137
+ return this._cloneInto();
3138
+ }
3139
+ destroy() {
3140
+ this.destroyed = true;
3141
+ this.oHash.destroy();
3142
+ this.iHash.destroy();
3143
+ }
3144
+ };
3145
+ var hmac = (hash, key, message) => new HMAC(hash, key).update(message).digest();
3146
+ hmac.create = (hash, key) => new HMAC(hash, key);
3147
+
3148
+ // node_modules/@noble/hashes/esm/_md.js
3149
+ function setBigUint64(view, byteOffset, value, isLE) {
3150
+ if (typeof view.setBigUint64 === "function")
3151
+ return view.setBigUint64(byteOffset, value, isLE);
3152
+ const _32n2 = BigInt(32);
3153
+ const _u32_max = BigInt(4294967295);
3154
+ const wh = Number(value >> _32n2 & _u32_max);
3155
+ const wl = Number(value & _u32_max);
3156
+ const h = isLE ? 4 : 0;
3157
+ const l = isLE ? 0 : 4;
3158
+ view.setUint32(byteOffset + h, wh, isLE);
3159
+ view.setUint32(byteOffset + l, wl, isLE);
3160
+ }
3161
+ var HashMD = class extends Hash {
3162
+ constructor(blockLen, outputLen, padOffset, isLE) {
3163
+ super();
3164
+ this.finished = false;
3165
+ this.length = 0;
3166
+ this.pos = 0;
3167
+ this.destroyed = false;
3168
+ this.blockLen = blockLen;
3169
+ this.outputLen = outputLen;
3170
+ this.padOffset = padOffset;
3171
+ this.isLE = isLE;
3172
+ this.buffer = new Uint8Array(blockLen);
3173
+ this.view = createView(this.buffer);
3174
+ }
3175
+ update(data) {
3176
+ aexists(this);
3177
+ data = toBytes(data);
3178
+ abytes(data);
3179
+ const { view, buffer, blockLen } = this;
3180
+ const len = data.length;
3181
+ for (let pos = 0; pos < len; ) {
3182
+ const take = Math.min(blockLen - this.pos, len - pos);
3183
+ if (take === blockLen) {
3184
+ const dataView = createView(data);
3185
+ for (; blockLen <= len - pos; pos += blockLen)
3186
+ this.process(dataView, pos);
3187
+ continue;
3188
+ }
3189
+ buffer.set(data.subarray(pos, pos + take), this.pos);
3190
+ this.pos += take;
3191
+ pos += take;
3192
+ if (this.pos === blockLen) {
3193
+ this.process(view, 0);
3194
+ this.pos = 0;
3195
+ }
3196
+ }
3197
+ this.length += data.length;
3198
+ this.roundClean();
3199
+ return this;
3200
+ }
3201
+ digestInto(out) {
3202
+ aexists(this);
3203
+ aoutput(out, this);
3204
+ this.finished = true;
3205
+ const { buffer, view, blockLen, isLE } = this;
3206
+ let { pos } = this;
3207
+ buffer[pos++] = 128;
3208
+ clean(this.buffer.subarray(pos));
3209
+ if (this.padOffset > blockLen - pos) {
3210
+ this.process(view, 0);
3211
+ pos = 0;
3212
+ }
3213
+ for (let i = pos; i < blockLen; i++)
3214
+ buffer[i] = 0;
3215
+ setBigUint64(view, blockLen - 8, BigInt(this.length * 8), isLE);
3216
+ this.process(view, 0);
3217
+ const oview = createView(out);
3218
+ const len = this.outputLen;
3219
+ if (len % 4)
3220
+ throw new Error("_sha2: outputLen should be aligned to 32bit");
3221
+ const outLen = len / 4;
3222
+ const state = this.get();
3223
+ if (outLen > state.length)
3224
+ throw new Error("_sha2: outputLen bigger than state");
3225
+ for (let i = 0; i < outLen; i++)
3226
+ oview.setUint32(4 * i, state[i], isLE);
3227
+ }
3228
+ digest() {
3229
+ const { buffer, outputLen } = this;
3230
+ this.digestInto(buffer);
3231
+ const res = buffer.slice(0, outputLen);
3232
+ this.destroy();
3233
+ return res;
3234
+ }
3235
+ _cloneInto(to) {
3236
+ to || (to = new this.constructor());
3237
+ to.set(...this.get());
3238
+ const { blockLen, buffer, length, finished: finished2, destroyed, pos } = this;
3239
+ to.destroyed = destroyed;
3240
+ to.finished = finished2;
3241
+ to.length = length;
3242
+ to.pos = pos;
3243
+ if (length % blockLen)
3244
+ to.buffer.set(buffer);
3245
+ return to;
3246
+ }
3247
+ clone() {
3248
+ return this._cloneInto();
3249
+ }
3250
+ };
3251
+ var SHA512_IV = /* @__PURE__ */ Uint32Array.from([
3252
+ 1779033703,
3253
+ 4089235720,
3254
+ 3144134277,
3255
+ 2227873595,
3256
+ 1013904242,
3257
+ 4271175723,
3258
+ 2773480762,
3259
+ 1595750129,
3260
+ 1359893119,
3261
+ 2917565137,
3262
+ 2600822924,
3263
+ 725511199,
3264
+ 528734635,
3265
+ 4215389547,
3266
+ 1541459225,
3267
+ 327033209
3268
+ ]);
3269
+
3270
+ // node_modules/@noble/hashes/esm/_u64.js
3271
+ var U32_MASK64 = /* @__PURE__ */ BigInt(2 ** 32 - 1);
3272
+ var _32n = /* @__PURE__ */ BigInt(32);
3273
+ function fromBig(n, le = false) {
3274
+ if (le)
3275
+ return { h: Number(n & U32_MASK64), l: Number(n >> _32n & U32_MASK64) };
3276
+ return { h: Number(n >> _32n & U32_MASK64) | 0, l: Number(n & U32_MASK64) | 0 };
3277
+ }
3278
+ function split(lst, le = false) {
3279
+ const len = lst.length;
3280
+ let Ah = new Uint32Array(len);
3281
+ let Al = new Uint32Array(len);
3282
+ for (let i = 0; i < len; i++) {
3283
+ const { h, l } = fromBig(lst[i], le);
3284
+ [Ah[i], Al[i]] = [h, l];
3285
+ }
3286
+ return [Ah, Al];
3287
+ }
3288
+ var shrSH = (h, _l, s) => h >>> s;
3289
+ var shrSL = (h, l, s) => h << 32 - s | l >>> s;
3290
+ var rotrSH = (h, l, s) => h >>> s | l << 32 - s;
3291
+ var rotrSL = (h, l, s) => h << 32 - s | l >>> s;
3292
+ var rotrBH = (h, l, s) => h << 64 - s | l >>> s - 32;
3293
+ var rotrBL = (h, l, s) => h >>> s - 32 | l << 64 - s;
3294
+ function add(Ah, Al, Bh, Bl) {
3295
+ const l = (Al >>> 0) + (Bl >>> 0);
3296
+ return { h: Ah + Bh + (l / 2 ** 32 | 0) | 0, l: l | 0 };
3297
+ }
3298
+ var add3L = (Al, Bl, Cl) => (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0);
3299
+ var add3H = (low, Ah, Bh, Ch) => Ah + Bh + Ch + (low / 2 ** 32 | 0) | 0;
3300
+ var add4L = (Al, Bl, Cl, Dl) => (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0) + (Dl >>> 0);
3301
+ var add4H = (low, Ah, Bh, Ch, Dh) => Ah + Bh + Ch + Dh + (low / 2 ** 32 | 0) | 0;
3302
+ var add5L = (Al, Bl, Cl, Dl, El) => (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0) + (Dl >>> 0) + (El >>> 0);
3303
+ var add5H = (low, Ah, Bh, Ch, Dh, Eh) => Ah + Bh + Ch + Dh + Eh + (low / 2 ** 32 | 0) | 0;
3304
+
3305
+ // node_modules/@noble/hashes/esm/sha2.js
3306
+ var K512 = /* @__PURE__ */ (() => split([
3307
+ "0x428a2f98d728ae22",
3308
+ "0x7137449123ef65cd",
3309
+ "0xb5c0fbcfec4d3b2f",
3310
+ "0xe9b5dba58189dbbc",
3311
+ "0x3956c25bf348b538",
3312
+ "0x59f111f1b605d019",
3313
+ "0x923f82a4af194f9b",
3314
+ "0xab1c5ed5da6d8118",
3315
+ "0xd807aa98a3030242",
3316
+ "0x12835b0145706fbe",
3317
+ "0x243185be4ee4b28c",
3318
+ "0x550c7dc3d5ffb4e2",
3319
+ "0x72be5d74f27b896f",
3320
+ "0x80deb1fe3b1696b1",
3321
+ "0x9bdc06a725c71235",
3322
+ "0xc19bf174cf692694",
3323
+ "0xe49b69c19ef14ad2",
3324
+ "0xefbe4786384f25e3",
3325
+ "0x0fc19dc68b8cd5b5",
3326
+ "0x240ca1cc77ac9c65",
3327
+ "0x2de92c6f592b0275",
3328
+ "0x4a7484aa6ea6e483",
3329
+ "0x5cb0a9dcbd41fbd4",
3330
+ "0x76f988da831153b5",
3331
+ "0x983e5152ee66dfab",
3332
+ "0xa831c66d2db43210",
3333
+ "0xb00327c898fb213f",
3334
+ "0xbf597fc7beef0ee4",
3335
+ "0xc6e00bf33da88fc2",
3336
+ "0xd5a79147930aa725",
3337
+ "0x06ca6351e003826f",
3338
+ "0x142929670a0e6e70",
3339
+ "0x27b70a8546d22ffc",
3340
+ "0x2e1b21385c26c926",
3341
+ "0x4d2c6dfc5ac42aed",
3342
+ "0x53380d139d95b3df",
3343
+ "0x650a73548baf63de",
3344
+ "0x766a0abb3c77b2a8",
3345
+ "0x81c2c92e47edaee6",
3346
+ "0x92722c851482353b",
3347
+ "0xa2bfe8a14cf10364",
3348
+ "0xa81a664bbc423001",
3349
+ "0xc24b8b70d0f89791",
3350
+ "0xc76c51a30654be30",
3351
+ "0xd192e819d6ef5218",
3352
+ "0xd69906245565a910",
3353
+ "0xf40e35855771202a",
3354
+ "0x106aa07032bbd1b8",
3355
+ "0x19a4c116b8d2d0c8",
3356
+ "0x1e376c085141ab53",
3357
+ "0x2748774cdf8eeb99",
3358
+ "0x34b0bcb5e19b48a8",
3359
+ "0x391c0cb3c5c95a63",
3360
+ "0x4ed8aa4ae3418acb",
3361
+ "0x5b9cca4f7763e373",
3362
+ "0x682e6ff3d6b2b8a3",
3363
+ "0x748f82ee5defb2fc",
3364
+ "0x78a5636f43172f60",
3365
+ "0x84c87814a1f0ab72",
3366
+ "0x8cc702081a6439ec",
3367
+ "0x90befffa23631e28",
3368
+ "0xa4506cebde82bde9",
3369
+ "0xbef9a3f7b2c67915",
3370
+ "0xc67178f2e372532b",
3371
+ "0xca273eceea26619c",
3372
+ "0xd186b8c721c0c207",
3373
+ "0xeada7dd6cde0eb1e",
3374
+ "0xf57d4f7fee6ed178",
3375
+ "0x06f067aa72176fba",
3376
+ "0x0a637dc5a2c898a6",
3377
+ "0x113f9804bef90dae",
3378
+ "0x1b710b35131c471b",
3379
+ "0x28db77f523047d84",
3380
+ "0x32caab7b40c72493",
3381
+ "0x3c9ebe0a15c9bebc",
3382
+ "0x431d67c49c100d4c",
3383
+ "0x4cc5d4becb3e42b6",
3384
+ "0x597f299cfc657e2a",
3385
+ "0x5fcb6fab3ad6faec",
3386
+ "0x6c44198c4a475817"
3387
+ ].map((n) => BigInt(n))))();
3388
+ var SHA512_Kh = /* @__PURE__ */ (() => K512[0])();
3389
+ var SHA512_Kl = /* @__PURE__ */ (() => K512[1])();
3390
+ var SHA512_W_H = /* @__PURE__ */ new Uint32Array(80);
3391
+ var SHA512_W_L = /* @__PURE__ */ new Uint32Array(80);
3392
+ var SHA512 = class extends HashMD {
3393
+ constructor(outputLen = 64) {
3394
+ super(128, outputLen, 16, false);
3395
+ this.Ah = SHA512_IV[0] | 0;
3396
+ this.Al = SHA512_IV[1] | 0;
3397
+ this.Bh = SHA512_IV[2] | 0;
3398
+ this.Bl = SHA512_IV[3] | 0;
3399
+ this.Ch = SHA512_IV[4] | 0;
3400
+ this.Cl = SHA512_IV[5] | 0;
3401
+ this.Dh = SHA512_IV[6] | 0;
3402
+ this.Dl = SHA512_IV[7] | 0;
3403
+ this.Eh = SHA512_IV[8] | 0;
3404
+ this.El = SHA512_IV[9] | 0;
3405
+ this.Fh = SHA512_IV[10] | 0;
3406
+ this.Fl = SHA512_IV[11] | 0;
3407
+ this.Gh = SHA512_IV[12] | 0;
3408
+ this.Gl = SHA512_IV[13] | 0;
3409
+ this.Hh = SHA512_IV[14] | 0;
3410
+ this.Hl = SHA512_IV[15] | 0;
3411
+ }
3412
+ // prettier-ignore
3413
+ get() {
3414
+ const { Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl } = this;
3415
+ return [Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl];
3416
+ }
3417
+ // prettier-ignore
3418
+ set(Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl) {
3419
+ this.Ah = Ah | 0;
3420
+ this.Al = Al | 0;
3421
+ this.Bh = Bh | 0;
3422
+ this.Bl = Bl | 0;
3423
+ this.Ch = Ch | 0;
3424
+ this.Cl = Cl | 0;
3425
+ this.Dh = Dh | 0;
3426
+ this.Dl = Dl | 0;
3427
+ this.Eh = Eh | 0;
3428
+ this.El = El | 0;
3429
+ this.Fh = Fh | 0;
3430
+ this.Fl = Fl | 0;
3431
+ this.Gh = Gh | 0;
3432
+ this.Gl = Gl | 0;
3433
+ this.Hh = Hh | 0;
3434
+ this.Hl = Hl | 0;
3435
+ }
3436
+ process(view, offset) {
3437
+ for (let i = 0; i < 16; i++, offset += 4) {
3438
+ SHA512_W_H[i] = view.getUint32(offset);
3439
+ SHA512_W_L[i] = view.getUint32(offset += 4);
3440
+ }
3441
+ for (let i = 16; i < 80; i++) {
3442
+ const W15h = SHA512_W_H[i - 15] | 0;
3443
+ const W15l = SHA512_W_L[i - 15] | 0;
3444
+ const s0h = rotrSH(W15h, W15l, 1) ^ rotrSH(W15h, W15l, 8) ^ shrSH(W15h, W15l, 7);
3445
+ const s0l = rotrSL(W15h, W15l, 1) ^ rotrSL(W15h, W15l, 8) ^ shrSL(W15h, W15l, 7);
3446
+ const W2h = SHA512_W_H[i - 2] | 0;
3447
+ const W2l = SHA512_W_L[i - 2] | 0;
3448
+ const s1h = rotrSH(W2h, W2l, 19) ^ rotrBH(W2h, W2l, 61) ^ shrSH(W2h, W2l, 6);
3449
+ const s1l = rotrSL(W2h, W2l, 19) ^ rotrBL(W2h, W2l, 61) ^ shrSL(W2h, W2l, 6);
3450
+ const SUMl = add4L(s0l, s1l, SHA512_W_L[i - 7], SHA512_W_L[i - 16]);
3451
+ const SUMh = add4H(SUMl, s0h, s1h, SHA512_W_H[i - 7], SHA512_W_H[i - 16]);
3452
+ SHA512_W_H[i] = SUMh | 0;
3453
+ SHA512_W_L[i] = SUMl | 0;
3454
+ }
3455
+ let { Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl } = this;
3456
+ for (let i = 0; i < 80; i++) {
3457
+ const sigma1h = rotrSH(Eh, El, 14) ^ rotrSH(Eh, El, 18) ^ rotrBH(Eh, El, 41);
3458
+ const sigma1l = rotrSL(Eh, El, 14) ^ rotrSL(Eh, El, 18) ^ rotrBL(Eh, El, 41);
3459
+ const CHIh = Eh & Fh ^ ~Eh & Gh;
3460
+ const CHIl = El & Fl ^ ~El & Gl;
3461
+ const T1ll = add5L(Hl, sigma1l, CHIl, SHA512_Kl[i], SHA512_W_L[i]);
3462
+ const T1h = add5H(T1ll, Hh, sigma1h, CHIh, SHA512_Kh[i], SHA512_W_H[i]);
3463
+ const T1l = T1ll | 0;
3464
+ const sigma0h = rotrSH(Ah, Al, 28) ^ rotrBH(Ah, Al, 34) ^ rotrBH(Ah, Al, 39);
3465
+ const sigma0l = rotrSL(Ah, Al, 28) ^ rotrBL(Ah, Al, 34) ^ rotrBL(Ah, Al, 39);
3466
+ const MAJh = Ah & Bh ^ Ah & Ch ^ Bh & Ch;
3467
+ const MAJl = Al & Bl ^ Al & Cl ^ Bl & Cl;
3468
+ Hh = Gh | 0;
3469
+ Hl = Gl | 0;
3470
+ Gh = Fh | 0;
3471
+ Gl = Fl | 0;
3472
+ Fh = Eh | 0;
3473
+ Fl = El | 0;
3474
+ ({ h: Eh, l: El } = add(Dh | 0, Dl | 0, T1h | 0, T1l | 0));
3475
+ Dh = Ch | 0;
3476
+ Dl = Cl | 0;
3477
+ Ch = Bh | 0;
3478
+ Cl = Bl | 0;
3479
+ Bh = Ah | 0;
3480
+ Bl = Al | 0;
3481
+ const All = add3L(T1l, sigma0l, MAJl);
3482
+ Ah = add3H(All, T1h, sigma0h, MAJh);
3483
+ Al = All | 0;
3484
+ }
3485
+ ({ h: Ah, l: Al } = add(this.Ah | 0, this.Al | 0, Ah | 0, Al | 0));
3486
+ ({ h: Bh, l: Bl } = add(this.Bh | 0, this.Bl | 0, Bh | 0, Bl | 0));
3487
+ ({ h: Ch, l: Cl } = add(this.Ch | 0, this.Cl | 0, Ch | 0, Cl | 0));
3488
+ ({ h: Dh, l: Dl } = add(this.Dh | 0, this.Dl | 0, Dh | 0, Dl | 0));
3489
+ ({ h: Eh, l: El } = add(this.Eh | 0, this.El | 0, Eh | 0, El | 0));
3490
+ ({ h: Fh, l: Fl } = add(this.Fh | 0, this.Fl | 0, Fh | 0, Fl | 0));
3491
+ ({ h: Gh, l: Gl } = add(this.Gh | 0, this.Gl | 0, Gh | 0, Gl | 0));
3492
+ ({ h: Hh, l: Hl } = add(this.Hh | 0, this.Hl | 0, Hh | 0, Hl | 0));
3493
+ this.set(Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl);
3494
+ }
3495
+ roundClean() {
3496
+ clean(SHA512_W_H, SHA512_W_L);
3497
+ }
3498
+ destroy() {
3499
+ clean(this.buffer);
3500
+ this.set(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
3501
+ }
3502
+ };
3503
+ var sha512 = /* @__PURE__ */ createHasher(() => new SHA512());
3504
+
3505
+ // node_modules/@noble/hashes/esm/sha512.js
3506
+ var sha5122 = sha512;
3507
+
3508
+ // src/wallet.ts
2841
3509
  import { privateKeyToAccount } from "viem/accounts";
2842
3510
  var ETH_DERIVATION_PATH = "m/44'/60'/0'/0/0";
2843
- var SOLANA_DERIVATION_PATH = "m/44'/501'/0'/0'";
3511
+ var SOLANA_HARDENED_INDICES = [
3512
+ 44 + 2147483648,
3513
+ 501 + 2147483648,
3514
+ 0 + 2147483648,
3515
+ 0 + 2147483648
3516
+ ];
2844
3517
  function generateWalletMnemonic() {
2845
3518
  return generateMnemonic(english, 256);
2846
3519
  }
@@ -2857,10 +3530,29 @@ function deriveEvmKey(mnemonic) {
2857
3530
  return { privateKey: hex, address: account.address };
2858
3531
  }
2859
3532
  function deriveSolanaKeyBytes(mnemonic) {
3533
+ const seed = mnemonicToSeedSync(mnemonic);
3534
+ let I = hmac(sha5122, "ed25519 seed", seed);
3535
+ let key = I.slice(0, 32);
3536
+ let chainCode = I.slice(32);
3537
+ for (const index of SOLANA_HARDENED_INDICES) {
3538
+ const data = new Uint8Array(37);
3539
+ data[0] = 0;
3540
+ data.set(key, 1);
3541
+ data[33] = index >>> 24 & 255;
3542
+ data[34] = index >>> 16 & 255;
3543
+ data[35] = index >>> 8 & 255;
3544
+ data[36] = index & 255;
3545
+ I = hmac(sha5122, chainCode, data);
3546
+ key = I.slice(0, 32);
3547
+ chainCode = I.slice(32);
3548
+ }
3549
+ return new Uint8Array(key);
3550
+ }
3551
+ function deriveSolanaKeyBytesLegacy(mnemonic) {
2860
3552
  const seed = mnemonicToSeedSync(mnemonic);
2861
3553
  const hdKey = HDKey.fromMasterSeed(seed);
2862
- const derived = hdKey.derive(SOLANA_DERIVATION_PATH);
2863
- if (!derived.privateKey) throw new Error("Failed to derive Solana private key");
3554
+ const derived = hdKey.derive("m/44'/501'/0'/0'");
3555
+ if (!derived.privateKey) throw new Error("Failed to derive legacy Solana private key");
2864
3556
  return new Uint8Array(derived.privateKey);
2865
3557
  }
2866
3558
  function deriveAllKeys(mnemonic) {
@@ -2884,7 +3576,9 @@ async function loadSavedWallet() {
2884
3576
  console.error(`[ClawRouter] \u2717 CRITICAL: Wallet file exists but has invalid format!`);
2885
3577
  console.error(`[ClawRouter] File: ${WALLET_FILE}`);
2886
3578
  console.error(`[ClawRouter] Expected: 0x followed by 64 hex characters (66 chars total)`);
2887
- console.error(`[ClawRouter] To fix: restore your backup key or set BLOCKRUN_WALLET_KEY env var`);
3579
+ console.error(
3580
+ `[ClawRouter] To fix: restore your backup key or set BLOCKRUN_WALLET_KEY env var`
3581
+ );
2888
3582
  throw new Error(
2889
3583
  `Wallet file at ${WALLET_FILE} is corrupted or has wrong format. Refusing to auto-generate new wallet to protect existing funds. Restore your backup key or set BLOCKRUN_WALLET_KEY environment variable.`
2890
3584
  );
@@ -2897,7 +3591,8 @@ async function loadSavedWallet() {
2897
3591
  `[ClawRouter] \u2717 Failed to read wallet file: ${err instanceof Error ? err.message : String(err)}`
2898
3592
  );
2899
3593
  throw new Error(
2900
- `Cannot read wallet file at ${WALLET_FILE}: ${err instanceof Error ? err.message : String(err)}. Refusing to auto-generate new wallet to protect existing funds. Fix file permissions or set BLOCKRUN_WALLET_KEY environment variable.`
3594
+ `Cannot read wallet file at ${WALLET_FILE}: ${err instanceof Error ? err.message : String(err)}. Refusing to auto-generate new wallet to protect existing funds. Fix file permissions or set BLOCKRUN_WALLET_KEY environment variable.`,
3595
+ { cause: err }
2901
3596
  );
2902
3597
  }
2903
3598
  }
@@ -2938,7 +3633,8 @@ async function generateAndSaveWallet() {
2938
3633
  console.log(`[ClawRouter] Wallet saved and verified at ${WALLET_FILE}`);
2939
3634
  } catch (err) {
2940
3635
  throw new Error(
2941
- `Failed to verify wallet file after creation: ${err instanceof Error ? err.message : String(err)}`
3636
+ `Failed to verify wallet file after creation: ${err instanceof Error ? err.message : String(err)}`,
3637
+ { cause: err }
2942
3638
  );
2943
3639
  }
2944
3640
  console.log(`[ClawRouter]`);
@@ -2964,6 +3660,29 @@ async function generateAndSaveWallet() {
2964
3660
  solanaPrivateKeyBytes: derived.solanaPrivateKeyBytes
2965
3661
  };
2966
3662
  }
3663
+ async function logMigrationWarning(legacyKeyBytes, newKeyBytes) {
3664
+ try {
3665
+ const { createKeyPairSignerFromPrivateKeyBytes } = await import("@solana/kit");
3666
+ const [oldSigner, newSigner] = await Promise.all([
3667
+ createKeyPairSignerFromPrivateKeyBytes(legacyKeyBytes),
3668
+ createKeyPairSignerFromPrivateKeyBytes(newKeyBytes)
3669
+ ]);
3670
+ console.log(`[ClawRouter]`);
3671
+ console.log(`[ClawRouter] \u26A0 SOLANA WALLET MIGRATION DETECTED`);
3672
+ console.log(`[ClawRouter] \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`);
3673
+ console.log(`[ClawRouter] Old address (secp256k1): ${oldSigner.address}`);
3674
+ console.log(`[ClawRouter] New address (SLIP-10): ${newSigner.address}`);
3675
+ console.log(`[ClawRouter]`);
3676
+ console.log(`[ClawRouter] Your Solana wallet derivation has been fixed to use`);
3677
+ console.log(`[ClawRouter] SLIP-10 Ed25519 (Phantom/Solflare compatible).`);
3678
+ console.log(`[ClawRouter]`);
3679
+ console.log(`[ClawRouter] If you had funds in the old wallet, run:`);
3680
+ console.log(`[ClawRouter] /wallet migrate-solana`);
3681
+ console.log(`[ClawRouter] \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`);
3682
+ console.log(`[ClawRouter]`);
3683
+ } catch {
3684
+ }
3685
+ }
2967
3686
  async function resolveOrGenerateWalletKey() {
2968
3687
  const saved = await loadSavedWallet();
2969
3688
  if (saved) {
@@ -2971,13 +3690,19 @@ async function resolveOrGenerateWalletKey() {
2971
3690
  const mnemonic = await loadMnemonic();
2972
3691
  if (mnemonic) {
2973
3692
  const solanaKeyBytes = deriveSolanaKeyBytes(mnemonic);
2974
- return {
3693
+ const result2 = {
2975
3694
  key: saved,
2976
3695
  address: account.address,
2977
3696
  source: "saved",
2978
3697
  mnemonic,
2979
3698
  solanaPrivateKeyBytes: solanaKeyBytes
2980
3699
  };
3700
+ const legacyKeyBytes = deriveSolanaKeyBytesLegacy(mnemonic);
3701
+ if (Buffer.from(legacyKeyBytes).toString("hex") !== Buffer.from(solanaKeyBytes).toString("hex")) {
3702
+ result2.legacySolanaKeyBytes = legacyKeyBytes;
3703
+ await logMigrationWarning(legacyKeyBytes, solanaKeyBytes);
3704
+ }
3705
+ return result2;
2981
3706
  }
2982
3707
  return { key: saved, address: account.address, source: "saved" };
2983
3708
  }
@@ -2987,13 +3712,19 @@ async function resolveOrGenerateWalletKey() {
2987
3712
  const mnemonic = await loadMnemonic();
2988
3713
  if (mnemonic) {
2989
3714
  const solanaKeyBytes = deriveSolanaKeyBytes(mnemonic);
2990
- return {
3715
+ const result2 = {
2991
3716
  key: envKey,
2992
3717
  address: account.address,
2993
3718
  source: "env",
2994
3719
  mnemonic,
2995
3720
  solanaPrivateKeyBytes: solanaKeyBytes
2996
3721
  };
3722
+ const legacyKeyBytes = deriveSolanaKeyBytesLegacy(mnemonic);
3723
+ if (Buffer.from(legacyKeyBytes).toString("hex") !== Buffer.from(solanaKeyBytes).toString("hex")) {
3724
+ result2.legacySolanaKeyBytes = legacyKeyBytes;
3725
+ await logMigrationWarning(legacyKeyBytes, solanaKeyBytes);
3726
+ }
3727
+ return result2;
2997
3728
  }
2998
3729
  return { key: envKey, address: account.address, source: "env" };
2999
3730
  }
@@ -3767,8 +4498,9 @@ function shouldCompress(messages) {
3767
4498
  }
3768
4499
 
3769
4500
  // src/session.ts
4501
+ import { createHash as createHash3 } from "crypto";
3770
4502
  var DEFAULT_SESSION_CONFIG = {
3771
- enabled: false,
4503
+ enabled: true,
3772
4504
  timeoutMs: 30 * 60 * 1e3,
3773
4505
  // 30 minutes
3774
4506
  headerName: "x-session-id"
@@ -3823,7 +4555,10 @@ var SessionStore = class {
3823
4555
  tier,
3824
4556
  createdAt: now,
3825
4557
  lastUsedAt: now,
3826
- requestCount: 1
4558
+ requestCount: 1,
4559
+ recentHashes: [],
4560
+ strikes: 0,
4561
+ escalated: false
3827
4562
  });
3828
4563
  }
3829
4564
  }
@@ -3875,6 +4610,43 @@ var SessionStore = class {
3875
4610
  }
3876
4611
  }
3877
4612
  }
4613
+ /**
4614
+ * Record a request content hash and detect repetitive patterns.
4615
+ * Returns true if escalation should be triggered (3+ consecutive similar requests).
4616
+ */
4617
+ recordRequestHash(sessionId, hash) {
4618
+ const entry = this.sessions.get(sessionId);
4619
+ if (!entry) return false;
4620
+ const prev = entry.recentHashes;
4621
+ if (prev.length > 0 && prev[prev.length - 1] === hash) {
4622
+ entry.strikes++;
4623
+ } else {
4624
+ entry.strikes = 0;
4625
+ }
4626
+ entry.recentHashes.push(hash);
4627
+ if (entry.recentHashes.length > 3) {
4628
+ entry.recentHashes.shift();
4629
+ }
4630
+ return entry.strikes >= 2 && !entry.escalated;
4631
+ }
4632
+ /**
4633
+ * Escalate session to next tier. Returns the new model/tier or null if already at max.
4634
+ */
4635
+ escalateSession(sessionId, tierConfigs) {
4636
+ const entry = this.sessions.get(sessionId);
4637
+ if (!entry) return null;
4638
+ const TIER_ORDER = ["SIMPLE", "MEDIUM", "COMPLEX", "REASONING"];
4639
+ const currentIdx = TIER_ORDER.indexOf(entry.tier);
4640
+ if (currentIdx < 0 || currentIdx >= TIER_ORDER.length - 1) return null;
4641
+ const nextTier = TIER_ORDER[currentIdx + 1];
4642
+ const nextConfig = tierConfigs[nextTier];
4643
+ if (!nextConfig) return null;
4644
+ entry.model = nextConfig.primary;
4645
+ entry.tier = nextTier;
4646
+ entry.strikes = 0;
4647
+ entry.escalated = true;
4648
+ return { model: nextConfig.primary, tier: nextTier };
4649
+ }
3878
4650
  /**
3879
4651
  * Stop the cleanup interval.
3880
4652
  */
@@ -3895,6 +4667,17 @@ function getSessionId(headers, headerName = DEFAULT_SESSION_CONFIG.headerName) {
3895
4667
  }
3896
4668
  return void 0;
3897
4669
  }
4670
+ function deriveSessionId(messages) {
4671
+ const firstUser = messages.find((m) => m.role === "user");
4672
+ if (!firstUser) return void 0;
4673
+ const content = typeof firstUser.content === "string" ? firstUser.content : JSON.stringify(firstUser.content);
4674
+ return createHash3("sha256").update(content).digest("hex").slice(0, 8);
4675
+ }
4676
+ function hashRequestContent(lastUserContent, toolCallNames) {
4677
+ const normalized = lastUserContent.replace(/\s+/g, " ").trim().slice(0, 500);
4678
+ const toolSuffix = toolCallNames?.length ? `|tools:${toolCallNames.sort().join(",")}` : "";
4679
+ return createHash3("sha256").update(normalized + toolSuffix).digest("hex").slice(0, 12);
4680
+ }
3898
4681
 
3899
4682
  // src/updater.ts
3900
4683
  var NPM_REGISTRY = "https://registry.npmjs.org/@blockrun/clawrouter/latest";
@@ -4652,13 +5435,36 @@ async function proxyPartnerRequest(req, res, apiBase, payFetch) {
4652
5435
  }).catch(() => {
4653
5436
  });
4654
5437
  }
5438
+ async function uploadDataUriToHost(dataUri) {
5439
+ const match = dataUri.match(/^data:(image\/\w+);base64,(.+)$/);
5440
+ if (!match) throw new Error("Invalid data URI format");
5441
+ const [, mimeType, b64Data] = match;
5442
+ const ext = mimeType === "image/jpeg" ? "jpg" : mimeType.split("/")[1] ?? "png";
5443
+ const buffer = Buffer.from(b64Data, "base64");
5444
+ const blob = new Blob([buffer], { type: mimeType });
5445
+ const form = new FormData();
5446
+ form.append("reqtype", "fileupload");
5447
+ form.append("fileToUpload", blob, `image.${ext}`);
5448
+ const resp = await fetch("https://catbox.moe/user/api.php", {
5449
+ method: "POST",
5450
+ body: form
5451
+ });
5452
+ if (!resp.ok) throw new Error(`catbox.moe upload failed: HTTP ${resp.status}`);
5453
+ const result = await resp.text();
5454
+ if (result.startsWith("https://")) {
5455
+ return result.trim();
5456
+ }
5457
+ throw new Error(`catbox.moe upload failed: ${result}`);
5458
+ }
4655
5459
  async function startProxy(options) {
4656
5460
  const walletKey = typeof options.wallet === "string" ? options.wallet : options.wallet.key;
4657
5461
  const solanaPrivateKeyBytes = typeof options.wallet === "string" ? void 0 : options.wallet.solanaPrivateKeyBytes;
4658
5462
  const paymentChain = options.paymentChain ?? await resolvePaymentChain();
4659
5463
  const apiBase = options.apiBase ?? (paymentChain === "solana" && solanaPrivateKeyBytes ? BLOCKRUN_SOLANA_API : BLOCKRUN_API);
4660
5464
  if (paymentChain === "solana" && !solanaPrivateKeyBytes) {
4661
- console.warn(`[ClawRouter] Payment chain is Solana but no Solana keys provided. Using Base (EVM).`);
5465
+ console.warn(
5466
+ `[ClawRouter] Payment chain is Solana but no Solana keys provided. Using Base (EVM).`
5467
+ );
4662
5468
  } else if (paymentChain === "solana") {
4663
5469
  console.log(`[ClawRouter] Payment chain: Solana (${BLOCKRUN_SOLANA_API})`);
4664
5470
  }
@@ -4666,7 +5472,6 @@ async function startProxy(options) {
4666
5472
  const existingProxy = await checkExistingProxy(listenPort);
4667
5473
  if (existingProxy) {
4668
5474
  const account2 = privateKeyToAccount3(walletKey);
4669
- const balanceMonitor2 = new BalanceMonitor(account2.address);
4670
5475
  const baseUrl2 = `http://127.0.0.1:${listenPort}`;
4671
5476
  if (existingProxy.wallet !== account2.address) {
4672
5477
  console.warn(
@@ -4680,7 +5485,9 @@ async function startProxy(options) {
4680
5485
  );
4681
5486
  }
4682
5487
  } else if (paymentChain !== "base") {
4683
- console.warn(`[ClawRouter] Existing proxy on port ${listenPort} does not report paymentChain (pre-v0.11 instance). Assuming Base.`);
5488
+ console.warn(
5489
+ `[ClawRouter] Existing proxy on port ${listenPort} does not report paymentChain (pre-v0.11 instance). Assuming Base.`
5490
+ );
4684
5491
  throw new Error(
4685
5492
  `Existing proxy on port ${listenPort} is a pre-v0.11 instance (assumed Base) but ${paymentChain} was requested. Stop the existing proxy first or use a different port.`
4686
5493
  );
@@ -4691,6 +5498,7 @@ async function startProxy(options) {
4691
5498
  const solanaSigner = await createKeyPairSignerFromPrivateKeyBytes(solanaPrivateKeyBytes);
4692
5499
  reuseSolanaAddress = solanaSigner.address;
4693
5500
  }
5501
+ const balanceMonitor2 = paymentChain === "solana" && reuseSolanaAddress ? new SolanaBalanceMonitor(reuseSolanaAddress) : new BalanceMonitor(account2.address);
4694
5502
  options.onReady?.(listenPort);
4695
5503
  return {
4696
5504
  port: listenPort,
@@ -4721,8 +5529,10 @@ async function startProxy(options) {
4721
5529
  const chain = network.startsWith("eip155") ? "Base (EVM)" : network.startsWith("solana") ? "Solana" : network;
4722
5530
  console.log(`[ClawRouter] Payment signed on ${chain} (${network})`);
4723
5531
  });
4724
- const payFetch = createPayFetchWithPreAuth(fetch, x402);
4725
- const balanceMonitor = new BalanceMonitor(account.address);
5532
+ const payFetch = createPayFetchWithPreAuth(fetch, x402, void 0, {
5533
+ skipPreAuth: paymentChain === "solana"
5534
+ });
5535
+ const balanceMonitor = paymentChain === "solana" && solanaAddress ? new SolanaBalanceMonitor(solanaAddress) : new BalanceMonitor(account.address);
4726
5536
  const routingConfig = mergeRoutingConfig(options.routingConfig);
4727
5537
  const modelPricing = buildModelPricing();
4728
5538
  const routerOpts = {
@@ -4876,7 +5686,11 @@ async function startProxy(options) {
4876
5686
  const existingProxy2 = await checkExistingProxy(listenPort);
4877
5687
  if (existingProxy2) {
4878
5688
  console.log(`[ClawRouter] Existing proxy detected on port ${listenPort}, reusing`);
4879
- rejectAttempt({ code: "REUSE_EXISTING", wallet: existingProxy2.wallet, existingChain: existingProxy2.paymentChain });
5689
+ rejectAttempt({
5690
+ code: "REUSE_EXISTING",
5691
+ wallet: existingProxy2.wallet,
5692
+ existingChain: existingProxy2.paymentChain
5693
+ });
4880
5694
  return;
4881
5695
  }
4882
5696
  if (attempt < PORT_RETRY_ATTEMPTS) {
@@ -4911,7 +5725,8 @@ async function startProxy(options) {
4911
5725
  if (error.code === "REUSE_EXISTING" && error.wallet) {
4912
5726
  if (error.existingChain && error.existingChain !== paymentChain) {
4913
5727
  throw new Error(
4914
- `Existing proxy on port ${listenPort} is using ${error.existingChain} but ${paymentChain} was requested. Stop the existing proxy first or use a different port.`
5728
+ `Existing proxy on port ${listenPort} is using ${error.existingChain} but ${paymentChain} was requested. Stop the existing proxy first or use a different port.`,
5729
+ { cause: err }
4915
5730
  );
4916
5731
  }
4917
5732
  const baseUrl2 = `http://127.0.0.1:${listenPort}`;
@@ -5019,15 +5834,12 @@ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxT
5019
5834
  } catch {
5020
5835
  }
5021
5836
  try {
5022
- const response = await payFetch(
5023
- upstreamUrl,
5024
- {
5025
- method,
5026
- headers,
5027
- body: requestBody.length > 0 ? new Uint8Array(requestBody) : void 0,
5028
- signal
5029
- }
5030
- );
5837
+ const response = await payFetch(upstreamUrl, {
5838
+ method,
5839
+ headers,
5840
+ body: requestBody.length > 0 ? new Uint8Array(requestBody) : void 0,
5841
+ signal
5842
+ });
5031
5843
  if (response.status !== 200) {
5032
5844
  const errorBody = await response.text();
5033
5845
  const isProviderErr = isProviderError(response.status, errorBody);
@@ -5075,14 +5887,19 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
5075
5887
  }
5076
5888
  let body = Buffer.concat(bodyChunks);
5077
5889
  const originalContextSizeKB = Math.ceil(body.length / 1024);
5890
+ const debugMode = req.headers["x-clawrouter-debug"] !== "false";
5078
5891
  let routingDecision;
5892
+ let hasTools = false;
5893
+ let hasVision = false;
5079
5894
  let isStreaming = false;
5080
5895
  let modelId = "";
5081
5896
  let maxTokens = 4096;
5082
5897
  let routingProfile = null;
5083
5898
  let accumulatedContent = "";
5899
+ let responseInputTokens;
5084
5900
  const isChatCompletion = req.url?.includes("/chat/completions");
5085
5901
  const sessionId = getSessionId(req.headers);
5902
+ let effectiveSessionId = sessionId;
5086
5903
  if (isChatCompletion && body.length > 0) {
5087
5904
  try {
5088
5905
  const parsed = JSON.parse(body.toString());
@@ -5090,10 +5907,12 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
5090
5907
  modelId = parsed.model || "";
5091
5908
  maxTokens = parsed.max_tokens || 4096;
5092
5909
  let bodyModified = false;
5093
- if (sessionId && Array.isArray(parsed.messages)) {
5094
- const messages = parsed.messages;
5095
- const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
5096
- const lastContent = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : "";
5910
+ const parsedMessages = Array.isArray(parsed.messages) ? parsed.messages : [];
5911
+ const lastUserMsg = [...parsedMessages].reverse().find((m) => m.role === "user");
5912
+ const rawLastContent = lastUserMsg?.content;
5913
+ const lastContent = typeof rawLastContent === "string" ? rawLastContent : Array.isArray(rawLastContent) ? rawLastContent.filter((b) => b.type === "text").map((b) => b.text ?? "").join(" ") : "";
5914
+ if (sessionId && parsedMessages.length > 0) {
5915
+ const messages = parsedMessages;
5097
5916
  if (sessionJournal.needsContext(lastContent)) {
5098
5917
  const journalText = sessionJournal.format(sessionId);
5099
5918
  if (journalText) {
@@ -5114,6 +5933,303 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
5114
5933
  }
5115
5934
  }
5116
5935
  }
5936
+ if (lastContent.startsWith("/debug")) {
5937
+ const debugPrompt = lastContent.slice("/debug".length).trim() || "hello";
5938
+ const messages = parsed.messages;
5939
+ const systemMsg = messages?.find((m) => m.role === "system");
5940
+ const systemPrompt = typeof systemMsg?.content === "string" ? systemMsg.content : void 0;
5941
+ const fullText = `${systemPrompt ?? ""} ${debugPrompt}`;
5942
+ const estimatedTokens = Math.ceil(fullText.length / 4);
5943
+ const normalizedModel2 = typeof parsed.model === "string" ? parsed.model.trim().toLowerCase() : "";
5944
+ const profileName = normalizedModel2.replace("blockrun/", "");
5945
+ const debugProfile = ["free", "eco", "auto", "premium"].includes(profileName) ? profileName : "auto";
5946
+ const scoring = classifyByRules(
5947
+ debugPrompt,
5948
+ systemPrompt,
5949
+ estimatedTokens,
5950
+ DEFAULT_ROUTING_CONFIG.scoring
5951
+ );
5952
+ const debugRouting = route(debugPrompt, systemPrompt, maxTokens, {
5953
+ ...routerOpts,
5954
+ routingProfile: debugProfile
5955
+ });
5956
+ const dimLines = (scoring.dimensions ?? []).map((d) => {
5957
+ const nameStr = (d.name + ":").padEnd(24);
5958
+ const scoreStr = d.score.toFixed(2).padStart(6);
5959
+ const sigStr = d.signal ? ` [${d.signal}]` : "";
5960
+ return ` ${nameStr}${scoreStr}${sigStr}`;
5961
+ }).join("\n");
5962
+ const sess = sessionId ? sessionStore.getSession(sessionId) : void 0;
5963
+ const sessLine = sess ? `Session: ${sessionId.slice(0, 8)}... \u2192 pinned: ${sess.model} (${sess.requestCount} requests)` : sessionId ? `Session: ${sessionId.slice(0, 8)}... \u2192 no pinned model` : "Session: none";
5964
+ const { simpleMedium, mediumComplex, complexReasoning } = DEFAULT_ROUTING_CONFIG.scoring.tierBoundaries;
5965
+ const debugText = [
5966
+ "ClawRouter Debug",
5967
+ "",
5968
+ `Profile: ${debugProfile} | Tier: ${debugRouting.tier} | Model: ${debugRouting.model}`,
5969
+ `Confidence: ${debugRouting.confidence.toFixed(2)} | Cost: $${debugRouting.costEstimate.toFixed(4)} | Savings: ${(debugRouting.savings * 100).toFixed(0)}%`,
5970
+ `Reasoning: ${debugRouting.reasoning}`,
5971
+ "",
5972
+ `Scoring (weighted: ${scoring.score.toFixed(3)})`,
5973
+ dimLines,
5974
+ "",
5975
+ `Tier Boundaries: SIMPLE <${simpleMedium.toFixed(2)} | MEDIUM <${mediumComplex.toFixed(2)} | COMPLEX <${complexReasoning.toFixed(2)} | REASONING >=${complexReasoning.toFixed(2)}`,
5976
+ "",
5977
+ sessLine
5978
+ ].join("\n");
5979
+ const completionId = `chatcmpl-debug-${Date.now()}`;
5980
+ const timestamp = Math.floor(Date.now() / 1e3);
5981
+ const syntheticResponse = {
5982
+ id: completionId,
5983
+ object: "chat.completion",
5984
+ created: timestamp,
5985
+ model: "clawrouter/debug",
5986
+ choices: [
5987
+ {
5988
+ index: 0,
5989
+ message: { role: "assistant", content: debugText },
5990
+ finish_reason: "stop"
5991
+ }
5992
+ ],
5993
+ usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
5994
+ };
5995
+ if (isStreaming) {
5996
+ res.writeHead(200, {
5997
+ "Content-Type": "text/event-stream",
5998
+ "Cache-Control": "no-cache",
5999
+ Connection: "keep-alive"
6000
+ });
6001
+ const sseChunk = {
6002
+ id: completionId,
6003
+ object: "chat.completion.chunk",
6004
+ created: timestamp,
6005
+ model: "clawrouter/debug",
6006
+ choices: [
6007
+ {
6008
+ index: 0,
6009
+ delta: { role: "assistant", content: debugText },
6010
+ finish_reason: null
6011
+ }
6012
+ ]
6013
+ };
6014
+ const sseDone = {
6015
+ id: completionId,
6016
+ object: "chat.completion.chunk",
6017
+ created: timestamp,
6018
+ model: "clawrouter/debug",
6019
+ choices: [{ index: 0, delta: {}, finish_reason: "stop" }]
6020
+ };
6021
+ res.write(`data: ${JSON.stringify(sseChunk)}
6022
+
6023
+ `);
6024
+ res.write(`data: ${JSON.stringify(sseDone)}
6025
+
6026
+ `);
6027
+ res.write("data: [DONE]\n\n");
6028
+ res.end();
6029
+ } else {
6030
+ res.writeHead(200, { "Content-Type": "application/json" });
6031
+ res.end(JSON.stringify(syntheticResponse));
6032
+ }
6033
+ console.log(`[ClawRouter] /debug command \u2192 ${debugRouting.tier} | ${debugRouting.model}`);
6034
+ return;
6035
+ }
6036
+ if (lastContent.startsWith("/imagegen")) {
6037
+ const imageArgs = lastContent.slice("/imagegen".length).trim();
6038
+ let imageModel = "google/nano-banana";
6039
+ let imageSize = "1024x1024";
6040
+ let imagePrompt = imageArgs;
6041
+ const modelMatch = imageArgs.match(/--model\s+(\S+)/);
6042
+ if (modelMatch) {
6043
+ const raw = modelMatch[1];
6044
+ const IMAGE_MODEL_ALIASES = {
6045
+ "dall-e-3": "openai/dall-e-3",
6046
+ dalle3: "openai/dall-e-3",
6047
+ dalle: "openai/dall-e-3",
6048
+ "gpt-image": "openai/gpt-image-1",
6049
+ "gpt-image-1": "openai/gpt-image-1",
6050
+ flux: "black-forest/flux-1.1-pro",
6051
+ "flux-pro": "black-forest/flux-1.1-pro",
6052
+ banana: "google/nano-banana",
6053
+ "nano-banana": "google/nano-banana",
6054
+ "banana-pro": "google/nano-banana-pro",
6055
+ "nano-banana-pro": "google/nano-banana-pro"
6056
+ };
6057
+ imageModel = IMAGE_MODEL_ALIASES[raw] ?? raw;
6058
+ imagePrompt = imagePrompt.replace(/--model\s+\S+/, "").trim();
6059
+ }
6060
+ const sizeMatch = imageArgs.match(/--size\s+(\d+x\d+)/);
6061
+ if (sizeMatch) {
6062
+ imageSize = sizeMatch[1];
6063
+ imagePrompt = imagePrompt.replace(/--size\s+\d+x\d+/, "").trim();
6064
+ }
6065
+ if (!imagePrompt) {
6066
+ const errorText = [
6067
+ "Usage: /imagegen <prompt>",
6068
+ "",
6069
+ "Options:",
6070
+ " --model <model> Model to use (default: nano-banana)",
6071
+ " --size <WxH> Image size (default: 1024x1024)",
6072
+ "",
6073
+ "Models:",
6074
+ " nano-banana Google Gemini Flash \u2014 $0.05/image",
6075
+ " banana-pro Google Gemini Pro \u2014 $0.10/image (up to 4K)",
6076
+ " dall-e-3 OpenAI DALL-E 3 \u2014 $0.04/image",
6077
+ " gpt-image OpenAI GPT Image 1 \u2014 $0.02/image",
6078
+ " flux Black Forest Flux 1.1 Pro \u2014 $0.04/image",
6079
+ "",
6080
+ "Examples:",
6081
+ " /imagegen a cat wearing sunglasses",
6082
+ " /imagegen --model dall-e-3 a futuristic city at sunset",
6083
+ " /imagegen --model banana-pro --size 2048x2048 mountain landscape"
6084
+ ].join("\n");
6085
+ const completionId = `chatcmpl-image-${Date.now()}`;
6086
+ const timestamp = Math.floor(Date.now() / 1e3);
6087
+ if (isStreaming) {
6088
+ res.writeHead(200, {
6089
+ "Content-Type": "text/event-stream",
6090
+ "Cache-Control": "no-cache",
6091
+ Connection: "keep-alive"
6092
+ });
6093
+ res.write(
6094
+ `data: ${JSON.stringify({ id: completionId, object: "chat.completion.chunk", created: timestamp, model: "clawrouter/image", choices: [{ index: 0, delta: { role: "assistant", content: errorText }, finish_reason: null }] })}
6095
+
6096
+ `
6097
+ );
6098
+ res.write(
6099
+ `data: ${JSON.stringify({ id: completionId, object: "chat.completion.chunk", created: timestamp, model: "clawrouter/image", choices: [{ index: 0, delta: {}, finish_reason: "stop" }] })}
6100
+
6101
+ `
6102
+ );
6103
+ res.write("data: [DONE]\n\n");
6104
+ res.end();
6105
+ } else {
6106
+ res.writeHead(200, { "Content-Type": "application/json" });
6107
+ res.end(
6108
+ JSON.stringify({
6109
+ id: completionId,
6110
+ object: "chat.completion",
6111
+ created: timestamp,
6112
+ model: "clawrouter/image",
6113
+ choices: [
6114
+ {
6115
+ index: 0,
6116
+ message: { role: "assistant", content: errorText },
6117
+ finish_reason: "stop"
6118
+ }
6119
+ ],
6120
+ usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
6121
+ })
6122
+ );
6123
+ }
6124
+ console.log(`[ClawRouter] /imagegen command \u2192 showing usage help`);
6125
+ return;
6126
+ }
6127
+ console.log(
6128
+ `[ClawRouter] /imagegen command \u2192 ${imageModel} (${imageSize}): ${imagePrompt.slice(0, 80)}...`
6129
+ );
6130
+ try {
6131
+ const imageUpstreamUrl = `${apiBase}/v1/images/generations`;
6132
+ const imageBody = JSON.stringify({
6133
+ model: imageModel,
6134
+ prompt: imagePrompt,
6135
+ size: imageSize,
6136
+ n: 1
6137
+ });
6138
+ const imageResponse = await payFetch(imageUpstreamUrl, {
6139
+ method: "POST",
6140
+ headers: { "content-type": "application/json", "user-agent": USER_AGENT },
6141
+ body: imageBody
6142
+ });
6143
+ const imageResult = await imageResponse.json();
6144
+ let responseText;
6145
+ if (!imageResponse.ok || imageResult.error) {
6146
+ const errMsg = typeof imageResult.error === "string" ? imageResult.error : imageResult.error?.message ?? `HTTP ${imageResponse.status}`;
6147
+ responseText = `Image generation failed: ${errMsg}`;
6148
+ console.log(`[ClawRouter] /imagegen error: ${errMsg}`);
6149
+ } else {
6150
+ const images = imageResult.data ?? [];
6151
+ if (images.length === 0) {
6152
+ responseText = "Image generation returned no results.";
6153
+ } else {
6154
+ const lines = [];
6155
+ for (const img of images) {
6156
+ if (img.url) {
6157
+ if (img.url.startsWith("data:")) {
6158
+ try {
6159
+ const hostedUrl = await uploadDataUriToHost(img.url);
6160
+ lines.push(hostedUrl);
6161
+ } catch (uploadErr) {
6162
+ console.error(
6163
+ `[ClawRouter] /imagegen: failed to upload data URI: ${uploadErr instanceof Error ? uploadErr.message : String(uploadErr)}`
6164
+ );
6165
+ lines.push(
6166
+ "Image generated but upload failed. Try again or use --model dall-e-3."
6167
+ );
6168
+ }
6169
+ } else {
6170
+ lines.push(img.url);
6171
+ }
6172
+ }
6173
+ if (img.revised_prompt) lines.push(`Revised prompt: ${img.revised_prompt}`);
6174
+ }
6175
+ lines.push("", `Model: ${imageModel} | Size: ${imageSize}`);
6176
+ responseText = lines.join("\n");
6177
+ }
6178
+ console.log(`[ClawRouter] /imagegen success: ${images.length} image(s) generated`);
6179
+ }
6180
+ const completionId = `chatcmpl-image-${Date.now()}`;
6181
+ const timestamp = Math.floor(Date.now() / 1e3);
6182
+ if (isStreaming) {
6183
+ res.writeHead(200, {
6184
+ "Content-Type": "text/event-stream",
6185
+ "Cache-Control": "no-cache",
6186
+ Connection: "keep-alive"
6187
+ });
6188
+ res.write(
6189
+ `data: ${JSON.stringify({ id: completionId, object: "chat.completion.chunk", created: timestamp, model: "clawrouter/image", choices: [{ index: 0, delta: { role: "assistant", content: responseText }, finish_reason: null }] })}
6190
+
6191
+ `
6192
+ );
6193
+ res.write(
6194
+ `data: ${JSON.stringify({ id: completionId, object: "chat.completion.chunk", created: timestamp, model: "clawrouter/image", choices: [{ index: 0, delta: {}, finish_reason: "stop" }] })}
6195
+
6196
+ `
6197
+ );
6198
+ res.write("data: [DONE]\n\n");
6199
+ res.end();
6200
+ } else {
6201
+ res.writeHead(200, { "Content-Type": "application/json" });
6202
+ res.end(
6203
+ JSON.stringify({
6204
+ id: completionId,
6205
+ object: "chat.completion",
6206
+ created: timestamp,
6207
+ model: "clawrouter/image",
6208
+ choices: [
6209
+ {
6210
+ index: 0,
6211
+ message: { role: "assistant", content: responseText },
6212
+ finish_reason: "stop"
6213
+ }
6214
+ ],
6215
+ usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
6216
+ })
6217
+ );
6218
+ }
6219
+ } catch (err) {
6220
+ const errMsg = err instanceof Error ? err.message : String(err);
6221
+ console.error(`[ClawRouter] /imagegen error: ${errMsg}`);
6222
+ if (!res.headersSent) {
6223
+ res.writeHead(500, { "Content-Type": "application/json" });
6224
+ res.end(
6225
+ JSON.stringify({
6226
+ error: { message: `Image generation failed: ${errMsg}`, type: "image_error" }
6227
+ })
6228
+ );
6229
+ }
6230
+ }
6231
+ return;
6232
+ }
5117
6233
  if (parsed.stream === true) {
5118
6234
  parsed.stream = false;
5119
6235
  bodyModified = true;
@@ -5154,54 +6270,118 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
5154
6270
  latencyMs: 0
5155
6271
  });
5156
6272
  } else {
5157
- const sessionId2 = getSessionId(
5158
- req.headers
5159
- );
5160
- const existingSession = sessionId2 ? sessionStore.getSession(sessionId2) : void 0;
6273
+ effectiveSessionId = getSessionId(req.headers) ?? deriveSessionId(parsedMessages);
6274
+ const existingSession = effectiveSessionId ? sessionStore.getSession(effectiveSessionId) : void 0;
6275
+ const rawPrompt = lastUserMsg?.content;
6276
+ const prompt = typeof rawPrompt === "string" ? rawPrompt : Array.isArray(rawPrompt) ? rawPrompt.filter((b) => b.type === "text").map((b) => b.text ?? "").join(" ") : "";
6277
+ const systemMsg = parsedMessages.find((m) => m.role === "system");
6278
+ const systemPrompt = typeof systemMsg?.content === "string" ? systemMsg.content : void 0;
6279
+ const tools = parsed.tools;
6280
+ hasTools = Array.isArray(tools) && tools.length > 0;
6281
+ if (hasTools && tools) {
6282
+ console.log(`[ClawRouter] Tools detected (${tools.length}), agentic mode via keywords`);
6283
+ }
6284
+ hasVision = parsedMessages.some((m) => {
6285
+ if (Array.isArray(m.content)) {
6286
+ return m.content.some((p) => p.type === "image_url");
6287
+ }
6288
+ return false;
6289
+ });
6290
+ if (hasVision) {
6291
+ console.log(`[ClawRouter] Vision content detected, filtering to vision-capable models`);
6292
+ }
6293
+ routingDecision = route(prompt, systemPrompt, maxTokens, {
6294
+ ...routerOpts,
6295
+ routingProfile: routingProfile ?? void 0
6296
+ });
5161
6297
  if (existingSession) {
5162
- console.log(
5163
- `[ClawRouter] Session ${sessionId2?.slice(0, 8)}... using pinned model: ${existingSession.model}`
5164
- );
5165
- parsed.model = existingSession.model;
5166
- modelId = existingSession.model;
5167
- bodyModified = true;
5168
- sessionStore.touchSession(sessionId2);
5169
- } else {
5170
- const messages = parsed.messages;
5171
- let lastUserMsg;
5172
- if (messages) {
5173
- for (let i = messages.length - 1; i >= 0; i--) {
5174
- if (messages[i].role === "user") {
5175
- lastUserMsg = messages[i];
5176
- break;
5177
- }
6298
+ const tierRank = {
6299
+ SIMPLE: 0,
6300
+ MEDIUM: 1,
6301
+ COMPLEX: 2,
6302
+ REASONING: 3
6303
+ };
6304
+ const existingRank = tierRank[existingSession.tier] ?? 0;
6305
+ const newRank = tierRank[routingDecision.tier] ?? 0;
6306
+ if (newRank > existingRank) {
6307
+ console.log(
6308
+ `[ClawRouter] Session ${effectiveSessionId?.slice(0, 8)}... upgrading: ${existingSession.tier} \u2192 ${routingDecision.tier} (${routingDecision.model})`
6309
+ );
6310
+ parsed.model = routingDecision.model;
6311
+ modelId = routingDecision.model;
6312
+ bodyModified = true;
6313
+ if (effectiveSessionId) {
6314
+ sessionStore.setSession(
6315
+ effectiveSessionId,
6316
+ routingDecision.model,
6317
+ routingDecision.tier
6318
+ );
5178
6319
  }
5179
- }
5180
- const systemMsg = messages?.find((m) => m.role === "system");
5181
- const prompt = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : "";
5182
- const systemPrompt = typeof systemMsg?.content === "string" ? systemMsg.content : void 0;
5183
- const tools = parsed.tools;
5184
- const hasTools = Array.isArray(tools) && tools.length > 0;
5185
- if (hasTools && tools) {
6320
+ } else {
5186
6321
  console.log(
5187
- `[ClawRouter] Tools detected (${tools.length}), agentic mode via keywords`
6322
+ `[ClawRouter] Session ${effectiveSessionId?.slice(0, 8)}... keeping pinned model: ${existingSession.model} (${existingSession.tier} >= ${routingDecision.tier})`
5188
6323
  );
6324
+ parsed.model = existingSession.model;
6325
+ modelId = existingSession.model;
6326
+ bodyModified = true;
6327
+ sessionStore.touchSession(effectiveSessionId);
6328
+ routingDecision = {
6329
+ ...routingDecision,
6330
+ model: existingSession.model,
6331
+ tier: existingSession.tier
6332
+ };
5189
6333
  }
5190
- routingDecision = route(prompt, systemPrompt, maxTokens, {
5191
- ...routerOpts,
5192
- routingProfile: routingProfile ?? void 0
5193
- });
6334
+ const lastAssistantMsg = [...parsedMessages].reverse().find((m) => m.role === "assistant");
6335
+ const assistantToolCalls = lastAssistantMsg?.tool_calls;
6336
+ const toolCallNames = Array.isArray(assistantToolCalls) ? assistantToolCalls.map((tc) => tc.function?.name).filter((n) => Boolean(n)) : void 0;
6337
+ const contentHash = hashRequestContent(prompt, toolCallNames);
6338
+ const shouldEscalate = sessionStore.recordRequestHash(effectiveSessionId, contentHash);
6339
+ if (shouldEscalate) {
6340
+ const activeTierConfigs = (() => {
6341
+ if (routingDecision.reasoning?.includes("agentic") && routerOpts.config.agenticTiers) {
6342
+ return routerOpts.config.agenticTiers;
6343
+ }
6344
+ if (routingProfile === "eco" && routerOpts.config.ecoTiers) {
6345
+ return routerOpts.config.ecoTiers;
6346
+ }
6347
+ if (routingProfile === "premium" && routerOpts.config.premiumTiers) {
6348
+ return routerOpts.config.premiumTiers;
6349
+ }
6350
+ return routerOpts.config.tiers;
6351
+ })();
6352
+ const escalation = sessionStore.escalateSession(
6353
+ effectiveSessionId,
6354
+ activeTierConfigs
6355
+ );
6356
+ if (escalation) {
6357
+ console.log(
6358
+ `[ClawRouter] \u26A1 3-strike escalation: ${existingSession.model} \u2192 ${escalation.model} (${existingSession.tier} \u2192 ${escalation.tier})`
6359
+ );
6360
+ parsed.model = escalation.model;
6361
+ modelId = escalation.model;
6362
+ routingDecision = {
6363
+ ...routingDecision,
6364
+ model: escalation.model,
6365
+ tier: escalation.tier
6366
+ };
6367
+ }
6368
+ }
6369
+ } else {
5194
6370
  parsed.model = routingDecision.model;
5195
6371
  modelId = routingDecision.model;
5196
6372
  bodyModified = true;
5197
- if (sessionId2) {
5198
- sessionStore.setSession(sessionId2, routingDecision.model, routingDecision.tier);
6373
+ if (effectiveSessionId) {
6374
+ sessionStore.setSession(
6375
+ effectiveSessionId,
6376
+ routingDecision.model,
6377
+ routingDecision.tier
6378
+ );
5199
6379
  console.log(
5200
- `[ClawRouter] Session ${sessionId2.slice(0, 8)}... pinned to model: ${routingDecision.model}`
6380
+ `[ClawRouter] Session ${effectiveSessionId.slice(0, 8)}... pinned to model: ${routingDecision.model}`
5201
6381
  );
5202
6382
  }
5203
- options.onRouted?.(routingDecision);
5204
6383
  }
6384
+ options.onRouted?.(routingDecision);
5205
6385
  }
5206
6386
  }
5207
6387
  if (bodyModified) {
@@ -5294,6 +6474,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
5294
6474
  }
5295
6475
  deduplicator.markInflight(dedupKey);
5296
6476
  let estimatedCostMicros;
6477
+ let balanceFallbackNotice;
5297
6478
  const isFreeModel = modelId === FREE_MODEL;
5298
6479
  if (modelId && !options.skipBalanceCheck && !isFreeModel) {
5299
6480
  const estimated = estimateAmount(modelId, body.length, maxTokens);
@@ -5304,12 +6485,17 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
5304
6485
  if (sufficiency.info.isEmpty || !sufficiency.sufficient) {
5305
6486
  const originalModel = modelId;
5306
6487
  console.log(
5307
- `[ClawRouter] Wallet ${sufficiency.info.isEmpty ? "empty" : "insufficient"} ($${sufficiency.info.balanceUSD}), falling back to free model: ${FREE_MODEL} (requested: ${originalModel})`
6488
+ `[ClawRouter] Wallet ${sufficiency.info.isEmpty ? "empty" : "insufficient"} (${sufficiency.info.balanceUSD}), falling back to free model: ${FREE_MODEL} (requested: ${originalModel})`
5308
6489
  );
5309
6490
  modelId = FREE_MODEL;
5310
6491
  const parsed = JSON.parse(body.toString());
5311
6492
  parsed.model = FREE_MODEL;
5312
6493
  body = Buffer.from(JSON.stringify(parsed));
6494
+ balanceFallbackNotice = sufficiency.info.isEmpty ? `> **\u26A0\uFE0F Wallet empty** \u2014 using free model. Fund your wallet to use ${originalModel}.
6495
+
6496
+ ` : `> **\u26A0\uFE0F Insufficient balance** (${sufficiency.info.balanceUSD}) \u2014 using free model instead of ${originalModel}.
6497
+
6498
+ `;
5313
6499
  options.onLowBalance?.({
5314
6500
  balanceUSD: sufficiency.info.balanceUSD,
5315
6501
  walletAddress: sufficiency.info.walletAddress
@@ -5373,8 +6559,18 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
5373
6559
  if (routingDecision) {
5374
6560
  const estimatedInputTokens = Math.ceil(body.length / 4);
5375
6561
  const estimatedTotalTokens = estimatedInputTokens + maxTokens;
5376
- const useAgenticTiers = routingDecision.reasoning?.includes("agentic") && routerOpts.config.agenticTiers;
5377
- const tierConfigs = useAgenticTiers ? routerOpts.config.agenticTiers : routerOpts.config.tiers;
6562
+ const tierConfigs = (() => {
6563
+ if (routingDecision.reasoning?.includes("agentic") && routerOpts.config.agenticTiers) {
6564
+ return routerOpts.config.agenticTiers;
6565
+ }
6566
+ if (routingProfile === "eco" && routerOpts.config.ecoTiers) {
6567
+ return routerOpts.config.ecoTiers;
6568
+ }
6569
+ if (routingProfile === "premium" && routerOpts.config.premiumTiers) {
6570
+ return routerOpts.config.premiumTiers;
6571
+ }
6572
+ return routerOpts.config.tiers;
6573
+ })();
5378
6574
  const fullChain = getFallbackChain(routingDecision.tier, tierConfigs);
5379
6575
  const contextFiltered = getFallbackChainFiltered(
5380
6576
  routingDecision.tier,
@@ -5388,14 +6584,27 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
5388
6584
  `[ClawRouter] Context filter (~${estimatedTotalTokens} tokens): excluded ${contextExcluded.join(", ")}`
5389
6585
  );
5390
6586
  }
5391
- modelsToTry = contextFiltered.slice(0, MAX_FALLBACK_ATTEMPTS);
6587
+ const toolFiltered = filterByToolCalling(contextFiltered, hasTools, supportsToolCalling);
6588
+ const toolExcluded = contextFiltered.filter((m) => !toolFiltered.includes(m));
6589
+ if (toolExcluded.length > 0) {
6590
+ console.log(
6591
+ `[ClawRouter] Tool-calling filter: excluded ${toolExcluded.join(", ")} (no structured function call support)`
6592
+ );
6593
+ }
6594
+ const visionFiltered = filterByVision(toolFiltered, hasVision, supportsVision);
6595
+ const visionExcluded = toolFiltered.filter((m) => !visionFiltered.includes(m));
6596
+ if (visionExcluded.length > 0) {
6597
+ console.log(
6598
+ `[ClawRouter] Vision filter: excluded ${visionExcluded.join(", ")} (no vision support)`
6599
+ );
6600
+ }
6601
+ modelsToTry = visionFiltered.slice(0, MAX_FALLBACK_ATTEMPTS);
5392
6602
  modelsToTry = prioritizeNonRateLimited(modelsToTry);
5393
6603
  } else {
5394
- if (modelId && modelId !== FREE_MODEL) {
5395
- modelsToTry = [modelId, FREE_MODEL];
5396
- } else {
5397
- modelsToTry = modelId ? [modelId] : [];
5398
- }
6604
+ modelsToTry = modelId ? [modelId] : [];
6605
+ }
6606
+ if (!modelsToTry.includes(FREE_MODEL)) {
6607
+ modelsToTry.push(FREE_MODEL);
5399
6608
  }
5400
6609
  let upstream;
5401
6610
  let lastError;
@@ -5429,6 +6638,17 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
5429
6638
  if (result.errorStatus === 429) {
5430
6639
  markRateLimited(tryModel);
5431
6640
  }
6641
+ const isPaymentErr = /payment.*verification.*failed|insufficient.*funds/i.test(
6642
+ result.errorBody || ""
6643
+ );
6644
+ if (isPaymentErr && tryModel !== FREE_MODEL) {
6645
+ const freeIdx = modelsToTry.indexOf(FREE_MODEL);
6646
+ if (freeIdx > i + 1) {
6647
+ console.log(`[ClawRouter] Payment error \u2014 skipping to free model: ${FREE_MODEL}`);
6648
+ i = freeIdx - 1;
6649
+ continue;
6650
+ }
6651
+ }
5432
6652
  console.log(
5433
6653
  `[ClawRouter] Provider error from ${tryModel}, trying fallback: ${result.errorBody?.slice(0, 100)}`
5434
6654
  );
@@ -5446,6 +6666,12 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
5446
6666
  clearInterval(heartbeatInterval);
5447
6667
  heartbeatInterval = void 0;
5448
6668
  }
6669
+ if (debugMode && headersSentEarly && routingDecision) {
6670
+ const debugComment = `: x-clawrouter-debug profile=${routingProfile ?? "auto"} tier=${routingDecision.tier} model=${actualModelUsed} agentic=${routingDecision.agenticScore?.toFixed(2) ?? "n/a"} confidence=${routingDecision.confidence.toFixed(2)} reasoning=${routingDecision.reasoning}
6671
+
6672
+ `;
6673
+ safeWrite(res, debugComment);
6674
+ }
5449
6675
  if (routingDecision && actualModelUsed !== routingDecision.model) {
5450
6676
  const estimatedInputTokens = Math.ceil(body.length / 4);
5451
6677
  const newCosts = calculateModelCost(
@@ -5464,6 +6690,12 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
5464
6690
  savings: newCosts.savings
5465
6691
  };
5466
6692
  options.onRouted?.(routingDecision);
6693
+ if (effectiveSessionId) {
6694
+ sessionStore.setSession(effectiveSessionId, actualModelUsed, routingDecision.tier);
6695
+ console.log(
6696
+ `[ClawRouter] Session ${effectiveSessionId.slice(0, 8)}... updated pin to fallback: ${actualModelUsed}`
6697
+ );
6698
+ }
5467
6699
  }
5468
6700
  if (!upstream) {
5469
6701
  const rawErrBody = lastError?.body || "All models in fallback chain failed";
@@ -5526,6 +6758,10 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
5526
6758
  const jsonStr = jsonBody.toString();
5527
6759
  try {
5528
6760
  const rsp = JSON.parse(jsonStr);
6761
+ if (rsp.usage && typeof rsp.usage === "object") {
6762
+ const u = rsp.usage;
6763
+ if (typeof u.prompt_tokens === "number") responseInputTokens = u.prompt_tokens;
6764
+ }
5529
6765
  const baseChunk = {
5530
6766
  id: rsp.id ?? `chatcmpl-${Date.now()}`,
5531
6767
  object: "chat.completion.chunk",
@@ -5551,6 +6787,25 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
5551
6787
  `;
5552
6788
  safeWrite(res, roleData);
5553
6789
  responseChunks.push(Buffer.from(roleData));
6790
+ if (balanceFallbackNotice) {
6791
+ const noticeChunk = {
6792
+ ...baseChunk,
6793
+ choices: [
6794
+ {
6795
+ index,
6796
+ delta: { content: balanceFallbackNotice },
6797
+ logprobs: null,
6798
+ finish_reason: null
6799
+ }
6800
+ ]
6801
+ };
6802
+ const noticeData = `data: ${JSON.stringify(noticeChunk)}
6803
+
6804
+ `;
6805
+ safeWrite(res, noticeData);
6806
+ responseChunks.push(Buffer.from(noticeData));
6807
+ balanceFallbackNotice = void 0;
6808
+ }
5554
6809
  if (content) {
5555
6810
  const contentChunk = {
5556
6811
  ...baseChunk,
@@ -5625,23 +6880,46 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
5625
6880
  });
5626
6881
  responseHeaders["x-context-used-kb"] = String(originalContextSizeKB);
5627
6882
  responseHeaders["x-context-limit-kb"] = String(CONTEXT_LIMIT_KB);
5628
- res.writeHead(upstream.status, responseHeaders);
6883
+ if (debugMode && routingDecision) {
6884
+ responseHeaders["x-clawrouter-profile"] = routingProfile ?? "auto";
6885
+ responseHeaders["x-clawrouter-tier"] = routingDecision.tier;
6886
+ responseHeaders["x-clawrouter-model"] = actualModelUsed;
6887
+ responseHeaders["x-clawrouter-confidence"] = routingDecision.confidence.toFixed(2);
6888
+ responseHeaders["x-clawrouter-reasoning"] = routingDecision.reasoning;
6889
+ if (routingDecision.agenticScore !== void 0) {
6890
+ responseHeaders["x-clawrouter-agentic-score"] = routingDecision.agenticScore.toFixed(2);
6891
+ }
6892
+ }
6893
+ const bodyParts = [];
5629
6894
  if (upstream.body) {
5630
6895
  const reader = upstream.body.getReader();
5631
6896
  try {
5632
6897
  while (true) {
5633
6898
  const { done, value } = await reader.read();
5634
6899
  if (done) break;
5635
- const chunk = Buffer.from(value);
5636
- safeWrite(res, chunk);
5637
- responseChunks.push(chunk);
6900
+ bodyParts.push(Buffer.from(value));
5638
6901
  }
5639
6902
  } finally {
5640
6903
  reader.releaseLock();
5641
6904
  }
5642
6905
  }
6906
+ let responseBody = Buffer.concat(bodyParts);
6907
+ if (balanceFallbackNotice && responseBody.length > 0) {
6908
+ try {
6909
+ const parsed = JSON.parse(responseBody.toString());
6910
+ if (parsed.choices?.[0]?.message?.content !== void 0) {
6911
+ parsed.choices[0].message.content = balanceFallbackNotice + parsed.choices[0].message.content;
6912
+ responseBody = Buffer.from(JSON.stringify(parsed));
6913
+ }
6914
+ } catch {
6915
+ }
6916
+ balanceFallbackNotice = void 0;
6917
+ }
6918
+ responseHeaders["content-length"] = String(responseBody.length);
6919
+ res.writeHead(upstream.status, responseHeaders);
6920
+ safeWrite(res, responseBody);
6921
+ responseChunks.push(responseBody);
5643
6922
  res.end();
5644
- const responseBody = Buffer.concat(responseChunks);
5645
6923
  deduplicator.complete(dedupKey, {
5646
6924
  status: upstream.status,
5647
6925
  headers: responseHeaders,
@@ -5664,6 +6942,10 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
5664
6942
  if (rspJson.choices?.[0]?.message?.content) {
5665
6943
  accumulatedContent = rspJson.choices[0].message.content;
5666
6944
  }
6945
+ if (rspJson.usage && typeof rspJson.usage === "object") {
6946
+ if (typeof rspJson.usage.prompt_tokens === "number")
6947
+ responseInputTokens = rspJson.usage.prompt_tokens;
6948
+ }
5667
6949
  } catch {
5668
6950
  }
5669
6951
  }
@@ -5689,7 +6971,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
5689
6971
  deduplicator.removeInflight(dedupKey);
5690
6972
  balanceMonitor.invalidate();
5691
6973
  if (err instanceof Error && err.name === "AbortError") {
5692
- throw new Error(`Request timed out after ${timeoutMs}ms`);
6974
+ throw new Error(`Request timed out after ${timeoutMs}ms`, { cause: err });
5693
6975
  }
5694
6976
  throw err;
5695
6977
  }
@@ -5712,13 +6994,53 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
5712
6994
  cost: costWithBuffer,
5713
6995
  baselineCost: baselineWithBuffer,
5714
6996
  savings: accurateCosts.savings,
5715
- latencyMs: Date.now() - startTime
6997
+ latencyMs: Date.now() - startTime,
6998
+ ...responseInputTokens !== void 0 && { inputTokens: responseInputTokens }
5716
6999
  };
5717
7000
  logUsage(entry).catch(() => {
5718
7001
  });
5719
7002
  }
5720
7003
  }
5721
7004
 
7005
+ // src/report.ts
7006
+ async function generateReport(period, json = false) {
7007
+ const days = period === "daily" ? 1 : period === "weekly" ? 7 : 30;
7008
+ const stats = await getStats(days);
7009
+ if (json) {
7010
+ return JSON.stringify(stats, null, 2);
7011
+ }
7012
+ return formatMarkdownReport(period, days, stats);
7013
+ }
7014
+ function formatMarkdownReport(period, days, stats) {
7015
+ const lines = [];
7016
+ lines.push(`# ClawRouter ${capitalize(period)} Report`);
7017
+ lines.push(`**Period:** Last ${days} day${days > 1 ? "s" : ""}`);
7018
+ lines.push(`**Generated:** ${(/* @__PURE__ */ new Date()).toISOString()}`);
7019
+ lines.push("");
7020
+ lines.push("## \u{1F4CA} Usage Summary");
7021
+ lines.push("");
7022
+ lines.push(`| Metric | Value |`);
7023
+ lines.push(`|--------|-------|`);
7024
+ lines.push(`| Total Requests | ${stats.totalRequests} |`);
7025
+ lines.push(`| Total Cost | $${stats.totalCost.toFixed(4)} |`);
7026
+ lines.push(`| Baseline Cost | $${stats.totalBaselineCost.toFixed(4)} |`);
7027
+ lines.push(`| **Savings** | **$${stats.totalSavings.toFixed(4)}** |`);
7028
+ lines.push(`| Savings % | ${stats.savingsPercentage.toFixed(1)}% |`);
7029
+ lines.push(`| Avg Latency | ${stats.avgLatencyMs.toFixed(0)}ms |`);
7030
+ lines.push("");
7031
+ lines.push("## \u{1F916} Model Distribution");
7032
+ lines.push("");
7033
+ const sortedModels = Object.entries(stats.byModel).sort((a, b) => b[1].count - a[1].count).slice(0, 10);
7034
+ for (const [model, data] of sortedModels) {
7035
+ lines.push(`- ${model}: ${data.count} reqs, $${data.cost.toFixed(4)}`);
7036
+ }
7037
+ lines.push("");
7038
+ return lines.join("\n");
7039
+ }
7040
+ function capitalize(str) {
7041
+ return str.charAt(0).toUpperCase() + str.slice(1);
7042
+ }
7043
+
5722
7044
  // src/doctor.ts
5723
7045
  import { platform, arch, freemem, totalmem } from "os";
5724
7046
  import { createPublicClient as createPublicClient3, http as http3 } from "viem";
@@ -5811,7 +7133,6 @@ async function collectNetworkInfo() {
5811
7133
  blockrunLatency = Date.now() - start;
5812
7134
  blockrunReachable = response.ok || response.status === 402;
5813
7135
  } catch {
5814
- blockrunReachable = false;
5815
7136
  }
5816
7137
  let proxyRunning = false;
5817
7138
  try {
@@ -5821,7 +7142,6 @@ async function collectNetworkInfo() {
5821
7142
  });
5822
7143
  proxyRunning = response.ok;
5823
7144
  } catch {
5824
- proxyRunning = false;
5825
7145
  }
5826
7146
  return {
5827
7147
  blockrunApi: { reachable: blockrunReachable, latencyMs: blockrunLatency },
@@ -5946,28 +7266,26 @@ async function analyzeWithAI(diagnostics, userQuestion, model = "sonnet") {
5946
7266
  const x402 = new x402Client2();
5947
7267
  registerExactEvmScheme2(x402, { signer: evmSigner });
5948
7268
  const paymentFetch = wrapFetchWithPayment(fetch, x402);
5949
- const response = await paymentFetch(
5950
- "https://blockrun.ai/api/v1/chat/completions",
5951
- {
5952
- method: "POST",
5953
- headers: { "Content-Type": "application/json" },
5954
- body: JSON.stringify({
5955
- model: modelConfig.id,
5956
- stream: false,
5957
- messages: [
5958
- {
5959
- role: "system",
5960
- content: `You are a technical support expert for BlockRun and ClawRouter.
7269
+ const response = await paymentFetch("https://blockrun.ai/api/v1/chat/completions", {
7270
+ method: "POST",
7271
+ headers: { "Content-Type": "application/json" },
7272
+ body: JSON.stringify({
7273
+ model: modelConfig.id,
7274
+ stream: false,
7275
+ messages: [
7276
+ {
7277
+ role: "system",
7278
+ content: `You are a technical support expert for BlockRun and ClawRouter.
5961
7279
  Analyze the diagnostics and:
5962
7280
  1. Identify the root cause of any issues
5963
7281
  2. Provide specific, actionable fix commands (bash)
5964
7282
  3. Explain why the issue occurred briefly
5965
7283
  4. Be concise but thorough
5966
7284
  5. Format commands in code blocks`
5967
- },
5968
- {
5969
- role: "user",
5970
- content: userQuestion ? `Here are my system diagnostics:
7285
+ },
7286
+ {
7287
+ role: "user",
7288
+ content: userQuestion ? `Here are my system diagnostics:
5971
7289
 
5972
7290
  ${JSON.stringify(diagnostics, null, 2)}
5973
7291
 
@@ -5976,12 +7294,11 @@ User's question: ${userQuestion}` : `Here are my system diagnostics:
5976
7294
  ${JSON.stringify(diagnostics, null, 2)}
5977
7295
 
5978
7296
  Please analyze and help me fix any issues.`
5979
- }
5980
- ],
5981
- max_tokens: 1e3
5982
- })
5983
- }
5984
- );
7297
+ }
7298
+ ],
7299
+ max_tokens: 1e3
7300
+ })
7301
+ });
5985
7302
  if (!response.ok) {
5986
7303
  const text = await response.text();
5987
7304
  console.log(`Error: ${response.status} - ${text}`);
@@ -6065,6 +7382,7 @@ Usage:
6065
7382
  clawrouter [options]
6066
7383
  clawrouter doctor [opus] [question]
6067
7384
  clawrouter partners [test]
7385
+ clawrouter report [daily|weekly|monthly] [--json]
6068
7386
 
6069
7387
  Options:
6070
7388
  --version, -v Show version number
@@ -6107,6 +7425,9 @@ function parseArgs(args) {
6107
7425
  doctor: false,
6108
7426
  partners: false,
6109
7427
  partnersTest: false,
7428
+ report: false,
7429
+ reportPeriod: "daily",
7430
+ reportJson: false,
6110
7431
  port: void 0
6111
7432
  };
6112
7433
  for (let i = 0; i < args.length; i++) {
@@ -6123,6 +7444,20 @@ function parseArgs(args) {
6123
7444
  result.partnersTest = true;
6124
7445
  i++;
6125
7446
  }
7447
+ } else if (arg === "report") {
7448
+ result.report = true;
7449
+ const next = args[i + 1];
7450
+ if (next && ["daily", "weekly", "monthly"].includes(next)) {
7451
+ result.reportPeriod = next;
7452
+ i++;
7453
+ if (args[i + 1] === "--json") {
7454
+ result.reportJson = true;
7455
+ i++;
7456
+ }
7457
+ } else if (next === "--json") {
7458
+ result.reportJson = true;
7459
+ i++;
7460
+ }
6126
7461
  } else if (arg === "--port" && args[i + 1]) {
6127
7462
  result.port = parseInt(args[i + 1], 10);
6128
7463
  i++;
@@ -6170,7 +7505,9 @@ ClawRouter Partner APIs (v${VERSION})
6170
7505
  console.log(` ${svc.description}`);
6171
7506
  console.log(` Tool: blockrun_${svc.id}`);
6172
7507
  console.log(` Method: ${svc.method} /v1${svc.proxyPath}`);
6173
- console.log(` Pricing: ${svc.pricing.perUnit} per ${svc.pricing.unit} (min ${svc.pricing.minimum}, max ${svc.pricing.maximum})`);
7508
+ console.log(
7509
+ ` Pricing: ${svc.pricing.perUnit} per ${svc.pricing.unit} (min ${svc.pricing.minimum}, max ${svc.pricing.maximum})`
7510
+ );
6174
7511
  console.log();
6175
7512
  }
6176
7513
  if (args.partnersTest) {
@@ -6191,6 +7528,11 @@ ClawRouter Partner APIs (v${VERSION})
6191
7528
  }
6192
7529
  process.exit(0);
6193
7530
  }
7531
+ if (args.report) {
7532
+ const report = await generateReport(args.reportPeriod, args.reportJson);
7533
+ console.log(report);
7534
+ process.exit(0);
7535
+ }
6194
7536
  const wallet = await resolveOrGenerateWalletKey();
6195
7537
  if (wallet.source === "generated") {
6196
7538
  console.log(`[ClawRouter] Generated new wallet: ${wallet.address}`);
@@ -6224,19 +7566,20 @@ ClawRouter Partner APIs (v${VERSION})
6224
7566
  console.error(`[ClawRouter] Need help? Run: npx @blockrun/clawrouter doctor`);
6225
7567
  }
6226
7568
  });
6227
- const monitor = new BalanceMonitor(wallet.address);
7569
+ const paymentChain = await resolvePaymentChain();
7570
+ const displayAddress = paymentChain === "solana" && proxy.solanaAddress ? proxy.solanaAddress : wallet.address;
6228
7571
  try {
6229
- const balance = await monitor.checkBalance();
7572
+ const balance = await proxy.balanceMonitor.checkBalance();
6230
7573
  if (balance.isEmpty) {
6231
7574
  console.log(`[ClawRouter] Wallet balance: $0.00 (using FREE model)`);
6232
- console.log(`[ClawRouter] Fund wallet for premium models: ${wallet.address}`);
7575
+ console.log(`[ClawRouter] Fund wallet for premium models: ${displayAddress}`);
6233
7576
  } else if (balance.isLow) {
6234
7577
  console.log(`[ClawRouter] Wallet balance: ${balance.balanceUSD} (low)`);
6235
7578
  } else {
6236
7579
  console.log(`[ClawRouter] Wallet balance: ${balance.balanceUSD}`);
6237
7580
  }
6238
7581
  } catch {
6239
- console.log(`[ClawRouter] Wallet: ${wallet.address} (balance check pending)`);
7582
+ console.log(`[ClawRouter] Wallet: ${displayAddress} (balance check pending)`);
6240
7583
  }
6241
7584
  console.log(`[ClawRouter] Ready - Ctrl+C to stop`);
6242
7585
  const shutdown = async (signal) => {
@@ -6261,4 +7604,9 @@ main().catch((err) => {
6261
7604
  console.error(`[ClawRouter] Need help? Run: npx @blockrun/clawrouter doctor`);
6262
7605
  process.exit(1);
6263
7606
  });
7607
+ /*! Bundled license information:
7608
+
7609
+ @noble/hashes/esm/utils.js:
7610
+ (*! noble-hashes - MIT License (c) 2022 Paul Miller (paulmillr.com) *)
7611
+ */
6264
7612
  //# sourceMappingURL=cli.js.map