@routstr/sdk 0.2.9 → 0.2.12

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
@@ -4,6 +4,9 @@ import { tap } from 'rxjs';
4
4
  import { getDecodedToken } from '@cashu/cashu-ts';
5
5
  import { createStore } from 'zustand/vanilla';
6
6
  import { Transform, Readable } from 'stream';
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import * as os from 'os';
7
10
 
8
11
  // core/errors.ts
9
12
  var InsufficientBalanceError = class extends Error {
@@ -688,10 +691,10 @@ var AuditLogger = class _AuditLogger {
688
691
  const logLine = JSON.stringify(fullEntry) + "\n";
689
692
  if (typeof window === "undefined") {
690
693
  try {
691
- const fs = await import('fs');
692
- const path = await import('path');
693
- const logPath = path.join(process.cwd(), "audit.log");
694
- fs.appendFileSync(logPath, logLine);
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);
695
698
  } catch (error) {
696
699
  console.error("[AuditLogger] Failed to write to file:", error);
697
700
  }
@@ -1927,17 +1930,48 @@ function extractResponseId(body) {
1927
1930
  return trimmed.length > 0 ? trimmed : void 0;
1928
1931
  }
1929
1932
  function extractUsageFromSSEJson(parsed, fallbackSatsCost = 0) {
1930
- if (!parsed || typeof parsed !== "object" || !parsed.usage) {
1933
+ if (!parsed || typeof parsed !== "object") {
1934
+ return null;
1935
+ }
1936
+ if (!parsed.usage && parsed.cost && typeof parsed.cost === "object") {
1937
+ const costObj = parsed.cost;
1938
+ const msats2 = costObj.total_msats ?? 0;
1939
+ const cost2 = costObj.total_usd ?? 0;
1940
+ if (msats2 === 0 && cost2 === 0) return null;
1941
+ return {
1942
+ promptTokens: Number(costObj.input_tokens ?? 0),
1943
+ completionTokens: Number(costObj.output_tokens ?? 0),
1944
+ totalTokens: Number((costObj.input_tokens ?? 0) + (costObj.output_tokens ?? 0)),
1945
+ cost: Number(cost2),
1946
+ satsCost: msats2 > 0 ? msats2 / 1e3 : fallbackSatsCost
1947
+ };
1948
+ }
1949
+ if (!parsed.usage) {
1931
1950
  return null;
1932
1951
  }
1933
1952
  const usage = parsed.usage;
1934
1953
  const usageCost = usage.cost;
1935
- const cost = typeof usageCost === "number" ? usageCost : usageCost?.total_usd ?? parsed.metadata?.routstr?.cost?.total_usd ?? 0;
1936
- const msats = parsed.metadata?.routstr?.cost?.total_msats ?? (typeof usage.cost_sats === "number" ? usage.cost_sats * 1e3 : 0);
1954
+ let cost = 0;
1955
+ let msats = 0;
1956
+ if (typeof usageCost === "number") {
1957
+ cost = usageCost;
1958
+ } else if (usageCost && typeof usageCost === "object") {
1959
+ cost = usageCost.total_usd ?? 0;
1960
+ msats = usageCost.total_msats ?? 0;
1961
+ }
1962
+ if (cost === 0) {
1963
+ cost = parsed.metadata?.routstr?.cost?.total_usd ?? 0;
1964
+ }
1965
+ if (msats === 0) {
1966
+ msats = parsed.metadata?.routstr?.cost?.total_msats ?? (typeof usage.cost_sats === "number" ? usage.cost_sats * 1e3 : 0);
1967
+ }
1968
+ const promptTokens = Number(usage.prompt_tokens ?? usage.input_tokens ?? 0);
1969
+ const completionTokens = Number(usage.completion_tokens ?? usage.output_tokens ?? 0);
1970
+ const totalTokens = Number(usage.total_tokens ?? promptTokens + completionTokens);
1937
1971
  const result = {
1938
- promptTokens: Number(usage.prompt_tokens ?? 0),
1939
- completionTokens: Number(usage.completion_tokens ?? 0),
1940
- totalTokens: Number(usage.total_tokens ?? 0),
1972
+ promptTokens,
1973
+ completionTokens,
1974
+ totalTokens,
1941
1975
  cost: Number(cost ?? 0),
1942
1976
  satsCost: msats > 0 ? msats / 1e3 : fallbackSatsCost
1943
1977
  };
@@ -2063,11 +2097,14 @@ var StreamProcessor = class {
2063
2097
  if (parsed.choices?.[0]?.delta?.reasoning) {
2064
2098
  result.reasoning = parsed.choices[0].delta.reasoning;
2065
2099
  }
2066
- if (parsed.usage) {
2067
- result.usage = toUsageStats(extractUsageFromSSEJson(parsed)) ?? {
2068
- total_tokens: parsed.usage.total_tokens,
2069
- prompt_tokens: parsed.usage.prompt_tokens,
2070
- completion_tokens: parsed.usage.completion_tokens
2100
+ const extractedUsage = extractUsageFromSSEJson(parsed);
2101
+ if (extractedUsage) {
2102
+ result.usage = toUsageStats(extractedUsage);
2103
+ } else if (parsed.usage) {
2104
+ result.usage = {
2105
+ total_tokens: parsed.usage.total_tokens ?? parsed.usage.input_tokens + parsed.usage.output_tokens,
2106
+ prompt_tokens: parsed.usage.prompt_tokens ?? parsed.usage.input_tokens,
2107
+ completion_tokens: parsed.usage.completion_tokens ?? parsed.usage.output_tokens
2071
2108
  };
2072
2109
  }
2073
2110
  if (parsed.id) {
@@ -2381,6 +2418,7 @@ var ProviderManager = class _ProviderManager {
2381
2418
  }
2382
2419
  /**
2383
2420
  * Clean up expired cooldown entries
2421
+ * Also removes the provider from failedProviders so it can be retried
2384
2422
  */
2385
2423
  cleanupExpiredCooldowns() {
2386
2424
  const now = Date.now();
@@ -2393,6 +2431,10 @@ var ProviderManager = class _ProviderManager {
2393
2431
  console.log(
2394
2432
  `[cleanupExpiredCooldowns:${this.instanceId}] Removing expired cooldown for ${url} (age: ${age}ms, cooldown: ${_ProviderManager.COOLDOWN_DURATION_MS}ms)`
2395
2433
  );
2434
+ this.failedProviders.delete(url);
2435
+ if (this.store) {
2436
+ this.store.getState().removeFailedProvider(url);
2437
+ }
2396
2438
  }
2397
2439
  return !isExpired;
2398
2440
  }
@@ -2566,22 +2608,49 @@ var ProviderManager = class _ProviderManager {
2566
2608
  const disabledProviders = new Set(
2567
2609
  this.providerRegistry.getDisabledProviders()
2568
2610
  );
2611
+ console.log(
2612
+ `[findNextBestProvider:${this.instanceId}] Starting search for model: ${modelId}`
2613
+ );
2614
+ console.log(
2615
+ `[findNextBestProvider:${this.instanceId}] disabledProviders: ${[...disabledProviders]}`
2616
+ );
2617
+ console.log(
2618
+ `[findNextBestProvider:${this.instanceId}] providersOnCooldown: ${this.providersOnCoolDown.map(([url]) => url)}`
2619
+ );
2569
2620
  const allProviders = this.providerRegistry.getAllProvidersModels();
2621
+ console.log(
2622
+ `[findNextBestProvider:${this.instanceId}] Total providers in registry: ${Object.keys(allProviders).length}`
2623
+ );
2570
2624
  const candidates = [];
2571
2625
  for (const [baseUrl, models] of Object.entries(allProviders)) {
2572
- if (baseUrl === currentBaseUrl || this.failedProviders.has(baseUrl) || disabledProviders.has(baseUrl) || this.isOnCooldown(baseUrl)) {
2626
+ if (baseUrl === currentBaseUrl) {
2627
+ console.log(
2628
+ `[findNextBestProvider:${this.instanceId}] SKIP (current): ${baseUrl}`
2629
+ );
2630
+ continue;
2631
+ }
2632
+ if (disabledProviders.has(baseUrl)) {
2633
+ continue;
2634
+ }
2635
+ if (this.isOnCooldown(baseUrl)) {
2573
2636
  continue;
2574
2637
  }
2575
2638
  if (!torMode && (isOnionUrl(baseUrl) || isInsecureHttpUrl(baseUrl))) {
2576
2639
  continue;
2577
2640
  }
2578
2641
  const model = models.find((m) => m.id === modelId);
2579
- if (!model) continue;
2642
+ if (!model) {
2643
+ continue;
2644
+ }
2580
2645
  const cost = model.sats_pricing?.completion ?? 0;
2581
2646
  candidates.push({ baseUrl, model, cost });
2582
2647
  }
2583
2648
  candidates.sort((a, b) => a.cost - b.cost);
2584
- return candidates.length > 0 ? candidates[0].baseUrl : null;
2649
+ if (candidates.length > 0) {
2650
+ return candidates[0].baseUrl;
2651
+ } else {
2652
+ return null;
2653
+ }
2585
2654
  } catch (error) {
2586
2655
  console.error("Error finding next best provider:", error);
2587
2656
  return null;
@@ -4449,69 +4518,95 @@ var getDefaultStorageAdapter = async () => createStorageAdapterFromStore(await g
4449
4518
  var getDefaultProviderRegistry = async () => createProviderRegistryFromStore(await getDefaultSdkStore());
4450
4519
  function createSSEParserTransform(onUsage, onResponseId) {
4451
4520
  let buffer = "";
4452
- let usageCaptured = false;
4521
+ let capturedUsage = null;
4453
4522
  let responseIdCaptured = false;
4454
- const maybeCaptureUsageFromJson = (jsonText) => {
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
+ };
4532
+ };
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;
4536
+ };
4537
+ const inspectDataPayload = (jsonText) => {
4538
+ if (responseIdCaptured && capturedUsage?.satsCost && capturedUsage.totalTokens) {
4539
+ return;
4540
+ }
4541
+ const trimmed = jsonText.trim();
4542
+ if (!trimmed || trimmed === "[DONE]") return;
4543
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return;
4455
4544
  try {
4456
- const data = JSON.parse(jsonText);
4457
- const responseId = data.id;
4458
- if (typeof responseId === "string" && responseId.trim().length > 0) {
4459
- onResponseId?.(responseId.trim());
4460
- responseIdCaptured = true;
4545
+ const data = JSON.parse(trimmed);
4546
+ if (!responseIdCaptured) {
4547
+ const responseId = data?.id;
4548
+ if (typeof responseId === "string" && responseId.trim().length > 0) {
4549
+ onResponseId?.(responseId.trim());
4550
+ responseIdCaptured = true;
4551
+ }
4461
4552
  }
4462
4553
  const usage = extractUsageFromSSEJson(data);
4463
4554
  if (usage) {
4464
- onUsage(usage);
4465
- usageCaptured = true;
4555
+ const mergedUsage = mergeUsage(capturedUsage, usage);
4556
+ if (hasUsageChanged(capturedUsage, mergedUsage)) {
4557
+ capturedUsage = mergedUsage;
4558
+ onUsage(mergedUsage);
4559
+ }
4466
4560
  }
4467
4561
  } catch {
4468
4562
  }
4469
4563
  };
4470
- const processLine = (self, line) => {
4471
- const trimmed = line.trim();
4472
- if (!trimmed) {
4473
- return;
4474
- }
4475
- if (trimmed === "data: [DONE]" || trimmed === "[DONE]") {
4476
- self.push("data: [DONE]\n\n");
4564
+ const inspectEventBlock = (eventBlock) => {
4565
+ if (responseIdCaptured && capturedUsage?.satsCost && capturedUsage.totalTokens) {
4477
4566
  return;
4478
4567
  }
4479
- if (trimmed.startsWith("data:")) {
4480
- const dataStr = trimmed.startsWith("data: ") ? trimmed.slice(6) : trimmed.slice(5).trimStart();
4481
- if (dataStr === "[DONE]") {
4482
- self.push("data: [DONE]\n\n");
4483
- return;
4568
+ const lines = eventBlock.split(/\r?\n/);
4569
+ const dataParts = [];
4570
+ for (const line of lines) {
4571
+ if (!line || line.startsWith(":")) continue;
4572
+ if (line.startsWith("data:")) {
4573
+ const value = line.startsWith("data: ") ? line.slice(6) : line.slice(5);
4574
+ dataParts.push(value);
4484
4575
  }
4485
- maybeCaptureUsageFromJson(dataStr);
4486
- self.push(`data: ${dataStr}
4487
-
4488
- `);
4489
- return;
4490
- }
4491
- if (trimmed.startsWith("{")) {
4492
- maybeCaptureUsageFromJson(trimmed);
4493
- self.push(`data: ${trimmed}
4494
-
4495
- `);
4496
- return;
4497
4576
  }
4498
- self.push(line + "\n");
4577
+ if (dataParts.length === 0) return;
4578
+ const payload = dataParts.join("\n");
4579
+ inspectDataPayload(payload);
4580
+ };
4581
+ const emitEventBlock = (self, eventBlock) => {
4582
+ if (eventBlock.length === 0) return;
4583
+ inspectEventBlock(eventBlock);
4584
+ self.push(eventBlock + "\n\n");
4499
4585
  };
4500
4586
  return new Transform({
4501
- transform(chunk, encoding, callback) {
4587
+ transform(chunk, _encoding, callback) {
4502
4588
  buffer += chunk.toString();
4503
- const lines = buffer.split(/\r?\n/);
4504
- buffer = lines.pop() || "";
4505
- for (const line of lines) {
4506
- processLine(this, line);
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);
4507
4599
  }
4508
4600
  callback();
4509
4601
  },
4510
4602
  flush(callback) {
4511
- if (buffer.trim()) {
4512
- processLine(this, buffer);
4603
+ if (buffer.length > 0) {
4604
+ const tail = buffer.replace(/\r?\n+$/, "");
4605
+ if (tail.length > 0) {
4606
+ emitEventBlock(this, tail);
4607
+ }
4608
+ buffer = "";
4513
4609
  }
4514
- buffer = "";
4515
4610
  callback();
4516
4611
  }
4517
4612
  });
@@ -4695,7 +4790,7 @@ var RoutstrClient = class {
4695
4790
  }
4696
4791
  async _prepareRoutedRequest(params) {
4697
4792
  const {
4698
- path,
4793
+ path: requestPath,
4699
4794
  method,
4700
4795
  body,
4701
4796
  headers = {},
@@ -4736,7 +4831,7 @@ var RoutstrClient = class {
4736
4831
  const baseHeaders = this._buildBaseHeaders();
4737
4832
  const requestHeaders = this._withAuthHeader(baseHeaders, token);
4738
4833
  const response = await this._makeRequest({
4739
- path,
4834
+ path: requestPath,
4740
4835
  method,
4741
4836
  body: method === "GET" ? void 0 : requestBody,
4742
4837
  baseUrl,
@@ -4755,7 +4850,20 @@ var RoutstrClient = class {
4755
4850
  let capturedUsage;
4756
4851
  let capturedResponseId;
4757
4852
  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);
4758
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
+ }
4866
+ });
4759
4867
  const sseParser = createSSEParserTransform(
4760
4868
  (usage) => {
4761
4869
  capturedUsage = usage;
@@ -4766,7 +4874,7 @@ var RoutstrClient = class {
4766
4874
  processedResponse.requestId = responseId;
4767
4875
  }
4768
4876
  );
4769
- const transformed = nodeReadable.pipe(sseParser, { end: true });
4877
+ const transformed = nodeReadable.pipe(loggingTransform).pipe(sseParser, { end: true });
4770
4878
  const webStream = Readable.toWeb(
4771
4879
  transformed
4772
4880
  );
@@ -4835,7 +4943,6 @@ var RoutstrClient = class {
4835
4943
  callbacks.onTokenCreated?.(this._getPendingCashuTokenAmount());
4836
4944
  const baseHeaders = this._buildBaseHeaders(headers);
4837
4945
  const requestHeaders = this._withAuthHeader(baseHeaders, token);
4838
- this.providerManager.resetFailedProviders();
4839
4946
  const providerInfo = await this.providerRegistry.getProviderInfo(baseUrl);
4840
4947
  const providerVersion = providerInfo?.version ?? "";
4841
4948
  let modelIdForRequest = selectedModel.id;
@@ -4938,9 +5045,9 @@ var RoutstrClient = class {
4938
5045
  * Make the API request with failover support
4939
5046
  */
4940
5047
  async _makeRequest(params) {
4941
- const { path, method, body, baseUrl, token, headers } = params;
5048
+ const { path: path2, method, body, baseUrl, token, headers } = params;
4942
5049
  try {
4943
- const url = `${baseUrl.replace(/\/$/, "")}${path}`;
5050
+ const url = `${baseUrl.replace(/\/$/, "")}${path2}`;
4944
5051
  if (this.mode === "xcashu") this._log("DEBUG", "HEADERS,", headers);
4945
5052
  const response = await fetch(url, {
4946
5053
  method,
@@ -4990,11 +5097,12 @@ var RoutstrClient = class {
4990
5097
  */
4991
5098
  async _handleErrorResponse(params, token, status, requestId, xCashuRefundToken, responseBody, retryCount = 0) {
4992
5099
  const MAX_RETRIES_PER_PROVIDER = 2;
4993
- const { path, method, body, selectedModel, baseUrl, mintUrl } = params;
5100
+ const { path: path2, method, body, selectedModel, baseUrl, mintUrl } = params;
4994
5101
  let tryNextProvider = false;
5102
+ const errorMessage = responseBody;
4995
5103
  this._log(
4996
5104
  "DEBUG",
4997
- `[RoutstrClient] _handleErrorResponse: status=${status}, baseUrl=${baseUrl}, mode=${this.mode}, token preview=${token}, requestId=${requestId}`
5105
+ `[RoutstrClient] _handleErrorResponse: status=${status}, baseUrl=${baseUrl}, mode=${this.mode}, token preview=${token}, requestId=${requestId}, errorMessage=${errorMessage}`
4998
5106
  );
4999
5107
  this._log(
5000
5108
  "DEBUG",
@@ -5291,7 +5399,7 @@ var RoutstrClient = class {
5291
5399
  });
5292
5400
  return this._makeRequest({
5293
5401
  ...params,
5294
- path,
5402
+ path: path2,
5295
5403
  method,
5296
5404
  body,
5297
5405
  baseUrl: nextProvider,
@@ -5699,7 +5807,7 @@ async function resolveRouteRequestContext(options) {
5699
5807
  const {
5700
5808
  modelId,
5701
5809
  requestBody,
5702
- path = "/v1/chat/completions",
5810
+ path: path2 = "/v1/chat/completions",
5703
5811
  headers = {},
5704
5812
  forcedProvider,
5705
5813
  walletAdapter,
@@ -5798,17 +5906,17 @@ async function resolveRouteRequestContext(options) {
5798
5906
  client,
5799
5907
  baseUrl,
5800
5908
  mintUrl,
5801
- path,
5909
+ path: path2,
5802
5910
  headers,
5803
5911
  modelId,
5804
5912
  proxiedBody
5805
5913
  };
5806
5914
  }
5807
5915
  async function routeRequests(options) {
5808
- const { client, baseUrl, mintUrl, path, headers, modelId, proxiedBody } = await resolveRouteRequestContext(options);
5916
+ const { client, baseUrl, mintUrl, path: path2, headers, modelId, proxiedBody } = await resolveRouteRequestContext(options);
5809
5917
  try {
5810
5918
  const response = await client.routeRequest({
5811
- path,
5919
+ path: path2,
5812
5920
  method: "POST",
5813
5921
  body: proxiedBody,
5814
5922
  headers,
@@ -5829,10 +5937,10 @@ async function routeRequests(options) {
5829
5937
  }
5830
5938
  async function routeRequestsToNodeResponse(options) {
5831
5939
  const { res } = options;
5832
- const { client, baseUrl, mintUrl, path, headers, modelId, proxiedBody } = await resolveRouteRequestContext(options);
5940
+ const { client, baseUrl, mintUrl, path: path2, headers, modelId, proxiedBody } = await resolveRouteRequestContext(options);
5833
5941
  try {
5834
5942
  await client.routeRequestToNodeResponse({
5835
- path,
5943
+ path: path2,
5836
5944
  method: "POST",
5837
5945
  body: proxiedBody,
5838
5946
  headers,