@routstr/sdk 0.2.4 → 0.2.6

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 (40) hide show
  1. package/README.md +10 -2
  2. package/dist/client/index.d.mts +38 -11
  3. package/dist/client/index.d.ts +38 -11
  4. package/dist/client/index.js +1771 -391
  5. package/dist/client/index.js.map +1 -1
  6. package/dist/client/index.mjs +1771 -392
  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 +26 -22
  15. package/dist/index.d.ts +26 -22
  16. package/dist/index.js +3005 -2107
  17. package/dist/index.js.map +1 -1
  18. package/dist/index.mjs +2997 -2108
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/{interfaces-B85Wx7ni.d.mts → interfaces-B62Rw-dd.d.ts} +20 -15
  21. package/dist/{interfaces-DGdP8fQp.d.mts → interfaces-BWJJTCXO.d.mts} +1 -1
  22. package/dist/{interfaces-CC0LT9p9.d.ts → interfaces-BxDEka72.d.ts} +1 -1
  23. package/dist/{interfaces-BVNyAmKu.d.ts → interfaces-C5fLD3jB.d.mts} +20 -15
  24. package/dist/storage/index.d.mts +38 -142
  25. package/dist/storage/index.d.ts +38 -142
  26. package/dist/storage/index.js +852 -158
  27. package/dist/storage/index.js.map +1 -1
  28. package/dist/storage/index.mjs +846 -159
  29. package/dist/storage/index.mjs.map +1 -1
  30. package/dist/store-BJlwiDX5.d.ts +151 -0
  31. package/dist/store-C5lnyX8k.d.mts +151 -0
  32. package/dist/{types-BlHjmWRK.d.mts → types-BYj_8c5c.d.mts} +3 -0
  33. package/dist/{types-BlHjmWRK.d.ts → types-BYj_8c5c.d.ts} +3 -0
  34. package/dist/wallet/index.d.mts +24 -26
  35. package/dist/wallet/index.d.ts +24 -26
  36. package/dist/wallet/index.js +130 -259
  37. package/dist/wallet/index.js.map +1 -1
  38. package/dist/wallet/index.mjs +130 -259
  39. package/dist/wallet/index.mjs.map +1 -1
  40. package/package.json +1 -1
@@ -1,3 +1,6 @@
1
+ import { createStore } from 'zustand/vanilla';
2
+ import { Transform, Readable } from 'stream';
3
+
1
4
  // core/errors.ts
2
5
  var InsufficientBalanceError = class extends Error {
3
6
  constructor(required, available, maxMintBalance = 0, maxMintUrl = "", customMessage) {
@@ -141,13 +144,8 @@ var CashuSpender = class {
141
144
  normalizedMintBalances[url] = balanceInSats;
142
145
  totalMintBalance += balanceInSats;
143
146
  }
144
- const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
145
147
  const providerBalances = {};
146
148
  let totalProviderBalance = 0;
147
- for (const pending of pendingDistribution) {
148
- providerBalances[pending.baseUrl] = (providerBalances[pending.baseUrl] || 0) + pending.amount;
149
- totalProviderBalance += pending.amount;
150
- }
151
149
  const apiKeys = this.storageAdapter.getAllApiKeys();
152
150
  for (const apiKey of apiKeys) {
153
151
  if (!providerBalances[apiKey.baseUrl]) {
@@ -368,27 +366,11 @@ var CashuSpender = class {
368
366
  };
369
367
  }
370
368
  }
371
- if (token && baseUrl) {
372
- try {
373
- this.storageAdapter.setToken(baseUrl, token);
374
- } catch (error) {
375
- if (error instanceof Error && error.message.includes("Token already exists")) {
376
- this._log(
377
- "DEBUG",
378
- `[CashuSpender] _spendInternal: Token already exists for ${baseUrl}, receiving newly created token and using existing`
379
- );
380
- const receiveResult = await this.receiveToken(token);
381
- if (receiveResult.success) {
382
- this._log(
383
- "DEBUG",
384
- `[CashuSpender] _spendInternal: Token restored successfully, amount=${receiveResult.amount}`
385
- );
386
- }
387
- token = this.storageAdapter.getToken(baseUrl);
388
- } else {
389
- throw error;
390
- }
391
- }
369
+ if (token) {
370
+ this._log(
371
+ "DEBUG",
372
+ `[CashuSpender] _spendInternal: Successfully spent ${spentAmount}, returning token with balance=${spentAmount}`
373
+ );
392
374
  }
393
375
  this._logTransaction("spend", {
394
376
  amount: spentAmount,
@@ -409,19 +391,19 @@ var CashuSpender = class {
409
391
  };
410
392
  }
411
393
  /**
412
- * Try to reuse an existing token
394
+ * Try to reuse an existing API key
413
395
  */
414
396
  async _tryReuseToken(baseUrl, amount, mintUrl) {
415
- const storedToken = this.storageAdapter.getToken(baseUrl);
416
- if (!storedToken) return null;
417
- const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
418
- const balanceForBaseUrl = pendingDistribution.find((b) => b.baseUrl === baseUrl)?.amount || 0;
419
- this._log("DEBUG", "RESUINGDSR GSODGNSD", balanceForBaseUrl, amount);
397
+ const apiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
398
+ if (!apiKeyEntry) return null;
399
+ const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
400
+ const balanceForBaseUrl = apiKeyDistribution.find((b) => b.baseUrl === baseUrl)?.amount || 0;
401
+ this._log("DEBUG", "Reusing API key", balanceForBaseUrl, amount);
420
402
  if (balanceForBaseUrl > amount) {
421
403
  const units = this.walletAdapter.getMintUnits();
422
404
  const unit = units[mintUrl] || "sat";
423
405
  return {
424
- token: storedToken,
406
+ token: apiKeyEntry.key,
425
407
  status: "success",
426
408
  balance: balanceForBaseUrl,
427
409
  unit
@@ -432,7 +414,8 @@ var CashuSpender = class {
432
414
  const topUpResult = await this.balanceManager.topUp({
433
415
  mintUrl,
434
416
  baseUrl,
435
- amount: topUpAmount
417
+ amount: topUpAmount,
418
+ token: apiKeyEntry.key
436
419
  });
437
420
  this._log("DEBUG", "TOPUP ", topUpResult);
438
421
  if (topUpResult.success && topUpResult.toppedUpAmount) {
@@ -446,7 +429,7 @@ var CashuSpender = class {
446
429
  status: "success"
447
430
  });
448
431
  return {
449
- token: storedToken,
432
+ token: apiKeyEntry.key,
450
433
  status: "success",
451
434
  balance: newBalance,
452
435
  unit
@@ -454,83 +437,108 @@ var CashuSpender = class {
454
437
  }
455
438
  const providerBalance = await this._getProviderTokenBalance(
456
439
  baseUrl,
457
- storedToken
440
+ apiKeyEntry.key
458
441
  );
459
442
  this._log("DEBUG", providerBalance);
460
443
  if (providerBalance <= 0) {
461
- this.storageAdapter.removeToken(baseUrl);
444
+ this.storageAdapter.removeApiKey(baseUrl);
462
445
  }
463
446
  }
464
447
  return null;
465
448
  }
466
449
  /**
467
- * Refund specific providers without retrying spend
450
+ * Refund all xcashu tokens from storage and increment tryCounts on failure.
451
+ * Reuses receiveToken from BalanceManager/CashuSpender for receiving refunds.
452
+ * @param mintUrl - The mint URL for receiving tokens
453
+ * @param excludeBaseUrls - Base URLs to exclude from refund (optional)
454
+ * @returns Results for each xcashu token refund attempt
468
455
  */
469
- async refundProviders(baseUrls, mintUrl, refundApiKeys = false) {
456
+ async refundXcashuTokens(mintUrl, excludeBaseUrls) {
470
457
  const results = [];
471
- const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
472
- const toRefund = pendingDistribution.filter(
473
- (p) => baseUrls.includes(p.baseUrl)
474
- );
475
- const refundResults = await Promise.allSettled(
476
- toRefund.map(async (pending) => {
477
- const token = this.storageAdapter.getToken(pending.baseUrl);
478
- this._log("DEBUG", token, this.balanceManager);
479
- if (!token || !this.balanceManager) {
480
- return { baseUrl: pending.baseUrl, success: false };
481
- }
482
- const tokenBalance = await this.balanceManager.getTokenBalance(
483
- token,
484
- pending.baseUrl
485
- );
486
- if (tokenBalance.reserved > 0) {
487
- return { baseUrl: pending.baseUrl, success: false };
488
- }
489
- const result = await this.balanceManager.refund({
490
- mintUrl,
491
- baseUrl: pending.baseUrl,
492
- token
493
- });
494
- this._log("DEBUG", result);
495
- if (result.success) {
496
- this.storageAdapter.removeToken(pending.baseUrl);
497
- }
498
- return { baseUrl: pending.baseUrl, success: result.success };
499
- })
500
- );
501
- results.push(
502
- ...refundResults.map(
503
- (r) => r.status === "fulfilled" ? r.value : { baseUrl: "", success: false }
504
- )
505
- );
506
- if (refundApiKeys) {
507
- const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
508
- const apiKeysToRefund = apiKeyDistribution.filter(
509
- (p) => baseUrls.includes(p.baseUrl)
510
- );
511
- for (const apiKeyEntry of apiKeysToRefund) {
512
- const apiKeyEntryFull = this.storageAdapter.getApiKey(
513
- apiKeyEntry.baseUrl
514
- );
515
- if (apiKeyEntryFull && this.balanceManager) {
516
- const refundResult = await this.balanceManager.refundApiKey({
517
- mintUrl,
518
- baseUrl: apiKeyEntry.baseUrl,
519
- apiKey: apiKeyEntryFull.key
520
- });
521
- if (refundResult.success) {
522
- this.storageAdapter.updateApiKeyBalance(apiKeyEntry.baseUrl, 0);
458
+ const xcashuTokens = this.storageAdapter.getXcashuTokens();
459
+ const excludedUrls = new Set(excludeBaseUrls || []);
460
+ for (const [baseUrl, tokens] of Object.entries(xcashuTokens)) {
461
+ if (excludedUrls.has(baseUrl)) continue;
462
+ for (const xcashuToken of tokens) {
463
+ try {
464
+ const receiveResult = await this.receiveToken(xcashuToken.token);
465
+ if (receiveResult.success) {
466
+ this.storageAdapter.removeXcashuToken(baseUrl, xcashuToken.token);
467
+ results.push({
468
+ baseUrl,
469
+ token: xcashuToken.token,
470
+ success: true
471
+ });
472
+ this._log(
473
+ "DEBUG",
474
+ `[CashuSpender] refundXcashuTokens: Successfully refunded xcashu token for ${baseUrl}, amount=${receiveResult.amount}`
475
+ );
476
+ } else {
477
+ const currentTryCount = xcashuToken.tryCount ?? 0;
478
+ const newTryCount = currentTryCount + 1;
479
+ this.storageAdapter.updateXcashuTokenTryCount(xcashuToken.token, newTryCount);
480
+ results.push({
481
+ baseUrl,
482
+ token: xcashuToken.token,
483
+ success: false,
484
+ error: receiveResult.message ?? "Refund failed"
485
+ });
486
+ this._log(
487
+ "DEBUG",
488
+ `[CashuSpender] refundXcashuTokens: Failed to refund xcashu token for ${baseUrl}, incremented tryCount to ${newTryCount}`
489
+ );
523
490
  }
491
+ } catch (error) {
492
+ const currentTryCount = xcashuToken.tryCount ?? 0;
493
+ const newTryCount = currentTryCount + 1;
494
+ this.storageAdapter.updateXcashuTokenTryCount(xcashuToken.token, newTryCount);
495
+ const errorMessage = error instanceof Error ? error.message : String(error);
524
496
  results.push({
525
- baseUrl: apiKeyEntry.baseUrl,
526
- success: refundResult.success
497
+ baseUrl,
498
+ token: xcashuToken.token,
499
+ success: false,
500
+ error: errorMessage
527
501
  });
502
+ this._log(
503
+ "ERROR",
504
+ `[CashuSpender] refundXcashuTokens: Exception during refund for ${baseUrl}: ${errorMessage}, incremented tryCount to ${newTryCount}`
505
+ );
506
+ }
507
+ }
508
+ }
509
+ return results;
510
+ }
511
+ /**
512
+ * Refund specific providers without retrying spend
513
+ */
514
+ async refundProviders(mintUrl, forceRefund) {
515
+ const results = [];
516
+ const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
517
+ for (const apiKeyEntry of apiKeyDistribution) {
518
+ const apiKeyEntryFull = this.storageAdapter.getApiKey(
519
+ apiKeyEntry.baseUrl
520
+ );
521
+ if (apiKeyEntryFull && this.balanceManager) {
522
+ const refundResult = await this.balanceManager.refundApiKey({
523
+ mintUrl,
524
+ baseUrl: apiKeyEntry.baseUrl,
525
+ apiKey: apiKeyEntryFull.key,
526
+ forceRefund
527
+ });
528
+ if (refundResult.success) {
529
+ this.storageAdapter.removeApiKey(apiKeyEntry.baseUrl);
528
530
  } else {
529
- results.push({
530
- baseUrl: apiKeyEntry.baseUrl,
531
- success: false
532
- });
531
+ this.storageAdapter.updateApiKeyBalance(apiKeyEntry.baseUrl, apiKeyEntry.amount);
533
532
  }
533
+ results.push({
534
+ baseUrl: apiKeyEntry.baseUrl,
535
+ success: refundResult.success
536
+ });
537
+ } else {
538
+ results.push({
539
+ baseUrl: apiKeyEntry.baseUrl,
540
+ success: false
541
+ });
534
542
  }
535
543
  }
536
544
  return results;
@@ -615,13 +623,8 @@ var BalanceManager = class {
615
623
  normalizedMintBalances[url] = balanceInSats;
616
624
  totalMintBalance += balanceInSats;
617
625
  }
618
- const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
619
626
  const providerBalances = {};
620
627
  let totalProviderBalance = 0;
621
- for (const pending of pendingDistribution) {
622
- providerBalances[pending.baseUrl] = (providerBalances[pending.baseUrl] || 0) + pending.amount;
623
- totalProviderBalance += pending.amount;
624
- }
625
628
  const apiKeys = this.storageAdapter.getAllApiKeys();
626
629
  for (const apiKey of apiKeys) {
627
630
  if (!providerBalances[apiKey.baseUrl]) {
@@ -636,65 +639,31 @@ var BalanceManager = class {
636
639
  mintBalances: normalizedMintBalances
637
640
  };
638
641
  }
639
- /**
640
- * Unified refund - handles both NIP-60 and legacy wallet refunds
641
- */
642
- async refund(options) {
643
- const { mintUrl, baseUrl, token: providedToken } = options;
644
- const storedToken = providedToken || this.storageAdapter.getToken(baseUrl);
645
- if (!storedToken) {
646
- console.log("[BalanceManager] No token to refund, returning early");
647
- return { success: true, message: "No API key to refund" };
648
- }
649
- let fetchResult;
650
- try {
651
- fetchResult = await this._fetchRefundToken(baseUrl, storedToken);
652
- if (!fetchResult.success) {
653
- return {
654
- success: false,
655
- message: fetchResult.error || "Refund failed",
656
- requestId: fetchResult.requestId
657
- };
658
- }
659
- if (!fetchResult.token) {
660
- return {
661
- success: false,
662
- message: "No token received from refund",
663
- requestId: fetchResult.requestId
664
- };
665
- }
666
- if (fetchResult.error === "No balance to refund") {
667
- console.log(
668
- "[BalanceManager] No balance to refund, removing stored token"
669
- );
670
- this.storageAdapter.removeToken(baseUrl);
671
- return { success: true, message: "No balance to refund" };
672
- }
673
- const receiveResult = await this.cashuSpender.receiveToken(
674
- fetchResult.token
675
- );
676
- const totalAmountMsat = receiveResult.unit === "msat" ? receiveResult.amount : receiveResult.amount * 1e3;
677
- if (!providedToken) {
678
- this.storageAdapter.removeToken(baseUrl);
679
- }
680
- return {
681
- success: receiveResult.success,
682
- refundedAmount: totalAmountMsat,
683
- requestId: fetchResult.requestId
684
- };
685
- } catch (error) {
686
- console.error("[BalanceManager] Refund error", error);
687
- return this._handleRefundError(error, mintUrl, fetchResult?.requestId);
688
- }
689
- }
690
642
  /**
691
643
  * Refund API key balance - convert remaining API key balance to cashu token
644
+ * @param options - Refund options including forceRefund flag
645
+ * @returns Refund result
692
646
  */
693
647
  async refundApiKey(options) {
694
- const { mintUrl, baseUrl, apiKey } = options;
648
+ const { mintUrl, baseUrl, apiKey, forceRefund } = options;
695
649
  if (!apiKey) {
696
650
  return { success: false, message: "No API key to refund" };
697
651
  }
652
+ if (!forceRefund) {
653
+ const apiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
654
+ if (apiKeyEntry?.lastUsed) {
655
+ const fiveMinutesAgo = Date.now() - 5 * 60 * 1e3;
656
+ if (apiKeyEntry.lastUsed > fiveMinutesAgo) {
657
+ console.log(
658
+ `[BalanceManager] Skipping refund for ${baseUrl} - used ${Math.round((Date.now() - apiKeyEntry.lastUsed) / 1e3)}s ago`
659
+ );
660
+ return {
661
+ success: false,
662
+ message: "API key was used recently, skipping refund"
663
+ };
664
+ }
665
+ }
666
+ }
698
667
  let fetchResult;
699
668
  try {
700
669
  fetchResult = await this._fetchRefundTokenWithApiKey(baseUrl, apiKey);
@@ -805,8 +774,9 @@ var BalanceManager = class {
805
774
  if (!amount || amount <= 0) {
806
775
  return { success: false, message: "Invalid top up amount" };
807
776
  }
808
- const storedToken = providedToken || this.storageAdapter.getToken(baseUrl);
809
- if (!storedToken) {
777
+ const apiKeyEntry = providedToken ? null : this.storageAdapter.getApiKey(baseUrl);
778
+ const apiKey = providedToken || apiKeyEntry?.key;
779
+ if (!apiKey) {
810
780
  return { success: false, message: "No API key available for top up" };
811
781
  }
812
782
  let cashuToken = null;
@@ -826,7 +796,7 @@ var BalanceManager = class {
826
796
  cashuToken = tokenResult.token;
827
797
  const topUpResult = await this._postTopUp(
828
798
  baseUrl,
829
- storedToken,
799
+ apiKey,
830
800
  cashuToken
831
801
  );
832
802
  requestId = topUpResult.requestId;
@@ -886,8 +856,8 @@ var BalanceManager = class {
886
856
  const refundableProviderBalance = Object.entries(
887
857
  balanceState.providerBalances
888
858
  ).filter(([providerBaseUrl]) => providerBaseUrl !== baseUrl).reduce((sum, [, value]) => sum + value, 0);
889
- if (totalMintBalance + targetProviderBalance < adjustedAmount && totalMintBalance + targetProviderBalance + refundableProviderBalance >= adjustedAmount && retryCount < 1) {
890
- await this._refundOtherProvidersForTopUp(baseUrl, mintUrl);
859
+ if (totalMintBalance + targetProviderBalance < adjustedAmount && totalMintBalance + targetProviderBalance + refundableProviderBalance >= adjustedAmount && retryCount < 2) {
860
+ await this._refundOtherProvidersForTopUp(baseUrl, mintUrl, retryCount);
891
861
  return this.createProviderToken({
892
862
  ...options,
893
863
  retryCount: retryCount + 1
@@ -1040,38 +1010,12 @@ var BalanceManager = class {
1040
1010
  }
1041
1011
  return candidates;
1042
1012
  }
1043
- async _refundOtherProvidersForTopUp(baseUrl, mintUrl) {
1044
- const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
1013
+ async _refundOtherProvidersForTopUp(baseUrl, mintUrl, retryCount) {
1045
1014
  const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
1046
- const toRefund = pendingDistribution.filter(
1047
- (pending) => pending.baseUrl !== baseUrl
1048
- );
1015
+ const forceRefund = retryCount >= 2;
1049
1016
  const apiKeysToRefund = apiKeyDistribution.filter(
1050
1017
  (apiKey) => apiKey.baseUrl !== baseUrl && apiKey.amount > 0
1051
1018
  );
1052
- const tokenRefundResults = await Promise.allSettled(
1053
- toRefund.map(async (pending) => {
1054
- const token = this.storageAdapter.getToken(pending.baseUrl);
1055
- if (!token) {
1056
- return { baseUrl: pending.baseUrl, success: false };
1057
- }
1058
- const tokenBalance = await this.getTokenBalance(token, pending.baseUrl);
1059
- if (tokenBalance.reserved > 0) {
1060
- return { baseUrl: pending.baseUrl, success: false };
1061
- }
1062
- const result = await this.refund({
1063
- mintUrl,
1064
- baseUrl: pending.baseUrl,
1065
- token
1066
- });
1067
- return { baseUrl: pending.baseUrl, success: result.success };
1068
- })
1069
- );
1070
- for (const result of tokenRefundResults) {
1071
- if (result.status === "fulfilled" && result.value.success) {
1072
- this.storageAdapter.removeToken(result.value.baseUrl);
1073
- }
1074
- }
1075
1019
  const apiKeyRefundResults = await Promise.allSettled(
1076
1020
  apiKeysToRefund.map(async (apiKeyEntry) => {
1077
1021
  const fullApiKeyEntry = this.storageAdapter.getApiKey(
@@ -1083,7 +1027,8 @@ var BalanceManager = class {
1083
1027
  const result = await this.refundApiKey({
1084
1028
  mintUrl,
1085
1029
  baseUrl: apiKeyEntry.baseUrl,
1086
- apiKey: fullApiKeyEntry.key
1030
+ apiKey: fullApiKeyEntry.key,
1031
+ forceRefund
1087
1032
  });
1088
1033
  return { baseUrl: apiKeyEntry.baseUrl, success: result.success };
1089
1034
  })
@@ -1094,77 +1039,6 @@ var BalanceManager = class {
1094
1039
  }
1095
1040
  }
1096
1041
  }
1097
- /**
1098
- * Fetch refund token from provider API
1099
- */
1100
- async _fetchRefundToken(baseUrl, storedToken) {
1101
- if (!baseUrl) {
1102
- return {
1103
- success: false,
1104
- error: "No base URL configured"
1105
- };
1106
- }
1107
- const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
1108
- const url = `${normalizedBaseUrl}v1/wallet/refund`;
1109
- const controller = new AbortController();
1110
- const timeoutId = setTimeout(() => {
1111
- controller.abort();
1112
- }, 6e4);
1113
- try {
1114
- const response = await fetch(url, {
1115
- method: "POST",
1116
- headers: {
1117
- Authorization: `Bearer ${storedToken}`,
1118
- "Content-Type": "application/json"
1119
- },
1120
- signal: controller.signal
1121
- });
1122
- clearTimeout(timeoutId);
1123
- const requestId = response.headers.get("x-routstr-request-id") || void 0;
1124
- if (!response.ok) {
1125
- const errorData = await response.json().catch(() => ({}));
1126
- if (response.status === 400 && errorData?.detail === "No balance to refund") {
1127
- this.storageAdapter.removeToken(baseUrl);
1128
- return {
1129
- success: false,
1130
- requestId,
1131
- error: "No balance to refund"
1132
- };
1133
- }
1134
- return {
1135
- success: false,
1136
- requestId,
1137
- error: `Refund request failed with status ${response.status}: ${errorData?.detail || response.statusText}`
1138
- };
1139
- }
1140
- const data = await response.json();
1141
- console.log("refund rsule", data);
1142
- return {
1143
- success: true,
1144
- token: data.token,
1145
- requestId
1146
- };
1147
- } catch (error) {
1148
- clearTimeout(timeoutId);
1149
- console.error("[BalanceManager._fetchRefundToken] Fetch error", error);
1150
- if (error instanceof Error) {
1151
- if (error.name === "AbortError") {
1152
- return {
1153
- success: false,
1154
- error: "Request timed out after 1 minute"
1155
- };
1156
- }
1157
- return {
1158
- success: false,
1159
- error: error.message
1160
- };
1161
- }
1162
- return {
1163
- success: false,
1164
- error: "Unknown error occurred during refund request"
1165
- };
1166
- }
1167
- }
1168
1042
  /**
1169
1043
  * Post topup request to provider API
1170
1044
  */
@@ -1337,6 +1211,77 @@ var BalanceManager = class {
1337
1211
  }
1338
1212
  };
1339
1213
 
1214
+ // client/usage.ts
1215
+ function extractUsageFromResponseBody(body, fallbackSatsCost = 0) {
1216
+ if (!body || typeof body !== "object") return null;
1217
+ const usage = body.usage;
1218
+ if (!usage || typeof usage !== "object") return null;
1219
+ const promptTokens = Number(usage.prompt_tokens ?? 0);
1220
+ const completionTokens = Number(usage.completion_tokens ?? 0);
1221
+ const totalTokens = Number(usage.total_tokens ?? 0);
1222
+ const costValue = usage.cost;
1223
+ let cost = 0;
1224
+ let satsCost = fallbackSatsCost;
1225
+ if (typeof costValue === "number") {
1226
+ cost = costValue;
1227
+ } else if (costValue && typeof costValue === "object") {
1228
+ const costObj = costValue;
1229
+ const totalUsd = costObj.total_usd;
1230
+ const totalMsats = costObj.total_msats;
1231
+ cost = typeof totalUsd === "number" ? totalUsd : 0;
1232
+ if (typeof totalMsats === "number") {
1233
+ satsCost = totalMsats / 1e3;
1234
+ }
1235
+ }
1236
+ if (promptTokens === 0 && completionTokens === 0 && totalTokens === 0 && cost === 0 && satsCost === 0) {
1237
+ return null;
1238
+ }
1239
+ return {
1240
+ promptTokens,
1241
+ completionTokens,
1242
+ totalTokens,
1243
+ cost,
1244
+ satsCost
1245
+ };
1246
+ }
1247
+ function extractResponseId(body) {
1248
+ if (!body || typeof body !== "object") return void 0;
1249
+ const id = body.id;
1250
+ if (typeof id !== "string") return void 0;
1251
+ const trimmed = id.trim();
1252
+ return trimmed.length > 0 ? trimmed : void 0;
1253
+ }
1254
+ function extractUsageFromSSEJson(parsed, fallbackSatsCost = 0) {
1255
+ if (!parsed || typeof parsed !== "object" || !parsed.usage) {
1256
+ return null;
1257
+ }
1258
+ const usage = parsed.usage;
1259
+ const usageCost = usage.cost;
1260
+ const cost = typeof usageCost === "number" ? usageCost : usageCost?.total_usd ?? parsed.metadata?.routstr?.cost?.total_usd ?? 0;
1261
+ const msats = parsed.metadata?.routstr?.cost?.total_msats ?? (typeof usage.cost_sats === "number" ? usage.cost_sats * 1e3 : 0);
1262
+ const result = {
1263
+ promptTokens: Number(usage.prompt_tokens ?? 0),
1264
+ completionTokens: Number(usage.completion_tokens ?? 0),
1265
+ totalTokens: Number(usage.total_tokens ?? 0),
1266
+ cost: Number(cost ?? 0),
1267
+ satsCost: msats > 0 ? msats / 1e3 : fallbackSatsCost
1268
+ };
1269
+ if (result.promptTokens === 0 && result.completionTokens === 0 && result.totalTokens === 0 && result.cost === 0 && result.satsCost === 0) {
1270
+ return null;
1271
+ }
1272
+ return result;
1273
+ }
1274
+ function toUsageStats(usage) {
1275
+ if (!usage) return void 0;
1276
+ return {
1277
+ total_tokens: usage.totalTokens,
1278
+ prompt_tokens: usage.promptTokens,
1279
+ completion_tokens: usage.completionTokens,
1280
+ cost: usage.cost,
1281
+ sats_cost: usage.satsCost
1282
+ };
1283
+ }
1284
+
1340
1285
  // client/StreamProcessor.ts
1341
1286
  var StreamProcessor = class {
1342
1287
  accumulatedContent = "";
@@ -1364,6 +1309,7 @@ var StreamProcessor = class {
1364
1309
  let finish_reason;
1365
1310
  let citations;
1366
1311
  let annotations;
1312
+ let responseId;
1367
1313
  try {
1368
1314
  while (true) {
1369
1315
  const { done, value } = await reader.read();
@@ -1392,6 +1338,9 @@ var StreamProcessor = class {
1392
1338
  if (parsed.finish_reason) {
1393
1339
  finish_reason = parsed.finish_reason;
1394
1340
  }
1341
+ if (parsed.responseId) {
1342
+ responseId = parsed.responseId;
1343
+ }
1395
1344
  if (parsed.citations) {
1396
1345
  citations = parsed.citations;
1397
1346
  }
@@ -1412,6 +1361,7 @@ var StreamProcessor = class {
1412
1361
  images: this.accumulatedImages.length > 0 ? this.accumulatedImages : void 0,
1413
1362
  usage,
1414
1363
  model,
1364
+ responseId,
1415
1365
  finish_reason,
1416
1366
  citations,
1417
1367
  annotations
@@ -1439,12 +1389,15 @@ var StreamProcessor = class {
1439
1389
  result.reasoning = parsed.choices[0].delta.reasoning;
1440
1390
  }
1441
1391
  if (parsed.usage) {
1442
- result.usage = {
1392
+ result.usage = toUsageStats(extractUsageFromSSEJson(parsed)) ?? {
1443
1393
  total_tokens: parsed.usage.total_tokens,
1444
1394
  prompt_tokens: parsed.usage.prompt_tokens,
1445
1395
  completion_tokens: parsed.usage.completion_tokens
1446
1396
  };
1447
1397
  }
1398
+ if (parsed.id) {
1399
+ result.responseId = parsed.id;
1400
+ }
1448
1401
  if (parsed.model) {
1449
1402
  result.model = parsed.model;
1450
1403
  }
@@ -1836,7 +1789,6 @@ var ProviderManager = class _ProviderManager {
1836
1789
  * Get providers for a model sorted by prompt+completion pricing
1837
1790
  */
1838
1791
  getProviderPriceRankingForModel(modelId, options = {}) {
1839
- const normalizedId = this.normalizeModelId(modelId);
1840
1792
  const includeDisabled = options.includeDisabled ?? false;
1841
1793
  const torMode = options.torMode ?? false;
1842
1794
  const disabledProviders = new Set(
@@ -1850,9 +1802,7 @@ var ProviderManager = class _ProviderManager {
1850
1802
  if (torMode && !baseUrl.includes(".onion")) continue;
1851
1803
  if (!torMode && (baseUrl.includes(".onion") || isInsecureHttpUrl(baseUrl)))
1852
1804
  continue;
1853
- const match = models.find(
1854
- (model) => this.normalizeModelId(model.id) === normalizedId
1855
- );
1805
+ const match = models.find((model) => model.id === modelId);
1856
1806
  if (!match?.sats_pricing) continue;
1857
1807
  const prompt = match.sats_pricing.prompt;
1858
1808
  const completion = match.sats_pricing.completion;
@@ -1965,36 +1915,1311 @@ var ProviderManager = class _ProviderManager {
1965
1915
  }
1966
1916
  };
1967
1917
 
1968
- // client/RoutstrClient.ts
1969
- var TOPUP_MARGIN = 1.2;
1970
- var RoutstrClient = class {
1971
- constructor(walletAdapter, storageAdapter, providerRegistry, alertLevel, mode = "xcashu") {
1972
- this.walletAdapter = walletAdapter;
1973
- this.storageAdapter = storageAdapter;
1974
- this.providerRegistry = providerRegistry;
1975
- this.balanceManager = new BalanceManager(
1976
- walletAdapter,
1977
- storageAdapter,
1978
- providerRegistry
1979
- );
1980
- this.cashuSpender = new CashuSpender(
1981
- walletAdapter,
1982
- storageAdapter,
1983
- providerRegistry,
1984
- this.balanceManager
1985
- );
1986
- this.streamProcessor = new StreamProcessor();
1987
- this.providerManager = new ProviderManager(providerRegistry);
1988
- this.alertLevel = alertLevel;
1989
- this.mode = mode;
1918
+ // storage/drivers/localStorage.ts
1919
+ var canUseLocalStorage = () => {
1920
+ return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
1921
+ };
1922
+ var isQuotaExceeded = (error) => {
1923
+ const e = error;
1924
+ return !!e && (e?.name === "QuotaExceededError" || e?.code === 22 || e?.code === 1014);
1925
+ };
1926
+ var NON_CRITICAL_KEYS = /* @__PURE__ */ new Set(["modelsFromAllProviders"]);
1927
+ var localStorageDriver = {
1928
+ async getItem(key, defaultValue) {
1929
+ if (!canUseLocalStorage()) return defaultValue;
1930
+ try {
1931
+ const item = window.localStorage.getItem(key);
1932
+ if (item === null) return defaultValue;
1933
+ try {
1934
+ return JSON.parse(item);
1935
+ } catch (parseError) {
1936
+ if (typeof defaultValue === "string") {
1937
+ return item;
1938
+ }
1939
+ throw parseError;
1940
+ }
1941
+ } catch (error) {
1942
+ console.error(`Error retrieving item with key "${key}":`, error);
1943
+ if (canUseLocalStorage()) {
1944
+ try {
1945
+ window.localStorage.removeItem(key);
1946
+ } catch (removeError) {
1947
+ console.error(
1948
+ `Error removing corrupted item with key "${key}":`,
1949
+ removeError
1950
+ );
1951
+ }
1952
+ }
1953
+ return defaultValue;
1954
+ }
1955
+ },
1956
+ async setItem(key, value) {
1957
+ if (!canUseLocalStorage()) return;
1958
+ try {
1959
+ window.localStorage.setItem(key, JSON.stringify(value));
1960
+ } catch (error) {
1961
+ if (isQuotaExceeded(error)) {
1962
+ if (NON_CRITICAL_KEYS.has(key)) {
1963
+ console.warn(
1964
+ `Storage quota exceeded; skipping non-critical key "${key}".`
1965
+ );
1966
+ return;
1967
+ }
1968
+ try {
1969
+ window.localStorage.removeItem("modelsFromAllProviders");
1970
+ } catch {
1971
+ }
1972
+ try {
1973
+ window.localStorage.setItem(key, JSON.stringify(value));
1974
+ return;
1975
+ } catch (retryError) {
1976
+ console.warn(
1977
+ `Storage quota exceeded; unable to persist key "${key}" after cleanup attempt.`,
1978
+ retryError
1979
+ );
1980
+ return;
1981
+ }
1982
+ }
1983
+ console.error(`Error storing item with key "${key}":`, error);
1984
+ }
1985
+ },
1986
+ async removeItem(key) {
1987
+ if (!canUseLocalStorage()) return;
1988
+ try {
1989
+ window.localStorage.removeItem(key);
1990
+ } catch (error) {
1991
+ console.error(`Error removing item with key "${key}":`, error);
1992
+ }
1990
1993
  }
1991
- cashuSpender;
1992
- balanceManager;
1993
- streamProcessor;
1994
+ };
1995
+
1996
+ // storage/drivers/memory.ts
1997
+ var createMemoryDriver = (seed) => {
1998
+ const store = /* @__PURE__ */ new Map();
1999
+ return {
2000
+ async getItem(key, defaultValue) {
2001
+ const item = store.get(key);
2002
+ if (item === void 0) return defaultValue;
2003
+ try {
2004
+ return JSON.parse(item);
2005
+ } catch (parseError) {
2006
+ if (typeof defaultValue === "string") {
2007
+ return item;
2008
+ }
2009
+ throw parseError;
2010
+ }
2011
+ },
2012
+ async setItem(key, value) {
2013
+ store.set(key, JSON.stringify(value));
2014
+ },
2015
+ async removeItem(key) {
2016
+ store.delete(key);
2017
+ }
2018
+ };
2019
+ };
2020
+
2021
+ // storage/drivers/sqlite.ts
2022
+ var isBun = () => {
2023
+ return typeof process.versions.bun !== "undefined";
2024
+ };
2025
+ var cachedDbModule = null;
2026
+ var loadDatabase = async (dbPath) => {
2027
+ if (isBun()) {
2028
+ throw new Error(
2029
+ "SQLite driver not supported in Bun. Use createBunSqliteDriver() instead."
2030
+ );
2031
+ }
2032
+ try {
2033
+ if (!cachedDbModule) {
2034
+ cachedDbModule = (await import('better-sqlite3')).default;
2035
+ }
2036
+ return new cachedDbModule(dbPath);
2037
+ } catch (error) {
2038
+ throw new Error(
2039
+ `better-sqlite3 is required for sqlite storage. Install it to use sqlite storage. (${error})`
2040
+ );
2041
+ }
2042
+ };
2043
+ var createSqliteDriver = (options = {}) => {
2044
+ const dbPath = options.dbPath || "routstr.sqlite";
2045
+ const tableName = options.tableName || "sdk_storage";
2046
+ let db;
2047
+ let selectStmt;
2048
+ let upsertStmt;
2049
+ let deleteStmt;
2050
+ const initDb = async () => {
2051
+ if (!db) {
2052
+ db = await loadDatabase(dbPath);
2053
+ db.exec(
2054
+ `CREATE TABLE IF NOT EXISTS ${tableName} (key TEXT PRIMARY KEY, value TEXT NOT NULL)`
2055
+ );
2056
+ selectStmt = db.prepare(`SELECT value FROM ${tableName} WHERE key = ?`);
2057
+ upsertStmt = db.prepare(
2058
+ `INSERT INTO ${tableName} (key, value) VALUES (?, ?)
2059
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`
2060
+ );
2061
+ deleteStmt = db.prepare(`DELETE FROM ${tableName} WHERE key = ?`);
2062
+ }
2063
+ };
2064
+ const ensureInit = async () => {
2065
+ if (!db) {
2066
+ await initDb();
2067
+ }
2068
+ };
2069
+ return {
2070
+ async getItem(key, defaultValue) {
2071
+ try {
2072
+ await ensureInit();
2073
+ const row = selectStmt.get(key);
2074
+ if (!row || typeof row.value !== "string") return defaultValue;
2075
+ try {
2076
+ return JSON.parse(row.value);
2077
+ } catch (parseError) {
2078
+ if (typeof defaultValue === "string") {
2079
+ return row.value;
2080
+ }
2081
+ throw parseError;
2082
+ }
2083
+ } catch (error) {
2084
+ console.error(`SQLite getItem failed for key "${key}":`, error);
2085
+ return defaultValue;
2086
+ }
2087
+ },
2088
+ async setItem(key, value) {
2089
+ try {
2090
+ await ensureInit();
2091
+ upsertStmt.run(key, JSON.stringify(value));
2092
+ } catch (error) {
2093
+ console.error(`SQLite setItem failed for key "${key}":`, error);
2094
+ }
2095
+ },
2096
+ async removeItem(key) {
2097
+ try {
2098
+ await ensureInit();
2099
+ deleteStmt.run(key);
2100
+ } catch (error) {
2101
+ console.error(`SQLite removeItem failed for key "${key}":`, error);
2102
+ }
2103
+ }
2104
+ };
2105
+ };
2106
+
2107
+ // storage/keys.ts
2108
+ var SDK_STORAGE_KEYS = {
2109
+ MODELS_FROM_ALL_PROVIDERS: "modelsFromAllProviders",
2110
+ LAST_USED_MODEL: "lastUsedModel",
2111
+ BASE_URLS_LIST: "base_urls_list",
2112
+ DISABLED_PROVIDERS: "disabled_providers",
2113
+ MINTS_FROM_ALL_PROVIDERS: "mints_from_all_providers",
2114
+ INFO_FROM_ALL_PROVIDERS: "info_from_all_providers",
2115
+ LAST_MODELS_UPDATE: "lastModelsUpdate",
2116
+ LAST_BASE_URLS_UPDATE: "lastBaseUrlsUpdate",
2117
+ API_KEYS: "api_keys",
2118
+ CHILD_KEYS: "child_keys",
2119
+ XCASHU_TOKENS: "xcashu_tokens",
2120
+ ROUTSTR21_MODELS: "routstr21Models",
2121
+ LAST_ROUTSTR21_MODELS_UPDATE: "lastRoutstr21ModelsUpdate",
2122
+ CACHED_RECEIVE_TOKENS: "cached_receive_tokens",
2123
+ USAGE_TRACKING: "usage_tracking",
2124
+ CLIENT_IDS: "client_ids"
2125
+ };
2126
+
2127
+ // storage/usageTracking/indexedDB.ts
2128
+ var DEFAULT_DB_NAME = "routstr-sdk";
2129
+ var DEFAULT_STORE_NAME = "usage_tracking";
2130
+ var MIGRATION_MARKER_KEY = "usage_tracking_migration_v1";
2131
+ var isBrowser = typeof indexedDB !== "undefined";
2132
+ var normalizeBaseUrl = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
2133
+ var openDatabase = (dbName, storeName) => {
2134
+ if (!isBrowser) {
2135
+ return Promise.reject(new Error("IndexedDB is not available"));
2136
+ }
2137
+ return new Promise((resolve, reject) => {
2138
+ const request = indexedDB.open(dbName, 1);
2139
+ request.onupgradeneeded = () => {
2140
+ const db = request.result;
2141
+ if (!db.objectStoreNames.contains(storeName)) {
2142
+ const store = db.createObjectStore(storeName, { keyPath: "id" });
2143
+ store.createIndex("timestamp", "timestamp", { unique: false });
2144
+ store.createIndex("modelId", "modelId", { unique: false });
2145
+ store.createIndex("baseUrl", "baseUrl", { unique: false });
2146
+ store.createIndex("sessionId", "sessionId", { unique: false });
2147
+ store.createIndex("client", "client", { unique: false });
2148
+ }
2149
+ };
2150
+ request.onsuccess = () => resolve(request.result);
2151
+ request.onerror = () => reject(request.error);
2152
+ });
2153
+ };
2154
+ var matchesFilters = (entry, options = {}) => {
2155
+ if (typeof options.before === "number" && entry.timestamp >= options.before) {
2156
+ return false;
2157
+ }
2158
+ if (typeof options.after === "number" && entry.timestamp <= options.after) {
2159
+ return false;
2160
+ }
2161
+ if (options.modelId && entry.modelId !== options.modelId) {
2162
+ return false;
2163
+ }
2164
+ if (options.baseUrl && normalizeBaseUrl(entry.baseUrl) !== normalizeBaseUrl(options.baseUrl)) {
2165
+ return false;
2166
+ }
2167
+ if (options.sessionId && entry.sessionId !== options.sessionId) {
2168
+ return false;
2169
+ }
2170
+ if (options.client && entry.client !== options.client) {
2171
+ return false;
2172
+ }
2173
+ return true;
2174
+ };
2175
+ var createIndexedDBUsageTrackingDriver = (options = {}) => {
2176
+ const dbName = options.dbName || DEFAULT_DB_NAME;
2177
+ const storeName = options.storeName || DEFAULT_STORE_NAME;
2178
+ const legacyStorageDriver = options.legacyStorageDriver;
2179
+ let dbPromise = null;
2180
+ let migrationPromise = null;
2181
+ const getDb = () => {
2182
+ if (!dbPromise) {
2183
+ dbPromise = openDatabase(dbName, storeName);
2184
+ }
2185
+ return dbPromise;
2186
+ };
2187
+ const putMany = async (entries) => {
2188
+ if (entries.length === 0) return;
2189
+ const db = await getDb();
2190
+ await new Promise((resolve, reject) => {
2191
+ const tx = db.transaction(storeName, "readwrite");
2192
+ const store = tx.objectStore(storeName);
2193
+ for (const entry of entries) {
2194
+ store.put({ ...entry, baseUrl: normalizeBaseUrl(entry.baseUrl) });
2195
+ }
2196
+ tx.oncomplete = () => resolve();
2197
+ tx.onerror = () => reject(tx.error);
2198
+ });
2199
+ };
2200
+ const ensureMigrated = async () => {
2201
+ if (!legacyStorageDriver) return;
2202
+ if (!migrationPromise) {
2203
+ migrationPromise = (async () => {
2204
+ const migrated = await legacyStorageDriver.getItem(
2205
+ MIGRATION_MARKER_KEY,
2206
+ false
2207
+ );
2208
+ if (migrated) return;
2209
+ const legacyEntries = await legacyStorageDriver.getItem(
2210
+ SDK_STORAGE_KEYS.USAGE_TRACKING,
2211
+ []
2212
+ );
2213
+ if (legacyEntries.length > 0) {
2214
+ await putMany(legacyEntries);
2215
+ await legacyStorageDriver.removeItem(SDK_STORAGE_KEYS.USAGE_TRACKING);
2216
+ }
2217
+ await legacyStorageDriver.setItem(MIGRATION_MARKER_KEY, true);
2218
+ })();
2219
+ }
2220
+ await migrationPromise;
2221
+ };
2222
+ return {
2223
+ async migrate() {
2224
+ await ensureMigrated();
2225
+ },
2226
+ async append(entry) {
2227
+ await ensureMigrated();
2228
+ await putMany([entry]);
2229
+ },
2230
+ async appendMany(entries) {
2231
+ await ensureMigrated();
2232
+ await putMany(entries);
2233
+ },
2234
+ async list(options2 = {}) {
2235
+ await ensureMigrated();
2236
+ const db = await getDb();
2237
+ return new Promise((resolve, reject) => {
2238
+ const tx = db.transaction(storeName, "readonly");
2239
+ const store = tx.objectStore(storeName);
2240
+ const index = store.index("timestamp");
2241
+ const direction = "prev";
2242
+ const request = index.openCursor(null, direction);
2243
+ const results = [];
2244
+ const limit = options2.limit;
2245
+ request.onsuccess = () => {
2246
+ const cursor = request.result;
2247
+ if (!cursor) {
2248
+ resolve(results);
2249
+ return;
2250
+ }
2251
+ const value = cursor.value;
2252
+ if (matchesFilters(value, options2)) {
2253
+ results.push(value);
2254
+ if (typeof limit === "number" && results.length >= limit) {
2255
+ resolve(results);
2256
+ return;
2257
+ }
2258
+ }
2259
+ cursor.continue();
2260
+ };
2261
+ request.onerror = () => reject(request.error);
2262
+ });
2263
+ },
2264
+ async count(options2 = {}) {
2265
+ const results = await this.list(options2);
2266
+ return results.length;
2267
+ },
2268
+ async deleteOlderThan(timestamp) {
2269
+ await ensureMigrated();
2270
+ const db = await getDb();
2271
+ return new Promise((resolve, reject) => {
2272
+ const tx = db.transaction(storeName, "readwrite");
2273
+ const store = tx.objectStore(storeName);
2274
+ const index = store.index("timestamp");
2275
+ const range = IDBKeyRange.upperBound(timestamp, true);
2276
+ const request = index.openCursor(range);
2277
+ let deleted = 0;
2278
+ request.onsuccess = () => {
2279
+ const cursor = request.result;
2280
+ if (!cursor) {
2281
+ resolve(deleted);
2282
+ return;
2283
+ }
2284
+ deleted += 1;
2285
+ cursor.delete();
2286
+ cursor.continue();
2287
+ };
2288
+ request.onerror = () => reject(request.error);
2289
+ });
2290
+ },
2291
+ async clear() {
2292
+ await ensureMigrated();
2293
+ const db = await getDb();
2294
+ await new Promise((resolve, reject) => {
2295
+ const tx = db.transaction(storeName, "readwrite");
2296
+ tx.objectStore(storeName).clear();
2297
+ tx.oncomplete = () => resolve();
2298
+ tx.onerror = () => reject(tx.error);
2299
+ });
2300
+ }
2301
+ };
2302
+ };
2303
+
2304
+ // storage/usageTracking/sqlite.ts
2305
+ var MIGRATION_MARKER_KEY2 = "usage_tracking_migration_v1";
2306
+ var normalizeBaseUrl2 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
2307
+ var isBun2 = () => {
2308
+ return typeof process.versions.bun !== "undefined";
2309
+ };
2310
+ var cachedDbModule2 = null;
2311
+ var loadDatabase2 = async (dbPath) => {
2312
+ if (isBun2()) {
2313
+ throw new Error(
2314
+ "SQLite driver not supported in Bun. Use createMemoryDriver() instead."
2315
+ );
2316
+ }
2317
+ try {
2318
+ if (!cachedDbModule2) {
2319
+ cachedDbModule2 = (await import('better-sqlite3')).default;
2320
+ }
2321
+ return new cachedDbModule2(dbPath);
2322
+ } catch (error) {
2323
+ throw new Error(
2324
+ `better-sqlite3 is required for sqlite usage tracking. Install it to use sqlite storage. (${error})`
2325
+ );
2326
+ }
2327
+ };
2328
+ var buildWhereClause = (options = {}) => {
2329
+ const clauses = [];
2330
+ const params = [];
2331
+ if (typeof options.before === "number") {
2332
+ clauses.push("timestamp < ?");
2333
+ params.push(options.before);
2334
+ }
2335
+ if (typeof options.after === "number") {
2336
+ clauses.push("timestamp > ?");
2337
+ params.push(options.after);
2338
+ }
2339
+ if (options.modelId) {
2340
+ clauses.push("model_id = ?");
2341
+ params.push(options.modelId);
2342
+ }
2343
+ if (options.baseUrl) {
2344
+ clauses.push("base_url = ?");
2345
+ params.push(normalizeBaseUrl2(options.baseUrl));
2346
+ }
2347
+ if (options.sessionId) {
2348
+ clauses.push("session_id = ?");
2349
+ params.push(options.sessionId);
2350
+ }
2351
+ if (options.client) {
2352
+ clauses.push("client = ?");
2353
+ params.push(options.client);
2354
+ }
2355
+ return {
2356
+ sql: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
2357
+ params
2358
+ };
2359
+ };
2360
+ var createSqliteUsageTrackingDriver = (options = {}) => {
2361
+ const dbPath = options.dbPath || "routstr.sqlite";
2362
+ const tableName = options.tableName || "usage_tracking";
2363
+ const legacyStorageDriver = options.legacyStorageDriver;
2364
+ let db;
2365
+ let insertStmt;
2366
+ let migrationComplete = false;
2367
+ const initDb = async () => {
2368
+ if (!db) {
2369
+ db = await loadDatabase2(dbPath);
2370
+ db.exec(`
2371
+ CREATE TABLE IF NOT EXISTS ${tableName} (
2372
+ id TEXT PRIMARY KEY,
2373
+ timestamp INTEGER NOT NULL,
2374
+ model_id TEXT NOT NULL,
2375
+ base_url TEXT NOT NULL,
2376
+ request_id TEXT NOT NULL,
2377
+ cost REAL NOT NULL,
2378
+ sats_cost REAL NOT NULL,
2379
+ prompt_tokens INTEGER NOT NULL,
2380
+ completion_tokens INTEGER NOT NULL,
2381
+ total_tokens INTEGER NOT NULL,
2382
+ client TEXT,
2383
+ session_id TEXT,
2384
+ tags TEXT
2385
+ );
2386
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_timestamp ON ${tableName}(timestamp);
2387
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_model_id ON ${tableName}(model_id);
2388
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_base_url ON ${tableName}(base_url);
2389
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_session_id ON ${tableName}(session_id);
2390
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_client ON ${tableName}(client);
2391
+ `);
2392
+ insertStmt = db.prepare(`
2393
+ INSERT OR REPLACE INTO ${tableName} (
2394
+ id, timestamp, model_id, base_url, request_id,
2395
+ cost, sats_cost, prompt_tokens, completion_tokens, total_tokens,
2396
+ client, session_id, tags
2397
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2398
+ `);
2399
+ }
2400
+ };
2401
+ const ensureInit = async () => {
2402
+ if (!db) {
2403
+ await initDb();
2404
+ }
2405
+ };
2406
+ const appendOne = (entry) => {
2407
+ insertStmt.run(
2408
+ entry.id,
2409
+ entry.timestamp,
2410
+ entry.modelId,
2411
+ normalizeBaseUrl2(entry.baseUrl),
2412
+ entry.requestId,
2413
+ entry.cost,
2414
+ entry.satsCost,
2415
+ entry.promptTokens,
2416
+ entry.completionTokens,
2417
+ entry.totalTokens,
2418
+ entry.client ?? null,
2419
+ entry.sessionId ?? null,
2420
+ JSON.stringify(entry.tags ?? [])
2421
+ );
2422
+ };
2423
+ const ensureMigrated = async () => {
2424
+ if (!legacyStorageDriver || migrationComplete) return;
2425
+ const migrated = await legacyStorageDriver.getItem(
2426
+ MIGRATION_MARKER_KEY2,
2427
+ false
2428
+ );
2429
+ if (migrated) {
2430
+ migrationComplete = true;
2431
+ return;
2432
+ }
2433
+ const legacyEntries = await legacyStorageDriver.getItem(
2434
+ SDK_STORAGE_KEYS.USAGE_TRACKING,
2435
+ []
2436
+ );
2437
+ for (const entry of legacyEntries) {
2438
+ appendOne(entry);
2439
+ }
2440
+ if (legacyEntries.length > 0) {
2441
+ await legacyStorageDriver.removeItem(SDK_STORAGE_KEYS.USAGE_TRACKING);
2442
+ }
2443
+ await legacyStorageDriver.setItem(MIGRATION_MARKER_KEY2, true);
2444
+ migrationComplete = true;
2445
+ };
2446
+ const mapRow = (row) => ({
2447
+ id: row.id,
2448
+ timestamp: row.timestamp,
2449
+ modelId: row.model_id,
2450
+ baseUrl: row.base_url,
2451
+ requestId: row.request_id,
2452
+ cost: row.cost,
2453
+ satsCost: row.sats_cost,
2454
+ promptTokens: row.prompt_tokens,
2455
+ completionTokens: row.completion_tokens,
2456
+ totalTokens: row.total_tokens,
2457
+ client: row.client ?? void 0,
2458
+ sessionId: row.session_id ?? void 0,
2459
+ tags: typeof row.tags === "string" ? JSON.parse(row.tags) : void 0
2460
+ });
2461
+ return {
2462
+ async migrate() {
2463
+ await ensureInit();
2464
+ await ensureMigrated();
2465
+ },
2466
+ async append(entry) {
2467
+ await ensureInit();
2468
+ await ensureMigrated();
2469
+ appendOne(entry);
2470
+ },
2471
+ async appendMany(entries) {
2472
+ await ensureInit();
2473
+ await ensureMigrated();
2474
+ for (const entry of entries) {
2475
+ appendOne(entry);
2476
+ }
2477
+ },
2478
+ async list(options2 = {}) {
2479
+ await ensureInit();
2480
+ await ensureMigrated();
2481
+ const { sql, params } = buildWhereClause(options2);
2482
+ const limitSql = typeof options2.limit === "number" ? " LIMIT ?" : "";
2483
+ const stmt = db.prepare(
2484
+ `SELECT * FROM ${tableName} ${sql} ORDER BY timestamp DESC${limitSql}`
2485
+ );
2486
+ const rows = stmt.all(
2487
+ ...typeof options2.limit === "number" ? [...params, options2.limit] : params
2488
+ );
2489
+ return rows.map(mapRow);
2490
+ },
2491
+ async count(options2 = {}) {
2492
+ await ensureInit();
2493
+ await ensureMigrated();
2494
+ const { sql, params } = buildWhereClause(options2);
2495
+ const stmt = db.prepare(`SELECT COUNT(*) as count FROM ${tableName} ${sql}`);
2496
+ const row = stmt.get(...params);
2497
+ return Number(row?.count ?? 0);
2498
+ },
2499
+ async deleteOlderThan(timestamp) {
2500
+ await ensureInit();
2501
+ await ensureMigrated();
2502
+ const stmt = db.prepare(`DELETE FROM ${tableName} WHERE timestamp < ?`);
2503
+ const result = stmt.run(timestamp);
2504
+ return result.changes;
2505
+ },
2506
+ async clear() {
2507
+ await ensureInit();
2508
+ await ensureMigrated();
2509
+ db.prepare(`DELETE FROM ${tableName}`).run();
2510
+ }
2511
+ };
2512
+ };
2513
+
2514
+ // storage/usageTracking/bunSqlite.ts
2515
+ var MIGRATION_MARKER_KEY3 = "usage_tracking_migration_v1";
2516
+ var normalizeBaseUrl3 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
2517
+ var buildWhereClause2 = (options = {}) => {
2518
+ const clauses = [];
2519
+ const params = [];
2520
+ if (typeof options.before === "number") {
2521
+ clauses.push("timestamp < ?");
2522
+ params.push(options.before);
2523
+ }
2524
+ if (typeof options.after === "number") {
2525
+ clauses.push("timestamp > ?");
2526
+ params.push(options.after);
2527
+ }
2528
+ if (options.modelId) {
2529
+ clauses.push("model_id = ?");
2530
+ params.push(options.modelId);
2531
+ }
2532
+ if (options.baseUrl) {
2533
+ clauses.push("base_url = ?");
2534
+ params.push(normalizeBaseUrl3(options.baseUrl));
2535
+ }
2536
+ if (options.sessionId) {
2537
+ clauses.push("session_id = ?");
2538
+ params.push(options.sessionId);
2539
+ }
2540
+ if (options.client) {
2541
+ clauses.push("client = ?");
2542
+ params.push(options.client);
2543
+ }
2544
+ return {
2545
+ sql: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
2546
+ params
2547
+ };
2548
+ };
2549
+ var createBunSqliteUsageTrackingDriver = (options = {}) => {
2550
+ const dbPath = options.dbPath || "routstr.sqlite";
2551
+ const tableName = options.tableName || "usage_tracking";
2552
+ const legacyStorageDriver = options.legacyStorageDriver;
2553
+ const SQLiteDatabase = options.sqlite?.Database;
2554
+ let migrationPromise = null;
2555
+ if (!SQLiteDatabase) {
2556
+ throw new Error(
2557
+ "Bun SQLite Database constructor is required. Pass { sqlite: { Database } } when creating the driver."
2558
+ );
2559
+ }
2560
+ const db = new SQLiteDatabase(dbPath);
2561
+ db.run(`
2562
+ CREATE TABLE IF NOT EXISTS ${tableName} (
2563
+ id TEXT PRIMARY KEY,
2564
+ timestamp INTEGER NOT NULL,
2565
+ model_id TEXT NOT NULL,
2566
+ base_url TEXT NOT NULL,
2567
+ request_id TEXT NOT NULL,
2568
+ cost REAL NOT NULL,
2569
+ sats_cost REAL NOT NULL,
2570
+ prompt_tokens INTEGER NOT NULL,
2571
+ completion_tokens INTEGER NOT NULL,
2572
+ total_tokens INTEGER NOT NULL,
2573
+ client TEXT,
2574
+ session_id TEXT,
2575
+ tags TEXT
2576
+ )
2577
+ `);
2578
+ db.run(`CREATE INDEX IF NOT EXISTS idx_${tableName}_timestamp ON ${tableName}(timestamp)`);
2579
+ db.run(`CREATE INDEX IF NOT EXISTS idx_${tableName}_model_id ON ${tableName}(model_id)`);
2580
+ db.run(`CREATE INDEX IF NOT EXISTS idx_${tableName}_base_url ON ${tableName}(base_url)`);
2581
+ const appendOne = (entry) => {
2582
+ db.query(`
2583
+ INSERT OR REPLACE INTO ${tableName} (
2584
+ id, timestamp, model_id, base_url, request_id,
2585
+ cost, sats_cost, prompt_tokens, completion_tokens, total_tokens,
2586
+ client, session_id, tags
2587
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2588
+ `).run(
2589
+ entry.id,
2590
+ entry.timestamp,
2591
+ entry.modelId,
2592
+ normalizeBaseUrl3(entry.baseUrl),
2593
+ entry.requestId,
2594
+ entry.cost,
2595
+ entry.satsCost,
2596
+ entry.promptTokens,
2597
+ entry.completionTokens,
2598
+ entry.totalTokens,
2599
+ entry.client ?? null,
2600
+ entry.sessionId ?? null,
2601
+ JSON.stringify(entry.tags ?? [])
2602
+ );
2603
+ };
2604
+ const mapRow = (row) => ({
2605
+ id: row.id,
2606
+ timestamp: row.timestamp,
2607
+ modelId: row.model_id,
2608
+ baseUrl: row.base_url,
2609
+ requestId: row.request_id,
2610
+ cost: row.cost,
2611
+ satsCost: row.sats_cost,
2612
+ promptTokens: row.prompt_tokens,
2613
+ completionTokens: row.completion_tokens,
2614
+ totalTokens: row.total_tokens,
2615
+ client: row.client ?? void 0,
2616
+ sessionId: row.session_id ?? void 0,
2617
+ tags: typeof row.tags === "string" ? JSON.parse(row.tags) : void 0
2618
+ });
2619
+ const ensureMigrated = async () => {
2620
+ if (!legacyStorageDriver) return;
2621
+ if (!migrationPromise) {
2622
+ migrationPromise = (async () => {
2623
+ const migrated = await legacyStorageDriver.getItem(
2624
+ MIGRATION_MARKER_KEY3,
2625
+ false
2626
+ );
2627
+ if (migrated) return;
2628
+ const legacyEntries = await legacyStorageDriver.getItem(
2629
+ SDK_STORAGE_KEYS.USAGE_TRACKING,
2630
+ []
2631
+ );
2632
+ if (legacyEntries.length > 0) {
2633
+ for (const entry of legacyEntries) {
2634
+ appendOne(entry);
2635
+ }
2636
+ await legacyStorageDriver.removeItem(SDK_STORAGE_KEYS.USAGE_TRACKING);
2637
+ }
2638
+ await legacyStorageDriver.setItem(MIGRATION_MARKER_KEY3, true);
2639
+ })();
2640
+ }
2641
+ await migrationPromise;
2642
+ };
2643
+ return {
2644
+ async migrate() {
2645
+ await ensureMigrated();
2646
+ },
2647
+ async append(entry) {
2648
+ await ensureMigrated();
2649
+ appendOne(entry);
2650
+ },
2651
+ async appendMany(entries) {
2652
+ await ensureMigrated();
2653
+ for (const entry of entries) {
2654
+ appendOne(entry);
2655
+ }
2656
+ },
2657
+ async list(options2 = {}) {
2658
+ await ensureMigrated();
2659
+ const { sql, params } = buildWhereClause2(options2);
2660
+ const limitSql = typeof options2.limit === "number" ? " LIMIT ?" : "";
2661
+ const query = `SELECT * FROM ${tableName} ${sql} ORDER BY timestamp DESC${limitSql}`;
2662
+ let rows;
2663
+ if (typeof options2.limit === "number") {
2664
+ rows = db.query(query).all(...params, options2.limit);
2665
+ } else {
2666
+ rows = db.query(query).all(...params);
2667
+ }
2668
+ return rows.map(mapRow);
2669
+ },
2670
+ async count(options2 = {}) {
2671
+ const { sql, params } = buildWhereClause2(options2);
2672
+ const query = `SELECT COUNT(*) as count FROM ${tableName} ${sql}`;
2673
+ const row = db.query(query).get(...params);
2674
+ return Number(row?.count ?? 0);
2675
+ },
2676
+ async deleteOlderThan(timestamp) {
2677
+ await ensureMigrated();
2678
+ const before = timestamp;
2679
+ const result = db.query(`DELETE FROM ${tableName} WHERE timestamp < ?`).run(before);
2680
+ return result.changes ?? 0;
2681
+ },
2682
+ async clear() {
2683
+ await ensureMigrated();
2684
+ db.query(`DELETE FROM ${tableName}`).run();
2685
+ }
2686
+ };
2687
+ };
2688
+
2689
+ // storage/usageTracking/memory.ts
2690
+ var normalizeBaseUrl4 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
2691
+ var matchesFilters2 = (entry, options = {}) => {
2692
+ if (typeof options.before === "number" && entry.timestamp >= options.before) {
2693
+ return false;
2694
+ }
2695
+ if (typeof options.after === "number" && entry.timestamp <= options.after) {
2696
+ return false;
2697
+ }
2698
+ if (options.modelId && entry.modelId !== options.modelId) {
2699
+ return false;
2700
+ }
2701
+ if (options.baseUrl && normalizeBaseUrl4(entry.baseUrl) !== normalizeBaseUrl4(options.baseUrl)) {
2702
+ return false;
2703
+ }
2704
+ if (options.sessionId && entry.sessionId !== options.sessionId) {
2705
+ return false;
2706
+ }
2707
+ if (options.client && entry.client !== options.client) {
2708
+ return false;
2709
+ }
2710
+ return true;
2711
+ };
2712
+ var createMemoryUsageTrackingDriver = (seed = []) => {
2713
+ const store = /* @__PURE__ */ new Map();
2714
+ for (const entry of seed) {
2715
+ store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl4(entry.baseUrl) });
2716
+ }
2717
+ return {
2718
+ async migrate() {
2719
+ return;
2720
+ },
2721
+ async append(entry) {
2722
+ store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl4(entry.baseUrl) });
2723
+ },
2724
+ async appendMany(entries) {
2725
+ for (const entry of entries) {
2726
+ store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl4(entry.baseUrl) });
2727
+ }
2728
+ },
2729
+ async list(options = {}) {
2730
+ const entries = [...store.values()].filter((entry) => matchesFilters2(entry, options)).sort((a, b) => b.timestamp - a.timestamp);
2731
+ if (typeof options.limit === "number") {
2732
+ return entries.slice(0, options.limit);
2733
+ }
2734
+ return entries;
2735
+ },
2736
+ async count(options = {}) {
2737
+ return (await this.list(options)).length;
2738
+ },
2739
+ async deleteOlderThan(timestamp) {
2740
+ let deleted = 0;
2741
+ for (const [id, entry] of store.entries()) {
2742
+ if (entry.timestamp < timestamp) {
2743
+ store.delete(id);
2744
+ deleted += 1;
2745
+ }
2746
+ }
2747
+ return deleted;
2748
+ },
2749
+ async clear() {
2750
+ store.clear();
2751
+ }
2752
+ };
2753
+ };
2754
+ var normalizeBaseUrl5 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
2755
+ var createEmptyStore = (driver) => createStore((set, get) => ({
2756
+ modelsFromAllProviders: {},
2757
+ lastUsedModel: null,
2758
+ baseUrlsList: [],
2759
+ lastBaseUrlsUpdate: null,
2760
+ disabledProviders: [],
2761
+ mintsFromAllProviders: {},
2762
+ infoFromAllProviders: {},
2763
+ lastModelsUpdate: {},
2764
+ apiKeys: [],
2765
+ childKeys: [],
2766
+ xcashuTokens: {},
2767
+ routstr21Models: [],
2768
+ lastRoutstr21ModelsUpdate: null,
2769
+ cachedReceiveTokens: [],
2770
+ clientIds: [],
2771
+ setModelsFromAllProviders: (value) => {
2772
+ const normalized = {};
2773
+ for (const [baseUrl, models] of Object.entries(value)) {
2774
+ normalized[normalizeBaseUrl5(baseUrl)] = models;
2775
+ }
2776
+ void driver.setItem(
2777
+ SDK_STORAGE_KEYS.MODELS_FROM_ALL_PROVIDERS,
2778
+ normalized
2779
+ );
2780
+ set({ modelsFromAllProviders: normalized });
2781
+ },
2782
+ setLastUsedModel: (value) => {
2783
+ void driver.setItem(SDK_STORAGE_KEYS.LAST_USED_MODEL, value);
2784
+ set({ lastUsedModel: value });
2785
+ },
2786
+ setBaseUrlsList: (value) => {
2787
+ const normalized = value.map((url) => normalizeBaseUrl5(url));
2788
+ void driver.setItem(SDK_STORAGE_KEYS.BASE_URLS_LIST, normalized);
2789
+ set({ baseUrlsList: normalized });
2790
+ },
2791
+ setBaseUrlsLastUpdate: (value) => {
2792
+ void driver.setItem(SDK_STORAGE_KEYS.LAST_BASE_URLS_UPDATE, value);
2793
+ set({ lastBaseUrlsUpdate: value });
2794
+ },
2795
+ setDisabledProviders: (value) => {
2796
+ const normalized = value.map((url) => normalizeBaseUrl5(url));
2797
+ void driver.setItem(SDK_STORAGE_KEYS.DISABLED_PROVIDERS, normalized);
2798
+ set({ disabledProviders: normalized });
2799
+ },
2800
+ setMintsFromAllProviders: (value) => {
2801
+ const normalized = {};
2802
+ for (const [baseUrl, mints] of Object.entries(value)) {
2803
+ normalized[normalizeBaseUrl5(baseUrl)] = mints.map(
2804
+ (mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint
2805
+ );
2806
+ }
2807
+ void driver.setItem(
2808
+ SDK_STORAGE_KEYS.MINTS_FROM_ALL_PROVIDERS,
2809
+ normalized
2810
+ );
2811
+ set({ mintsFromAllProviders: normalized });
2812
+ },
2813
+ setInfoFromAllProviders: (value) => {
2814
+ const normalized = {};
2815
+ for (const [baseUrl, info] of Object.entries(value)) {
2816
+ normalized[normalizeBaseUrl5(baseUrl)] = info;
2817
+ }
2818
+ void driver.setItem(SDK_STORAGE_KEYS.INFO_FROM_ALL_PROVIDERS, normalized);
2819
+ set({ infoFromAllProviders: normalized });
2820
+ },
2821
+ setLastModelsUpdate: (value) => {
2822
+ const normalized = {};
2823
+ for (const [baseUrl, timestamp] of Object.entries(value)) {
2824
+ normalized[normalizeBaseUrl5(baseUrl)] = timestamp;
2825
+ }
2826
+ void driver.setItem(SDK_STORAGE_KEYS.LAST_MODELS_UPDATE, normalized);
2827
+ set({ lastModelsUpdate: normalized });
2828
+ },
2829
+ setApiKeys: (value) => {
2830
+ set((state) => {
2831
+ const updates = typeof value === "function" ? value(state.apiKeys) : value;
2832
+ const normalized = updates.map((entry) => ({
2833
+ ...entry,
2834
+ baseUrl: normalizeBaseUrl5(entry.baseUrl),
2835
+ balance: entry.balance ?? 0,
2836
+ lastUsed: entry.lastUsed ?? null
2837
+ }));
2838
+ void driver.setItem(SDK_STORAGE_KEYS.API_KEYS, normalized);
2839
+ return { apiKeys: normalized };
2840
+ });
2841
+ },
2842
+ setChildKeys: (value) => {
2843
+ set((state) => {
2844
+ const updates = typeof value === "function" ? value(state.childKeys) : value;
2845
+ const normalized = updates.map((entry) => ({
2846
+ parentBaseUrl: normalizeBaseUrl5(entry.parentBaseUrl),
2847
+ childKey: entry.childKey,
2848
+ balance: entry.balance ?? 0,
2849
+ balanceLimit: entry.balanceLimit,
2850
+ validityDate: entry.validityDate,
2851
+ createdAt: entry.createdAt ?? Date.now()
2852
+ }));
2853
+ void driver.setItem(SDK_STORAGE_KEYS.CHILD_KEYS, normalized);
2854
+ return { childKeys: normalized };
2855
+ });
2856
+ },
2857
+ setXcashuTokens: (value) => {
2858
+ const normalized = {};
2859
+ for (const [baseUrl, tokens] of Object.entries(value)) {
2860
+ normalized[normalizeBaseUrl5(baseUrl)] = tokens.map((entry) => ({
2861
+ ...entry,
2862
+ baseUrl: normalizeBaseUrl5(entry.baseUrl),
2863
+ createdAt: entry.createdAt ?? Date.now(),
2864
+ tryCount: entry.tryCount ?? 0
2865
+ }));
2866
+ }
2867
+ void driver.setItem(SDK_STORAGE_KEYS.XCASHU_TOKENS, normalized);
2868
+ set({ xcashuTokens: normalized });
2869
+ },
2870
+ updateXcashuTokenTryCount: (token, tryCount) => {
2871
+ const currentTokens = get().xcashuTokens;
2872
+ const updatedTokens = {};
2873
+ for (const [baseUrl, tokens] of Object.entries(currentTokens)) {
2874
+ updatedTokens[baseUrl] = tokens.map(
2875
+ (entry) => entry.token === token ? { ...entry, tryCount } : entry
2876
+ );
2877
+ }
2878
+ void driver.setItem(SDK_STORAGE_KEYS.XCASHU_TOKENS, updatedTokens);
2879
+ set({ xcashuTokens: updatedTokens });
2880
+ },
2881
+ setRoutstr21Models: (value) => {
2882
+ void driver.setItem(SDK_STORAGE_KEYS.ROUTSTR21_MODELS, value);
2883
+ set({ routstr21Models: value });
2884
+ },
2885
+ setRoutstr21ModelsLastUpdate: (value) => {
2886
+ void driver.setItem(SDK_STORAGE_KEYS.LAST_ROUTSTR21_MODELS_UPDATE, value);
2887
+ set({ lastRoutstr21ModelsUpdate: value });
2888
+ },
2889
+ setCachedReceiveTokens: (value) => {
2890
+ const normalized = value.map((entry) => ({
2891
+ token: entry.token,
2892
+ amount: entry.amount,
2893
+ unit: entry.unit || "sat",
2894
+ createdAt: entry.createdAt ?? Date.now()
2895
+ }));
2896
+ void driver.setItem(SDK_STORAGE_KEYS.CACHED_RECEIVE_TOKENS, normalized);
2897
+ set({ cachedReceiveTokens: normalized });
2898
+ },
2899
+ setClientIds: (value) => {
2900
+ set((state) => {
2901
+ const updates = typeof value === "function" ? value(state.clientIds) : value;
2902
+ const normalized = updates.map((entry) => ({
2903
+ ...entry,
2904
+ createdAt: entry.createdAt ?? Date.now(),
2905
+ lastUsed: entry.lastUsed ?? null
2906
+ }));
2907
+ void driver.setItem(SDK_STORAGE_KEYS.CLIENT_IDS, normalized);
2908
+ return { clientIds: normalized };
2909
+ });
2910
+ }
2911
+ }));
2912
+ var hydrateStoreFromDriver = async (store, driver) => {
2913
+ const [
2914
+ rawModels,
2915
+ lastUsedModel,
2916
+ rawBaseUrls,
2917
+ lastBaseUrlsUpdate,
2918
+ rawDisabledProviders,
2919
+ rawMints,
2920
+ rawInfo,
2921
+ rawLastModelsUpdate,
2922
+ rawApiKeys,
2923
+ rawChildKeys,
2924
+ rawXcashuTokens,
2925
+ rawRoutstr21Models,
2926
+ rawLastRoutstr21ModelsUpdate,
2927
+ rawCachedReceiveTokens,
2928
+ rawClientIds
2929
+ ] = await Promise.all([
2930
+ driver.getItem(
2931
+ SDK_STORAGE_KEYS.MODELS_FROM_ALL_PROVIDERS,
2932
+ {}
2933
+ ),
2934
+ driver.getItem(SDK_STORAGE_KEYS.LAST_USED_MODEL, null),
2935
+ driver.getItem(SDK_STORAGE_KEYS.BASE_URLS_LIST, []),
2936
+ driver.getItem(SDK_STORAGE_KEYS.LAST_BASE_URLS_UPDATE, null),
2937
+ driver.getItem(SDK_STORAGE_KEYS.DISABLED_PROVIDERS, []),
2938
+ driver.getItem(
2939
+ SDK_STORAGE_KEYS.MINTS_FROM_ALL_PROVIDERS,
2940
+ {}
2941
+ ),
2942
+ driver.getItem(
2943
+ SDK_STORAGE_KEYS.INFO_FROM_ALL_PROVIDERS,
2944
+ {}
2945
+ ),
2946
+ driver.getItem(
2947
+ SDK_STORAGE_KEYS.LAST_MODELS_UPDATE,
2948
+ {}
2949
+ ),
2950
+ driver.getItem(SDK_STORAGE_KEYS.API_KEYS, []),
2951
+ driver.getItem(SDK_STORAGE_KEYS.CHILD_KEYS, []),
2952
+ driver.getItem(SDK_STORAGE_KEYS.XCASHU_TOKENS, {}),
2953
+ driver.getItem(SDK_STORAGE_KEYS.ROUTSTR21_MODELS, []),
2954
+ driver.getItem(
2955
+ SDK_STORAGE_KEYS.LAST_ROUTSTR21_MODELS_UPDATE,
2956
+ null
2957
+ ),
2958
+ driver.getItem(SDK_STORAGE_KEYS.CACHED_RECEIVE_TOKENS, []),
2959
+ driver.getItem(SDK_STORAGE_KEYS.CLIENT_IDS, [])
2960
+ ]);
2961
+ const modelsFromAllProviders = Object.fromEntries(
2962
+ Object.entries(rawModels).map(([baseUrl, models]) => [
2963
+ normalizeBaseUrl5(baseUrl),
2964
+ models
2965
+ ])
2966
+ );
2967
+ const baseUrlsList = rawBaseUrls.map((url) => normalizeBaseUrl5(url));
2968
+ const disabledProviders = rawDisabledProviders.map(
2969
+ (url) => normalizeBaseUrl5(url)
2970
+ );
2971
+ const mintsFromAllProviders = Object.fromEntries(
2972
+ Object.entries(rawMints).map(([baseUrl, mints]) => [
2973
+ normalizeBaseUrl5(baseUrl),
2974
+ mints.map((mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint)
2975
+ ])
2976
+ );
2977
+ const infoFromAllProviders = Object.fromEntries(
2978
+ Object.entries(rawInfo).map(([baseUrl, info]) => [
2979
+ normalizeBaseUrl5(baseUrl),
2980
+ info
2981
+ ])
2982
+ );
2983
+ const lastModelsUpdate = Object.fromEntries(
2984
+ Object.entries(rawLastModelsUpdate).map(([baseUrl, timestamp]) => [
2985
+ normalizeBaseUrl5(baseUrl),
2986
+ timestamp
2987
+ ])
2988
+ );
2989
+ const apiKeys = rawApiKeys.map((entry) => ({
2990
+ ...entry,
2991
+ baseUrl: normalizeBaseUrl5(entry.baseUrl),
2992
+ balance: entry.balance ?? 0,
2993
+ lastUsed: entry.lastUsed ?? null
2994
+ }));
2995
+ const childKeys = rawChildKeys.map((entry) => ({
2996
+ parentBaseUrl: normalizeBaseUrl5(entry.parentBaseUrl),
2997
+ childKey: entry.childKey,
2998
+ balance: entry.balance ?? 0,
2999
+ balanceLimit: entry.balanceLimit,
3000
+ validityDate: entry.validityDate,
3001
+ createdAt: entry.createdAt ?? Date.now()
3002
+ }));
3003
+ const xcashuTokens = Object.fromEntries(
3004
+ Object.entries(rawXcashuTokens).map(([baseUrl, tokens]) => [
3005
+ normalizeBaseUrl5(baseUrl),
3006
+ tokens.map((entry) => ({
3007
+ baseUrl: normalizeBaseUrl5(entry.baseUrl),
3008
+ token: entry.token,
3009
+ createdAt: entry.createdAt ?? Date.now(),
3010
+ tryCount: entry.tryCount ?? 0
3011
+ }))
3012
+ ])
3013
+ );
3014
+ const routstr21Models = rawRoutstr21Models;
3015
+ const lastRoutstr21ModelsUpdate = rawLastRoutstr21ModelsUpdate;
3016
+ const cachedReceiveTokens = rawCachedReceiveTokens?.map((entry) => ({
3017
+ token: entry.token,
3018
+ amount: entry.amount,
3019
+ unit: entry.unit || "sat",
3020
+ createdAt: entry.createdAt ?? Date.now()
3021
+ }));
3022
+ const clientIds = rawClientIds.map((entry) => ({
3023
+ ...entry,
3024
+ createdAt: entry.createdAt ?? Date.now(),
3025
+ lastUsed: entry.lastUsed ?? null
3026
+ }));
3027
+ store.setState({
3028
+ modelsFromAllProviders,
3029
+ lastUsedModel,
3030
+ baseUrlsList,
3031
+ lastBaseUrlsUpdate,
3032
+ disabledProviders,
3033
+ mintsFromAllProviders,
3034
+ infoFromAllProviders,
3035
+ lastModelsUpdate,
3036
+ apiKeys,
3037
+ childKeys,
3038
+ xcashuTokens,
3039
+ routstr21Models,
3040
+ lastRoutstr21ModelsUpdate,
3041
+ cachedReceiveTokens,
3042
+ clientIds
3043
+ });
3044
+ };
3045
+ var createSdkStore = ({
3046
+ driver
3047
+ }) => {
3048
+ const store = createEmptyStore(driver);
3049
+ return {
3050
+ store,
3051
+ hydrate: hydrateStoreFromDriver(store, driver)
3052
+ };
3053
+ };
3054
+
3055
+ // storage/index.ts
3056
+ var isBrowser2 = () => {
3057
+ try {
3058
+ return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
3059
+ } catch {
3060
+ return false;
3061
+ }
3062
+ };
3063
+ var isNode = () => {
3064
+ try {
3065
+ return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
3066
+ } catch {
3067
+ return false;
3068
+ }
3069
+ };
3070
+ var defaultDriver = null;
3071
+ var isBun3 = () => {
3072
+ return typeof process.versions.bun !== "undefined";
3073
+ };
3074
+ var getDefaultSdkDriver = () => {
3075
+ if (defaultDriver) return defaultDriver;
3076
+ if (isBrowser2()) {
3077
+ defaultDriver = localStorageDriver;
3078
+ return defaultDriver;
3079
+ }
3080
+ if (isBun3()) {
3081
+ defaultDriver = createMemoryDriver();
3082
+ return defaultDriver;
3083
+ }
3084
+ if (isNode()) {
3085
+ defaultDriver = createSqliteDriver();
3086
+ return defaultDriver;
3087
+ }
3088
+ defaultDriver = createMemoryDriver();
3089
+ return defaultDriver;
3090
+ };
3091
+ var defaultStore = null;
3092
+ var defaultUsageTrackingDriver = null;
3093
+ var getDefaultSdkStore = () => {
3094
+ if (!defaultStore) {
3095
+ defaultStore = createSdkStore({ driver: getDefaultSdkDriver() });
3096
+ }
3097
+ return defaultStore.hydrate.then(() => defaultStore.store);
3098
+ };
3099
+ var getDefaultUsageTrackingDriver = () => {
3100
+ if (defaultUsageTrackingDriver) return defaultUsageTrackingDriver;
3101
+ const storageDriver = getDefaultSdkDriver();
3102
+ if (isBrowser2()) {
3103
+ defaultUsageTrackingDriver = createIndexedDBUsageTrackingDriver({
3104
+ legacyStorageDriver: storageDriver
3105
+ });
3106
+ return defaultUsageTrackingDriver;
3107
+ }
3108
+ if (isBun3()) {
3109
+ defaultUsageTrackingDriver = createBunSqliteUsageTrackingDriver();
3110
+ return defaultUsageTrackingDriver;
3111
+ }
3112
+ if (isNode()) {
3113
+ defaultUsageTrackingDriver = createSqliteUsageTrackingDriver({
3114
+ legacyStorageDriver: storageDriver
3115
+ });
3116
+ return defaultUsageTrackingDriver;
3117
+ }
3118
+ defaultUsageTrackingDriver = createMemoryUsageTrackingDriver();
3119
+ return defaultUsageTrackingDriver;
3120
+ };
3121
+ function createSSEParserTransform(onUsage, onResponseId) {
3122
+ let buffer = "";
3123
+ let usageCaptured = false;
3124
+ let responseIdCaptured = false;
3125
+ const maybeCaptureUsageFromJson = (jsonText) => {
3126
+ try {
3127
+ const data = JSON.parse(jsonText);
3128
+ const responseId = data.id;
3129
+ if (typeof responseId === "string" && responseId.trim().length > 0) {
3130
+ onResponseId?.(responseId.trim());
3131
+ responseIdCaptured = true;
3132
+ }
3133
+ const usage = extractUsageFromSSEJson(data);
3134
+ if (usage) {
3135
+ onUsage(usage);
3136
+ usageCaptured = true;
3137
+ }
3138
+ } catch {
3139
+ }
3140
+ };
3141
+ const processLine = (self, line) => {
3142
+ const trimmed = line.trim();
3143
+ if (!trimmed) {
3144
+ return;
3145
+ }
3146
+ if (trimmed === "data: [DONE]" || trimmed === "[DONE]") {
3147
+ self.push("data: [DONE]\n\n");
3148
+ return;
3149
+ }
3150
+ if (trimmed.startsWith("data:")) {
3151
+ const dataStr = trimmed.startsWith("data: ") ? trimmed.slice(6) : trimmed.slice(5).trimStart();
3152
+ if (dataStr === "[DONE]") {
3153
+ self.push("data: [DONE]\n\n");
3154
+ return;
3155
+ }
3156
+ maybeCaptureUsageFromJson(dataStr);
3157
+ self.push(`data: ${dataStr}
3158
+
3159
+ `);
3160
+ return;
3161
+ }
3162
+ if (trimmed.startsWith("{")) {
3163
+ maybeCaptureUsageFromJson(trimmed);
3164
+ self.push(`data: ${trimmed}
3165
+
3166
+ `);
3167
+ return;
3168
+ }
3169
+ self.push(line + "\n");
3170
+ };
3171
+ return new Transform({
3172
+ transform(chunk, encoding, callback) {
3173
+ buffer += chunk.toString();
3174
+ const lines = buffer.split(/\r?\n/);
3175
+ buffer = lines.pop() || "";
3176
+ for (const line of lines) {
3177
+ processLine(this, line);
3178
+ }
3179
+ callback();
3180
+ },
3181
+ flush(callback) {
3182
+ if (buffer.trim()) {
3183
+ processLine(this, buffer);
3184
+ }
3185
+ buffer = "";
3186
+ callback();
3187
+ }
3188
+ });
3189
+ }
3190
+ var TOPUP_MARGIN = 1.2;
3191
+ var RoutstrClient = class {
3192
+ constructor(walletAdapter, storageAdapter, providerRegistry, alertLevel, mode = "xcashu", options = {}) {
3193
+ this.walletAdapter = walletAdapter;
3194
+ this.storageAdapter = storageAdapter;
3195
+ this.providerRegistry = providerRegistry;
3196
+ this.balanceManager = new BalanceManager(
3197
+ walletAdapter,
3198
+ storageAdapter,
3199
+ providerRegistry
3200
+ );
3201
+ this.cashuSpender = new CashuSpender(
3202
+ walletAdapter,
3203
+ storageAdapter,
3204
+ providerRegistry,
3205
+ this.balanceManager
3206
+ );
3207
+ this.streamProcessor = new StreamProcessor();
3208
+ this.providerManager = new ProviderManager(providerRegistry);
3209
+ this.alertLevel = alertLevel;
3210
+ this.mode = mode;
3211
+ this.usageTrackingDriver = options.usageTrackingDriver;
3212
+ this.sdkStore = options.sdkStore;
3213
+ }
3214
+ cashuSpender;
3215
+ balanceManager;
3216
+ streamProcessor;
1994
3217
  providerManager;
1995
3218
  alertLevel;
1996
3219
  mode;
1997
3220
  debugLevel = "WARN";
3221
+ usageTrackingDriver;
3222
+ sdkStore;
1998
3223
  /**
1999
3224
  * Get the current client mode
2000
3225
  */
@@ -2060,6 +3285,86 @@ var RoutstrClient = class {
2060
3285
  * requests and get responses back.
2061
3286
  */
2062
3287
  async routeRequest(params) {
3288
+ const prepared = await this._prepareRoutedRequest(params);
3289
+ const satsSpent = await this._handlePostResponseBalanceUpdate({
3290
+ token: prepared.tokenUsed,
3291
+ baseUrl: prepared.baseUrlUsed,
3292
+ mintUrl: params.mintUrl,
3293
+ initialTokenBalance: prepared.tokenBalanceInSats,
3294
+ response: prepared.response,
3295
+ modelId: prepared.modelId,
3296
+ usage: prepared.capturedUsage,
3297
+ requestId: prepared.capturedResponseId,
3298
+ clientApiKey: prepared.clientApiKey
3299
+ });
3300
+ prepared.response.satsSpent = satsSpent;
3301
+ prepared.response.usage = prepared.capturedUsage;
3302
+ prepared.response.requestId = prepared.capturedResponseId;
3303
+ return prepared.response;
3304
+ }
3305
+ async routeRequestToNodeResponse(params) {
3306
+ const { res } = params;
3307
+ const prepared = await this._prepareRoutedRequest(params);
3308
+ res.statusCode = prepared.response.status;
3309
+ prepared.response.headers.forEach((value, key) => {
3310
+ res.setHeader(key, value);
3311
+ });
3312
+ const body = prepared.response.body;
3313
+ if (!body) {
3314
+ const satsSpent = await this._handlePostResponseBalanceUpdate({
3315
+ token: prepared.tokenUsed,
3316
+ baseUrl: prepared.baseUrlUsed,
3317
+ mintUrl: params.mintUrl,
3318
+ initialTokenBalance: prepared.tokenBalanceInSats,
3319
+ response: prepared.response,
3320
+ modelId: prepared.modelId,
3321
+ usage: prepared.capturedUsage,
3322
+ requestId: prepared.capturedResponseId,
3323
+ clientApiKey: prepared.clientApiKey
3324
+ });
3325
+ prepared.response.satsSpent = satsSpent;
3326
+ res.end();
3327
+ return;
3328
+ }
3329
+ const nodeReadable = Readable.fromWeb(body);
3330
+ await new Promise((resolve, reject) => {
3331
+ let settled = false;
3332
+ const finish = async () => {
3333
+ if (settled) return;
3334
+ settled = true;
3335
+ try {
3336
+ const satsSpent = await this._handlePostResponseBalanceUpdate({
3337
+ token: prepared.tokenUsed,
3338
+ baseUrl: prepared.baseUrlUsed,
3339
+ mintUrl: params.mintUrl,
3340
+ initialTokenBalance: prepared.tokenBalanceInSats,
3341
+ response: prepared.response,
3342
+ modelId: prepared.modelId,
3343
+ usage: prepared.capturedUsage,
3344
+ requestId: prepared.capturedResponseId,
3345
+ clientApiKey: prepared.clientApiKey
3346
+ });
3347
+ prepared.response.satsSpent = satsSpent;
3348
+ prepared.response.usage = prepared.capturedUsage;
3349
+ prepared.response.requestId = prepared.capturedResponseId;
3350
+ resolve();
3351
+ } catch (error) {
3352
+ reject(error);
3353
+ }
3354
+ };
3355
+ const fail = (error) => {
3356
+ if (settled) return;
3357
+ settled = true;
3358
+ reject(error);
3359
+ };
3360
+ res.once("finish", finish);
3361
+ res.once("close", finish);
3362
+ res.once("error", fail);
3363
+ nodeReadable.once("error", fail);
3364
+ nodeReadable.pipe(res);
3365
+ });
3366
+ }
3367
+ async _prepareRoutedRequest(params) {
2063
3368
  const {
2064
3369
  path,
2065
3370
  method,
@@ -2067,8 +3372,10 @@ var RoutstrClient = class {
2067
3372
  headers = {},
2068
3373
  baseUrl,
2069
3374
  mintUrl,
2070
- modelId
3375
+ modelId,
3376
+ clientApiKey: providedClientApiKey
2071
3377
  } = params;
3378
+ const clientApiKey = providedClientApiKey ?? this._extractClientApiKey(headers);
2072
3379
  await this._checkBalance();
2073
3380
  let requiredSats = 1;
2074
3381
  let selectedModel;
@@ -2090,7 +3397,6 @@ var RoutstrClient = class {
2090
3397
  amount: requiredSats,
2091
3398
  baseUrl
2092
3399
  });
2093
- this._log("DEBUG", token, baseUrl);
2094
3400
  let requestBody = body;
2095
3401
  if (body && typeof body === "object") {
2096
3402
  const bodyObj = body;
@@ -2098,7 +3404,7 @@ var RoutstrClient = class {
2098
3404
  requestBody = { ...bodyObj, stream: false };
2099
3405
  }
2100
3406
  }
2101
- const baseHeaders = this._buildBaseHeaders(headers);
3407
+ const baseHeaders = this._buildBaseHeaders();
2102
3408
  const requestHeaders = this._withAuthHeader(baseHeaders, token);
2103
3409
  const response = await this._makeRequest({
2104
3410
  path,
@@ -2115,13 +3421,55 @@ var RoutstrClient = class {
2115
3421
  const tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
2116
3422
  const baseUrlUsed = response.baseUrl || baseUrl;
2117
3423
  const tokenUsed = response.token || token;
2118
- await this._handlePostResponseBalanceUpdate({
2119
- token: tokenUsed,
2120
- baseUrl: baseUrlUsed,
2121
- initialTokenBalance: tokenBalanceInSats,
2122
- response
2123
- });
2124
- return response;
3424
+ const contentType = response.headers.get("content-type") || "";
3425
+ let processedResponse = response;
3426
+ let capturedUsage;
3427
+ let capturedResponseId;
3428
+ if (contentType.includes("text/event-stream") && response.body) {
3429
+ const nodeReadable = Readable.fromWeb(response.body);
3430
+ const sseParser = createSSEParserTransform(
3431
+ (usage) => {
3432
+ capturedUsage = usage;
3433
+ processedResponse.usage = usage;
3434
+ },
3435
+ (responseId) => {
3436
+ capturedResponseId = responseId;
3437
+ processedResponse.requestId = responseId;
3438
+ }
3439
+ );
3440
+ const transformed = nodeReadable.pipe(sseParser, { end: true });
3441
+ const webStream = Readable.toWeb(
3442
+ transformed
3443
+ );
3444
+ processedResponse = new Response(webStream, {
3445
+ status: response.status,
3446
+ statusText: response.statusText,
3447
+ headers: response.headers
3448
+ });
3449
+ processedResponse.baseUrl = response.baseUrl;
3450
+ processedResponse.token = response.token;
3451
+ }
3452
+ return {
3453
+ response: processedResponse,
3454
+ tokenUsed,
3455
+ baseUrlUsed,
3456
+ tokenBalanceInSats,
3457
+ modelId,
3458
+ capturedUsage,
3459
+ capturedResponseId,
3460
+ clientApiKey
3461
+ };
3462
+ }
3463
+ /**
3464
+ * Extract clientApiKey from Authorization Bearer token if present
3465
+ */
3466
+ _extractClientApiKey(headers) {
3467
+ const authHeader = headers["Authorization"] || headers["authorization"];
3468
+ if (authHeader?.startsWith("Bearer ")) {
3469
+ const extractedKey = authHeader.slice(7);
3470
+ return extractedKey;
3471
+ }
3472
+ return void 0;
2125
3473
  }
2126
3474
  /**
2127
3475
  * Fetch AI response with streaming
@@ -2226,9 +3574,21 @@ var RoutstrClient = class {
2226
3574
  let satsSpent = await this._handlePostResponseBalanceUpdate({
2227
3575
  token,
2228
3576
  baseUrl: baseUrlUsed,
3577
+ mintUrl,
2229
3578
  initialTokenBalance: tokenBalanceInSats,
2230
3579
  fallbackSatsSpent: isApikeysEstimate ? this._getEstimatedCosts(selectedModel, streamingResult) : void 0,
2231
- response
3580
+ response,
3581
+ modelId: selectedModel.id,
3582
+ usage: streamingResult.usage ? {
3583
+ promptTokens: Number(streamingResult.usage.prompt_tokens ?? 0),
3584
+ completionTokens: Number(
3585
+ streamingResult.usage.completion_tokens ?? 0
3586
+ ),
3587
+ totalTokens: Number(streamingResult.usage.total_tokens ?? 0),
3588
+ cost: Number(streamingResult.usage.cost ?? 0),
3589
+ satsCost: Number(streamingResult.usage.sats_cost ?? 0)
3590
+ } : void 0,
3591
+ requestId: streamingResult.responseId
2232
3592
  });
2233
3593
  const estimatedCosts = this._getEstimatedCosts(
2234
3594
  selectedModel,
@@ -2253,7 +3613,6 @@ var RoutstrClient = class {
2253
3613
  try {
2254
3614
  const url = `${baseUrl.replace(/\/$/, "")}${path}`;
2255
3615
  if (this.mode === "xcashu") this._log("DEBUG", "HEADERS,", headers);
2256
- this._log("DEBUG", "HEADERS,", headers);
2257
3616
  const response = await fetch(url, {
2258
3617
  method,
2259
3618
  headers,
@@ -2322,8 +3681,6 @@ var RoutstrClient = class {
2322
3681
  `[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${tryReceiveTokenResult.amount}`
2323
3682
  );
2324
3683
  tryNextProvider = true;
2325
- if (this.mode === "lazyrefund")
2326
- this.storageAdapter.removeToken(baseUrl);
2327
3684
  } else {
2328
3685
  this._log(
2329
3686
  "DEBUG",
@@ -2371,22 +3728,15 @@ var RoutstrClient = class {
2371
3728
  );
2372
3729
  }
2373
3730
  }
2374
- if (status === 402 && !tryNextProvider && (this.mode === "apikeys" || this.mode === "lazyrefund")) {
3731
+ if (status === 402 && !tryNextProvider && this.mode === "apikeys") {
2375
3732
  this.storageAdapter.getApiKey(baseUrl);
2376
3733
  let topupAmount = params.requiredSats;
2377
3734
  try {
2378
- let currentBalance = 0;
2379
- if (this.mode === "apikeys") {
2380
- const currentBalanceInfo = await this.balanceManager.getTokenBalance(
2381
- params.token,
2382
- baseUrl
2383
- );
2384
- currentBalance = currentBalanceInfo.unit === "msat" ? currentBalanceInfo.amount / 1e3 : currentBalanceInfo.amount;
2385
- } else if (this.mode === "lazyrefund") {
2386
- const distribution = this.storageAdapter.getCachedTokenDistribution();
2387
- const tokenEntry = distribution.find((t) => t.baseUrl === baseUrl);
2388
- currentBalance = tokenEntry?.amount ?? 0;
2389
- }
3735
+ const currentBalanceInfo = await this.balanceManager.getTokenBalance(
3736
+ params.token,
3737
+ baseUrl
3738
+ );
3739
+ const currentBalance = currentBalanceInfo.unit === "msat" ? currentBalanceInfo.amount / 1e3 : currentBalanceInfo.amount;
2390
3740
  const shortfall = Math.max(0, params.requiredSats - currentBalance);
2391
3741
  topupAmount = shortfall > 0 ? shortfall : params.requiredSats;
2392
3742
  } catch (e) {
@@ -2523,34 +3873,7 @@ var RoutstrClient = class {
2523
3873
  "DEBUG",
2524
3874
  `[RoutstrClient] _handleErrorResponse: Status ${status} (auth/server error), attempting refund for ${baseUrl}, mode=${this.mode}`
2525
3875
  );
2526
- if (this.mode === "lazyrefund") {
2527
- try {
2528
- const refundResult = await this.balanceManager.refund({
2529
- mintUrl,
2530
- baseUrl,
2531
- token: params.token
2532
- });
2533
- this._log(
2534
- "DEBUG",
2535
- `[RoutstrClient] _handleErrorResponse: Lazyrefund result: success=${refundResult.success}`
2536
- );
2537
- if (refundResult.success) this.storageAdapter.removeToken(baseUrl);
2538
- else
2539
- throw new ProviderError(
2540
- baseUrl,
2541
- status,
2542
- "refund failed",
2543
- requestId
2544
- );
2545
- } catch (error) {
2546
- throw new ProviderError(
2547
- baseUrl,
2548
- status,
2549
- "Failed to refund token",
2550
- requestId
2551
- );
2552
- }
2553
- } else if (this.mode === "apikeys") {
3876
+ if (this.mode === "apikeys") {
2554
3877
  this._log(
2555
3878
  "DEBUG",
2556
3879
  `[RoutstrClient] _handleErrorResponse: Attempting API key refund for ${baseUrl}, key preview=${token}`
@@ -2566,7 +3889,8 @@ var RoutstrClient = class {
2566
3889
  const refundResult = await this.balanceManager.refundApiKey({
2567
3890
  mintUrl,
2568
3891
  baseUrl,
2569
- apiKey: token
3892
+ apiKey: token,
3893
+ forceRefund: true
2570
3894
  });
2571
3895
  this._log(
2572
3896
  "DEBUG",
@@ -2645,26 +3969,32 @@ var RoutstrClient = class {
2645
3969
  * Handle post-response balance update for all modes
2646
3970
  */
2647
3971
  async _handlePostResponseBalanceUpdate(params) {
2648
- const { token, baseUrl, initialTokenBalance, fallbackSatsSpent, response } = params;
3972
+ const {
3973
+ token,
3974
+ baseUrl,
3975
+ mintUrl,
3976
+ initialTokenBalance,
3977
+ fallbackSatsSpent,
3978
+ response,
3979
+ modelId,
3980
+ usage,
3981
+ requestId,
3982
+ clientApiKey
3983
+ } = params;
2649
3984
  let satsSpent = initialTokenBalance;
2650
3985
  if (this.mode === "xcashu" && response) {
2651
3986
  const refundToken = response.headers.get("x-cashu") ?? void 0;
2652
3987
  if (refundToken) {
2653
3988
  try {
2654
3989
  const receiveResult = await this.cashuSpender.receiveToken(refundToken);
2655
- satsSpent = initialTokenBalance - receiveResult.amount * (receiveResult.unit == "sat" ? 1 : 1e3);
3990
+ if (receiveResult.success) {
3991
+ this.storageAdapter.removeXcashuToken(baseUrl, token);
3992
+ satsSpent = initialTokenBalance - receiveResult.amount * (receiveResult.unit == "sat" ? 1 : 1e3);
3993
+ }
2656
3994
  } catch (error) {
2657
3995
  this._log("ERROR", "[xcashu] Failed to receive refund token:", error);
2658
3996
  }
2659
3997
  }
2660
- } else if (this.mode === "lazyrefund") {
2661
- const latestBalanceInfo = await this.balanceManager.getTokenBalance(
2662
- token,
2663
- baseUrl
2664
- );
2665
- const latestTokenBalance = latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
2666
- this.storageAdapter.updateTokenBalance(baseUrl, latestTokenBalance);
2667
- satsSpent = initialTokenBalance - latestTokenBalance;
2668
3998
  } else if (this.mode === "apikeys") {
2669
3999
  try {
2670
4000
  const latestBalanceInfo = await this.balanceManager.getTokenBalance(
@@ -2692,8 +4022,87 @@ var RoutstrClient = class {
2692
4022
  satsSpent = fallbackSatsSpent ?? initialTokenBalance;
2693
4023
  }
2694
4024
  }
4025
+ await this._trackResponseUsage({
4026
+ token,
4027
+ baseUrl,
4028
+ response,
4029
+ modelId,
4030
+ satsSpent,
4031
+ usage,
4032
+ requestId,
4033
+ clientApiKey
4034
+ });
4035
+ (async () => {
4036
+ try {
4037
+ const xcashuResults = await this.cashuSpender.refundXcashuTokens(mintUrl);
4038
+ this._log("DEBUG", "Refund xcashu tokens results:", xcashuResults);
4039
+ const results = await this.cashuSpender.refundProviders(mintUrl);
4040
+ } catch (error) {
4041
+ this._log("ERROR", "Failed to refund providers:", error);
4042
+ }
4043
+ })();
2695
4044
  return satsSpent;
2696
4045
  }
4046
+ async _trackResponseUsage(params) {
4047
+ const {
4048
+ token,
4049
+ baseUrl,
4050
+ response,
4051
+ modelId,
4052
+ satsSpent,
4053
+ usage: providedUsage,
4054
+ requestId: providedRequestId,
4055
+ clientApiKey
4056
+ } = params;
4057
+ if (!response || !modelId) {
4058
+ return;
4059
+ }
4060
+ try {
4061
+ let usage = providedUsage;
4062
+ let requestId = providedRequestId;
4063
+ if (!usage || !requestId) {
4064
+ const contentType = response.headers.get("content-type") || "";
4065
+ if (contentType.includes("text/event-stream")) {
4066
+ usage = usage ?? response.usage;
4067
+ requestId = requestId ?? response.requestId ?? response.headers.get("x-routstr-request-id") ?? void 0;
4068
+ if (!usage) {
4069
+ return;
4070
+ }
4071
+ } else {
4072
+ const cloned = response.clone();
4073
+ const responseBody = await cloned.json();
4074
+ usage = usage ?? extractUsageFromResponseBody(responseBody, satsSpent) ?? void 0;
4075
+ requestId = requestId ?? extractResponseId(responseBody) ?? response.headers.get("x-routstr-request-id") ?? void 0;
4076
+ }
4077
+ }
4078
+ if (!usage) {
4079
+ return;
4080
+ }
4081
+ const finalRequestId = requestId || "unknown";
4082
+ const store = this.sdkStore ?? await getDefaultSdkStore();
4083
+ const state = store.getState();
4084
+ const matchKey = clientApiKey ?? token;
4085
+ const matchingClient = state.clientIds.find(
4086
+ (client) => client.apiKey === matchKey
4087
+ );
4088
+ const entryId = finalRequestId === "unknown" ? `req-${Date.now()}-${modelId}` : finalRequestId;
4089
+ const usageTracking = this.usageTrackingDriver ?? getDefaultUsageTrackingDriver();
4090
+ const entry = {
4091
+ id: entryId,
4092
+ timestamp: Date.now(),
4093
+ modelId,
4094
+ baseUrl,
4095
+ requestId: finalRequestId,
4096
+ client: matchingClient?.clientId,
4097
+ ...usage
4098
+ };
4099
+ if (this.mode === "xcashu") {
4100
+ entry.satsCost = satsSpent;
4101
+ }
4102
+ await usageTracking.append(entry);
4103
+ } catch (error) {
4104
+ }
4105
+ }
2697
4106
  /**
2698
4107
  * Convert messages for API format
2699
4108
  */
@@ -2738,37 +4147,6 @@ var RoutstrClient = class {
2738
4147
  content: result.content || ""
2739
4148
  };
2740
4149
  }
2741
- /**
2742
- * Create a child key for a parent API key via the provider's API
2743
- * POST /v1/balance/child-key
2744
- */
2745
- async _createChildKey(baseUrl, parentApiKey, options) {
2746
- const response = await fetch(`${baseUrl}v1/balance/child-key`, {
2747
- method: "POST",
2748
- headers: {
2749
- "Content-Type": "application/json",
2750
- Authorization: `Bearer ${parentApiKey}`
2751
- },
2752
- body: JSON.stringify({
2753
- count: options?.count ?? 1,
2754
- balance_limit: options?.balanceLimit,
2755
- balance_limit_reset: options?.balanceLimitReset,
2756
- validity_date: options?.validityDate
2757
- })
2758
- });
2759
- if (!response.ok) {
2760
- throw new Error(
2761
- `Failed to create child key: ${response.status} ${await response.text()}`
2762
- );
2763
- }
2764
- const data = await response.json();
2765
- return {
2766
- childKey: data.api_keys?.[0],
2767
- balance: data.balance ?? 0,
2768
- balanceLimit: data.balance_limit,
2769
- validityDate: data.validity_date
2770
- };
2771
- }
2772
4150
  /**
2773
4151
  * Calculate estimated costs from usage
2774
4152
  */
@@ -2783,11 +4161,11 @@ var RoutstrClient = class {
2783
4161
  return estimatedCosts;
2784
4162
  }
2785
4163
  /**
2786
- * Get pending cashu token amount
4164
+ * Get pending API key amount
2787
4165
  */
2788
4166
  _getPendingCashuTokenAmount() {
2789
- const distribution = this.storageAdapter.getCachedTokenDistribution();
2790
- return distribution.reduce((total, item) => total + item.amount, 0);
4167
+ const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
4168
+ return apiKeyDistribution.reduce((total, item) => total + item.amount, 0);
2791
4169
  }
2792
4170
  /**
2793
4171
  * Handle errors and notify callbacks
@@ -2934,8 +4312,8 @@ var RoutstrClient = class {
2934
4312
  const spendResult = await this.cashuSpender.spend({
2935
4313
  mintUrl,
2936
4314
  amount,
2937
- baseUrl: this.mode === "lazyrefund" ? baseUrl : "",
2938
- reuseToken: this.mode === "lazyrefund"
4315
+ baseUrl: "",
4316
+ reuseToken: false
2939
4317
  });
2940
4318
  if (!spendResult.token) {
2941
4319
  this._log(
@@ -2948,6 +4326,7 @@ var RoutstrClient = class {
2948
4326
  "DEBUG",
2949
4327
  `[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult.token}, balance: ${spendResult.balance} ${spendResult.unit ?? "sat"}`
2950
4328
  );
4329
+ this.storageAdapter.addXcashuToken(baseUrl, spendResult.token);
2951
4330
  }
2952
4331
  return {
2953
4332
  token: spendResult.token,
@@ -2979,6 +4358,6 @@ var RoutstrClient = class {
2979
4358
  }
2980
4359
  };
2981
4360
 
2982
- export { ProviderManager, RoutstrClient, StreamProcessor };
4361
+ export { ProviderManager, RoutstrClient, StreamProcessor, createSSEParserTransform };
2983
4362
  //# sourceMappingURL=index.mjs.map
2984
4363
  //# sourceMappingURL=index.mjs.map