@routstr/sdk 0.2.9 → 0.2.11

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.
@@ -372,6 +372,21 @@ interface UsageTrackingData {
372
372
  satsCost: number;
373
373
  }
374
374
 
375
+ /**
376
+ * SSE parser transform that preserves event boundaries verbatim.
377
+ *
378
+ * Unlike a naive line-splitter, this buffers until a full SSE event is
379
+ * received (terminated by a blank line, per the SSE spec), then forwards the
380
+ * entire event unchanged downstream. This means:
381
+ * - Multi-line events (multiple `data:` lines, plus `event:`/`id:`/`retry:`
382
+ * fields) are preserved.
383
+ * - Comments / keepalives (lines beginning with `:`) are preserved.
384
+ * - Chunks that contain multiple events, or events split across chunks, are
385
+ * handled correctly without merging or losing packets.
386
+ *
387
+ * As a side-effect, it inspects `data:` payloads for usage/responseId and
388
+ * invokes the provided callbacks the first time each is seen.
389
+ */
375
390
  declare function createSSEParserTransform(onUsage: (usage: UsageTrackingData) => void, onResponseId?: (responseId: string) => void): Transform;
376
391
 
377
392
  export { type AlertLevel, type DebugLevel, type FetchOptions, type ModelProviderPrice, ProviderManager, type RouteRequestParams, type RouteRequestToNodeResponseParams, RoutstrClient, type RoutstrClientConfig, type RoutstrClientMode, type StreamCallbacks, StreamProcessor, createSSEParserTransform };
@@ -372,6 +372,21 @@ interface UsageTrackingData {
372
372
  satsCost: number;
373
373
  }
374
374
 
375
+ /**
376
+ * SSE parser transform that preserves event boundaries verbatim.
377
+ *
378
+ * Unlike a naive line-splitter, this buffers until a full SSE event is
379
+ * received (terminated by a blank line, per the SSE spec), then forwards the
380
+ * entire event unchanged downstream. This means:
381
+ * - Multi-line events (multiple `data:` lines, plus `event:`/`id:`/`retry:`
382
+ * fields) are preserved.
383
+ * - Comments / keepalives (lines beginning with `:`) are preserved.
384
+ * - Chunks that contain multiple events, or events split across chunks, are
385
+ * handled correctly without merging or losing packets.
386
+ *
387
+ * As a side-effect, it inspects `data:` payloads for usage/responseId and
388
+ * invokes the provided callbacks the first time each is seen.
389
+ */
375
390
  declare function createSSEParserTransform(onUsage: (usage: UsageTrackingData) => void, onResponseId?: (responseId: string) => void): Transform;
376
391
 
377
392
  export { type AlertLevel, type DebugLevel, type FetchOptions, type ModelProviderPrice, ProviderManager, type RouteRequestParams, type RouteRequestToNodeResponseParams, RoutstrClient, type RoutstrClientConfig, type RoutstrClientMode, type StreamCallbacks, StreamProcessor, createSSEParserTransform };
@@ -1295,17 +1295,48 @@ function extractResponseId(body) {
1295
1295
  return trimmed.length > 0 ? trimmed : void 0;
1296
1296
  }
1297
1297
  function extractUsageFromSSEJson(parsed, fallbackSatsCost = 0) {
1298
- if (!parsed || typeof parsed !== "object" || !parsed.usage) {
1298
+ if (!parsed || typeof parsed !== "object") {
1299
+ return null;
1300
+ }
1301
+ if (!parsed.usage && parsed.cost && typeof parsed.cost === "object") {
1302
+ const costObj = parsed.cost;
1303
+ const msats2 = costObj.total_msats ?? 0;
1304
+ const cost2 = costObj.total_usd ?? 0;
1305
+ if (msats2 === 0 && cost2 === 0) return null;
1306
+ return {
1307
+ promptTokens: Number(costObj.input_tokens ?? 0),
1308
+ completionTokens: Number(costObj.output_tokens ?? 0),
1309
+ totalTokens: Number((costObj.input_tokens ?? 0) + (costObj.output_tokens ?? 0)),
1310
+ cost: Number(cost2),
1311
+ satsCost: msats2 > 0 ? msats2 / 1e3 : fallbackSatsCost
1312
+ };
1313
+ }
1314
+ if (!parsed.usage) {
1299
1315
  return null;
1300
1316
  }
1301
1317
  const usage = parsed.usage;
1302
1318
  const usageCost = usage.cost;
1303
- const cost = typeof usageCost === "number" ? usageCost : usageCost?.total_usd ?? parsed.metadata?.routstr?.cost?.total_usd ?? 0;
1304
- const msats = parsed.metadata?.routstr?.cost?.total_msats ?? (typeof usage.cost_sats === "number" ? usage.cost_sats * 1e3 : 0);
1319
+ let cost = 0;
1320
+ let msats = 0;
1321
+ if (typeof usageCost === "number") {
1322
+ cost = usageCost;
1323
+ } else if (usageCost && typeof usageCost === "object") {
1324
+ cost = usageCost.total_usd ?? 0;
1325
+ msats = usageCost.total_msats ?? 0;
1326
+ }
1327
+ if (cost === 0) {
1328
+ cost = parsed.metadata?.routstr?.cost?.total_usd ?? 0;
1329
+ }
1330
+ if (msats === 0) {
1331
+ msats = parsed.metadata?.routstr?.cost?.total_msats ?? (typeof usage.cost_sats === "number" ? usage.cost_sats * 1e3 : 0);
1332
+ }
1333
+ const promptTokens = Number(usage.prompt_tokens ?? usage.input_tokens ?? 0);
1334
+ const completionTokens = Number(usage.completion_tokens ?? usage.output_tokens ?? 0);
1335
+ const totalTokens = Number(usage.total_tokens ?? promptTokens + completionTokens);
1305
1336
  const result = {
1306
- promptTokens: Number(usage.prompt_tokens ?? 0),
1307
- completionTokens: Number(usage.completion_tokens ?? 0),
1308
- totalTokens: Number(usage.total_tokens ?? 0),
1337
+ promptTokens,
1338
+ completionTokens,
1339
+ totalTokens,
1309
1340
  cost: Number(cost ?? 0),
1310
1341
  satsCost: msats > 0 ? msats / 1e3 : fallbackSatsCost
1311
1342
  };
@@ -1431,11 +1462,14 @@ var StreamProcessor = class {
1431
1462
  if (parsed.choices?.[0]?.delta?.reasoning) {
1432
1463
  result.reasoning = parsed.choices[0].delta.reasoning;
1433
1464
  }
1434
- if (parsed.usage) {
1435
- result.usage = toUsageStats(extractUsageFromSSEJson(parsed)) ?? {
1436
- total_tokens: parsed.usage.total_tokens,
1437
- prompt_tokens: parsed.usage.prompt_tokens,
1438
- completion_tokens: parsed.usage.completion_tokens
1465
+ const extractedUsage = extractUsageFromSSEJson(parsed);
1466
+ if (extractedUsage) {
1467
+ result.usage = toUsageStats(extractedUsage);
1468
+ } else if (parsed.usage) {
1469
+ result.usage = {
1470
+ total_tokens: parsed.usage.total_tokens ?? parsed.usage.input_tokens + parsed.usage.output_tokens,
1471
+ prompt_tokens: parsed.usage.prompt_tokens ?? parsed.usage.input_tokens,
1472
+ completion_tokens: parsed.usage.completion_tokens ?? parsed.usage.output_tokens
1439
1473
  };
1440
1474
  }
1441
1475
  if (parsed.id) {
@@ -1878,22 +1912,62 @@ var ProviderManager = class _ProviderManager {
1878
1912
  const disabledProviders = new Set(
1879
1913
  this.providerRegistry.getDisabledProviders()
1880
1914
  );
1915
+ console.log(`[findNextBestProvider:${this.instanceId}] Starting search for model: ${modelId}`);
1916
+ console.log(`[findNextBestProvider:${this.instanceId}] currentBaseUrl: ${currentBaseUrl}`);
1917
+ console.log(`[findNextBestProvider:${this.instanceId}] torMode: ${torMode}`);
1918
+ console.log(`[findNextBestProvider:${this.instanceId}] disabledProviders: ${[...disabledProviders]}`);
1919
+ console.log(`[findNextBestProvider:${this.instanceId}] failedProviders: ${[...this.failedProviders]}`);
1920
+ console.log(`[findNextBestProvider:${this.instanceId}] providersOnCooldown: ${this.providersOnCoolDown.map(([url]) => url)}`);
1881
1921
  const allProviders = this.providerRegistry.getAllProvidersModels();
1922
+ console.log(`[findNextBestProvider:${this.instanceId}] Total providers in registry: ${Object.keys(allProviders).length}`);
1882
1923
  const candidates = [];
1924
+ let skippedCurrent = 0, skippedFailed = 0, skippedDisabled = 0, skippedCooldown = 0, skippedOnion = 0, skippedNoModel = 0;
1883
1925
  for (const [baseUrl, models] of Object.entries(allProviders)) {
1884
- if (baseUrl === currentBaseUrl || this.failedProviders.has(baseUrl) || disabledProviders.has(baseUrl) || this.isOnCooldown(baseUrl)) {
1926
+ if (baseUrl === currentBaseUrl) {
1927
+ console.log(`[findNextBestProvider:${this.instanceId}] SKIP (current): ${baseUrl}`);
1928
+ skippedCurrent++;
1929
+ continue;
1930
+ }
1931
+ if (this.failedProviders.has(baseUrl)) {
1932
+ console.log(`[findNextBestProvider:${this.instanceId}] SKIP (failed): ${baseUrl}`);
1933
+ skippedFailed++;
1934
+ continue;
1935
+ }
1936
+ if (disabledProviders.has(baseUrl)) {
1937
+ console.log(`[findNextBestProvider:${this.instanceId}] SKIP (disabled): ${baseUrl}`);
1938
+ skippedDisabled++;
1939
+ continue;
1940
+ }
1941
+ if (this.isOnCooldown(baseUrl)) {
1942
+ console.log(`[findNextBestProvider:${this.instanceId}] SKIP (cooldown): ${baseUrl}`);
1943
+ skippedCooldown++;
1885
1944
  continue;
1886
1945
  }
1887
1946
  if (!torMode && (isOnionUrl(baseUrl) || isInsecureHttpUrl(baseUrl))) {
1947
+ console.log(`[findNextBestProvider:${this.instanceId}] SKIP (onion/http): ${baseUrl}`);
1948
+ skippedOnion++;
1888
1949
  continue;
1889
1950
  }
1890
1951
  const model = models.find((m) => m.id === modelId);
1891
- if (!model) continue;
1952
+ if (!model) {
1953
+ console.log(`[findNextBestProvider:${this.instanceId}] SKIP (no model ${modelId}): ${baseUrl} has models: ${models.map((m) => m.id).join(", ")}`);
1954
+ skippedNoModel++;
1955
+ continue;
1956
+ }
1892
1957
  const cost = model.sats_pricing?.completion ?? 0;
1958
+ console.log(`[findNextBestProvider:${this.instanceId}] CANDIDATE: ${baseUrl} cost: ${cost}`);
1893
1959
  candidates.push({ baseUrl, model, cost });
1894
1960
  }
1961
+ console.log(`[findNextBestProvider:${this.instanceId}] Skipped: current=${skippedCurrent}, failed=${skippedFailed}, disabled=${skippedDisabled}, cooldown=${skippedCooldown}, onion=${skippedOnion}, noModel=${skippedNoModel}`);
1962
+ console.log(`[findNextBestProvider:${this.instanceId}] Total candidates: ${candidates.length}`);
1895
1963
  candidates.sort((a, b) => a.cost - b.cost);
1896
- return candidates.length > 0 ? candidates[0].baseUrl : null;
1964
+ if (candidates.length > 0) {
1965
+ console.log(`[findNextBestProvider:${this.instanceId}] Selected provider: ${candidates[0].baseUrl} with cost: ${candidates[0].cost}`);
1966
+ return candidates[0].baseUrl;
1967
+ } else {
1968
+ console.log(`[findNextBestProvider:${this.instanceId}] No candidate providers found`);
1969
+ return null;
1970
+ }
1897
1971
  } catch (error) {
1898
1972
  console.error("Error finding next best provider:", error);
1899
1973
  return null;
@@ -3366,67 +3440,74 @@ function createSSEParserTransform(onUsage, onResponseId) {
3366
3440
  let buffer = "";
3367
3441
  let usageCaptured = false;
3368
3442
  let responseIdCaptured = false;
3369
- const maybeCaptureUsageFromJson = (jsonText) => {
3443
+ const inspectDataPayload = (jsonText) => {
3444
+ if (usageCaptured && responseIdCaptured) return;
3445
+ const trimmed = jsonText.trim();
3446
+ if (!trimmed || trimmed === "[DONE]") return;
3447
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return;
3370
3448
  try {
3371
- const data = JSON.parse(jsonText);
3372
- const responseId = data.id;
3373
- if (typeof responseId === "string" && responseId.trim().length > 0) {
3374
- onResponseId?.(responseId.trim());
3375
- responseIdCaptured = true;
3449
+ const data = JSON.parse(trimmed);
3450
+ if (!responseIdCaptured) {
3451
+ const responseId = data?.id;
3452
+ if (typeof responseId === "string" && responseId.trim().length > 0) {
3453
+ onResponseId?.(responseId.trim());
3454
+ responseIdCaptured = true;
3455
+ }
3376
3456
  }
3377
- const usage = extractUsageFromSSEJson(data);
3378
- if (usage) {
3379
- onUsage(usage);
3380
- usageCaptured = true;
3457
+ if (!usageCaptured) {
3458
+ const usage = extractUsageFromSSEJson(data);
3459
+ if (usage) {
3460
+ onUsage(usage);
3461
+ usageCaptured = true;
3462
+ }
3381
3463
  }
3382
3464
  } catch {
3383
3465
  }
3384
3466
  };
3385
- const processLine = (self, line) => {
3386
- const trimmed = line.trim();
3387
- if (!trimmed) {
3388
- return;
3389
- }
3390
- if (trimmed === "data: [DONE]" || trimmed === "[DONE]") {
3391
- self.push("data: [DONE]\n\n");
3392
- return;
3393
- }
3394
- if (trimmed.startsWith("data:")) {
3395
- const dataStr = trimmed.startsWith("data: ") ? trimmed.slice(6) : trimmed.slice(5).trimStart();
3396
- if (dataStr === "[DONE]") {
3397
- self.push("data: [DONE]\n\n");
3398
- return;
3467
+ const inspectEventBlock = (eventBlock) => {
3468
+ if (usageCaptured && responseIdCaptured) return;
3469
+ const lines = eventBlock.split(/\r?\n/);
3470
+ const dataParts = [];
3471
+ for (const line of lines) {
3472
+ if (!line || line.startsWith(":")) continue;
3473
+ if (line.startsWith("data:")) {
3474
+ const value = line.startsWith("data: ") ? line.slice(6) : line.slice(5);
3475
+ dataParts.push(value);
3399
3476
  }
3400
- maybeCaptureUsageFromJson(dataStr);
3401
- self.push(`data: ${dataStr}
3402
-
3403
- `);
3404
- return;
3405
- }
3406
- if (trimmed.startsWith("{")) {
3407
- maybeCaptureUsageFromJson(trimmed);
3408
- self.push(`data: ${trimmed}
3409
-
3410
- `);
3411
- return;
3412
3477
  }
3413
- self.push(line + "\n");
3478
+ if (dataParts.length === 0) return;
3479
+ const payload = dataParts.join("\n");
3480
+ inspectDataPayload(payload);
3481
+ };
3482
+ const emitEventBlock = (self, eventBlock) => {
3483
+ if (eventBlock.length === 0) return;
3484
+ inspectEventBlock(eventBlock);
3485
+ self.push(eventBlock + "\n\n");
3414
3486
  };
3415
3487
  return new stream.Transform({
3416
- transform(chunk, encoding, callback) {
3488
+ transform(chunk, _encoding, callback) {
3417
3489
  buffer += chunk.toString();
3418
- const lines = buffer.split(/\r?\n/);
3419
- buffer = lines.pop() || "";
3420
- for (const line of lines) {
3421
- processLine(this, line);
3490
+ const terminator = /\r?\n\r?\n/g;
3491
+ let lastIndex = 0;
3492
+ let match;
3493
+ while ((match = terminator.exec(buffer)) !== null) {
3494
+ const block = buffer.slice(lastIndex, match.index);
3495
+ lastIndex = match.index + match[0].length;
3496
+ emitEventBlock(this, block);
3497
+ }
3498
+ if (lastIndex > 0) {
3499
+ buffer = buffer.slice(lastIndex);
3422
3500
  }
3423
3501
  callback();
3424
3502
  },
3425
3503
  flush(callback) {
3426
- if (buffer.trim()) {
3427
- processLine(this, buffer);
3504
+ if (buffer.length > 0) {
3505
+ const tail = buffer.replace(/\r?\n+$/, "");
3506
+ if (tail.length > 0) {
3507
+ emitEventBlock(this, tail);
3508
+ }
3509
+ buffer = "";
3428
3510
  }
3429
- buffer = "";
3430
3511
  callback();
3431
3512
  }
3432
3513
  });
@@ -3907,9 +3988,10 @@ var RoutstrClient = class {
3907
3988
  const MAX_RETRIES_PER_PROVIDER = 2;
3908
3989
  const { path, method, body, selectedModel, baseUrl, mintUrl } = params;
3909
3990
  let tryNextProvider = false;
3991
+ const errorMessage = responseBody;
3910
3992
  this._log(
3911
3993
  "DEBUG",
3912
- `[RoutstrClient] _handleErrorResponse: status=${status}, baseUrl=${baseUrl}, mode=${this.mode}, token preview=${token}, requestId=${requestId}`
3994
+ `[RoutstrClient] _handleErrorResponse: status=${status}, baseUrl=${baseUrl}, mode=${this.mode}, token preview=${token}, requestId=${requestId}, errorMessage=${errorMessage}`
3913
3995
  );
3914
3996
  this._log(
3915
3997
  "DEBUG",