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