@routstr/sdk 0.2.3 → 0.2.5

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.
Files changed (38) hide show
  1. package/README.md +9 -0
  2. package/dist/client/index.d.mts +21 -8
  3. package/dist/client/index.d.ts +21 -8
  4. package/dist/client/index.js +1406 -69
  5. package/dist/client/index.js.map +1 -1
  6. package/dist/client/index.mjs +1406 -70
  7. package/dist/client/index.mjs.map +1 -1
  8. package/dist/discovery/index.d.mts +2 -2
  9. package/dist/discovery/index.d.ts +2 -2
  10. package/dist/discovery/index.js +1 -4
  11. package/dist/discovery/index.js.map +1 -1
  12. package/dist/discovery/index.mjs +1 -4
  13. package/dist/discovery/index.mjs.map +1 -1
  14. package/dist/index.d.mts +15 -19
  15. package/dist/index.d.ts +15 -19
  16. package/dist/index.js +2385 -1574
  17. package/dist/index.js.map +1 -1
  18. package/dist/index.mjs +2380 -1575
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/{interfaces-DGdP8fQp.d.mts → interfaces-BWJJTCXO.d.mts} +1 -1
  21. package/dist/{interfaces-CC0LT9p9.d.ts → interfaces-BxDEka72.d.ts} +1 -1
  22. package/dist/{interfaces-B85Wx7ni.d.mts → interfaces-C6Dr6hKy.d.mts} +1 -1
  23. package/dist/{interfaces-BVNyAmKu.d.ts → interfaces-CluftN4z.d.ts} +1 -1
  24. package/dist/storage/index.d.mts +56 -34
  25. package/dist/storage/index.d.ts +56 -34
  26. package/dist/storage/index.js +500 -51
  27. package/dist/storage/index.js.map +1 -1
  28. package/dist/storage/index.mjs +497 -52
  29. package/dist/storage/index.mjs.map +1 -1
  30. package/dist/{types-BlHjmWRK.d.mts → types-BYj_8c5c.d.mts} +3 -0
  31. package/dist/{types-BlHjmWRK.d.ts → types-BYj_8c5c.d.ts} +3 -0
  32. package/dist/wallet/index.d.mts +9 -5
  33. package/dist/wallet/index.d.ts +9 -5
  34. package/dist/wallet/index.js +54 -22
  35. package/dist/wallet/index.js.map +1 -1
  36. package/dist/wallet/index.mjs +54 -22
  37. package/dist/wallet/index.mjs.map +1 -1
  38. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ var applesauceCore = require('applesauce-core');
5
5
  var rxjs = require('rxjs');
6
6
  var vanilla = require('zustand/vanilla');
7
7
  var cashuTs = require('@cashu/cashu-ts');
8
+ var stream = require('stream');
8
9
 
9
10
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
10
11
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
@@ -385,10 +386,7 @@ var ModelManager = class _ModelManager {
385
386
  throw new Error(`Failed to fetch models: ${res.status}`);
386
387
  }
387
388
  const json = await res.json();
388
- const list = Array.isArray(json?.data) ? json.data.map((m) => ({
389
- ...m,
390
- id: m.id.split("/").pop() || m.id
391
- })) : [];
389
+ const list = Array.isArray(json?.data) ? json.data : [];
392
390
  return list;
393
391
  }
394
392
  isProviderDownError(error) {
@@ -1115,7 +1113,7 @@ var CashuSpender = class {
1115
1113
  /**
1116
1114
  * Refund specific providers without retrying spend
1117
1115
  */
1118
- async refundProviders(baseUrls, mintUrl, refundApiKeys = false) {
1116
+ async refundProviders(baseUrls, mintUrl, refundApiKeys = false, forceRefund) {
1119
1117
  const results = [];
1120
1118
  const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
1121
1119
  const toRefund = pendingDistribution.filter(
@@ -1165,7 +1163,8 @@ var CashuSpender = class {
1165
1163
  const refundResult = await this.balanceManager.refundApiKey({
1166
1164
  mintUrl,
1167
1165
  baseUrl: apiKeyEntry.baseUrl,
1168
- apiKey: apiKeyEntryFull.key
1166
+ apiKey: apiKeyEntryFull.key,
1167
+ forceRefund
1169
1168
  });
1170
1169
  if (refundResult.success) {
1171
1170
  this.storageAdapter.updateApiKeyBalance(apiKeyEntry.baseUrl, 0);
@@ -1338,12 +1337,29 @@ var BalanceManager = class {
1338
1337
  }
1339
1338
  /**
1340
1339
  * Refund API key balance - convert remaining API key balance to cashu token
1340
+ * @param options - Refund options including forceRefund flag
1341
+ * @returns Refund result
1341
1342
  */
1342
1343
  async refundApiKey(options) {
1343
- const { mintUrl, baseUrl, apiKey } = options;
1344
+ const { mintUrl, baseUrl, apiKey, forceRefund } = options;
1344
1345
  if (!apiKey) {
1345
1346
  return { success: false, message: "No API key to refund" };
1346
1347
  }
1348
+ if (!forceRefund) {
1349
+ const apiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
1350
+ if (apiKeyEntry?.lastUsed) {
1351
+ const fiveMinutesAgo = Date.now() - 5 * 60 * 1e3;
1352
+ if (apiKeyEntry.lastUsed > fiveMinutesAgo) {
1353
+ console.log(
1354
+ `[BalanceManager] Skipping refund for ${baseUrl} - used ${Math.round((Date.now() - apiKeyEntry.lastUsed) / 1e3)}s ago`
1355
+ );
1356
+ return {
1357
+ success: false,
1358
+ message: "API key was used recently, skipping refund"
1359
+ };
1360
+ }
1361
+ }
1362
+ }
1347
1363
  let fetchResult;
1348
1364
  try {
1349
1365
  fetchResult = await this._fetchRefundTokenWithApiKey(baseUrl, apiKey);
@@ -1515,9 +1531,13 @@ var BalanceManager = class {
1515
1531
  p2pkPubkey
1516
1532
  } = options;
1517
1533
  const adjustedAmount = Math.ceil(amount);
1518
- console.log(`[BalanceManager.createProviderToken] Starting: baseUrl=${baseUrl}, mintUrl=${mintUrl}, amount=${amount}, adjustedAmount=${adjustedAmount}, retryCount=${retryCount}`);
1534
+ console.log(
1535
+ `[BalanceManager.createProviderToken] Starting: baseUrl=${baseUrl}, mintUrl=${mintUrl}, amount=${amount}, adjustedAmount=${adjustedAmount}, retryCount=${retryCount}`
1536
+ );
1519
1537
  if (!adjustedAmount || isNaN(adjustedAmount)) {
1520
- console.error(`[BalanceManager.createProviderToken] FAILURE: Invalid amount - amount=${amount}, adjustedAmount=${adjustedAmount}`);
1538
+ console.error(
1539
+ `[BalanceManager.createProviderToken] FAILURE: Invalid amount - amount=${amount}, adjustedAmount=${adjustedAmount}`
1540
+ );
1521
1541
  return { success: false, error: "Invalid top up amount" };
1522
1542
  }
1523
1543
  const balanceState = await this.getBalanceState();
@@ -1531,8 +1551,8 @@ var BalanceManager = class {
1531
1551
  const refundableProviderBalance = Object.entries(
1532
1552
  balanceState.providerBalances
1533
1553
  ).filter(([providerBaseUrl]) => providerBaseUrl !== baseUrl).reduce((sum, [, value]) => sum + value, 0);
1534
- if (totalMintBalance + targetProviderBalance < adjustedAmount && totalMintBalance + targetProviderBalance + refundableProviderBalance >= adjustedAmount && retryCount < 1) {
1535
- await this._refundOtherProvidersForTopUp(baseUrl, mintUrl);
1554
+ if (totalMintBalance + targetProviderBalance < adjustedAmount && totalMintBalance + targetProviderBalance + refundableProviderBalance >= adjustedAmount && retryCount < 2) {
1555
+ await this._refundOtherProvidersForTopUp(baseUrl, mintUrl, retryCount);
1536
1556
  return this.createProviderToken({
1537
1557
  ...options,
1538
1558
  retryCount: retryCount + 1
@@ -1548,15 +1568,11 @@ var BalanceManager = class {
1548
1568
  { url: "", balance: 0 }
1549
1569
  ).url
1550
1570
  );
1551
- console.error(`[BalanceManager.createProviderToken] FAILURE: Insufficient balance - required=${adjustedAmount}, available=${totalMintBalance + targetProviderBalance}, totalMintBalance=${totalMintBalance}, targetProviderBalance=${targetProviderBalance}, refundableProviderBalance=${refundableProviderBalance}`);
1571
+ console.error(
1572
+ `[BalanceManager.createProviderToken] FAILURE: Insufficient balance - required=${adjustedAmount}, available=${totalMintBalance + targetProviderBalance}, totalMintBalance=${totalMintBalance}, targetProviderBalance=${targetProviderBalance}, refundableProviderBalance=${refundableProviderBalance}`
1573
+ );
1552
1574
  return { success: false, error: error.message };
1553
1575
  }
1554
- if (targetProviderBalance >= adjustedAmount) {
1555
- return {
1556
- success: true,
1557
- amountSpent: 0
1558
- };
1559
- }
1560
1576
  const providerMints = baseUrl && this.providerRegistry ? this.providerRegistry.getProviderMints(baseUrl) : [];
1561
1577
  let requiredAmount = adjustedAmount;
1562
1578
  const supportedMintsOnly = providerMints.length > 0;
@@ -1590,7 +1606,9 @@ var BalanceManager = class {
1590
1606
  maxMintUrl = mintUrl2;
1591
1607
  }
1592
1608
  }
1593
- console.error(`[BalanceManager.createProviderToken] FAILURE: No candidate mints found - requiredAmount=${requiredAmount}, totalMintBalance=${totalMintBalance}, maxBalance=${maxBalance}, maxMintUrl=${maxMintUrl}, providerMints=${JSON.stringify(providerMints)}`);
1609
+ console.error(
1610
+ `[BalanceManager.createProviderToken] FAILURE: No candidate mints found - requiredAmount=${requiredAmount}, totalMintBalance=${totalMintBalance}, maxBalance=${maxBalance}, maxMintUrl=${maxMintUrl}, providerMints=${JSON.stringify(providerMints)}`
1611
+ );
1594
1612
  const error = new InsufficientBalanceError(
1595
1613
  adjustedAmount,
1596
1614
  totalMintBalance,
@@ -1602,13 +1620,17 @@ var BalanceManager = class {
1602
1620
  let lastError;
1603
1621
  for (const candidateMint of candidates) {
1604
1622
  try {
1605
- console.log(`[BalanceManager.createProviderToken] Attempting mint: ${candidateMint}, amount: ${requiredAmount}`);
1623
+ console.log(
1624
+ `[BalanceManager.createProviderToken] Attempting mint: ${candidateMint}, amount: ${requiredAmount}`
1625
+ );
1606
1626
  const token = await this.walletAdapter.sendToken(
1607
1627
  candidateMint,
1608
1628
  requiredAmount,
1609
1629
  p2pkPubkey
1610
1630
  );
1611
- console.log(`[BalanceManager.createProviderToken] SUCCESS: Token created from mint ${candidateMint}`);
1631
+ console.log(
1632
+ `[BalanceManager.createProviderToken] SUCCESS: Token created from mint ${candidateMint}`
1633
+ );
1612
1634
  return {
1613
1635
  success: true,
1614
1636
  token,
@@ -1617,11 +1639,15 @@ var BalanceManager = class {
1617
1639
  };
1618
1640
  } catch (error) {
1619
1641
  const errorMsg = error instanceof Error ? error.message : String(error);
1620
- console.error(`[BalanceManager.createProviderToken] FAILURE: Mint ${candidateMint} failed with error: ${errorMsg}`);
1642
+ console.error(
1643
+ `[BalanceManager.createProviderToken] FAILURE: Mint ${candidateMint} failed with error: ${errorMsg}`
1644
+ );
1621
1645
  if (error instanceof Error) {
1622
1646
  lastError = errorMsg;
1623
1647
  if (isNetworkErrorMessage(error.message)) {
1624
- console.warn(`[BalanceManager.createProviderToken] Network error from ${candidateMint}, trying next mint...`);
1648
+ console.warn(
1649
+ `[BalanceManager.createProviderToken] Network error from ${candidateMint}, trying next mint...`
1650
+ );
1625
1651
  continue;
1626
1652
  }
1627
1653
  }
@@ -1631,7 +1657,9 @@ var BalanceManager = class {
1631
1657
  };
1632
1658
  }
1633
1659
  }
1634
- console.error(`[BalanceManager.createProviderToken] FAILURE: All candidate mints exhausted - lastError=${lastError}, candidates=${JSON.stringify(candidates)}`);
1660
+ console.error(
1661
+ `[BalanceManager.createProviderToken] FAILURE: All candidate mints exhausted - lastError=${lastError}, candidates=${JSON.stringify(candidates)}`
1662
+ );
1635
1663
  return {
1636
1664
  success: false,
1637
1665
  error: lastError || "All candidate mints failed while creating top up token"
@@ -1677,9 +1705,10 @@ var BalanceManager = class {
1677
1705
  }
1678
1706
  return candidates;
1679
1707
  }
1680
- async _refundOtherProvidersForTopUp(baseUrl, mintUrl) {
1708
+ async _refundOtherProvidersForTopUp(baseUrl, mintUrl, retryCount) {
1681
1709
  const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
1682
1710
  const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
1711
+ const forceRefund = retryCount >= 2;
1683
1712
  const toRefund = pendingDistribution.filter(
1684
1713
  (pending) => pending.baseUrl !== baseUrl
1685
1714
  );
@@ -1720,7 +1749,8 @@ var BalanceManager = class {
1720
1749
  const result = await this.refundApiKey({
1721
1750
  mintUrl,
1722
1751
  baseUrl: apiKeyEntry.baseUrl,
1723
- apiKey: fullApiKeyEntry.key
1752
+ apiKey: fullApiKeyEntry.key,
1753
+ forceRefund
1724
1754
  });
1725
1755
  return { baseUrl: apiKeyEntry.baseUrl, success: result.success };
1726
1756
  })
@@ -1974,6 +2004,77 @@ var BalanceManager = class {
1974
2004
  }
1975
2005
  };
1976
2006
 
2007
+ // client/usage.ts
2008
+ function extractUsageFromResponseBody(body, fallbackSatsCost = 0) {
2009
+ if (!body || typeof body !== "object") return null;
2010
+ const usage = body.usage;
2011
+ if (!usage || typeof usage !== "object") return null;
2012
+ const promptTokens = Number(usage.prompt_tokens ?? 0);
2013
+ const completionTokens = Number(usage.completion_tokens ?? 0);
2014
+ const totalTokens = Number(usage.total_tokens ?? 0);
2015
+ const costValue = usage.cost;
2016
+ let cost = 0;
2017
+ let satsCost = fallbackSatsCost;
2018
+ if (typeof costValue === "number") {
2019
+ cost = costValue;
2020
+ } else if (costValue && typeof costValue === "object") {
2021
+ const costObj = costValue;
2022
+ const totalUsd = costObj.total_usd;
2023
+ const totalMsats = costObj.total_msats;
2024
+ cost = typeof totalUsd === "number" ? totalUsd : 0;
2025
+ if (typeof totalMsats === "number") {
2026
+ satsCost = totalMsats / 1e3;
2027
+ }
2028
+ }
2029
+ if (promptTokens === 0 && completionTokens === 0 && totalTokens === 0 && cost === 0 && satsCost === 0) {
2030
+ return null;
2031
+ }
2032
+ return {
2033
+ promptTokens,
2034
+ completionTokens,
2035
+ totalTokens,
2036
+ cost,
2037
+ satsCost
2038
+ };
2039
+ }
2040
+ function extractResponseId(body) {
2041
+ if (!body || typeof body !== "object") return void 0;
2042
+ const id = body.id;
2043
+ if (typeof id !== "string") return void 0;
2044
+ const trimmed = id.trim();
2045
+ return trimmed.length > 0 ? trimmed : void 0;
2046
+ }
2047
+ function extractUsageFromSSEJson(parsed, fallbackSatsCost = 0) {
2048
+ if (!parsed || typeof parsed !== "object" || !parsed.usage) {
2049
+ return null;
2050
+ }
2051
+ const usage = parsed.usage;
2052
+ const usageCost = usage.cost;
2053
+ const cost = typeof usageCost === "number" ? usageCost : usageCost?.total_usd ?? parsed.metadata?.routstr?.cost?.total_usd ?? 0;
2054
+ const msats = parsed.metadata?.routstr?.cost?.total_msats ?? (typeof usage.cost_sats === "number" ? usage.cost_sats * 1e3 : 0);
2055
+ const result = {
2056
+ promptTokens: Number(usage.prompt_tokens ?? 0),
2057
+ completionTokens: Number(usage.completion_tokens ?? 0),
2058
+ totalTokens: Number(usage.total_tokens ?? 0),
2059
+ cost: Number(cost ?? 0),
2060
+ satsCost: msats > 0 ? msats / 1e3 : fallbackSatsCost
2061
+ };
2062
+ if (result.promptTokens === 0 && result.completionTokens === 0 && result.totalTokens === 0 && result.cost === 0 && result.satsCost === 0) {
2063
+ return null;
2064
+ }
2065
+ return result;
2066
+ }
2067
+ function toUsageStats(usage) {
2068
+ if (!usage) return void 0;
2069
+ return {
2070
+ total_tokens: usage.totalTokens,
2071
+ prompt_tokens: usage.promptTokens,
2072
+ completion_tokens: usage.completionTokens,
2073
+ cost: usage.cost,
2074
+ sats_cost: usage.satsCost
2075
+ };
2076
+ }
2077
+
1977
2078
  // client/StreamProcessor.ts
1978
2079
  var StreamProcessor = class {
1979
2080
  accumulatedContent = "";
@@ -2001,6 +2102,7 @@ var StreamProcessor = class {
2001
2102
  let finish_reason;
2002
2103
  let citations;
2003
2104
  let annotations;
2105
+ let responseId;
2004
2106
  try {
2005
2107
  while (true) {
2006
2108
  const { done, value } = await reader.read();
@@ -2029,6 +2131,9 @@ var StreamProcessor = class {
2029
2131
  if (parsed.finish_reason) {
2030
2132
  finish_reason = parsed.finish_reason;
2031
2133
  }
2134
+ if (parsed.responseId) {
2135
+ responseId = parsed.responseId;
2136
+ }
2032
2137
  if (parsed.citations) {
2033
2138
  citations = parsed.citations;
2034
2139
  }
@@ -2049,6 +2154,7 @@ var StreamProcessor = class {
2049
2154
  images: this.accumulatedImages.length > 0 ? this.accumulatedImages : void 0,
2050
2155
  usage,
2051
2156
  model,
2157
+ responseId,
2052
2158
  finish_reason,
2053
2159
  citations,
2054
2160
  annotations
@@ -2076,12 +2182,15 @@ var StreamProcessor = class {
2076
2182
  result.reasoning = parsed.choices[0].delta.reasoning;
2077
2183
  }
2078
2184
  if (parsed.usage) {
2079
- result.usage = {
2185
+ result.usage = toUsageStats(extractUsageFromSSEJson(parsed)) ?? {
2080
2186
  total_tokens: parsed.usage.total_tokens,
2081
2187
  prompt_tokens: parsed.usage.prompt_tokens,
2082
2188
  completion_tokens: parsed.usage.completion_tokens
2083
2189
  };
2084
2190
  }
2191
+ if (parsed.id) {
2192
+ result.responseId = parsed.id;
2193
+ }
2085
2194
  if (parsed.model) {
2086
2195
  result.model = parsed.model;
2087
2196
  }
@@ -2529,7 +2638,6 @@ var ProviderManager = class _ProviderManager {
2529
2638
  * Get providers for a model sorted by prompt+completion pricing
2530
2639
  */
2531
2640
  getProviderPriceRankingForModel(modelId, options = {}) {
2532
- const normalizedId = this.normalizeModelId(modelId);
2533
2641
  const includeDisabled = options.includeDisabled ?? false;
2534
2642
  const torMode = options.torMode ?? false;
2535
2643
  const disabledProviders = new Set(
@@ -2543,9 +2651,7 @@ var ProviderManager = class _ProviderManager {
2543
2651
  if (torMode && !baseUrl.includes(".onion")) continue;
2544
2652
  if (!torMode && (baseUrl.includes(".onion") || isInsecureHttpUrl(baseUrl)))
2545
2653
  continue;
2546
- const match = models.find(
2547
- (model) => this.normalizeModelId(model.id) === normalizedId
2548
- );
2654
+ const match = models.find((model) => model.id === modelId);
2549
2655
  if (!match?.sats_pricing) continue;
2550
2656
  const prompt = match.sats_pricing.prompt;
2551
2657
  const completion = match.sats_pricing.completion;
@@ -2658,1323 +2764,740 @@ var ProviderManager = class _ProviderManager {
2658
2764
  }
2659
2765
  };
2660
2766
 
2661
- // client/RoutstrClient.ts
2662
- var TOPUP_MARGIN = 1.2;
2663
- var RoutstrClient = class {
2664
- constructor(walletAdapter, storageAdapter, providerRegistry, alertLevel, mode = "xcashu") {
2665
- this.walletAdapter = walletAdapter;
2666
- this.storageAdapter = storageAdapter;
2667
- this.providerRegistry = providerRegistry;
2668
- this.balanceManager = new BalanceManager(
2669
- walletAdapter,
2670
- storageAdapter,
2671
- providerRegistry
2672
- );
2673
- this.cashuSpender = new CashuSpender(
2674
- walletAdapter,
2675
- storageAdapter,
2676
- providerRegistry,
2677
- this.balanceManager
2678
- );
2679
- this.streamProcessor = new StreamProcessor();
2680
- this.providerManager = new ProviderManager(providerRegistry);
2681
- this.alertLevel = alertLevel;
2682
- this.mode = mode;
2683
- }
2684
- cashuSpender;
2685
- balanceManager;
2686
- streamProcessor;
2687
- providerManager;
2688
- alertLevel;
2689
- mode;
2690
- debugLevel = "WARN";
2691
- /**
2692
- * Get the current client mode
2693
- */
2694
- getMode() {
2695
- return this.mode;
2696
- }
2697
- getDebugLevel() {
2698
- return this.debugLevel;
2699
- }
2700
- setDebugLevel(level) {
2701
- this.debugLevel = level;
2702
- }
2703
- _log(level, ...args) {
2704
- const levelPriority = {
2705
- DEBUG: 0,
2706
- WARN: 1,
2707
- ERROR: 2
2708
- };
2709
- if (levelPriority[level] >= levelPriority[this.debugLevel]) {
2710
- switch (level) {
2711
- case "DEBUG":
2712
- console.log(...args);
2713
- break;
2714
- case "WARN":
2715
- console.warn(...args);
2716
- break;
2717
- case "ERROR":
2718
- console.error(...args);
2719
- break;
2767
+ // storage/drivers/localStorage.ts
2768
+ var canUseLocalStorage = () => {
2769
+ return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
2770
+ };
2771
+ var isQuotaExceeded = (error) => {
2772
+ const e = error;
2773
+ return !!e && (e?.name === "QuotaExceededError" || e?.code === 22 || e?.code === 1014);
2774
+ };
2775
+ var NON_CRITICAL_KEYS = /* @__PURE__ */ new Set(["modelsFromAllProviders"]);
2776
+ var localStorageDriver = {
2777
+ async getItem(key, defaultValue) {
2778
+ if (!canUseLocalStorage()) return defaultValue;
2779
+ try {
2780
+ const item = window.localStorage.getItem(key);
2781
+ if (item === null) return defaultValue;
2782
+ try {
2783
+ return JSON.parse(item);
2784
+ } catch (parseError) {
2785
+ if (typeof defaultValue === "string") {
2786
+ return item;
2787
+ }
2788
+ throw parseError;
2789
+ }
2790
+ } catch (error) {
2791
+ console.error(`Error retrieving item with key "${key}":`, error);
2792
+ if (canUseLocalStorage()) {
2793
+ try {
2794
+ window.localStorage.removeItem(key);
2795
+ } catch (removeError) {
2796
+ console.error(
2797
+ `Error removing corrupted item with key "${key}":`,
2798
+ removeError
2799
+ );
2800
+ }
2720
2801
  }
2802
+ return defaultValue;
2803
+ }
2804
+ },
2805
+ async setItem(key, value) {
2806
+ if (!canUseLocalStorage()) return;
2807
+ try {
2808
+ window.localStorage.setItem(key, JSON.stringify(value));
2809
+ } catch (error) {
2810
+ if (isQuotaExceeded(error)) {
2811
+ if (NON_CRITICAL_KEYS.has(key)) {
2812
+ console.warn(
2813
+ `Storage quota exceeded; skipping non-critical key "${key}".`
2814
+ );
2815
+ return;
2816
+ }
2817
+ try {
2818
+ window.localStorage.removeItem("modelsFromAllProviders");
2819
+ } catch {
2820
+ }
2821
+ try {
2822
+ window.localStorage.setItem(key, JSON.stringify(value));
2823
+ return;
2824
+ } catch (retryError) {
2825
+ console.warn(
2826
+ `Storage quota exceeded; unable to persist key "${key}" after cleanup attempt.`,
2827
+ retryError
2828
+ );
2829
+ return;
2830
+ }
2831
+ }
2832
+ console.error(`Error storing item with key "${key}":`, error);
2833
+ }
2834
+ },
2835
+ async removeItem(key) {
2836
+ if (!canUseLocalStorage()) return;
2837
+ try {
2838
+ window.localStorage.removeItem(key);
2839
+ } catch (error) {
2840
+ console.error(`Error removing item with key "${key}":`, error);
2721
2841
  }
2722
2842
  }
2723
- /**
2724
- * Get the CashuSpender instance
2725
- */
2726
- getCashuSpender() {
2727
- return this.cashuSpender;
2728
- }
2729
- /**
2730
- * Get the BalanceManager instance
2731
- */
2732
- getBalanceManager() {
2733
- return this.balanceManager;
2734
- }
2735
- /**
2736
- * Get the ProviderManager instance
2737
- */
2738
- getProviderManager() {
2739
- return this.providerManager;
2740
- }
2741
- /**
2742
- * Check if the client is currently busy (in critical section)
2743
- */
2744
- get isBusy() {
2745
- return this.cashuSpender.isBusy;
2843
+ };
2844
+
2845
+ // storage/drivers/memory.ts
2846
+ var createMemoryDriver = (seed) => {
2847
+ const store = /* @__PURE__ */ new Map();
2848
+ if (seed) {
2849
+ for (const [key, value] of Object.entries(seed)) {
2850
+ store.set(key, value);
2851
+ }
2746
2852
  }
2747
- /**
2748
- * Route an API request to the upstream provider
2749
- *
2750
- * This is a simpler alternative to fetchAIResponse that just proxies
2751
- * the request upstream without the streaming callback machinery.
2752
- * Useful for daemon-style routing where you just need to forward
2753
- * requests and get responses back.
2754
- */
2755
- async routeRequest(params) {
2756
- const {
2757
- path,
2758
- method,
2759
- body,
2760
- headers = {},
2761
- baseUrl,
2762
- mintUrl,
2763
- modelId
2764
- } = params;
2765
- await this._checkBalance();
2766
- let requiredSats = 1;
2767
- let selectedModel;
2768
- if (modelId) {
2769
- const providerModel = await this.providerManager.getModelForProvider(
2770
- baseUrl,
2771
- modelId
2772
- );
2773
- selectedModel = providerModel ?? void 0;
2774
- if (selectedModel) {
2775
- requiredSats = this.providerManager.getRequiredSatsForModel(
2776
- selectedModel,
2777
- []
2778
- );
2853
+ return {
2854
+ async getItem(key, defaultValue) {
2855
+ const item = store.get(key);
2856
+ if (item === void 0) return defaultValue;
2857
+ try {
2858
+ return JSON.parse(item);
2859
+ } catch (parseError) {
2860
+ if (typeof defaultValue === "string") {
2861
+ return item;
2862
+ }
2863
+ throw parseError;
2779
2864
  }
2865
+ },
2866
+ async setItem(key, value) {
2867
+ store.set(key, JSON.stringify(value));
2868
+ },
2869
+ async removeItem(key) {
2870
+ store.delete(key);
2780
2871
  }
2781
- const { token, tokenBalance, tokenBalanceUnit } = await this._spendToken({
2782
- mintUrl,
2783
- amount: requiredSats,
2784
- baseUrl
2785
- });
2786
- this._log("DEBUG", token, baseUrl);
2787
- let requestBody = body;
2788
- if (body && typeof body === "object") {
2789
- const bodyObj = body;
2790
- if (!bodyObj.stream) {
2791
- requestBody = { ...bodyObj, stream: false };
2792
- }
2793
- }
2794
- const baseHeaders = this._buildBaseHeaders(headers);
2795
- const requestHeaders = this._withAuthHeader(baseHeaders, token);
2796
- const response = await this._makeRequest({
2797
- path,
2798
- method,
2799
- body: method === "GET" ? void 0 : requestBody,
2800
- baseUrl,
2801
- mintUrl,
2802
- token,
2803
- requiredSats,
2804
- headers: requestHeaders,
2805
- baseHeaders,
2806
- selectedModel
2807
- });
2808
- const tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
2809
- const baseUrlUsed = response.baseUrl || baseUrl;
2810
- const tokenUsed = response.token || token;
2811
- await this._handlePostResponseBalanceUpdate({
2812
- token: tokenUsed,
2813
- baseUrl: baseUrlUsed,
2814
- initialTokenBalance: tokenBalanceInSats,
2815
- response
2816
- });
2817
- return response;
2872
+ };
2873
+ };
2874
+
2875
+ // storage/drivers/sqlite.ts
2876
+ var isBun = () => {
2877
+ return typeof process.versions.bun !== "undefined";
2878
+ };
2879
+ var createDatabase = (dbPath) => {
2880
+ if (isBun()) {
2881
+ throw new Error(
2882
+ "SQLite driver not supported in Bun. Use createMemoryDriver() instead."
2883
+ );
2818
2884
  }
2819
- /**
2820
- * Fetch AI response with streaming
2821
- */
2822
- async fetchAIResponse(options, callbacks) {
2823
- const {
2824
- messageHistory,
2825
- selectedModel,
2826
- baseUrl,
2827
- mintUrl,
2828
- balance,
2829
- transactionHistory,
2830
- maxTokens,
2831
- headers
2832
- } = options;
2833
- const apiMessages = await this._convertMessages(messageHistory);
2834
- const requiredSats = this.providerManager.getRequiredSatsForModel(
2835
- selectedModel,
2836
- apiMessages,
2837
- maxTokens
2885
+ let Database = null;
2886
+ try {
2887
+ Database = __require("better-sqlite3");
2888
+ } catch (error) {
2889
+ throw new Error(
2890
+ `better-sqlite3 is required for sqlite storage. Install it to use sqlite storage. (${error})`
2838
2891
  );
2839
- try {
2840
- await this._checkBalance();
2841
- callbacks.onPaymentProcessing?.(true);
2842
- const spendResult = await this._spendToken({
2843
- mintUrl,
2844
- amount: requiredSats,
2845
- baseUrl
2846
- });
2847
- let token = spendResult.token;
2848
- let tokenBalance = spendResult.tokenBalance;
2849
- let tokenBalanceUnit = spendResult.tokenBalanceUnit;
2850
- const tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
2851
- callbacks.onTokenCreated?.(this._getPendingCashuTokenAmount());
2852
- const baseHeaders = this._buildBaseHeaders(headers);
2853
- const requestHeaders = this._withAuthHeader(baseHeaders, token);
2854
- this.providerManager.resetFailedProviders();
2855
- const providerInfo = await this.providerRegistry.getProviderInfo(baseUrl);
2856
- const providerVersion = providerInfo?.version ?? "";
2857
- let modelIdForRequest = selectedModel.id;
2858
- if (/^0\.1\./.test(providerVersion)) {
2859
- const newModel = await this.providerManager.getModelForProvider(
2860
- baseUrl,
2861
- selectedModel.id
2862
- );
2863
- modelIdForRequest = newModel?.id ?? selectedModel.id;
2864
- }
2865
- const body = {
2866
- model: modelIdForRequest,
2867
- messages: apiMessages,
2868
- stream: true
2869
- };
2870
- if (maxTokens !== void 0) {
2871
- body.max_tokens = maxTokens;
2872
- }
2873
- if (selectedModel?.name?.startsWith("OpenAI:")) {
2874
- body.tools = [{ type: "web_search" }];
2875
- }
2876
- const response = await this._makeRequest({
2877
- path: "/v1/chat/completions",
2878
- method: "POST",
2879
- body,
2880
- selectedModel,
2881
- baseUrl,
2882
- mintUrl,
2883
- token,
2884
- requiredSats,
2885
- maxTokens,
2886
- headers: requestHeaders,
2887
- baseHeaders
2888
- });
2889
- if (!response.body) {
2890
- throw new Error("Response body is not available");
2891
- }
2892
- if (response.status === 200) {
2893
- const baseUrlUsed = response.baseUrl || baseUrl;
2894
- const streamingResult = await this.streamProcessor.process(
2895
- response,
2896
- {
2897
- onContent: callbacks.onStreamingUpdate,
2898
- onThinking: callbacks.onThinkingUpdate
2899
- },
2900
- selectedModel.id
2901
- );
2902
- if (streamingResult.finish_reason === "content_filter") {
2903
- callbacks.onMessageAppend({
2904
- role: "assistant",
2905
- content: "Your request was denied due to content filtering."
2906
- });
2907
- } else if (streamingResult.content || streamingResult.images && streamingResult.images.length > 0) {
2908
- const message = await this._createAssistantMessage(streamingResult);
2909
- callbacks.onMessageAppend(message);
2910
- } else {
2911
- callbacks.onMessageAppend({
2912
- role: "system",
2913
- content: "The provider did not respond to this request."
2914
- });
2915
- }
2916
- callbacks.onStreamingUpdate("");
2917
- callbacks.onThinkingUpdate("");
2918
- const isApikeysEstimate = this.mode === "apikeys";
2919
- let satsSpent = await this._handlePostResponseBalanceUpdate({
2920
- token,
2921
- baseUrl: baseUrlUsed,
2922
- initialTokenBalance: tokenBalanceInSats,
2923
- fallbackSatsSpent: isApikeysEstimate ? this._getEstimatedCosts(selectedModel, streamingResult) : void 0,
2924
- response
2925
- });
2926
- const estimatedCosts = this._getEstimatedCosts(
2927
- selectedModel,
2928
- streamingResult
2929
- );
2930
- const onLastMessageSatsUpdate = callbacks.onLastMessageSatsUpdate;
2931
- onLastMessageSatsUpdate?.(satsSpent, estimatedCosts);
2932
- } else {
2933
- throw new Error(`${response.status} ${response.statusText}`);
2934
- }
2935
- } catch (error) {
2936
- this._handleError(error, callbacks);
2937
- } finally {
2938
- callbacks.onPaymentProcessing?.(false);
2939
- }
2940
2892
  }
2941
- /**
2942
- * Make the API request with failover support
2943
- */
2944
- async _makeRequest(params) {
2945
- const { path, method, body, baseUrl, token, headers } = params;
2946
- try {
2947
- const url = `${baseUrl.replace(/\/$/, "")}${path}`;
2948
- if (this.mode === "xcashu") this._log("DEBUG", "HEADERS,", headers);
2949
- this._log("DEBUG", "HEADERS,", headers);
2950
- const response = await fetch(url, {
2951
- method,
2952
- headers,
2953
- body: body === void 0 || method === "GET" ? void 0 : JSON.stringify(body)
2954
- });
2955
- if (this.mode === "xcashu") this._log("DEBUG", "response,", response);
2956
- response.baseUrl = baseUrl;
2957
- response.token = token;
2958
- if (!response.ok) {
2959
- const requestId = response.headers.get("x-routstr-request-id") || void 0;
2960
- let bodyText;
2893
+ return new Database(dbPath);
2894
+ };
2895
+ var createSqliteDriver = (options = {}) => {
2896
+ const dbPath = options.dbPath || "routstr.sqlite";
2897
+ const tableName = options.tableName || "sdk_storage";
2898
+ const db = createDatabase(dbPath);
2899
+ db.exec(
2900
+ `CREATE TABLE IF NOT EXISTS ${tableName} (key TEXT PRIMARY KEY, value TEXT NOT NULL)`
2901
+ );
2902
+ const selectStmt = db.prepare(`SELECT value FROM ${tableName} WHERE key = ?`);
2903
+ const upsertStmt = db.prepare(
2904
+ `INSERT INTO ${tableName} (key, value) VALUES (?, ?)
2905
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`
2906
+ );
2907
+ const deleteStmt = db.prepare(`DELETE FROM ${tableName} WHERE key = ?`);
2908
+ return {
2909
+ async getItem(key, defaultValue) {
2910
+ try {
2911
+ const row = selectStmt.get(key);
2912
+ if (!row || typeof row.value !== "string") return defaultValue;
2961
2913
  try {
2962
- bodyText = await response.text();
2963
- } catch (e) {
2964
- bodyText = void 0;
2914
+ return JSON.parse(row.value);
2915
+ } catch (parseError) {
2916
+ if (typeof defaultValue === "string") {
2917
+ return row.value;
2918
+ }
2919
+ throw parseError;
2965
2920
  }
2966
- return await this._handleErrorResponse(
2967
- params,
2968
- token,
2969
- response.status,
2970
- requestId,
2971
- this.mode === "xcashu" ? response.headers.get("x-cashu") ?? void 0 : void 0,
2972
- bodyText,
2973
- params.retryCount ?? 0
2974
- );
2921
+ } catch (error) {
2922
+ console.error(`SQLite getItem failed for key "${key}":`, error);
2923
+ return defaultValue;
2975
2924
  }
2976
- return response;
2977
- } catch (error) {
2978
- if (isNetworkErrorMessage(error?.message || "")) {
2979
- return await this._handleErrorResponse(
2980
- params,
2981
- token,
2982
- -1,
2983
- // just for Network Error to skip all statuses
2984
- void 0,
2985
- void 0,
2986
- void 0,
2987
- params.retryCount ?? 0
2988
- );
2925
+ },
2926
+ async setItem(key, value) {
2927
+ try {
2928
+ upsertStmt.run(key, JSON.stringify(value));
2929
+ } catch (error) {
2930
+ console.error(`SQLite setItem failed for key "${key}":`, error);
2931
+ }
2932
+ },
2933
+ async removeItem(key) {
2934
+ try {
2935
+ deleteStmt.run(key);
2936
+ } catch (error) {
2937
+ console.error(`SQLite removeItem failed for key "${key}":`, error);
2989
2938
  }
2990
- throw error;
2991
2939
  }
2940
+ };
2941
+ };
2942
+
2943
+ // storage/drivers/indexedDB.ts
2944
+ var isBrowser = typeof indexedDB !== "undefined";
2945
+ var openDatabase = (dbName, storeName) => {
2946
+ if (!isBrowser) {
2947
+ return Promise.reject(new Error("IndexedDB is not available"));
2992
2948
  }
2993
- /**
2994
- * Handle error responses with failover
2995
- */
2996
- async _handleErrorResponse(params, token, status, requestId, xCashuRefundToken, responseBody, retryCount = 0) {
2997
- const MAX_RETRIES_PER_PROVIDER = 2;
2998
- const { path, method, body, selectedModel, baseUrl, mintUrl } = params;
2999
- let tryNextProvider = false;
3000
- this._log(
3001
- "DEBUG",
3002
- `[RoutstrClient] _handleErrorResponse: status=${status}, baseUrl=${baseUrl}, mode=${this.mode}, token preview=${token}, requestId=${requestId}`
3003
- );
3004
- this._log(
3005
- "DEBUG",
3006
- `[RoutstrClient] _handleErrorResponse: Attempting to receive/restore token for ${baseUrl}`
3007
- );
3008
- if (params.token.startsWith("cashu")) {
3009
- const tryReceiveTokenResult = await this.cashuSpender.receiveToken(
3010
- params.token
3011
- );
3012
- if (tryReceiveTokenResult.success) {
3013
- this._log(
3014
- "DEBUG",
3015
- `[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${tryReceiveTokenResult.amount}`
3016
- );
3017
- tryNextProvider = true;
3018
- if (this.mode === "lazyrefund")
3019
- this.storageAdapter.removeToken(baseUrl);
3020
- } else {
3021
- this._log(
3022
- "DEBUG",
3023
- `[RoutstrClient] _handleErrorResponse: Failed to receive token. `
3024
- );
3025
- }
3026
- }
3027
- if (this.mode === "xcashu") {
3028
- if (xCashuRefundToken) {
3029
- this._log(
3030
- "DEBUG",
3031
- `[RoutstrClient] _handleErrorResponse: Attempting to receive xcashu refund token, preview=${xCashuRefundToken.substring(0, 20)}...`
3032
- );
3033
- try {
3034
- const receiveResult = await this.cashuSpender.receiveToken(xCashuRefundToken);
3035
- if (receiveResult.success) {
3036
- this._log(
3037
- "DEBUG",
3038
- `[RoutstrClient] _handleErrorResponse: xcashu refund received, amount=${receiveResult.amount}`
3039
- );
3040
- tryNextProvider = true;
3041
- } else
3042
- throw new ProviderError(
3043
- baseUrl,
3044
- status,
3045
- "xcashu refund failed",
3046
- requestId
3047
- );
3048
- } catch (error) {
3049
- this._log("ERROR", "[xcashu] Failed to receive refund token:", error);
3050
- throw new ProviderError(
3051
- baseUrl,
3052
- status,
3053
- "[xcashu] Failed to receive refund token",
3054
- requestId
3055
- );
3056
- }
3057
- } else {
3058
- if (!tryNextProvider)
3059
- throw new ProviderError(
3060
- baseUrl,
3061
- status,
3062
- "[xcashu] Failed to receive refund token",
3063
- requestId
3064
- );
3065
- }
3066
- }
3067
- if (status === 402 && !tryNextProvider && (this.mode === "apikeys" || this.mode === "lazyrefund")) {
3068
- this.storageAdapter.getApiKey(baseUrl);
3069
- let topupAmount = params.requiredSats;
3070
- try {
3071
- let currentBalance = 0;
3072
- if (this.mode === "apikeys") {
3073
- const currentBalanceInfo = await this.balanceManager.getTokenBalance(
3074
- params.token,
3075
- baseUrl
3076
- );
3077
- currentBalance = currentBalanceInfo.unit === "msat" ? currentBalanceInfo.amount / 1e3 : currentBalanceInfo.amount;
3078
- } else if (this.mode === "lazyrefund") {
3079
- const distribution = this.storageAdapter.getCachedTokenDistribution();
3080
- const tokenEntry = distribution.find((t) => t.baseUrl === baseUrl);
3081
- currentBalance = tokenEntry?.amount ?? 0;
3082
- }
3083
- const shortfall = Math.max(0, params.requiredSats - currentBalance);
3084
- topupAmount = shortfall > 0 ? shortfall : params.requiredSats;
3085
- } catch (e) {
3086
- this._log(
3087
- "WARN",
3088
- "Could not get current token balance for topup calculation:",
3089
- e
3090
- );
3091
- }
3092
- const topupResult = await this.balanceManager.topUp({
3093
- mintUrl,
3094
- baseUrl,
3095
- amount: topupAmount * TOPUP_MARGIN,
3096
- token: params.token
3097
- });
3098
- this._log(
3099
- "DEBUG",
3100
- `[RoutstrClient] _handleErrorResponse: Topup result for ${baseUrl}: success=${topupResult.success}, message=${topupResult.message}`
3101
- );
3102
- if (!topupResult.success) {
3103
- const message = topupResult.message || "";
3104
- if (message.includes("Insufficient balance")) {
3105
- const needMatch = message.match(/need (\d+)/);
3106
- const haveMatch = message.match(/have (\d+)/);
3107
- const required = needMatch ? parseInt(needMatch[1], 10) : params.requiredSats;
3108
- const available = haveMatch ? parseInt(haveMatch[1], 10) : 0;
3109
- this._log(
3110
- "DEBUG",
3111
- `[RoutstrClient] _handleErrorResponse: Insufficient balance, need=${required}, have=${available}`
3112
- );
3113
- throw new InsufficientBalanceError(
3114
- required,
3115
- available,
3116
- 0,
3117
- "",
3118
- message
3119
- );
3120
- } else {
3121
- this._log(
3122
- "DEBUG",
3123
- `[RoutstrClient] _handleErrorResponse: Topup failed with non-insufficient-balance error, will try next provider`
3124
- );
3125
- tryNextProvider = true;
3126
- }
3127
- } else {
3128
- this._log(
3129
- "DEBUG",
3130
- `[RoutstrClient] _handleErrorResponse: Topup successful, will retry with new token`
3131
- );
3132
- }
3133
- if (!tryNextProvider) {
3134
- if (retryCount < MAX_RETRIES_PER_PROVIDER) {
3135
- this._log(
3136
- "DEBUG",
3137
- `[RoutstrClient] _handleErrorResponse: Retrying 402 (attempt ${retryCount + 1}/${MAX_RETRIES_PER_PROVIDER})`
3138
- );
3139
- return this._makeRequest({
3140
- ...params,
3141
- token: params.token,
3142
- headers: this._withAuthHeader(params.baseHeaders, params.token),
3143
- retryCount: retryCount + 1
3144
- });
3145
- } else {
3146
- this._log(
3147
- "DEBUG",
3148
- `[RoutstrClient] _handleErrorResponse: 402 retry limit reached (${retryCount}/${MAX_RETRIES_PER_PROVIDER}), failing over to next provider`
3149
- );
3150
- tryNextProvider = true;
3151
- }
2949
+ return new Promise((resolve, reject) => {
2950
+ const request = indexedDB.open(dbName, 1);
2951
+ request.onupgradeneeded = () => {
2952
+ const db = request.result;
2953
+ if (!db.objectStoreNames.contains(storeName)) {
2954
+ db.createObjectStore(storeName);
3152
2955
  }
2956
+ };
2957
+ request.onsuccess = () => resolve(request.result);
2958
+ request.onerror = () => reject(request.error);
2959
+ });
2960
+ };
2961
+ var createIndexedDBDriver = (options = {}) => {
2962
+ const dbName = options.dbName || "routstr-sdk";
2963
+ const storeName = options.storeName || "sdk_storage";
2964
+ let dbPromise = null;
2965
+ const getDb = () => {
2966
+ if (!dbPromise) {
2967
+ dbPromise = openDatabase(dbName, storeName);
3153
2968
  }
3154
- const isInsufficientBalance413 = status === 413 && responseBody?.includes("Insufficient balance");
3155
- if (isInsufficientBalance413 && !tryNextProvider && this.mode === "apikeys") {
3156
- let retryToken = params.token;
2969
+ return dbPromise;
2970
+ };
2971
+ return {
2972
+ async getItem(key, defaultValue) {
3157
2973
  try {
3158
- const latestBalanceInfo = await this.balanceManager.getTokenBalance(
3159
- params.token,
3160
- baseUrl
3161
- );
3162
- if (latestBalanceInfo.isInvalidApiKey) {
3163
- this._log(
3164
- "DEBUG",
3165
- `[RoutstrClient] _handleErrorResponse: Invalid API key (proofs already spent), removing for ${baseUrl}`
3166
- );
3167
- this.storageAdapter.removeApiKey(baseUrl);
3168
- tryNextProvider = true;
3169
- } else {
3170
- const latestTokenBalance = latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
3171
- if (latestBalanceInfo.apiKey) {
3172
- const storedApiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
3173
- if (storedApiKeyEntry?.key !== latestBalanceInfo.apiKey) {
3174
- if (storedApiKeyEntry) {
3175
- this.storageAdapter.removeApiKey(baseUrl);
2974
+ const db = await getDb();
2975
+ return new Promise((resolve, reject) => {
2976
+ const tx = db.transaction(storeName, "readonly");
2977
+ const store = tx.objectStore(storeName);
2978
+ const request = store.get(key);
2979
+ request.onsuccess = () => {
2980
+ const raw = request.result;
2981
+ if (raw === void 0) {
2982
+ resolve(defaultValue);
2983
+ return;
2984
+ }
2985
+ if (typeof raw === "string") {
2986
+ try {
2987
+ resolve(JSON.parse(raw));
2988
+ } catch {
2989
+ if (typeof defaultValue === "string") {
2990
+ resolve(raw);
2991
+ } else {
2992
+ resolve(defaultValue);
2993
+ }
3176
2994
  }
3177
- this.storageAdapter.setApiKey(baseUrl, latestBalanceInfo.apiKey);
2995
+ } else {
2996
+ resolve(raw);
3178
2997
  }
3179
- retryToken = latestBalanceInfo.apiKey;
3180
- }
3181
- if (latestTokenBalance >= 0) {
3182
- this.storageAdapter.updateApiKeyBalance(
3183
- baseUrl,
3184
- latestTokenBalance
3185
- );
3186
- }
3187
- }
2998
+ };
2999
+ request.onerror = () => reject(request.error);
3000
+ });
3188
3001
  } catch (error) {
3189
- this._log(
3190
- "WARN",
3191
- `[RoutstrClient] _handleErrorResponse: Failed to refresh API key after 413 insufficient balance for ${baseUrl}`,
3192
- error
3193
- );
3002
+ console.error(`IndexedDB getItem failed for key "${key}":`, error);
3003
+ return defaultValue;
3194
3004
  }
3195
- if (retryCount < MAX_RETRIES_PER_PROVIDER) {
3196
- this._log(
3197
- "DEBUG",
3198
- `[RoutstrClient] _handleErrorResponse: Retrying 413 (attempt ${retryCount + 1}/${MAX_RETRIES_PER_PROVIDER})`
3199
- );
3200
- return this._makeRequest({
3201
- ...params,
3202
- token: retryToken,
3203
- headers: this._withAuthHeader(params.baseHeaders, retryToken),
3204
- retryCount: retryCount + 1
3005
+ },
3006
+ async setItem(key, value) {
3007
+ try {
3008
+ const db = await getDb();
3009
+ return new Promise((resolve, reject) => {
3010
+ const tx = db.transaction(storeName, "readwrite");
3011
+ const store = tx.objectStore(storeName);
3012
+ store.put(JSON.stringify(value), key);
3013
+ tx.oncomplete = () => resolve();
3014
+ tx.onerror = () => reject(tx.error);
3205
3015
  });
3206
- } else {
3207
- this._log(
3208
- "DEBUG",
3209
- `[RoutstrClient] _handleErrorResponse: 413 retry limit reached (${retryCount}/${MAX_RETRIES_PER_PROVIDER}), failing over to next provider`
3210
- );
3211
- tryNextProvider = true;
3016
+ } catch (error) {
3017
+ console.error(`IndexedDB setItem failed for key "${key}":`, error);
3018
+ }
3019
+ },
3020
+ async removeItem(key) {
3021
+ try {
3022
+ const db = await getDb();
3023
+ return new Promise((resolve, reject) => {
3024
+ const tx = db.transaction(storeName, "readwrite");
3025
+ const store = tx.objectStore(storeName);
3026
+ store.delete(key);
3027
+ tx.oncomplete = () => resolve();
3028
+ tx.onerror = () => reject(tx.error);
3029
+ });
3030
+ } catch (error) {
3031
+ console.error(`IndexedDB removeItem failed for key "${key}":`, error);
3212
3032
  }
3213
3033
  }
3214
- if ((status === 401 || status === 403 || status === 413 || status === 400 || status === 500 || status === 502 || status === 503 || status === 504 || status === 521) && !tryNextProvider) {
3215
- this._log(
3216
- "DEBUG",
3217
- `[RoutstrClient] _handleErrorResponse: Status ${status} (auth/server error), attempting refund for ${baseUrl}, mode=${this.mode}`
3218
- );
3219
- if (this.mode === "lazyrefund") {
3220
- try {
3221
- const refundResult = await this.balanceManager.refund({
3222
- mintUrl,
3223
- baseUrl,
3224
- token: params.token
3225
- });
3226
- this._log(
3227
- "DEBUG",
3228
- `[RoutstrClient] _handleErrorResponse: Lazyrefund result: success=${refundResult.success}`
3229
- );
3230
- if (refundResult.success) this.storageAdapter.removeToken(baseUrl);
3231
- else
3232
- throw new ProviderError(
3233
- baseUrl,
3234
- status,
3235
- "refund failed",
3236
- requestId
3237
- );
3238
- } catch (error) {
3239
- throw new ProviderError(
3240
- baseUrl,
3241
- status,
3242
- "Failed to refund token",
3243
- requestId
3244
- );
3245
- }
3246
- } else if (this.mode === "apikeys") {
3247
- this._log(
3248
- "DEBUG",
3249
- `[RoutstrClient] _handleErrorResponse: Attempting API key refund for ${baseUrl}, key preview=${token}`
3250
- );
3251
- const initialBalance = await this.balanceManager.getTokenBalance(
3252
- token,
3253
- baseUrl
3254
- );
3255
- this._log(
3256
- "DEBUG",
3257
- `[RoutstrClient] _handleErrorResponse: Initial API key balance: ${initialBalance.amount}`
3034
+ };
3035
+ };
3036
+
3037
+ // storage/keys.ts
3038
+ var SDK_STORAGE_KEYS = {
3039
+ MODELS_FROM_ALL_PROVIDERS: "modelsFromAllProviders",
3040
+ LAST_USED_MODEL: "lastUsedModel",
3041
+ BASE_URLS_LIST: "base_urls_list",
3042
+ DISABLED_PROVIDERS: "disabled_providers",
3043
+ MINTS_FROM_ALL_PROVIDERS: "mints_from_all_providers",
3044
+ INFO_FROM_ALL_PROVIDERS: "info_from_all_providers",
3045
+ LAST_MODELS_UPDATE: "lastModelsUpdate",
3046
+ LAST_BASE_URLS_UPDATE: "lastBaseUrlsUpdate",
3047
+ LOCAL_CASHU_TOKENS: "local_cashu_tokens",
3048
+ API_KEYS: "api_keys",
3049
+ CHILD_KEYS: "child_keys",
3050
+ ROUTSTR21_MODELS: "routstr21Models",
3051
+ LAST_ROUTSTR21_MODELS_UPDATE: "lastRoutstr21ModelsUpdate",
3052
+ CACHED_RECEIVE_TOKENS: "cached_receive_tokens",
3053
+ USAGE_TRACKING: "usage_tracking",
3054
+ CLIENT_IDS: "client_ids"
3055
+ };
3056
+
3057
+ // storage/usageTracking/indexedDB.ts
3058
+ var DEFAULT_DB_NAME = "routstr-sdk";
3059
+ var DEFAULT_STORE_NAME = "usage_tracking";
3060
+ var MIGRATION_MARKER_KEY = "usage_tracking_migration_v1";
3061
+ var isBrowser2 = typeof indexedDB !== "undefined";
3062
+ var normalizeBaseUrl = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
3063
+ var openDatabase2 = (dbName, storeName) => {
3064
+ if (!isBrowser2) {
3065
+ return Promise.reject(new Error("IndexedDB is not available"));
3066
+ }
3067
+ return new Promise((resolve, reject) => {
3068
+ const request = indexedDB.open(dbName, 1);
3069
+ request.onupgradeneeded = () => {
3070
+ const db = request.result;
3071
+ if (!db.objectStoreNames.contains(storeName)) {
3072
+ const store = db.createObjectStore(storeName, { keyPath: "id" });
3073
+ store.createIndex("timestamp", "timestamp", { unique: false });
3074
+ store.createIndex("modelId", "modelId", { unique: false });
3075
+ store.createIndex("baseUrl", "baseUrl", { unique: false });
3076
+ store.createIndex("sessionId", "sessionId", { unique: false });
3077
+ store.createIndex("client", "client", { unique: false });
3078
+ }
3079
+ };
3080
+ request.onsuccess = () => resolve(request.result);
3081
+ request.onerror = () => reject(request.error);
3082
+ });
3083
+ };
3084
+ var matchesFilters = (entry, options = {}) => {
3085
+ if (typeof options.before === "number" && entry.timestamp >= options.before) {
3086
+ return false;
3087
+ }
3088
+ if (typeof options.after === "number" && entry.timestamp <= options.after) {
3089
+ return false;
3090
+ }
3091
+ if (options.modelId && entry.modelId !== options.modelId) {
3092
+ return false;
3093
+ }
3094
+ if (options.baseUrl && normalizeBaseUrl(entry.baseUrl) !== normalizeBaseUrl(options.baseUrl)) {
3095
+ return false;
3096
+ }
3097
+ if (options.sessionId && entry.sessionId !== options.sessionId) {
3098
+ return false;
3099
+ }
3100
+ if (options.client && entry.client !== options.client) {
3101
+ return false;
3102
+ }
3103
+ return true;
3104
+ };
3105
+ var createIndexedDBUsageTrackingDriver = (options = {}) => {
3106
+ const dbName = options.dbName || DEFAULT_DB_NAME;
3107
+ const storeName = options.storeName || DEFAULT_STORE_NAME;
3108
+ const legacyStorageDriver = options.legacyStorageDriver;
3109
+ let dbPromise = null;
3110
+ let migrationPromise = null;
3111
+ const getDb = () => {
3112
+ if (!dbPromise) {
3113
+ dbPromise = openDatabase2(dbName, storeName);
3114
+ }
3115
+ return dbPromise;
3116
+ };
3117
+ const putMany = async (entries) => {
3118
+ if (entries.length === 0) return;
3119
+ const db = await getDb();
3120
+ await new Promise((resolve, reject) => {
3121
+ const tx = db.transaction(storeName, "readwrite");
3122
+ const store = tx.objectStore(storeName);
3123
+ for (const entry of entries) {
3124
+ store.put({ ...entry, baseUrl: normalizeBaseUrl(entry.baseUrl) });
3125
+ }
3126
+ tx.oncomplete = () => resolve();
3127
+ tx.onerror = () => reject(tx.error);
3128
+ });
3129
+ };
3130
+ const ensureMigrated = async () => {
3131
+ if (!legacyStorageDriver) return;
3132
+ if (!migrationPromise) {
3133
+ migrationPromise = (async () => {
3134
+ const migrated = await legacyStorageDriver.getItem(
3135
+ MIGRATION_MARKER_KEY,
3136
+ false
3258
3137
  );
3259
- const refundResult = await this.balanceManager.refundApiKey({
3260
- mintUrl,
3261
- baseUrl,
3262
- apiKey: token
3263
- });
3264
- this._log(
3265
- "DEBUG",
3266
- `[RoutstrClient] _handleErrorResponse: API key refund result: success=${refundResult.success}, message=${refundResult.message}`
3138
+ if (migrated) return;
3139
+ const legacyEntries = await legacyStorageDriver.getItem(
3140
+ SDK_STORAGE_KEYS.USAGE_TRACKING,
3141
+ []
3267
3142
  );
3268
- if (!refundResult.success && initialBalance.amount > 0) {
3269
- throw new ProviderError(
3270
- baseUrl,
3271
- status,
3272
- refundResult.message ?? "Unknown error"
3273
- );
3143
+ if (legacyEntries.length > 0) {
3144
+ await putMany(legacyEntries);
3145
+ await legacyStorageDriver.removeItem(SDK_STORAGE_KEYS.USAGE_TRACKING);
3274
3146
  }
3275
- }
3276
- }
3277
- this.providerManager.markFailed(baseUrl);
3278
- this._log(
3279
- "DEBUG",
3280
- `[RoutstrClient] _handleErrorResponse: Marked provider ${baseUrl} as failed`
3281
- );
3282
- if (!selectedModel) {
3283
- throw new ProviderError(
3284
- baseUrl,
3285
- status,
3286
- "Funny, no selected model. HMM. "
3287
- );
3147
+ await legacyStorageDriver.setItem(MIGRATION_MARKER_KEY, true);
3148
+ })();
3288
3149
  }
3289
- const nextProvider = this.providerManager.findNextBestProvider(
3290
- selectedModel.id,
3291
- baseUrl
3292
- );
3293
- if (nextProvider) {
3294
- this._log(
3295
- "DEBUG",
3296
- `[RoutstrClient] _handleErrorResponse: Failing over to next provider: ${nextProvider}, model: ${selectedModel.id}`
3297
- );
3298
- const newModel = await this.providerManager.getModelForProvider(
3299
- nextProvider,
3300
- selectedModel.id
3301
- ) ?? selectedModel;
3302
- const messagesForPricing = Array.isArray(
3303
- body?.messages
3304
- ) ? body.messages : [];
3305
- const newRequiredSats = this.providerManager.getRequiredSatsForModel(
3306
- newModel,
3307
- messagesForPricing,
3308
- params.maxTokens
3309
- );
3310
- this._log(
3311
- "DEBUG",
3312
- `[RoutstrClient] _handleErrorResponse: Creating new token for failover provider ${nextProvider}, required sats: ${newRequiredSats}`
3313
- );
3314
- const spendResult = await this._spendToken({
3315
- mintUrl,
3316
- amount: newRequiredSats,
3317
- baseUrl: nextProvider
3150
+ await migrationPromise;
3151
+ };
3152
+ return {
3153
+ async migrate() {
3154
+ await ensureMigrated();
3155
+ },
3156
+ async append(entry) {
3157
+ await ensureMigrated();
3158
+ await putMany([entry]);
3159
+ },
3160
+ async appendMany(entries) {
3161
+ await ensureMigrated();
3162
+ await putMany(entries);
3163
+ },
3164
+ async list(options2 = {}) {
3165
+ await ensureMigrated();
3166
+ const db = await getDb();
3167
+ return new Promise((resolve, reject) => {
3168
+ const tx = db.transaction(storeName, "readonly");
3169
+ const store = tx.objectStore(storeName);
3170
+ const index = store.index("timestamp");
3171
+ const direction = "prev";
3172
+ const request = index.openCursor(null, direction);
3173
+ const results = [];
3174
+ const limit = options2.limit;
3175
+ request.onsuccess = () => {
3176
+ const cursor = request.result;
3177
+ if (!cursor) {
3178
+ resolve(results);
3179
+ return;
3180
+ }
3181
+ const value = cursor.value;
3182
+ if (matchesFilters(value, options2)) {
3183
+ results.push(value);
3184
+ if (typeof limit === "number" && results.length >= limit) {
3185
+ resolve(results);
3186
+ return;
3187
+ }
3188
+ }
3189
+ cursor.continue();
3190
+ };
3191
+ request.onerror = () => reject(request.error);
3318
3192
  });
3319
- return this._makeRequest({
3320
- ...params,
3321
- path,
3322
- method,
3323
- body,
3324
- baseUrl: nextProvider,
3325
- selectedModel: newModel,
3326
- token: spendResult.token,
3327
- requiredSats: newRequiredSats,
3328
- headers: this._withAuthHeader(params.baseHeaders, spendResult.token),
3329
- retryCount: 0
3193
+ },
3194
+ async count(options2 = {}) {
3195
+ const results = await this.list(options2);
3196
+ return results.length;
3197
+ },
3198
+ async deleteOlderThan(timestamp) {
3199
+ await ensureMigrated();
3200
+ const db = await getDb();
3201
+ return new Promise((resolve, reject) => {
3202
+ const tx = db.transaction(storeName, "readwrite");
3203
+ const store = tx.objectStore(storeName);
3204
+ const index = store.index("timestamp");
3205
+ const range = IDBKeyRange.upperBound(timestamp, true);
3206
+ const request = index.openCursor(range);
3207
+ let deleted = 0;
3208
+ request.onsuccess = () => {
3209
+ const cursor = request.result;
3210
+ if (!cursor) {
3211
+ resolve(deleted);
3212
+ return;
3213
+ }
3214
+ deleted += 1;
3215
+ cursor.delete();
3216
+ cursor.continue();
3217
+ };
3218
+ request.onerror = () => reject(request.error);
3219
+ });
3220
+ },
3221
+ async clear() {
3222
+ await ensureMigrated();
3223
+ const db = await getDb();
3224
+ await new Promise((resolve, reject) => {
3225
+ const tx = db.transaction(storeName, "readwrite");
3226
+ tx.objectStore(storeName).clear();
3227
+ tx.oncomplete = () => resolve();
3228
+ tx.onerror = () => reject(tx.error);
3330
3229
  });
3331
3230
  }
3332
- throw new FailoverError(
3333
- baseUrl,
3334
- Array.from(this.providerManager.getFailedProviders())
3231
+ };
3232
+ };
3233
+
3234
+ // storage/usageTracking/sqlite.ts
3235
+ var MIGRATION_MARKER_KEY2 = "usage_tracking_migration_v1";
3236
+ var normalizeBaseUrl2 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
3237
+ var isBun2 = () => {
3238
+ return typeof process.versions.bun !== "undefined";
3239
+ };
3240
+ var createDatabase2 = (dbPath) => {
3241
+ if (isBun2()) {
3242
+ throw new Error(
3243
+ "SQLite driver not supported in Bun. Use createMemoryDriver() instead."
3335
3244
  );
3336
3245
  }
3337
- /**
3338
- * Handle post-response balance update for all modes
3339
- */
3340
- async _handlePostResponseBalanceUpdate(params) {
3341
- const { token, baseUrl, initialTokenBalance, fallbackSatsSpent, response } = params;
3342
- let satsSpent = initialTokenBalance;
3343
- if (this.mode === "xcashu" && response) {
3344
- const refundToken = response.headers.get("x-cashu") ?? void 0;
3345
- if (refundToken) {
3346
- try {
3347
- const receiveResult = await this.cashuSpender.receiveToken(refundToken);
3348
- satsSpent = initialTokenBalance - receiveResult.amount * (receiveResult.unit == "sat" ? 1 : 1e3);
3349
- } catch (error) {
3350
- this._log("ERROR", "[xcashu] Failed to receive refund token:", error);
3351
- }
3352
- }
3353
- } else if (this.mode === "lazyrefund") {
3354
- const latestBalanceInfo = await this.balanceManager.getTokenBalance(
3355
- token,
3356
- baseUrl
3357
- );
3358
- const latestTokenBalance = latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
3359
- this.storageAdapter.updateTokenBalance(baseUrl, latestTokenBalance);
3360
- satsSpent = initialTokenBalance - latestTokenBalance;
3361
- } else if (this.mode === "apikeys") {
3362
- try {
3363
- const latestBalanceInfo = await this.balanceManager.getTokenBalance(
3364
- token,
3365
- baseUrl
3366
- );
3367
- this._log(
3368
- "DEBUG",
3369
- "LATEST Balance",
3370
- latestBalanceInfo.amount,
3371
- latestBalanceInfo.reserved,
3372
- latestBalanceInfo.apiKey,
3373
- baseUrl
3374
- );
3375
- const latestTokenBalance = latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
3376
- const storedApiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
3377
- if (storedApiKeyEntry?.key.startsWith("cashu") && latestBalanceInfo.apiKey) {
3378
- this.storageAdapter.removeApiKey(baseUrl);
3379
- this.storageAdapter.setApiKey(baseUrl, latestBalanceInfo.apiKey);
3380
- }
3381
- this.storageAdapter.updateApiKeyBalance(baseUrl, latestTokenBalance);
3382
- satsSpent = initialTokenBalance - latestTokenBalance;
3383
- } catch (e) {
3384
- this._log("WARN", "Could not get updated API key balance:", e);
3385
- satsSpent = fallbackSatsSpent ?? initialTokenBalance;
3386
- }
3387
- }
3388
- return satsSpent;
3389
- }
3390
- /**
3391
- * Convert messages for API format
3392
- */
3393
- async _convertMessages(messages) {
3394
- return Promise.all(
3395
- messages.filter((m) => m.role !== "system").map(async (m) => ({
3396
- role: m.role,
3397
- content: typeof m.content === "string" ? m.content : m.content
3398
- }))
3246
+ let Database = null;
3247
+ try {
3248
+ Database = __require("better-sqlite3");
3249
+ } catch (error) {
3250
+ throw new Error(
3251
+ `better-sqlite3 is required for sqlite usage tracking. Install it to use sqlite storage. (${error})`
3399
3252
  );
3400
3253
  }
3401
- /**
3402
- * Create assistant message from streaming result
3403
- */
3404
- async _createAssistantMessage(result) {
3405
- if (result.images && result.images.length > 0) {
3406
- const content = [];
3407
- if (result.content) {
3408
- content.push({
3409
- type: "text",
3410
- text: result.content,
3411
- thinking: result.thinking,
3412
- citations: result.citations,
3413
- annotations: result.annotations
3414
- });
3415
- }
3416
- for (const img of result.images) {
3417
- content.push({
3418
- type: "image_url",
3419
- image_url: {
3420
- url: img.image_url.url
3421
- }
3422
- });
3423
- }
3424
- return {
3425
- role: "assistant",
3426
- content
3427
- };
3428
- }
3429
- return {
3430
- role: "assistant",
3431
- content: result.content || ""
3432
- };
3254
+ return new Database(dbPath);
3255
+ };
3256
+ var buildWhereClause = (options = {}) => {
3257
+ const clauses = [];
3258
+ const params = [];
3259
+ if (typeof options.before === "number") {
3260
+ clauses.push("timestamp < ?");
3261
+ params.push(options.before);
3433
3262
  }
3434
- /**
3435
- * Create a child key for a parent API key via the provider's API
3436
- * POST /v1/balance/child-key
3437
- */
3438
- async _createChildKey(baseUrl, parentApiKey, options) {
3439
- const response = await fetch(`${baseUrl}v1/balance/child-key`, {
3440
- method: "POST",
3441
- headers: {
3442
- "Content-Type": "application/json",
3443
- Authorization: `Bearer ${parentApiKey}`
3444
- },
3445
- body: JSON.stringify({
3446
- count: options?.count ?? 1,
3447
- balance_limit: options?.balanceLimit,
3448
- balance_limit_reset: options?.balanceLimitReset,
3449
- validity_date: options?.validityDate
3450
- })
3451
- });
3452
- if (!response.ok) {
3453
- throw new Error(
3454
- `Failed to create child key: ${response.status} ${await response.text()}`
3455
- );
3456
- }
3457
- const data = await response.json();
3458
- return {
3459
- childKey: data.api_keys?.[0],
3460
- balance: data.balance ?? 0,
3461
- balanceLimit: data.balance_limit,
3462
- validityDate: data.validity_date
3463
- };
3263
+ if (typeof options.after === "number") {
3264
+ clauses.push("timestamp > ?");
3265
+ params.push(options.after);
3464
3266
  }
3465
- /**
3466
- * Calculate estimated costs from usage
3467
- */
3468
- _getEstimatedCosts(selectedModel, streamingResult) {
3469
- let estimatedCosts = 0;
3470
- if (streamingResult.usage) {
3471
- const { completion_tokens, prompt_tokens } = streamingResult.usage;
3472
- if (completion_tokens !== void 0 && prompt_tokens !== void 0) {
3473
- estimatedCosts = (selectedModel.sats_pricing?.completion ?? 0) * completion_tokens + (selectedModel.sats_pricing?.prompt ?? 0) * prompt_tokens;
3474
- }
3475
- }
3476
- return estimatedCosts;
3267
+ if (options.modelId) {
3268
+ clauses.push("model_id = ?");
3269
+ params.push(options.modelId);
3477
3270
  }
3478
- /**
3479
- * Get pending cashu token amount
3480
- */
3481
- _getPendingCashuTokenAmount() {
3482
- const distribution = this.storageAdapter.getCachedTokenDistribution();
3483
- return distribution.reduce((total, item) => total + item.amount, 0);
3271
+ if (options.baseUrl) {
3272
+ clauses.push("base_url = ?");
3273
+ params.push(normalizeBaseUrl2(options.baseUrl));
3484
3274
  }
3485
- /**
3486
- * Handle errors and notify callbacks
3487
- */
3488
- _handleError(error, callbacks) {
3489
- this._log("ERROR", "[RoutstrClient] _handleError: Error occurred", error);
3490
- if (error instanceof Error) {
3491
- const isStreamError = error.message.includes("Error in input stream") || error.message.includes("Load failed");
3492
- const modifiedErrorMsg = isStreamError ? "AI stream was cut off, turn on Keep Active or please try again" : error.message;
3493
- this._log(
3494
- "ERROR",
3495
- `[RoutstrClient] _handleError: Error type=${error.constructor.name}, message=${modifiedErrorMsg}, isStreamError=${isStreamError}`
3496
- );
3497
- callbacks.onMessageAppend({
3498
- role: "system",
3499
- content: "Uncaught Error: " + modifiedErrorMsg + (this.alertLevel === "max" ? " | " + error.stack : "")
3500
- });
3501
- } else {
3502
- callbacks.onMessageAppend({
3503
- role: "system",
3504
- content: "Unknown Error: Please tag Routstr on Nostr and/or retry."
3505
- });
3506
- }
3275
+ if (options.sessionId) {
3276
+ clauses.push("session_id = ?");
3277
+ params.push(options.sessionId);
3507
3278
  }
3508
- /**
3509
- * Check wallet balance and throw if insufficient
3510
- */
3511
- async _checkBalance() {
3512
- const balances = await this.walletAdapter.getBalances();
3513
- const totalBalance = Object.values(balances).reduce((sum, v) => sum + v, 0);
3514
- if (totalBalance <= 0) {
3515
- throw new InsufficientBalanceError(1, 0);
3516
- }
3279
+ if (options.client) {
3280
+ clauses.push("client = ?");
3281
+ params.push(options.client);
3517
3282
  }
3518
- /**
3519
- * Spend a token using CashuSpender with standardized error handling
3520
- */
3521
- async _spendToken(params) {
3522
- const { mintUrl, amount, baseUrl } = params;
3523
- this._log(
3524
- "DEBUG",
3525
- `[RoutstrClient] _spendToken: mode=${this.mode}, amount=${amount}, baseUrl=${baseUrl}, mintUrl=${mintUrl}`
3283
+ return {
3284
+ sql: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
3285
+ params
3286
+ };
3287
+ };
3288
+ var createSqliteUsageTrackingDriver = (options = {}) => {
3289
+ const dbPath = options.dbPath || "routstr.sqlite";
3290
+ const tableName = options.tableName || "usage_tracking";
3291
+ const db = createDatabase2(dbPath);
3292
+ const legacyStorageDriver = options.legacyStorageDriver;
3293
+ db.exec(`
3294
+ CREATE TABLE IF NOT EXISTS ${tableName} (
3295
+ id TEXT PRIMARY KEY,
3296
+ timestamp INTEGER NOT NULL,
3297
+ model_id TEXT NOT NULL,
3298
+ base_url TEXT NOT NULL,
3299
+ request_id TEXT NOT NULL,
3300
+ cost REAL NOT NULL,
3301
+ sats_cost REAL NOT NULL,
3302
+ prompt_tokens INTEGER NOT NULL,
3303
+ completion_tokens INTEGER NOT NULL,
3304
+ total_tokens INTEGER NOT NULL,
3305
+ client TEXT,
3306
+ session_id TEXT,
3307
+ tags TEXT
3526
3308
  );
3527
- if (this.mode === "apikeys") {
3528
- let parentApiKey = this.storageAdapter.getApiKey(baseUrl);
3529
- if (!parentApiKey) {
3530
- this._log(
3531
- "DEBUG",
3532
- `[RoutstrClient] _spendToken: No existing API key for ${baseUrl}, creating new one via Cashu`
3533
- );
3534
- const spendResult2 = await this.cashuSpender.spend({
3535
- mintUrl,
3536
- amount: amount * TOPUP_MARGIN,
3537
- baseUrl: "",
3538
- reuseToken: false
3539
- });
3540
- if (!spendResult2.token) {
3541
- this._log(
3542
- "ERROR",
3543
- `[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error:`,
3544
- spendResult2.error
3545
- );
3546
- throw new Error(
3547
- `[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error: ${spendResult2.error}`
3548
- );
3549
- } else {
3550
- this._log(
3551
- "DEBUG",
3552
- `[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult2.token}`
3553
- );
3554
- }
3555
- this._log(
3556
- "DEBUG",
3557
- `[RoutstrClient] _spendToken: Created API key for ${baseUrl}, key preview: ${spendResult2.token}, balance: ${spendResult2.balance}`
3558
- );
3559
- try {
3560
- this.storageAdapter.setApiKey(baseUrl, spendResult2.token);
3561
- } catch (error) {
3562
- if (error instanceof Error && error.message.includes("ApiKey already exists")) {
3563
- const tryReceiveTokenResult = await this.cashuSpender.receiveToken(
3564
- spendResult2.token
3565
- );
3566
- if (tryReceiveTokenResult.success) {
3567
- this._log(
3568
- "DEBUG",
3569
- `[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${tryReceiveTokenResult.amount}`
3570
- );
3571
- } else {
3572
- this._log(
3573
- "DEBUG",
3574
- `[RoutstrClient] _handleErrorResponse: Token restore failed or not needed`
3575
- );
3576
- }
3577
- this._log(
3578
- "DEBUG",
3579
- `[RoutstrClient] _spendToken: API key already exists for ${baseUrl}, using existing key`
3580
- );
3581
- } else {
3582
- throw error;
3583
- }
3584
- }
3585
- parentApiKey = this.storageAdapter.getApiKey(baseUrl);
3586
- } else {
3587
- this._log(
3588
- "DEBUG",
3589
- `[RoutstrClient] _spendToken: Using existing API key for ${baseUrl}, key preview: ${parentApiKey.key}`
3590
- );
3309
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_timestamp ON ${tableName}(timestamp);
3310
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_model_id ON ${tableName}(model_id);
3311
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_base_url ON ${tableName}(base_url);
3312
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_session_id ON ${tableName}(session_id);
3313
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_client ON ${tableName}(client);
3314
+ `);
3315
+ const insertStmt = db.prepare(`
3316
+ INSERT OR REPLACE INTO ${tableName} (
3317
+ id, timestamp, model_id, base_url, request_id,
3318
+ cost, sats_cost, prompt_tokens, completion_tokens, total_tokens,
3319
+ client, session_id, tags
3320
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3321
+ `);
3322
+ let migrationComplete = false;
3323
+ const appendOne = (entry) => {
3324
+ insertStmt.run(
3325
+ entry.id,
3326
+ entry.timestamp,
3327
+ entry.modelId,
3328
+ normalizeBaseUrl2(entry.baseUrl),
3329
+ entry.requestId,
3330
+ entry.cost,
3331
+ entry.satsCost,
3332
+ entry.promptTokens,
3333
+ entry.completionTokens,
3334
+ entry.totalTokens,
3335
+ entry.client ?? null,
3336
+ entry.sessionId ?? null,
3337
+ JSON.stringify(entry.tags ?? [])
3338
+ );
3339
+ };
3340
+ const ensureMigrated = async () => {
3341
+ if (!legacyStorageDriver || migrationComplete) return;
3342
+ const migrated = await legacyStorageDriver.getItem(
3343
+ MIGRATION_MARKER_KEY2,
3344
+ false
3345
+ );
3346
+ if (migrated) {
3347
+ migrationComplete = true;
3348
+ return;
3349
+ }
3350
+ const legacyEntries = await legacyStorageDriver.getItem(
3351
+ SDK_STORAGE_KEYS.USAGE_TRACKING,
3352
+ []
3353
+ );
3354
+ for (const entry of legacyEntries) {
3355
+ appendOne(entry);
3356
+ }
3357
+ if (legacyEntries.length > 0) {
3358
+ await legacyStorageDriver.removeItem(SDK_STORAGE_KEYS.USAGE_TRACKING);
3359
+ }
3360
+ await legacyStorageDriver.setItem(MIGRATION_MARKER_KEY2, true);
3361
+ migrationComplete = true;
3362
+ };
3363
+ const mapRow = (row) => ({
3364
+ id: row.id,
3365
+ timestamp: row.timestamp,
3366
+ modelId: row.model_id,
3367
+ baseUrl: row.base_url,
3368
+ requestId: row.request_id,
3369
+ cost: row.cost,
3370
+ satsCost: row.sats_cost,
3371
+ promptTokens: row.prompt_tokens,
3372
+ completionTokens: row.completion_tokens,
3373
+ totalTokens: row.total_tokens,
3374
+ client: row.client ?? void 0,
3375
+ sessionId: row.session_id ?? void 0,
3376
+ tags: typeof row.tags === "string" ? JSON.parse(row.tags) : void 0
3377
+ });
3378
+ return {
3379
+ async migrate() {
3380
+ await ensureMigrated();
3381
+ },
3382
+ async append(entry) {
3383
+ await ensureMigrated();
3384
+ appendOne(entry);
3385
+ },
3386
+ async appendMany(entries) {
3387
+ await ensureMigrated();
3388
+ for (const entry of entries) {
3389
+ appendOne(entry);
3591
3390
  }
3592
- let tokenBalance = 0;
3593
- let tokenBalanceUnit = "sat";
3594
- const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
3595
- const distributionForBaseUrl = apiKeyDistribution.find(
3596
- (d) => d.baseUrl === baseUrl
3391
+ },
3392
+ async list(options2 = {}) {
3393
+ await ensureMigrated();
3394
+ const { sql, params } = buildWhereClause(options2);
3395
+ const limitSql = typeof options2.limit === "number" ? " LIMIT ?" : "";
3396
+ const stmt = db.prepare(
3397
+ `SELECT * FROM ${tableName} ${sql} ORDER BY timestamp DESC${limitSql}`
3597
3398
  );
3598
- if (distributionForBaseUrl) {
3599
- tokenBalance = distributionForBaseUrl.amount;
3600
- }
3601
- if (tokenBalance === 0 && parentApiKey) {
3602
- try {
3603
- const balanceInfo = await this.balanceManager.getTokenBalance(
3604
- parentApiKey.key,
3605
- baseUrl
3606
- );
3607
- tokenBalance = balanceInfo.amount;
3608
- tokenBalanceUnit = balanceInfo.unit;
3609
- } catch (e) {
3610
- this._log("WARN", "Could not get initial API key balance:", e);
3611
- }
3612
- }
3613
- this._log(
3614
- "DEBUG",
3615
- `[RoutstrClient] _spendToken: Returning token with balance=${tokenBalance} ${tokenBalanceUnit}`
3616
- );
3617
- return {
3618
- token: parentApiKey?.key ?? "",
3619
- tokenBalance,
3620
- tokenBalanceUnit
3621
- };
3622
- }
3623
- this._log(
3624
- "DEBUG",
3625
- `[RoutstrClient] _spendToken: Calling CashuSpender.spend for amount=${amount}, mintUrl=${mintUrl}, mode=${this.mode}`
3626
- );
3627
- const spendResult = await this.cashuSpender.spend({
3628
- mintUrl,
3629
- amount,
3630
- baseUrl: this.mode === "lazyrefund" ? baseUrl : "",
3631
- reuseToken: this.mode === "lazyrefund"
3632
- });
3633
- if (!spendResult.token) {
3634
- this._log(
3635
- "ERROR",
3636
- `[RoutstrClient] _spendToken: CashuSpender.spend failed, error:`,
3637
- spendResult.error
3638
- );
3639
- } else {
3640
- this._log(
3641
- "DEBUG",
3642
- `[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult.token}, balance: ${spendResult.balance} ${spendResult.unit ?? "sat"}`
3399
+ const rows = stmt.all(
3400
+ ...typeof options2.limit === "number" ? [...params, options2.limit] : params
3643
3401
  );
3402
+ return rows.map(mapRow);
3403
+ },
3404
+ async count(options2 = {}) {
3405
+ await ensureMigrated();
3406
+ const { sql, params } = buildWhereClause(options2);
3407
+ const stmt = db.prepare(`SELECT COUNT(*) as count FROM ${tableName} ${sql}`);
3408
+ const row = stmt.get(...params);
3409
+ return Number(row?.count ?? 0);
3410
+ },
3411
+ async deleteOlderThan(timestamp) {
3412
+ await ensureMigrated();
3413
+ const stmt = db.prepare(`DELETE FROM ${tableName} WHERE timestamp < ?`);
3414
+ const result = stmt.run(timestamp);
3415
+ return result.changes;
3416
+ },
3417
+ async clear() {
3418
+ await ensureMigrated();
3419
+ db.prepare(`DELETE FROM ${tableName}`).run();
3644
3420
  }
3645
- return {
3646
- token: spendResult.token,
3647
- tokenBalance: spendResult.balance,
3648
- tokenBalanceUnit: spendResult.unit ?? "sat"
3649
- };
3421
+ };
3422
+ };
3423
+
3424
+ // storage/usageTracking/memory.ts
3425
+ var normalizeBaseUrl3 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
3426
+ var matchesFilters2 = (entry, options = {}) => {
3427
+ if (typeof options.before === "number" && entry.timestamp >= options.before) {
3428
+ return false;
3650
3429
  }
3651
- /**
3652
- * Build request headers with common defaults and dev mock controls
3653
- */
3654
- _buildBaseHeaders(additionalHeaders = {}, token) {
3655
- const headers = {
3656
- ...additionalHeaders,
3657
- "Content-Type": "application/json"
3658
- };
3659
- return headers;
3430
+ if (typeof options.after === "number" && entry.timestamp <= options.after) {
3431
+ return false;
3660
3432
  }
3661
- /**
3662
- * Attach auth headers using the active client mode
3663
- */
3664
- _withAuthHeader(headers, token) {
3665
- const nextHeaders = { ...headers };
3666
- if (this.mode === "xcashu") {
3667
- nextHeaders["X-Cashu"] = token;
3668
- } else {
3669
- nextHeaders["Authorization"] = `Bearer ${token}`;
3670
- }
3671
- return nextHeaders;
3433
+ if (options.modelId && entry.modelId !== options.modelId) {
3434
+ return false;
3672
3435
  }
3673
- };
3674
-
3675
- // storage/drivers/localStorage.ts
3676
- var canUseLocalStorage = () => {
3677
- return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
3678
- };
3679
- var isQuotaExceeded = (error) => {
3680
- const e = error;
3681
- return !!e && (e?.name === "QuotaExceededError" || e?.code === 22 || e?.code === 1014);
3682
- };
3683
- var NON_CRITICAL_KEYS = /* @__PURE__ */ new Set(["modelsFromAllProviders"]);
3684
- var localStorageDriver = {
3685
- async getItem(key, defaultValue) {
3686
- if (!canUseLocalStorage()) return defaultValue;
3687
- try {
3688
- const item = window.localStorage.getItem(key);
3689
- if (item === null) return defaultValue;
3690
- try {
3691
- return JSON.parse(item);
3692
- } catch (parseError) {
3693
- if (typeof defaultValue === "string") {
3694
- return item;
3695
- }
3696
- throw parseError;
3697
- }
3698
- } catch (error) {
3699
- console.error(`Error retrieving item with key "${key}":`, error);
3700
- if (canUseLocalStorage()) {
3701
- try {
3702
- window.localStorage.removeItem(key);
3703
- } catch (removeError) {
3704
- console.error(
3705
- `Error removing corrupted item with key "${key}":`,
3706
- removeError
3707
- );
3708
- }
3709
- }
3710
- return defaultValue;
3711
- }
3712
- },
3713
- async setItem(key, value) {
3714
- if (!canUseLocalStorage()) return;
3715
- try {
3716
- window.localStorage.setItem(key, JSON.stringify(value));
3717
- } catch (error) {
3718
- if (isQuotaExceeded(error)) {
3719
- if (NON_CRITICAL_KEYS.has(key)) {
3720
- console.warn(
3721
- `Storage quota exceeded; skipping non-critical key "${key}".`
3722
- );
3723
- return;
3724
- }
3725
- try {
3726
- window.localStorage.removeItem("modelsFromAllProviders");
3727
- } catch {
3728
- }
3729
- try {
3730
- window.localStorage.setItem(key, JSON.stringify(value));
3731
- return;
3732
- } catch (retryError) {
3733
- console.warn(
3734
- `Storage quota exceeded; unable to persist key "${key}" after cleanup attempt.`,
3735
- retryError
3736
- );
3737
- return;
3738
- }
3739
- }
3740
- console.error(`Error storing item with key "${key}":`, error);
3741
- }
3742
- },
3743
- async removeItem(key) {
3744
- if (!canUseLocalStorage()) return;
3745
- try {
3746
- window.localStorage.removeItem(key);
3747
- } catch (error) {
3748
- console.error(`Error removing item with key "${key}":`, error);
3749
- }
3436
+ if (options.baseUrl && normalizeBaseUrl3(entry.baseUrl) !== normalizeBaseUrl3(options.baseUrl)) {
3437
+ return false;
3750
3438
  }
3439
+ if (options.sessionId && entry.sessionId !== options.sessionId) {
3440
+ return false;
3441
+ }
3442
+ if (options.client && entry.client !== options.client) {
3443
+ return false;
3444
+ }
3445
+ return true;
3751
3446
  };
3752
-
3753
- // storage/drivers/memory.ts
3754
- var createMemoryDriver = (seed) => {
3447
+ var createMemoryUsageTrackingDriver = (seed = []) => {
3755
3448
  const store = /* @__PURE__ */ new Map();
3756
- if (seed) {
3757
- for (const [key, value] of Object.entries(seed)) {
3758
- store.set(key, value);
3759
- }
3449
+ for (const entry of seed) {
3450
+ store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl3(entry.baseUrl) });
3760
3451
  }
3761
3452
  return {
3762
- async getItem(key, defaultValue) {
3763
- const item = store.get(key);
3764
- if (item === void 0) return defaultValue;
3765
- try {
3766
- return JSON.parse(item);
3767
- } catch (parseError) {
3768
- if (typeof defaultValue === "string") {
3769
- return item;
3770
- }
3771
- throw parseError;
3772
- }
3453
+ async migrate() {
3454
+ return;
3773
3455
  },
3774
- async setItem(key, value) {
3775
- store.set(key, JSON.stringify(value));
3456
+ async append(entry) {
3457
+ store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl3(entry.baseUrl) });
3776
3458
  },
3777
- async removeItem(key) {
3778
- store.delete(key);
3779
- }
3780
- };
3781
- };
3782
-
3783
- // storage/drivers/sqlite.ts
3784
- var isBun = () => {
3785
- return typeof process.versions.bun !== "undefined";
3786
- };
3787
- var createDatabase = (dbPath) => {
3788
- if (isBun()) {
3789
- throw new Error(
3790
- "SQLite driver not supported in Bun. Use createMemoryDriver() instead."
3791
- );
3792
- }
3793
- let Database = null;
3794
- try {
3795
- Database = __require("better-sqlite3");
3796
- } catch (error) {
3797
- throw new Error(
3798
- `better-sqlite3 is required for sqlite storage. Install it to use sqlite storage. (${error})`
3799
- );
3800
- }
3801
- return new Database(dbPath);
3802
- };
3803
- var createSqliteDriver = (options = {}) => {
3804
- const dbPath = options.dbPath || "routstr.sqlite";
3805
- const tableName = options.tableName || "sdk_storage";
3806
- const db = createDatabase(dbPath);
3807
- db.exec(
3808
- `CREATE TABLE IF NOT EXISTS ${tableName} (key TEXT PRIMARY KEY, value TEXT NOT NULL)`
3809
- );
3810
- const selectStmt = db.prepare(`SELECT value FROM ${tableName} WHERE key = ?`);
3811
- const upsertStmt = db.prepare(
3812
- `INSERT INTO ${tableName} (key, value) VALUES (?, ?)
3813
- ON CONFLICT(key) DO UPDATE SET value = excluded.value`
3814
- );
3815
- const deleteStmt = db.prepare(`DELETE FROM ${tableName} WHERE key = ?`);
3816
- return {
3817
- async getItem(key, defaultValue) {
3818
- try {
3819
- const row = selectStmt.get(key);
3820
- if (!row || typeof row.value !== "string") return defaultValue;
3821
- try {
3822
- return JSON.parse(row.value);
3823
- } catch (parseError) {
3824
- if (typeof defaultValue === "string") {
3825
- return row.value;
3826
- }
3827
- throw parseError;
3828
- }
3829
- } catch (error) {
3830
- console.error(`SQLite getItem failed for key "${key}":`, error);
3831
- return defaultValue;
3459
+ async appendMany(entries) {
3460
+ for (const entry of entries) {
3461
+ store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl3(entry.baseUrl) });
3832
3462
  }
3833
3463
  },
3834
- async setItem(key, value) {
3835
- try {
3836
- upsertStmt.run(key, JSON.stringify(value));
3837
- } catch (error) {
3838
- console.error(`SQLite setItem failed for key "${key}":`, error);
3464
+ async list(options = {}) {
3465
+ const entries = [...store.values()].filter((entry) => matchesFilters2(entry, options)).sort((a, b) => b.timestamp - a.timestamp);
3466
+ if (typeof options.limit === "number") {
3467
+ return entries.slice(0, options.limit);
3839
3468
  }
3469
+ return entries;
3840
3470
  },
3841
- async removeItem(key) {
3842
- try {
3843
- deleteStmt.run(key);
3844
- } catch (error) {
3845
- console.error(`SQLite removeItem failed for key "${key}":`, error);
3471
+ async count(options = {}) {
3472
+ return (await this.list(options)).length;
3473
+ },
3474
+ async deleteOlderThan(timestamp) {
3475
+ let deleted = 0;
3476
+ for (const [id, entry] of store.entries()) {
3477
+ if (entry.timestamp < timestamp) {
3478
+ store.delete(id);
3479
+ deleted += 1;
3480
+ }
3846
3481
  }
3482
+ return deleted;
3483
+ },
3484
+ async clear() {
3485
+ store.clear();
3847
3486
  }
3848
3487
  };
3849
3488
  };
3850
-
3851
- // storage/drivers/indexedDB.ts
3852
- var isBrowser = typeof indexedDB !== "undefined";
3853
- var openDatabase = (dbName, storeName) => {
3854
- if (!isBrowser) {
3855
- return Promise.reject(new Error("IndexedDB is not available"));
3856
- }
3857
- return new Promise((resolve, reject) => {
3858
- const request = indexedDB.open(dbName, 1);
3859
- request.onupgradeneeded = () => {
3860
- const db = request.result;
3861
- if (!db.objectStoreNames.contains(storeName)) {
3862
- db.createObjectStore(storeName);
3863
- }
3864
- };
3865
- request.onsuccess = () => resolve(request.result);
3866
- request.onerror = () => reject(request.error);
3867
- });
3868
- };
3869
- var createIndexedDBDriver = (options = {}) => {
3870
- const dbName = options.dbName || "routstr-sdk";
3871
- const storeName = options.storeName || "sdk_storage";
3872
- let dbPromise = null;
3873
- const getDb = () => {
3874
- if (!dbPromise) {
3875
- dbPromise = openDatabase(dbName, storeName);
3876
- }
3877
- return dbPromise;
3878
- };
3879
- return {
3880
- async getItem(key, defaultValue) {
3881
- try {
3882
- const db = await getDb();
3883
- return new Promise((resolve, reject) => {
3884
- const tx = db.transaction(storeName, "readonly");
3885
- const store = tx.objectStore(storeName);
3886
- const request = store.get(key);
3887
- request.onsuccess = () => {
3888
- const raw = request.result;
3889
- if (raw === void 0) {
3890
- resolve(defaultValue);
3891
- return;
3892
- }
3893
- if (typeof raw === "string") {
3894
- try {
3895
- resolve(JSON.parse(raw));
3896
- } catch {
3897
- if (typeof defaultValue === "string") {
3898
- resolve(raw);
3899
- } else {
3900
- resolve(defaultValue);
3901
- }
3902
- }
3903
- } else {
3904
- resolve(raw);
3905
- }
3906
- };
3907
- request.onerror = () => reject(request.error);
3908
- });
3909
- } catch (error) {
3910
- console.error(`IndexedDB getItem failed for key "${key}":`, error);
3911
- return defaultValue;
3912
- }
3913
- },
3914
- async setItem(key, value) {
3915
- try {
3916
- const db = await getDb();
3917
- return new Promise((resolve, reject) => {
3918
- const tx = db.transaction(storeName, "readwrite");
3919
- const store = tx.objectStore(storeName);
3920
- store.put(JSON.stringify(value), key);
3921
- tx.oncomplete = () => resolve();
3922
- tx.onerror = () => reject(tx.error);
3923
- });
3924
- } catch (error) {
3925
- console.error(`IndexedDB setItem failed for key "${key}":`, error);
3926
- }
3927
- },
3928
- async removeItem(key) {
3929
- try {
3930
- const db = await getDb();
3931
- return new Promise((resolve, reject) => {
3932
- const tx = db.transaction(storeName, "readwrite");
3933
- const store = tx.objectStore(storeName);
3934
- store.delete(key);
3935
- tx.oncomplete = () => resolve();
3936
- tx.onerror = () => reject(tx.error);
3937
- });
3938
- } catch (error) {
3939
- console.error(`IndexedDB removeItem failed for key "${key}":`, error);
3940
- }
3941
- }
3942
- };
3943
- };
3944
-
3945
- // storage/keys.ts
3946
- var SDK_STORAGE_KEYS = {
3947
- MODELS_FROM_ALL_PROVIDERS: "modelsFromAllProviders",
3948
- LAST_USED_MODEL: "lastUsedModel",
3949
- BASE_URLS_LIST: "base_urls_list",
3950
- DISABLED_PROVIDERS: "disabled_providers",
3951
- MINTS_FROM_ALL_PROVIDERS: "mints_from_all_providers",
3952
- INFO_FROM_ALL_PROVIDERS: "info_from_all_providers",
3953
- LAST_MODELS_UPDATE: "lastModelsUpdate",
3954
- LAST_BASE_URLS_UPDATE: "lastBaseUrlsUpdate",
3955
- LOCAL_CASHU_TOKENS: "local_cashu_tokens",
3956
- API_KEYS: "api_keys",
3957
- CHILD_KEYS: "child_keys",
3958
- ROUTSTR21_MODELS: "routstr21Models",
3959
- LAST_ROUTSTR21_MODELS_UPDATE: "lastRoutstr21ModelsUpdate",
3960
- CACHED_RECEIVE_TOKENS: "cached_receive_tokens",
3961
- USAGE_TRACKING: "usage_tracking",
3962
- CLIENT_IDS: "client_ids"
3963
- };
3964
-
3965
- // storage/store.ts
3966
- var normalizeBaseUrl = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
3967
- var getCashuTokenBalance = (token) => {
3968
- try {
3969
- const decoded = cashuTs.getDecodedToken(token);
3970
- const unitDivisor = decoded.unit === "msat" ? 1e3 : 1;
3971
- let sum = 0;
3972
- for (const proof of decoded.proofs) {
3973
- sum += proof.amount / unitDivisor;
3974
- }
3975
- return sum;
3976
- } catch {
3977
- return 0;
3489
+ var normalizeBaseUrl4 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
3490
+ var getCashuTokenBalance = (token) => {
3491
+ try {
3492
+ const decoded = cashuTs.getDecodedToken(token);
3493
+ const unitDivisor = decoded.unit === "msat" ? 1e3 : 1;
3494
+ let sum = 0;
3495
+ for (const proof of decoded.proofs) {
3496
+ sum += proof.amount / unitDivisor;
3497
+ }
3498
+ return sum;
3499
+ } catch {
3500
+ return 0;
3978
3501
  }
3979
3502
  };
3980
3503
  var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
@@ -3992,12 +3515,11 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3992
3515
  routstr21Models: [],
3993
3516
  lastRoutstr21ModelsUpdate: null,
3994
3517
  cachedReceiveTokens: [],
3995
- usageTracking: [],
3996
3518
  clientIds: [],
3997
3519
  setModelsFromAllProviders: (value) => {
3998
3520
  const normalized = {};
3999
3521
  for (const [baseUrl, models] of Object.entries(value)) {
4000
- normalized[normalizeBaseUrl(baseUrl)] = models;
3522
+ normalized[normalizeBaseUrl4(baseUrl)] = models;
4001
3523
  }
4002
3524
  void driver.setItem(
4003
3525
  SDK_STORAGE_KEYS.MODELS_FROM_ALL_PROVIDERS,
@@ -4010,7 +3532,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
4010
3532
  set({ lastUsedModel: value });
4011
3533
  },
4012
3534
  setBaseUrlsList: (value) => {
4013
- const normalized = value.map((url) => normalizeBaseUrl(url));
3535
+ const normalized = value.map((url) => normalizeBaseUrl4(url));
4014
3536
  void driver.setItem(SDK_STORAGE_KEYS.BASE_URLS_LIST, normalized);
4015
3537
  set({ baseUrlsList: normalized });
4016
3538
  },
@@ -4019,14 +3541,14 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
4019
3541
  set({ lastBaseUrlsUpdate: value });
4020
3542
  },
4021
3543
  setDisabledProviders: (value) => {
4022
- const normalized = value.map((url) => normalizeBaseUrl(url));
3544
+ const normalized = value.map((url) => normalizeBaseUrl4(url));
4023
3545
  void driver.setItem(SDK_STORAGE_KEYS.DISABLED_PROVIDERS, normalized);
4024
3546
  set({ disabledProviders: normalized });
4025
3547
  },
4026
3548
  setMintsFromAllProviders: (value) => {
4027
3549
  const normalized = {};
4028
3550
  for (const [baseUrl, mints] of Object.entries(value)) {
4029
- normalized[normalizeBaseUrl(baseUrl)] = mints.map(
3551
+ normalized[normalizeBaseUrl4(baseUrl)] = mints.map(
4030
3552
  (mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint
4031
3553
  );
4032
3554
  }
@@ -4039,7 +3561,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
4039
3561
  setInfoFromAllProviders: (value) => {
4040
3562
  const normalized = {};
4041
3563
  for (const [baseUrl, info] of Object.entries(value)) {
4042
- normalized[normalizeBaseUrl(baseUrl)] = info;
3564
+ normalized[normalizeBaseUrl4(baseUrl)] = info;
4043
3565
  }
4044
3566
  void driver.setItem(SDK_STORAGE_KEYS.INFO_FROM_ALL_PROVIDERS, normalized);
4045
3567
  set({ infoFromAllProviders: normalized });
@@ -4047,7 +3569,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
4047
3569
  setLastModelsUpdate: (value) => {
4048
3570
  const normalized = {};
4049
3571
  for (const [baseUrl, timestamp] of Object.entries(value)) {
4050
- normalized[normalizeBaseUrl(baseUrl)] = timestamp;
3572
+ normalized[normalizeBaseUrl4(baseUrl)] = timestamp;
4051
3573
  }
4052
3574
  void driver.setItem(SDK_STORAGE_KEYS.LAST_MODELS_UPDATE, normalized);
4053
3575
  set({ lastModelsUpdate: normalized });
@@ -4057,7 +3579,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
4057
3579
  const updates = typeof value === "function" ? value(state.cachedTokens) : value;
4058
3580
  const normalized = updates.map((entry) => ({
4059
3581
  ...entry,
4060
- baseUrl: normalizeBaseUrl(entry.baseUrl),
3582
+ baseUrl: normalizeBaseUrl4(entry.baseUrl),
4061
3583
  balance: typeof entry.balance === "number" ? entry.balance : getCashuTokenBalance(entry.token),
4062
3584
  lastUsed: entry.lastUsed ?? null
4063
3585
  }));
@@ -4070,7 +3592,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
4070
3592
  const updates = typeof value === "function" ? value(state.apiKeys) : value;
4071
3593
  const normalized = updates.map((entry) => ({
4072
3594
  ...entry,
4073
- baseUrl: normalizeBaseUrl(entry.baseUrl),
3595
+ baseUrl: normalizeBaseUrl4(entry.baseUrl),
4074
3596
  balance: entry.balance ?? 0,
4075
3597
  lastUsed: entry.lastUsed ?? null
4076
3598
  }));
@@ -4082,7 +3604,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
4082
3604
  set((state) => {
4083
3605
  const updates = typeof value === "function" ? value(state.childKeys) : value;
4084
3606
  const normalized = updates.map((entry) => ({
4085
- parentBaseUrl: normalizeBaseUrl(entry.parentBaseUrl),
3607
+ parentBaseUrl: normalizeBaseUrl4(entry.parentBaseUrl),
4086
3608
  childKey: entry.childKey,
4087
3609
  balance: entry.balance ?? 0,
4088
3610
  balanceLimit: entry.balanceLimit,
@@ -4111,10 +3633,6 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
4111
3633
  void driver.setItem(SDK_STORAGE_KEYS.CACHED_RECEIVE_TOKENS, normalized);
4112
3634
  set({ cachedReceiveTokens: normalized });
4113
3635
  },
4114
- setUsageTracking: (value) => {
4115
- void driver.setItem(SDK_STORAGE_KEYS.USAGE_TRACKING, value);
4116
- set({ usageTracking: value });
4117
- },
4118
3636
  setClientIds: (value) => {
4119
3637
  set((state) => {
4120
3638
  const updates = typeof value === "function" ? value(state.clientIds) : value;
@@ -4144,7 +3662,6 @@ var hydrateStoreFromDriver = async (store, driver) => {
4144
3662
  rawRoutstr21Models,
4145
3663
  rawLastRoutstr21ModelsUpdate,
4146
3664
  rawCachedReceiveTokens,
4147
- rawUsageTracking,
4148
3665
  rawClientIds
4149
3666
  ] = await Promise.all([
4150
3667
  driver.getItem(
@@ -4176,51 +3693,50 @@ var hydrateStoreFromDriver = async (store, driver) => {
4176
3693
  null
4177
3694
  ),
4178
3695
  driver.getItem(SDK_STORAGE_KEYS.CACHED_RECEIVE_TOKENS, []),
4179
- driver.getItem(SDK_STORAGE_KEYS.USAGE_TRACKING, []),
4180
3696
  driver.getItem(SDK_STORAGE_KEYS.CLIENT_IDS, [])
4181
3697
  ]);
4182
3698
  const modelsFromAllProviders = Object.fromEntries(
4183
3699
  Object.entries(rawModels).map(([baseUrl, models]) => [
4184
- normalizeBaseUrl(baseUrl),
3700
+ normalizeBaseUrl4(baseUrl),
4185
3701
  models
4186
3702
  ])
4187
3703
  );
4188
- const baseUrlsList = rawBaseUrls.map((url) => normalizeBaseUrl(url));
3704
+ const baseUrlsList = rawBaseUrls.map((url) => normalizeBaseUrl4(url));
4189
3705
  const disabledProviders = rawDisabledProviders.map(
4190
- (url) => normalizeBaseUrl(url)
3706
+ (url) => normalizeBaseUrl4(url)
4191
3707
  );
4192
3708
  const mintsFromAllProviders = Object.fromEntries(
4193
3709
  Object.entries(rawMints).map(([baseUrl, mints]) => [
4194
- normalizeBaseUrl(baseUrl),
3710
+ normalizeBaseUrl4(baseUrl),
4195
3711
  mints.map((mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint)
4196
3712
  ])
4197
3713
  );
4198
3714
  const infoFromAllProviders = Object.fromEntries(
4199
3715
  Object.entries(rawInfo).map(([baseUrl, info]) => [
4200
- normalizeBaseUrl(baseUrl),
3716
+ normalizeBaseUrl4(baseUrl),
4201
3717
  info
4202
3718
  ])
4203
3719
  );
4204
3720
  const lastModelsUpdate = Object.fromEntries(
4205
3721
  Object.entries(rawLastModelsUpdate).map(([baseUrl, timestamp]) => [
4206
- normalizeBaseUrl(baseUrl),
3722
+ normalizeBaseUrl4(baseUrl),
4207
3723
  timestamp
4208
3724
  ])
4209
3725
  );
4210
3726
  const cachedTokens = rawCachedTokens.map((entry) => ({
4211
3727
  ...entry,
4212
- baseUrl: normalizeBaseUrl(entry.baseUrl),
3728
+ baseUrl: normalizeBaseUrl4(entry.baseUrl),
4213
3729
  balance: typeof entry.balance === "number" ? entry.balance : getCashuTokenBalance(entry.token),
4214
3730
  lastUsed: entry.lastUsed ?? null
4215
3731
  }));
4216
3732
  const apiKeys = rawApiKeys.map((entry) => ({
4217
3733
  ...entry,
4218
- baseUrl: normalizeBaseUrl(entry.baseUrl),
3734
+ baseUrl: normalizeBaseUrl4(entry.baseUrl),
4219
3735
  balance: entry.balance ?? 0,
4220
3736
  lastUsed: entry.lastUsed ?? null
4221
3737
  }));
4222
3738
  const childKeys = rawChildKeys.map((entry) => ({
4223
- parentBaseUrl: normalizeBaseUrl(entry.parentBaseUrl),
3739
+ parentBaseUrl: normalizeBaseUrl4(entry.parentBaseUrl),
4224
3740
  childKey: entry.childKey,
4225
3741
  balance: entry.balance ?? 0,
4226
3742
  balanceLimit: entry.balanceLimit,
@@ -4235,7 +3751,6 @@ var hydrateStoreFromDriver = async (store, driver) => {
4235
3751
  unit: entry.unit || "sat",
4236
3752
  createdAt: entry.createdAt ?? Date.now()
4237
3753
  }));
4238
- const usageTracking = rawUsageTracking;
4239
3754
  const clientIds = rawClientIds.map((entry) => ({
4240
3755
  ...entry,
4241
3756
  createdAt: entry.createdAt ?? Date.now(),
@@ -4256,7 +3771,6 @@ var hydrateStoreFromDriver = async (store, driver) => {
4256
3771
  routstr21Models,
4257
3772
  lastRoutstr21ModelsUpdate,
4258
3773
  cachedReceiveTokens,
4259
- usageTracking,
4260
3774
  clientIds
4261
3775
  });
4262
3776
  };
@@ -4277,12 +3791,12 @@ var createDiscoveryAdapterFromStore = (store) => ({
4277
3791
  getCachedProviderInfo: () => store.getState().infoFromAllProviders,
4278
3792
  setCachedProviderInfo: (info) => store.getState().setInfoFromAllProviders(info),
4279
3793
  getProviderLastUpdate: (baseUrl) => {
4280
- const normalized = normalizeBaseUrl(baseUrl);
3794
+ const normalized = normalizeBaseUrl4(baseUrl);
4281
3795
  const timestamps = store.getState().lastModelsUpdate;
4282
3796
  return timestamps[normalized] || null;
4283
3797
  },
4284
3798
  setProviderLastUpdate: (baseUrl, timestamp) => {
4285
- const normalized = normalizeBaseUrl(baseUrl);
3799
+ const normalized = normalizeBaseUrl4(baseUrl);
4286
3800
  const timestamps = { ...store.getState().lastModelsUpdate };
4287
3801
  timestamps[normalized] = timestamp;
4288
3802
  store.getState().setLastModelsUpdate(timestamps);
@@ -4301,7 +3815,7 @@ var createDiscoveryAdapterFromStore = (store) => ({
4301
3815
  });
4302
3816
  var createStorageAdapterFromStore = (store) => ({
4303
3817
  getToken: (baseUrl) => {
4304
- const normalized = normalizeBaseUrl(baseUrl);
3818
+ const normalized = normalizeBaseUrl4(baseUrl);
4305
3819
  const entry = store.getState().cachedTokens.find((token) => token.baseUrl === normalized);
4306
3820
  if (!entry) return null;
4307
3821
  const next = store.getState().cachedTokens.map(
@@ -4311,7 +3825,7 @@ var createStorageAdapterFromStore = (store) => ({
4311
3825
  return entry.token;
4312
3826
  },
4313
3827
  setToken: (baseUrl, token) => {
4314
- const normalized = normalizeBaseUrl(baseUrl);
3828
+ const normalized = normalizeBaseUrl4(baseUrl);
4315
3829
  const tokens = store.getState().cachedTokens;
4316
3830
  const balance = getCashuTokenBalance(token);
4317
3831
  const existingIndex = tokens.findIndex(
@@ -4320,266 +3834,1532 @@ var createStorageAdapterFromStore = (store) => ({
4320
3834
  if (existingIndex !== -1) {
4321
3835
  throw new Error(`Token already exists for baseUrl: ${normalized}`);
4322
3836
  }
4323
- const next = [...tokens];
4324
- next.push({
4325
- baseUrl: normalized,
3837
+ const next = [...tokens];
3838
+ next.push({
3839
+ baseUrl: normalized,
3840
+ token,
3841
+ balance,
3842
+ lastUsed: Date.now()
3843
+ });
3844
+ store.getState().setCachedTokens(next);
3845
+ },
3846
+ removeToken: (baseUrl) => {
3847
+ const normalized = normalizeBaseUrl4(baseUrl);
3848
+ const next = store.getState().cachedTokens.filter((entry) => entry.baseUrl !== normalized);
3849
+ store.getState().setCachedTokens(next);
3850
+ },
3851
+ updateTokenBalance: (baseUrl, balance) => {
3852
+ const normalized = normalizeBaseUrl4(baseUrl);
3853
+ const tokens = store.getState().cachedTokens;
3854
+ const next = tokens.map(
3855
+ (entry) => entry.baseUrl === normalized ? { ...entry, balance } : entry
3856
+ );
3857
+ store.getState().setCachedTokens(next);
3858
+ },
3859
+ getCachedTokenDistribution: () => {
3860
+ const cachedTokens = store.getState().cachedTokens;
3861
+ const distributionMap = {};
3862
+ for (const entry of cachedTokens) {
3863
+ const sum = entry.balance || 0;
3864
+ if (sum > 0) {
3865
+ distributionMap[entry.baseUrl] = (distributionMap[entry.baseUrl] || 0) + sum;
3866
+ }
3867
+ }
3868
+ return Object.entries(distributionMap).map(([baseUrl, amt]) => ({ baseUrl, amount: amt })).sort((a, b) => b.amount - a.amount);
3869
+ },
3870
+ getApiKeyDistribution: () => {
3871
+ const apiKeys = store.getState().apiKeys;
3872
+ const distributionMap = {};
3873
+ for (const entry of apiKeys) {
3874
+ const sum = entry.balance || 0;
3875
+ if (sum > 0) {
3876
+ distributionMap[entry.baseUrl] = (distributionMap[entry.baseUrl] || 0) + sum;
3877
+ }
3878
+ }
3879
+ return Object.entries(distributionMap).map(([baseUrl, amt]) => ({ baseUrl, amount: amt })).sort((a, b) => b.amount - a.amount);
3880
+ },
3881
+ saveProviderInfo: (baseUrl, info) => {
3882
+ const normalized = normalizeBaseUrl4(baseUrl);
3883
+ const next = { ...store.getState().infoFromAllProviders };
3884
+ next[normalized] = info;
3885
+ store.getState().setInfoFromAllProviders(next);
3886
+ },
3887
+ getProviderInfo: (baseUrl) => {
3888
+ const normalized = normalizeBaseUrl4(baseUrl);
3889
+ return store.getState().infoFromAllProviders[normalized] || null;
3890
+ },
3891
+ // ========== API Keys (for apikeys mode) ==========
3892
+ getApiKey: (baseUrl) => {
3893
+ const normalized = normalizeBaseUrl4(baseUrl);
3894
+ const entry = store.getState().apiKeys.find((key) => key.baseUrl === normalized);
3895
+ if (!entry) return null;
3896
+ const next = store.getState().apiKeys.map(
3897
+ (key) => key.baseUrl === normalized ? { ...key, lastUsed: Date.now() } : key
3898
+ );
3899
+ store.getState().setApiKeys(next);
3900
+ return entry;
3901
+ },
3902
+ setApiKey: (baseUrl, key) => {
3903
+ const normalized = normalizeBaseUrl4(baseUrl);
3904
+ const keys = store.getState().apiKeys;
3905
+ const existingIndex = keys.findIndex(
3906
+ (entry) => entry.baseUrl === normalized
3907
+ );
3908
+ if (existingIndex !== -1) {
3909
+ throw new Error(`ApiKey already exists for baseUrl: ${normalized}`);
3910
+ }
3911
+ const next = [...keys];
3912
+ next.push({
3913
+ baseUrl: normalized,
3914
+ key,
3915
+ balance: 0,
3916
+ lastUsed: Date.now()
3917
+ });
3918
+ store.getState().setApiKeys(next);
3919
+ },
3920
+ updateApiKeyBalance: (baseUrl, balance) => {
3921
+ const normalized = normalizeBaseUrl4(baseUrl);
3922
+ const keys = store.getState().apiKeys;
3923
+ const next = keys.map(
3924
+ (entry) => entry.baseUrl === normalized ? { ...entry, balance } : entry
3925
+ );
3926
+ store.getState().setApiKeys(next);
3927
+ },
3928
+ removeApiKey: (baseUrl) => {
3929
+ const normalized = normalizeBaseUrl4(baseUrl);
3930
+ const next = store.getState().apiKeys.filter((entry) => entry.baseUrl !== normalized);
3931
+ store.getState().setApiKeys(next);
3932
+ },
3933
+ getAllApiKeys: () => {
3934
+ return store.getState().apiKeys.map((entry) => ({
3935
+ baseUrl: entry.baseUrl,
3936
+ key: entry.key,
3937
+ balance: entry.balance,
3938
+ lastUsed: entry.lastUsed
3939
+ }));
3940
+ },
3941
+ // ========== Child Keys ==========
3942
+ getChildKey: (parentBaseUrl) => {
3943
+ const normalized = normalizeBaseUrl4(parentBaseUrl);
3944
+ const entry = store.getState().childKeys.find((key) => key.parentBaseUrl === normalized);
3945
+ if (!entry) return null;
3946
+ return {
3947
+ parentBaseUrl: entry.parentBaseUrl,
3948
+ childKey: entry.childKey,
3949
+ balance: entry.balance,
3950
+ balanceLimit: entry.balanceLimit,
3951
+ validityDate: entry.validityDate,
3952
+ createdAt: entry.createdAt
3953
+ };
3954
+ },
3955
+ setChildKey: (parentBaseUrl, childKey, balance, validityDate, balanceLimit) => {
3956
+ const normalized = normalizeBaseUrl4(parentBaseUrl);
3957
+ const keys = store.getState().childKeys;
3958
+ const existingIndex = keys.findIndex(
3959
+ (entry) => entry.parentBaseUrl === normalized
3960
+ );
3961
+ if (existingIndex !== -1) {
3962
+ const next = keys.map(
3963
+ (entry) => entry.parentBaseUrl === normalized ? {
3964
+ ...entry,
3965
+ childKey,
3966
+ balance: balance ?? 0,
3967
+ validityDate,
3968
+ balanceLimit,
3969
+ createdAt: Date.now()
3970
+ } : entry
3971
+ );
3972
+ store.getState().setChildKeys(next);
3973
+ } else {
3974
+ const next = [...keys];
3975
+ next.push({
3976
+ parentBaseUrl: normalized,
3977
+ childKey,
3978
+ balance: balance ?? 0,
3979
+ validityDate,
3980
+ balanceLimit,
3981
+ createdAt: Date.now()
3982
+ });
3983
+ store.getState().setChildKeys(next);
3984
+ }
3985
+ },
3986
+ updateChildKeyBalance: (parentBaseUrl, balance) => {
3987
+ const normalized = normalizeBaseUrl4(parentBaseUrl);
3988
+ const keys = store.getState().childKeys;
3989
+ const next = keys.map(
3990
+ (entry) => entry.parentBaseUrl === normalized ? { ...entry, balance } : entry
3991
+ );
3992
+ store.getState().setChildKeys(next);
3993
+ },
3994
+ removeChildKey: (parentBaseUrl) => {
3995
+ const normalized = normalizeBaseUrl4(parentBaseUrl);
3996
+ const next = store.getState().childKeys.filter((entry) => entry.parentBaseUrl !== normalized);
3997
+ store.getState().setChildKeys(next);
3998
+ },
3999
+ getAllChildKeys: () => {
4000
+ return store.getState().childKeys.map((entry) => ({
4001
+ parentBaseUrl: entry.parentBaseUrl,
4002
+ childKey: entry.childKey,
4003
+ balance: entry.balance,
4004
+ balanceLimit: entry.balanceLimit,
4005
+ validityDate: entry.validityDate,
4006
+ createdAt: entry.createdAt
4007
+ }));
4008
+ },
4009
+ getCachedReceiveTokens: () => {
4010
+ return store.getState().cachedReceiveTokens;
4011
+ },
4012
+ setCachedReceiveTokens: (tokens) => {
4013
+ store.getState().setCachedReceiveTokens(tokens);
4014
+ }
4015
+ });
4016
+ var createProviderRegistryFromStore = (store) => ({
4017
+ getModelsForProvider: (baseUrl) => {
4018
+ const normalized = normalizeBaseUrl4(baseUrl);
4019
+ return store.getState().modelsFromAllProviders[normalized] || [];
4020
+ },
4021
+ getDisabledProviders: () => store.getState().disabledProviders,
4022
+ getProviderMints: (baseUrl) => {
4023
+ const normalized = normalizeBaseUrl4(baseUrl);
4024
+ return store.getState().mintsFromAllProviders[normalized] || [];
4025
+ },
4026
+ getProviderInfo: async (baseUrl) => {
4027
+ const normalized = normalizeBaseUrl4(baseUrl);
4028
+ const cached = store.getState().infoFromAllProviders[normalized];
4029
+ if (cached) return cached;
4030
+ try {
4031
+ const response = await fetch(`${normalized}v1/info`);
4032
+ if (!response.ok) {
4033
+ throw new Error(`Failed ${response.status}`);
4034
+ }
4035
+ const info = await response.json();
4036
+ const next = { ...store.getState().infoFromAllProviders };
4037
+ next[normalized] = info;
4038
+ store.getState().setInfoFromAllProviders(next);
4039
+ return info;
4040
+ } catch (error) {
4041
+ console.warn(`Failed to fetch provider info from ${normalized}:`, error);
4042
+ return null;
4043
+ }
4044
+ },
4045
+ getAllProvidersModels: () => store.getState().modelsFromAllProviders
4046
+ });
4047
+
4048
+ // storage/index.ts
4049
+ var isBrowser3 = () => {
4050
+ try {
4051
+ return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
4052
+ } catch {
4053
+ return false;
4054
+ }
4055
+ };
4056
+ var isNode = () => {
4057
+ try {
4058
+ return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
4059
+ } catch {
4060
+ return false;
4061
+ }
4062
+ };
4063
+ var defaultDriver = null;
4064
+ var isBun3 = () => {
4065
+ return typeof process.versions.bun !== "undefined";
4066
+ };
4067
+ var getDefaultSdkDriver = () => {
4068
+ if (defaultDriver) return defaultDriver;
4069
+ if (isBrowser3()) {
4070
+ defaultDriver = localStorageDriver;
4071
+ return defaultDriver;
4072
+ }
4073
+ if (isBun3()) {
4074
+ defaultDriver = createMemoryDriver();
4075
+ return defaultDriver;
4076
+ }
4077
+ if (isNode()) {
4078
+ defaultDriver = createSqliteDriver();
4079
+ return defaultDriver;
4080
+ }
4081
+ defaultDriver = createMemoryDriver();
4082
+ return defaultDriver;
4083
+ };
4084
+ var defaultStore = null;
4085
+ var defaultUsageTrackingDriver = null;
4086
+ var getDefaultSdkStore = () => {
4087
+ if (!defaultStore) {
4088
+ defaultStore = createSdkStore({ driver: getDefaultSdkDriver() });
4089
+ }
4090
+ return defaultStore.hydrate.then(() => defaultStore.store);
4091
+ };
4092
+ var getDefaultUsageTrackingDriver = () => {
4093
+ if (defaultUsageTrackingDriver) return defaultUsageTrackingDriver;
4094
+ const storageDriver = getDefaultSdkDriver();
4095
+ if (isBrowser3()) {
4096
+ defaultUsageTrackingDriver = createIndexedDBUsageTrackingDriver({
4097
+ legacyStorageDriver: storageDriver
4098
+ });
4099
+ return defaultUsageTrackingDriver;
4100
+ }
4101
+ if (isBun3()) {
4102
+ defaultUsageTrackingDriver = createMemoryUsageTrackingDriver();
4103
+ return defaultUsageTrackingDriver;
4104
+ }
4105
+ if (isNode()) {
4106
+ defaultUsageTrackingDriver = createSqliteUsageTrackingDriver({
4107
+ legacyStorageDriver: storageDriver
4108
+ });
4109
+ return defaultUsageTrackingDriver;
4110
+ }
4111
+ defaultUsageTrackingDriver = createMemoryUsageTrackingDriver();
4112
+ return defaultUsageTrackingDriver;
4113
+ };
4114
+ var getDefaultDiscoveryAdapter = async () => createDiscoveryAdapterFromStore(await getDefaultSdkStore());
4115
+ var getDefaultStorageAdapter = async () => createStorageAdapterFromStore(await getDefaultSdkStore());
4116
+ var getDefaultProviderRegistry = async () => createProviderRegistryFromStore(await getDefaultSdkStore());
4117
+ function createSSEParserTransform(onUsage, onResponseId) {
4118
+ let buffer = "";
4119
+ const maybeCaptureUsageFromJson = (jsonText) => {
4120
+ try {
4121
+ const data = JSON.parse(jsonText);
4122
+ const responseId = data.id;
4123
+ if (typeof responseId === "string" && responseId.trim().length > 0) {
4124
+ onResponseId?.(responseId.trim());
4125
+ }
4126
+ const usage = extractUsageFromSSEJson(data);
4127
+ if (usage) {
4128
+ onUsage(usage);
4129
+ }
4130
+ } catch {
4131
+ }
4132
+ };
4133
+ const processLine = (self, line) => {
4134
+ const trimmed = line.trim();
4135
+ if (!trimmed) {
4136
+ return;
4137
+ }
4138
+ if (trimmed === "data: [DONE]" || trimmed === "[DONE]") {
4139
+ self.push("data: [DONE]\n\n");
4140
+ return;
4141
+ }
4142
+ if (trimmed.startsWith("data:")) {
4143
+ const dataStr = trimmed.startsWith("data: ") ? trimmed.slice(6) : trimmed.slice(5).trimStart();
4144
+ if (dataStr === "[DONE]") {
4145
+ self.push("data: [DONE]\n\n");
4146
+ return;
4147
+ }
4148
+ maybeCaptureUsageFromJson(dataStr);
4149
+ self.push(`data: ${dataStr}
4150
+
4151
+ `);
4152
+ return;
4153
+ }
4154
+ if (trimmed.startsWith("{")) {
4155
+ maybeCaptureUsageFromJson(trimmed);
4156
+ self.push(`data: ${trimmed}
4157
+
4158
+ `);
4159
+ return;
4160
+ }
4161
+ self.push(line + "\n");
4162
+ };
4163
+ return new stream.Transform({
4164
+ transform(chunk, encoding, callback) {
4165
+ buffer += chunk.toString();
4166
+ const lines = buffer.split(/\r?\n/);
4167
+ buffer = lines.pop() || "";
4168
+ for (const line of lines) {
4169
+ processLine(this, line);
4170
+ }
4171
+ callback();
4172
+ },
4173
+ flush(callback) {
4174
+ if (buffer.trim()) {
4175
+ processLine(this, buffer);
4176
+ }
4177
+ buffer = "";
4178
+ callback();
4179
+ }
4180
+ });
4181
+ }
4182
+ var TOPUP_MARGIN = 1.2;
4183
+ var RoutstrClient = class {
4184
+ constructor(walletAdapter, storageAdapter, providerRegistry, alertLevel, mode = "xcashu") {
4185
+ this.walletAdapter = walletAdapter;
4186
+ this.storageAdapter = storageAdapter;
4187
+ this.providerRegistry = providerRegistry;
4188
+ this.balanceManager = new BalanceManager(
4189
+ walletAdapter,
4190
+ storageAdapter,
4191
+ providerRegistry
4192
+ );
4193
+ this.cashuSpender = new CashuSpender(
4194
+ walletAdapter,
4195
+ storageAdapter,
4196
+ providerRegistry,
4197
+ this.balanceManager
4198
+ );
4199
+ this.streamProcessor = new StreamProcessor();
4200
+ this.providerManager = new ProviderManager(providerRegistry);
4201
+ this.alertLevel = alertLevel;
4202
+ if (mode === "lazyrefund") {
4203
+ this.mode = "apikeys";
4204
+ } else if (mode === "apikeys") {
4205
+ this.mode = "lazyrefund";
4206
+ } else {
4207
+ this.mode = mode;
4208
+ }
4209
+ }
4210
+ cashuSpender;
4211
+ balanceManager;
4212
+ streamProcessor;
4213
+ providerManager;
4214
+ alertLevel;
4215
+ mode;
4216
+ debugLevel = "WARN";
4217
+ /**
4218
+ * Get the current client mode
4219
+ */
4220
+ getMode() {
4221
+ return this.mode;
4222
+ }
4223
+ getDebugLevel() {
4224
+ return this.debugLevel;
4225
+ }
4226
+ setDebugLevel(level) {
4227
+ this.debugLevel = level;
4228
+ }
4229
+ _log(level, ...args) {
4230
+ const levelPriority = {
4231
+ DEBUG: 0,
4232
+ WARN: 1,
4233
+ ERROR: 2
4234
+ };
4235
+ if (levelPriority[level] >= levelPriority[this.debugLevel]) {
4236
+ switch (level) {
4237
+ case "DEBUG":
4238
+ console.log(...args);
4239
+ break;
4240
+ case "WARN":
4241
+ console.warn(...args);
4242
+ break;
4243
+ case "ERROR":
4244
+ console.error(...args);
4245
+ break;
4246
+ }
4247
+ }
4248
+ }
4249
+ /**
4250
+ * Get the CashuSpender instance
4251
+ */
4252
+ getCashuSpender() {
4253
+ return this.cashuSpender;
4254
+ }
4255
+ /**
4256
+ * Get the BalanceManager instance
4257
+ */
4258
+ getBalanceManager() {
4259
+ return this.balanceManager;
4260
+ }
4261
+ /**
4262
+ * Get the ProviderManager instance
4263
+ */
4264
+ getProviderManager() {
4265
+ return this.providerManager;
4266
+ }
4267
+ /**
4268
+ * Check if the client is currently busy (in critical section)
4269
+ */
4270
+ get isBusy() {
4271
+ return this.cashuSpender.isBusy;
4272
+ }
4273
+ /**
4274
+ * Route an API request to the upstream provider
4275
+ *
4276
+ * This is a simpler alternative to fetchAIResponse that just proxies
4277
+ * the request upstream without the streaming callback machinery.
4278
+ * Useful for daemon-style routing where you just need to forward
4279
+ * requests and get responses back.
4280
+ */
4281
+ async routeRequest(params) {
4282
+ const prepared = await this._prepareRoutedRequest(params);
4283
+ const satsSpent = await this._handlePostResponseBalanceUpdate({
4284
+ token: prepared.tokenUsed,
4285
+ baseUrl: prepared.baseUrlUsed,
4286
+ initialTokenBalance: prepared.tokenBalanceInSats,
4287
+ response: prepared.response,
4288
+ modelId: prepared.modelId,
4289
+ usage: prepared.capturedUsage,
4290
+ requestId: prepared.capturedResponseId
4291
+ });
4292
+ prepared.response.satsSpent = satsSpent;
4293
+ prepared.response.usage = prepared.capturedUsage;
4294
+ prepared.response.requestId = prepared.capturedResponseId;
4295
+ return prepared.response;
4296
+ }
4297
+ async routeRequestToNodeResponse(params) {
4298
+ const { res } = params;
4299
+ const prepared = await this._prepareRoutedRequest(params);
4300
+ res.statusCode = prepared.response.status;
4301
+ prepared.response.headers.forEach((value, key) => {
4302
+ res.setHeader(key, value);
4303
+ });
4304
+ const body = prepared.response.body;
4305
+ if (!body) {
4306
+ const satsSpent = await this._handlePostResponseBalanceUpdate({
4307
+ token: prepared.tokenUsed,
4308
+ baseUrl: prepared.baseUrlUsed,
4309
+ initialTokenBalance: prepared.tokenBalanceInSats,
4310
+ response: prepared.response,
4311
+ modelId: prepared.modelId,
4312
+ usage: prepared.capturedUsage,
4313
+ requestId: prepared.capturedResponseId
4314
+ });
4315
+ prepared.response.satsSpent = satsSpent;
4316
+ res.end();
4317
+ return;
4318
+ }
4319
+ const nodeReadable = stream.Readable.fromWeb(body);
4320
+ await new Promise((resolve, reject) => {
4321
+ let settled = false;
4322
+ const finish = async () => {
4323
+ if (settled) return;
4324
+ settled = true;
4325
+ try {
4326
+ const satsSpent = await this._handlePostResponseBalanceUpdate({
4327
+ token: prepared.tokenUsed,
4328
+ baseUrl: prepared.baseUrlUsed,
4329
+ initialTokenBalance: prepared.tokenBalanceInSats,
4330
+ response: prepared.response,
4331
+ modelId: prepared.modelId,
4332
+ usage: prepared.capturedUsage,
4333
+ requestId: prepared.capturedResponseId
4334
+ });
4335
+ prepared.response.satsSpent = satsSpent;
4336
+ prepared.response.usage = prepared.capturedUsage;
4337
+ prepared.response.requestId = prepared.capturedResponseId;
4338
+ resolve();
4339
+ } catch (error) {
4340
+ reject(error);
4341
+ }
4342
+ };
4343
+ const fail = (error) => {
4344
+ if (settled) return;
4345
+ settled = true;
4346
+ reject(error);
4347
+ };
4348
+ res.once("finish", finish);
4349
+ res.once("close", finish);
4350
+ res.once("error", fail);
4351
+ nodeReadable.once("error", fail);
4352
+ nodeReadable.pipe(res);
4353
+ });
4354
+ }
4355
+ async _prepareRoutedRequest(params) {
4356
+ const {
4357
+ path,
4358
+ method,
4359
+ body,
4360
+ headers = {},
4361
+ baseUrl,
4362
+ mintUrl,
4363
+ modelId
4364
+ } = params;
4365
+ await this._checkBalance();
4366
+ let requiredSats = 1;
4367
+ let selectedModel;
4368
+ if (modelId) {
4369
+ const providerModel = await this.providerManager.getModelForProvider(
4370
+ baseUrl,
4371
+ modelId
4372
+ );
4373
+ selectedModel = providerModel ?? void 0;
4374
+ if (selectedModel) {
4375
+ requiredSats = this.providerManager.getRequiredSatsForModel(
4376
+ selectedModel,
4377
+ []
4378
+ );
4379
+ }
4380
+ }
4381
+ const { token, tokenBalance, tokenBalanceUnit } = await this._spendToken({
4382
+ mintUrl,
4383
+ amount: requiredSats,
4384
+ baseUrl
4385
+ });
4386
+ this._log("DEBUG", token, baseUrl);
4387
+ let requestBody = body;
4388
+ if (body && typeof body === "object") {
4389
+ const bodyObj = body;
4390
+ if (!bodyObj.stream) {
4391
+ requestBody = { ...bodyObj, stream: false };
4392
+ }
4393
+ }
4394
+ const baseHeaders = this._buildBaseHeaders(headers);
4395
+ const requestHeaders = this._withAuthHeader(baseHeaders, token);
4396
+ const response = await this._makeRequest({
4397
+ path,
4398
+ method,
4399
+ body: method === "GET" ? void 0 : requestBody,
4400
+ baseUrl,
4401
+ mintUrl,
4402
+ token,
4403
+ requiredSats,
4404
+ headers: requestHeaders,
4405
+ baseHeaders,
4406
+ selectedModel
4407
+ });
4408
+ const tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
4409
+ const baseUrlUsed = response.baseUrl || baseUrl;
4410
+ const tokenUsed = response.token || token;
4411
+ const contentType = response.headers.get("content-type") || "";
4412
+ let processedResponse = response;
4413
+ let capturedUsage;
4414
+ let capturedResponseId;
4415
+ if (contentType.includes("text/event-stream") && response.body) {
4416
+ const nodeReadable = stream.Readable.fromWeb(response.body);
4417
+ const sseParser = createSSEParserTransform(
4418
+ (usage) => {
4419
+ capturedUsage = usage;
4420
+ processedResponse.usage = usage;
4421
+ },
4422
+ (responseId) => {
4423
+ capturedResponseId = responseId;
4424
+ processedResponse.requestId = responseId;
4425
+ }
4426
+ );
4427
+ const transformed = nodeReadable.pipe(sseParser, { end: true });
4428
+ const webStream = stream.Readable.toWeb(
4429
+ transformed
4430
+ );
4431
+ processedResponse = new Response(webStream, {
4432
+ status: response.status,
4433
+ statusText: response.statusText,
4434
+ headers: response.headers
4435
+ });
4436
+ processedResponse.baseUrl = response.baseUrl;
4437
+ processedResponse.token = response.token;
4438
+ }
4439
+ return {
4440
+ response: processedResponse,
4441
+ tokenUsed,
4442
+ baseUrlUsed,
4443
+ tokenBalanceInSats,
4444
+ modelId,
4445
+ capturedUsage,
4446
+ capturedResponseId
4447
+ };
4448
+ }
4449
+ /**
4450
+ * Fetch AI response with streaming
4451
+ */
4452
+ async fetchAIResponse(options, callbacks) {
4453
+ const {
4454
+ messageHistory,
4455
+ selectedModel,
4456
+ baseUrl,
4457
+ mintUrl,
4458
+ balance,
4459
+ transactionHistory,
4460
+ maxTokens,
4461
+ headers
4462
+ } = options;
4463
+ const apiMessages = await this._convertMessages(messageHistory);
4464
+ const requiredSats = this.providerManager.getRequiredSatsForModel(
4465
+ selectedModel,
4466
+ apiMessages,
4467
+ maxTokens
4468
+ );
4469
+ try {
4470
+ await this._checkBalance();
4471
+ callbacks.onPaymentProcessing?.(true);
4472
+ const spendResult = await this._spendToken({
4473
+ mintUrl,
4474
+ amount: requiredSats,
4475
+ baseUrl
4476
+ });
4477
+ let token = spendResult.token;
4478
+ let tokenBalance = spendResult.tokenBalance;
4479
+ let tokenBalanceUnit = spendResult.tokenBalanceUnit;
4480
+ const tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
4481
+ callbacks.onTokenCreated?.(this._getPendingCashuTokenAmount());
4482
+ const baseHeaders = this._buildBaseHeaders(headers);
4483
+ const requestHeaders = this._withAuthHeader(baseHeaders, token);
4484
+ this.providerManager.resetFailedProviders();
4485
+ const providerInfo = await this.providerRegistry.getProviderInfo(baseUrl);
4486
+ const providerVersion = providerInfo?.version ?? "";
4487
+ let modelIdForRequest = selectedModel.id;
4488
+ if (/^0\.1\./.test(providerVersion)) {
4489
+ const newModel = await this.providerManager.getModelForProvider(
4490
+ baseUrl,
4491
+ selectedModel.id
4492
+ );
4493
+ modelIdForRequest = newModel?.id ?? selectedModel.id;
4494
+ }
4495
+ const body = {
4496
+ model: modelIdForRequest,
4497
+ messages: apiMessages,
4498
+ stream: true
4499
+ };
4500
+ if (maxTokens !== void 0) {
4501
+ body.max_tokens = maxTokens;
4502
+ }
4503
+ if (selectedModel?.name?.startsWith("OpenAI:")) {
4504
+ body.tools = [{ type: "web_search" }];
4505
+ }
4506
+ const response = await this._makeRequest({
4507
+ path: "/v1/chat/completions",
4508
+ method: "POST",
4509
+ body,
4510
+ selectedModel,
4511
+ baseUrl,
4512
+ mintUrl,
4513
+ token,
4514
+ requiredSats,
4515
+ maxTokens,
4516
+ headers: requestHeaders,
4517
+ baseHeaders
4518
+ });
4519
+ if (!response.body) {
4520
+ throw new Error("Response body is not available");
4521
+ }
4522
+ if (response.status === 200) {
4523
+ const baseUrlUsed = response.baseUrl || baseUrl;
4524
+ const streamingResult = await this.streamProcessor.process(
4525
+ response,
4526
+ {
4527
+ onContent: callbacks.onStreamingUpdate,
4528
+ onThinking: callbacks.onThinkingUpdate
4529
+ },
4530
+ selectedModel.id
4531
+ );
4532
+ if (streamingResult.finish_reason === "content_filter") {
4533
+ callbacks.onMessageAppend({
4534
+ role: "assistant",
4535
+ content: "Your request was denied due to content filtering."
4536
+ });
4537
+ } else if (streamingResult.content || streamingResult.images && streamingResult.images.length > 0) {
4538
+ const message = await this._createAssistantMessage(streamingResult);
4539
+ callbacks.onMessageAppend(message);
4540
+ } else {
4541
+ callbacks.onMessageAppend({
4542
+ role: "system",
4543
+ content: "The provider did not respond to this request."
4544
+ });
4545
+ }
4546
+ callbacks.onStreamingUpdate("");
4547
+ callbacks.onThinkingUpdate("");
4548
+ const isApikeysEstimate = this.mode === "apikeys";
4549
+ let satsSpent = await this._handlePostResponseBalanceUpdate({
4550
+ token,
4551
+ baseUrl: baseUrlUsed,
4552
+ initialTokenBalance: tokenBalanceInSats,
4553
+ fallbackSatsSpent: isApikeysEstimate ? this._getEstimatedCosts(selectedModel, streamingResult) : void 0,
4554
+ response,
4555
+ modelId: selectedModel.id,
4556
+ usage: streamingResult.usage ? {
4557
+ promptTokens: Number(streamingResult.usage.prompt_tokens ?? 0),
4558
+ completionTokens: Number(
4559
+ streamingResult.usage.completion_tokens ?? 0
4560
+ ),
4561
+ totalTokens: Number(streamingResult.usage.total_tokens ?? 0),
4562
+ cost: Number(streamingResult.usage.cost ?? 0),
4563
+ satsCost: Number(streamingResult.usage.sats_cost ?? 0)
4564
+ } : void 0,
4565
+ requestId: streamingResult.responseId
4566
+ });
4567
+ const estimatedCosts = this._getEstimatedCosts(
4568
+ selectedModel,
4569
+ streamingResult
4570
+ );
4571
+ const onLastMessageSatsUpdate = callbacks.onLastMessageSatsUpdate;
4572
+ onLastMessageSatsUpdate?.(satsSpent, estimatedCosts);
4573
+ } else {
4574
+ throw new Error(`${response.status} ${response.statusText}`);
4575
+ }
4576
+ } catch (error) {
4577
+ this._handleError(error, callbacks);
4578
+ } finally {
4579
+ callbacks.onPaymentProcessing?.(false);
4580
+ }
4581
+ }
4582
+ /**
4583
+ * Make the API request with failover support
4584
+ */
4585
+ async _makeRequest(params) {
4586
+ const { path, method, body, baseUrl, token, headers } = params;
4587
+ try {
4588
+ const url = `${baseUrl.replace(/\/$/, "")}${path}`;
4589
+ if (this.mode === "xcashu") this._log("DEBUG", "HEADERS,", headers);
4590
+ this._log("DEBUG", "HEADERS,", headers);
4591
+ const response = await fetch(url, {
4592
+ method,
4593
+ headers,
4594
+ body: body === void 0 || method === "GET" ? void 0 : JSON.stringify(body)
4595
+ });
4596
+ if (this.mode === "xcashu") this._log("DEBUG", "response,", response);
4597
+ response.baseUrl = baseUrl;
4598
+ response.token = token;
4599
+ if (!response.ok) {
4600
+ const requestId = response.headers.get("x-routstr-request-id") || void 0;
4601
+ let bodyText;
4602
+ try {
4603
+ bodyText = await response.text();
4604
+ } catch (e) {
4605
+ bodyText = void 0;
4606
+ }
4607
+ return await this._handleErrorResponse(
4608
+ params,
4609
+ token,
4610
+ response.status,
4611
+ requestId,
4612
+ this.mode === "xcashu" ? response.headers.get("x-cashu") ?? void 0 : void 0,
4613
+ bodyText,
4614
+ params.retryCount ?? 0
4615
+ );
4616
+ }
4617
+ return response;
4618
+ } catch (error) {
4619
+ if (isNetworkErrorMessage(error?.message || "")) {
4620
+ return await this._handleErrorResponse(
4621
+ params,
4622
+ token,
4623
+ -1,
4624
+ // just for Network Error to skip all statuses
4625
+ void 0,
4626
+ void 0,
4627
+ void 0,
4628
+ params.retryCount ?? 0
4629
+ );
4630
+ }
4631
+ throw error;
4632
+ }
4633
+ }
4634
+ /**
4635
+ * Handle error responses with failover
4636
+ */
4637
+ async _handleErrorResponse(params, token, status, requestId, xCashuRefundToken, responseBody, retryCount = 0) {
4638
+ const MAX_RETRIES_PER_PROVIDER = 2;
4639
+ const { path, method, body, selectedModel, baseUrl, mintUrl } = params;
4640
+ let tryNextProvider = false;
4641
+ this._log(
4642
+ "DEBUG",
4643
+ `[RoutstrClient] _handleErrorResponse: status=${status}, baseUrl=${baseUrl}, mode=${this.mode}, token preview=${token}, requestId=${requestId}`
4644
+ );
4645
+ this._log(
4646
+ "DEBUG",
4647
+ `[RoutstrClient] _handleErrorResponse: Attempting to receive/restore token for ${baseUrl}`
4648
+ );
4649
+ if (params.token.startsWith("cashu")) {
4650
+ const tryReceiveTokenResult = await this.cashuSpender.receiveToken(
4651
+ params.token
4652
+ );
4653
+ if (tryReceiveTokenResult.success) {
4654
+ this._log(
4655
+ "DEBUG",
4656
+ `[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${tryReceiveTokenResult.amount}`
4657
+ );
4658
+ tryNextProvider = true;
4659
+ if (this.mode === "lazyrefund")
4660
+ this.storageAdapter.removeToken(baseUrl);
4661
+ } else {
4662
+ this._log(
4663
+ "DEBUG",
4664
+ `[RoutstrClient] _handleErrorResponse: Failed to receive token. `
4665
+ );
4666
+ }
4667
+ }
4668
+ if (this.mode === "xcashu") {
4669
+ if (xCashuRefundToken) {
4670
+ this._log(
4671
+ "DEBUG",
4672
+ `[RoutstrClient] _handleErrorResponse: Attempting to receive xcashu refund token, preview=${xCashuRefundToken.substring(0, 20)}...`
4673
+ );
4674
+ try {
4675
+ const receiveResult = await this.cashuSpender.receiveToken(xCashuRefundToken);
4676
+ if (receiveResult.success) {
4677
+ this._log(
4678
+ "DEBUG",
4679
+ `[RoutstrClient] _handleErrorResponse: xcashu refund received, amount=${receiveResult.amount}`
4680
+ );
4681
+ tryNextProvider = true;
4682
+ } else
4683
+ throw new ProviderError(
4684
+ baseUrl,
4685
+ status,
4686
+ "xcashu refund failed",
4687
+ requestId
4688
+ );
4689
+ } catch (error) {
4690
+ this._log("ERROR", "[xcashu] Failed to receive refund token:", error);
4691
+ throw new ProviderError(
4692
+ baseUrl,
4693
+ status,
4694
+ "[xcashu] Failed to receive refund token",
4695
+ requestId
4696
+ );
4697
+ }
4698
+ } else {
4699
+ if (!tryNextProvider)
4700
+ throw new ProviderError(
4701
+ baseUrl,
4702
+ status,
4703
+ "[xcashu] Failed to receive refund token",
4704
+ requestId
4705
+ );
4706
+ }
4707
+ }
4708
+ if (status === 402 && !tryNextProvider && (this.mode === "apikeys" || this.mode === "lazyrefund")) {
4709
+ this.storageAdapter.getApiKey(baseUrl);
4710
+ let topupAmount = params.requiredSats;
4711
+ try {
4712
+ let currentBalance = 0;
4713
+ if (this.mode === "apikeys") {
4714
+ const currentBalanceInfo = await this.balanceManager.getTokenBalance(
4715
+ params.token,
4716
+ baseUrl
4717
+ );
4718
+ currentBalance = currentBalanceInfo.unit === "msat" ? currentBalanceInfo.amount / 1e3 : currentBalanceInfo.amount;
4719
+ } else if (this.mode === "lazyrefund") {
4720
+ const distribution = this.storageAdapter.getCachedTokenDistribution();
4721
+ const tokenEntry = distribution.find((t) => t.baseUrl === baseUrl);
4722
+ currentBalance = tokenEntry?.amount ?? 0;
4723
+ }
4724
+ const shortfall = Math.max(0, params.requiredSats - currentBalance);
4725
+ topupAmount = shortfall > 0 ? shortfall : params.requiredSats;
4726
+ } catch (e) {
4727
+ this._log(
4728
+ "WARN",
4729
+ "Could not get current token balance for topup calculation:",
4730
+ e
4731
+ );
4732
+ }
4733
+ const topupResult = await this.balanceManager.topUp({
4734
+ mintUrl,
4735
+ baseUrl,
4736
+ amount: topupAmount * TOPUP_MARGIN,
4737
+ token: params.token
4738
+ });
4739
+ this._log(
4740
+ "DEBUG",
4741
+ `[RoutstrClient] _handleErrorResponse: Topup result for ${baseUrl}: success=${topupResult.success}, message=${topupResult.message}`
4742
+ );
4743
+ if (!topupResult.success) {
4744
+ const message = topupResult.message || "";
4745
+ if (message.includes("Insufficient balance")) {
4746
+ const needMatch = message.match(/need (\d+)/);
4747
+ const haveMatch = message.match(/have (\d+)/);
4748
+ const required = needMatch ? parseInt(needMatch[1], 10) : params.requiredSats;
4749
+ const available = haveMatch ? parseInt(haveMatch[1], 10) : 0;
4750
+ this._log(
4751
+ "DEBUG",
4752
+ `[RoutstrClient] _handleErrorResponse: Insufficient balance, need=${required}, have=${available}`
4753
+ );
4754
+ throw new InsufficientBalanceError(
4755
+ required,
4756
+ available,
4757
+ 0,
4758
+ "",
4759
+ message
4760
+ );
4761
+ } else {
4762
+ this._log(
4763
+ "DEBUG",
4764
+ `[RoutstrClient] _handleErrorResponse: Topup failed with non-insufficient-balance error, will try next provider`
4765
+ );
4766
+ tryNextProvider = true;
4767
+ }
4768
+ } else {
4769
+ this._log(
4770
+ "DEBUG",
4771
+ `[RoutstrClient] _handleErrorResponse: Topup successful, will retry with new token`
4772
+ );
4773
+ }
4774
+ if (!tryNextProvider) {
4775
+ if (retryCount < MAX_RETRIES_PER_PROVIDER) {
4776
+ this._log(
4777
+ "DEBUG",
4778
+ `[RoutstrClient] _handleErrorResponse: Retrying 402 (attempt ${retryCount + 1}/${MAX_RETRIES_PER_PROVIDER})`
4779
+ );
4780
+ return this._makeRequest({
4781
+ ...params,
4782
+ token: params.token,
4783
+ headers: this._withAuthHeader(params.baseHeaders, params.token),
4784
+ retryCount: retryCount + 1
4785
+ });
4786
+ } else {
4787
+ this._log(
4788
+ "DEBUG",
4789
+ `[RoutstrClient] _handleErrorResponse: 402 retry limit reached (${retryCount}/${MAX_RETRIES_PER_PROVIDER}), failing over to next provider`
4790
+ );
4791
+ tryNextProvider = true;
4792
+ }
4793
+ }
4794
+ }
4795
+ const isInsufficientBalance413 = status === 413 && responseBody?.includes("Insufficient balance");
4796
+ if (isInsufficientBalance413 && !tryNextProvider && this.mode === "apikeys") {
4797
+ let retryToken = params.token;
4798
+ try {
4799
+ const latestBalanceInfo = await this.balanceManager.getTokenBalance(
4800
+ params.token,
4801
+ baseUrl
4802
+ );
4803
+ if (latestBalanceInfo.isInvalidApiKey) {
4804
+ this._log(
4805
+ "DEBUG",
4806
+ `[RoutstrClient] _handleErrorResponse: Invalid API key (proofs already spent), removing for ${baseUrl}`
4807
+ );
4808
+ this.storageAdapter.removeApiKey(baseUrl);
4809
+ tryNextProvider = true;
4810
+ } else {
4811
+ const latestTokenBalance = latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
4812
+ if (latestBalanceInfo.apiKey) {
4813
+ const storedApiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
4814
+ if (storedApiKeyEntry?.key !== latestBalanceInfo.apiKey) {
4815
+ if (storedApiKeyEntry) {
4816
+ this.storageAdapter.removeApiKey(baseUrl);
4817
+ }
4818
+ this.storageAdapter.setApiKey(baseUrl, latestBalanceInfo.apiKey);
4819
+ }
4820
+ retryToken = latestBalanceInfo.apiKey;
4821
+ }
4822
+ if (latestTokenBalance >= 0) {
4823
+ this.storageAdapter.updateApiKeyBalance(
4824
+ baseUrl,
4825
+ latestTokenBalance
4826
+ );
4827
+ }
4828
+ }
4829
+ } catch (error) {
4830
+ this._log(
4831
+ "WARN",
4832
+ `[RoutstrClient] _handleErrorResponse: Failed to refresh API key after 413 insufficient balance for ${baseUrl}`,
4833
+ error
4834
+ );
4835
+ }
4836
+ if (retryCount < MAX_RETRIES_PER_PROVIDER) {
4837
+ this._log(
4838
+ "DEBUG",
4839
+ `[RoutstrClient] _handleErrorResponse: Retrying 413 (attempt ${retryCount + 1}/${MAX_RETRIES_PER_PROVIDER})`
4840
+ );
4841
+ return this._makeRequest({
4842
+ ...params,
4843
+ token: retryToken,
4844
+ headers: this._withAuthHeader(params.baseHeaders, retryToken),
4845
+ retryCount: retryCount + 1
4846
+ });
4847
+ } else {
4848
+ this._log(
4849
+ "DEBUG",
4850
+ `[RoutstrClient] _handleErrorResponse: 413 retry limit reached (${retryCount}/${MAX_RETRIES_PER_PROVIDER}), failing over to next provider`
4851
+ );
4852
+ tryNextProvider = true;
4853
+ }
4854
+ }
4855
+ if ((status === 401 || status === 403 || status === 413 || status === 400 || status === 500 || status === 502 || status === 503 || status === 504 || status === 521) && !tryNextProvider) {
4856
+ this._log(
4857
+ "DEBUG",
4858
+ `[RoutstrClient] _handleErrorResponse: Status ${status} (auth/server error), attempting refund for ${baseUrl}, mode=${this.mode}`
4859
+ );
4860
+ if (this.mode === "lazyrefund") {
4861
+ try {
4862
+ const refundResult = await this.balanceManager.refund({
4863
+ mintUrl,
4864
+ baseUrl,
4865
+ token: params.token
4866
+ });
4867
+ this._log(
4868
+ "DEBUG",
4869
+ `[RoutstrClient] _handleErrorResponse: Lazyrefund result: success=${refundResult.success}`
4870
+ );
4871
+ if (refundResult.success) this.storageAdapter.removeToken(baseUrl);
4872
+ else
4873
+ throw new ProviderError(
4874
+ baseUrl,
4875
+ status,
4876
+ "refund failed",
4877
+ requestId
4878
+ );
4879
+ } catch (error) {
4880
+ throw new ProviderError(
4881
+ baseUrl,
4882
+ status,
4883
+ "Failed to refund token",
4884
+ requestId
4885
+ );
4886
+ }
4887
+ } else if (this.mode === "apikeys") {
4888
+ this._log(
4889
+ "DEBUG",
4890
+ `[RoutstrClient] _handleErrorResponse: Attempting API key refund for ${baseUrl}, key preview=${token}`
4891
+ );
4892
+ const initialBalance = await this.balanceManager.getTokenBalance(
4893
+ token,
4894
+ baseUrl
4895
+ );
4896
+ this._log(
4897
+ "DEBUG",
4898
+ `[RoutstrClient] _handleErrorResponse: Initial API key balance: ${initialBalance.amount}`
4899
+ );
4900
+ const refundResult = await this.balanceManager.refundApiKey({
4901
+ mintUrl,
4902
+ baseUrl,
4903
+ apiKey: token
4904
+ });
4905
+ this._log(
4906
+ "DEBUG",
4907
+ `[RoutstrClient] _handleErrorResponse: API key refund result: success=${refundResult.success}, message=${refundResult.message}`
4908
+ );
4909
+ if (!refundResult.success && initialBalance.amount > 0) {
4910
+ throw new ProviderError(
4911
+ baseUrl,
4912
+ status,
4913
+ refundResult.message ?? "Unknown error"
4914
+ );
4915
+ }
4916
+ }
4917
+ }
4918
+ this.providerManager.markFailed(baseUrl);
4919
+ this._log(
4920
+ "DEBUG",
4921
+ `[RoutstrClient] _handleErrorResponse: Marked provider ${baseUrl} as failed`
4922
+ );
4923
+ if (!selectedModel) {
4924
+ throw new ProviderError(
4925
+ baseUrl,
4926
+ status,
4927
+ "Funny, no selected model. HMM. "
4928
+ );
4929
+ }
4930
+ const nextProvider = this.providerManager.findNextBestProvider(
4931
+ selectedModel.id,
4932
+ baseUrl
4933
+ );
4934
+ if (nextProvider) {
4935
+ this._log(
4936
+ "DEBUG",
4937
+ `[RoutstrClient] _handleErrorResponse: Failing over to next provider: ${nextProvider}, model: ${selectedModel.id}`
4938
+ );
4939
+ const newModel = await this.providerManager.getModelForProvider(
4940
+ nextProvider,
4941
+ selectedModel.id
4942
+ ) ?? selectedModel;
4943
+ const messagesForPricing = Array.isArray(
4944
+ body?.messages
4945
+ ) ? body.messages : [];
4946
+ const newRequiredSats = this.providerManager.getRequiredSatsForModel(
4947
+ newModel,
4948
+ messagesForPricing,
4949
+ params.maxTokens
4950
+ );
4951
+ this._log(
4952
+ "DEBUG",
4953
+ `[RoutstrClient] _handleErrorResponse: Creating new token for failover provider ${nextProvider}, required sats: ${newRequiredSats}`
4954
+ );
4955
+ const spendResult = await this._spendToken({
4956
+ mintUrl,
4957
+ amount: newRequiredSats,
4958
+ baseUrl: nextProvider
4959
+ });
4960
+ return this._makeRequest({
4961
+ ...params,
4962
+ path,
4963
+ method,
4964
+ body,
4965
+ baseUrl: nextProvider,
4966
+ selectedModel: newModel,
4967
+ token: spendResult.token,
4968
+ requiredSats: newRequiredSats,
4969
+ headers: this._withAuthHeader(params.baseHeaders, spendResult.token),
4970
+ retryCount: 0
4971
+ });
4972
+ }
4973
+ throw new FailoverError(
4974
+ baseUrl,
4975
+ Array.from(this.providerManager.getFailedProviders())
4976
+ );
4977
+ }
4978
+ /**
4979
+ * Handle post-response balance update for all modes
4980
+ */
4981
+ async _handlePostResponseBalanceUpdate(params) {
4982
+ const {
4983
+ token,
4984
+ baseUrl,
4985
+ initialTokenBalance,
4986
+ fallbackSatsSpent,
4987
+ response,
4988
+ modelId,
4989
+ usage,
4990
+ requestId
4991
+ } = params;
4992
+ let satsSpent = initialTokenBalance;
4993
+ if (this.mode === "xcashu" && response) {
4994
+ const refundToken = response.headers.get("x-cashu") ?? void 0;
4995
+ if (refundToken) {
4996
+ try {
4997
+ const receiveResult = await this.cashuSpender.receiveToken(refundToken);
4998
+ satsSpent = initialTokenBalance - receiveResult.amount * (receiveResult.unit == "sat" ? 1 : 1e3);
4999
+ } catch (error) {
5000
+ this._log("ERROR", "[xcashu] Failed to receive refund token:", error);
5001
+ }
5002
+ }
5003
+ } else if (this.mode === "lazyrefund") {
5004
+ const latestBalanceInfo = await this.balanceManager.getTokenBalance(
5005
+ token,
5006
+ baseUrl
5007
+ );
5008
+ const latestTokenBalance = latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
5009
+ this.storageAdapter.updateTokenBalance(baseUrl, latestTokenBalance);
5010
+ satsSpent = initialTokenBalance - latestTokenBalance;
5011
+ } else if (this.mode === "apikeys") {
5012
+ try {
5013
+ const latestBalanceInfo = await this.balanceManager.getTokenBalance(
5014
+ token,
5015
+ baseUrl
5016
+ );
5017
+ this._log(
5018
+ "DEBUG",
5019
+ "LATEST Balance",
5020
+ latestBalanceInfo.amount,
5021
+ latestBalanceInfo.reserved,
5022
+ latestBalanceInfo.apiKey,
5023
+ baseUrl
5024
+ );
5025
+ const latestTokenBalance = latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
5026
+ const storedApiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
5027
+ if (storedApiKeyEntry?.key.startsWith("cashu") && latestBalanceInfo.apiKey) {
5028
+ this.storageAdapter.removeApiKey(baseUrl);
5029
+ this.storageAdapter.setApiKey(baseUrl, latestBalanceInfo.apiKey);
5030
+ }
5031
+ this.storageAdapter.updateApiKeyBalance(baseUrl, latestTokenBalance);
5032
+ satsSpent = initialTokenBalance - latestTokenBalance;
5033
+ } catch (e) {
5034
+ this._log("WARN", "Could not get updated API key balance:", e);
5035
+ satsSpent = fallbackSatsSpent ?? initialTokenBalance;
5036
+ }
5037
+ }
5038
+ await this._trackResponseUsage({
4326
5039
  token,
4327
- balance,
4328
- lastUsed: Date.now()
5040
+ baseUrl,
5041
+ response,
5042
+ modelId,
5043
+ satsSpent,
5044
+ usage,
5045
+ requestId
4329
5046
  });
4330
- store.getState().setCachedTokens(next);
4331
- },
4332
- removeToken: (baseUrl) => {
4333
- const normalized = normalizeBaseUrl(baseUrl);
4334
- const next = store.getState().cachedTokens.filter((entry) => entry.baseUrl !== normalized);
4335
- store.getState().setCachedTokens(next);
4336
- },
4337
- updateTokenBalance: (baseUrl, balance) => {
4338
- const normalized = normalizeBaseUrl(baseUrl);
4339
- const tokens = store.getState().cachedTokens;
4340
- const next = tokens.map(
4341
- (entry) => entry.baseUrl === normalized ? { ...entry, balance } : entry
5047
+ return satsSpent;
5048
+ }
5049
+ async _trackResponseUsage(params) {
5050
+ const {
5051
+ token,
5052
+ baseUrl,
5053
+ response,
5054
+ modelId,
5055
+ satsSpent,
5056
+ usage: providedUsage,
5057
+ requestId: providedRequestId
5058
+ } = params;
5059
+ if (!response || !modelId) {
5060
+ return;
5061
+ }
5062
+ try {
5063
+ let usage = providedUsage;
5064
+ let requestId = providedRequestId;
5065
+ if (!usage || !requestId) {
5066
+ const contentType = response.headers.get("content-type") || "";
5067
+ if (contentType.includes("text/event-stream")) {
5068
+ usage = usage ?? response.usage;
5069
+ requestId = requestId ?? response.requestId ?? response.headers.get("x-routstr-request-id") ?? void 0;
5070
+ if (!usage) {
5071
+ return;
5072
+ }
5073
+ } else {
5074
+ const cloned = response.clone();
5075
+ const responseBody = await cloned.json();
5076
+ usage = usage ?? extractUsageFromResponseBody(responseBody, satsSpent) ?? void 0;
5077
+ requestId = requestId ?? extractResponseId(responseBody) ?? response.headers.get("x-routstr-request-id") ?? void 0;
5078
+ }
5079
+ }
5080
+ if (!usage) {
5081
+ return;
5082
+ }
5083
+ const finalRequestId = requestId || "unknown";
5084
+ const store = await getDefaultSdkStore();
5085
+ const state = store.getState();
5086
+ const matchingClient = state.clientIds.find(
5087
+ (client) => client.apiKey === token
5088
+ );
5089
+ const entryId = finalRequestId === "unknown" ? `req-${Date.now()}-${modelId}` : finalRequestId;
5090
+ const usageTracking = getDefaultUsageTrackingDriver();
5091
+ const entry = {
5092
+ id: entryId,
5093
+ timestamp: Date.now(),
5094
+ modelId,
5095
+ baseUrl,
5096
+ requestId: finalRequestId,
5097
+ client: matchingClient?.clientId,
5098
+ ...usage
5099
+ };
5100
+ if (this.mode === "xcashu") {
5101
+ entry.satsCost = satsSpent;
5102
+ }
5103
+ await usageTracking.append(entry);
5104
+ } catch (error) {
5105
+ }
5106
+ }
5107
+ /**
5108
+ * Convert messages for API format
5109
+ */
5110
+ async _convertMessages(messages) {
5111
+ return Promise.all(
5112
+ messages.filter((m) => m.role !== "system").map(async (m) => ({
5113
+ role: m.role,
5114
+ content: typeof m.content === "string" ? m.content : m.content
5115
+ }))
4342
5116
  );
4343
- store.getState().setCachedTokens(next);
4344
- },
4345
- getCachedTokenDistribution: () => {
4346
- const cachedTokens = store.getState().cachedTokens;
4347
- const distributionMap = {};
4348
- for (const entry of cachedTokens) {
4349
- const sum = entry.balance || 0;
4350
- if (sum > 0) {
4351
- distributionMap[entry.baseUrl] = (distributionMap[entry.baseUrl] || 0) + sum;
5117
+ }
5118
+ /**
5119
+ * Create assistant message from streaming result
5120
+ */
5121
+ async _createAssistantMessage(result) {
5122
+ if (result.images && result.images.length > 0) {
5123
+ const content = [];
5124
+ if (result.content) {
5125
+ content.push({
5126
+ type: "text",
5127
+ text: result.content,
5128
+ thinking: result.thinking,
5129
+ citations: result.citations,
5130
+ annotations: result.annotations
5131
+ });
5132
+ }
5133
+ for (const img of result.images) {
5134
+ content.push({
5135
+ type: "image_url",
5136
+ image_url: {
5137
+ url: img.image_url.url
5138
+ }
5139
+ });
4352
5140
  }
5141
+ return {
5142
+ role: "assistant",
5143
+ content
5144
+ };
4353
5145
  }
4354
- return Object.entries(distributionMap).map(([baseUrl, amt]) => ({ baseUrl, amount: amt })).sort((a, b) => b.amount - a.amount);
4355
- },
4356
- getApiKeyDistribution: () => {
4357
- const apiKeys = store.getState().apiKeys;
4358
- const distributionMap = {};
4359
- for (const entry of apiKeys) {
4360
- const sum = entry.balance || 0;
4361
- if (sum > 0) {
4362
- distributionMap[entry.baseUrl] = (distributionMap[entry.baseUrl] || 0) + sum;
5146
+ return {
5147
+ role: "assistant",
5148
+ content: result.content || ""
5149
+ };
5150
+ }
5151
+ /**
5152
+ * Calculate estimated costs from usage
5153
+ */
5154
+ _getEstimatedCosts(selectedModel, streamingResult) {
5155
+ let estimatedCosts = 0;
5156
+ if (streamingResult.usage) {
5157
+ const { completion_tokens, prompt_tokens } = streamingResult.usage;
5158
+ if (completion_tokens !== void 0 && prompt_tokens !== void 0) {
5159
+ estimatedCosts = (selectedModel.sats_pricing?.completion ?? 0) * completion_tokens + (selectedModel.sats_pricing?.prompt ?? 0) * prompt_tokens;
4363
5160
  }
4364
5161
  }
4365
- return Object.entries(distributionMap).map(([baseUrl, amt]) => ({ baseUrl, amount: amt })).sort((a, b) => b.amount - a.amount);
4366
- },
4367
- saveProviderInfo: (baseUrl, info) => {
4368
- const normalized = normalizeBaseUrl(baseUrl);
4369
- const next = { ...store.getState().infoFromAllProviders };
4370
- next[normalized] = info;
4371
- store.getState().setInfoFromAllProviders(next);
4372
- },
4373
- getProviderInfo: (baseUrl) => {
4374
- const normalized = normalizeBaseUrl(baseUrl);
4375
- return store.getState().infoFromAllProviders[normalized] || null;
4376
- },
4377
- // ========== API Keys (for apikeys mode) ==========
4378
- getApiKey: (baseUrl) => {
4379
- const normalized = normalizeBaseUrl(baseUrl);
4380
- const entry = store.getState().apiKeys.find((key) => key.baseUrl === normalized);
4381
- if (!entry) return null;
4382
- const next = store.getState().apiKeys.map(
4383
- (key) => key.baseUrl === normalized ? { ...key, lastUsed: Date.now() } : key
4384
- );
4385
- store.getState().setApiKeys(next);
4386
- return entry;
4387
- },
4388
- setApiKey: (baseUrl, key) => {
4389
- const normalized = normalizeBaseUrl(baseUrl);
4390
- const keys = store.getState().apiKeys;
4391
- const existingIndex = keys.findIndex(
4392
- (entry) => entry.baseUrl === normalized
5162
+ return estimatedCosts;
5163
+ }
5164
+ /**
5165
+ * Get pending cashu token amount
5166
+ */
5167
+ _getPendingCashuTokenAmount() {
5168
+ const distribution = this.storageAdapter.getCachedTokenDistribution();
5169
+ return distribution.reduce((total, item) => total + item.amount, 0);
5170
+ }
5171
+ /**
5172
+ * Handle errors and notify callbacks
5173
+ */
5174
+ _handleError(error, callbacks) {
5175
+ this._log("ERROR", "[RoutstrClient] _handleError: Error occurred", error);
5176
+ if (error instanceof Error) {
5177
+ const isStreamError = error.message.includes("Error in input stream") || error.message.includes("Load failed");
5178
+ const modifiedErrorMsg = isStreamError ? "AI stream was cut off, turn on Keep Active or please try again" : error.message;
5179
+ this._log(
5180
+ "ERROR",
5181
+ `[RoutstrClient] _handleError: Error type=${error.constructor.name}, message=${modifiedErrorMsg}, isStreamError=${isStreamError}`
5182
+ );
5183
+ callbacks.onMessageAppend({
5184
+ role: "system",
5185
+ content: "Uncaught Error: " + modifiedErrorMsg + (this.alertLevel === "max" ? " | " + error.stack : "")
5186
+ });
5187
+ } else {
5188
+ callbacks.onMessageAppend({
5189
+ role: "system",
5190
+ content: "Unknown Error: Please tag Routstr on Nostr and/or retry."
5191
+ });
5192
+ }
5193
+ }
5194
+ /**
5195
+ * Check wallet balance and throw if insufficient
5196
+ */
5197
+ async _checkBalance() {
5198
+ const balances = await this.walletAdapter.getBalances();
5199
+ const totalBalance = Object.values(balances).reduce((sum, v) => sum + v, 0);
5200
+ if (totalBalance <= 0) {
5201
+ throw new InsufficientBalanceError(1, 0);
5202
+ }
5203
+ }
5204
+ /**
5205
+ * Spend a token using CashuSpender with standardized error handling
5206
+ */
5207
+ async _spendToken(params) {
5208
+ const { mintUrl, amount, baseUrl } = params;
5209
+ this._log(
5210
+ "DEBUG",
5211
+ `[RoutstrClient] _spendToken: mode=${this.mode}, amount=${amount}, baseUrl=${baseUrl}, mintUrl=${mintUrl}`
4393
5212
  );
4394
- if (existingIndex !== -1) {
4395
- throw new Error(`ApiKey already exists for baseUrl: ${normalized}`);
5213
+ if (this.mode === "apikeys") {
5214
+ let parentApiKey = this.storageAdapter.getApiKey(baseUrl);
5215
+ if (!parentApiKey) {
5216
+ this._log(
5217
+ "DEBUG",
5218
+ `[RoutstrClient] _spendToken: No existing API key for ${baseUrl}, creating new one via Cashu`
5219
+ );
5220
+ const spendResult2 = await this.cashuSpender.spend({
5221
+ mintUrl,
5222
+ amount: amount * TOPUP_MARGIN,
5223
+ baseUrl: "",
5224
+ reuseToken: false
5225
+ });
5226
+ if (!spendResult2.token) {
5227
+ this._log(
5228
+ "ERROR",
5229
+ `[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error:`,
5230
+ spendResult2.error
5231
+ );
5232
+ throw new Error(
5233
+ `[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error: ${spendResult2.error}`
5234
+ );
5235
+ } else {
5236
+ this._log(
5237
+ "DEBUG",
5238
+ `[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult2.token}`
5239
+ );
5240
+ }
5241
+ this._log(
5242
+ "DEBUG",
5243
+ `[RoutstrClient] _spendToken: Created API key for ${baseUrl}, key preview: ${spendResult2.token}, balance: ${spendResult2.balance}`
5244
+ );
5245
+ try {
5246
+ this.storageAdapter.setApiKey(baseUrl, spendResult2.token);
5247
+ } catch (error) {
5248
+ if (error instanceof Error && error.message.includes("ApiKey already exists")) {
5249
+ const tryReceiveTokenResult = await this.cashuSpender.receiveToken(
5250
+ spendResult2.token
5251
+ );
5252
+ if (tryReceiveTokenResult.success) {
5253
+ this._log(
5254
+ "DEBUG",
5255
+ `[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${tryReceiveTokenResult.amount}`
5256
+ );
5257
+ } else {
5258
+ this._log(
5259
+ "DEBUG",
5260
+ `[RoutstrClient] _handleErrorResponse: Token restore failed or not needed`
5261
+ );
5262
+ }
5263
+ this._log(
5264
+ "DEBUG",
5265
+ `[RoutstrClient] _spendToken: API key already exists for ${baseUrl}, using existing key`
5266
+ );
5267
+ } else {
5268
+ throw error;
5269
+ }
5270
+ }
5271
+ parentApiKey = this.storageAdapter.getApiKey(baseUrl);
5272
+ } else {
5273
+ this._log(
5274
+ "DEBUG",
5275
+ `[RoutstrClient] _spendToken: Using existing API key for ${baseUrl}, key preview: ${parentApiKey.key}`
5276
+ );
5277
+ }
5278
+ let tokenBalance = 0;
5279
+ let tokenBalanceUnit = "sat";
5280
+ const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
5281
+ const distributionForBaseUrl = apiKeyDistribution.find(
5282
+ (d) => d.baseUrl === baseUrl
5283
+ );
5284
+ if (distributionForBaseUrl) {
5285
+ tokenBalance = distributionForBaseUrl.amount;
5286
+ }
5287
+ if (tokenBalance === 0 && parentApiKey) {
5288
+ try {
5289
+ const balanceInfo = await this.balanceManager.getTokenBalance(
5290
+ parentApiKey.key,
5291
+ baseUrl
5292
+ );
5293
+ tokenBalance = balanceInfo.amount;
5294
+ tokenBalanceUnit = balanceInfo.unit;
5295
+ } catch (e) {
5296
+ this._log("WARN", "Could not get initial API key balance:", e);
5297
+ }
5298
+ }
5299
+ this._log(
5300
+ "DEBUG",
5301
+ `[RoutstrClient] _spendToken: Returning token with balance=${tokenBalance} ${tokenBalanceUnit}`
5302
+ );
5303
+ return {
5304
+ token: parentApiKey?.key ?? "",
5305
+ tokenBalance,
5306
+ tokenBalanceUnit
5307
+ };
4396
5308
  }
4397
- const next = [...keys];
4398
- next.push({
4399
- baseUrl: normalized,
4400
- key,
4401
- balance: 0,
4402
- lastUsed: Date.now()
4403
- });
4404
- store.getState().setApiKeys(next);
4405
- },
4406
- updateApiKeyBalance: (baseUrl, balance) => {
4407
- const normalized = normalizeBaseUrl(baseUrl);
4408
- const keys = store.getState().apiKeys;
4409
- const next = keys.map(
4410
- (entry) => entry.baseUrl === normalized ? { ...entry, balance } : entry
4411
- );
4412
- store.getState().setApiKeys(next);
4413
- },
4414
- removeApiKey: (baseUrl) => {
4415
- const normalized = normalizeBaseUrl(baseUrl);
4416
- const next = store.getState().apiKeys.filter((entry) => entry.baseUrl !== normalized);
4417
- store.getState().setApiKeys(next);
4418
- },
4419
- getAllApiKeys: () => {
4420
- return store.getState().apiKeys.map((entry) => ({
4421
- baseUrl: entry.baseUrl,
4422
- key: entry.key,
4423
- balance: entry.balance,
4424
- lastUsed: entry.lastUsed
4425
- }));
4426
- },
4427
- // ========== Child Keys ==========
4428
- getChildKey: (parentBaseUrl) => {
4429
- const normalized = normalizeBaseUrl(parentBaseUrl);
4430
- const entry = store.getState().childKeys.find((key) => key.parentBaseUrl === normalized);
4431
- if (!entry) return null;
4432
- return {
4433
- parentBaseUrl: entry.parentBaseUrl,
4434
- childKey: entry.childKey,
4435
- balance: entry.balance,
4436
- balanceLimit: entry.balanceLimit,
4437
- validityDate: entry.validityDate,
4438
- createdAt: entry.createdAt
4439
- };
4440
- },
4441
- setChildKey: (parentBaseUrl, childKey, balance, validityDate, balanceLimit) => {
4442
- const normalized = normalizeBaseUrl(parentBaseUrl);
4443
- const keys = store.getState().childKeys;
4444
- const existingIndex = keys.findIndex(
4445
- (entry) => entry.parentBaseUrl === normalized
5309
+ this._log(
5310
+ "DEBUG",
5311
+ `[RoutstrClient] _spendToken: Calling CashuSpender.spend for amount=${amount}, mintUrl=${mintUrl}, mode=${this.mode}`
4446
5312
  );
4447
- if (existingIndex !== -1) {
4448
- const next = keys.map(
4449
- (entry) => entry.parentBaseUrl === normalized ? {
4450
- ...entry,
4451
- childKey,
4452
- balance: balance ?? 0,
4453
- validityDate,
4454
- balanceLimit,
4455
- createdAt: Date.now()
4456
- } : entry
5313
+ const spendResult = await this.cashuSpender.spend({
5314
+ mintUrl,
5315
+ amount,
5316
+ baseUrl: this.mode === "lazyrefund" ? baseUrl : "",
5317
+ reuseToken: this.mode === "lazyrefund"
5318
+ });
5319
+ if (!spendResult.token) {
5320
+ this._log(
5321
+ "ERROR",
5322
+ `[RoutstrClient] _spendToken: CashuSpender.spend failed, error:`,
5323
+ spendResult.error
4457
5324
  );
4458
- store.getState().setChildKeys(next);
4459
5325
  } else {
4460
- const next = [...keys];
4461
- next.push({
4462
- parentBaseUrl: normalized,
4463
- childKey,
4464
- balance: balance ?? 0,
4465
- validityDate,
4466
- balanceLimit,
4467
- createdAt: Date.now()
4468
- });
4469
- store.getState().setChildKeys(next);
4470
- }
4471
- },
4472
- updateChildKeyBalance: (parentBaseUrl, balance) => {
4473
- const normalized = normalizeBaseUrl(parentBaseUrl);
4474
- const keys = store.getState().childKeys;
4475
- const next = keys.map(
4476
- (entry) => entry.parentBaseUrl === normalized ? { ...entry, balance } : entry
4477
- );
4478
- store.getState().setChildKeys(next);
4479
- },
4480
- removeChildKey: (parentBaseUrl) => {
4481
- const normalized = normalizeBaseUrl(parentBaseUrl);
4482
- const next = store.getState().childKeys.filter((entry) => entry.parentBaseUrl !== normalized);
4483
- store.getState().setChildKeys(next);
4484
- },
4485
- getAllChildKeys: () => {
4486
- return store.getState().childKeys.map((entry) => ({
4487
- parentBaseUrl: entry.parentBaseUrl,
4488
- childKey: entry.childKey,
4489
- balance: entry.balance,
4490
- balanceLimit: entry.balanceLimit,
4491
- validityDate: entry.validityDate,
4492
- createdAt: entry.createdAt
4493
- }));
4494
- },
4495
- getCachedReceiveTokens: () => {
4496
- return store.getState().cachedReceiveTokens;
4497
- },
4498
- setCachedReceiveTokens: (tokens) => {
4499
- store.getState().setCachedReceiveTokens(tokens);
4500
- }
4501
- });
4502
- var createProviderRegistryFromStore = (store) => ({
4503
- getModelsForProvider: (baseUrl) => {
4504
- const normalized = normalizeBaseUrl(baseUrl);
4505
- return store.getState().modelsFromAllProviders[normalized] || [];
4506
- },
4507
- getDisabledProviders: () => store.getState().disabledProviders,
4508
- getProviderMints: (baseUrl) => {
4509
- const normalized = normalizeBaseUrl(baseUrl);
4510
- return store.getState().mintsFromAllProviders[normalized] || [];
4511
- },
4512
- getProviderInfo: async (baseUrl) => {
4513
- const normalized = normalizeBaseUrl(baseUrl);
4514
- const cached = store.getState().infoFromAllProviders[normalized];
4515
- if (cached) return cached;
4516
- try {
4517
- const response = await fetch(`${normalized}v1/info`);
4518
- if (!response.ok) {
4519
- throw new Error(`Failed ${response.status}`);
4520
- }
4521
- const info = await response.json();
4522
- const next = { ...store.getState().infoFromAllProviders };
4523
- next[normalized] = info;
4524
- store.getState().setInfoFromAllProviders(next);
4525
- return info;
4526
- } catch (error) {
4527
- console.warn(`Failed to fetch provider info from ${normalized}:`, error);
4528
- return null;
5326
+ this._log(
5327
+ "DEBUG",
5328
+ `[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult.token}, balance: ${spendResult.balance} ${spendResult.unit ?? "sat"}`
5329
+ );
4529
5330
  }
4530
- },
4531
- getAllProvidersModels: () => store.getState().modelsFromAllProviders
4532
- });
4533
-
4534
- // storage/index.ts
4535
- var isBrowser2 = () => {
4536
- try {
4537
- return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
4538
- } catch {
4539
- return false;
4540
- }
4541
- };
4542
- var isNode = () => {
4543
- try {
4544
- return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
4545
- } catch {
4546
- return false;
4547
- }
4548
- };
4549
- var defaultDriver = null;
4550
- var isBun2 = () => {
4551
- return typeof process.versions.bun !== "undefined";
4552
- };
4553
- var getDefaultSdkDriver = () => {
4554
- if (defaultDriver) return defaultDriver;
4555
- if (isBrowser2()) {
4556
- defaultDriver = localStorageDriver;
4557
- return defaultDriver;
4558
- }
4559
- if (isBun2()) {
4560
- defaultDriver = createMemoryDriver();
4561
- return defaultDriver;
5331
+ return {
5332
+ token: spendResult.token,
5333
+ tokenBalance: spendResult.balance,
5334
+ tokenBalanceUnit: spendResult.unit ?? "sat"
5335
+ };
4562
5336
  }
4563
- if (isNode()) {
4564
- defaultDriver = createSqliteDriver();
4565
- return defaultDriver;
5337
+ /**
5338
+ * Build request headers with common defaults and dev mock controls
5339
+ */
5340
+ _buildBaseHeaders(additionalHeaders = {}, token) {
5341
+ const headers = {
5342
+ ...additionalHeaders,
5343
+ "Content-Type": "application/json"
5344
+ };
5345
+ return headers;
4566
5346
  }
4567
- defaultDriver = createMemoryDriver();
4568
- return defaultDriver;
4569
- };
4570
- var defaultStore = null;
4571
- var getDefaultSdkStore = () => {
4572
- if (!defaultStore) {
4573
- defaultStore = createSdkStore({ driver: getDefaultSdkDriver() });
5347
+ /**
5348
+ * Attach auth headers using the active client mode
5349
+ */
5350
+ _withAuthHeader(headers, token) {
5351
+ const nextHeaders = { ...headers };
5352
+ if (this.mode === "xcashu") {
5353
+ nextHeaders["X-Cashu"] = token;
5354
+ } else {
5355
+ nextHeaders["Authorization"] = `Bearer ${token}`;
5356
+ }
5357
+ return nextHeaders;
4574
5358
  }
4575
- return defaultStore.hydrate.then(() => defaultStore.store);
4576
5359
  };
4577
- var getDefaultDiscoveryAdapter = async () => createDiscoveryAdapterFromStore(await getDefaultSdkStore());
4578
- var getDefaultStorageAdapter = async () => createStorageAdapterFromStore(await getDefaultSdkStore());
4579
- var getDefaultProviderRegistry = async () => createProviderRegistryFromStore(await getDefaultSdkStore());
4580
5360
 
4581
5361
  // routeRequests.ts
4582
- async function routeRequests(options) {
5362
+ async function resolveRouteRequestContext(options) {
4583
5363
  const {
4584
5364
  modelId,
4585
5365
  requestBody,
@@ -4653,12 +5433,11 @@ async function routeRequests(options) {
4653
5433
  if (!mintUrl) {
4654
5434
  throw new Error("No mint configured in wallet");
4655
5435
  }
4656
- const alertLevel = "min";
4657
5436
  const client = new RoutstrClient(
4658
5437
  walletAdapter,
4659
5438
  storageAdapter,
4660
5439
  providerRegistry,
4661
- alertLevel,
5440
+ "min",
4662
5441
  mode
4663
5442
  );
4664
5443
  if (debugLevel) {
@@ -4666,18 +5445,27 @@ async function routeRequests(options) {
4666
5445
  }
4667
5446
  const maxTokens = extractMaxTokens(requestBody);
4668
5447
  const stream = extractStream(requestBody);
4669
- let response = null;
5448
+ const proxiedBody = requestBody && typeof requestBody === "object" ? { ...requestBody } : {};
5449
+ proxiedBody.model = selectedModel.id;
5450
+ if (stream !== void 0) {
5451
+ proxiedBody.stream = stream;
5452
+ }
5453
+ if (maxTokens !== void 0) {
5454
+ proxiedBody.max_tokens = maxTokens;
5455
+ }
5456
+ return {
5457
+ client,
5458
+ baseUrl,
5459
+ mintUrl,
5460
+ path,
5461
+ modelId,
5462
+ proxiedBody
5463
+ };
5464
+ }
5465
+ async function routeRequests(options) {
5466
+ const { client, baseUrl, mintUrl, path, modelId, proxiedBody } = await resolveRouteRequestContext(options);
4670
5467
  try {
4671
- const proxiedBody = requestBody && typeof requestBody === "object" ? { ...requestBody } : {};
4672
- proxiedBody.model = selectedModel.id;
4673
- if (stream !== void 0) {
4674
- proxiedBody.stream = stream;
4675
- }
4676
- if (maxTokens !== void 0) {
4677
- proxiedBody.max_tokens = maxTokens;
4678
- }
4679
- console.log(modelId);
4680
- response = await client.routeRequest({
5468
+ const response = await client.routeRequest({
4681
5469
  path,
4682
5470
  method: "POST",
4683
5471
  body: proxiedBody,
@@ -4696,24 +5484,41 @@ async function routeRequests(options) {
4696
5484
  throw error;
4697
5485
  }
4698
5486
  }
5487
+ async function routeRequestsToNodeResponse(options) {
5488
+ const { res } = options;
5489
+ const { client, baseUrl, mintUrl, path, modelId, proxiedBody } = await resolveRouteRequestContext(options);
5490
+ try {
5491
+ await client.routeRequestToNodeResponse({
5492
+ path,
5493
+ method: "POST",
5494
+ body: proxiedBody,
5495
+ baseUrl,
5496
+ mintUrl,
5497
+ modelId,
5498
+ res
5499
+ });
5500
+ } catch (error) {
5501
+ if (error instanceof Error && (error.message.includes("401") || error.message.includes("402") || error.message.includes("403"))) {
5502
+ throw new Error(`Authentication failed: ${error.message}`);
5503
+ }
5504
+ throw error;
5505
+ }
5506
+ }
4699
5507
  function extractMaxTokens(requestBody) {
4700
5508
  if (!requestBody || typeof requestBody !== "object") {
4701
5509
  return void 0;
4702
5510
  }
4703
5511
  const body = requestBody;
4704
5512
  const maxTokens = body.max_tokens;
4705
- if (typeof maxTokens === "number") {
4706
- return maxTokens;
4707
- }
4708
- return void 0;
5513
+ return typeof maxTokens === "number" ? maxTokens : void 0;
4709
5514
  }
4710
5515
  function extractStream(requestBody) {
4711
5516
  if (!requestBody || typeof requestBody !== "object") {
4712
- return false;
5517
+ return void 0;
4713
5518
  }
4714
5519
  const body = requestBody;
4715
5520
  const stream = body.stream;
4716
- return stream === true;
5521
+ return typeof stream === "boolean" ? stream : void 0;
4717
5522
  }
4718
5523
 
4719
5524
  exports.BalanceManager = BalanceManager;
@@ -4736,10 +5541,14 @@ exports.StreamingError = StreamingError;
4736
5541
  exports.TokenOperationError = TokenOperationError;
4737
5542
  exports.createDiscoveryAdapterFromStore = createDiscoveryAdapterFromStore;
4738
5543
  exports.createIndexedDBDriver = createIndexedDBDriver;
5544
+ exports.createIndexedDBUsageTrackingDriver = createIndexedDBUsageTrackingDriver;
4739
5545
  exports.createMemoryDriver = createMemoryDriver;
5546
+ exports.createMemoryUsageTrackingDriver = createMemoryUsageTrackingDriver;
4740
5547
  exports.createProviderRegistryFromStore = createProviderRegistryFromStore;
5548
+ exports.createSSEParserTransform = createSSEParserTransform;
4741
5549
  exports.createSdkStore = createSdkStore;
4742
5550
  exports.createSqliteDriver = createSqliteDriver;
5551
+ exports.createSqliteUsageTrackingDriver = createSqliteUsageTrackingDriver;
4743
5552
  exports.createStorageAdapterFromStore = createStorageAdapterFromStore;
4744
5553
  exports.filterBaseUrlsForTor = filterBaseUrlsForTor;
4745
5554
  exports.getDefaultDiscoveryAdapter = getDefaultDiscoveryAdapter;
@@ -4747,11 +5556,13 @@ exports.getDefaultProviderRegistry = getDefaultProviderRegistry;
4747
5556
  exports.getDefaultSdkDriver = getDefaultSdkDriver;
4748
5557
  exports.getDefaultSdkStore = getDefaultSdkStore;
4749
5558
  exports.getDefaultStorageAdapter = getDefaultStorageAdapter;
5559
+ exports.getDefaultUsageTrackingDriver = getDefaultUsageTrackingDriver;
4750
5560
  exports.getProviderEndpoints = getProviderEndpoints;
4751
5561
  exports.isOnionUrl = isOnionUrl;
4752
5562
  exports.isTorContext = isTorContext;
4753
5563
  exports.localStorageDriver = localStorageDriver;
4754
5564
  exports.normalizeProviderUrl = normalizeProviderUrl;
4755
5565
  exports.routeRequests = routeRequests;
5566
+ exports.routeRequestsToNodeResponse = routeRequestsToNodeResponse;
4756
5567
  //# sourceMappingURL=index.js.map
4757
5568
  //# sourceMappingURL=index.js.map