@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.
package/dist/index.mjs CHANGED
@@ -3,7 +3,8 @@ import { EventStore } from 'applesauce-core';
3
3
  import { tap } from 'rxjs';
4
4
  import { getDecodedToken } from '@cashu/cashu-ts';
5
5
  import { createStore } from 'zustand/vanilla';
6
- import { Transform, Readable } from 'stream';
6
+ import { Transform } from 'stream';
7
+ import { StringDecoder } from 'string_decoder';
7
8
 
8
9
  // core/errors.ts
9
10
  var InsufficientBalanceError = class extends Error {
@@ -1270,7 +1271,7 @@ var CashuSpender = class {
1270
1271
  };
1271
1272
 
1272
1273
  // wallet/BalanceManager.ts
1273
- var BalanceManager = class {
1274
+ var BalanceManager = class _BalanceManager {
1274
1275
  constructor(walletAdapter, storageAdapter, providerRegistry, cashuSpender) {
1275
1276
  this.walletAdapter = walletAdapter;
1276
1277
  this.storageAdapter = storageAdapter;
@@ -1287,6 +1288,47 @@ var BalanceManager = class {
1287
1288
  }
1288
1289
  }
1289
1290
  cashuSpender;
1291
+ /** In-memory guard for per-provider wallet mutations (topup / refund) */
1292
+ providerWalletOps = /* @__PURE__ */ new Map();
1293
+ /** Cooldown (ms) between opposite operations on the same provider */
1294
+ static PROVIDER_WALLET_COOLDOWN_MS = 1e4;
1295
+ /**
1296
+ * Check whether a wallet operation (topup/refund) may run for a provider.
1297
+ * Returns the reason when blocked.
1298
+ */
1299
+ _canRunProviderWalletOperation(baseUrl, type) {
1300
+ const existing = this.providerWalletOps.get(baseUrl);
1301
+ if (!existing) {
1302
+ return { allowed: true };
1303
+ }
1304
+ if (existing.type === type) {
1305
+ return { allowed: true };
1306
+ }
1307
+ if (!existing.endTime) {
1308
+ return {
1309
+ allowed: false,
1310
+ reason: `Provider wallet operation locked; ${existing.type} in progress`
1311
+ };
1312
+ }
1313
+ const elapsed = Date.now() - existing.endTime;
1314
+ if (elapsed < _BalanceManager.PROVIDER_WALLET_COOLDOWN_MS) {
1315
+ return {
1316
+ allowed: false,
1317
+ reason: `Provider wallet operation locked; recent ${existing.type} completed ${Math.round(elapsed / 1e3)}s ago`
1318
+ };
1319
+ }
1320
+ this.providerWalletOps.delete(baseUrl);
1321
+ return { allowed: true };
1322
+ }
1323
+ _beginProviderWalletOperation(baseUrl, type) {
1324
+ this.providerWalletOps.set(baseUrl, { type, startTime: Date.now() });
1325
+ }
1326
+ _endProviderWalletOperation(baseUrl, type) {
1327
+ const existing = this.providerWalletOps.get(baseUrl);
1328
+ if (existing && existing.type === type) {
1329
+ existing.endTime = Date.now();
1330
+ }
1331
+ }
1290
1332
  async getBalanceState() {
1291
1333
  const mintBalances = await this.walletAdapter.getBalances();
1292
1334
  const units = this.walletAdapter.getMintUnits();
@@ -1321,6 +1363,20 @@ var BalanceManager = class {
1321
1363
  * @returns Refund result
1322
1364
  */
1323
1365
  async refundApiKey(options) {
1366
+ const { mintUrl, baseUrl, apiKey, forceRefund } = options;
1367
+ const guard = this._canRunProviderWalletOperation(baseUrl, "refund");
1368
+ if (!guard.allowed) {
1369
+ console.log(`[BalanceManager] Skipping refund for ${baseUrl} - ${guard.reason}`);
1370
+ return { success: false, message: guard.reason };
1371
+ }
1372
+ this._beginProviderWalletOperation(baseUrl, "refund");
1373
+ try {
1374
+ return await this._refundApiKeyImpl({ mintUrl, baseUrl, apiKey, forceRefund });
1375
+ } finally {
1376
+ this._endProviderWalletOperation(baseUrl, "refund");
1377
+ }
1378
+ }
1379
+ async _refundApiKeyImpl(options) {
1324
1380
  const { mintUrl, baseUrl, apiKey, forceRefund } = options;
1325
1381
  if (!apiKey) {
1326
1382
  return { success: false, message: "No API key to refund" };
@@ -1449,6 +1505,20 @@ var BalanceManager = class {
1449
1505
  * Top up API key balance with a cashu token
1450
1506
  */
1451
1507
  async topUp(options) {
1508
+ const { mintUrl, baseUrl, amount, token: providedToken } = options;
1509
+ const guard = this._canRunProviderWalletOperation(baseUrl, "topup");
1510
+ if (!guard.allowed) {
1511
+ console.log(`[BalanceManager] Skipping topup for ${baseUrl} - ${guard.reason}`);
1512
+ return { success: false, message: guard.reason };
1513
+ }
1514
+ this._beginProviderWalletOperation(baseUrl, "topup");
1515
+ try {
1516
+ return await this._topUpImpl({ mintUrl, baseUrl, amount, token: providedToken });
1517
+ } finally {
1518
+ this._endProviderWalletOperation(baseUrl, "topup");
1519
+ }
1520
+ }
1521
+ async _topUpImpl(options) {
1452
1522
  const { mintUrl, baseUrl, amount, token: providedToken } = options;
1453
1523
  if (!amount || amount <= 0) {
1454
1524
  return { success: false, message: "Invalid top up amount" };
@@ -1609,7 +1679,7 @@ var BalanceManager = class {
1609
1679
  p2pkPubkey
1610
1680
  );
1611
1681
  console.log(
1612
- `[BalanceManager.createProviderToken] SUCCESS: Token created from mint ${candidateMint}`
1682
+ `[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])])))}`
1613
1683
  );
1614
1684
  return {
1615
1685
  success: true,
@@ -2415,6 +2485,7 @@ var ProviderManager = class _ProviderManager {
2415
2485
  }
2416
2486
  /**
2417
2487
  * Clean up expired cooldown entries
2488
+ * Also removes the provider from failedProviders so it can be retried
2418
2489
  */
2419
2490
  cleanupExpiredCooldowns() {
2420
2491
  const now = Date.now();
@@ -2427,6 +2498,10 @@ var ProviderManager = class _ProviderManager {
2427
2498
  console.log(
2428
2499
  `[cleanupExpiredCooldowns:${this.instanceId}] Removing expired cooldown for ${url} (age: ${age}ms, cooldown: ${_ProviderManager.COOLDOWN_DURATION_MS}ms)`
2429
2500
  );
2501
+ this.failedProviders.delete(url);
2502
+ if (this.store) {
2503
+ this.store.getState().removeFailedProvider(url);
2504
+ }
2430
2505
  }
2431
2506
  return !isExpired;
2432
2507
  }
@@ -2600,60 +2675,47 @@ var ProviderManager = class _ProviderManager {
2600
2675
  const disabledProviders = new Set(
2601
2676
  this.providerRegistry.getDisabledProviders()
2602
2677
  );
2603
- console.log(`[findNextBestProvider:${this.instanceId}] Starting search for model: ${modelId}`);
2604
- console.log(`[findNextBestProvider:${this.instanceId}] currentBaseUrl: ${currentBaseUrl}`);
2605
- console.log(`[findNextBestProvider:${this.instanceId}] torMode: ${torMode}`);
2606
- console.log(`[findNextBestProvider:${this.instanceId}] disabledProviders: ${[...disabledProviders]}`);
2607
- console.log(`[findNextBestProvider:${this.instanceId}] failedProviders: ${[...this.failedProviders]}`);
2608
- console.log(`[findNextBestProvider:${this.instanceId}] providersOnCooldown: ${this.providersOnCoolDown.map(([url]) => url)}`);
2678
+ console.log(
2679
+ `[findNextBestProvider:${this.instanceId}] Starting search for model: ${modelId}`
2680
+ );
2681
+ console.log(
2682
+ `[findNextBestProvider:${this.instanceId}] disabledProviders: ${[...disabledProviders]}`
2683
+ );
2684
+ console.log(
2685
+ `[findNextBestProvider:${this.instanceId}] providersOnCooldown: ${this.providersOnCoolDown.map(([url]) => url)}`
2686
+ );
2609
2687
  const allProviders = this.providerRegistry.getAllProvidersModels();
2610
- console.log(`[findNextBestProvider:${this.instanceId}] Total providers in registry: ${Object.keys(allProviders).length}`);
2688
+ console.log(
2689
+ `[findNextBestProvider:${this.instanceId}] Total providers in registry: ${Object.keys(allProviders).length}`
2690
+ );
2611
2691
  const candidates = [];
2612
- let skippedCurrent = 0, skippedFailed = 0, skippedDisabled = 0, skippedCooldown = 0, skippedOnion = 0, skippedNoModel = 0;
2613
2692
  for (const [baseUrl, models] of Object.entries(allProviders)) {
2614
2693
  if (baseUrl === currentBaseUrl) {
2615
- console.log(`[findNextBestProvider:${this.instanceId}] SKIP (current): ${baseUrl}`);
2616
- skippedCurrent++;
2617
- continue;
2618
- }
2619
- if (this.failedProviders.has(baseUrl)) {
2620
- console.log(`[findNextBestProvider:${this.instanceId}] SKIP (failed): ${baseUrl}`);
2621
- skippedFailed++;
2694
+ console.log(
2695
+ `[findNextBestProvider:${this.instanceId}] SKIP (current): ${baseUrl}`
2696
+ );
2622
2697
  continue;
2623
2698
  }
2624
2699
  if (disabledProviders.has(baseUrl)) {
2625
- console.log(`[findNextBestProvider:${this.instanceId}] SKIP (disabled): ${baseUrl}`);
2626
- skippedDisabled++;
2627
2700
  continue;
2628
2701
  }
2629
2702
  if (this.isOnCooldown(baseUrl)) {
2630
- console.log(`[findNextBestProvider:${this.instanceId}] SKIP (cooldown): ${baseUrl}`);
2631
- skippedCooldown++;
2632
2703
  continue;
2633
2704
  }
2634
2705
  if (!torMode && (isOnionUrl(baseUrl) || isInsecureHttpUrl(baseUrl))) {
2635
- console.log(`[findNextBestProvider:${this.instanceId}] SKIP (onion/http): ${baseUrl}`);
2636
- skippedOnion++;
2637
2706
  continue;
2638
2707
  }
2639
2708
  const model = models.find((m) => m.id === modelId);
2640
2709
  if (!model) {
2641
- console.log(`[findNextBestProvider:${this.instanceId}] SKIP (no model ${modelId}): ${baseUrl} has models: ${models.map((m) => m.id).join(", ")}`);
2642
- skippedNoModel++;
2643
2710
  continue;
2644
2711
  }
2645
2712
  const cost = model.sats_pricing?.completion ?? 0;
2646
- console.log(`[findNextBestProvider:${this.instanceId}] CANDIDATE: ${baseUrl} cost: ${cost}`);
2647
2713
  candidates.push({ baseUrl, model, cost });
2648
2714
  }
2649
- console.log(`[findNextBestProvider:${this.instanceId}] Skipped: current=${skippedCurrent}, failed=${skippedFailed}, disabled=${skippedDisabled}, cooldown=${skippedCooldown}, onion=${skippedOnion}, noModel=${skippedNoModel}`);
2650
- console.log(`[findNextBestProvider:${this.instanceId}] Total candidates: ${candidates.length}`);
2651
2715
  candidates.sort((a, b) => a.cost - b.cost);
2652
2716
  if (candidates.length > 0) {
2653
- console.log(`[findNextBestProvider:${this.instanceId}] Selected provider: ${candidates[0].baseUrl} with cost: ${candidates[0].cost}`);
2654
2717
  return candidates[0].baseUrl;
2655
2718
  } else {
2656
- console.log(`[findNextBestProvider:${this.instanceId}] No candidate providers found`);
2657
2719
  return null;
2658
2720
  }
2659
2721
  } catch (error) {
@@ -2812,7 +2874,9 @@ var ProviderManager = class _ProviderManager {
2812
2874
  const approximateTokens = apiMessagesNoImages ? Math.ceil(JSON.stringify(apiMessagesNoImages, null, 2).length / 2.84) : 1e4;
2813
2875
  const totalInputTokens = approximateTokens + imageTokens;
2814
2876
  const sp = model?.sats_pricing;
2815
- if (!sp) return 0;
2877
+ if (!sp) {
2878
+ return 0;
2879
+ }
2816
2880
  if (!sp.max_completion_cost) {
2817
2881
  return sp.max_cost ?? 50;
2818
2882
  }
@@ -4521,12 +4585,119 @@ var setDefaultUsageTrackingDriver = (driver) => {
4521
4585
  var getDefaultDiscoveryAdapter = async () => createDiscoveryAdapterFromStore(await getDefaultSdkStore());
4522
4586
  var getDefaultStorageAdapter = async () => createStorageAdapterFromStore(await getDefaultSdkStore());
4523
4587
  var getDefaultProviderRegistry = async () => createProviderRegistryFromStore(await getDefaultSdkStore());
4588
+ function mergeUsage(previous, next) {
4589
+ if (!previous) return next;
4590
+ return {
4591
+ promptTokens: next.promptTokens > 0 ? next.promptTokens : previous.promptTokens,
4592
+ completionTokens: next.completionTokens > 0 ? next.completionTokens : previous.completionTokens,
4593
+ totalTokens: next.totalTokens > 0 ? next.totalTokens : previous.totalTokens,
4594
+ cost: next.cost > 0 ? next.cost : previous.cost,
4595
+ satsCost: next.satsCost > 0 ? next.satsCost : previous.satsCost
4596
+ };
4597
+ }
4598
+ function hasUsageChanged(previous, next) {
4599
+ if (!previous) return true;
4600
+ return previous.promptTokens !== next.promptTokens || previous.completionTokens !== next.completionTokens || previous.totalTokens !== next.totalTokens || previous.cost !== next.cost || previous.satsCost !== next.satsCost;
4601
+ }
4602
+ async function inspectSSEWebStream(stream, onUsage, onResponseId) {
4603
+ const reader = stream.getReader();
4604
+ const decoder = new TextDecoder("utf-8");
4605
+ let buffer = "";
4606
+ let capturedUsage = null;
4607
+ let capturedResponseId;
4608
+ let responseIdCaptured = false;
4609
+ const inspectDataPayload = (jsonText) => {
4610
+ if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
4611
+ return;
4612
+ }
4613
+ const trimmed = jsonText.trim();
4614
+ if (!trimmed || trimmed === "[DONE]") return;
4615
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return;
4616
+ try {
4617
+ const data = JSON.parse(trimmed);
4618
+ if (!responseIdCaptured) {
4619
+ const responseId = data?.id;
4620
+ if (typeof responseId === "string" && responseId.trim().length > 0) {
4621
+ capturedResponseId = responseId.trim();
4622
+ onResponseId?.(capturedResponseId);
4623
+ responseIdCaptured = true;
4624
+ }
4625
+ }
4626
+ const usage = extractUsageFromSSEJson(data);
4627
+ if (usage) {
4628
+ const merged = mergeUsage(capturedUsage, usage);
4629
+ if (hasUsageChanged(capturedUsage, merged)) {
4630
+ capturedUsage = merged;
4631
+ onUsage(merged);
4632
+ }
4633
+ }
4634
+ } catch {
4635
+ }
4636
+ };
4637
+ const inspectEventBlock = (eventBlock) => {
4638
+ if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
4639
+ return;
4640
+ }
4641
+ const lines = eventBlock.split(/\r?\n/);
4642
+ const dataParts = [];
4643
+ for (const line of lines) {
4644
+ if (!line || line.startsWith(":")) continue;
4645
+ if (line.startsWith("data:")) {
4646
+ const value = line.startsWith("data: ") ? line.slice(6) : line.slice(5);
4647
+ dataParts.push(value);
4648
+ }
4649
+ }
4650
+ if (dataParts.length === 0) return;
4651
+ inspectDataPayload(dataParts.join("\n"));
4652
+ };
4653
+ const drainBufferedEvents = () => {
4654
+ const terminator = /\r?\n\r?\n/g;
4655
+ let lastIndex = 0;
4656
+ let match;
4657
+ while ((match = terminator.exec(buffer)) !== null) {
4658
+ const block = buffer.slice(lastIndex, match.index);
4659
+ lastIndex = match.index + match[0].length;
4660
+ if (block.length > 0) inspectEventBlock(block);
4661
+ }
4662
+ if (lastIndex > 0) buffer = buffer.slice(lastIndex);
4663
+ };
4664
+ try {
4665
+ while (true) {
4666
+ const { value, done } = await reader.read();
4667
+ if (done) break;
4668
+ if (value && value.byteLength > 0) {
4669
+ buffer += decoder.decode(value, { stream: true });
4670
+ drainBufferedEvents();
4671
+ }
4672
+ }
4673
+ buffer += decoder.decode();
4674
+ drainBufferedEvents();
4675
+ if (buffer.length > 0) {
4676
+ const tail = buffer.replace(/\r?\n+$/, "");
4677
+ if (tail.length > 0) inspectEventBlock(tail);
4678
+ buffer = "";
4679
+ }
4680
+ } catch {
4681
+ } finally {
4682
+ try {
4683
+ reader.releaseLock();
4684
+ } catch {
4685
+ }
4686
+ }
4687
+ return {
4688
+ capturedUsage: capturedUsage ?? void 0,
4689
+ capturedResponseId
4690
+ };
4691
+ }
4524
4692
  function createSSEParserTransform(onUsage, onResponseId) {
4525
4693
  let buffer = "";
4526
- let usageCaptured = false;
4694
+ const decoder = new StringDecoder("utf8");
4695
+ let capturedUsage = null;
4527
4696
  let responseIdCaptured = false;
4528
4697
  const inspectDataPayload = (jsonText) => {
4529
- if (usageCaptured && responseIdCaptured) return;
4698
+ if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
4699
+ return;
4700
+ }
4530
4701
  const trimmed = jsonText.trim();
4531
4702
  if (!trimmed || trimmed === "[DONE]") return;
4532
4703
  if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return;
@@ -4539,18 +4710,21 @@ function createSSEParserTransform(onUsage, onResponseId) {
4539
4710
  responseIdCaptured = true;
4540
4711
  }
4541
4712
  }
4542
- if (!usageCaptured) {
4543
- const usage = extractUsageFromSSEJson(data);
4544
- if (usage) {
4545
- onUsage(usage);
4546
- usageCaptured = true;
4713
+ const usage = extractUsageFromSSEJson(data);
4714
+ if (usage) {
4715
+ const mergedUsage = mergeUsage(capturedUsage, usage);
4716
+ if (hasUsageChanged(capturedUsage, mergedUsage)) {
4717
+ capturedUsage = mergedUsage;
4718
+ onUsage(mergedUsage);
4547
4719
  }
4548
4720
  }
4549
4721
  } catch {
4550
4722
  }
4551
4723
  };
4552
4724
  const inspectEventBlock = (eventBlock) => {
4553
- if (usageCaptured && responseIdCaptured) return;
4725
+ if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
4726
+ return;
4727
+ }
4554
4728
  const lines = eventBlock.split(/\r?\n/);
4555
4729
  const dataParts = [];
4556
4730
  for (const line of lines) {
@@ -4564,32 +4738,35 @@ function createSSEParserTransform(onUsage, onResponseId) {
4564
4738
  const payload = dataParts.join("\n");
4565
4739
  inspectDataPayload(payload);
4566
4740
  };
4567
- const emitEventBlock = (self, eventBlock) => {
4568
- if (eventBlock.length === 0) return;
4569
- inspectEventBlock(eventBlock);
4570
- self.push(eventBlock + "\n\n");
4741
+ const processBufferedEvents = () => {
4742
+ const terminator = /\r?\n\r?\n/g;
4743
+ let lastIndex = 0;
4744
+ let match;
4745
+ while ((match = terminator.exec(buffer)) !== null) {
4746
+ const block = buffer.slice(lastIndex, match.index);
4747
+ lastIndex = match.index + match[0].length;
4748
+ if (block.length > 0) {
4749
+ inspectEventBlock(block);
4750
+ }
4751
+ }
4752
+ if (lastIndex > 0) {
4753
+ buffer = buffer.slice(lastIndex);
4754
+ }
4571
4755
  };
4572
4756
  return new Transform({
4573
4757
  transform(chunk, _encoding, callback) {
4574
- buffer += chunk.toString();
4575
- const terminator = /\r?\n\r?\n/g;
4576
- let lastIndex = 0;
4577
- let match;
4578
- while ((match = terminator.exec(buffer)) !== null) {
4579
- const block = buffer.slice(lastIndex, match.index);
4580
- lastIndex = match.index + match[0].length;
4581
- emitEventBlock(this, block);
4582
- }
4583
- if (lastIndex > 0) {
4584
- buffer = buffer.slice(lastIndex);
4585
- }
4758
+ this.push(chunk);
4759
+ buffer += decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
4760
+ processBufferedEvents();
4586
4761
  callback();
4587
4762
  },
4588
4763
  flush(callback) {
4764
+ buffer += decoder.end();
4765
+ processBufferedEvents();
4589
4766
  if (buffer.length > 0) {
4590
4767
  const tail = buffer.replace(/\r?\n+$/, "");
4591
4768
  if (tail.length > 0) {
4592
- emitEventBlock(this, tail);
4769
+ inspectEventBlock(tail);
4593
4770
  }
4594
4771
  buffer = "";
4595
4772
  }
@@ -4597,6 +4774,8 @@ function createSSEParserTransform(onUsage, onResponseId) {
4597
4774
  }
4598
4775
  });
4599
4776
  }
4777
+
4778
+ // client/RoutstrClient.ts
4600
4779
  var TOPUP_MARGIN = 1.2;
4601
4780
  var RoutstrClient = class {
4602
4781
  constructor(walletAdapter, storageAdapter, providerRegistry, alertLevel, mode = "xcashu", options = {}) {
@@ -4696,31 +4875,12 @@ var RoutstrClient = class {
4696
4875
  */
4697
4876
  async routeRequest(params) {
4698
4877
  const prepared = await this._prepareRoutedRequest(params);
4699
- const satsSpent = await this._handlePostResponseBalanceUpdate({
4700
- token: prepared.tokenUsed,
4701
- baseUrl: prepared.baseUrlUsed,
4702
- mintUrl: params.mintUrl,
4703
- initialTokenBalance: prepared.tokenBalanceInSats,
4704
- response: prepared.response,
4705
- modelId: prepared.modelId,
4706
- usage: prepared.capturedUsage,
4707
- requestId: prepared.capturedResponseId,
4708
- clientApiKey: prepared.clientApiKey
4709
- });
4710
- prepared.response.satsSpent = satsSpent;
4711
- prepared.response.usage = prepared.capturedUsage;
4712
- prepared.response.requestId = prepared.capturedResponseId;
4713
- return prepared.response;
4714
- }
4715
- async routeRequestToNodeResponse(params) {
4716
- const { res } = params;
4717
- const prepared = await this._prepareRoutedRequest(params);
4718
- res.statusCode = prepared.response.status;
4719
- prepared.response.headers.forEach((value, key) => {
4720
- res.setHeader(key, value);
4721
- });
4722
- const body = prepared.response.body;
4723
- if (!body) {
4878
+ const contentType = prepared.response.headers.get("content-type") || "";
4879
+ const isSSE = contentType.includes("text/event-stream");
4880
+ const runFinalize = async () => {
4881
+ const { capturedUsage, capturedResponseId } = await prepared.usagePromise;
4882
+ const usage = capturedUsage ?? prepared.capturedUsage;
4883
+ const requestId = capturedResponseId ?? prepared.capturedResponseId;
4724
4884
  const satsSpent = await this._handlePostResponseBalanceUpdate({
4725
4885
  token: prepared.tokenUsed,
4726
4886
  baseUrl: prepared.baseUrlUsed,
@@ -4728,55 +4888,29 @@ var RoutstrClient = class {
4728
4888
  initialTokenBalance: prepared.tokenBalanceInSats,
4729
4889
  response: prepared.response,
4730
4890
  modelId: prepared.modelId,
4731
- usage: prepared.capturedUsage,
4732
- requestId: prepared.capturedResponseId,
4891
+ usage,
4892
+ requestId,
4733
4893
  clientApiKey: prepared.clientApiKey
4734
4894
  });
4735
4895
  prepared.response.satsSpent = satsSpent;
4736
- res.end();
4737
- return;
4896
+ prepared.response.usage = usage;
4897
+ prepared.response.requestId = requestId;
4898
+ return satsSpent;
4899
+ };
4900
+ if (isSSE) {
4901
+ const finalizePromise = runFinalize().catch((error) => {
4902
+ this._log("ERROR", "[RoutstrClient] SSE finalize failed:", error);
4903
+ return 0;
4904
+ });
4905
+ prepared.response.finalize = () => finalizePromise;
4906
+ return prepared.response;
4738
4907
  }
4739
- const nodeReadable = Readable.fromWeb(body);
4740
- await new Promise((resolve, reject) => {
4741
- let settled = false;
4742
- const finish = async () => {
4743
- if (settled) return;
4744
- settled = true;
4745
- try {
4746
- const satsSpent = await this._handlePostResponseBalanceUpdate({
4747
- token: prepared.tokenUsed,
4748
- baseUrl: prepared.baseUrlUsed,
4749
- mintUrl: params.mintUrl,
4750
- initialTokenBalance: prepared.tokenBalanceInSats,
4751
- response: prepared.response,
4752
- modelId: prepared.modelId,
4753
- usage: prepared.capturedUsage,
4754
- requestId: prepared.capturedResponseId,
4755
- clientApiKey: prepared.clientApiKey
4756
- });
4757
- prepared.response.satsSpent = satsSpent;
4758
- prepared.response.usage = prepared.capturedUsage;
4759
- prepared.response.requestId = prepared.capturedResponseId;
4760
- resolve();
4761
- } catch (error) {
4762
- reject(error);
4763
- }
4764
- };
4765
- const fail = (error) => {
4766
- if (settled) return;
4767
- settled = true;
4768
- reject(error);
4769
- };
4770
- res.once("finish", finish);
4771
- res.once("close", finish);
4772
- res.once("error", fail);
4773
- nodeReadable.once("error", fail);
4774
- nodeReadable.pipe(res);
4775
- });
4908
+ await runFinalize();
4909
+ return prepared.response;
4776
4910
  }
4777
4911
  async _prepareRoutedRequest(params) {
4778
4912
  const {
4779
- path,
4913
+ path: requestPath,
4780
4914
  method,
4781
4915
  body,
4782
4916
  headers = {},
@@ -4796,9 +4930,23 @@ var RoutstrClient = class {
4796
4930
  );
4797
4931
  selectedModel = providerModel ?? void 0;
4798
4932
  if (selectedModel) {
4933
+ const requestMessages = Array.isArray(
4934
+ body?.messages
4935
+ ) ? body.messages : [];
4936
+ const requestMaxTokens = typeof body?.max_tokens === "number" ? body.max_tokens : void 0;
4937
+ this._log(
4938
+ "DEBUG",
4939
+ "[RoutstrClient] generic request pricing input",
4940
+ {
4941
+ modelId: selectedModel.id,
4942
+ messageCount: requestMessages.length,
4943
+ maxTokens: requestMaxTokens
4944
+ }
4945
+ );
4799
4946
  requiredSats = this.providerManager.getRequiredSatsForModel(
4800
4947
  selectedModel,
4801
- []
4948
+ requestMessages,
4949
+ requestMaxTokens
4802
4950
  );
4803
4951
  }
4804
4952
  }
@@ -4817,7 +4965,7 @@ var RoutstrClient = class {
4817
4965
  const baseHeaders = this._buildBaseHeaders();
4818
4966
  const requestHeaders = this._withAuthHeader(baseHeaders, token);
4819
4967
  const response = await this._makeRequest({
4820
- path,
4968
+ path: requestPath,
4821
4969
  method,
4822
4970
  body: method === "GET" ? void 0 : requestBody,
4823
4971
  baseUrl,
@@ -4835,9 +4983,18 @@ var RoutstrClient = class {
4835
4983
  let processedResponse = response;
4836
4984
  let capturedUsage;
4837
4985
  let capturedResponseId;
4986
+ let usagePromise = Promise.resolve({});
4838
4987
  if (contentType.includes("text/event-stream") && response.body) {
4839
- const nodeReadable = Readable.fromWeb(response.body);
4840
- const sseParser = createSSEParserTransform(
4988
+ const [clientStream, inspectStream] = response.body.tee();
4989
+ processedResponse = new Response(clientStream, {
4990
+ status: response.status,
4991
+ statusText: response.statusText,
4992
+ headers: response.headers
4993
+ });
4994
+ processedResponse.baseUrl = response.baseUrl;
4995
+ processedResponse.token = response.token;
4996
+ usagePromise = inspectSSEWebStream(
4997
+ inspectStream,
4841
4998
  (usage) => {
4842
4999
  capturedUsage = usage;
4843
5000
  processedResponse.usage = usage;
@@ -4847,17 +5004,7 @@ var RoutstrClient = class {
4847
5004
  processedResponse.requestId = responseId;
4848
5005
  }
4849
5006
  );
4850
- const transformed = nodeReadable.pipe(sseParser, { end: true });
4851
- const webStream = Readable.toWeb(
4852
- transformed
4853
- );
4854
- processedResponse = new Response(webStream, {
4855
- status: response.status,
4856
- statusText: response.statusText,
4857
- headers: response.headers
4858
- });
4859
- processedResponse.baseUrl = response.baseUrl;
4860
- processedResponse.token = response.token;
5007
+ processedResponse.usagePromise = usagePromise;
4861
5008
  }
4862
5009
  return {
4863
5010
  response: processedResponse,
@@ -4867,7 +5014,8 @@ var RoutstrClient = class {
4867
5014
  modelId,
4868
5015
  capturedUsage,
4869
5016
  capturedResponseId,
4870
- clientApiKey
5017
+ clientApiKey,
5018
+ usagePromise
4871
5019
  };
4872
5020
  }
4873
5021
  /**
@@ -4916,7 +5064,6 @@ var RoutstrClient = class {
4916
5064
  callbacks.onTokenCreated?.(this._getPendingCashuTokenAmount());
4917
5065
  const baseHeaders = this._buildBaseHeaders(headers);
4918
5066
  const requestHeaders = this._withAuthHeader(baseHeaders, token);
4919
- this.providerManager.resetFailedProviders();
4920
5067
  const providerInfo = await this.providerRegistry.getProviderInfo(baseUrl);
4921
5068
  const providerVersion = providerInfo?.version ?? "";
4922
5069
  let modelIdForRequest = selectedModel.id;
@@ -5144,7 +5291,7 @@ var RoutstrClient = class {
5144
5291
  );
5145
5292
  const currentBalance = currentBalanceInfo.unit === "msat" ? currentBalanceInfo.amount / 1e3 : currentBalanceInfo.amount;
5146
5293
  const shortfall = Math.max(0, params.requiredSats - currentBalance);
5147
- topupAmount = shortfall > 0 ? shortfall : params.requiredSats;
5294
+ topupAmount = shortfall > 0.21 * params.requiredSats ? shortfall : 0.21 * params.requiredSats;
5148
5295
  this._log(
5149
5296
  "DEBUG",
5150
5297
  `The shortfall is: ${shortfall}. requiredSats: ${params.requiredSats}. Current Balance: ${currentBalance} `
@@ -5909,27 +6056,6 @@ async function routeRequests(options) {
5909
6056
  throw error;
5910
6057
  }
5911
6058
  }
5912
- async function routeRequestsToNodeResponse(options) {
5913
- const { res } = options;
5914
- const { client, baseUrl, mintUrl, path, headers, modelId, proxiedBody } = await resolveRouteRequestContext(options);
5915
- try {
5916
- await client.routeRequestToNodeResponse({
5917
- path,
5918
- method: "POST",
5919
- body: proxiedBody,
5920
- headers,
5921
- baseUrl,
5922
- mintUrl,
5923
- modelId,
5924
- res
5925
- });
5926
- } catch (error) {
5927
- if (error instanceof Error && (error.message.includes("401") || error.message.includes("402") || error.message.includes("403"))) {
5928
- throw new Error(`Authentication failed: ${error.message}`);
5929
- }
5930
- throw error;
5931
- }
5932
- }
5933
6059
  function extractMaxTokens(requestBody) {
5934
6060
  if (!requestBody || typeof requestBody !== "object") {
5935
6061
  return void 0;
@@ -5947,6 +6073,6 @@ function extractStream(requestBody) {
5947
6073
  return typeof stream === "boolean" ? stream : void 0;
5948
6074
  }
5949
6075
 
5950
- export { BalanceManager, CashuSpender, FailoverError, InsufficientBalanceError, MintDiscovery, MintDiscoveryError, MintUnreachableError, ModelManager, ModelNotFoundError, NoProvidersAvailableError, ProviderBootstrapError, ProviderError, ProviderManager, RoutstrClient, SDK_STORAGE_KEYS, StreamProcessor, StreamingError, TokenOperationError, createBunSqliteDriver, createBunSqliteUsageTrackingDriver, createDiscoveryAdapterFromStore, createIndexedDBDriver, createIndexedDBUsageTrackingDriver, createMemoryDriver, createMemoryUsageTrackingDriver, createProviderRegistryFromStore, createSSEParserTransform, createSdkStore, createSqliteDriver, createSqliteUsageTrackingDriver, createStorageAdapterFromStore, filterBaseUrlsForTor, getDefaultDiscoveryAdapter, getDefaultProviderRegistry, getDefaultSdkDriver, getDefaultSdkStore, getDefaultStorageAdapter, getDefaultUsageTrackingDriver, getProviderEndpoints, isOnionUrl, isTorContext, localStorageDriver, normalizeProviderUrl, routeRequests, routeRequestsToNodeResponse, setDefaultUsageTrackingDriver };
6076
+ export { BalanceManager, CashuSpender, FailoverError, InsufficientBalanceError, MintDiscovery, MintDiscoveryError, MintUnreachableError, ModelManager, ModelNotFoundError, NoProvidersAvailableError, ProviderBootstrapError, ProviderError, ProviderManager, RoutstrClient, SDK_STORAGE_KEYS, StreamProcessor, StreamingError, TokenOperationError, createBunSqliteDriver, createBunSqliteUsageTrackingDriver, createDiscoveryAdapterFromStore, createIndexedDBDriver, createIndexedDBUsageTrackingDriver, createMemoryDriver, createMemoryUsageTrackingDriver, createProviderRegistryFromStore, createSSEParserTransform, createSdkStore, createSqliteDriver, createSqliteUsageTrackingDriver, createStorageAdapterFromStore, filterBaseUrlsForTor, getDefaultDiscoveryAdapter, getDefaultProviderRegistry, getDefaultSdkDriver, getDefaultSdkStore, getDefaultStorageAdapter, getDefaultUsageTrackingDriver, getProviderEndpoints, inspectSSEWebStream, isOnionUrl, isTorContext, localStorageDriver, normalizeProviderUrl, routeRequests, setDefaultUsageTrackingDriver };
5951
6077
  //# sourceMappingURL=index.mjs.map
5952
6078
  //# sourceMappingURL=index.mjs.map