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