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