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