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