@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.
@@ -1,6 +1,9 @@
1
1
  import { getDecodedToken } from '@cashu/cashu-ts';
2
2
  import { createStore } from 'zustand/vanilla';
3
3
  import { Transform, Readable } from 'stream';
4
+ import * as fs from 'fs';
5
+ import * as path from 'path';
6
+ import * as os from 'os';
4
7
 
5
8
  // core/errors.ts
6
9
  var InsufficientBalanceError = class extends Error {
@@ -54,10 +57,10 @@ var AuditLogger = class _AuditLogger {
54
57
  const logLine = JSON.stringify(fullEntry) + "\n";
55
58
  if (typeof window === "undefined") {
56
59
  try {
57
- const fs = await import('fs');
58
- const path = await import('path');
59
- const logPath = path.join(process.cwd(), "audit.log");
60
- fs.appendFileSync(logPath, logLine);
60
+ const fs2 = await import('fs');
61
+ const path2 = await import('path');
62
+ const logPath = path2.join(process.cwd(), "audit.log");
63
+ fs2.appendFileSync(logPath, logLine);
61
64
  } catch (error) {
62
65
  console.error("[AuditLogger] Failed to write to file:", error);
63
66
  }
@@ -1293,17 +1296,48 @@ function extractResponseId(body) {
1293
1296
  return trimmed.length > 0 ? trimmed : void 0;
1294
1297
  }
1295
1298
  function extractUsageFromSSEJson(parsed, fallbackSatsCost = 0) {
1296
- if (!parsed || typeof parsed !== "object" || !parsed.usage) {
1299
+ if (!parsed || typeof parsed !== "object") {
1300
+ return null;
1301
+ }
1302
+ if (!parsed.usage && parsed.cost && typeof parsed.cost === "object") {
1303
+ const costObj = parsed.cost;
1304
+ const msats2 = costObj.total_msats ?? 0;
1305
+ const cost2 = costObj.total_usd ?? 0;
1306
+ if (msats2 === 0 && cost2 === 0) return null;
1307
+ return {
1308
+ promptTokens: Number(costObj.input_tokens ?? 0),
1309
+ completionTokens: Number(costObj.output_tokens ?? 0),
1310
+ totalTokens: Number((costObj.input_tokens ?? 0) + (costObj.output_tokens ?? 0)),
1311
+ cost: Number(cost2),
1312
+ satsCost: msats2 > 0 ? msats2 / 1e3 : fallbackSatsCost
1313
+ };
1314
+ }
1315
+ if (!parsed.usage) {
1297
1316
  return null;
1298
1317
  }
1299
1318
  const usage = parsed.usage;
1300
1319
  const usageCost = usage.cost;
1301
- const cost = typeof usageCost === "number" ? usageCost : usageCost?.total_usd ?? parsed.metadata?.routstr?.cost?.total_usd ?? 0;
1302
- const msats = parsed.metadata?.routstr?.cost?.total_msats ?? (typeof usage.cost_sats === "number" ? usage.cost_sats * 1e3 : 0);
1320
+ let cost = 0;
1321
+ let msats = 0;
1322
+ if (typeof usageCost === "number") {
1323
+ cost = usageCost;
1324
+ } else if (usageCost && typeof usageCost === "object") {
1325
+ cost = usageCost.total_usd ?? 0;
1326
+ msats = usageCost.total_msats ?? 0;
1327
+ }
1328
+ if (cost === 0) {
1329
+ cost = parsed.metadata?.routstr?.cost?.total_usd ?? 0;
1330
+ }
1331
+ if (msats === 0) {
1332
+ msats = parsed.metadata?.routstr?.cost?.total_msats ?? (typeof usage.cost_sats === "number" ? usage.cost_sats * 1e3 : 0);
1333
+ }
1334
+ const promptTokens = Number(usage.prompt_tokens ?? usage.input_tokens ?? 0);
1335
+ const completionTokens = Number(usage.completion_tokens ?? usage.output_tokens ?? 0);
1336
+ const totalTokens = Number(usage.total_tokens ?? promptTokens + completionTokens);
1303
1337
  const result = {
1304
- promptTokens: Number(usage.prompt_tokens ?? 0),
1305
- completionTokens: Number(usage.completion_tokens ?? 0),
1306
- totalTokens: Number(usage.total_tokens ?? 0),
1338
+ promptTokens,
1339
+ completionTokens,
1340
+ totalTokens,
1307
1341
  cost: Number(cost ?? 0),
1308
1342
  satsCost: msats > 0 ? msats / 1e3 : fallbackSatsCost
1309
1343
  };
@@ -1429,11 +1463,14 @@ var StreamProcessor = class {
1429
1463
  if (parsed.choices?.[0]?.delta?.reasoning) {
1430
1464
  result.reasoning = parsed.choices[0].delta.reasoning;
1431
1465
  }
1432
- if (parsed.usage) {
1433
- result.usage = toUsageStats(extractUsageFromSSEJson(parsed)) ?? {
1434
- total_tokens: parsed.usage.total_tokens,
1435
- prompt_tokens: parsed.usage.prompt_tokens,
1436
- completion_tokens: parsed.usage.completion_tokens
1466
+ const extractedUsage = extractUsageFromSSEJson(parsed);
1467
+ if (extractedUsage) {
1468
+ result.usage = toUsageStats(extractedUsage);
1469
+ } else if (parsed.usage) {
1470
+ result.usage = {
1471
+ total_tokens: parsed.usage.total_tokens ?? parsed.usage.input_tokens + parsed.usage.output_tokens,
1472
+ prompt_tokens: parsed.usage.prompt_tokens ?? parsed.usage.input_tokens,
1473
+ completion_tokens: parsed.usage.completion_tokens ?? parsed.usage.output_tokens
1437
1474
  };
1438
1475
  }
1439
1476
  if (parsed.id) {
@@ -1691,6 +1728,7 @@ var ProviderManager = class _ProviderManager {
1691
1728
  }
1692
1729
  /**
1693
1730
  * Clean up expired cooldown entries
1731
+ * Also removes the provider from failedProviders so it can be retried
1694
1732
  */
1695
1733
  cleanupExpiredCooldowns() {
1696
1734
  const now = Date.now();
@@ -1703,6 +1741,10 @@ var ProviderManager = class _ProviderManager {
1703
1741
  console.log(
1704
1742
  `[cleanupExpiredCooldowns:${this.instanceId}] Removing expired cooldown for ${url} (age: ${age}ms, cooldown: ${_ProviderManager.COOLDOWN_DURATION_MS}ms)`
1705
1743
  );
1744
+ this.failedProviders.delete(url);
1745
+ if (this.store) {
1746
+ this.store.getState().removeFailedProvider(url);
1747
+ }
1706
1748
  }
1707
1749
  return !isExpired;
1708
1750
  }
@@ -1876,22 +1918,49 @@ var ProviderManager = class _ProviderManager {
1876
1918
  const disabledProviders = new Set(
1877
1919
  this.providerRegistry.getDisabledProviders()
1878
1920
  );
1921
+ console.log(
1922
+ `[findNextBestProvider:${this.instanceId}] Starting search for model: ${modelId}`
1923
+ );
1924
+ console.log(
1925
+ `[findNextBestProvider:${this.instanceId}] disabledProviders: ${[...disabledProviders]}`
1926
+ );
1927
+ console.log(
1928
+ `[findNextBestProvider:${this.instanceId}] providersOnCooldown: ${this.providersOnCoolDown.map(([url]) => url)}`
1929
+ );
1879
1930
  const allProviders = this.providerRegistry.getAllProvidersModels();
1931
+ console.log(
1932
+ `[findNextBestProvider:${this.instanceId}] Total providers in registry: ${Object.keys(allProviders).length}`
1933
+ );
1880
1934
  const candidates = [];
1881
1935
  for (const [baseUrl, models] of Object.entries(allProviders)) {
1882
- if (baseUrl === currentBaseUrl || this.failedProviders.has(baseUrl) || disabledProviders.has(baseUrl) || this.isOnCooldown(baseUrl)) {
1936
+ if (baseUrl === currentBaseUrl) {
1937
+ console.log(
1938
+ `[findNextBestProvider:${this.instanceId}] SKIP (current): ${baseUrl}`
1939
+ );
1940
+ continue;
1941
+ }
1942
+ if (disabledProviders.has(baseUrl)) {
1943
+ continue;
1944
+ }
1945
+ if (this.isOnCooldown(baseUrl)) {
1883
1946
  continue;
1884
1947
  }
1885
1948
  if (!torMode && (isOnionUrl(baseUrl) || isInsecureHttpUrl(baseUrl))) {
1886
1949
  continue;
1887
1950
  }
1888
1951
  const model = models.find((m) => m.id === modelId);
1889
- if (!model) continue;
1952
+ if (!model) {
1953
+ continue;
1954
+ }
1890
1955
  const cost = model.sats_pricing?.completion ?? 0;
1891
1956
  candidates.push({ baseUrl, model, cost });
1892
1957
  }
1893
1958
  candidates.sort((a, b) => a.cost - b.cost);
1894
- return candidates.length > 0 ? candidates[0].baseUrl : null;
1959
+ if (candidates.length > 0) {
1960
+ return candidates[0].baseUrl;
1961
+ } else {
1962
+ return null;
1963
+ }
1895
1964
  } catch (error) {
1896
1965
  console.error("Error finding next best provider:", error);
1897
1966
  return null;
@@ -3362,69 +3431,95 @@ var getDefaultUsageTrackingDriver = () => {
3362
3431
  };
3363
3432
  function createSSEParserTransform(onUsage, onResponseId) {
3364
3433
  let buffer = "";
3365
- let usageCaptured = false;
3434
+ let capturedUsage = null;
3366
3435
  let responseIdCaptured = false;
3367
- const maybeCaptureUsageFromJson = (jsonText) => {
3436
+ const mergeUsage = (previous, next) => {
3437
+ if (!previous) return next;
3438
+ return {
3439
+ promptTokens: next.promptTokens > 0 ? next.promptTokens : previous.promptTokens,
3440
+ completionTokens: next.completionTokens > 0 ? next.completionTokens : previous.completionTokens,
3441
+ totalTokens: next.totalTokens > 0 ? next.totalTokens : previous.totalTokens,
3442
+ cost: next.cost > 0 ? next.cost : previous.cost,
3443
+ satsCost: next.satsCost > 0 ? next.satsCost : previous.satsCost
3444
+ };
3445
+ };
3446
+ const hasUsageChanged = (previous, next) => {
3447
+ if (!previous) return true;
3448
+ return previous.promptTokens !== next.promptTokens || previous.completionTokens !== next.completionTokens || previous.totalTokens !== next.totalTokens || previous.cost !== next.cost || previous.satsCost !== next.satsCost;
3449
+ };
3450
+ const inspectDataPayload = (jsonText) => {
3451
+ if (responseIdCaptured && capturedUsage?.satsCost && capturedUsage.totalTokens) {
3452
+ return;
3453
+ }
3454
+ const trimmed = jsonText.trim();
3455
+ if (!trimmed || trimmed === "[DONE]") return;
3456
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return;
3368
3457
  try {
3369
- const data = JSON.parse(jsonText);
3370
- const responseId = data.id;
3371
- if (typeof responseId === "string" && responseId.trim().length > 0) {
3372
- onResponseId?.(responseId.trim());
3373
- responseIdCaptured = true;
3458
+ const data = JSON.parse(trimmed);
3459
+ if (!responseIdCaptured) {
3460
+ const responseId = data?.id;
3461
+ if (typeof responseId === "string" && responseId.trim().length > 0) {
3462
+ onResponseId?.(responseId.trim());
3463
+ responseIdCaptured = true;
3464
+ }
3374
3465
  }
3375
3466
  const usage = extractUsageFromSSEJson(data);
3376
3467
  if (usage) {
3377
- onUsage(usage);
3378
- usageCaptured = true;
3468
+ const mergedUsage = mergeUsage(capturedUsage, usage);
3469
+ if (hasUsageChanged(capturedUsage, mergedUsage)) {
3470
+ capturedUsage = mergedUsage;
3471
+ onUsage(mergedUsage);
3472
+ }
3379
3473
  }
3380
3474
  } catch {
3381
3475
  }
3382
3476
  };
3383
- const processLine = (self, line) => {
3384
- const trimmed = line.trim();
3385
- if (!trimmed) {
3386
- return;
3387
- }
3388
- if (trimmed === "data: [DONE]" || trimmed === "[DONE]") {
3389
- self.push("data: [DONE]\n\n");
3477
+ const inspectEventBlock = (eventBlock) => {
3478
+ if (responseIdCaptured && capturedUsage?.satsCost && capturedUsage.totalTokens) {
3390
3479
  return;
3391
3480
  }
3392
- if (trimmed.startsWith("data:")) {
3393
- const dataStr = trimmed.startsWith("data: ") ? trimmed.slice(6) : trimmed.slice(5).trimStart();
3394
- if (dataStr === "[DONE]") {
3395
- self.push("data: [DONE]\n\n");
3396
- return;
3481
+ const lines = eventBlock.split(/\r?\n/);
3482
+ const dataParts = [];
3483
+ for (const line of lines) {
3484
+ if (!line || line.startsWith(":")) continue;
3485
+ if (line.startsWith("data:")) {
3486
+ const value = line.startsWith("data: ") ? line.slice(6) : line.slice(5);
3487
+ dataParts.push(value);
3397
3488
  }
3398
- maybeCaptureUsageFromJson(dataStr);
3399
- self.push(`data: ${dataStr}
3400
-
3401
- `);
3402
- return;
3403
- }
3404
- if (trimmed.startsWith("{")) {
3405
- maybeCaptureUsageFromJson(trimmed);
3406
- self.push(`data: ${trimmed}
3407
-
3408
- `);
3409
- return;
3410
3489
  }
3411
- self.push(line + "\n");
3490
+ if (dataParts.length === 0) return;
3491
+ const payload = dataParts.join("\n");
3492
+ inspectDataPayload(payload);
3493
+ };
3494
+ const emitEventBlock = (self, eventBlock) => {
3495
+ if (eventBlock.length === 0) return;
3496
+ inspectEventBlock(eventBlock);
3497
+ self.push(eventBlock + "\n\n");
3412
3498
  };
3413
3499
  return new Transform({
3414
- transform(chunk, encoding, callback) {
3500
+ transform(chunk, _encoding, callback) {
3415
3501
  buffer += chunk.toString();
3416
- const lines = buffer.split(/\r?\n/);
3417
- buffer = lines.pop() || "";
3418
- for (const line of lines) {
3419
- processLine(this, line);
3502
+ const terminator = /\r?\n\r?\n/g;
3503
+ let lastIndex = 0;
3504
+ let match;
3505
+ while ((match = terminator.exec(buffer)) !== null) {
3506
+ const block = buffer.slice(lastIndex, match.index);
3507
+ lastIndex = match.index + match[0].length;
3508
+ emitEventBlock(this, block);
3509
+ }
3510
+ if (lastIndex > 0) {
3511
+ buffer = buffer.slice(lastIndex);
3420
3512
  }
3421
3513
  callback();
3422
3514
  },
3423
3515
  flush(callback) {
3424
- if (buffer.trim()) {
3425
- processLine(this, buffer);
3516
+ if (buffer.length > 0) {
3517
+ const tail = buffer.replace(/\r?\n+$/, "");
3518
+ if (tail.length > 0) {
3519
+ emitEventBlock(this, tail);
3520
+ }
3521
+ buffer = "";
3426
3522
  }
3427
- buffer = "";
3428
3523
  callback();
3429
3524
  }
3430
3525
  });
@@ -3608,7 +3703,7 @@ var RoutstrClient = class {
3608
3703
  }
3609
3704
  async _prepareRoutedRequest(params) {
3610
3705
  const {
3611
- path,
3706
+ path: requestPath,
3612
3707
  method,
3613
3708
  body,
3614
3709
  headers = {},
@@ -3649,7 +3744,7 @@ var RoutstrClient = class {
3649
3744
  const baseHeaders = this._buildBaseHeaders();
3650
3745
  const requestHeaders = this._withAuthHeader(baseHeaders, token);
3651
3746
  const response = await this._makeRequest({
3652
- path,
3747
+ path: requestPath,
3653
3748
  method,
3654
3749
  body: method === "GET" ? void 0 : requestBody,
3655
3750
  baseUrl,
@@ -3668,7 +3763,20 @@ var RoutstrClient = class {
3668
3763
  let capturedUsage;
3669
3764
  let capturedResponseId;
3670
3765
  if (contentType.includes("text/event-stream") && response.body) {
3766
+ const logDir = path.join(os.homedir(), ".routstrd", "stream-response");
3767
+ if (!fs.existsSync(logDir)) {
3768
+ fs.mkdirSync(logDir, { recursive: true });
3769
+ }
3770
+ const logFile = path.join(logDir, `${Date.now()}.jsonl`);
3771
+ const logStream = fs.createWriteStream(logFile);
3671
3772
  const nodeReadable = Readable.fromWeb(response.body);
3773
+ const loggingTransform = new Transform({
3774
+ transform(chunk, encoding, callback) {
3775
+ const raw = chunk.toString();
3776
+ logStream.write(JSON.stringify({ raw, timestamp: Date.now() }) + "\n");
3777
+ callback(null, chunk);
3778
+ }
3779
+ });
3672
3780
  const sseParser = createSSEParserTransform(
3673
3781
  (usage) => {
3674
3782
  capturedUsage = usage;
@@ -3679,7 +3787,7 @@ var RoutstrClient = class {
3679
3787
  processedResponse.requestId = responseId;
3680
3788
  }
3681
3789
  );
3682
- const transformed = nodeReadable.pipe(sseParser, { end: true });
3790
+ const transformed = nodeReadable.pipe(loggingTransform).pipe(sseParser, { end: true });
3683
3791
  const webStream = Readable.toWeb(
3684
3792
  transformed
3685
3793
  );
@@ -3748,7 +3856,6 @@ var RoutstrClient = class {
3748
3856
  callbacks.onTokenCreated?.(this._getPendingCashuTokenAmount());
3749
3857
  const baseHeaders = this._buildBaseHeaders(headers);
3750
3858
  const requestHeaders = this._withAuthHeader(baseHeaders, token);
3751
- this.providerManager.resetFailedProviders();
3752
3859
  const providerInfo = await this.providerRegistry.getProviderInfo(baseUrl);
3753
3860
  const providerVersion = providerInfo?.version ?? "";
3754
3861
  let modelIdForRequest = selectedModel.id;
@@ -3851,9 +3958,9 @@ var RoutstrClient = class {
3851
3958
  * Make the API request with failover support
3852
3959
  */
3853
3960
  async _makeRequest(params) {
3854
- const { path, method, body, baseUrl, token, headers } = params;
3961
+ const { path: path2, method, body, baseUrl, token, headers } = params;
3855
3962
  try {
3856
- const url = `${baseUrl.replace(/\/$/, "")}${path}`;
3963
+ const url = `${baseUrl.replace(/\/$/, "")}${path2}`;
3857
3964
  if (this.mode === "xcashu") this._log("DEBUG", "HEADERS,", headers);
3858
3965
  const response = await fetch(url, {
3859
3966
  method,
@@ -3903,11 +4010,12 @@ var RoutstrClient = class {
3903
4010
  */
3904
4011
  async _handleErrorResponse(params, token, status, requestId, xCashuRefundToken, responseBody, retryCount = 0) {
3905
4012
  const MAX_RETRIES_PER_PROVIDER = 2;
3906
- const { path, method, body, selectedModel, baseUrl, mintUrl } = params;
4013
+ const { path: path2, method, body, selectedModel, baseUrl, mintUrl } = params;
3907
4014
  let tryNextProvider = false;
4015
+ const errorMessage = responseBody;
3908
4016
  this._log(
3909
4017
  "DEBUG",
3910
- `[RoutstrClient] _handleErrorResponse: status=${status}, baseUrl=${baseUrl}, mode=${this.mode}, token preview=${token}, requestId=${requestId}`
4018
+ `[RoutstrClient] _handleErrorResponse: status=${status}, baseUrl=${baseUrl}, mode=${this.mode}, token preview=${token}, requestId=${requestId}, errorMessage=${errorMessage}`
3911
4019
  );
3912
4020
  this._log(
3913
4021
  "DEBUG",
@@ -4204,7 +4312,7 @@ var RoutstrClient = class {
4204
4312
  });
4205
4313
  return this._makeRequest({
4206
4314
  ...params,
4207
- path,
4315
+ path: path2,
4208
4316
  method,
4209
4317
  body,
4210
4318
  baseUrl: nextProvider,