@routstr/sdk 0.3.8 → 0.3.10

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.
Files changed (65) hide show
  1. package/dist/browser.d.mts +12 -0
  2. package/dist/browser.d.ts +12 -0
  3. package/dist/browser.js +6278 -0
  4. package/dist/browser.js.map +1 -0
  5. package/dist/browser.mjs +6230 -0
  6. package/dist/browser.mjs.map +1 -0
  7. package/dist/bun.d.mts +29 -0
  8. package/dist/bun.d.ts +29 -0
  9. package/dist/bun.js +6586 -0
  10. package/dist/bun.js.map +1 -0
  11. package/dist/bun.mjs +6532 -0
  12. package/dist/bun.mjs.map +1 -0
  13. package/dist/bunSqlite-BMTseLIz.d.ts +18 -0
  14. package/dist/bunSqlite-D6AreVE2.d.mts +18 -0
  15. package/dist/client/index.d.mts +63 -41
  16. package/dist/client/index.d.ts +63 -41
  17. package/dist/client/index.js +1223 -1658
  18. package/dist/client/index.js.map +1 -1
  19. package/dist/client/index.mjs +1223 -1659
  20. package/dist/client/index.mjs.map +1 -1
  21. package/dist/discovery/index.d.mts +67 -3
  22. package/dist/discovery/index.d.ts +67 -3
  23. package/dist/discovery/index.js +242 -79
  24. package/dist/discovery/index.js.map +1 -1
  25. package/dist/discovery/index.mjs +242 -79
  26. package/dist/discovery/index.mjs.map +1 -1
  27. package/dist/index.d.mts +5 -4
  28. package/dist/index.d.ts +5 -4
  29. package/dist/index.js +1975 -2004
  30. package/dist/index.js.map +1 -1
  31. package/dist/index.mjs +1973 -2001
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/node.d.mts +22 -0
  34. package/dist/node.d.ts +22 -0
  35. package/dist/node.js +6651 -0
  36. package/dist/node.js.map +1 -0
  37. package/dist/node.mjs +6599 -0
  38. package/dist/node.mjs.map +1 -0
  39. package/dist/storage/bun.d.mts +16 -0
  40. package/dist/storage/bun.d.ts +16 -0
  41. package/dist/storage/bun.js +1801 -0
  42. package/dist/storage/bun.js.map +1 -0
  43. package/dist/storage/bun.mjs +1777 -0
  44. package/dist/storage/bun.mjs.map +1 -0
  45. package/dist/storage/index.d.mts +30 -30
  46. package/dist/storage/index.d.ts +30 -30
  47. package/dist/storage/index.js +393 -625
  48. package/dist/storage/index.js.map +1 -1
  49. package/dist/storage/index.mjs +392 -622
  50. package/dist/storage/index.mjs.map +1 -1
  51. package/dist/storage/node.d.mts +22 -0
  52. package/dist/storage/node.d.ts +22 -0
  53. package/dist/storage/node.js +1864 -0
  54. package/dist/storage/node.js.map +1 -0
  55. package/dist/storage/node.mjs +1842 -0
  56. package/dist/storage/node.mjs.map +1 -0
  57. package/dist/{store-C6dfj1cc.d.mts → store-BiuM2V9N.d.mts} +14 -0
  58. package/dist/{store-58VcEUoA.d.ts → store-C8MZlfuz.d.ts} +14 -0
  59. package/dist/wallet/index.d.mts +4 -0
  60. package/dist/wallet/index.d.ts +4 -0
  61. package/dist/wallet/index.js +11 -4
  62. package/dist/wallet/index.js.map +1 -1
  63. package/dist/wallet/index.mjs +11 -4
  64. package/dist/wallet/index.mjs.map +1 -1
  65. package/package.json +28 -2
@@ -605,7 +605,7 @@ var CashuSpender = class {
605
605
  });
606
606
  continue;
607
607
  }
608
- if (balanceResult.amount >= 0) {
608
+ if (balanceResult.amount >= 0 && !balanceResult.balanceUnknown) {
609
609
  const balanceSat = balanceResult.unit === "msat" ? Math.floor(balanceResult.amount / 1e3) : balanceResult.amount;
610
610
  this.storageAdapter.updateApiKeyBalance(
611
611
  apiKeyEntry.baseUrl,
@@ -1354,17 +1354,24 @@ var BalanceManager = class _BalanceManager {
1354
1354
  this.logger.warn("getTokenBalance: FAILED", data);
1355
1355
  const isInvalidApiKey = response.status === 401 && data?.detail?.error?.code === "invalid_api_key" && data?.detail?.error?.message?.includes("proofs already spent");
1356
1356
  return {
1357
- amount: -1,
1357
+ amount: 0,
1358
1358
  reserved: data.reserved ?? 0,
1359
1359
  unit: "msat",
1360
1360
  apiKey: data.api_key,
1361
- isInvalidApiKey
1361
+ isInvalidApiKey,
1362
+ balanceUnknown: true
1362
1363
  };
1363
1364
  }
1364
1365
  } catch (error) {
1365
1366
  this.logger.error("getTokenBalance error", error);
1366
1367
  }
1367
- return { amount: -1, reserved: 0, unit: "sat", apiKey: "" };
1368
+ return {
1369
+ amount: 0,
1370
+ reserved: 0,
1371
+ unit: "sat",
1372
+ apiKey: "",
1373
+ balanceUnknown: true
1374
+ };
1368
1375
  }
1369
1376
  /**
1370
1377
  * Handle topup errors with specific error types
@@ -1399,726 +1406,409 @@ var BalanceManager = class _BalanceManager {
1399
1406
  }
1400
1407
  };
1401
1408
 
1402
- // client/usage.ts
1403
- function extractUsageFromResponseBody(body, fallbackSatsCost = 0) {
1404
- if (!body || typeof body !== "object") return null;
1405
- const usage = body.usage;
1406
- if (!usage || typeof usage !== "object") return null;
1407
- const promptTokens = Number(usage.prompt_tokens ?? 0);
1408
- const completionTokens = Number(usage.completion_tokens ?? 0);
1409
- const totalTokens = Number(usage.total_tokens ?? 0);
1410
- const costValue = usage.cost;
1411
- let cost = 0;
1412
- let satsCost = fallbackSatsCost;
1413
- if (typeof costValue === "number") {
1414
- cost = costValue;
1415
- } else if (costValue && typeof costValue === "object") {
1416
- const costObj = costValue;
1417
- const totalUsd = costObj.total_usd;
1418
- const totalMsats = costObj.total_msats;
1419
- cost = typeof totalUsd === "number" ? totalUsd : 0;
1420
- if (typeof totalMsats === "number") {
1421
- satsCost = totalMsats / 1e3;
1422
- }
1409
+ // utils/torUtils.ts
1410
+ var TOR_ONION_SUFFIX = ".onion";
1411
+ var isTorContext = () => {
1412
+ if (typeof window === "undefined") return false;
1413
+ const hostname = window.location.hostname.toLowerCase();
1414
+ return hostname.endsWith(TOR_ONION_SUFFIX);
1415
+ };
1416
+ var isOnionUrl = (url) => {
1417
+ if (!url) return false;
1418
+ const trimmed = url.trim().toLowerCase();
1419
+ if (!trimmed) return false;
1420
+ try {
1421
+ const candidate = trimmed.startsWith("http") ? trimmed : `http://${trimmed}`;
1422
+ return new URL(candidate).hostname.endsWith(TOR_ONION_SUFFIX);
1423
+ } catch {
1424
+ return trimmed.includes(TOR_ONION_SUFFIX);
1423
1425
  }
1424
- if (promptTokens === 0 && completionTokens === 0 && totalTokens === 0 && cost === 0 && satsCost === 0) {
1426
+ };
1427
+
1428
+ // client/ProviderManager.ts
1429
+ function getImageResolutionFromDataUrl(dataUrl) {
1430
+ try {
1431
+ if (typeof dataUrl !== "string" || !dataUrl.startsWith("data:"))
1432
+ return null;
1433
+ const commaIdx = dataUrl.indexOf(",");
1434
+ if (commaIdx === -1) return null;
1435
+ const meta = dataUrl.slice(5, commaIdx);
1436
+ const base64 = dataUrl.slice(commaIdx + 1);
1437
+ const binary = typeof atob === "function" ? atob(base64) : Buffer.from(base64, "base64").toString("binary");
1438
+ const len = binary.length;
1439
+ const bytes = new Uint8Array(len);
1440
+ for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i);
1441
+ const isPNG = meta.includes("image/png");
1442
+ const isJPEG = meta.includes("image/jpeg") || meta.includes("image/jpg");
1443
+ if (isPNG) {
1444
+ const sig = [137, 80, 78, 71, 13, 10, 26, 10];
1445
+ for (let i = 0; i < sig.length; i++) {
1446
+ if (bytes[i] !== sig[i]) return null;
1447
+ }
1448
+ const view = new DataView(
1449
+ bytes.buffer,
1450
+ bytes.byteOffset,
1451
+ bytes.byteLength
1452
+ );
1453
+ const width = view.getUint32(16, false);
1454
+ const height = view.getUint32(20, false);
1455
+ if (width > 0 && height > 0) return { width, height };
1456
+ return null;
1457
+ }
1458
+ if (isJPEG) {
1459
+ let offset = 0;
1460
+ if (bytes[offset++] !== 255 || bytes[offset++] !== 216) return null;
1461
+ while (offset < bytes.length) {
1462
+ while (offset < bytes.length && bytes[offset] !== 255) offset++;
1463
+ if (offset + 1 >= bytes.length) break;
1464
+ while (bytes[offset] === 255) offset++;
1465
+ const marker = bytes[offset++];
1466
+ if (marker === 216 || marker === 217) continue;
1467
+ if (offset + 1 >= bytes.length) break;
1468
+ const length = bytes[offset] << 8 | bytes[offset + 1];
1469
+ offset += 2;
1470
+ if (marker === 192 || marker === 194) {
1471
+ if (length < 7 || offset + length - 2 > bytes.length) return null;
1472
+ const precision = bytes[offset];
1473
+ const height = bytes[offset + 1] << 8 | bytes[offset + 2];
1474
+ const width = bytes[offset + 3] << 8 | bytes[offset + 4];
1475
+ if (precision > 0 && width > 0 && height > 0)
1476
+ return { width, height };
1477
+ return null;
1478
+ } else {
1479
+ offset += length - 2;
1480
+ }
1481
+ }
1482
+ return null;
1483
+ }
1425
1484
  return null;
1426
- }
1427
- return {
1428
- promptTokens,
1429
- completionTokens,
1430
- totalTokens,
1431
- cost,
1432
- satsCost
1433
- };
1434
- }
1435
- function extractResponseId(body) {
1436
- if (!body || typeof body !== "object") return void 0;
1437
- const id = body.id;
1438
- if (typeof id !== "string") return void 0;
1439
- const trimmed = id.trim();
1440
- return trimmed.length > 0 ? trimmed : void 0;
1441
- }
1442
- function extractUsageFromSSEJson(parsed, fallbackSatsCost = 0) {
1443
- if (!parsed || typeof parsed !== "object") {
1485
+ } catch {
1444
1486
  return null;
1445
1487
  }
1446
- if (!parsed.usage && parsed.cost && typeof parsed.cost === "object") {
1447
- const costObj = parsed.cost;
1448
- const msats2 = costObj.total_msats ?? 0;
1449
- const cost2 = costObj.total_usd ?? 0;
1450
- if (msats2 === 0 && cost2 === 0) return null;
1451
- return {
1452
- promptTokens: Number(costObj.input_tokens ?? 0),
1453
- completionTokens: Number(costObj.output_tokens ?? 0),
1454
- totalTokens: Number((costObj.input_tokens ?? 0) + (costObj.output_tokens ?? 0)),
1455
- cost: Number(cost2),
1456
- satsCost: msats2 > 0 ? msats2 / 1e3 : fallbackSatsCost
1457
- };
1458
- }
1459
- if (!parsed.usage) {
1460
- return null;
1488
+ }
1489
+ function calculateImageTokens(width, height, detail = "auto") {
1490
+ if (detail === "low") return 85;
1491
+ let w = width;
1492
+ let h = height;
1493
+ if (w > 2048 || h > 2048) {
1494
+ const aspectRatio = w / h;
1495
+ if (w > h) {
1496
+ w = 2048;
1497
+ h = Math.floor(w / aspectRatio);
1498
+ } else {
1499
+ h = 2048;
1500
+ w = Math.floor(h * aspectRatio);
1501
+ }
1461
1502
  }
1462
- const usage = parsed.usage;
1463
- const usageCost = usage.cost;
1464
- let cost = 0;
1465
- let msats = 0;
1466
- if (typeof usageCost === "number") {
1467
- cost = usageCost;
1468
- } else if (usageCost && typeof usageCost === "object") {
1469
- cost = usageCost.total_usd ?? 0;
1470
- msats = usageCost.total_msats ?? 0;
1503
+ if (w > 768 || h > 768) {
1504
+ const aspectRatio = w / h;
1505
+ if (w > h) {
1506
+ w = 768;
1507
+ h = Math.floor(w / aspectRatio);
1508
+ } else {
1509
+ h = 768;
1510
+ w = Math.floor(h * aspectRatio);
1511
+ }
1471
1512
  }
1472
- if (cost === 0) {
1473
- cost = parsed.metadata?.routstr?.cost?.total_usd ?? 0;
1513
+ const tilesWidth = Math.floor((w + 511) / 512);
1514
+ const tilesHeight = Math.floor((h + 511) / 512);
1515
+ const numTiles = tilesWidth * tilesHeight;
1516
+ return 85 + 170 * numTiles;
1517
+ }
1518
+ var ProviderManager = class _ProviderManager {
1519
+ constructor(providerRegistry, store, logger) {
1520
+ this.providerRegistry = providerRegistry;
1521
+ this.instanceId = `pm_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
1522
+ this.logger = (logger ?? consoleLogger).child(`ProviderManager:${this.instanceId}`);
1523
+ if (store) {
1524
+ this.store = store;
1525
+ this.hydrateFromStore();
1526
+ }
1474
1527
  }
1475
- if (msats === 0) {
1476
- msats = parsed.metadata?.routstr?.cost?.total_msats ?? (typeof usage.cost_sats === "number" ? usage.cost_sats * 1e3 : 0);
1528
+ providerRegistry;
1529
+ failedProviders = /* @__PURE__ */ new Set();
1530
+ /** Track when each provider last failed (provider URL -> timestamp) */
1531
+ lastFailed = /* @__PURE__ */ new Map();
1532
+ /** Providers on cooldown: [provider_url, cooldown_started_timestamp][] */
1533
+ providersOnCoolDown = [];
1534
+ /** Cooldown duration in milliseconds (42 seconds) */
1535
+ static COOLDOWN_DURATION_MS = 42 * 1e3;
1536
+ /** Optional persistent store for failure tracking */
1537
+ store = null;
1538
+ /** Instance ID for debugging */
1539
+ instanceId;
1540
+ logger;
1541
+ /**
1542
+ * Hydrate in-memory state from persistent store
1543
+ */
1544
+ hydrateFromStore() {
1545
+ if (!this.store) return;
1546
+ const state = this.store.getState();
1547
+ this.failedProviders = new Set(state.failedProviders);
1548
+ this.lastFailed = new Map(Object.entries(state.lastFailed));
1549
+ const now = Date.now();
1550
+ this.providersOnCoolDown = state.providersOnCooldown.filter(
1551
+ (entry) => now - entry.timestamp < _ProviderManager.COOLDOWN_DURATION_MS
1552
+ ).map((entry) => [entry.baseUrl, entry.timestamp]);
1553
+ this.logger.log(`Hydrated from store: failedProviders=${this.failedProviders.size} lastFailed=${this.lastFailed.size} providersOnCooldown=${this.providersOnCoolDown.length}`);
1477
1554
  }
1478
- const promptTokens = Number(usage.prompt_tokens ?? usage.input_tokens ?? 0);
1479
- const completionTokens = Number(usage.completion_tokens ?? usage.output_tokens ?? 0);
1480
- const totalTokens = Number(usage.total_tokens ?? promptTokens + completionTokens);
1481
- const result = {
1482
- promptTokens,
1483
- completionTokens,
1484
- totalTokens,
1485
- cost: Number(cost ?? 0),
1486
- satsCost: msats > 0 ? msats / 1e3 : fallbackSatsCost
1487
- };
1488
- if (result.promptTokens === 0 && result.completionTokens === 0 && result.totalTokens === 0 && result.cost === 0 && result.satsCost === 0) {
1489
- return null;
1555
+ /**
1556
+ * Get instance ID for debugging
1557
+ */
1558
+ getInstanceId() {
1559
+ return this.instanceId;
1490
1560
  }
1491
- return result;
1492
- }
1493
- function toUsageStats(usage) {
1494
- if (!usage) return void 0;
1495
- return {
1496
- total_tokens: usage.totalTokens,
1497
- prompt_tokens: usage.promptTokens,
1498
- completion_tokens: usage.completionTokens,
1499
- cost: usage.cost,
1500
- sats_cost: usage.satsCost
1501
- };
1502
- }
1503
-
1504
- // client/StreamProcessor.ts
1505
- var StreamProcessor = class {
1506
- accumulatedContent = "";
1507
- accumulatedThinking = "";
1508
- accumulatedImages = [];
1509
- isInThinking = false;
1510
- isInContent = false;
1511
1561
  /**
1512
- * Process a streaming response
1562
+ * Clean up expired cooldown entries
1563
+ * Also removes the provider from failedProviders so it can be retried
1513
1564
  */
1514
- async process(response, callbacks, modelId) {
1515
- if (!response.body) {
1516
- throw new Error("Response body is not available");
1517
- }
1518
- const reader = response.body.getReader();
1519
- const decoder = new TextDecoder("utf-8");
1520
- let buffer = "";
1521
- this.accumulatedContent = "";
1522
- this.accumulatedThinking = "";
1523
- this.accumulatedImages = [];
1524
- this.isInThinking = false;
1525
- this.isInContent = false;
1526
- let usage;
1527
- let model;
1528
- let finish_reason;
1529
- let citations;
1530
- let annotations;
1531
- let responseId;
1532
- try {
1533
- while (true) {
1534
- const { done, value } = await reader.read();
1535
- if (done) {
1536
- break;
1537
- }
1538
- const chunk = decoder.decode(value, { stream: true });
1539
- buffer += chunk;
1540
- const lines = buffer.split("\n");
1541
- buffer = lines.pop() || "";
1542
- for (const line of lines) {
1543
- const parsed = this._parseLine(line);
1544
- if (!parsed) continue;
1545
- if (parsed.content) {
1546
- this._handleContent(parsed.content, callbacks, modelId);
1547
- }
1548
- if (parsed.reasoning) {
1549
- this._handleThinking(parsed.reasoning, callbacks);
1550
- }
1551
- if (parsed.usage) {
1552
- usage = parsed.usage;
1553
- }
1554
- if (parsed.model) {
1555
- model = parsed.model;
1556
- }
1557
- if (parsed.finish_reason) {
1558
- finish_reason = parsed.finish_reason;
1559
- }
1560
- if (parsed.responseId) {
1561
- responseId = parsed.responseId;
1562
- }
1563
- if (parsed.citations) {
1564
- citations = parsed.citations;
1565
- }
1566
- if (parsed.annotations) {
1567
- annotations = parsed.annotations;
1568
- }
1569
- if (parsed.images) {
1570
- this._mergeImages(parsed.images);
1565
+ cleanupExpiredCooldowns() {
1566
+ const now = Date.now();
1567
+ const before = this.providersOnCoolDown.length;
1568
+ this.providersOnCoolDown = this.providersOnCoolDown.filter(
1569
+ ([url, timestamp]) => {
1570
+ const age = now - timestamp;
1571
+ const isExpired = age >= _ProviderManager.COOLDOWN_DURATION_MS;
1572
+ if (isExpired) {
1573
+ this.logger.log(`Removing expired cooldown for ${url} (age: ${age}ms)`);
1574
+ this.failedProviders.delete(url);
1575
+ if (this.store) {
1576
+ this.store.getState().removeFailedProvider(url);
1571
1577
  }
1572
1578
  }
1579
+ return !isExpired;
1573
1580
  }
1574
- } finally {
1575
- reader.releaseLock();
1581
+ );
1582
+ const after = this.providersOnCoolDown.length;
1583
+ if (before !== after) {
1584
+ this.logger.log(`Cleaned up ${before - after} expired cooldown(s), ${after} remaining`);
1576
1585
  }
1577
- return {
1578
- content: this.accumulatedContent,
1579
- thinking: this.accumulatedThinking || void 0,
1580
- images: this.accumulatedImages.length > 0 ? this.accumulatedImages : void 0,
1581
- usage,
1582
- model,
1583
- responseId,
1584
- finish_reason,
1585
- citations,
1586
- annotations
1587
- };
1588
1586
  }
1589
1587
  /**
1590
- * Parse a single SSE line
1588
+ * Get the cooldown duration in milliseconds
1591
1589
  */
1592
- _parseLine(line) {
1593
- if (!line.trim()) return null;
1594
- if (!line.startsWith("data: ")) {
1595
- return null;
1590
+ getCooldownDurationMs() {
1591
+ return _ProviderManager.COOLDOWN_DURATION_MS;
1592
+ }
1593
+ /**
1594
+ * Check if a provider is currently on cooldown
1595
+ */
1596
+ isOnCooldown(baseUrl) {
1597
+ this.cleanupExpiredCooldowns();
1598
+ const result = this.providersOnCoolDown.some(([url]) => url === baseUrl);
1599
+ return result;
1600
+ }
1601
+ /**
1602
+ * Get all providers currently on cooldown
1603
+ */
1604
+ getProvidersOnCooldown() {
1605
+ this.cleanupExpiredCooldowns();
1606
+ return [...this.providersOnCoolDown];
1607
+ }
1608
+ /**
1609
+ * Reset the failed providers list
1610
+ */
1611
+ resetFailedProviders() {
1612
+ this.failedProviders.clear();
1613
+ if (this.store) {
1614
+ this.store.getState().setFailedProviders([]);
1596
1615
  }
1597
- const jsonData = line.slice(6);
1598
- if (jsonData === "[DONE]") {
1599
- return null;
1616
+ }
1617
+ /**
1618
+ * Get the last failed timestamp for a provider
1619
+ */
1620
+ getLastFailed(baseUrl) {
1621
+ return this.lastFailed.get(baseUrl);
1622
+ }
1623
+ /**
1624
+ * Get all providers with their last failed timestamps
1625
+ */
1626
+ getAllLastFailed() {
1627
+ return new Map(this.lastFailed);
1628
+ }
1629
+ /**
1630
+ * Mark a provider as failed
1631
+ * If a provider fails twice within 5 minutes, it's added to cooldown
1632
+ */
1633
+ markFailed(baseUrl) {
1634
+ const now = Date.now();
1635
+ const lastFailure = this.lastFailed.get(baseUrl);
1636
+ this.logger.log(`markFailed: ${baseUrl} lastFailure=${lastFailure} now=${now}`);
1637
+ if (lastFailure !== void 0) {
1638
+ const timeSinceLastFailure = now - lastFailure;
1639
+ this.logger.log(`markFailed: timeSinceLastFailure=${timeSinceLastFailure}ms withinCooldown=${timeSinceLastFailure < _ProviderManager.COOLDOWN_DURATION_MS}`);
1600
1640
  }
1601
- try {
1602
- const parsed = JSON.parse(jsonData);
1603
- const result = {};
1604
- if (parsed.choices?.[0]?.delta?.content) {
1605
- result.content = parsed.choices[0].delta.content;
1606
- }
1607
- if (parsed.choices?.[0]?.delta?.reasoning) {
1608
- result.reasoning = parsed.choices[0].delta.reasoning;
1609
- }
1610
- const extractedUsage = extractUsageFromSSEJson(parsed);
1611
- if (extractedUsage) {
1612
- result.usage = toUsageStats(extractedUsage);
1613
- } else if (parsed.usage) {
1614
- result.usage = {
1615
- total_tokens: parsed.usage.total_tokens ?? parsed.usage.input_tokens + parsed.usage.output_tokens,
1616
- prompt_tokens: parsed.usage.prompt_tokens ?? parsed.usage.input_tokens,
1617
- completion_tokens: parsed.usage.completion_tokens ?? parsed.usage.output_tokens
1618
- };
1619
- }
1620
- if (parsed.id) {
1621
- result.responseId = parsed.id;
1622
- }
1623
- if (parsed.model) {
1624
- result.model = parsed.model;
1625
- }
1626
- if (parsed.citations) {
1627
- result.citations = parsed.citations;
1628
- }
1629
- if (parsed.annotations) {
1630
- result.annotations = parsed.annotations;
1631
- }
1632
- if (parsed.choices?.[0]?.finish_reason) {
1633
- result.finish_reason = parsed.choices[0].finish_reason;
1641
+ this.lastFailed.set(baseUrl, now);
1642
+ this.failedProviders.add(baseUrl);
1643
+ if (this.store) {
1644
+ this.store.getState().setLastFailedTimestamp(baseUrl, now);
1645
+ this.store.getState().addFailedProvider(baseUrl);
1646
+ }
1647
+ this.logger.log(`markFailed: updated ${baseUrl} to ${now}, failedProviders=${this.failedProviders.size}`);
1648
+ if (lastFailure !== void 0 && now - lastFailure < _ProviderManager.COOLDOWN_DURATION_MS) {
1649
+ this.logger.log(`markFailed: second failure within cooldown window for ${baseUrl}`);
1650
+ if (!this.isOnCooldown(baseUrl)) {
1651
+ this.providersOnCoolDown.push([baseUrl, now]);
1652
+ if (this.store) {
1653
+ this.store.getState().addProviderOnCooldown(baseUrl, now);
1654
+ }
1655
+ this.logger.log(`markFailed: ${baseUrl} added to cooldown`);
1656
+ } else {
1657
+ this.logger.log(`markFailed: ${baseUrl} already on cooldown`);
1634
1658
  }
1635
- const images = parsed.choices?.[0]?.message?.images || parsed.choices?.[0]?.delta?.images;
1636
- if (images && Array.isArray(images)) {
1637
- result.images = images;
1659
+ } else {
1660
+ if (lastFailure === void 0) {
1661
+ this.logger.log(`markFailed: first failure for ${baseUrl}`);
1662
+ } else {
1663
+ this.logger.log(`markFailed: failure outside cooldown window for ${baseUrl} (${now - lastFailure}ms ago)`);
1638
1664
  }
1639
- return result;
1640
- } catch {
1641
- return null;
1642
1665
  }
1643
1666
  }
1644
1667
  /**
1645
- * Handle content delta with thinking support
1668
+ * Remove a provider from cooldown (e.g., after successful request)
1646
1669
  */
1647
- _handleContent(content, callbacks, modelId) {
1648
- if (this.isInThinking && !this.isInContent) {
1649
- this.accumulatedThinking += "</thinking>";
1650
- callbacks.onThinking(this.accumulatedThinking);
1651
- this.isInThinking = false;
1652
- this.isInContent = true;
1653
- }
1654
- if (modelId) {
1655
- this._extractThinkingFromContent(content, callbacks);
1656
- } else {
1657
- this.accumulatedContent += content;
1670
+ removeFromCooldown(baseUrl) {
1671
+ this.providersOnCoolDown = this.providersOnCoolDown.filter(
1672
+ ([url]) => url !== baseUrl
1673
+ );
1674
+ if (this.store) {
1675
+ this.store.getState().removeProviderFromCooldown(baseUrl);
1658
1676
  }
1659
- callbacks.onContent(this.accumulatedContent);
1660
1677
  }
1661
1678
  /**
1662
- * Handle thinking/reasoning content
1679
+ * Clear all cooldown tracking
1663
1680
  */
1664
- _handleThinking(reasoning, callbacks) {
1665
- if (!this.isInThinking) {
1666
- this.accumulatedThinking += "<thinking> ";
1667
- this.isInThinking = true;
1681
+ clearCooldowns() {
1682
+ this.providersOnCoolDown = [];
1683
+ if (this.store) {
1684
+ this.store.getState().clearProvidersOnCooldown();
1668
1685
  }
1669
- this.accumulatedThinking += reasoning;
1670
- callbacks.onThinking(this.accumulatedThinking);
1671
1686
  }
1672
1687
  /**
1673
- * Extract thinking blocks from content (for models with inline thinking)
1688
+ * Clear all failure tracking (lastFailed timestamps)
1674
1689
  */
1675
- _extractThinkingFromContent(content, callbacks) {
1676
- const parts = content.split(/(<thinking>|<\/thinking>)/);
1677
- for (const part of parts) {
1678
- if (part === "<thinking>") {
1679
- this.isInThinking = true;
1680
- if (!this.accumulatedThinking.includes("<thinking>")) {
1681
- this.accumulatedThinking += "<thinking> ";
1682
- }
1683
- } else if (part === "</thinking>") {
1684
- this.isInThinking = false;
1685
- this.accumulatedThinking += "</thinking>";
1686
- } else if (this.isInThinking) {
1687
- this.accumulatedThinking += part;
1688
- } else {
1689
- this.accumulatedContent += part;
1690
- }
1690
+ clearFailureHistory() {
1691
+ this.lastFailed.clear();
1692
+ if (this.store) {
1693
+ this.store.getState().setLastFailed({});
1691
1694
  }
1692
1695
  }
1693
1696
  /**
1694
- * Merge images into accumulated array, avoiding duplicates
1697
+ * Check if a provider has failed
1695
1698
  */
1696
- _mergeImages(newImages) {
1697
- for (const img of newImages) {
1698
- const newUrl = img.image_url?.url;
1699
- const existingIndex = this.accumulatedImages.findIndex((existing) => {
1700
- const existingUrl = existing.image_url?.url;
1701
- if (newUrl && existingUrl) {
1702
- return existingUrl === newUrl;
1703
- }
1704
- if (img.index !== void 0 && existing.index !== void 0) {
1705
- return existing.index === img.index;
1706
- }
1707
- return false;
1708
- });
1709
- if (existingIndex === -1) {
1710
- this.accumulatedImages.push(img);
1711
- } else {
1712
- this.accumulatedImages[existingIndex] = img;
1713
- }
1714
- }
1699
+ hasFailed(baseUrl) {
1700
+ return this.failedProviders.has(baseUrl);
1715
1701
  }
1716
- };
1717
-
1718
- // utils/torUtils.ts
1719
- var TOR_ONION_SUFFIX = ".onion";
1720
- var isTorContext = () => {
1721
- if (typeof window === "undefined") return false;
1722
- const hostname = window.location.hostname.toLowerCase();
1723
- return hostname.endsWith(TOR_ONION_SUFFIX);
1724
- };
1725
- var isOnionUrl = (url) => {
1726
- if (!url) return false;
1727
- const trimmed = url.trim().toLowerCase();
1728
- if (!trimmed) return false;
1729
- try {
1730
- const candidate = trimmed.startsWith("http") ? trimmed : `http://${trimmed}`;
1731
- return new URL(candidate).hostname.endsWith(TOR_ONION_SUFFIX);
1732
- } catch {
1733
- return trimmed.includes(TOR_ONION_SUFFIX);
1702
+ /**
1703
+ * Get a copy of the failed providers set
1704
+ */
1705
+ getFailedProviders() {
1706
+ return new Set(this.failedProviders);
1734
1707
  }
1735
- };
1736
-
1737
- // client/ProviderManager.ts
1738
- function getImageResolutionFromDataUrl(dataUrl) {
1739
- try {
1740
- if (typeof dataUrl !== "string" || !dataUrl.startsWith("data:"))
1741
- return null;
1742
- const commaIdx = dataUrl.indexOf(",");
1743
- if (commaIdx === -1) return null;
1744
- const meta = dataUrl.slice(5, commaIdx);
1745
- const base64 = dataUrl.slice(commaIdx + 1);
1746
- const binary = typeof atob === "function" ? atob(base64) : Buffer.from(base64, "base64").toString("binary");
1747
- const len = binary.length;
1748
- const bytes = new Uint8Array(len);
1749
- for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i);
1750
- const isPNG = meta.includes("image/png");
1751
- const isJPEG = meta.includes("image/jpeg") || meta.includes("image/jpg");
1752
- if (isPNG) {
1753
- const sig = [137, 80, 78, 71, 13, 10, 26, 10];
1754
- for (let i = 0; i < sig.length; i++) {
1755
- if (bytes[i] !== sig[i]) return null;
1756
- }
1757
- const view = new DataView(
1758
- bytes.buffer,
1759
- bytes.byteOffset,
1760
- bytes.byteLength
1708
+ /**
1709
+ * Find the next best provider for a model
1710
+ * @param modelId The model ID to find a provider for
1711
+ * @param currentBaseUrl The current provider to exclude
1712
+ * @returns The best provider URL or null if none available
1713
+ */
1714
+ findNextBestProvider(modelId, currentBaseUrl) {
1715
+ try {
1716
+ const torMode = isTorContext();
1717
+ const disabledProviders = new Set(
1718
+ this.providerRegistry.getDisabledProviders()
1761
1719
  );
1762
- const width = view.getUint32(16, false);
1763
- const height = view.getUint32(20, false);
1764
- if (width > 0 && height > 0) return { width, height };
1765
- return null;
1766
- }
1767
- if (isJPEG) {
1768
- let offset = 0;
1769
- if (bytes[offset++] !== 255 || bytes[offset++] !== 216) return null;
1770
- while (offset < bytes.length) {
1771
- while (offset < bytes.length && bytes[offset] !== 255) offset++;
1772
- if (offset + 1 >= bytes.length) break;
1773
- while (bytes[offset] === 255) offset++;
1774
- const marker = bytes[offset++];
1775
- if (marker === 216 || marker === 217) continue;
1776
- if (offset + 1 >= bytes.length) break;
1777
- const length = bytes[offset] << 8 | bytes[offset + 1];
1778
- offset += 2;
1779
- if (marker === 192 || marker === 194) {
1780
- if (length < 7 || offset + length - 2 > bytes.length) return null;
1781
- const precision = bytes[offset];
1782
- const height = bytes[offset + 1] << 8 | bytes[offset + 2];
1783
- const width = bytes[offset + 3] << 8 | bytes[offset + 4];
1784
- if (precision > 0 && width > 0 && height > 0)
1785
- return { width, height };
1786
- return null;
1787
- } else {
1788
- offset += length - 2;
1720
+ this.logger.log(`findNextBestProvider: model=${modelId} disabled=${[...disabledProviders].length} onCooldown=${this.providersOnCoolDown.length}`);
1721
+ const allProviders = this.providerRegistry.getAllProvidersModels();
1722
+ this.logger.log(`findNextBestProvider: total providers=${Object.keys(allProviders).length}`);
1723
+ const candidates = [];
1724
+ for (const [baseUrl, models] of Object.entries(allProviders)) {
1725
+ if (baseUrl === currentBaseUrl) {
1726
+ continue;
1727
+ }
1728
+ if (disabledProviders.has(baseUrl)) {
1729
+ continue;
1730
+ }
1731
+ if (this.isOnCooldown(baseUrl)) {
1732
+ continue;
1733
+ }
1734
+ if (!torMode && isOnionUrl(baseUrl)) {
1735
+ continue;
1736
+ }
1737
+ const model = models.find((m) => m.id === modelId);
1738
+ if (!model) {
1739
+ continue;
1789
1740
  }
1741
+ const cost = model.sats_pricing?.completion ?? 0;
1742
+ candidates.push({ baseUrl, model, cost });
1743
+ }
1744
+ candidates.sort((a, b) => a.cost - b.cost);
1745
+ if (candidates.length > 0) {
1746
+ return candidates[0].baseUrl;
1747
+ } else {
1748
+ return null;
1790
1749
  }
1750
+ } catch (error) {
1751
+ this.logger.error("findNextBestProvider error:", error);
1791
1752
  return null;
1792
1753
  }
1793
- return null;
1794
- } catch {
1795
- return null;
1796
- }
1797
- }
1798
- function calculateImageTokens(width, height, detail = "auto") {
1799
- if (detail === "low") return 85;
1800
- let w = width;
1801
- let h = height;
1802
- if (w > 2048 || h > 2048) {
1803
- const aspectRatio = w / h;
1804
- if (w > h) {
1805
- w = 2048;
1806
- h = Math.floor(w / aspectRatio);
1807
- } else {
1808
- h = 2048;
1809
- w = Math.floor(h * aspectRatio);
1810
- }
1811
- }
1812
- if (w > 768 || h > 768) {
1813
- const aspectRatio = w / h;
1814
- if (w > h) {
1815
- w = 768;
1816
- h = Math.floor(w / aspectRatio);
1817
- } else {
1818
- h = 768;
1819
- w = Math.floor(h * aspectRatio);
1820
- }
1821
1754
  }
1822
- const tilesWidth = Math.floor((w + 511) / 512);
1823
- const tilesHeight = Math.floor((h + 511) / 512);
1824
- const numTiles = tilesWidth * tilesHeight;
1825
- return 85 + 170 * numTiles;
1826
- }
1827
- function isInsecureHttpUrl(url) {
1828
- return url.startsWith("http://");
1829
- }
1830
- var ProviderManager = class _ProviderManager {
1831
- constructor(providerRegistry, store, logger) {
1832
- this.providerRegistry = providerRegistry;
1833
- this.instanceId = `pm_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
1834
- this.logger = (logger ?? consoleLogger).child(`ProviderManager:${this.instanceId}`);
1835
- if (store) {
1836
- this.store = store;
1837
- this.hydrateFromStore();
1838
- }
1839
- }
1840
- providerRegistry;
1841
- failedProviders = /* @__PURE__ */ new Set();
1842
- /** Track when each provider last failed (provider URL -> timestamp) */
1843
- lastFailed = /* @__PURE__ */ new Map();
1844
- /** Providers on cooldown: [provider_url, cooldown_started_timestamp][] */
1845
- providersOnCoolDown = [];
1846
- /** Cooldown duration in milliseconds (42 seconds) */
1847
- static COOLDOWN_DURATION_MS = 42 * 1e3;
1848
- /** Optional persistent store for failure tracking */
1849
- store = null;
1850
- /** Instance ID for debugging */
1851
- instanceId;
1852
- logger;
1853
1755
  /**
1854
- * Hydrate in-memory state from persistent store
1756
+ * Find the best model for a provider
1757
+ * Useful when switching providers and need to find equivalent model
1855
1758
  */
1856
- hydrateFromStore() {
1857
- if (!this.store) return;
1858
- const state = this.store.getState();
1859
- this.failedProviders = new Set(state.failedProviders);
1860
- this.lastFailed = new Map(Object.entries(state.lastFailed));
1861
- const now = Date.now();
1862
- this.providersOnCoolDown = state.providersOnCooldown.filter(
1863
- (entry) => now - entry.timestamp < _ProviderManager.COOLDOWN_DURATION_MS
1864
- ).map((entry) => [entry.baseUrl, entry.timestamp]);
1865
- this.logger.log(`Hydrated from store: failedProviders=${this.failedProviders.size} lastFailed=${this.lastFailed.size} providersOnCooldown=${this.providersOnCoolDown.length}`);
1759
+ async getModelForProvider(baseUrl, modelId) {
1760
+ const models = this.providerRegistry.getModelsForProvider(baseUrl);
1761
+ const exactMatch = models.find((m) => m.id === modelId);
1762
+ if (exactMatch) return exactMatch;
1763
+ const providerInfo = await this.providerRegistry.getProviderInfo(baseUrl);
1764
+ if (providerInfo?.version && /^0\.1\./.test(providerInfo.version)) {
1765
+ const suffix = modelId.split("/").pop();
1766
+ const suffixMatch = models.find((m) => m.id === suffix);
1767
+ if (suffixMatch) return suffixMatch;
1768
+ }
1769
+ return null;
1866
1770
  }
1867
1771
  /**
1868
- * Get instance ID for debugging
1772
+ * Get all available providers for a model
1773
+ * Returns sorted list by price
1869
1774
  */
1870
- getInstanceId() {
1871
- return this.instanceId;
1775
+ getAllProvidersForModel(modelId) {
1776
+ const candidates = [];
1777
+ const allProviders = this.providerRegistry.getAllProvidersModels();
1778
+ const disabledProviders = new Set(
1779
+ this.providerRegistry.getDisabledProviders()
1780
+ );
1781
+ const torMode = isTorContext();
1782
+ for (const [baseUrl, models] of Object.entries(allProviders)) {
1783
+ if (disabledProviders.has(baseUrl)) continue;
1784
+ if (this.isOnCooldown(baseUrl)) continue;
1785
+ if (!torMode && isOnionUrl(baseUrl))
1786
+ continue;
1787
+ const model = models.find((m) => m.id === modelId);
1788
+ if (!model) continue;
1789
+ const cost = model.sats_pricing?.completion ?? 0;
1790
+ candidates.push({ baseUrl, model, cost });
1791
+ }
1792
+ return candidates.sort((a, b) => a.cost - b.cost);
1872
1793
  }
1873
1794
  /**
1874
- * Clean up expired cooldown entries
1875
- * Also removes the provider from failedProviders so it can be retried
1876
- */
1877
- cleanupExpiredCooldowns() {
1878
- const now = Date.now();
1879
- const before = this.providersOnCoolDown.length;
1880
- this.providersOnCoolDown = this.providersOnCoolDown.filter(
1881
- ([url, timestamp]) => {
1882
- const age = now - timestamp;
1883
- const isExpired = age >= _ProviderManager.COOLDOWN_DURATION_MS;
1884
- if (isExpired) {
1885
- this.logger.log(`Removing expired cooldown for ${url} (age: ${age}ms)`);
1886
- this.failedProviders.delete(url);
1887
- if (this.store) {
1888
- this.store.getState().removeFailedProvider(url);
1889
- }
1890
- }
1891
- return !isExpired;
1892
- }
1893
- );
1894
- const after = this.providersOnCoolDown.length;
1895
- if (before !== after) {
1896
- this.logger.log(`Cleaned up ${before - after} expired cooldown(s), ${after} remaining`);
1897
- }
1898
- }
1899
- /**
1900
- * Get the cooldown duration in milliseconds
1901
- */
1902
- getCooldownDurationMs() {
1903
- return _ProviderManager.COOLDOWN_DURATION_MS;
1904
- }
1905
- /**
1906
- * Check if a provider is currently on cooldown
1907
- */
1908
- isOnCooldown(baseUrl) {
1909
- this.cleanupExpiredCooldowns();
1910
- const result = this.providersOnCoolDown.some(([url]) => url === baseUrl);
1911
- return result;
1912
- }
1913
- /**
1914
- * Get all providers currently on cooldown
1915
- */
1916
- getProvidersOnCooldown() {
1917
- this.cleanupExpiredCooldowns();
1918
- return [...this.providersOnCoolDown];
1919
- }
1920
- /**
1921
- * Reset the failed providers list
1922
- */
1923
- resetFailedProviders() {
1924
- this.failedProviders.clear();
1925
- if (this.store) {
1926
- this.store.getState().setFailedProviders([]);
1927
- }
1928
- }
1929
- /**
1930
- * Get the last failed timestamp for a provider
1931
- */
1932
- getLastFailed(baseUrl) {
1933
- return this.lastFailed.get(baseUrl);
1934
- }
1935
- /**
1936
- * Get all providers with their last failed timestamps
1937
- */
1938
- getAllLastFailed() {
1939
- return new Map(this.lastFailed);
1940
- }
1941
- /**
1942
- * Mark a provider as failed
1943
- * If a provider fails twice within 5 minutes, it's added to cooldown
1944
- */
1945
- markFailed(baseUrl) {
1946
- const now = Date.now();
1947
- const lastFailure = this.lastFailed.get(baseUrl);
1948
- this.logger.log(`markFailed: ${baseUrl} lastFailure=${lastFailure} now=${now}`);
1949
- if (lastFailure !== void 0) {
1950
- const timeSinceLastFailure = now - lastFailure;
1951
- this.logger.log(`markFailed: timeSinceLastFailure=${timeSinceLastFailure}ms withinCooldown=${timeSinceLastFailure < _ProviderManager.COOLDOWN_DURATION_MS}`);
1952
- }
1953
- this.lastFailed.set(baseUrl, now);
1954
- this.failedProviders.add(baseUrl);
1955
- if (this.store) {
1956
- this.store.getState().setLastFailedTimestamp(baseUrl, now);
1957
- this.store.getState().addFailedProvider(baseUrl);
1958
- }
1959
- this.logger.log(`markFailed: updated ${baseUrl} to ${now}, failedProviders=${this.failedProviders.size}`);
1960
- if (lastFailure !== void 0 && now - lastFailure < _ProviderManager.COOLDOWN_DURATION_MS) {
1961
- this.logger.log(`markFailed: second failure within cooldown window for ${baseUrl}`);
1962
- if (!this.isOnCooldown(baseUrl)) {
1963
- this.providersOnCoolDown.push([baseUrl, now]);
1964
- if (this.store) {
1965
- this.store.getState().addProviderOnCooldown(baseUrl, now);
1966
- }
1967
- this.logger.log(`markFailed: ${baseUrl} added to cooldown`);
1968
- } else {
1969
- this.logger.log(`markFailed: ${baseUrl} already on cooldown`);
1970
- }
1971
- } else {
1972
- if (lastFailure === void 0) {
1973
- this.logger.log(`markFailed: first failure for ${baseUrl}`);
1974
- } else {
1975
- this.logger.log(`markFailed: failure outside cooldown window for ${baseUrl} (${now - lastFailure}ms ago)`);
1976
- }
1977
- }
1978
- }
1979
- /**
1980
- * Remove a provider from cooldown (e.g., after successful request)
1981
- */
1982
- removeFromCooldown(baseUrl) {
1983
- this.providersOnCoolDown = this.providersOnCoolDown.filter(
1984
- ([url]) => url !== baseUrl
1985
- );
1986
- if (this.store) {
1987
- this.store.getState().removeProviderFromCooldown(baseUrl);
1988
- }
1989
- }
1990
- /**
1991
- * Clear all cooldown tracking
1992
- */
1993
- clearCooldowns() {
1994
- this.providersOnCoolDown = [];
1995
- if (this.store) {
1996
- this.store.getState().clearProvidersOnCooldown();
1997
- }
1998
- }
1999
- /**
2000
- * Clear all failure tracking (lastFailed timestamps)
2001
- */
2002
- clearFailureHistory() {
2003
- this.lastFailed.clear();
2004
- if (this.store) {
2005
- this.store.getState().setLastFailed({});
2006
- }
2007
- }
2008
- /**
2009
- * Check if a provider has failed
2010
- */
2011
- hasFailed(baseUrl) {
2012
- return this.failedProviders.has(baseUrl);
2013
- }
2014
- /**
2015
- * Get a copy of the failed providers set
2016
- */
2017
- getFailedProviders() {
2018
- return new Set(this.failedProviders);
2019
- }
2020
- /**
2021
- * Find the next best provider for a model
2022
- * @param modelId The model ID to find a provider for
2023
- * @param currentBaseUrl The current provider to exclude
2024
- * @returns The best provider URL or null if none available
2025
- */
2026
- findNextBestProvider(modelId, currentBaseUrl) {
2027
- try {
2028
- const torMode = isTorContext();
2029
- const disabledProviders = new Set(
2030
- this.providerRegistry.getDisabledProviders()
2031
- );
2032
- this.logger.log(`findNextBestProvider: model=${modelId} disabled=${[...disabledProviders].length} onCooldown=${this.providersOnCoolDown.length}`);
2033
- const allProviders = this.providerRegistry.getAllProvidersModels();
2034
- this.logger.log(`findNextBestProvider: total providers=${Object.keys(allProviders).length}`);
2035
- const candidates = [];
2036
- for (const [baseUrl, models] of Object.entries(allProviders)) {
2037
- if (baseUrl === currentBaseUrl) {
2038
- continue;
2039
- }
2040
- if (disabledProviders.has(baseUrl)) {
2041
- continue;
2042
- }
2043
- if (this.isOnCooldown(baseUrl)) {
2044
- continue;
2045
- }
2046
- if (!torMode && (isOnionUrl(baseUrl) || isInsecureHttpUrl(baseUrl))) {
2047
- continue;
2048
- }
2049
- const model = models.find((m) => m.id === modelId);
2050
- if (!model) {
2051
- continue;
2052
- }
2053
- const cost = model.sats_pricing?.completion ?? 0;
2054
- candidates.push({ baseUrl, model, cost });
2055
- }
2056
- candidates.sort((a, b) => a.cost - b.cost);
2057
- if (candidates.length > 0) {
2058
- return candidates[0].baseUrl;
2059
- } else {
2060
- return null;
2061
- }
2062
- } catch (error) {
2063
- this.logger.error("findNextBestProvider error:", error);
2064
- return null;
2065
- }
2066
- }
2067
- /**
2068
- * Find the best model for a provider
2069
- * Useful when switching providers and need to find equivalent model
2070
- */
2071
- async getModelForProvider(baseUrl, modelId) {
2072
- const models = this.providerRegistry.getModelsForProvider(baseUrl);
2073
- const exactMatch = models.find((m) => m.id === modelId);
2074
- if (exactMatch) return exactMatch;
2075
- const providerInfo = await this.providerRegistry.getProviderInfo(baseUrl);
2076
- if (providerInfo?.version && /^0\.1\./.test(providerInfo.version)) {
2077
- const suffix = modelId.split("/").pop();
2078
- const suffixMatch = models.find((m) => m.id === suffix);
2079
- if (suffixMatch) return suffixMatch;
2080
- }
2081
- return null;
2082
- }
2083
- /**
2084
- * Get all available providers for a model
2085
- * Returns sorted list by price
2086
- */
2087
- getAllProvidersForModel(modelId) {
2088
- const candidates = [];
2089
- const allProviders = this.providerRegistry.getAllProvidersModels();
2090
- const disabledProviders = new Set(
2091
- this.providerRegistry.getDisabledProviders()
2092
- );
2093
- const torMode = isTorContext();
2094
- for (const [baseUrl, models] of Object.entries(allProviders)) {
2095
- if (disabledProviders.has(baseUrl)) continue;
2096
- if (this.isOnCooldown(baseUrl)) continue;
2097
- if (!torMode && (isOnionUrl(baseUrl) || isInsecureHttpUrl(baseUrl)))
2098
- continue;
2099
- const model = models.find((m) => m.id === modelId);
2100
- if (!model) continue;
2101
- const cost = model.sats_pricing?.completion ?? 0;
2102
- candidates.push({ baseUrl, model, cost });
2103
- }
2104
- return candidates.sort((a, b) => a.cost - b.cost);
2105
- }
2106
- /**
2107
- * Get providers for a model sorted by prompt+completion pricing
1795
+ * Get providers for a model sorted by prompt+completion pricing
2108
1796
  */
2109
1797
  getProviderPriceRankingForModel(modelId, options = {}) {
2110
1798
  const includeDisabled = options.includeDisabled ?? false;
2111
1799
  const torMode = options.torMode ?? false;
2112
- const disabledProviders = new Set(
2113
- this.providerRegistry.getDisabledProviders()
2114
- );
1800
+ const disabledProviderList = this.providerRegistry.getDisabledProviders();
1801
+ const disabledProviders = new Set(disabledProviderList);
1802
+ if (disabledProviderList.length > 0) {
1803
+ this.logger.log(`getProviderPriceRankingForModel: disabled providers (${disabledProviderList.length}): ${disabledProviderList.join(", ")}`);
1804
+ }
2115
1805
  const allModels = this.providerRegistry.getAllProvidersModels();
2116
1806
  const results = [];
2117
1807
  for (const [baseUrl, models] of Object.entries(allModels)) {
2118
1808
  if (!includeDisabled && disabledProviders.has(baseUrl)) continue;
2119
1809
  if (this.isOnCooldown(baseUrl)) continue;
2120
1810
  if (torMode && !baseUrl.includes(".onion")) continue;
2121
- if (!torMode && (baseUrl.includes(".onion") || isInsecureHttpUrl(baseUrl)))
1811
+ if (!torMode && baseUrl.includes(".onion"))
2122
1812
  continue;
2123
1813
  const match = models.find((model) => model.id === modelId);
2124
1814
  if (!match?.sats_pricing) continue;
@@ -2138,12 +1828,20 @@ var ProviderManager = class _ProviderManager {
2138
1828
  totalPerMillion
2139
1829
  });
2140
1830
  }
2141
- return results.sort((a, b) => {
1831
+ results.sort((a, b) => {
2142
1832
  if (a.totalPerMillion !== b.totalPerMillion) {
2143
1833
  return a.totalPerMillion - b.totalPerMillion;
2144
1834
  }
2145
1835
  return a.baseUrl.localeCompare(b.baseUrl);
2146
1836
  });
1837
+ if (results.length > 0) {
1838
+ const ranking = results.map((r, i) => ` ${i + 1}. ${r.baseUrl} total=${r.totalPerMillion.toFixed(2)} sats/M (prompt=${r.promptPerMillion.toFixed(2)} completion=${r.completionPerMillion.toFixed(2)})`).join("\n");
1839
+ this.logger.log(`getProviderPriceRankingForModel: ${modelId} ranking (${results.length} providers):
1840
+ ${ranking}`);
1841
+ } else {
1842
+ this.logger.log(`getProviderPriceRankingForModel: ${modelId} no providers found`);
1843
+ }
1844
+ return results;
2147
1845
  }
2148
1846
  /**
2149
1847
  * Get best-priced provider for a specific model
@@ -2331,92 +2029,6 @@ var createMemoryDriver = (seed) => {
2331
2029
  };
2332
2030
  };
2333
2031
 
2334
- // storage/drivers/sqlite.ts
2335
- var isBun = () => {
2336
- return typeof process.versions.bun !== "undefined";
2337
- };
2338
- var cachedDbModule = null;
2339
- var loadDatabase = async (dbPath) => {
2340
- if (isBun()) {
2341
- throw new Error(
2342
- "SQLite driver not supported in Bun. Use createBunSqliteDriver() instead."
2343
- );
2344
- }
2345
- try {
2346
- if (!cachedDbModule) {
2347
- cachedDbModule = (await import('better-sqlite3')).default;
2348
- }
2349
- return new cachedDbModule(dbPath);
2350
- } catch (error) {
2351
- throw new Error(
2352
- `better-sqlite3 is required for sqlite storage. Install it to use sqlite storage. (${error})`
2353
- );
2354
- }
2355
- };
2356
- var createSqliteDriver = (options = {}) => {
2357
- const dbPath = options.dbPath || "routstr.sqlite";
2358
- const tableName = options.tableName || "sdk_storage";
2359
- let db;
2360
- let selectStmt;
2361
- let upsertStmt;
2362
- let deleteStmt;
2363
- const initDb = async () => {
2364
- if (!db) {
2365
- db = await loadDatabase(dbPath);
2366
- db.exec(
2367
- `CREATE TABLE IF NOT EXISTS ${tableName} (key TEXT PRIMARY KEY, value TEXT NOT NULL)`
2368
- );
2369
- selectStmt = db.prepare(`SELECT value FROM ${tableName} WHERE key = ?`);
2370
- upsertStmt = db.prepare(
2371
- `INSERT INTO ${tableName} (key, value) VALUES (?, ?)
2372
- ON CONFLICT(key) DO UPDATE SET value = excluded.value`
2373
- );
2374
- deleteStmt = db.prepare(`DELETE FROM ${tableName} WHERE key = ?`);
2375
- }
2376
- };
2377
- const ensureInit = async () => {
2378
- if (!db) {
2379
- await initDb();
2380
- }
2381
- };
2382
- return {
2383
- async getItem(key, defaultValue) {
2384
- try {
2385
- await ensureInit();
2386
- const row = selectStmt.get(key);
2387
- if (!row || typeof row.value !== "string") return defaultValue;
2388
- try {
2389
- return JSON.parse(row.value);
2390
- } catch (parseError) {
2391
- if (typeof defaultValue === "string") {
2392
- return row.value;
2393
- }
2394
- throw parseError;
2395
- }
2396
- } catch (error) {
2397
- console.error(`SQLite getItem failed for key "${key}":`, error);
2398
- return defaultValue;
2399
- }
2400
- },
2401
- async setItem(key, value) {
2402
- try {
2403
- await ensureInit();
2404
- upsertStmt.run(key, JSON.stringify(value));
2405
- } catch (error) {
2406
- console.error(`SQLite setItem failed for key "${key}":`, error);
2407
- }
2408
- },
2409
- async removeItem(key) {
2410
- try {
2411
- await ensureInit();
2412
- deleteStmt.run(key);
2413
- } catch (error) {
2414
- console.error(`SQLite removeItem failed for key "${key}":`, error);
2415
- }
2416
- }
2417
- };
2418
- };
2419
-
2420
2032
  // storage/keys.ts
2421
2033
  var SDK_STORAGE_KEYS = {
2422
2034
  MODELS_FROM_ALL_PROVIDERS: "modelsFromAllProviders",
@@ -2451,9 +2063,10 @@ var openDatabase = (dbName, storeName) => {
2451
2063
  return Promise.reject(new Error("IndexedDB is not available"));
2452
2064
  }
2453
2065
  return new Promise((resolve, reject) => {
2454
- const request = indexedDB.open(dbName, 1);
2066
+ const request = indexedDB.open(dbName, 3);
2455
2067
  request.onupgradeneeded = () => {
2456
2068
  const db = request.result;
2069
+ const tx = request.transaction;
2457
2070
  if (!db.objectStoreNames.contains(storeName)) {
2458
2071
  const store = db.createObjectStore(storeName, { keyPath: "id" });
2459
2072
  store.createIndex("timestamp", "timestamp", { unique: false });
@@ -2461,10 +2074,25 @@ var openDatabase = (dbName, storeName) => {
2461
2074
  store.createIndex("baseUrl", "baseUrl", { unique: false });
2462
2075
  store.createIndex("sessionId", "sessionId", { unique: false });
2463
2076
  store.createIndex("client", "client", { unique: false });
2077
+ store.createIndex("provider", "provider", { unique: false });
2078
+ } else if (tx) {
2079
+ const store = tx.objectStore(storeName);
2080
+ if (!store.indexNames.contains("provider")) {
2081
+ store.createIndex("provider", "provider", { unique: false });
2082
+ }
2083
+ }
2084
+ if (storeName !== "sdk_storage" && !db.objectStoreNames.contains("sdk_storage")) {
2085
+ db.createObjectStore("sdk_storage");
2464
2086
  }
2465
2087
  };
2466
2088
  request.onsuccess = () => resolve(request.result);
2467
2089
  request.onerror = () => reject(request.error);
2090
+ request.onblocked = () => {
2091
+ console.warn(
2092
+ `[usageTracking IndexedDB] open blocked for "${dbName}" \u2014 close other tabs using this DB`
2093
+ );
2094
+ reject(new Error(`IndexedDB "${dbName}" blocked by another connection`));
2095
+ };
2468
2096
  });
2469
2097
  };
2470
2098
  var matchesFilters = (entry, options = {}) => {
@@ -2486,6 +2114,9 @@ var matchesFilters = (entry, options = {}) => {
2486
2114
  if (options.client && entry.client !== options.client) {
2487
2115
  return false;
2488
2116
  }
2117
+ if (options.provider && entry.provider !== options.provider) {
2118
+ return false;
2119
+ }
2489
2120
  return true;
2490
2121
  };
2491
2122
  var createIndexedDBUsageTrackingDriver = (options = {}) => {
@@ -2617,393 +2248,8 @@ var createIndexedDBUsageTrackingDriver = (options = {}) => {
2617
2248
  };
2618
2249
  };
2619
2250
 
2620
- // storage/usageTracking/sqlite.ts
2621
- var MIGRATION_MARKER_KEY2 = "usage_tracking_migration_v1";
2622
- var normalizeBaseUrl2 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
2623
- var isBun2 = () => {
2624
- return typeof process.versions.bun !== "undefined";
2625
- };
2626
- var cachedDbModule2 = null;
2627
- var loadDatabase2 = async (dbPath) => {
2628
- if (isBun2()) {
2629
- throw new Error(
2630
- "SQLite driver not supported in Bun. Use createMemoryDriver() instead."
2631
- );
2632
- }
2633
- try {
2634
- if (!cachedDbModule2) {
2635
- cachedDbModule2 = (await import('better-sqlite3')).default;
2636
- }
2637
- return new cachedDbModule2(dbPath);
2638
- } catch (error) {
2639
- throw new Error(
2640
- `better-sqlite3 is required for sqlite usage tracking. Install it to use sqlite storage. (${error})`
2641
- );
2642
- }
2643
- };
2644
- var buildWhereClause = (options = {}) => {
2645
- const clauses = [];
2646
- const params = [];
2647
- if (typeof options.before === "number") {
2648
- clauses.push("timestamp < ?");
2649
- params.push(options.before);
2650
- }
2651
- if (typeof options.after === "number") {
2652
- clauses.push("timestamp > ?");
2653
- params.push(options.after);
2654
- }
2655
- if (options.modelId) {
2656
- clauses.push("model_id = ?");
2657
- params.push(options.modelId);
2658
- }
2659
- if (options.baseUrl) {
2660
- clauses.push("base_url = ?");
2661
- params.push(normalizeBaseUrl2(options.baseUrl));
2662
- }
2663
- if (options.sessionId) {
2664
- clauses.push("session_id = ?");
2665
- params.push(options.sessionId);
2666
- }
2667
- if (options.client) {
2668
- clauses.push("client = ?");
2669
- params.push(options.client);
2670
- }
2671
- return {
2672
- sql: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
2673
- params
2674
- };
2675
- };
2676
- var createSqliteUsageTrackingDriver = (options = {}) => {
2677
- const dbPath = options.dbPath || "routstr.sqlite";
2678
- const tableName = options.tableName || "usage_tracking";
2679
- const legacyStorageDriver = options.legacyStorageDriver;
2680
- let db;
2681
- let insertStmt;
2682
- let migrationComplete = false;
2683
- const initDb = async () => {
2684
- if (!db) {
2685
- db = await loadDatabase2(dbPath);
2686
- db.exec(`
2687
- CREATE TABLE IF NOT EXISTS ${tableName} (
2688
- id TEXT PRIMARY KEY,
2689
- timestamp INTEGER NOT NULL,
2690
- model_id TEXT NOT NULL,
2691
- base_url TEXT NOT NULL,
2692
- request_id TEXT NOT NULL,
2693
- cost REAL NOT NULL,
2694
- sats_cost REAL NOT NULL,
2695
- prompt_tokens INTEGER NOT NULL,
2696
- completion_tokens INTEGER NOT NULL,
2697
- total_tokens INTEGER NOT NULL,
2698
- client TEXT,
2699
- session_id TEXT,
2700
- tags TEXT
2701
- );
2702
- CREATE INDEX IF NOT EXISTS idx_${tableName}_timestamp ON ${tableName}(timestamp);
2703
- CREATE INDEX IF NOT EXISTS idx_${tableName}_model_id ON ${tableName}(model_id);
2704
- CREATE INDEX IF NOT EXISTS idx_${tableName}_base_url ON ${tableName}(base_url);
2705
- CREATE INDEX IF NOT EXISTS idx_${tableName}_session_id ON ${tableName}(session_id);
2706
- CREATE INDEX IF NOT EXISTS idx_${tableName}_client ON ${tableName}(client);
2707
- `);
2708
- insertStmt = db.prepare(`
2709
- INSERT OR REPLACE INTO ${tableName} (
2710
- id, timestamp, model_id, base_url, request_id,
2711
- cost, sats_cost, prompt_tokens, completion_tokens, total_tokens,
2712
- client, session_id, tags
2713
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2714
- `);
2715
- }
2716
- };
2717
- const ensureInit = async () => {
2718
- if (!db) {
2719
- await initDb();
2720
- }
2721
- };
2722
- const appendOne = (entry) => {
2723
- insertStmt.run(
2724
- entry.id,
2725
- entry.timestamp,
2726
- entry.modelId,
2727
- normalizeBaseUrl2(entry.baseUrl),
2728
- entry.requestId,
2729
- entry.cost,
2730
- entry.satsCost,
2731
- entry.promptTokens,
2732
- entry.completionTokens,
2733
- entry.totalTokens,
2734
- entry.client ?? null,
2735
- entry.sessionId ?? null,
2736
- JSON.stringify(entry.tags ?? [])
2737
- );
2738
- };
2739
- const ensureMigrated = async () => {
2740
- if (!legacyStorageDriver || migrationComplete) return;
2741
- const migrated = await legacyStorageDriver.getItem(
2742
- MIGRATION_MARKER_KEY2,
2743
- false
2744
- );
2745
- if (migrated) {
2746
- migrationComplete = true;
2747
- return;
2748
- }
2749
- const legacyEntries = await legacyStorageDriver.getItem(
2750
- SDK_STORAGE_KEYS.USAGE_TRACKING,
2751
- []
2752
- );
2753
- for (const entry of legacyEntries) {
2754
- appendOne(entry);
2755
- }
2756
- if (legacyEntries.length > 0) {
2757
- await legacyStorageDriver.removeItem(SDK_STORAGE_KEYS.USAGE_TRACKING);
2758
- }
2759
- await legacyStorageDriver.setItem(MIGRATION_MARKER_KEY2, true);
2760
- migrationComplete = true;
2761
- };
2762
- const mapRow = (row) => ({
2763
- id: row.id,
2764
- timestamp: row.timestamp,
2765
- modelId: row.model_id,
2766
- baseUrl: row.base_url,
2767
- requestId: row.request_id,
2768
- cost: row.cost,
2769
- satsCost: row.sats_cost,
2770
- promptTokens: row.prompt_tokens,
2771
- completionTokens: row.completion_tokens,
2772
- totalTokens: row.total_tokens,
2773
- client: row.client ?? void 0,
2774
- sessionId: row.session_id ?? void 0,
2775
- tags: typeof row.tags === "string" ? JSON.parse(row.tags) : void 0
2776
- });
2777
- return {
2778
- async migrate() {
2779
- await ensureInit();
2780
- await ensureMigrated();
2781
- },
2782
- async append(entry) {
2783
- await ensureInit();
2784
- await ensureMigrated();
2785
- appendOne(entry);
2786
- },
2787
- async appendMany(entries) {
2788
- await ensureInit();
2789
- await ensureMigrated();
2790
- for (const entry of entries) {
2791
- appendOne(entry);
2792
- }
2793
- },
2794
- async list(options2 = {}) {
2795
- await ensureInit();
2796
- await ensureMigrated();
2797
- const { sql, params } = buildWhereClause(options2);
2798
- const limitSql = typeof options2.limit === "number" ? " LIMIT ?" : "";
2799
- const stmt = db.prepare(
2800
- `SELECT * FROM ${tableName} ${sql} ORDER BY timestamp DESC${limitSql}`
2801
- );
2802
- const rows = stmt.all(
2803
- ...typeof options2.limit === "number" ? [...params, options2.limit] : params
2804
- );
2805
- return rows.map(mapRow);
2806
- },
2807
- async count(options2 = {}) {
2808
- await ensureInit();
2809
- await ensureMigrated();
2810
- const { sql, params } = buildWhereClause(options2);
2811
- const stmt = db.prepare(`SELECT COUNT(*) as count FROM ${tableName} ${sql}`);
2812
- const row = stmt.get(...params);
2813
- return Number(row?.count ?? 0);
2814
- },
2815
- async deleteOlderThan(timestamp) {
2816
- await ensureInit();
2817
- await ensureMigrated();
2818
- const stmt = db.prepare(`DELETE FROM ${tableName} WHERE timestamp < ?`);
2819
- const result = stmt.run(timestamp);
2820
- return result.changes;
2821
- },
2822
- async clear() {
2823
- await ensureInit();
2824
- await ensureMigrated();
2825
- db.prepare(`DELETE FROM ${tableName}`).run();
2826
- }
2827
- };
2828
- };
2829
-
2830
- // storage/usageTracking/bunSqlite.ts
2831
- var MIGRATION_MARKER_KEY3 = "usage_tracking_migration_v1";
2832
- var normalizeBaseUrl3 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
2833
- var buildWhereClause2 = (options = {}) => {
2834
- const clauses = [];
2835
- const params = [];
2836
- if (typeof options.before === "number") {
2837
- clauses.push("timestamp < ?");
2838
- params.push(options.before);
2839
- }
2840
- if (typeof options.after === "number") {
2841
- clauses.push("timestamp > ?");
2842
- params.push(options.after);
2843
- }
2844
- if (options.modelId) {
2845
- clauses.push("model_id = ?");
2846
- params.push(options.modelId);
2847
- }
2848
- if (options.baseUrl) {
2849
- clauses.push("base_url = ?");
2850
- params.push(normalizeBaseUrl3(options.baseUrl));
2851
- }
2852
- if (options.sessionId) {
2853
- clauses.push("session_id = ?");
2854
- params.push(options.sessionId);
2855
- }
2856
- if (options.client) {
2857
- clauses.push("client = ?");
2858
- params.push(options.client);
2859
- }
2860
- return {
2861
- sql: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
2862
- params
2863
- };
2864
- };
2865
- var createBunSqliteUsageTrackingDriver = (options = {}) => {
2866
- const dbPath = options.dbPath || "routstr.sqlite";
2867
- const tableName = options.tableName || "usage_tracking";
2868
- const legacyStorageDriver = options.legacyStorageDriver;
2869
- const SQLiteDatabase = options.sqlite?.Database;
2870
- let migrationPromise = null;
2871
- if (!SQLiteDatabase) {
2872
- throw new Error(
2873
- "Bun SQLite Database constructor is required. Pass { sqlite: { Database } } when creating the driver."
2874
- );
2875
- }
2876
- const db = new SQLiteDatabase(dbPath);
2877
- db.run(`
2878
- CREATE TABLE IF NOT EXISTS ${tableName} (
2879
- id TEXT PRIMARY KEY,
2880
- timestamp INTEGER NOT NULL,
2881
- model_id TEXT NOT NULL,
2882
- base_url TEXT NOT NULL,
2883
- request_id TEXT NOT NULL,
2884
- cost REAL NOT NULL,
2885
- sats_cost REAL NOT NULL,
2886
- prompt_tokens INTEGER NOT NULL,
2887
- completion_tokens INTEGER NOT NULL,
2888
- total_tokens INTEGER NOT NULL,
2889
- client TEXT,
2890
- session_id TEXT,
2891
- tags TEXT
2892
- )
2893
- `);
2894
- db.run(`CREATE INDEX IF NOT EXISTS idx_${tableName}_timestamp ON ${tableName}(timestamp)`);
2895
- db.run(`CREATE INDEX IF NOT EXISTS idx_${tableName}_model_id ON ${tableName}(model_id)`);
2896
- db.run(`CREATE INDEX IF NOT EXISTS idx_${tableName}_base_url ON ${tableName}(base_url)`);
2897
- const appendOne = (entry) => {
2898
- db.query(`
2899
- INSERT OR REPLACE INTO ${tableName} (
2900
- id, timestamp, model_id, base_url, request_id,
2901
- cost, sats_cost, prompt_tokens, completion_tokens, total_tokens,
2902
- client, session_id, tags
2903
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2904
- `).run(
2905
- entry.id,
2906
- entry.timestamp,
2907
- entry.modelId,
2908
- normalizeBaseUrl3(entry.baseUrl),
2909
- entry.requestId,
2910
- entry.cost,
2911
- entry.satsCost,
2912
- entry.promptTokens,
2913
- entry.completionTokens,
2914
- entry.totalTokens,
2915
- entry.client ?? null,
2916
- entry.sessionId ?? null,
2917
- JSON.stringify(entry.tags ?? [])
2918
- );
2919
- };
2920
- const mapRow = (row) => ({
2921
- id: row.id,
2922
- timestamp: row.timestamp,
2923
- modelId: row.model_id,
2924
- baseUrl: row.base_url,
2925
- requestId: row.request_id,
2926
- cost: row.cost,
2927
- satsCost: row.sats_cost,
2928
- promptTokens: row.prompt_tokens,
2929
- completionTokens: row.completion_tokens,
2930
- totalTokens: row.total_tokens,
2931
- client: row.client ?? void 0,
2932
- sessionId: row.session_id ?? void 0,
2933
- tags: typeof row.tags === "string" ? JSON.parse(row.tags) : void 0
2934
- });
2935
- const ensureMigrated = async () => {
2936
- if (!legacyStorageDriver) return;
2937
- if (!migrationPromise) {
2938
- migrationPromise = (async () => {
2939
- const migrated = await legacyStorageDriver.getItem(
2940
- MIGRATION_MARKER_KEY3,
2941
- false
2942
- );
2943
- if (migrated) return;
2944
- const legacyEntries = await legacyStorageDriver.getItem(
2945
- SDK_STORAGE_KEYS.USAGE_TRACKING,
2946
- []
2947
- );
2948
- if (legacyEntries.length > 0) {
2949
- for (const entry of legacyEntries) {
2950
- appendOne(entry);
2951
- }
2952
- await legacyStorageDriver.removeItem(SDK_STORAGE_KEYS.USAGE_TRACKING);
2953
- }
2954
- await legacyStorageDriver.setItem(MIGRATION_MARKER_KEY3, true);
2955
- })();
2956
- }
2957
- await migrationPromise;
2958
- };
2959
- return {
2960
- async migrate() {
2961
- await ensureMigrated();
2962
- },
2963
- async append(entry) {
2964
- await ensureMigrated();
2965
- appendOne(entry);
2966
- },
2967
- async appendMany(entries) {
2968
- await ensureMigrated();
2969
- for (const entry of entries) {
2970
- appendOne(entry);
2971
- }
2972
- },
2973
- async list(options2 = {}) {
2974
- await ensureMigrated();
2975
- const { sql, params } = buildWhereClause2(options2);
2976
- const limitSql = typeof options2.limit === "number" ? " LIMIT ?" : "";
2977
- const query = `SELECT * FROM ${tableName} ${sql} ORDER BY timestamp DESC${limitSql}`;
2978
- let rows;
2979
- if (typeof options2.limit === "number") {
2980
- rows = db.query(query).all(...params, options2.limit);
2981
- } else {
2982
- rows = db.query(query).all(...params);
2983
- }
2984
- return rows.map(mapRow);
2985
- },
2986
- async count(options2 = {}) {
2987
- const { sql, params } = buildWhereClause2(options2);
2988
- const query = `SELECT COUNT(*) as count FROM ${tableName} ${sql}`;
2989
- const row = db.query(query).get(...params);
2990
- return Number(row?.count ?? 0);
2991
- },
2992
- async deleteOlderThan(timestamp) {
2993
- await ensureMigrated();
2994
- const before = timestamp;
2995
- const result = db.query(`DELETE FROM ${tableName} WHERE timestamp < ?`).run(before);
2996
- return result.changes ?? 0;
2997
- },
2998
- async clear() {
2999
- await ensureMigrated();
3000
- db.query(`DELETE FROM ${tableName}`).run();
3001
- }
3002
- };
3003
- };
3004
-
3005
2251
  // storage/usageTracking/memory.ts
3006
- var normalizeBaseUrl4 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
2252
+ var normalizeBaseUrl2 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
3007
2253
  var matchesFilters2 = (entry, options = {}) => {
3008
2254
  if (typeof options.before === "number" && entry.timestamp >= options.before) {
3009
2255
  return false;
@@ -3014,7 +2260,7 @@ var matchesFilters2 = (entry, options = {}) => {
3014
2260
  if (options.modelId && entry.modelId !== options.modelId) {
3015
2261
  return false;
3016
2262
  }
3017
- if (options.baseUrl && normalizeBaseUrl4(entry.baseUrl) !== normalizeBaseUrl4(options.baseUrl)) {
2263
+ if (options.baseUrl && normalizeBaseUrl2(entry.baseUrl) !== normalizeBaseUrl2(options.baseUrl)) {
3018
2264
  return false;
3019
2265
  }
3020
2266
  if (options.sessionId && entry.sessionId !== options.sessionId) {
@@ -3023,23 +2269,26 @@ var matchesFilters2 = (entry, options = {}) => {
3023
2269
  if (options.client && entry.client !== options.client) {
3024
2270
  return false;
3025
2271
  }
2272
+ if (options.provider && entry.provider !== options.provider) {
2273
+ return false;
2274
+ }
3026
2275
  return true;
3027
2276
  };
3028
2277
  var createMemoryUsageTrackingDriver = (seed = []) => {
3029
2278
  const store = /* @__PURE__ */ new Map();
3030
2279
  for (const entry of seed) {
3031
- store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl4(entry.baseUrl) });
2280
+ store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl2(entry.baseUrl) });
3032
2281
  }
3033
2282
  return {
3034
2283
  async migrate() {
3035
2284
  return;
3036
2285
  },
3037
2286
  async append(entry) {
3038
- store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl4(entry.baseUrl) });
2287
+ store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl2(entry.baseUrl) });
3039
2288
  },
3040
2289
  async appendMany(entries) {
3041
2290
  for (const entry of entries) {
3042
- store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl4(entry.baseUrl) });
2291
+ store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl2(entry.baseUrl) });
3043
2292
  }
3044
2293
  },
3045
2294
  async list(options = {}) {
@@ -3067,7 +2316,7 @@ var createMemoryUsageTrackingDriver = (seed = []) => {
3067
2316
  }
3068
2317
  };
3069
2318
  };
3070
- var normalizeBaseUrl5 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
2319
+ var normalizeBaseUrl3 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
3071
2320
  var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3072
2321
  modelsFromAllProviders: {},
3073
2322
  lastUsedModel: null,
@@ -3090,7 +2339,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3090
2339
  setModelsFromAllProviders: (value) => {
3091
2340
  const normalized = {};
3092
2341
  for (const [baseUrl, models] of Object.entries(value)) {
3093
- normalized[normalizeBaseUrl5(baseUrl)] = models;
2342
+ normalized[normalizeBaseUrl3(baseUrl)] = models;
3094
2343
  }
3095
2344
  void driver.setItem(
3096
2345
  SDK_STORAGE_KEYS.MODELS_FROM_ALL_PROVIDERS,
@@ -3103,7 +2352,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3103
2352
  set({ lastUsedModel: value });
3104
2353
  },
3105
2354
  setBaseUrlsList: (value) => {
3106
- const normalized = value.map((url) => normalizeBaseUrl5(url));
2355
+ const normalized = value.map((url) => normalizeBaseUrl3(url));
3107
2356
  void driver.setItem(SDK_STORAGE_KEYS.BASE_URLS_LIST, normalized);
3108
2357
  set({ baseUrlsList: normalized });
3109
2358
  },
@@ -3112,14 +2361,14 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3112
2361
  set({ lastBaseUrlsUpdate: value });
3113
2362
  },
3114
2363
  setDisabledProviders: (value) => {
3115
- const normalized = value.map((url) => normalizeBaseUrl5(url));
2364
+ const normalized = value.map((url) => normalizeBaseUrl3(url));
3116
2365
  void driver.setItem(SDK_STORAGE_KEYS.DISABLED_PROVIDERS, normalized);
3117
2366
  set({ disabledProviders: normalized });
3118
2367
  },
3119
2368
  setMintsFromAllProviders: (value) => {
3120
2369
  const normalized = {};
3121
2370
  for (const [baseUrl, mints] of Object.entries(value)) {
3122
- normalized[normalizeBaseUrl5(baseUrl)] = mints.map(
2371
+ normalized[normalizeBaseUrl3(baseUrl)] = mints.map(
3123
2372
  (mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint
3124
2373
  );
3125
2374
  }
@@ -3132,7 +2381,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3132
2381
  setInfoFromAllProviders: (value) => {
3133
2382
  const normalized = {};
3134
2383
  for (const [baseUrl, info] of Object.entries(value)) {
3135
- normalized[normalizeBaseUrl5(baseUrl)] = info;
2384
+ normalized[normalizeBaseUrl3(baseUrl)] = info;
3136
2385
  }
3137
2386
  void driver.setItem(SDK_STORAGE_KEYS.INFO_FROM_ALL_PROVIDERS, normalized);
3138
2387
  set({ infoFromAllProviders: normalized });
@@ -3140,7 +2389,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3140
2389
  setLastModelsUpdate: (value) => {
3141
2390
  const normalized = {};
3142
2391
  for (const [baseUrl, timestamp] of Object.entries(value)) {
3143
- normalized[normalizeBaseUrl5(baseUrl)] = timestamp;
2392
+ normalized[normalizeBaseUrl3(baseUrl)] = timestamp;
3144
2393
  }
3145
2394
  void driver.setItem(SDK_STORAGE_KEYS.LAST_MODELS_UPDATE, normalized);
3146
2395
  set({ lastModelsUpdate: normalized });
@@ -3150,7 +2399,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3150
2399
  const updates = typeof value === "function" ? value(state.apiKeys) : value;
3151
2400
  const normalized = updates.map((entry) => ({
3152
2401
  ...entry,
3153
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
2402
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
3154
2403
  balance: entry.balance ?? 0,
3155
2404
  lastUsed: entry.lastUsed ?? null
3156
2405
  }));
@@ -3162,7 +2411,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3162
2411
  set((state) => {
3163
2412
  const updates = typeof value === "function" ? value(state.childKeys) : value;
3164
2413
  const normalized = updates.map((entry) => ({
3165
- parentBaseUrl: normalizeBaseUrl5(entry.parentBaseUrl),
2414
+ parentBaseUrl: normalizeBaseUrl3(entry.parentBaseUrl),
3166
2415
  childKey: entry.childKey,
3167
2416
  balance: entry.balance ?? 0,
3168
2417
  balanceLimit: entry.balanceLimit,
@@ -3176,9 +2425,9 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3176
2425
  setXcashuTokens: (value) => {
3177
2426
  const normalized = {};
3178
2427
  for (const [baseUrl, tokens] of Object.entries(value)) {
3179
- normalized[normalizeBaseUrl5(baseUrl)] = tokens.map((entry) => ({
2428
+ normalized[normalizeBaseUrl3(baseUrl)] = tokens.map((entry) => ({
3180
2429
  ...entry,
3181
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
2430
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
3182
2431
  createdAt: entry.createdAt ?? Date.now(),
3183
2432
  tryCount: entry.tryCount ?? 0
3184
2433
  }));
@@ -3229,12 +2478,12 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3229
2478
  },
3230
2479
  // ========== Failure Tracking ==========
3231
2480
  setFailedProviders: (value) => {
3232
- const normalized = value.map((url) => normalizeBaseUrl5(url));
2481
+ const normalized = value.map((url) => normalizeBaseUrl3(url));
3233
2482
  void driver.setItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, normalized);
3234
2483
  set({ failedProviders: normalized });
3235
2484
  },
3236
2485
  addFailedProvider: (baseUrl) => {
3237
- const normalized = normalizeBaseUrl5(baseUrl);
2486
+ const normalized = normalizeBaseUrl3(baseUrl);
3238
2487
  const current = get().failedProviders;
3239
2488
  if (!current.includes(normalized)) {
3240
2489
  const updated = [...current, normalized];
@@ -3243,7 +2492,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3243
2492
  }
3244
2493
  },
3245
2494
  removeFailedProvider: (baseUrl) => {
3246
- const normalized = normalizeBaseUrl5(baseUrl);
2495
+ const normalized = normalizeBaseUrl3(baseUrl);
3247
2496
  const current = get().failedProviders;
3248
2497
  const updated = current.filter((url) => url !== normalized);
3249
2498
  void driver.setItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, updated);
@@ -3252,13 +2501,13 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3252
2501
  setLastFailed: (value) => {
3253
2502
  const normalized = {};
3254
2503
  for (const [baseUrl, timestamp] of Object.entries(value)) {
3255
- normalized[normalizeBaseUrl5(baseUrl)] = timestamp;
2504
+ normalized[normalizeBaseUrl3(baseUrl)] = timestamp;
3256
2505
  }
3257
2506
  void driver.setItem(SDK_STORAGE_KEYS.LAST_FAILED, normalized);
3258
2507
  set({ lastFailed: normalized });
3259
2508
  },
3260
2509
  setLastFailedTimestamp: (baseUrl, timestamp) => {
3261
- const normalized = normalizeBaseUrl5(baseUrl);
2510
+ const normalized = normalizeBaseUrl3(baseUrl);
3262
2511
  const current = get().lastFailed;
3263
2512
  const updated = { ...current, [normalized]: timestamp };
3264
2513
  void driver.setItem(SDK_STORAGE_KEYS.LAST_FAILED, updated);
@@ -3266,14 +2515,14 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3266
2515
  },
3267
2516
  setProvidersOnCooldown: (value) => {
3268
2517
  const normalized = value.map((entry) => ({
3269
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
2518
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
3270
2519
  timestamp: entry.timestamp
3271
2520
  }));
3272
2521
  void driver.setItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, normalized);
3273
2522
  set({ providersOnCooldown: normalized });
3274
2523
  },
3275
2524
  addProviderOnCooldown: (baseUrl, timestamp) => {
3276
- const normalized = normalizeBaseUrl5(baseUrl);
2525
+ const normalized = normalizeBaseUrl3(baseUrl);
3277
2526
  const current = get().providersOnCooldown;
3278
2527
  if (!current.some((entry) => entry.baseUrl === normalized)) {
3279
2528
  const updated = [...current, { baseUrl: normalized, timestamp }];
@@ -3282,7 +2531,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3282
2531
  }
3283
2532
  },
3284
2533
  removeProviderFromCooldown: (baseUrl) => {
3285
- const normalized = normalizeBaseUrl5(baseUrl);
2534
+ const normalized = normalizeBaseUrl3(baseUrl);
3286
2535
  const current = get().providersOnCooldown;
3287
2536
  const updated = current.filter((entry) => entry.baseUrl !== normalized);
3288
2537
  void driver.setItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, updated);
@@ -3353,40 +2602,40 @@ var hydrateStoreFromDriver = async (store, driver) => {
3353
2602
  ]);
3354
2603
  const modelsFromAllProviders = Object.fromEntries(
3355
2604
  Object.entries(rawModels).map(([baseUrl, models]) => [
3356
- normalizeBaseUrl5(baseUrl),
2605
+ normalizeBaseUrl3(baseUrl),
3357
2606
  models
3358
2607
  ])
3359
2608
  );
3360
- const baseUrlsList = rawBaseUrls.map((url) => normalizeBaseUrl5(url));
2609
+ const baseUrlsList = rawBaseUrls.map((url) => normalizeBaseUrl3(url));
3361
2610
  const disabledProviders = rawDisabledProviders.map(
3362
- (url) => normalizeBaseUrl5(url)
2611
+ (url) => normalizeBaseUrl3(url)
3363
2612
  );
3364
2613
  const mintsFromAllProviders = Object.fromEntries(
3365
2614
  Object.entries(rawMints).map(([baseUrl, mints]) => [
3366
- normalizeBaseUrl5(baseUrl),
2615
+ normalizeBaseUrl3(baseUrl),
3367
2616
  mints.map((mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint)
3368
2617
  ])
3369
2618
  );
3370
2619
  const infoFromAllProviders = Object.fromEntries(
3371
2620
  Object.entries(rawInfo).map(([baseUrl, info]) => [
3372
- normalizeBaseUrl5(baseUrl),
2621
+ normalizeBaseUrl3(baseUrl),
3373
2622
  info
3374
2623
  ])
3375
2624
  );
3376
2625
  const lastModelsUpdate = Object.fromEntries(
3377
2626
  Object.entries(rawLastModelsUpdate).map(([baseUrl, timestamp]) => [
3378
- normalizeBaseUrl5(baseUrl),
2627
+ normalizeBaseUrl3(baseUrl),
3379
2628
  timestamp
3380
2629
  ])
3381
2630
  );
3382
2631
  const apiKeys = rawApiKeys.map((entry) => ({
3383
2632
  ...entry,
3384
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
2633
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
3385
2634
  balance: entry.balance ?? 0,
3386
2635
  lastUsed: entry.lastUsed ?? null
3387
2636
  }));
3388
2637
  const childKeys = rawChildKeys.map((entry) => ({
3389
- parentBaseUrl: normalizeBaseUrl5(entry.parentBaseUrl),
2638
+ parentBaseUrl: normalizeBaseUrl3(entry.parentBaseUrl),
3390
2639
  childKey: entry.childKey,
3391
2640
  balance: entry.balance ?? 0,
3392
2641
  balanceLimit: entry.balanceLimit,
@@ -3395,9 +2644,9 @@ var hydrateStoreFromDriver = async (store, driver) => {
3395
2644
  }));
3396
2645
  const xcashuTokens = Object.fromEntries(
3397
2646
  Object.entries(rawXcashuTokens).map(([baseUrl, tokens]) => [
3398
- normalizeBaseUrl5(baseUrl),
2647
+ normalizeBaseUrl3(baseUrl),
3399
2648
  tokens.map((entry) => ({
3400
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
2649
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
3401
2650
  token: entry.token,
3402
2651
  createdAt: entry.createdAt ?? Date.now(),
3403
2652
  tryCount: entry.tryCount ?? 0
@@ -3418,16 +2667,16 @@ var hydrateStoreFromDriver = async (store, driver) => {
3418
2667
  lastUsed: entry.lastUsed ?? null
3419
2668
  }));
3420
2669
  const failedProviders = rawFailedProviders.map(
3421
- (url) => normalizeBaseUrl5(url)
2670
+ (url) => normalizeBaseUrl3(url)
3422
2671
  );
3423
2672
  const lastFailed = Object.fromEntries(
3424
2673
  Object.entries(rawLastFailed).map(([baseUrl, timestamp]) => [
3425
- normalizeBaseUrl5(baseUrl),
2674
+ normalizeBaseUrl3(baseUrl),
3426
2675
  timestamp
3427
2676
  ])
3428
2677
  );
3429
2678
  const providersOnCooldown = rawProvidersOnCooldown.map((entry) => ({
3430
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
2679
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
3431
2680
  timestamp: entry.timestamp
3432
2681
  }));
3433
2682
  store.setState({
@@ -3469,31 +2718,13 @@ var isBrowser2 = () => {
3469
2718
  return false;
3470
2719
  }
3471
2720
  };
3472
- var isNode = () => {
3473
- try {
3474
- return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
3475
- } catch {
3476
- return false;
3477
- }
3478
- };
3479
2721
  var defaultDriver = null;
3480
- var isBun3 = () => {
3481
- return typeof process.versions.bun !== "undefined";
3482
- };
3483
2722
  var getDefaultSdkDriver = () => {
3484
2723
  if (defaultDriver) return defaultDriver;
3485
2724
  if (isBrowser2()) {
3486
2725
  defaultDriver = localStorageDriver;
3487
2726
  return defaultDriver;
3488
2727
  }
3489
- if (isBun3()) {
3490
- defaultDriver = createMemoryDriver();
3491
- return defaultDriver;
3492
- }
3493
- if (isNode()) {
3494
- defaultDriver = createSqliteDriver();
3495
- return defaultDriver;
3496
- }
3497
2728
  defaultDriver = createMemoryDriver();
3498
2729
  return defaultDriver;
3499
2730
  };
@@ -3514,97 +2745,258 @@ var getDefaultUsageTrackingDriver = () => {
3514
2745
  });
3515
2746
  return defaultUsageTrackingDriver;
3516
2747
  }
3517
- if (isBun3()) {
3518
- defaultUsageTrackingDriver = createBunSqliteUsageTrackingDriver();
3519
- return defaultUsageTrackingDriver;
3520
- }
3521
- if (isNode()) {
3522
- defaultUsageTrackingDriver = createSqliteUsageTrackingDriver({
3523
- legacyStorageDriver: storageDriver
3524
- });
3525
- return defaultUsageTrackingDriver;
3526
- }
3527
2748
  defaultUsageTrackingDriver = createMemoryUsageTrackingDriver();
3528
2749
  return defaultUsageTrackingDriver;
3529
2750
  };
3530
- function mergeUsage(previous, next) {
3531
- if (!previous) return next;
2751
+
2752
+ // client/usage.ts
2753
+ var numOrUndef = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
2754
+ function extractCostBreakdown(costObj) {
2755
+ if (!costObj || typeof costObj !== "object") return {};
3532
2756
  return {
3533
- promptTokens: next.promptTokens > 0 ? next.promptTokens : previous.promptTokens,
3534
- completionTokens: next.completionTokens > 0 ? next.completionTokens : previous.completionTokens,
3535
- totalTokens: next.totalTokens > 0 ? next.totalTokens : previous.totalTokens,
3536
- cost: next.cost > 0 ? next.cost : previous.cost,
3537
- satsCost: next.satsCost > 0 ? next.satsCost : previous.satsCost
2757
+ baseMsats: numOrUndef(costObj.base_msats),
2758
+ inputMsats: numOrUndef(costObj.input_msats),
2759
+ outputMsats: numOrUndef(costObj.output_msats),
2760
+ totalMsats: numOrUndef(costObj.total_msats),
2761
+ totalUsd: numOrUndef(costObj.total_usd),
2762
+ cacheReadInputTokens: numOrUndef(costObj.cache_read_input_tokens),
2763
+ cacheCreationInputTokens: numOrUndef(costObj.cache_creation_input_tokens),
2764
+ cacheReadMsats: numOrUndef(costObj.cache_read_msats),
2765
+ cacheCreationMsats: numOrUndef(costObj.cache_creation_msats),
2766
+ remainingBalanceMsats: numOrUndef(costObj.remaining_balance_msats)
3538
2767
  };
3539
2768
  }
3540
- function hasUsageChanged(previous, next) {
3541
- if (!previous) return true;
3542
- return previous.promptTokens !== next.promptTokens || previous.completionTokens !== next.completionTokens || previous.totalTokens !== next.totalTokens || previous.cost !== next.cost || previous.satsCost !== next.satsCost;
3543
- }
3544
- async function inspectSSEWebStream(stream, onUsage, onResponseId) {
3545
- const reader = stream.getReader();
3546
- const decoder = new TextDecoder("utf-8");
3547
- let buffer = "";
3548
- let capturedUsage = null;
3549
- let capturedResponseId;
3550
- let responseIdCaptured = false;
3551
- const inspectDataPayload = (jsonText) => {
3552
- if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
3553
- return;
3554
- }
3555
- const trimmed = jsonText.trim();
3556
- if (!trimmed || trimmed === "[DONE]") return;
3557
- if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return;
3558
- try {
3559
- const data = JSON.parse(trimmed);
3560
- if (!responseIdCaptured) {
3561
- const responseId = data?.id;
3562
- if (typeof responseId === "string" && responseId.trim().length > 0) {
3563
- capturedResponseId = responseId.trim();
3564
- onResponseId?.(capturedResponseId);
3565
- responseIdCaptured = true;
3566
- }
3567
- }
3568
- const usage = extractUsageFromSSEJson(data);
3569
- if (usage) {
3570
- const merged = mergeUsage(capturedUsage, usage);
3571
- if (hasUsageChanged(capturedUsage, merged)) {
3572
- capturedUsage = merged;
3573
- onUsage(merged);
3574
- }
3575
- }
3576
- } catch {
3577
- }
3578
- };
3579
- const inspectEventBlock = (eventBlock) => {
3580
- if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
3581
- return;
3582
- }
3583
- const lines = eventBlock.split(/\r?\n/);
3584
- const dataParts = [];
3585
- for (const line of lines) {
3586
- if (!line || line.startsWith(":")) continue;
3587
- if (line.startsWith("data:")) {
3588
- const value = line.startsWith("data: ") ? line.slice(6) : line.slice(5);
3589
- dataParts.push(value);
3590
- }
3591
- }
3592
- if (dataParts.length === 0) return;
3593
- inspectDataPayload(dataParts.join("\n"));
3594
- };
3595
- const drainBufferedEvents = () => {
3596
- const terminator = /\r?\n\r?\n/g;
3597
- let lastIndex = 0;
3598
- let match;
3599
- while ((match = terminator.exec(buffer)) !== null) {
3600
- const block = buffer.slice(lastIndex, match.index);
3601
- lastIndex = match.index + match[0].length;
3602
- if (block.length > 0) inspectEventBlock(block);
2769
+ function extractUsageFromResponseBody(body, fallbackSatsCost = 0) {
2770
+ if (!body || typeof body !== "object") return null;
2771
+ const usage = body.usage;
2772
+ if (!usage || typeof usage !== "object") return null;
2773
+ const promptTokens = Number(usage.prompt_tokens ?? 0);
2774
+ const completionTokens = Number(usage.completion_tokens ?? 0);
2775
+ const totalTokens = Number(usage.total_tokens ?? 0);
2776
+ const costValue = usage.cost;
2777
+ let cost = 0;
2778
+ let satsCost = fallbackSatsCost;
2779
+ let breakdown = {};
2780
+ if (typeof costValue === "number") {
2781
+ cost = costValue;
2782
+ } else if (costValue && typeof costValue === "object") {
2783
+ const costObj = costValue;
2784
+ const totalUsd = costObj.total_usd;
2785
+ const totalMsats = costObj.total_msats;
2786
+ cost = typeof totalUsd === "number" ? totalUsd : 0;
2787
+ if (typeof totalMsats === "number") {
2788
+ satsCost = totalMsats / 1e3;
3603
2789
  }
3604
- if (lastIndex > 0) buffer = buffer.slice(lastIndex);
2790
+ breakdown = extractCostBreakdown(costObj);
2791
+ }
2792
+ const provider = typeof body.provider === "string" ? body.provider : void 0;
2793
+ if (promptTokens === 0 && completionTokens === 0 && totalTokens === 0 && cost === 0 && satsCost === 0) {
2794
+ return null;
2795
+ }
2796
+ return {
2797
+ promptTokens,
2798
+ completionTokens,
2799
+ totalTokens,
2800
+ cost,
2801
+ satsCost,
2802
+ provider,
2803
+ ...breakdown
3605
2804
  };
3606
- try {
3607
- while (true) {
2805
+ }
2806
+ function extractResponseId(body) {
2807
+ if (!body || typeof body !== "object") return void 0;
2808
+ const id = body.id;
2809
+ if (typeof id !== "string") return void 0;
2810
+ const trimmed = id.trim();
2811
+ return trimmed.length > 0 ? trimmed : void 0;
2812
+ }
2813
+ function extractUsageFromSSEJson(parsed, fallbackSatsCost = 0) {
2814
+ if (!parsed || typeof parsed !== "object") {
2815
+ return null;
2816
+ }
2817
+ const provider = typeof parsed.provider === "string" ? parsed.provider : void 0;
2818
+ if (!parsed.usage && parsed.cost && typeof parsed.cost === "object") {
2819
+ const costObj = parsed.cost;
2820
+ const msats2 = costObj.total_msats ?? 0;
2821
+ const cost2 = costObj.total_usd ?? 0;
2822
+ if (msats2 === 0 && cost2 === 0) return null;
2823
+ return {
2824
+ promptTokens: Number(costObj.input_tokens ?? 0),
2825
+ completionTokens: Number(costObj.output_tokens ?? 0),
2826
+ totalTokens: Number((costObj.input_tokens ?? 0) + (costObj.output_tokens ?? 0)),
2827
+ cost: Number(cost2),
2828
+ satsCost: msats2 > 0 ? msats2 / 1e3 : fallbackSatsCost,
2829
+ provider,
2830
+ ...extractCostBreakdown(costObj)
2831
+ };
2832
+ }
2833
+ if (!parsed.usage) {
2834
+ return null;
2835
+ }
2836
+ const usage = parsed.usage;
2837
+ const usageCost = usage.cost;
2838
+ let cost = 0;
2839
+ let msats = 0;
2840
+ let breakdown = {};
2841
+ if (typeof usageCost === "number") {
2842
+ cost = usageCost;
2843
+ } else if (usageCost && typeof usageCost === "object") {
2844
+ cost = usageCost.total_usd ?? 0;
2845
+ msats = usageCost.total_msats ?? 0;
2846
+ breakdown = extractCostBreakdown(usageCost);
2847
+ }
2848
+ const routstrCost = parsed.metadata?.routstr?.cost;
2849
+ if (routstrCost && typeof routstrCost === "object") {
2850
+ breakdown = { ...extractCostBreakdown(routstrCost), ...breakdown };
2851
+ }
2852
+ if (cost === 0) {
2853
+ cost = parsed.metadata?.routstr?.cost?.total_usd ?? 0;
2854
+ }
2855
+ if (msats === 0) {
2856
+ msats = parsed.metadata?.routstr?.cost?.total_msats ?? (typeof usage.cost_sats === "number" ? usage.cost_sats * 1e3 : 0);
2857
+ }
2858
+ const promptTokens = Number(usage.prompt_tokens ?? usage.input_tokens ?? 0);
2859
+ const completionTokens = Number(usage.completion_tokens ?? usage.output_tokens ?? 0);
2860
+ const totalTokens = Number(usage.total_tokens ?? promptTokens + completionTokens);
2861
+ const result = {
2862
+ promptTokens,
2863
+ completionTokens,
2864
+ totalTokens,
2865
+ cost: Number(cost ?? 0),
2866
+ satsCost: msats > 0 ? msats / 1e3 : fallbackSatsCost,
2867
+ provider,
2868
+ ...breakdown
2869
+ };
2870
+ if (result.promptTokens === 0 && result.completionTokens === 0 && result.totalTokens === 0 && result.cost === 0 && result.satsCost === 0) {
2871
+ return null;
2872
+ }
2873
+ return result;
2874
+ }
2875
+ function toUsageStats(usage) {
2876
+ if (!usage) return void 0;
2877
+ return {
2878
+ total_tokens: usage.totalTokens,
2879
+ prompt_tokens: usage.promptTokens,
2880
+ completion_tokens: usage.completionTokens,
2881
+ cost: usage.cost,
2882
+ sats_cost: usage.satsCost
2883
+ };
2884
+ }
2885
+ function mergeUsage(previous, next) {
2886
+ if (!previous) return next;
2887
+ const pickNum = (n, p) => typeof n === "number" && n > 0 ? n : p ?? n;
2888
+ return {
2889
+ promptTokens: next.promptTokens > 0 ? next.promptTokens : previous.promptTokens,
2890
+ completionTokens: next.completionTokens > 0 ? next.completionTokens : previous.completionTokens,
2891
+ totalTokens: next.totalTokens > 0 ? next.totalTokens : previous.totalTokens,
2892
+ cost: next.cost > 0 ? next.cost : previous.cost,
2893
+ satsCost: next.satsCost > 0 ? next.satsCost : previous.satsCost,
2894
+ provider: next.provider ?? previous.provider,
2895
+ baseMsats: pickNum(next.baseMsats, previous.baseMsats),
2896
+ inputMsats: pickNum(next.inputMsats, previous.inputMsats),
2897
+ outputMsats: pickNum(next.outputMsats, previous.outputMsats),
2898
+ totalMsats: pickNum(next.totalMsats, previous.totalMsats),
2899
+ totalUsd: pickNum(next.totalUsd, previous.totalUsd),
2900
+ cacheReadInputTokens: pickNum(
2901
+ next.cacheReadInputTokens,
2902
+ previous.cacheReadInputTokens
2903
+ ),
2904
+ cacheCreationInputTokens: pickNum(
2905
+ next.cacheCreationInputTokens,
2906
+ previous.cacheCreationInputTokens
2907
+ ),
2908
+ cacheReadMsats: pickNum(next.cacheReadMsats, previous.cacheReadMsats),
2909
+ cacheCreationMsats: pickNum(
2910
+ next.cacheCreationMsats,
2911
+ previous.cacheCreationMsats
2912
+ ),
2913
+ remainingBalanceMsats: pickNum(
2914
+ next.remainingBalanceMsats,
2915
+ previous.remainingBalanceMsats
2916
+ )
2917
+ };
2918
+ }
2919
+ function hasUsageChanged(previous, next) {
2920
+ if (!previous) return true;
2921
+ return previous.promptTokens !== next.promptTokens || previous.completionTokens !== next.completionTokens || previous.totalTokens !== next.totalTokens || previous.cost !== next.cost || previous.satsCost !== next.satsCost || previous.provider !== next.provider || previous.totalMsats !== next.totalMsats || previous.remainingBalanceMsats !== next.remainingBalanceMsats;
2922
+ }
2923
+ function isInspectionComplete(responseIdCaptured, usage) {
2924
+ return responseIdCaptured && !!usage && usage.totalTokens > 0 && typeof usage.totalMsats === "number" && !!usage.provider;
2925
+ }
2926
+ async function inspectSSEWebStream(stream, onUsage, onResponseId) {
2927
+ const reader = stream.getReader();
2928
+ const decoder = new TextDecoder("utf-8");
2929
+ let buffer = "";
2930
+ let capturedUsage = null;
2931
+ let capturedResponseId;
2932
+ let responseIdCaptured = false;
2933
+ const inspectDataPayload = (jsonText) => {
2934
+ const trimmed = jsonText.trim();
2935
+ if (!trimmed || trimmed === "[DONE]") {
2936
+ if (trimmed === "[DONE]") console.log("[routstr:sse] [DONE]");
2937
+ return;
2938
+ }
2939
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
2940
+ console.log("[routstr:sse] non-JSON payload:", trimmed.slice(0, 200));
2941
+ return;
2942
+ }
2943
+ try {
2944
+ const data = JSON.parse(trimmed);
2945
+ console.log("[routstr:sse] chunk:", JSON.stringify(data));
2946
+ if (isInspectionComplete(responseIdCaptured, capturedUsage)) {
2947
+ console.log("[routstr:sse] (inspection already complete, skipping)");
2948
+ return;
2949
+ }
2950
+ if (!responseIdCaptured) {
2951
+ const responseId = data?.id;
2952
+ if (typeof responseId === "string" && responseId.trim().length > 0) {
2953
+ capturedResponseId = responseId.trim();
2954
+ onResponseId?.(capturedResponseId);
2955
+ responseIdCaptured = true;
2956
+ }
2957
+ }
2958
+ const usage = extractUsageFromSSEJson(data);
2959
+ if (usage) {
2960
+ console.log("[routstr:sse] \u2192 usage detected:", usage);
2961
+ const merged = mergeUsage(capturedUsage, usage);
2962
+ if (hasUsageChanged(capturedUsage, merged)) {
2963
+ capturedUsage = merged;
2964
+ console.log("[routstr:sse] \u2192 merged (changed):", merged);
2965
+ onUsage(merged);
2966
+ } else {
2967
+ console.log("[routstr:sse] \u2192 merged (no change)");
2968
+ }
2969
+ }
2970
+ } catch {
2971
+ console.log("[routstr:sse] failed to parse payload:", trimmed.slice(0, 200));
2972
+ }
2973
+ };
2974
+ const inspectEventBlock = (eventBlock) => {
2975
+ const lines = eventBlock.split(/\r?\n/);
2976
+ const dataParts = [];
2977
+ for (const line of lines) {
2978
+ if (!line || line.startsWith(":")) continue;
2979
+ if (line.startsWith("data:")) {
2980
+ const value = line.startsWith("data: ") ? line.slice(6) : line.slice(5);
2981
+ dataParts.push(value);
2982
+ }
2983
+ }
2984
+ if (dataParts.length === 0) return;
2985
+ inspectDataPayload(dataParts.join("\n"));
2986
+ };
2987
+ const drainBufferedEvents = () => {
2988
+ const terminator = /\r?\n\r?\n/g;
2989
+ let lastIndex = 0;
2990
+ let match;
2991
+ while ((match = terminator.exec(buffer)) !== null) {
2992
+ const block = buffer.slice(lastIndex, match.index);
2993
+ lastIndex = match.index + match[0].length;
2994
+ if (block.length > 0) inspectEventBlock(block);
2995
+ }
2996
+ if (lastIndex > 0) buffer = buffer.slice(lastIndex);
2997
+ };
2998
+ try {
2999
+ while (true) {
3608
3000
  const { value, done } = await reader.read();
3609
3001
  if (done) break;
3610
3002
  if (value && value.byteLength > 0) {
@@ -3637,14 +3029,22 @@ function createSSEParserTransform(onUsage, onResponseId) {
3637
3029
  let capturedUsage = null;
3638
3030
  let responseIdCaptured = false;
3639
3031
  const inspectDataPayload = (jsonText) => {
3640
- if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
3032
+ const trimmed = jsonText.trim();
3033
+ if (!trimmed || trimmed === "[DONE]") {
3034
+ if (trimmed === "[DONE]") console.log("[routstr:sse] [DONE]");
3035
+ return;
3036
+ }
3037
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
3038
+ console.log("[routstr:sse] non-JSON payload:", trimmed.slice(0, 200));
3641
3039
  return;
3642
3040
  }
3643
- const trimmed = jsonText.trim();
3644
- if (!trimmed || trimmed === "[DONE]") return;
3645
- if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return;
3646
3041
  try {
3647
3042
  const data = JSON.parse(trimmed);
3043
+ console.log("[routstr:sse] chunk:", JSON.stringify(data));
3044
+ if (isInspectionComplete(responseIdCaptured, capturedUsage)) {
3045
+ console.log("[routstr:sse] (inspection already complete, skipping)");
3046
+ return;
3047
+ }
3648
3048
  if (!responseIdCaptured) {
3649
3049
  const responseId = data?.id;
3650
3050
  if (typeof responseId === "string" && responseId.trim().length > 0) {
@@ -3654,19 +3054,21 @@ function createSSEParserTransform(onUsage, onResponseId) {
3654
3054
  }
3655
3055
  const usage = extractUsageFromSSEJson(data);
3656
3056
  if (usage) {
3057
+ console.log("[routstr:sse] \u2192 usage detected:", usage);
3657
3058
  const mergedUsage = mergeUsage(capturedUsage, usage);
3658
3059
  if (hasUsageChanged(capturedUsage, mergedUsage)) {
3659
3060
  capturedUsage = mergedUsage;
3061
+ console.log("[routstr:sse] \u2192 merged (changed):", mergedUsage);
3660
3062
  onUsage(mergedUsage);
3063
+ } else {
3064
+ console.log("[routstr:sse] \u2192 merged (no change)");
3661
3065
  }
3662
3066
  }
3663
3067
  } catch {
3068
+ console.log("[routstr:sse] failed to parse payload:", trimmed.slice(0, 200));
3664
3069
  }
3665
3070
  };
3666
3071
  const inspectEventBlock = (eventBlock) => {
3667
- if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
3668
- return;
3669
- }
3670
3072
  const lines = eventBlock.split(/\r?\n/);
3671
3073
  const dataParts = [];
3672
3074
  for (const line of lines) {
@@ -3739,7 +3141,6 @@ var RoutstrClient = class {
3739
3141
  this.balanceManager,
3740
3142
  this.logger
3741
3143
  );
3742
- this.streamProcessor = new StreamProcessor();
3743
3144
  this.alertLevel = alertLevel;
3744
3145
  this.mode = mode;
3745
3146
  this.usageTrackingDriver = options.usageTrackingDriver;
@@ -3751,7 +3152,6 @@ var RoutstrClient = class {
3751
3152
  providerRegistry;
3752
3153
  cashuSpender;
3753
3154
  balanceManager;
3754
- streamProcessor;
3755
3155
  providerManager;
3756
3156
  alertLevel;
3757
3157
  mode;
@@ -3836,6 +3236,8 @@ var RoutstrClient = class {
3836
3236
  baseUrl: prepared.baseUrlUsed,
3837
3237
  mintUrl: params.mintUrl,
3838
3238
  initialTokenBalance: prepared.tokenBalanceInSats,
3239
+ initialTokenBalanceUnknown: prepared.tokenBalanceUnknown,
3240
+ fallbackSatsSpent: usage?.satsCost,
3839
3241
  response: prepared.response,
3840
3242
  modelId: prepared.modelId,
3841
3243
  usage,
@@ -3900,7 +3302,7 @@ var RoutstrClient = class {
3900
3302
  );
3901
3303
  }
3902
3304
  }
3903
- const { token, tokenBalance, tokenBalanceUnit } = await this._spendToken({
3305
+ const { token, tokenBalance, tokenBalanceUnit, tokenBalanceUnknown } = await this._spendToken({
3904
3306
  mintUrl,
3905
3307
  amount: requiredSats,
3906
3308
  baseUrl
@@ -3926,9 +3328,20 @@ var RoutstrClient = class {
3926
3328
  baseHeaders,
3927
3329
  selectedModel
3928
3330
  });
3929
- const tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
3331
+ let tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
3332
+ let initialTokenBalanceUnknown = tokenBalanceUnknown;
3930
3333
  const baseUrlUsed = response.baseUrl || baseUrl;
3931
3334
  const tokenUsed = response.token || token;
3335
+ if (baseUrlUsed !== baseUrl || tokenUsed !== token) {
3336
+ if (typeof response.initialTokenBalanceInSats === "number") {
3337
+ tokenBalanceInSats = response.initialTokenBalanceInSats;
3338
+ initialTokenBalanceUnknown = Boolean(
3339
+ response.initialTokenBalanceUnknown
3340
+ );
3341
+ } else {
3342
+ initialTokenBalanceUnknown = true;
3343
+ }
3344
+ }
3932
3345
  const contentType = response.headers.get("content-type") || "";
3933
3346
  let processedResponse = response;
3934
3347
  let capturedUsage;
@@ -3961,6 +3374,7 @@ var RoutstrClient = class {
3961
3374
  tokenUsed,
3962
3375
  baseUrlUsed,
3963
3376
  tokenBalanceInSats,
3377
+ tokenBalanceUnknown: initialTokenBalanceUnknown,
3964
3378
  modelId,
3965
3379
  capturedUsage,
3966
3380
  capturedResponseId,
@@ -3979,139 +3393,6 @@ var RoutstrClient = class {
3979
3393
  }
3980
3394
  return void 0;
3981
3395
  }
3982
- /**
3983
- * Fetch AI response with streaming
3984
- */
3985
- async fetchAIResponse(options, callbacks) {
3986
- const {
3987
- messageHistory,
3988
- selectedModel,
3989
- baseUrl,
3990
- mintUrl,
3991
- balance,
3992
- transactionHistory,
3993
- maxTokens,
3994
- headers
3995
- } = options;
3996
- const apiMessages = await this._convertMessages(messageHistory);
3997
- const requiredSats = this.providerManager.getRequiredSatsForModel(
3998
- selectedModel,
3999
- apiMessages,
4000
- maxTokens
4001
- );
4002
- try {
4003
- await this._checkBalance();
4004
- callbacks.onPaymentProcessing?.(true);
4005
- const spendResult = await this._spendToken({
4006
- mintUrl,
4007
- amount: requiredSats,
4008
- baseUrl
4009
- });
4010
- let token = spendResult.token;
4011
- let tokenBalance = spendResult.tokenBalance;
4012
- let tokenBalanceUnit = spendResult.tokenBalanceUnit;
4013
- const tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
4014
- callbacks.onTokenCreated?.(this._getPendingCashuTokenAmount());
4015
- const baseHeaders = this._buildBaseHeaders(headers);
4016
- const requestHeaders = this._withAuthHeader(baseHeaders, token);
4017
- const providerInfo = await this.providerRegistry.getProviderInfo(baseUrl);
4018
- const providerVersion = providerInfo?.version ?? "";
4019
- let modelIdForRequest = selectedModel.id;
4020
- if (/^0\.1\./.test(providerVersion)) {
4021
- const newModel = await this.providerManager.getModelForProvider(
4022
- baseUrl,
4023
- selectedModel.id
4024
- );
4025
- modelIdForRequest = newModel?.id ?? selectedModel.id;
4026
- }
4027
- const body = {
4028
- model: modelIdForRequest,
4029
- messages: apiMessages,
4030
- stream: true
4031
- };
4032
- if (maxTokens !== void 0) {
4033
- body.max_tokens = maxTokens;
4034
- }
4035
- if (selectedModel?.name?.startsWith("OpenAI:")) {
4036
- body.tools = [{ type: "web_search" }];
4037
- }
4038
- const response = await this._makeRequest({
4039
- path: "/v1/chat/completions",
4040
- method: "POST",
4041
- body,
4042
- selectedModel,
4043
- baseUrl,
4044
- mintUrl,
4045
- token,
4046
- requiredSats,
4047
- maxTokens,
4048
- headers: requestHeaders,
4049
- baseHeaders
4050
- });
4051
- if (!response.body) {
4052
- throw new Error("Response body is not available");
4053
- }
4054
- if (response.status === 200) {
4055
- const baseUrlUsed = response.baseUrl || baseUrl;
4056
- const streamingResult = await this.streamProcessor.process(
4057
- response,
4058
- {
4059
- onContent: callbacks.onStreamingUpdate,
4060
- onThinking: callbacks.onThinkingUpdate
4061
- },
4062
- selectedModel.id
4063
- );
4064
- if (streamingResult.finish_reason === "content_filter") {
4065
- callbacks.onMessageAppend({
4066
- role: "assistant",
4067
- content: "Your request was denied due to content filtering."
4068
- });
4069
- } else if (streamingResult.content || streamingResult.images && streamingResult.images.length > 0) {
4070
- const message = await this._createAssistantMessage(streamingResult);
4071
- callbacks.onMessageAppend(message);
4072
- } else {
4073
- callbacks.onMessageAppend({
4074
- role: "system",
4075
- content: "The provider did not respond to this request."
4076
- });
4077
- }
4078
- callbacks.onStreamingUpdate("");
4079
- callbacks.onThinkingUpdate("");
4080
- const isApikeysEstimate = this.mode === "apikeys";
4081
- let satsSpent = await this._handlePostResponseBalanceUpdate({
4082
- token,
4083
- baseUrl: baseUrlUsed,
4084
- mintUrl,
4085
- initialTokenBalance: tokenBalanceInSats,
4086
- fallbackSatsSpent: isApikeysEstimate ? this._getEstimatedCosts(selectedModel, streamingResult) : void 0,
4087
- response,
4088
- modelId: selectedModel.id,
4089
- usage: streamingResult.usage ? {
4090
- promptTokens: Number(streamingResult.usage.prompt_tokens ?? 0),
4091
- completionTokens: Number(
4092
- streamingResult.usage.completion_tokens ?? 0
4093
- ),
4094
- totalTokens: Number(streamingResult.usage.total_tokens ?? 0),
4095
- cost: Number(streamingResult.usage.cost ?? 0),
4096
- satsCost: Number(streamingResult.usage.sats_cost ?? 0)
4097
- } : void 0,
4098
- requestId: streamingResult.responseId
4099
- });
4100
- const estimatedCosts = this._getEstimatedCosts(
4101
- selectedModel,
4102
- streamingResult
4103
- );
4104
- const onLastMessageSatsUpdate = callbacks.onLastMessageSatsUpdate;
4105
- onLastMessageSatsUpdate?.(satsSpent, estimatedCosts);
4106
- } else {
4107
- throw new Error(`${response.status} ${response.statusText}`);
4108
- }
4109
- } catch (error) {
4110
- this._handleError(error, callbacks);
4111
- } finally {
4112
- callbacks.onPaymentProcessing?.(false);
4113
- }
4114
- }
4115
3396
  /**
4116
3397
  * Make the API request with failover support
4117
3398
  */
@@ -4239,14 +3520,24 @@ var RoutstrClient = class {
4239
3520
  params.token,
4240
3521
  baseUrl
4241
3522
  );
4242
- const currentBalance = currentBalanceInfo.unit === "msat" ? currentBalanceInfo.amount / 1e3 : currentBalanceInfo.amount;
4243
- const reservedBalance = currentBalanceInfo.unit === "msat" ? (currentBalanceInfo.reserved ?? 0) / 1e3 : currentBalanceInfo.reserved ?? 0;
4244
- const shortfall = Math.max(0, params.requiredSats - currentBalance + reservedBalance);
4245
- topupAmount = shortfall > 0.21 * params.requiredSats ? shortfall : 0.21 * params.requiredSats;
4246
- this._log(
4247
- "DEBUG",
4248
- `The shortfall is: ${shortfall}. requiredSats: ${params.requiredSats}. Current Balance: ${currentBalance}. Reserved Balance: ${reservedBalance}. Available Balance: ${currentBalance - reservedBalance}`
4249
- );
3523
+ if (currentBalanceInfo.balanceUnknown) {
3524
+ this._log(
3525
+ "DEBUG",
3526
+ `[RoutstrClient] _handleErrorResponse: Current balance unknown for ${baseUrl}; using default topup amount=${topupAmount}`
3527
+ );
3528
+ } else {
3529
+ const currentBalance = currentBalanceInfo.unit === "msat" ? currentBalanceInfo.amount / 1e3 : currentBalanceInfo.amount;
3530
+ const reservedBalance = currentBalanceInfo.unit === "msat" ? (currentBalanceInfo.reserved ?? 0) / 1e3 : currentBalanceInfo.reserved ?? 0;
3531
+ const shortfall = Math.max(
3532
+ 0,
3533
+ params.requiredSats - currentBalance + reservedBalance
3534
+ );
3535
+ topupAmount = shortfall > 0.21 * params.requiredSats ? shortfall : 0.21 * params.requiredSats;
3536
+ this._log(
3537
+ "DEBUG",
3538
+ `The shortfall is: ${shortfall}. requiredSats: ${params.requiredSats}. Current Balance: ${currentBalance}. Reserved Balance: ${reservedBalance}. Available Balance: ${currentBalance - reservedBalance}`
3539
+ );
3540
+ }
4250
3541
  } catch (e) {
4251
3542
  this._log(
4252
3543
  "WARN",
@@ -4332,7 +3623,7 @@ var RoutstrClient = class {
4332
3623
  this.storageAdapter.removeApiKey(baseUrl);
4333
3624
  tryNextProvider = true;
4334
3625
  } else {
4335
- const latestTokenBalance = latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
3626
+ const latestTokenBalance = latestBalanceInfo.balanceUnknown ? void 0 : latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
4336
3627
  if (latestBalanceInfo.apiKey) {
4337
3628
  const storedApiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
4338
3629
  if (storedApiKeyEntry?.key !== latestBalanceInfo.apiKey) {
@@ -4343,7 +3634,7 @@ var RoutstrClient = class {
4343
3634
  }
4344
3635
  retryToken = latestBalanceInfo.apiKey;
4345
3636
  }
4346
- if (latestTokenBalance >= 0) {
3637
+ if (latestTokenBalance !== void 0 && latestTokenBalance >= 0) {
4347
3638
  this.storageAdapter.updateApiKeyBalance(
4348
3639
  baseUrl,
4349
3640
  latestTokenBalance
@@ -4418,7 +3709,7 @@ var RoutstrClient = class {
4418
3709
  "DEBUG",
4419
3710
  `[RoutstrClient] _handleErrorResponse: API key refund result: success=${refundResult.success}, message=${refundResult.message}`
4420
3711
  );
4421
- if (!refundResult.success && latestBalanceInfo.amount > 0) {
3712
+ if (!refundResult.success && latestBalanceInfo.amount > 0 && !latestBalanceInfo.balanceUnknown) {
4422
3713
  throw new ProviderError(
4423
3714
  baseUrl,
4424
3715
  status,
@@ -4469,7 +3760,7 @@ var RoutstrClient = class {
4469
3760
  amount: newRequiredSats,
4470
3761
  baseUrl: nextProvider
4471
3762
  });
4472
- return this._makeRequest({
3763
+ const retryResponse = await this._makeRequest({
4473
3764
  ...params,
4474
3765
  path,
4475
3766
  method,
@@ -4481,6 +3772,9 @@ var RoutstrClient = class {
4481
3772
  headers: this._withAuthHeader(params.baseHeaders, spendResult.token),
4482
3773
  retryCount: 0
4483
3774
  });
3775
+ retryResponse.initialTokenBalanceInSats = spendResult.tokenBalanceUnit === "msat" ? spendResult.tokenBalance / 1e3 : spendResult.tokenBalance;
3776
+ retryResponse.initialTokenBalanceUnknown = spendResult.tokenBalanceUnknown;
3777
+ return retryResponse;
4484
3778
  }
4485
3779
  throw new FailoverError(
4486
3780
  baseUrl,
@@ -4496,6 +3790,7 @@ var RoutstrClient = class {
4496
3790
  baseUrl,
4497
3791
  mintUrl,
4498
3792
  initialTokenBalance,
3793
+ initialTokenBalanceUnknown,
4499
3794
  fallbackSatsSpent,
4500
3795
  response,
4501
3796
  modelId,
@@ -4532,17 +3827,19 @@ var RoutstrClient = class {
4532
3827
  latestBalanceInfo.apiKey,
4533
3828
  baseUrl
4534
3829
  );
4535
- const latestTokenBalance = latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
3830
+ const latestTokenBalance = latestBalanceInfo.balanceUnknown ? void 0 : latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
4536
3831
  const storedApiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
4537
3832
  if (storedApiKeyEntry?.key.startsWith("cashu") && latestBalanceInfo.apiKey) {
4538
3833
  this.storageAdapter.removeApiKey(baseUrl);
4539
3834
  this.storageAdapter.setApiKey(baseUrl, latestBalanceInfo.apiKey);
4540
3835
  }
4541
- this.storageAdapter.updateApiKeyBalance(baseUrl, latestTokenBalance);
4542
- satsSpent = initialTokenBalance - latestTokenBalance;
3836
+ if (latestTokenBalance !== void 0) {
3837
+ this.storageAdapter.updateApiKeyBalance(baseUrl, latestTokenBalance);
3838
+ }
3839
+ satsSpent = latestTokenBalance !== void 0 && !initialTokenBalanceUnknown ? Math.max(0, initialTokenBalance - latestTokenBalance) : fallbackSatsSpent ?? usage?.satsCost ?? 0;
4543
3840
  } catch (e) {
4544
3841
  this._log("WARN", "Could not get updated API key balance:", e);
4545
- satsSpent = fallbackSatsSpent ?? initialTokenBalance;
3842
+ satsSpent = fallbackSatsSpent ?? usage?.satsCost ?? 0;
4546
3843
  }
4547
3844
  }
4548
3845
  await this._trackResponseUsage({
@@ -4620,264 +3917,532 @@ var RoutstrClient = class {
4620
3917
  }
4621
3918
  }
4622
3919
  /**
4623
- * Convert messages for API format
3920
+ * Check wallet balance and throw if insufficient
3921
+ */
3922
+ async _checkBalance() {
3923
+ const balances = await this.walletAdapter.getBalances();
3924
+ const totalBalance = Object.values(balances).reduce((sum, v) => sum + v, 0);
3925
+ if (totalBalance <= 0) {
3926
+ throw new InsufficientBalanceError(1, 0);
3927
+ }
3928
+ }
3929
+ /**
3930
+ * Spend a token using CashuSpender with standardized error handling
3931
+ */
3932
+ async _spendToken(params) {
3933
+ const { mintUrl, amount, baseUrl } = params;
3934
+ this._log(
3935
+ "DEBUG",
3936
+ `[RoutstrClient] _spendToken: mode=${this.mode}, amount=${amount}, baseUrl=${baseUrl}, mintUrl=${mintUrl}`
3937
+ );
3938
+ if (this.mode === "apikeys") {
3939
+ let parentApiKey = this.storageAdapter.getApiKey(baseUrl);
3940
+ if (!parentApiKey) {
3941
+ this._log(
3942
+ "DEBUG",
3943
+ `[RoutstrClient] _spendToken: No existing API key for ${baseUrl}, creating new one via Cashu`
3944
+ );
3945
+ const spendResult2 = await this.cashuSpender.spend({
3946
+ mintUrl,
3947
+ amount: amount * TOPUP_MARGIN,
3948
+ baseUrl: "",
3949
+ reuseToken: false
3950
+ });
3951
+ if (!spendResult2.token) {
3952
+ this._log(
3953
+ "ERROR",
3954
+ `[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error:`,
3955
+ spendResult2.error
3956
+ );
3957
+ throw new Error(
3958
+ `[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error: ${spendResult2.error}`
3959
+ );
3960
+ } else {
3961
+ this._log(
3962
+ "DEBUG",
3963
+ `[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult2.token}`
3964
+ );
3965
+ }
3966
+ this._log(
3967
+ "DEBUG",
3968
+ `[RoutstrClient] _spendToken: Created API key for ${baseUrl}, key preview: ${spendResult2.token}, balance: ${spendResult2.balance}`
3969
+ );
3970
+ try {
3971
+ this.storageAdapter.setApiKey(baseUrl, spendResult2.token);
3972
+ } catch (error) {
3973
+ if (error instanceof Error && error.message.includes("ApiKey already exists")) {
3974
+ const receiveResult = await this.cashuSpender.receiveToken(
3975
+ spendResult2.token
3976
+ );
3977
+ if (receiveResult.success) {
3978
+ this._log(
3979
+ "DEBUG",
3980
+ `[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${receiveResult.amount}`
3981
+ );
3982
+ } else {
3983
+ this._log(
3984
+ "DEBUG",
3985
+ `[RoutstrClient] _handleErrorResponse: Token restore failed: ${receiveResult.message}`
3986
+ );
3987
+ }
3988
+ this._log(
3989
+ "DEBUG",
3990
+ `[RoutstrClient] _spendToken: API key already exists for ${baseUrl}, using existing key`
3991
+ );
3992
+ } else {
3993
+ throw error;
3994
+ }
3995
+ }
3996
+ parentApiKey = this.storageAdapter.getApiKey(baseUrl);
3997
+ } else {
3998
+ this._log(
3999
+ "DEBUG",
4000
+ `[RoutstrClient] _spendToken: Using existing API key for ${baseUrl}, key preview: ${parentApiKey.key}`
4001
+ );
4002
+ }
4003
+ let tokenBalance = 0;
4004
+ let tokenBalanceUnit = "sat";
4005
+ let tokenBalanceUnknown = false;
4006
+ const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
4007
+ const distributionForBaseUrl = apiKeyDistribution.find(
4008
+ (d) => d.baseUrl === baseUrl
4009
+ );
4010
+ if (distributionForBaseUrl) {
4011
+ tokenBalance = distributionForBaseUrl.amount;
4012
+ }
4013
+ if (tokenBalance === 0 && parentApiKey) {
4014
+ try {
4015
+ const balanceInfo = await this.balanceManager.getTokenBalance(
4016
+ parentApiKey.key,
4017
+ baseUrl
4018
+ );
4019
+ tokenBalance = balanceInfo.amount;
4020
+ tokenBalanceUnit = balanceInfo.unit;
4021
+ tokenBalanceUnknown = Boolean(balanceInfo.balanceUnknown);
4022
+ } catch (e) {
4023
+ this._log("WARN", "Could not get initial API key balance:", e);
4024
+ }
4025
+ }
4026
+ this._log(
4027
+ "DEBUG",
4028
+ `[RoutstrClient] _spendToken: Returning token with balance=${tokenBalance} ${tokenBalanceUnit}`
4029
+ );
4030
+ return {
4031
+ token: parentApiKey?.key ?? "",
4032
+ tokenBalance,
4033
+ tokenBalanceUnit,
4034
+ tokenBalanceUnknown
4035
+ };
4036
+ }
4037
+ this._log(
4038
+ "DEBUG",
4039
+ `[RoutstrClient] _spendToken: Calling CashuSpender.spend for amount=${amount}, mintUrl=${mintUrl}, mode=${this.mode}`
4040
+ );
4041
+ const spendResult = await this.cashuSpender.spend({
4042
+ mintUrl,
4043
+ amount,
4044
+ baseUrl: "",
4045
+ reuseToken: false
4046
+ });
4047
+ if (!spendResult.token) {
4048
+ this._log(
4049
+ "ERROR",
4050
+ `[RoutstrClient] _spendToken: CashuSpender.spend failed, error:`,
4051
+ spendResult.error
4052
+ );
4053
+ } else {
4054
+ this._log(
4055
+ "DEBUG",
4056
+ `[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult.token}, balance: ${spendResult.balance} ${spendResult.unit ?? "sat"}`
4057
+ );
4058
+ this.storageAdapter.addXcashuToken(baseUrl, spendResult.token);
4059
+ }
4060
+ return {
4061
+ token: spendResult.token,
4062
+ tokenBalance: spendResult.balance,
4063
+ tokenBalanceUnit: spendResult.unit ?? "sat",
4064
+ tokenBalanceUnknown: false
4065
+ };
4066
+ }
4067
+ /**
4068
+ * Build request headers with common defaults and dev mock controls
4069
+ */
4070
+ _buildBaseHeaders(additionalHeaders = {}, token) {
4071
+ const headers = {
4072
+ ...additionalHeaders,
4073
+ "Content-Type": "application/json"
4074
+ };
4075
+ return headers;
4076
+ }
4077
+ /**
4078
+ * Attach auth headers using the active client mode
4079
+ */
4080
+ _withAuthHeader(headers, token) {
4081
+ const nextHeaders = { ...headers };
4082
+ if (this.mode === "xcashu") {
4083
+ nextHeaders["X-Cashu"] = token;
4084
+ } else {
4085
+ nextHeaders["Authorization"] = `Bearer ${token}`;
4086
+ }
4087
+ return nextHeaders;
4088
+ }
4089
+ };
4090
+
4091
+ // client/StreamProcessor.ts
4092
+ var StreamProcessor = class {
4093
+ accumulatedContent = "";
4094
+ accumulatedThinking = "";
4095
+ accumulatedImages = [];
4096
+ isInThinking = false;
4097
+ isInContent = false;
4098
+ /**
4099
+ * Process a streaming response
4100
+ */
4101
+ async process(response, callbacks, modelId) {
4102
+ if (!response.body) {
4103
+ throw new Error("Response body is not available");
4104
+ }
4105
+ const reader = response.body.getReader();
4106
+ const decoder = new TextDecoder("utf-8");
4107
+ let buffer = "";
4108
+ this.accumulatedContent = "";
4109
+ this.accumulatedThinking = "";
4110
+ this.accumulatedImages = [];
4111
+ this.isInThinking = false;
4112
+ this.isInContent = false;
4113
+ let usage;
4114
+ let model;
4115
+ let finish_reason;
4116
+ let citations;
4117
+ let annotations;
4118
+ let responseId;
4119
+ try {
4120
+ while (true) {
4121
+ const { done, value } = await reader.read();
4122
+ if (done) {
4123
+ break;
4124
+ }
4125
+ const chunk = decoder.decode(value, { stream: true });
4126
+ buffer += chunk;
4127
+ const lines = buffer.split("\n");
4128
+ buffer = lines.pop() || "";
4129
+ for (const line of lines) {
4130
+ const parsed = this._parseLine(line);
4131
+ if (!parsed) continue;
4132
+ if (parsed.content) {
4133
+ this._handleContent(parsed.content, callbacks, modelId);
4134
+ }
4135
+ if (parsed.reasoning) {
4136
+ this._handleThinking(parsed.reasoning, callbacks);
4137
+ }
4138
+ if (parsed.usage) {
4139
+ usage = parsed.usage;
4140
+ }
4141
+ if (parsed.model) {
4142
+ model = parsed.model;
4143
+ }
4144
+ if (parsed.finish_reason) {
4145
+ finish_reason = parsed.finish_reason;
4146
+ }
4147
+ if (parsed.responseId) {
4148
+ responseId = parsed.responseId;
4149
+ }
4150
+ if (parsed.citations) {
4151
+ citations = parsed.citations;
4152
+ }
4153
+ if (parsed.annotations) {
4154
+ annotations = parsed.annotations;
4155
+ }
4156
+ if (parsed.images) {
4157
+ this._mergeImages(parsed.images);
4158
+ }
4159
+ }
4160
+ }
4161
+ } finally {
4162
+ reader.releaseLock();
4163
+ }
4164
+ return {
4165
+ content: this.accumulatedContent,
4166
+ thinking: this.accumulatedThinking || void 0,
4167
+ images: this.accumulatedImages.length > 0 ? this.accumulatedImages : void 0,
4168
+ usage,
4169
+ model,
4170
+ responseId,
4171
+ finish_reason,
4172
+ citations,
4173
+ annotations
4174
+ };
4175
+ }
4176
+ /**
4177
+ * Parse a single SSE line
4178
+ */
4179
+ _parseLine(line) {
4180
+ if (!line.trim()) return null;
4181
+ if (!line.startsWith("data: ")) {
4182
+ return null;
4183
+ }
4184
+ const jsonData = line.slice(6);
4185
+ if (jsonData === "[DONE]") {
4186
+ return null;
4187
+ }
4188
+ try {
4189
+ const parsed = JSON.parse(jsonData);
4190
+ const result = {};
4191
+ if (parsed.choices?.[0]?.delta?.content) {
4192
+ result.content = parsed.choices[0].delta.content;
4193
+ }
4194
+ if (parsed.choices?.[0]?.delta?.reasoning) {
4195
+ result.reasoning = parsed.choices[0].delta.reasoning;
4196
+ }
4197
+ const extractedUsage = extractUsageFromSSEJson(parsed);
4198
+ if (extractedUsage) {
4199
+ result.usage = toUsageStats(extractedUsage);
4200
+ } else if (parsed.usage) {
4201
+ result.usage = {
4202
+ total_tokens: parsed.usage.total_tokens ?? parsed.usage.input_tokens + parsed.usage.output_tokens,
4203
+ prompt_tokens: parsed.usage.prompt_tokens ?? parsed.usage.input_tokens,
4204
+ completion_tokens: parsed.usage.completion_tokens ?? parsed.usage.output_tokens
4205
+ };
4206
+ }
4207
+ if (parsed.id) {
4208
+ result.responseId = parsed.id;
4209
+ }
4210
+ if (parsed.model) {
4211
+ result.model = parsed.model;
4212
+ }
4213
+ if (parsed.citations) {
4214
+ result.citations = parsed.citations;
4215
+ }
4216
+ if (parsed.annotations) {
4217
+ result.annotations = parsed.annotations;
4218
+ }
4219
+ if (parsed.choices?.[0]?.finish_reason) {
4220
+ result.finish_reason = parsed.choices[0].finish_reason;
4221
+ }
4222
+ const images = parsed.choices?.[0]?.message?.images || parsed.choices?.[0]?.delta?.images;
4223
+ if (images && Array.isArray(images)) {
4224
+ result.images = images;
4225
+ }
4226
+ return result;
4227
+ } catch {
4228
+ return null;
4229
+ }
4230
+ }
4231
+ /**
4232
+ * Handle content delta with thinking support
4233
+ */
4234
+ _handleContent(content, callbacks, modelId) {
4235
+ if (this.isInThinking && !this.isInContent) {
4236
+ this.accumulatedThinking += "</thinking>";
4237
+ callbacks.onThinking(this.accumulatedThinking);
4238
+ this.isInThinking = false;
4239
+ this.isInContent = true;
4240
+ }
4241
+ if (modelId) {
4242
+ this._extractThinkingFromContent(content, callbacks);
4243
+ } else {
4244
+ this.accumulatedContent += content;
4245
+ }
4246
+ callbacks.onContent(this.accumulatedContent);
4247
+ }
4248
+ /**
4249
+ * Handle thinking/reasoning content
4624
4250
  */
4625
- async _convertMessages(messages) {
4626
- return Promise.all(
4627
- messages.filter((m) => m.role !== "system").map(async (m) => ({
4628
- role: m.role,
4629
- content: typeof m.content === "string" ? m.content : m.content
4630
- }))
4631
- );
4251
+ _handleThinking(reasoning, callbacks) {
4252
+ if (!this.isInThinking) {
4253
+ this.accumulatedThinking += "<thinking> ";
4254
+ this.isInThinking = true;
4255
+ }
4256
+ this.accumulatedThinking += reasoning;
4257
+ callbacks.onThinking(this.accumulatedThinking);
4632
4258
  }
4633
4259
  /**
4634
- * Create assistant message from streaming result
4260
+ * Extract thinking blocks from content (for models with inline thinking)
4635
4261
  */
4636
- async _createAssistantMessage(result) {
4637
- if (result.images && result.images.length > 0) {
4638
- const content = [];
4639
- if (result.content) {
4640
- content.push({
4641
- type: "text",
4642
- text: result.content,
4643
- thinking: result.thinking,
4644
- citations: result.citations,
4645
- annotations: result.annotations
4646
- });
4647
- }
4648
- for (const img of result.images) {
4649
- content.push({
4650
- type: "image_url",
4651
- image_url: {
4652
- url: img.image_url.url
4653
- }
4654
- });
4262
+ _extractThinkingFromContent(content, callbacks) {
4263
+ const parts = content.split(/(<thinking>|<\/thinking>)/);
4264
+ for (const part of parts) {
4265
+ if (part === "<thinking>") {
4266
+ this.isInThinking = true;
4267
+ if (!this.accumulatedThinking.includes("<thinking>")) {
4268
+ this.accumulatedThinking += "<thinking> ";
4269
+ }
4270
+ } else if (part === "</thinking>") {
4271
+ this.isInThinking = false;
4272
+ this.accumulatedThinking += "</thinking>";
4273
+ } else if (this.isInThinking) {
4274
+ this.accumulatedThinking += part;
4275
+ } else {
4276
+ this.accumulatedContent += part;
4655
4277
  }
4656
- return {
4657
- role: "assistant",
4658
- content
4659
- };
4660
4278
  }
4661
- return {
4662
- role: "assistant",
4663
- content: result.content || ""
4664
- };
4665
4279
  }
4666
4280
  /**
4667
- * Calculate estimated costs from usage
4281
+ * Merge images into accumulated array, avoiding duplicates
4668
4282
  */
4669
- _getEstimatedCosts(selectedModel, streamingResult) {
4670
- let estimatedCosts = 0;
4671
- if (streamingResult.usage) {
4672
- const { completion_tokens, prompt_tokens } = streamingResult.usage;
4673
- if (completion_tokens !== void 0 && prompt_tokens !== void 0) {
4674
- estimatedCosts = (selectedModel.sats_pricing?.completion ?? 0) * completion_tokens + (selectedModel.sats_pricing?.prompt ?? 0) * prompt_tokens;
4283
+ _mergeImages(newImages) {
4284
+ for (const img of newImages) {
4285
+ const newUrl = img.image_url?.url;
4286
+ const existingIndex = this.accumulatedImages.findIndex((existing) => {
4287
+ const existingUrl = existing.image_url?.url;
4288
+ if (newUrl && existingUrl) {
4289
+ return existingUrl === newUrl;
4290
+ }
4291
+ if (img.index !== void 0 && existing.index !== void 0) {
4292
+ return existing.index === img.index;
4293
+ }
4294
+ return false;
4295
+ });
4296
+ if (existingIndex === -1) {
4297
+ this.accumulatedImages.push(img);
4298
+ } else {
4299
+ this.accumulatedImages[existingIndex] = img;
4675
4300
  }
4676
4301
  }
4677
- return estimatedCosts;
4678
- }
4679
- /**
4680
- * Get pending API key amount
4681
- */
4682
- _getPendingCashuTokenAmount() {
4683
- const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
4684
- return apiKeyDistribution.reduce((total, item) => total + item.amount, 0);
4685
4302
  }
4686
- /**
4687
- * Handle errors and notify callbacks
4688
- */
4689
- _handleError(error, callbacks) {
4690
- this._log("ERROR", "[RoutstrClient] _handleError: Error occurred", error);
4691
- if (error instanceof Error) {
4692
- const isStreamError = error.message.includes("Error in input stream") || error.message.includes("Load failed");
4693
- const modifiedErrorMsg = isStreamError ? "AI stream was cut off, turn on Keep Active or please try again" : error.message;
4694
- this._log(
4695
- "ERROR",
4696
- `[RoutstrClient] _handleError: Error type=${error.constructor.name}, message=${modifiedErrorMsg}, isStreamError=${isStreamError}`
4697
- );
4303
+ };
4304
+
4305
+ // client/fetchAIResponse.ts
4306
+ async function fetchAIResponse(options, callbacks, deps) {
4307
+ const {
4308
+ messageHistory,
4309
+ selectedModel,
4310
+ baseUrl,
4311
+ mintUrl,
4312
+ maxTokens,
4313
+ headers
4314
+ } = options;
4315
+ try {
4316
+ const apiMessages = await convertMessages(messageHistory);
4317
+ callbacks.onPaymentProcessing?.(true);
4318
+ callbacks.onTokenCreated?.(deps.getPendingCashuTokenAmount?.() ?? 0);
4319
+ const providerInfo = await deps.providerRegistry.getProviderInfo(baseUrl);
4320
+ const providerVersion = providerInfo?.version ?? "";
4321
+ let modelIdForRequest = selectedModel.id;
4322
+ if (/^0\.1\./.test(providerVersion)) {
4323
+ const newModel = await deps.client.getProviderManager().getModelForProvider(baseUrl, selectedModel.id);
4324
+ modelIdForRequest = newModel?.id ?? selectedModel.id;
4325
+ }
4326
+ const body = {
4327
+ model: modelIdForRequest,
4328
+ messages: apiMessages,
4329
+ stream: true
4330
+ };
4331
+ if (maxTokens !== void 0) {
4332
+ body.max_tokens = maxTokens;
4333
+ }
4334
+ if (selectedModel?.name?.startsWith("OpenAI:")) {
4335
+ body.tools = [{ type: "web_search" }];
4336
+ }
4337
+ const response = await deps.client.routeRequest({
4338
+ path: "/v1/chat/completions",
4339
+ method: "POST",
4340
+ body,
4341
+ headers,
4342
+ baseUrl,
4343
+ mintUrl,
4344
+ modelId: selectedModel.id
4345
+ });
4346
+ if (!response.body) {
4347
+ throw new Error("Response body is not available");
4348
+ }
4349
+ if (response.status !== 200) {
4350
+ throw new Error(`${response.status} ${response.statusText}`);
4351
+ }
4352
+ const streamProcessor = new StreamProcessor();
4353
+ const streamingResult = await streamProcessor.process(
4354
+ response,
4355
+ {
4356
+ onContent: callbacks.onStreamingUpdate,
4357
+ onThinking: callbacks.onThinkingUpdate
4358
+ },
4359
+ selectedModel.id
4360
+ );
4361
+ if (streamingResult.finish_reason === "content_filter") {
4698
4362
  callbacks.onMessageAppend({
4699
- role: "system",
4700
- content: "Uncaught Error: " + modifiedErrorMsg + (this.alertLevel === "max" ? " | " + error.stack : "")
4363
+ role: "assistant",
4364
+ content: "Your request was denied due to content filtering."
4701
4365
  });
4366
+ } else if (streamingResult.content || streamingResult.images && streamingResult.images.length > 0) {
4367
+ const message = await createAssistantMessage(streamingResult);
4368
+ callbacks.onMessageAppend(message);
4702
4369
  } else {
4703
4370
  callbacks.onMessageAppend({
4704
4371
  role: "system",
4705
- content: "Unknown Error: Please tag Routstr on Nostr and/or retry."
4372
+ content: "The provider did not respond to this request."
4706
4373
  });
4707
4374
  }
4375
+ callbacks.onStreamingUpdate("");
4376
+ callbacks.onThinkingUpdate("");
4377
+ } catch (error) {
4378
+ handleError(error, callbacks, deps.alertLevel, deps.logger);
4379
+ } finally {
4380
+ callbacks.onPaymentProcessing?.(false);
4708
4381
  }
4709
- /**
4710
- * Check wallet balance and throw if insufficient
4711
- */
4712
- async _checkBalance() {
4713
- const balances = await this.walletAdapter.getBalances();
4714
- const totalBalance = Object.values(balances).reduce((sum, v) => sum + v, 0);
4715
- if (totalBalance <= 0) {
4716
- throw new InsufficientBalanceError(1, 0);
4382
+ }
4383
+ async function convertMessages(messages) {
4384
+ return Promise.all(
4385
+ messages.filter((m) => m.role !== "system").map(async (m) => ({
4386
+ role: m.role,
4387
+ content: typeof m.content === "string" ? m.content : m.content
4388
+ }))
4389
+ );
4390
+ }
4391
+ async function createAssistantMessage(result) {
4392
+ if (result.images && result.images.length > 0) {
4393
+ const content = [];
4394
+ if (result.content) {
4395
+ content.push({
4396
+ type: "text",
4397
+ text: result.content,
4398
+ thinking: result.thinking,
4399
+ citations: result.citations,
4400
+ annotations: result.annotations
4401
+ });
4717
4402
  }
4718
- }
4719
- /**
4720
- * Spend a token using CashuSpender with standardized error handling
4721
- */
4722
- async _spendToken(params) {
4723
- const { mintUrl, amount, baseUrl } = params;
4724
- this._log(
4725
- "DEBUG",
4726
- `[RoutstrClient] _spendToken: mode=${this.mode}, amount=${amount}, baseUrl=${baseUrl}, mintUrl=${mintUrl}`
4727
- );
4728
- if (this.mode === "apikeys") {
4729
- let parentApiKey = this.storageAdapter.getApiKey(baseUrl);
4730
- if (!parentApiKey) {
4731
- this._log(
4732
- "DEBUG",
4733
- `[RoutstrClient] _spendToken: No existing API key for ${baseUrl}, creating new one via Cashu`
4734
- );
4735
- const spendResult2 = await this.cashuSpender.spend({
4736
- mintUrl,
4737
- amount: amount * TOPUP_MARGIN,
4738
- baseUrl: "",
4739
- reuseToken: false
4740
- });
4741
- if (!spendResult2.token) {
4742
- this._log(
4743
- "ERROR",
4744
- `[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error:`,
4745
- spendResult2.error
4746
- );
4747
- throw new Error(
4748
- `[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error: ${spendResult2.error}`
4749
- );
4750
- } else {
4751
- this._log(
4752
- "DEBUG",
4753
- `[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult2.token}`
4754
- );
4755
- }
4756
- this._log(
4757
- "DEBUG",
4758
- `[RoutstrClient] _spendToken: Created API key for ${baseUrl}, key preview: ${spendResult2.token}, balance: ${spendResult2.balance}`
4759
- );
4760
- try {
4761
- this.storageAdapter.setApiKey(baseUrl, spendResult2.token);
4762
- } catch (error) {
4763
- if (error instanceof Error && error.message.includes("ApiKey already exists")) {
4764
- const receiveResult = await this.cashuSpender.receiveToken(
4765
- spendResult2.token
4766
- );
4767
- if (receiveResult.success) {
4768
- this._log(
4769
- "DEBUG",
4770
- `[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${receiveResult.amount}`
4771
- );
4772
- } else {
4773
- this._log(
4774
- "DEBUG",
4775
- `[RoutstrClient] _handleErrorResponse: Token restore failed: ${receiveResult.message}`
4776
- );
4777
- }
4778
- this._log(
4779
- "DEBUG",
4780
- `[RoutstrClient] _spendToken: API key already exists for ${baseUrl}, using existing key`
4781
- );
4782
- } else {
4783
- throw error;
4784
- }
4785
- }
4786
- parentApiKey = this.storageAdapter.getApiKey(baseUrl);
4787
- } else {
4788
- this._log(
4789
- "DEBUG",
4790
- `[RoutstrClient] _spendToken: Using existing API key for ${baseUrl}, key preview: ${parentApiKey.key}`
4791
- );
4792
- }
4793
- let tokenBalance = 0;
4794
- let tokenBalanceUnit = "sat";
4795
- const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
4796
- const distributionForBaseUrl = apiKeyDistribution.find(
4797
- (d) => d.baseUrl === baseUrl
4798
- );
4799
- if (distributionForBaseUrl) {
4800
- tokenBalance = distributionForBaseUrl.amount;
4801
- }
4802
- if (tokenBalance === 0 && parentApiKey) {
4803
- try {
4804
- const balanceInfo = await this.balanceManager.getTokenBalance(
4805
- parentApiKey.key,
4806
- baseUrl
4807
- );
4808
- tokenBalance = balanceInfo.amount;
4809
- tokenBalanceUnit = balanceInfo.unit;
4810
- } catch (e) {
4811
- this._log("WARN", "Could not get initial API key balance:", e);
4403
+ for (const img of result.images) {
4404
+ content.push({
4405
+ type: "image_url",
4406
+ image_url: {
4407
+ url: img.image_url.url
4812
4408
  }
4813
- }
4814
- this._log(
4815
- "DEBUG",
4816
- `[RoutstrClient] _spendToken: Returning token with balance=${tokenBalance} ${tokenBalanceUnit}`
4817
- );
4818
- return {
4819
- token: parentApiKey?.key ?? "",
4820
- tokenBalance,
4821
- tokenBalanceUnit
4822
- };
4823
- }
4824
- this._log(
4825
- "DEBUG",
4826
- `[RoutstrClient] _spendToken: Calling CashuSpender.spend for amount=${amount}, mintUrl=${mintUrl}, mode=${this.mode}`
4827
- );
4828
- const spendResult = await this.cashuSpender.spend({
4829
- mintUrl,
4830
- amount,
4831
- baseUrl: "",
4832
- reuseToken: false
4833
- });
4834
- if (!spendResult.token) {
4835
- this._log(
4836
- "ERROR",
4837
- `[RoutstrClient] _spendToken: CashuSpender.spend failed, error:`,
4838
- spendResult.error
4839
- );
4840
- } else {
4841
- this._log(
4842
- "DEBUG",
4843
- `[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult.token}, balance: ${spendResult.balance} ${spendResult.unit ?? "sat"}`
4844
- );
4845
- this.storageAdapter.addXcashuToken(baseUrl, spendResult.token);
4409
+ });
4846
4410
  }
4847
4411
  return {
4848
- token: spendResult.token,
4849
- tokenBalance: spendResult.balance,
4850
- tokenBalanceUnit: spendResult.unit ?? "sat"
4851
- };
4852
- }
4853
- /**
4854
- * Build request headers with common defaults and dev mock controls
4855
- */
4856
- _buildBaseHeaders(additionalHeaders = {}, token) {
4857
- const headers = {
4858
- ...additionalHeaders,
4859
- "Content-Type": "application/json"
4412
+ role: "assistant",
4413
+ content
4860
4414
  };
4861
- return headers;
4862
4415
  }
4863
- /**
4864
- * Attach auth headers using the active client mode
4865
- */
4866
- _withAuthHeader(headers, token) {
4867
- const nextHeaders = { ...headers };
4868
- if (this.mode === "xcashu") {
4869
- nextHeaders["X-Cashu"] = token;
4870
- } else {
4871
- nextHeaders["Authorization"] = `Bearer ${token}`;
4872
- }
4873
- return nextHeaders;
4416
+ return {
4417
+ role: "assistant",
4418
+ content: result.content || ""
4419
+ };
4420
+ }
4421
+ function handleError(error, callbacks, alertLevel, logger) {
4422
+ logger.error("[fetchAIResponse] Error occurred", error);
4423
+ if (error instanceof Error) {
4424
+ const isStreamError = error.message.includes("Error in input stream") || error.message.includes("Load failed");
4425
+ const modifiedErrorMsg = isStreamError ? "AI stream was cut off, turn on Keep Active or please try again" : error.message;
4426
+ logger.error(
4427
+ `[fetchAIResponse] Error type=${error.constructor.name}, message=${modifiedErrorMsg}, isStreamError=${isStreamError}`
4428
+ );
4429
+ callbacks.onMessageAppend({
4430
+ role: "system",
4431
+ content: "Uncaught Error: " + modifiedErrorMsg + (alertLevel === "max" ? " | " + error.stack : "")
4432
+ });
4433
+ } else {
4434
+ callbacks.onMessageAppend({
4435
+ role: "system",
4436
+ content: "Unknown Error: Please tag Routstr on Nostr and/or retry."
4437
+ });
4874
4438
  }
4875
- };
4439
+ }
4876
4440
 
4877
4441
  exports.ProviderManager = ProviderManager;
4878
4442
  exports.RoutstrClient = RoutstrClient;
4879
4443
  exports.StreamProcessor = StreamProcessor;
4880
4444
  exports.createSSEParserTransform = createSSEParserTransform;
4445
+ exports.fetchAIResponse = fetchAIResponse;
4881
4446
  exports.inspectSSEWebStream = inspectSSEWebStream;
4882
4447
  //# sourceMappingURL=index.js.map
4883
4448
  //# sourceMappingURL=index.js.map