@routstr/sdk 0.2.12 → 0.3.1

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,10 +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';
7
- import * as fs from 'fs';
8
- import * as path from 'path';
9
- import * as os from 'os';
6
+ import { Transform } from 'stream';
7
+ import { StringDecoder } from 'string_decoder';
10
8
 
11
9
  // core/errors.ts
12
10
  var InsufficientBalanceError = class extends Error {
@@ -387,6 +385,7 @@ var ModelManager = class _ModelManager {
387
385
  if (!(error instanceof Error)) return false;
388
386
  const msg = error.message.toLowerCase();
389
387
  if (msg.includes("fetch failed")) return true;
388
+ if (msg.includes("429")) return true;
390
389
  if (msg.includes("502")) return true;
391
390
  if (msg.includes("503")) return true;
392
391
  if (msg.includes("504")) return true;
@@ -665,6 +664,7 @@ var MintDiscovery = class {
665
664
  if (!(error instanceof Error)) return false;
666
665
  const msg = error.message.toLowerCase();
667
666
  if (msg.includes("fetch failed")) return true;
667
+ if (msg.includes("429")) return true;
668
668
  if (msg.includes("502")) return true;
669
669
  if (msg.includes("503")) return true;
670
670
  if (msg.includes("504")) return true;
@@ -691,10 +691,10 @@ var AuditLogger = class _AuditLogger {
691
691
  const logLine = JSON.stringify(fullEntry) + "\n";
692
692
  if (typeof window === "undefined") {
693
693
  try {
694
- const fs2 = await import('fs');
695
- const path2 = await import('path');
696
- const logPath = path2.join(process.cwd(), "audit.log");
697
- fs2.appendFileSync(logPath, logLine);
694
+ const fs = await import('fs');
695
+ const path = await import('path');
696
+ const logPath = path.join(process.cwd(), "audit.log");
697
+ fs.appendFileSync(logPath, logLine);
698
698
  } catch (error) {
699
699
  console.error("[AuditLogger] Failed to write to file:", error);
700
700
  }
@@ -1273,7 +1273,7 @@ var CashuSpender = class {
1273
1273
  };
1274
1274
 
1275
1275
  // wallet/BalanceManager.ts
1276
- var BalanceManager = class {
1276
+ var BalanceManager = class _BalanceManager {
1277
1277
  constructor(walletAdapter, storageAdapter, providerRegistry, cashuSpender) {
1278
1278
  this.walletAdapter = walletAdapter;
1279
1279
  this.storageAdapter = storageAdapter;
@@ -1290,6 +1290,47 @@ var BalanceManager = class {
1290
1290
  }
1291
1291
  }
1292
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
+ }
1293
1334
  async getBalanceState() {
1294
1335
  const mintBalances = await this.walletAdapter.getBalances();
1295
1336
  const units = this.walletAdapter.getMintUnits();
@@ -1324,6 +1365,20 @@ var BalanceManager = class {
1324
1365
  * @returns Refund result
1325
1366
  */
1326
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) {
1327
1382
  const { mintUrl, baseUrl, apiKey, forceRefund } = options;
1328
1383
  if (!apiKey) {
1329
1384
  return { success: false, message: "No API key to refund" };
@@ -1452,6 +1507,20 @@ var BalanceManager = class {
1452
1507
  * Top up API key balance with a cashu token
1453
1508
  */
1454
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) {
1455
1524
  const { mintUrl, baseUrl, amount, token: providedToken } = options;
1456
1525
  if (!amount || amount <= 0) {
1457
1526
  return { success: false, message: "Invalid top up amount" };
@@ -1612,7 +1681,7 @@ var BalanceManager = class {
1612
1681
  p2pkPubkey
1613
1682
  );
1614
1683
  console.log(
1615
- `[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])])))}`
1616
1685
  );
1617
1686
  return {
1618
1687
  success: true,
@@ -2807,7 +2876,9 @@ var ProviderManager = class _ProviderManager {
2807
2876
  const approximateTokens = apiMessagesNoImages ? Math.ceil(JSON.stringify(apiMessagesNoImages, null, 2).length / 2.84) : 1e4;
2808
2877
  const totalInputTokens = approximateTokens + imageTokens;
2809
2878
  const sp = model?.sats_pricing;
2810
- if (!sp) return 0;
2879
+ if (!sp) {
2880
+ return 0;
2881
+ }
2811
2882
  if (!sp.max_completion_cost) {
2812
2883
  return sp.max_cost ?? 50;
2813
2884
  }
@@ -4516,26 +4587,117 @@ var setDefaultUsageTrackingDriver = (driver) => {
4516
4587
  var getDefaultDiscoveryAdapter = async () => createDiscoveryAdapterFromStore(await getDefaultSdkStore());
4517
4588
  var getDefaultStorageAdapter = async () => createStorageAdapterFromStore(await getDefaultSdkStore());
4518
4589
  var getDefaultProviderRegistry = async () => createProviderRegistryFromStore(await getDefaultSdkStore());
4519
- function createSSEParserTransform(onUsage, onResponseId) {
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");
4520
4607
  let buffer = "";
4521
4608
  let capturedUsage = null;
4609
+ let capturedResponseId;
4522
4610
  let responseIdCaptured = false;
4523
- const mergeUsage = (previous, next) => {
4524
- if (!previous) return next;
4525
- return {
4526
- promptTokens: next.promptTokens > 0 ? next.promptTokens : previous.promptTokens,
4527
- completionTokens: next.completionTokens > 0 ? next.completionTokens : previous.completionTokens,
4528
- totalTokens: next.totalTokens > 0 ? next.totalTokens : previous.totalTokens,
4529
- cost: next.cost > 0 ? next.cost : previous.cost,
4530
- satsCost: next.satsCost > 0 ? next.satsCost : previous.satsCost
4531
- };
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
+ }
4532
4638
  };
4533
- const hasUsageChanged = (previous, next) => {
4534
- if (!previous) return true;
4535
- return previous.promptTokens !== next.promptTokens || previous.completionTokens !== next.completionTokens || previous.totalTokens !== next.totalTokens || previous.cost !== next.cost || previous.satsCost !== next.satsCost;
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);
4536
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
+ }
4694
+ function createSSEParserTransform(onUsage, onResponseId) {
4695
+ let buffer = "";
4696
+ const decoder = new StringDecoder("utf8");
4697
+ let capturedUsage = null;
4698
+ let responseIdCaptured = false;
4537
4699
  const inspectDataPayload = (jsonText) => {
4538
- if (responseIdCaptured && capturedUsage?.satsCost && capturedUsage.totalTokens) {
4700
+ if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
4539
4701
  return;
4540
4702
  }
4541
4703
  const trimmed = jsonText.trim();
@@ -4562,7 +4724,7 @@ function createSSEParserTransform(onUsage, onResponseId) {
4562
4724
  }
4563
4725
  };
4564
4726
  const inspectEventBlock = (eventBlock) => {
4565
- if (responseIdCaptured && capturedUsage?.satsCost && capturedUsage.totalTokens) {
4727
+ if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
4566
4728
  return;
4567
4729
  }
4568
4730
  const lines = eventBlock.split(/\r?\n/);
@@ -4578,32 +4740,35 @@ function createSSEParserTransform(onUsage, onResponseId) {
4578
4740
  const payload = dataParts.join("\n");
4579
4741
  inspectDataPayload(payload);
4580
4742
  };
4581
- const emitEventBlock = (self, eventBlock) => {
4582
- if (eventBlock.length === 0) return;
4583
- inspectEventBlock(eventBlock);
4584
- 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
+ }
4585
4757
  };
4586
4758
  return new Transform({
4587
4759
  transform(chunk, _encoding, callback) {
4588
- buffer += chunk.toString();
4589
- const terminator = /\r?\n\r?\n/g;
4590
- let lastIndex = 0;
4591
- let match;
4592
- while ((match = terminator.exec(buffer)) !== null) {
4593
- const block = buffer.slice(lastIndex, match.index);
4594
- lastIndex = match.index + match[0].length;
4595
- emitEventBlock(this, block);
4596
- }
4597
- if (lastIndex > 0) {
4598
- buffer = buffer.slice(lastIndex);
4599
- }
4760
+ this.push(chunk);
4761
+ buffer += decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
4762
+ processBufferedEvents();
4600
4763
  callback();
4601
4764
  },
4602
4765
  flush(callback) {
4766
+ buffer += decoder.end();
4767
+ processBufferedEvents();
4603
4768
  if (buffer.length > 0) {
4604
4769
  const tail = buffer.replace(/\r?\n+$/, "");
4605
4770
  if (tail.length > 0) {
4606
- emitEventBlock(this, tail);
4771
+ inspectEventBlock(tail);
4607
4772
  }
4608
4773
  buffer = "";
4609
4774
  }
@@ -4611,6 +4776,8 @@ function createSSEParserTransform(onUsage, onResponseId) {
4611
4776
  }
4612
4777
  });
4613
4778
  }
4779
+
4780
+ // client/RoutstrClient.ts
4614
4781
  var TOPUP_MARGIN = 1.2;
4615
4782
  var RoutstrClient = class {
4616
4783
  constructor(walletAdapter, storageAdapter, providerRegistry, alertLevel, mode = "xcashu", options = {}) {
@@ -4710,31 +4877,12 @@ var RoutstrClient = class {
4710
4877
  */
4711
4878
  async routeRequest(params) {
4712
4879
  const prepared = await this._prepareRoutedRequest(params);
4713
- const satsSpent = await this._handlePostResponseBalanceUpdate({
4714
- token: prepared.tokenUsed,
4715
- baseUrl: prepared.baseUrlUsed,
4716
- mintUrl: params.mintUrl,
4717
- initialTokenBalance: prepared.tokenBalanceInSats,
4718
- response: prepared.response,
4719
- modelId: prepared.modelId,
4720
- usage: prepared.capturedUsage,
4721
- requestId: prepared.capturedResponseId,
4722
- clientApiKey: prepared.clientApiKey
4723
- });
4724
- prepared.response.satsSpent = satsSpent;
4725
- prepared.response.usage = prepared.capturedUsage;
4726
- prepared.response.requestId = prepared.capturedResponseId;
4727
- return prepared.response;
4728
- }
4729
- async routeRequestToNodeResponse(params) {
4730
- const { res } = params;
4731
- const prepared = await this._prepareRoutedRequest(params);
4732
- res.statusCode = prepared.response.status;
4733
- prepared.response.headers.forEach((value, key) => {
4734
- res.setHeader(key, value);
4735
- });
4736
- const body = prepared.response.body;
4737
- 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;
4738
4886
  const satsSpent = await this._handlePostResponseBalanceUpdate({
4739
4887
  token: prepared.tokenUsed,
4740
4888
  baseUrl: prepared.baseUrlUsed,
@@ -4742,51 +4890,25 @@ var RoutstrClient = class {
4742
4890
  initialTokenBalance: prepared.tokenBalanceInSats,
4743
4891
  response: prepared.response,
4744
4892
  modelId: prepared.modelId,
4745
- usage: prepared.capturedUsage,
4746
- requestId: prepared.capturedResponseId,
4893
+ usage,
4894
+ requestId,
4747
4895
  clientApiKey: prepared.clientApiKey
4748
4896
  });
4749
4897
  prepared.response.satsSpent = satsSpent;
4750
- res.end();
4751
- 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;
4752
4909
  }
4753
- const nodeReadable = Readable.fromWeb(body);
4754
- await new Promise((resolve, reject) => {
4755
- let settled = false;
4756
- const finish = async () => {
4757
- if (settled) return;
4758
- settled = true;
4759
- try {
4760
- const satsSpent = await this._handlePostResponseBalanceUpdate({
4761
- token: prepared.tokenUsed,
4762
- baseUrl: prepared.baseUrlUsed,
4763
- mintUrl: params.mintUrl,
4764
- initialTokenBalance: prepared.tokenBalanceInSats,
4765
- response: prepared.response,
4766
- modelId: prepared.modelId,
4767
- usage: prepared.capturedUsage,
4768
- requestId: prepared.capturedResponseId,
4769
- clientApiKey: prepared.clientApiKey
4770
- });
4771
- prepared.response.satsSpent = satsSpent;
4772
- prepared.response.usage = prepared.capturedUsage;
4773
- prepared.response.requestId = prepared.capturedResponseId;
4774
- resolve();
4775
- } catch (error) {
4776
- reject(error);
4777
- }
4778
- };
4779
- const fail = (error) => {
4780
- if (settled) return;
4781
- settled = true;
4782
- reject(error);
4783
- };
4784
- res.once("finish", finish);
4785
- res.once("close", finish);
4786
- res.once("error", fail);
4787
- nodeReadable.once("error", fail);
4788
- nodeReadable.pipe(res);
4789
- });
4910
+ await runFinalize();
4911
+ return prepared.response;
4790
4912
  }
4791
4913
  async _prepareRoutedRequest(params) {
4792
4914
  const {
@@ -4810,9 +4932,23 @@ var RoutstrClient = class {
4810
4932
  );
4811
4933
  selectedModel = providerModel ?? void 0;
4812
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
+ );
4813
4948
  requiredSats = this.providerManager.getRequiredSatsForModel(
4814
4949
  selectedModel,
4815
- []
4950
+ requestMessages,
4951
+ requestMaxTokens
4816
4952
  );
4817
4953
  }
4818
4954
  }
@@ -4849,22 +4985,18 @@ var RoutstrClient = class {
4849
4985
  let processedResponse = response;
4850
4986
  let capturedUsage;
4851
4987
  let capturedResponseId;
4988
+ let usagePromise = Promise.resolve({});
4852
4989
  if (contentType.includes("text/event-stream") && response.body) {
4853
- const logDir = path.join(os.homedir(), ".routstrd", "stream-response");
4854
- if (!fs.existsSync(logDir)) {
4855
- fs.mkdirSync(logDir, { recursive: true });
4856
- }
4857
- const logFile = path.join(logDir, `${Date.now()}.jsonl`);
4858
- const logStream = fs.createWriteStream(logFile);
4859
- const nodeReadable = Readable.fromWeb(response.body);
4860
- const loggingTransform = new Transform({
4861
- transform(chunk, encoding, callback) {
4862
- const raw = chunk.toString();
4863
- logStream.write(JSON.stringify({ raw, timestamp: Date.now() }) + "\n");
4864
- callback(null, chunk);
4865
- }
4990
+ const [clientStream, inspectStream] = response.body.tee();
4991
+ processedResponse = new Response(clientStream, {
4992
+ status: response.status,
4993
+ statusText: response.statusText,
4994
+ headers: response.headers
4866
4995
  });
4867
- const sseParser = createSSEParserTransform(
4996
+ processedResponse.baseUrl = response.baseUrl;
4997
+ processedResponse.token = response.token;
4998
+ usagePromise = inspectSSEWebStream(
4999
+ inspectStream,
4868
5000
  (usage) => {
4869
5001
  capturedUsage = usage;
4870
5002
  processedResponse.usage = usage;
@@ -4874,17 +5006,7 @@ var RoutstrClient = class {
4874
5006
  processedResponse.requestId = responseId;
4875
5007
  }
4876
5008
  );
4877
- const transformed = nodeReadable.pipe(loggingTransform).pipe(sseParser, { end: true });
4878
- const webStream = Readable.toWeb(
4879
- transformed
4880
- );
4881
- processedResponse = new Response(webStream, {
4882
- status: response.status,
4883
- statusText: response.statusText,
4884
- headers: response.headers
4885
- });
4886
- processedResponse.baseUrl = response.baseUrl;
4887
- processedResponse.token = response.token;
5009
+ processedResponse.usagePromise = usagePromise;
4888
5010
  }
4889
5011
  return {
4890
5012
  response: processedResponse,
@@ -4894,7 +5016,8 @@ var RoutstrClient = class {
4894
5016
  modelId,
4895
5017
  capturedUsage,
4896
5018
  capturedResponseId,
4897
- clientApiKey
5019
+ clientApiKey,
5020
+ usagePromise
4898
5021
  };
4899
5022
  }
4900
5023
  /**
@@ -5045,9 +5168,9 @@ var RoutstrClient = class {
5045
5168
  * Make the API request with failover support
5046
5169
  */
5047
5170
  async _makeRequest(params) {
5048
- const { path: path2, method, body, baseUrl, token, headers } = params;
5171
+ const { path, method, body, baseUrl, token, headers } = params;
5049
5172
  try {
5050
- const url = `${baseUrl.replace(/\/$/, "")}${path2}`;
5173
+ const url = `${baseUrl.replace(/\/$/, "")}${path}`;
5051
5174
  if (this.mode === "xcashu") this._log("DEBUG", "HEADERS,", headers);
5052
5175
  const response = await fetch(url, {
5053
5176
  method,
@@ -5097,7 +5220,7 @@ var RoutstrClient = class {
5097
5220
  */
5098
5221
  async _handleErrorResponse(params, token, status, requestId, xCashuRefundToken, responseBody, retryCount = 0) {
5099
5222
  const MAX_RETRIES_PER_PROVIDER = 2;
5100
- const { path: path2, method, body, selectedModel, baseUrl, mintUrl } = params;
5223
+ const { path, method, body, selectedModel, baseUrl, mintUrl } = params;
5101
5224
  let tryNextProvider = false;
5102
5225
  const errorMessage = responseBody;
5103
5226
  this._log(
@@ -5169,11 +5292,12 @@ var RoutstrClient = class {
5169
5292
  baseUrl
5170
5293
  );
5171
5294
  const currentBalance = currentBalanceInfo.unit === "msat" ? currentBalanceInfo.amount / 1e3 : currentBalanceInfo.amount;
5172
- const shortfall = Math.max(0, params.requiredSats - currentBalance);
5173
- topupAmount = shortfall > 0 ? shortfall : params.requiredSats;
5295
+ const reservedBalance = currentBalanceInfo.unit === "msat" ? (currentBalanceInfo.reserved ?? 0) / 1e3 : currentBalanceInfo.reserved ?? 0;
5296
+ const shortfall = Math.max(0, params.requiredSats - currentBalance + reservedBalance);
5297
+ topupAmount = shortfall > 0.21 * params.requiredSats ? shortfall : 0.21 * params.requiredSats;
5174
5298
  this._log(
5175
5299
  "DEBUG",
5176
- `The shortfall is: ${shortfall}. requiredSats: ${params.requiredSats}. Current Balance: ${currentBalance} `
5300
+ `The shortfall is: ${shortfall}. requiredSats: ${params.requiredSats}. Current Balance: ${currentBalance}. Reserved Balance: ${reservedBalance}. Available Balance: ${currentBalance - reservedBalance}`
5177
5301
  );
5178
5302
  } catch (e) {
5179
5303
  this._log(
@@ -5318,10 +5442,10 @@ var RoutstrClient = class {
5318
5442
  tryNextProvider = true;
5319
5443
  }
5320
5444
  }
5321
- if ((status === 401 || status === 403 || status === 413 || status === 400 || status === 500 || status === 502 || status === 503 || status === 504 || status === 521) && !tryNextProvider) {
5445
+ if ((status === 401 || status === 403 || status === 413 || status === 400 || status === 429 || status === 500 || status === 502 || status === 503 || status === 504 || status === 521) && !tryNextProvider) {
5322
5446
  this._log(
5323
5447
  "DEBUG",
5324
- `[RoutstrClient] _handleErrorResponse: Status ${status} (auth/server error), attempting refund for ${baseUrl}, mode=${this.mode}`
5448
+ `[RoutstrClient] _handleErrorResponse: Status ${status} (${status === 429 ? "rate limited" : "auth/server error"}), attempting refund for ${baseUrl}, mode=${this.mode}`
5325
5449
  );
5326
5450
  if (this.mode === "apikeys") {
5327
5451
  this._log(
@@ -5399,7 +5523,7 @@ var RoutstrClient = class {
5399
5523
  });
5400
5524
  return this._makeRequest({
5401
5525
  ...params,
5402
- path: path2,
5526
+ path,
5403
5527
  method,
5404
5528
  body,
5405
5529
  baseUrl: nextProvider,
@@ -5807,7 +5931,7 @@ async function resolveRouteRequestContext(options) {
5807
5931
  const {
5808
5932
  modelId,
5809
5933
  requestBody,
5810
- path: path2 = "/v1/chat/completions",
5934
+ path = "/v1/chat/completions",
5811
5935
  headers = {},
5812
5936
  forcedProvider,
5813
5937
  walletAdapter,
@@ -5906,17 +6030,17 @@ async function resolveRouteRequestContext(options) {
5906
6030
  client,
5907
6031
  baseUrl,
5908
6032
  mintUrl,
5909
- path: path2,
6033
+ path,
5910
6034
  headers,
5911
6035
  modelId,
5912
6036
  proxiedBody
5913
6037
  };
5914
6038
  }
5915
6039
  async function routeRequests(options) {
5916
- const { client, baseUrl, mintUrl, path: path2, headers, modelId, proxiedBody } = await resolveRouteRequestContext(options);
6040
+ const { client, baseUrl, mintUrl, path, headers, modelId, proxiedBody } = await resolveRouteRequestContext(options);
5917
6041
  try {
5918
6042
  const response = await client.routeRequest({
5919
- path: path2,
6043
+ path,
5920
6044
  method: "POST",
5921
6045
  body: proxiedBody,
5922
6046
  headers,
@@ -5935,27 +6059,6 @@ async function routeRequests(options) {
5935
6059
  throw error;
5936
6060
  }
5937
6061
  }
5938
- async function routeRequestsToNodeResponse(options) {
5939
- const { res } = options;
5940
- const { client, baseUrl, mintUrl, path: path2, headers, modelId, proxiedBody } = await resolveRouteRequestContext(options);
5941
- try {
5942
- await client.routeRequestToNodeResponse({
5943
- path: path2,
5944
- method: "POST",
5945
- body: proxiedBody,
5946
- headers,
5947
- baseUrl,
5948
- mintUrl,
5949
- modelId,
5950
- res
5951
- });
5952
- } catch (error) {
5953
- if (error instanceof Error && (error.message.includes("401") || error.message.includes("402") || error.message.includes("403"))) {
5954
- throw new Error(`Authentication failed: ${error.message}`);
5955
- }
5956
- throw error;
5957
- }
5958
- }
5959
6062
  function extractMaxTokens(requestBody) {
5960
6063
  if (!requestBody || typeof requestBody !== "object") {
5961
6064
  return void 0;
@@ -5973,6 +6076,6 @@ function extractStream(requestBody) {
5973
6076
  return typeof stream === "boolean" ? stream : void 0;
5974
6077
  }
5975
6078
 
5976
- 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 };
6079
+ 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 };
5977
6080
  //# sourceMappingURL=index.mjs.map
5978
6081
  //# sourceMappingURL=index.mjs.map