@routstr/sdk 0.2.11 → 0.3.0

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.
@@ -3,6 +3,7 @@
3
3
  var cashuTs = require('@cashu/cashu-ts');
4
4
  var vanilla = require('zustand/vanilla');
5
5
  var stream = require('stream');
6
+ var string_decoder = require('string_decoder');
6
7
 
7
8
  // core/errors.ts
8
9
  var InsufficientBalanceError = class extends Error {
@@ -638,7 +639,7 @@ var CashuSpender = class {
638
639
  };
639
640
 
640
641
  // wallet/BalanceManager.ts
641
- var BalanceManager = class {
642
+ var BalanceManager = class _BalanceManager {
642
643
  constructor(walletAdapter, storageAdapter, providerRegistry, cashuSpender) {
643
644
  this.walletAdapter = walletAdapter;
644
645
  this.storageAdapter = storageAdapter;
@@ -655,6 +656,47 @@ var BalanceManager = class {
655
656
  }
656
657
  }
657
658
  cashuSpender;
659
+ /** In-memory guard for per-provider wallet mutations (topup / refund) */
660
+ providerWalletOps = /* @__PURE__ */ new Map();
661
+ /** Cooldown (ms) between opposite operations on the same provider */
662
+ static PROVIDER_WALLET_COOLDOWN_MS = 1e4;
663
+ /**
664
+ * Check whether a wallet operation (topup/refund) may run for a provider.
665
+ * Returns the reason when blocked.
666
+ */
667
+ _canRunProviderWalletOperation(baseUrl, type) {
668
+ const existing = this.providerWalletOps.get(baseUrl);
669
+ if (!existing) {
670
+ return { allowed: true };
671
+ }
672
+ if (existing.type === type) {
673
+ return { allowed: true };
674
+ }
675
+ if (!existing.endTime) {
676
+ return {
677
+ allowed: false,
678
+ reason: `Provider wallet operation locked; ${existing.type} in progress`
679
+ };
680
+ }
681
+ const elapsed = Date.now() - existing.endTime;
682
+ if (elapsed < _BalanceManager.PROVIDER_WALLET_COOLDOWN_MS) {
683
+ return {
684
+ allowed: false,
685
+ reason: `Provider wallet operation locked; recent ${existing.type} completed ${Math.round(elapsed / 1e3)}s ago`
686
+ };
687
+ }
688
+ this.providerWalletOps.delete(baseUrl);
689
+ return { allowed: true };
690
+ }
691
+ _beginProviderWalletOperation(baseUrl, type) {
692
+ this.providerWalletOps.set(baseUrl, { type, startTime: Date.now() });
693
+ }
694
+ _endProviderWalletOperation(baseUrl, type) {
695
+ const existing = this.providerWalletOps.get(baseUrl);
696
+ if (existing && existing.type === type) {
697
+ existing.endTime = Date.now();
698
+ }
699
+ }
658
700
  async getBalanceState() {
659
701
  const mintBalances = await this.walletAdapter.getBalances();
660
702
  const units = this.walletAdapter.getMintUnits();
@@ -689,6 +731,20 @@ var BalanceManager = class {
689
731
  * @returns Refund result
690
732
  */
691
733
  async refundApiKey(options) {
734
+ const { mintUrl, baseUrl, apiKey, forceRefund } = options;
735
+ const guard = this._canRunProviderWalletOperation(baseUrl, "refund");
736
+ if (!guard.allowed) {
737
+ console.log(`[BalanceManager] Skipping refund for ${baseUrl} - ${guard.reason}`);
738
+ return { success: false, message: guard.reason };
739
+ }
740
+ this._beginProviderWalletOperation(baseUrl, "refund");
741
+ try {
742
+ return await this._refundApiKeyImpl({ mintUrl, baseUrl, apiKey, forceRefund });
743
+ } finally {
744
+ this._endProviderWalletOperation(baseUrl, "refund");
745
+ }
746
+ }
747
+ async _refundApiKeyImpl(options) {
692
748
  const { mintUrl, baseUrl, apiKey, forceRefund } = options;
693
749
  if (!apiKey) {
694
750
  return { success: false, message: "No API key to refund" };
@@ -817,6 +873,20 @@ var BalanceManager = class {
817
873
  * Top up API key balance with a cashu token
818
874
  */
819
875
  async topUp(options) {
876
+ const { mintUrl, baseUrl, amount, token: providedToken } = options;
877
+ const guard = this._canRunProviderWalletOperation(baseUrl, "topup");
878
+ if (!guard.allowed) {
879
+ console.log(`[BalanceManager] Skipping topup for ${baseUrl} - ${guard.reason}`);
880
+ return { success: false, message: guard.reason };
881
+ }
882
+ this._beginProviderWalletOperation(baseUrl, "topup");
883
+ try {
884
+ return await this._topUpImpl({ mintUrl, baseUrl, amount, token: providedToken });
885
+ } finally {
886
+ this._endProviderWalletOperation(baseUrl, "topup");
887
+ }
888
+ }
889
+ async _topUpImpl(options) {
820
890
  const { mintUrl, baseUrl, amount, token: providedToken } = options;
821
891
  if (!amount || amount <= 0) {
822
892
  return { success: false, message: "Invalid top up amount" };
@@ -977,7 +1047,7 @@ var BalanceManager = class {
977
1047
  p2pkPubkey
978
1048
  );
979
1049
  console.log(
980
- `[BalanceManager.createProviderToken] SUCCESS: Token created from mint ${candidateMint}`
1050
+ `[BalanceManager.createProviderToken] SUCCESS: Token created from mint ${candidateMint}, all mint balances: ${JSON.stringify(Object.fromEntries(Object.entries(balances).map(([mint, balance]) => [mint, getBalanceInSats(balance, units[mint])])))}`
981
1051
  );
982
1052
  return {
983
1053
  success: true,
@@ -1727,6 +1797,7 @@ var ProviderManager = class _ProviderManager {
1727
1797
  }
1728
1798
  /**
1729
1799
  * Clean up expired cooldown entries
1800
+ * Also removes the provider from failedProviders so it can be retried
1730
1801
  */
1731
1802
  cleanupExpiredCooldowns() {
1732
1803
  const now = Date.now();
@@ -1739,6 +1810,10 @@ var ProviderManager = class _ProviderManager {
1739
1810
  console.log(
1740
1811
  `[cleanupExpiredCooldowns:${this.instanceId}] Removing expired cooldown for ${url} (age: ${age}ms, cooldown: ${_ProviderManager.COOLDOWN_DURATION_MS}ms)`
1741
1812
  );
1813
+ this.failedProviders.delete(url);
1814
+ if (this.store) {
1815
+ this.store.getState().removeFailedProvider(url);
1816
+ }
1742
1817
  }
1743
1818
  return !isExpired;
1744
1819
  }
@@ -1912,60 +1987,47 @@ var ProviderManager = class _ProviderManager {
1912
1987
  const disabledProviders = new Set(
1913
1988
  this.providerRegistry.getDisabledProviders()
1914
1989
  );
1915
- console.log(`[findNextBestProvider:${this.instanceId}] Starting search for model: ${modelId}`);
1916
- console.log(`[findNextBestProvider:${this.instanceId}] currentBaseUrl: ${currentBaseUrl}`);
1917
- console.log(`[findNextBestProvider:${this.instanceId}] torMode: ${torMode}`);
1918
- console.log(`[findNextBestProvider:${this.instanceId}] disabledProviders: ${[...disabledProviders]}`);
1919
- console.log(`[findNextBestProvider:${this.instanceId}] failedProviders: ${[...this.failedProviders]}`);
1920
- console.log(`[findNextBestProvider:${this.instanceId}] providersOnCooldown: ${this.providersOnCoolDown.map(([url]) => url)}`);
1990
+ console.log(
1991
+ `[findNextBestProvider:${this.instanceId}] Starting search for model: ${modelId}`
1992
+ );
1993
+ console.log(
1994
+ `[findNextBestProvider:${this.instanceId}] disabledProviders: ${[...disabledProviders]}`
1995
+ );
1996
+ console.log(
1997
+ `[findNextBestProvider:${this.instanceId}] providersOnCooldown: ${this.providersOnCoolDown.map(([url]) => url)}`
1998
+ );
1921
1999
  const allProviders = this.providerRegistry.getAllProvidersModels();
1922
- console.log(`[findNextBestProvider:${this.instanceId}] Total providers in registry: ${Object.keys(allProviders).length}`);
2000
+ console.log(
2001
+ `[findNextBestProvider:${this.instanceId}] Total providers in registry: ${Object.keys(allProviders).length}`
2002
+ );
1923
2003
  const candidates = [];
1924
- let skippedCurrent = 0, skippedFailed = 0, skippedDisabled = 0, skippedCooldown = 0, skippedOnion = 0, skippedNoModel = 0;
1925
2004
  for (const [baseUrl, models] of Object.entries(allProviders)) {
1926
2005
  if (baseUrl === currentBaseUrl) {
1927
- console.log(`[findNextBestProvider:${this.instanceId}] SKIP (current): ${baseUrl}`);
1928
- skippedCurrent++;
1929
- continue;
1930
- }
1931
- if (this.failedProviders.has(baseUrl)) {
1932
- console.log(`[findNextBestProvider:${this.instanceId}] SKIP (failed): ${baseUrl}`);
1933
- skippedFailed++;
2006
+ console.log(
2007
+ `[findNextBestProvider:${this.instanceId}] SKIP (current): ${baseUrl}`
2008
+ );
1934
2009
  continue;
1935
2010
  }
1936
2011
  if (disabledProviders.has(baseUrl)) {
1937
- console.log(`[findNextBestProvider:${this.instanceId}] SKIP (disabled): ${baseUrl}`);
1938
- skippedDisabled++;
1939
2012
  continue;
1940
2013
  }
1941
2014
  if (this.isOnCooldown(baseUrl)) {
1942
- console.log(`[findNextBestProvider:${this.instanceId}] SKIP (cooldown): ${baseUrl}`);
1943
- skippedCooldown++;
1944
2015
  continue;
1945
2016
  }
1946
2017
  if (!torMode && (isOnionUrl(baseUrl) || isInsecureHttpUrl(baseUrl))) {
1947
- console.log(`[findNextBestProvider:${this.instanceId}] SKIP (onion/http): ${baseUrl}`);
1948
- skippedOnion++;
1949
2018
  continue;
1950
2019
  }
1951
2020
  const model = models.find((m) => m.id === modelId);
1952
2021
  if (!model) {
1953
- console.log(`[findNextBestProvider:${this.instanceId}] SKIP (no model ${modelId}): ${baseUrl} has models: ${models.map((m) => m.id).join(", ")}`);
1954
- skippedNoModel++;
1955
2022
  continue;
1956
2023
  }
1957
2024
  const cost = model.sats_pricing?.completion ?? 0;
1958
- console.log(`[findNextBestProvider:${this.instanceId}] CANDIDATE: ${baseUrl} cost: ${cost}`);
1959
2025
  candidates.push({ baseUrl, model, cost });
1960
2026
  }
1961
- console.log(`[findNextBestProvider:${this.instanceId}] Skipped: current=${skippedCurrent}, failed=${skippedFailed}, disabled=${skippedDisabled}, cooldown=${skippedCooldown}, onion=${skippedOnion}, noModel=${skippedNoModel}`);
1962
- console.log(`[findNextBestProvider:${this.instanceId}] Total candidates: ${candidates.length}`);
1963
2027
  candidates.sort((a, b) => a.cost - b.cost);
1964
2028
  if (candidates.length > 0) {
1965
- console.log(`[findNextBestProvider:${this.instanceId}] Selected provider: ${candidates[0].baseUrl} with cost: ${candidates[0].cost}`);
1966
2029
  return candidates[0].baseUrl;
1967
2030
  } else {
1968
- console.log(`[findNextBestProvider:${this.instanceId}] No candidate providers found`);
1969
2031
  return null;
1970
2032
  }
1971
2033
  } catch (error) {
@@ -2124,7 +2186,9 @@ var ProviderManager = class _ProviderManager {
2124
2186
  const approximateTokens = apiMessagesNoImages ? Math.ceil(JSON.stringify(apiMessagesNoImages, null, 2).length / 2.84) : 1e4;
2125
2187
  const totalInputTokens = approximateTokens + imageTokens;
2126
2188
  const sp = model?.sats_pricing;
2127
- if (!sp) return 0;
2189
+ if (!sp) {
2190
+ return 0;
2191
+ }
2128
2192
  if (!sp.max_completion_cost) {
2129
2193
  return sp.max_cost ?? 50;
2130
2194
  }
@@ -3436,12 +3500,119 @@ var getDefaultUsageTrackingDriver = () => {
3436
3500
  defaultUsageTrackingDriver = createMemoryUsageTrackingDriver();
3437
3501
  return defaultUsageTrackingDriver;
3438
3502
  };
3503
+ function mergeUsage(previous, next) {
3504
+ if (!previous) return next;
3505
+ return {
3506
+ promptTokens: next.promptTokens > 0 ? next.promptTokens : previous.promptTokens,
3507
+ completionTokens: next.completionTokens > 0 ? next.completionTokens : previous.completionTokens,
3508
+ totalTokens: next.totalTokens > 0 ? next.totalTokens : previous.totalTokens,
3509
+ cost: next.cost > 0 ? next.cost : previous.cost,
3510
+ satsCost: next.satsCost > 0 ? next.satsCost : previous.satsCost
3511
+ };
3512
+ }
3513
+ function hasUsageChanged(previous, next) {
3514
+ if (!previous) return true;
3515
+ return previous.promptTokens !== next.promptTokens || previous.completionTokens !== next.completionTokens || previous.totalTokens !== next.totalTokens || previous.cost !== next.cost || previous.satsCost !== next.satsCost;
3516
+ }
3517
+ async function inspectSSEWebStream(stream, onUsage, onResponseId) {
3518
+ const reader = stream.getReader();
3519
+ const decoder = new TextDecoder("utf-8");
3520
+ let buffer = "";
3521
+ let capturedUsage = null;
3522
+ let capturedResponseId;
3523
+ let responseIdCaptured = false;
3524
+ const inspectDataPayload = (jsonText) => {
3525
+ if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
3526
+ return;
3527
+ }
3528
+ const trimmed = jsonText.trim();
3529
+ if (!trimmed || trimmed === "[DONE]") return;
3530
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return;
3531
+ try {
3532
+ const data = JSON.parse(trimmed);
3533
+ if (!responseIdCaptured) {
3534
+ const responseId = data?.id;
3535
+ if (typeof responseId === "string" && responseId.trim().length > 0) {
3536
+ capturedResponseId = responseId.trim();
3537
+ onResponseId?.(capturedResponseId);
3538
+ responseIdCaptured = true;
3539
+ }
3540
+ }
3541
+ const usage = extractUsageFromSSEJson(data);
3542
+ if (usage) {
3543
+ const merged = mergeUsage(capturedUsage, usage);
3544
+ if (hasUsageChanged(capturedUsage, merged)) {
3545
+ capturedUsage = merged;
3546
+ onUsage(merged);
3547
+ }
3548
+ }
3549
+ } catch {
3550
+ }
3551
+ };
3552
+ const inspectEventBlock = (eventBlock) => {
3553
+ if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
3554
+ return;
3555
+ }
3556
+ const lines = eventBlock.split(/\r?\n/);
3557
+ const dataParts = [];
3558
+ for (const line of lines) {
3559
+ if (!line || line.startsWith(":")) continue;
3560
+ if (line.startsWith("data:")) {
3561
+ const value = line.startsWith("data: ") ? line.slice(6) : line.slice(5);
3562
+ dataParts.push(value);
3563
+ }
3564
+ }
3565
+ if (dataParts.length === 0) return;
3566
+ inspectDataPayload(dataParts.join("\n"));
3567
+ };
3568
+ const drainBufferedEvents = () => {
3569
+ const terminator = /\r?\n\r?\n/g;
3570
+ let lastIndex = 0;
3571
+ let match;
3572
+ while ((match = terminator.exec(buffer)) !== null) {
3573
+ const block = buffer.slice(lastIndex, match.index);
3574
+ lastIndex = match.index + match[0].length;
3575
+ if (block.length > 0) inspectEventBlock(block);
3576
+ }
3577
+ if (lastIndex > 0) buffer = buffer.slice(lastIndex);
3578
+ };
3579
+ try {
3580
+ while (true) {
3581
+ const { value, done } = await reader.read();
3582
+ if (done) break;
3583
+ if (value && value.byteLength > 0) {
3584
+ buffer += decoder.decode(value, { stream: true });
3585
+ drainBufferedEvents();
3586
+ }
3587
+ }
3588
+ buffer += decoder.decode();
3589
+ drainBufferedEvents();
3590
+ if (buffer.length > 0) {
3591
+ const tail = buffer.replace(/\r?\n+$/, "");
3592
+ if (tail.length > 0) inspectEventBlock(tail);
3593
+ buffer = "";
3594
+ }
3595
+ } catch {
3596
+ } finally {
3597
+ try {
3598
+ reader.releaseLock();
3599
+ } catch {
3600
+ }
3601
+ }
3602
+ return {
3603
+ capturedUsage: capturedUsage ?? void 0,
3604
+ capturedResponseId
3605
+ };
3606
+ }
3439
3607
  function createSSEParserTransform(onUsage, onResponseId) {
3440
3608
  let buffer = "";
3441
- let usageCaptured = false;
3609
+ const decoder = new string_decoder.StringDecoder("utf8");
3610
+ let capturedUsage = null;
3442
3611
  let responseIdCaptured = false;
3443
3612
  const inspectDataPayload = (jsonText) => {
3444
- if (usageCaptured && responseIdCaptured) return;
3613
+ if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
3614
+ return;
3615
+ }
3445
3616
  const trimmed = jsonText.trim();
3446
3617
  if (!trimmed || trimmed === "[DONE]") return;
3447
3618
  if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return;
@@ -3454,18 +3625,21 @@ function createSSEParserTransform(onUsage, onResponseId) {
3454
3625
  responseIdCaptured = true;
3455
3626
  }
3456
3627
  }
3457
- if (!usageCaptured) {
3458
- const usage = extractUsageFromSSEJson(data);
3459
- if (usage) {
3460
- onUsage(usage);
3461
- usageCaptured = true;
3628
+ const usage = extractUsageFromSSEJson(data);
3629
+ if (usage) {
3630
+ const mergedUsage = mergeUsage(capturedUsage, usage);
3631
+ if (hasUsageChanged(capturedUsage, mergedUsage)) {
3632
+ capturedUsage = mergedUsage;
3633
+ onUsage(mergedUsage);
3462
3634
  }
3463
3635
  }
3464
3636
  } catch {
3465
3637
  }
3466
3638
  };
3467
3639
  const inspectEventBlock = (eventBlock) => {
3468
- if (usageCaptured && responseIdCaptured) return;
3640
+ if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
3641
+ return;
3642
+ }
3469
3643
  const lines = eventBlock.split(/\r?\n/);
3470
3644
  const dataParts = [];
3471
3645
  for (const line of lines) {
@@ -3479,32 +3653,35 @@ function createSSEParserTransform(onUsage, onResponseId) {
3479
3653
  const payload = dataParts.join("\n");
3480
3654
  inspectDataPayload(payload);
3481
3655
  };
3482
- const emitEventBlock = (self, eventBlock) => {
3483
- if (eventBlock.length === 0) return;
3484
- inspectEventBlock(eventBlock);
3485
- self.push(eventBlock + "\n\n");
3656
+ const processBufferedEvents = () => {
3657
+ const terminator = /\r?\n\r?\n/g;
3658
+ let lastIndex = 0;
3659
+ let match;
3660
+ while ((match = terminator.exec(buffer)) !== null) {
3661
+ const block = buffer.slice(lastIndex, match.index);
3662
+ lastIndex = match.index + match[0].length;
3663
+ if (block.length > 0) {
3664
+ inspectEventBlock(block);
3665
+ }
3666
+ }
3667
+ if (lastIndex > 0) {
3668
+ buffer = buffer.slice(lastIndex);
3669
+ }
3486
3670
  };
3487
3671
  return new stream.Transform({
3488
3672
  transform(chunk, _encoding, callback) {
3489
- buffer += chunk.toString();
3490
- const terminator = /\r?\n\r?\n/g;
3491
- let lastIndex = 0;
3492
- let match;
3493
- while ((match = terminator.exec(buffer)) !== null) {
3494
- const block = buffer.slice(lastIndex, match.index);
3495
- lastIndex = match.index + match[0].length;
3496
- emitEventBlock(this, block);
3497
- }
3498
- if (lastIndex > 0) {
3499
- buffer = buffer.slice(lastIndex);
3500
- }
3673
+ this.push(chunk);
3674
+ buffer += decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
3675
+ processBufferedEvents();
3501
3676
  callback();
3502
3677
  },
3503
3678
  flush(callback) {
3679
+ buffer += decoder.end();
3680
+ processBufferedEvents();
3504
3681
  if (buffer.length > 0) {
3505
3682
  const tail = buffer.replace(/\r?\n+$/, "");
3506
3683
  if (tail.length > 0) {
3507
- emitEventBlock(this, tail);
3684
+ inspectEventBlock(tail);
3508
3685
  }
3509
3686
  buffer = "";
3510
3687
  }
@@ -3512,6 +3689,8 @@ function createSSEParserTransform(onUsage, onResponseId) {
3512
3689
  }
3513
3690
  });
3514
3691
  }
3692
+
3693
+ // client/RoutstrClient.ts
3515
3694
  var TOPUP_MARGIN = 1.2;
3516
3695
  var RoutstrClient = class {
3517
3696
  constructor(walletAdapter, storageAdapter, providerRegistry, alertLevel, mode = "xcashu", options = {}) {
@@ -3611,31 +3790,12 @@ var RoutstrClient = class {
3611
3790
  */
3612
3791
  async routeRequest(params) {
3613
3792
  const prepared = await this._prepareRoutedRequest(params);
3614
- const satsSpent = await this._handlePostResponseBalanceUpdate({
3615
- token: prepared.tokenUsed,
3616
- baseUrl: prepared.baseUrlUsed,
3617
- mintUrl: params.mintUrl,
3618
- initialTokenBalance: prepared.tokenBalanceInSats,
3619
- response: prepared.response,
3620
- modelId: prepared.modelId,
3621
- usage: prepared.capturedUsage,
3622
- requestId: prepared.capturedResponseId,
3623
- clientApiKey: prepared.clientApiKey
3624
- });
3625
- prepared.response.satsSpent = satsSpent;
3626
- prepared.response.usage = prepared.capturedUsage;
3627
- prepared.response.requestId = prepared.capturedResponseId;
3628
- return prepared.response;
3629
- }
3630
- async routeRequestToNodeResponse(params) {
3631
- const { res } = params;
3632
- const prepared = await this._prepareRoutedRequest(params);
3633
- res.statusCode = prepared.response.status;
3634
- prepared.response.headers.forEach((value, key) => {
3635
- res.setHeader(key, value);
3636
- });
3637
- const body = prepared.response.body;
3638
- if (!body) {
3793
+ const contentType = prepared.response.headers.get("content-type") || "";
3794
+ const isSSE = contentType.includes("text/event-stream");
3795
+ const runFinalize = async () => {
3796
+ const { capturedUsage, capturedResponseId } = await prepared.usagePromise;
3797
+ const usage = capturedUsage ?? prepared.capturedUsage;
3798
+ const requestId = capturedResponseId ?? prepared.capturedResponseId;
3639
3799
  const satsSpent = await this._handlePostResponseBalanceUpdate({
3640
3800
  token: prepared.tokenUsed,
3641
3801
  baseUrl: prepared.baseUrlUsed,
@@ -3643,55 +3803,29 @@ var RoutstrClient = class {
3643
3803
  initialTokenBalance: prepared.tokenBalanceInSats,
3644
3804
  response: prepared.response,
3645
3805
  modelId: prepared.modelId,
3646
- usage: prepared.capturedUsage,
3647
- requestId: prepared.capturedResponseId,
3806
+ usage,
3807
+ requestId,
3648
3808
  clientApiKey: prepared.clientApiKey
3649
3809
  });
3650
3810
  prepared.response.satsSpent = satsSpent;
3651
- res.end();
3652
- return;
3811
+ prepared.response.usage = usage;
3812
+ prepared.response.requestId = requestId;
3813
+ return satsSpent;
3814
+ };
3815
+ if (isSSE) {
3816
+ const finalizePromise = runFinalize().catch((error) => {
3817
+ this._log("ERROR", "[RoutstrClient] SSE finalize failed:", error);
3818
+ return 0;
3819
+ });
3820
+ prepared.response.finalize = () => finalizePromise;
3821
+ return prepared.response;
3653
3822
  }
3654
- const nodeReadable = stream.Readable.fromWeb(body);
3655
- await new Promise((resolve, reject) => {
3656
- let settled = false;
3657
- const finish = async () => {
3658
- if (settled) return;
3659
- settled = true;
3660
- try {
3661
- const satsSpent = await this._handlePostResponseBalanceUpdate({
3662
- token: prepared.tokenUsed,
3663
- baseUrl: prepared.baseUrlUsed,
3664
- mintUrl: params.mintUrl,
3665
- initialTokenBalance: prepared.tokenBalanceInSats,
3666
- response: prepared.response,
3667
- modelId: prepared.modelId,
3668
- usage: prepared.capturedUsage,
3669
- requestId: prepared.capturedResponseId,
3670
- clientApiKey: prepared.clientApiKey
3671
- });
3672
- prepared.response.satsSpent = satsSpent;
3673
- prepared.response.usage = prepared.capturedUsage;
3674
- prepared.response.requestId = prepared.capturedResponseId;
3675
- resolve();
3676
- } catch (error) {
3677
- reject(error);
3678
- }
3679
- };
3680
- const fail = (error) => {
3681
- if (settled) return;
3682
- settled = true;
3683
- reject(error);
3684
- };
3685
- res.once("finish", finish);
3686
- res.once("close", finish);
3687
- res.once("error", fail);
3688
- nodeReadable.once("error", fail);
3689
- nodeReadable.pipe(res);
3690
- });
3823
+ await runFinalize();
3824
+ return prepared.response;
3691
3825
  }
3692
3826
  async _prepareRoutedRequest(params) {
3693
3827
  const {
3694
- path,
3828
+ path: requestPath,
3695
3829
  method,
3696
3830
  body,
3697
3831
  headers = {},
@@ -3711,9 +3845,23 @@ var RoutstrClient = class {
3711
3845
  );
3712
3846
  selectedModel = providerModel ?? void 0;
3713
3847
  if (selectedModel) {
3848
+ const requestMessages = Array.isArray(
3849
+ body?.messages
3850
+ ) ? body.messages : [];
3851
+ const requestMaxTokens = typeof body?.max_tokens === "number" ? body.max_tokens : void 0;
3852
+ this._log(
3853
+ "DEBUG",
3854
+ "[RoutstrClient] generic request pricing input",
3855
+ {
3856
+ modelId: selectedModel.id,
3857
+ messageCount: requestMessages.length,
3858
+ maxTokens: requestMaxTokens
3859
+ }
3860
+ );
3714
3861
  requiredSats = this.providerManager.getRequiredSatsForModel(
3715
3862
  selectedModel,
3716
- []
3863
+ requestMessages,
3864
+ requestMaxTokens
3717
3865
  );
3718
3866
  }
3719
3867
  }
@@ -3732,7 +3880,7 @@ var RoutstrClient = class {
3732
3880
  const baseHeaders = this._buildBaseHeaders();
3733
3881
  const requestHeaders = this._withAuthHeader(baseHeaders, token);
3734
3882
  const response = await this._makeRequest({
3735
- path,
3883
+ path: requestPath,
3736
3884
  method,
3737
3885
  body: method === "GET" ? void 0 : requestBody,
3738
3886
  baseUrl,
@@ -3750,9 +3898,18 @@ var RoutstrClient = class {
3750
3898
  let processedResponse = response;
3751
3899
  let capturedUsage;
3752
3900
  let capturedResponseId;
3901
+ let usagePromise = Promise.resolve({});
3753
3902
  if (contentType.includes("text/event-stream") && response.body) {
3754
- const nodeReadable = stream.Readable.fromWeb(response.body);
3755
- const sseParser = createSSEParserTransform(
3903
+ const [clientStream, inspectStream] = response.body.tee();
3904
+ processedResponse = new Response(clientStream, {
3905
+ status: response.status,
3906
+ statusText: response.statusText,
3907
+ headers: response.headers
3908
+ });
3909
+ processedResponse.baseUrl = response.baseUrl;
3910
+ processedResponse.token = response.token;
3911
+ usagePromise = inspectSSEWebStream(
3912
+ inspectStream,
3756
3913
  (usage) => {
3757
3914
  capturedUsage = usage;
3758
3915
  processedResponse.usage = usage;
@@ -3762,17 +3919,7 @@ var RoutstrClient = class {
3762
3919
  processedResponse.requestId = responseId;
3763
3920
  }
3764
3921
  );
3765
- const transformed = nodeReadable.pipe(sseParser, { end: true });
3766
- const webStream = stream.Readable.toWeb(
3767
- transformed
3768
- );
3769
- processedResponse = new Response(webStream, {
3770
- status: response.status,
3771
- statusText: response.statusText,
3772
- headers: response.headers
3773
- });
3774
- processedResponse.baseUrl = response.baseUrl;
3775
- processedResponse.token = response.token;
3922
+ processedResponse.usagePromise = usagePromise;
3776
3923
  }
3777
3924
  return {
3778
3925
  response: processedResponse,
@@ -3782,7 +3929,8 @@ var RoutstrClient = class {
3782
3929
  modelId,
3783
3930
  capturedUsage,
3784
3931
  capturedResponseId,
3785
- clientApiKey
3932
+ clientApiKey,
3933
+ usagePromise
3786
3934
  };
3787
3935
  }
3788
3936
  /**
@@ -3831,7 +3979,6 @@ var RoutstrClient = class {
3831
3979
  callbacks.onTokenCreated?.(this._getPendingCashuTokenAmount());
3832
3980
  const baseHeaders = this._buildBaseHeaders(headers);
3833
3981
  const requestHeaders = this._withAuthHeader(baseHeaders, token);
3834
- this.providerManager.resetFailedProviders();
3835
3982
  const providerInfo = await this.providerRegistry.getProviderInfo(baseUrl);
3836
3983
  const providerVersion = providerInfo?.version ?? "";
3837
3984
  let modelIdForRequest = selectedModel.id;
@@ -4059,7 +4206,7 @@ var RoutstrClient = class {
4059
4206
  );
4060
4207
  const currentBalance = currentBalanceInfo.unit === "msat" ? currentBalanceInfo.amount / 1e3 : currentBalanceInfo.amount;
4061
4208
  const shortfall = Math.max(0, params.requiredSats - currentBalance);
4062
- topupAmount = shortfall > 0 ? shortfall : params.requiredSats;
4209
+ topupAmount = shortfall > 0.21 * params.requiredSats ? shortfall : 0.21 * params.requiredSats;
4063
4210
  this._log(
4064
4211
  "DEBUG",
4065
4212
  `The shortfall is: ${shortfall}. requiredSats: ${params.requiredSats}. Current Balance: ${currentBalance} `
@@ -4695,5 +4842,6 @@ exports.ProviderManager = ProviderManager;
4695
4842
  exports.RoutstrClient = RoutstrClient;
4696
4843
  exports.StreamProcessor = StreamProcessor;
4697
4844
  exports.createSSEParserTransform = createSSEParserTransform;
4845
+ exports.inspectSSEWebStream = inspectSSEWebStream;
4698
4846
  //# sourceMappingURL=index.js.map
4699
4847
  //# sourceMappingURL=index.js.map