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