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