@routstr/sdk 0.3.9 → 0.3.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/browser.d.mts +12 -0
  2. package/dist/browser.d.ts +12 -0
  3. package/dist/browser.js +6413 -0
  4. package/dist/browser.js.map +1 -0
  5. package/dist/browser.mjs +6361 -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 +6791 -0
  10. package/dist/bun.js.map +1 -0
  11. package/dist/bun.mjs +6733 -0
  12. package/dist/bun.mjs.map +1 -0
  13. package/dist/bunSqlite-BmXWNc25.d.ts +18 -0
  14. package/dist/bunSqlite-Bro9efsl.d.mts +18 -0
  15. package/dist/client/index.d.mts +85 -42
  16. package/dist/client/index.d.ts +85 -42
  17. package/dist/client/index.js +1243 -1584
  18. package/dist/client/index.js.map +1 -1
  19. package/dist/client/index.mjs +1239 -1585
  20. package/dist/client/index.mjs.map +1 -1
  21. package/dist/discovery/index.d.mts +33 -3
  22. package/dist/discovery/index.d.ts +33 -3
  23. package/dist/discovery/index.js +30 -31
  24. package/dist/discovery/index.js.map +1 -1
  25. package/dist/discovery/index.mjs +30 -31
  26. package/dist/discovery/index.mjs.map +1 -1
  27. package/dist/index.d.mts +9 -7
  28. package/dist/index.d.ts +9 -7
  29. package/dist/index.js +1264 -1648
  30. package/dist/index.js.map +1 -1
  31. package/dist/index.mjs +1260 -1645
  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 +6857 -0
  36. package/dist/node.js.map +1 -0
  37. package/dist/node.mjs +6801 -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 +1970 -0
  42. package/dist/storage/bun.js.map +1 -0
  43. package/dist/storage/bun.mjs +1946 -0
  44. package/dist/storage/bun.mjs.map +1 -0
  45. package/dist/storage/index.d.mts +4 -30
  46. package/dist/storage/index.d.ts +4 -30
  47. package/dist/storage/index.js +238 -650
  48. package/dist/storage/index.js.map +1 -1
  49. package/dist/storage/index.mjs +239 -647
  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 +2034 -0
  54. package/dist/storage/node.js.map +1 -0
  55. package/dist/storage/node.mjs +2012 -0
  56. package/dist/storage/node.mjs.map +1 -0
  57. package/dist/{store-58VcEUoA.d.ts → store-CAQLSbEj.d.ts} +52 -1
  58. package/dist/{store-C6dfj1cc.d.mts → store-CuXwe5Rg.d.mts} +52 -1
  59. package/dist/wallet/index.js +38 -24
  60. package/dist/wallet/index.js.map +1 -1
  61. package/dist/wallet/index.mjs +38 -24
  62. package/dist/wallet/index.mjs.map +1 -1
  63. package/package.json +26 -1
@@ -1061,8 +1061,8 @@ var BalanceManager = class _BalanceManager {
1061
1061
  const refundableProviderBalance = Object.entries(
1062
1062
  balanceState.providerBalances
1063
1063
  ).filter(([providerBaseUrl]) => providerBaseUrl !== baseUrl).reduce((sum, [, value]) => sum + value, 0);
1064
- if (totalMintBalance + targetProviderBalance < adjustedAmount && totalMintBalance + targetProviderBalance + refundableProviderBalance >= adjustedAmount && retryCount < 2) {
1065
- await this._refundOtherProvidersForTopUp(baseUrl, mintUrl, retryCount);
1064
+ if (totalMintBalance + targetProviderBalance < adjustedAmount && totalMintBalance + targetProviderBalance + refundableProviderBalance >= adjustedAmount && retryCount < 3) {
1065
+ await this._refundOtherProvidersForTopUp(baseUrl, mintUrl, retryCount, adjustedAmount);
1066
1066
  return this.createProviderToken({
1067
1067
  ...options,
1068
1068
  retryCount: retryCount + 1
@@ -1201,33 +1201,47 @@ var BalanceManager = class _BalanceManager {
1201
1201
  }
1202
1202
  return candidates;
1203
1203
  }
1204
- async _refundOtherProvidersForTopUp(baseUrl, mintUrl, retryCount) {
1204
+ async _refundOtherProvidersForTopUp(baseUrl, mintUrl, retryCount, requiredAmount) {
1205
1205
  const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
1206
1206
  const forceRefund = retryCount >= 2;
1207
- const apiKeysToRefund = apiKeyDistribution.filter(
1208
- (apiKey) => apiKey.baseUrl !== baseUrl && apiKey.amount > 0
1209
- );
1210
- const apiKeyRefundResults = await Promise.allSettled(
1211
- apiKeysToRefund.map(async (apiKeyEntry) => {
1212
- const fullApiKeyEntry = this.storageAdapter.getApiKey(
1213
- apiKeyEntry.baseUrl
1214
- );
1215
- if (!fullApiKeyEntry) {
1216
- return { baseUrl: apiKeyEntry.baseUrl, success: false };
1217
- }
1218
- const result = await this.refundApiKey({
1207
+ const candidates = apiKeyDistribution.filter((apiKey) => apiKey.baseUrl !== baseUrl && apiKey.amount > 0).map((apiKey) => {
1208
+ const full = this.storageAdapter.getApiKey(apiKey.baseUrl);
1209
+ return {
1210
+ baseUrl: apiKey.baseUrl,
1211
+ amount: apiKey.amount,
1212
+ lastUsed: full?.lastUsed ?? 0,
1213
+ key: full?.key
1214
+ };
1215
+ }).filter((c) => c.key != null).sort((a, b) => a.lastUsed - b.lastUsed);
1216
+ if (candidates.length === 0) return;
1217
+ if (forceRefund) {
1218
+ for (const candidate of candidates) {
1219
+ await this.refundApiKey({
1219
1220
  mintUrl,
1220
- baseUrl: apiKeyEntry.baseUrl,
1221
- apiKey: fullApiKeyEntry.key,
1222
- forceRefund
1221
+ baseUrl: candidate.baseUrl,
1222
+ apiKey: candidate.key,
1223
+ forceRefund: true
1223
1224
  });
1224
- return { baseUrl: apiKeyEntry.baseUrl, success: result.success };
1225
- })
1226
- );
1227
- for (const result of apiKeyRefundResults) {
1228
- if (result.status === "fulfilled" && result.value.success) {
1229
- this.storageAdapter.updateApiKeyBalance(result.value.baseUrl, 0);
1225
+ const newState = await this.getBalanceState();
1226
+ const newAvailable = (newState.mintBalances[mintUrl] || 0) + (newState.providerBalances[baseUrl] || 0);
1227
+ if (newAvailable >= requiredAmount) {
1228
+ this.logger.log(
1229
+ `_refundOtherProvidersForTopUp: freed enough balance (${newAvailable} >= ${requiredAmount}), stopping early`
1230
+ );
1231
+ return;
1232
+ }
1230
1233
  }
1234
+ } else {
1235
+ await Promise.allSettled(
1236
+ candidates.map(
1237
+ (candidate) => this.refundApiKey({
1238
+ mintUrl,
1239
+ baseUrl: candidate.baseUrl,
1240
+ apiKey: candidate.key,
1241
+ forceRefund: false
1242
+ })
1243
+ )
1244
+ );
1231
1245
  }
1232
1246
  }
1233
1247
  /**
@@ -1406,690 +1420,371 @@ var BalanceManager = class _BalanceManager {
1406
1420
  }
1407
1421
  };
1408
1422
 
1409
- // client/usage.ts
1410
- function extractUsageFromResponseBody(body, fallbackSatsCost = 0) {
1411
- if (!body || typeof body !== "object") return null;
1412
- const usage = body.usage;
1413
- if (!usage || typeof usage !== "object") return null;
1414
- const promptTokens = Number(usage.prompt_tokens ?? 0);
1415
- const completionTokens = Number(usage.completion_tokens ?? 0);
1416
- const totalTokens = Number(usage.total_tokens ?? 0);
1417
- const costValue = usage.cost;
1418
- let cost = 0;
1419
- let satsCost = fallbackSatsCost;
1420
- if (typeof costValue === "number") {
1421
- cost = costValue;
1422
- } else if (costValue && typeof costValue === "object") {
1423
- const costObj = costValue;
1424
- const totalUsd = costObj.total_usd;
1425
- const totalMsats = costObj.total_msats;
1426
- cost = typeof totalUsd === "number" ? totalUsd : 0;
1427
- if (typeof totalMsats === "number") {
1428
- satsCost = totalMsats / 1e3;
1429
- }
1430
- }
1431
- if (promptTokens === 0 && completionTokens === 0 && totalTokens === 0 && cost === 0 && satsCost === 0) {
1432
- return null;
1423
+ // utils/torUtils.ts
1424
+ var TOR_ONION_SUFFIX = ".onion";
1425
+ var isTorContext = () => {
1426
+ if (typeof window === "undefined") return false;
1427
+ const hostname = window.location.hostname.toLowerCase();
1428
+ return hostname.endsWith(TOR_ONION_SUFFIX);
1429
+ };
1430
+ var isOnionUrl = (url) => {
1431
+ if (!url) return false;
1432
+ const trimmed = url.trim().toLowerCase();
1433
+ if (!trimmed) return false;
1434
+ try {
1435
+ const candidate = trimmed.startsWith("http") ? trimmed : `http://${trimmed}`;
1436
+ return new URL(candidate).hostname.endsWith(TOR_ONION_SUFFIX);
1437
+ } catch {
1438
+ return trimmed.includes(TOR_ONION_SUFFIX);
1433
1439
  }
1434
- return {
1435
- promptTokens,
1436
- completionTokens,
1437
- totalTokens,
1438
- cost,
1439
- satsCost
1440
- };
1441
- }
1442
- function extractResponseId(body) {
1443
- if (!body || typeof body !== "object") return void 0;
1444
- const id = body.id;
1445
- if (typeof id !== "string") return void 0;
1446
- const trimmed = id.trim();
1447
- return trimmed.length > 0 ? trimmed : void 0;
1448
- }
1449
- function extractUsageFromSSEJson(parsed, fallbackSatsCost = 0) {
1450
- if (!parsed || typeof parsed !== "object") {
1440
+ };
1441
+
1442
+ // client/ProviderManager.ts
1443
+ function getImageResolutionFromDataUrl(dataUrl) {
1444
+ try {
1445
+ if (typeof dataUrl !== "string" || !dataUrl.startsWith("data:"))
1446
+ return null;
1447
+ const commaIdx = dataUrl.indexOf(",");
1448
+ if (commaIdx === -1) return null;
1449
+ const meta = dataUrl.slice(5, commaIdx);
1450
+ const base64 = dataUrl.slice(commaIdx + 1);
1451
+ const binary = typeof atob === "function" ? atob(base64) : Buffer.from(base64, "base64").toString("binary");
1452
+ const len = binary.length;
1453
+ const bytes = new Uint8Array(len);
1454
+ for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i);
1455
+ const isPNG = meta.includes("image/png");
1456
+ const isJPEG = meta.includes("image/jpeg") || meta.includes("image/jpg");
1457
+ if (isPNG) {
1458
+ const sig = [137, 80, 78, 71, 13, 10, 26, 10];
1459
+ for (let i = 0; i < sig.length; i++) {
1460
+ if (bytes[i] !== sig[i]) return null;
1461
+ }
1462
+ const view = new DataView(
1463
+ bytes.buffer,
1464
+ bytes.byteOffset,
1465
+ bytes.byteLength
1466
+ );
1467
+ const width = view.getUint32(16, false);
1468
+ const height = view.getUint32(20, false);
1469
+ if (width > 0 && height > 0) return { width, height };
1470
+ return null;
1471
+ }
1472
+ if (isJPEG) {
1473
+ let offset = 0;
1474
+ if (bytes[offset++] !== 255 || bytes[offset++] !== 216) return null;
1475
+ while (offset < bytes.length) {
1476
+ while (offset < bytes.length && bytes[offset] !== 255) offset++;
1477
+ if (offset + 1 >= bytes.length) break;
1478
+ while (bytes[offset] === 255) offset++;
1479
+ const marker = bytes[offset++];
1480
+ if (marker === 216 || marker === 217) continue;
1481
+ if (offset + 1 >= bytes.length) break;
1482
+ const length = bytes[offset] << 8 | bytes[offset + 1];
1483
+ offset += 2;
1484
+ if (marker === 192 || marker === 194) {
1485
+ if (length < 7 || offset + length - 2 > bytes.length) return null;
1486
+ const precision = bytes[offset];
1487
+ const height = bytes[offset + 1] << 8 | bytes[offset + 2];
1488
+ const width = bytes[offset + 3] << 8 | bytes[offset + 4];
1489
+ if (precision > 0 && width > 0 && height > 0)
1490
+ return { width, height };
1491
+ return null;
1492
+ } else {
1493
+ offset += length - 2;
1494
+ }
1495
+ }
1496
+ return null;
1497
+ }
1451
1498
  return null;
1452
- }
1453
- if (!parsed.usage && parsed.cost && typeof parsed.cost === "object") {
1454
- const costObj = parsed.cost;
1455
- const msats2 = costObj.total_msats ?? 0;
1456
- const cost2 = costObj.total_usd ?? 0;
1457
- if (msats2 === 0 && cost2 === 0) return null;
1458
- return {
1459
- promptTokens: Number(costObj.input_tokens ?? 0),
1460
- completionTokens: Number(costObj.output_tokens ?? 0),
1461
- totalTokens: Number((costObj.input_tokens ?? 0) + (costObj.output_tokens ?? 0)),
1462
- cost: Number(cost2),
1463
- satsCost: msats2 > 0 ? msats2 / 1e3 : fallbackSatsCost
1464
- };
1465
- }
1466
- if (!parsed.usage) {
1499
+ } catch {
1467
1500
  return null;
1468
1501
  }
1469
- const usage = parsed.usage;
1470
- const usageCost = usage.cost;
1471
- let cost = 0;
1472
- let msats = 0;
1473
- if (typeof usageCost === "number") {
1474
- cost = usageCost;
1475
- } else if (usageCost && typeof usageCost === "object") {
1476
- cost = usageCost.total_usd ?? 0;
1477
- msats = usageCost.total_msats ?? 0;
1502
+ }
1503
+ function calculateImageTokens(width, height, detail = "auto") {
1504
+ if (detail === "low") return 85;
1505
+ let w = width;
1506
+ let h = height;
1507
+ if (w > 2048 || h > 2048) {
1508
+ const aspectRatio = w / h;
1509
+ if (w > h) {
1510
+ w = 2048;
1511
+ h = Math.floor(w / aspectRatio);
1512
+ } else {
1513
+ h = 2048;
1514
+ w = Math.floor(h * aspectRatio);
1515
+ }
1478
1516
  }
1479
- if (cost === 0) {
1480
- cost = parsed.metadata?.routstr?.cost?.total_usd ?? 0;
1517
+ if (w > 768 || h > 768) {
1518
+ const aspectRatio = w / h;
1519
+ if (w > h) {
1520
+ w = 768;
1521
+ h = Math.floor(w / aspectRatio);
1522
+ } else {
1523
+ h = 768;
1524
+ w = Math.floor(h * aspectRatio);
1525
+ }
1481
1526
  }
1482
- if (msats === 0) {
1483
- msats = parsed.metadata?.routstr?.cost?.total_msats ?? (typeof usage.cost_sats === "number" ? usage.cost_sats * 1e3 : 0);
1527
+ const tilesWidth = Math.floor((w + 511) / 512);
1528
+ const tilesHeight = Math.floor((h + 511) / 512);
1529
+ const numTiles = tilesWidth * tilesHeight;
1530
+ return 85 + 170 * numTiles;
1531
+ }
1532
+ var ProviderManager = class _ProviderManager {
1533
+ constructor(providerRegistry, store, logger) {
1534
+ this.providerRegistry = providerRegistry;
1535
+ this.instanceId = `pm_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
1536
+ this.logger = (logger ?? consoleLogger).child(`ProviderManager:${this.instanceId}`);
1537
+ if (store) {
1538
+ this.store = store;
1539
+ this.hydrateFromStore();
1540
+ }
1484
1541
  }
1485
- const promptTokens = Number(usage.prompt_tokens ?? usage.input_tokens ?? 0);
1486
- const completionTokens = Number(usage.completion_tokens ?? usage.output_tokens ?? 0);
1487
- const totalTokens = Number(usage.total_tokens ?? promptTokens + completionTokens);
1488
- const result = {
1489
- promptTokens,
1490
- completionTokens,
1491
- totalTokens,
1492
- cost: Number(cost ?? 0),
1493
- satsCost: msats > 0 ? msats / 1e3 : fallbackSatsCost
1494
- };
1495
- if (result.promptTokens === 0 && result.completionTokens === 0 && result.totalTokens === 0 && result.cost === 0 && result.satsCost === 0) {
1496
- return null;
1542
+ providerRegistry;
1543
+ failedProviders = /* @__PURE__ */ new Set();
1544
+ /** Track when each provider last failed (provider URL -> timestamp) */
1545
+ lastFailed = /* @__PURE__ */ new Map();
1546
+ /** Providers on cooldown: [provider_url, cooldown_started_timestamp][] */
1547
+ providersOnCoolDown = [];
1548
+ /** Cooldown duration in milliseconds (42 seconds) */
1549
+ static COOLDOWN_DURATION_MS = 42 * 1e3;
1550
+ /** Optional persistent store for failure tracking */
1551
+ store = null;
1552
+ /** Instance ID for debugging */
1553
+ instanceId;
1554
+ logger;
1555
+ /**
1556
+ * Hydrate in-memory state from persistent store
1557
+ */
1558
+ hydrateFromStore() {
1559
+ if (!this.store) return;
1560
+ const state = this.store.getState();
1561
+ this.failedProviders = new Set(state.failedProviders);
1562
+ this.lastFailed = new Map(Object.entries(state.lastFailed));
1563
+ const now = Date.now();
1564
+ this.providersOnCoolDown = state.providersOnCooldown.filter(
1565
+ (entry) => now - entry.timestamp < _ProviderManager.COOLDOWN_DURATION_MS
1566
+ ).map((entry) => [entry.baseUrl, entry.timestamp]);
1567
+ this.logger.log(`Hydrated from store: failedProviders=${this.failedProviders.size} lastFailed=${this.lastFailed.size} providersOnCooldown=${this.providersOnCoolDown.length}`);
1497
1568
  }
1498
- return result;
1499
- }
1500
- function toUsageStats(usage) {
1501
- if (!usage) return void 0;
1502
- return {
1503
- total_tokens: usage.totalTokens,
1504
- prompt_tokens: usage.promptTokens,
1505
- completion_tokens: usage.completionTokens,
1506
- cost: usage.cost,
1507
- sats_cost: usage.satsCost
1508
- };
1509
- }
1510
-
1511
- // client/StreamProcessor.ts
1512
- var StreamProcessor = class {
1513
- accumulatedContent = "";
1514
- accumulatedThinking = "";
1515
- accumulatedImages = [];
1516
- isInThinking = false;
1517
- isInContent = false;
1518
1569
  /**
1519
- * Process a streaming response
1570
+ * Get instance ID for debugging
1520
1571
  */
1521
- async process(response, callbacks, modelId) {
1522
- if (!response.body) {
1523
- throw new Error("Response body is not available");
1524
- }
1525
- const reader = response.body.getReader();
1526
- const decoder = new TextDecoder("utf-8");
1527
- let buffer = "";
1528
- this.accumulatedContent = "";
1529
- this.accumulatedThinking = "";
1530
- this.accumulatedImages = [];
1531
- this.isInThinking = false;
1532
- this.isInContent = false;
1533
- let usage;
1534
- let model;
1535
- let finish_reason;
1536
- let citations;
1537
- let annotations;
1538
- let responseId;
1539
- try {
1540
- while (true) {
1541
- const { done, value } = await reader.read();
1542
- if (done) {
1543
- break;
1544
- }
1545
- const chunk = decoder.decode(value, { stream: true });
1546
- buffer += chunk;
1547
- const lines = buffer.split("\n");
1548
- buffer = lines.pop() || "";
1549
- for (const line of lines) {
1550
- const parsed = this._parseLine(line);
1551
- if (!parsed) continue;
1552
- if (parsed.content) {
1553
- this._handleContent(parsed.content, callbacks, modelId);
1554
- }
1555
- if (parsed.reasoning) {
1556
- this._handleThinking(parsed.reasoning, callbacks);
1557
- }
1558
- if (parsed.usage) {
1559
- usage = parsed.usage;
1560
- }
1561
- if (parsed.model) {
1562
- model = parsed.model;
1563
- }
1564
- if (parsed.finish_reason) {
1565
- finish_reason = parsed.finish_reason;
1566
- }
1567
- if (parsed.responseId) {
1568
- responseId = parsed.responseId;
1569
- }
1570
- if (parsed.citations) {
1571
- citations = parsed.citations;
1572
- }
1573
- if (parsed.annotations) {
1574
- annotations = parsed.annotations;
1575
- }
1576
- if (parsed.images) {
1577
- this._mergeImages(parsed.images);
1572
+ getInstanceId() {
1573
+ return this.instanceId;
1574
+ }
1575
+ /**
1576
+ * Clean up expired cooldown entries
1577
+ * Also removes the provider from failedProviders so it can be retried
1578
+ */
1579
+ cleanupExpiredCooldowns() {
1580
+ const now = Date.now();
1581
+ const before = this.providersOnCoolDown.length;
1582
+ this.providersOnCoolDown = this.providersOnCoolDown.filter(
1583
+ ([url, timestamp]) => {
1584
+ const age = now - timestamp;
1585
+ const isExpired = age >= _ProviderManager.COOLDOWN_DURATION_MS;
1586
+ if (isExpired) {
1587
+ this.logger.log(`Removing expired cooldown for ${url} (age: ${age}ms)`);
1588
+ this.failedProviders.delete(url);
1589
+ if (this.store) {
1590
+ this.store.getState().removeFailedProvider(url);
1578
1591
  }
1579
1592
  }
1593
+ return !isExpired;
1580
1594
  }
1581
- } finally {
1582
- reader.releaseLock();
1595
+ );
1596
+ const after = this.providersOnCoolDown.length;
1597
+ if (before !== after) {
1598
+ this.logger.log(`Cleaned up ${before - after} expired cooldown(s), ${after} remaining`);
1583
1599
  }
1584
- return {
1585
- content: this.accumulatedContent,
1586
- thinking: this.accumulatedThinking || void 0,
1587
- images: this.accumulatedImages.length > 0 ? this.accumulatedImages : void 0,
1588
- usage,
1589
- model,
1590
- responseId,
1591
- finish_reason,
1592
- citations,
1593
- annotations
1594
- };
1595
1600
  }
1596
1601
  /**
1597
- * Parse a single SSE line
1602
+ * Get the cooldown duration in milliseconds
1598
1603
  */
1599
- _parseLine(line) {
1600
- if (!line.trim()) return null;
1601
- if (!line.startsWith("data: ")) {
1602
- return null;
1603
- }
1604
- const jsonData = line.slice(6);
1605
- if (jsonData === "[DONE]") {
1606
- return null;
1607
- }
1608
- try {
1609
- const parsed = JSON.parse(jsonData);
1610
- const result = {};
1611
- if (parsed.choices?.[0]?.delta?.content) {
1612
- result.content = parsed.choices[0].delta.content;
1613
- }
1614
- if (parsed.choices?.[0]?.delta?.reasoning) {
1615
- result.reasoning = parsed.choices[0].delta.reasoning;
1616
- }
1617
- const extractedUsage = extractUsageFromSSEJson(parsed);
1618
- if (extractedUsage) {
1619
- result.usage = toUsageStats(extractedUsage);
1620
- } else if (parsed.usage) {
1621
- result.usage = {
1622
- total_tokens: parsed.usage.total_tokens ?? parsed.usage.input_tokens + parsed.usage.output_tokens,
1623
- prompt_tokens: parsed.usage.prompt_tokens ?? parsed.usage.input_tokens,
1624
- completion_tokens: parsed.usage.completion_tokens ?? parsed.usage.output_tokens
1625
- };
1626
- }
1627
- if (parsed.id) {
1628
- result.responseId = parsed.id;
1629
- }
1630
- if (parsed.model) {
1631
- result.model = parsed.model;
1632
- }
1633
- if (parsed.citations) {
1634
- result.citations = parsed.citations;
1635
- }
1636
- if (parsed.annotations) {
1637
- result.annotations = parsed.annotations;
1638
- }
1639
- if (parsed.choices?.[0]?.finish_reason) {
1640
- result.finish_reason = parsed.choices[0].finish_reason;
1641
- }
1642
- const images = parsed.choices?.[0]?.message?.images || parsed.choices?.[0]?.delta?.images;
1643
- if (images && Array.isArray(images)) {
1644
- result.images = images;
1645
- }
1646
- return result;
1647
- } catch {
1648
- return null;
1649
- }
1604
+ getCooldownDurationMs() {
1605
+ return _ProviderManager.COOLDOWN_DURATION_MS;
1650
1606
  }
1651
1607
  /**
1652
- * Handle content delta with thinking support
1608
+ * Check if a provider is currently on cooldown
1653
1609
  */
1654
- _handleContent(content, callbacks, modelId) {
1655
- if (this.isInThinking && !this.isInContent) {
1656
- this.accumulatedThinking += "</thinking>";
1657
- callbacks.onThinking(this.accumulatedThinking);
1658
- this.isInThinking = false;
1659
- this.isInContent = true;
1660
- }
1661
- if (modelId) {
1662
- this._extractThinkingFromContent(content, callbacks);
1663
- } else {
1664
- this.accumulatedContent += content;
1665
- }
1666
- callbacks.onContent(this.accumulatedContent);
1610
+ isOnCooldown(baseUrl) {
1611
+ this.cleanupExpiredCooldowns();
1612
+ const result = this.providersOnCoolDown.some(([url]) => url === baseUrl);
1613
+ return result;
1667
1614
  }
1668
1615
  /**
1669
- * Handle thinking/reasoning content
1616
+ * Get all providers currently on cooldown
1670
1617
  */
1671
- _handleThinking(reasoning, callbacks) {
1672
- if (!this.isInThinking) {
1673
- this.accumulatedThinking += "<thinking> ";
1674
- this.isInThinking = true;
1618
+ getProvidersOnCooldown() {
1619
+ this.cleanupExpiredCooldowns();
1620
+ return [...this.providersOnCoolDown];
1621
+ }
1622
+ /**
1623
+ * Reset the failed providers list
1624
+ */
1625
+ resetFailedProviders() {
1626
+ this.failedProviders.clear();
1627
+ if (this.store) {
1628
+ this.store.getState().setFailedProviders([]);
1675
1629
  }
1676
- this.accumulatedThinking += reasoning;
1677
- callbacks.onThinking(this.accumulatedThinking);
1678
1630
  }
1679
1631
  /**
1680
- * Extract thinking blocks from content (for models with inline thinking)
1632
+ * Get the last failed timestamp for a provider
1681
1633
  */
1682
- _extractThinkingFromContent(content, callbacks) {
1683
- const parts = content.split(/(<thinking>|<\/thinking>)/);
1684
- for (const part of parts) {
1685
- if (part === "<thinking>") {
1686
- this.isInThinking = true;
1687
- if (!this.accumulatedThinking.includes("<thinking>")) {
1688
- this.accumulatedThinking += "<thinking> ";
1634
+ getLastFailed(baseUrl) {
1635
+ return this.lastFailed.get(baseUrl);
1636
+ }
1637
+ /**
1638
+ * Get all providers with their last failed timestamps
1639
+ */
1640
+ getAllLastFailed() {
1641
+ return new Map(this.lastFailed);
1642
+ }
1643
+ /**
1644
+ * Mark a provider as failed
1645
+ * If a provider fails twice within 5 minutes, it's added to cooldown
1646
+ */
1647
+ markFailed(baseUrl) {
1648
+ const now = Date.now();
1649
+ const lastFailure = this.lastFailed.get(baseUrl);
1650
+ this.logger.log(`markFailed: ${baseUrl} lastFailure=${lastFailure} now=${now}`);
1651
+ if (lastFailure !== void 0) {
1652
+ const timeSinceLastFailure = now - lastFailure;
1653
+ this.logger.log(`markFailed: timeSinceLastFailure=${timeSinceLastFailure}ms withinCooldown=${timeSinceLastFailure < _ProviderManager.COOLDOWN_DURATION_MS}`);
1654
+ }
1655
+ this.lastFailed.set(baseUrl, now);
1656
+ this.failedProviders.add(baseUrl);
1657
+ if (this.store) {
1658
+ this.store.getState().setLastFailedTimestamp(baseUrl, now);
1659
+ this.store.getState().addFailedProvider(baseUrl);
1660
+ }
1661
+ this.logger.log(`markFailed: updated ${baseUrl} to ${now}, failedProviders=${this.failedProviders.size}`);
1662
+ if (lastFailure !== void 0 && now - lastFailure < _ProviderManager.COOLDOWN_DURATION_MS) {
1663
+ this.logger.log(`markFailed: second failure within cooldown window for ${baseUrl}`);
1664
+ if (!this.isOnCooldown(baseUrl)) {
1665
+ this.providersOnCoolDown.push([baseUrl, now]);
1666
+ if (this.store) {
1667
+ this.store.getState().addProviderOnCooldown(baseUrl, now);
1689
1668
  }
1690
- } else if (part === "</thinking>") {
1691
- this.isInThinking = false;
1692
- this.accumulatedThinking += "</thinking>";
1693
- } else if (this.isInThinking) {
1694
- this.accumulatedThinking += part;
1669
+ this.logger.log(`markFailed: ${baseUrl} added to cooldown`);
1695
1670
  } else {
1696
- this.accumulatedContent += part;
1671
+ this.logger.log(`markFailed: ${baseUrl} already on cooldown`);
1672
+ }
1673
+ } else {
1674
+ if (lastFailure === void 0) {
1675
+ this.logger.log(`markFailed: first failure for ${baseUrl}`);
1676
+ } else {
1677
+ this.logger.log(`markFailed: failure outside cooldown window for ${baseUrl} (${now - lastFailure}ms ago)`);
1697
1678
  }
1698
1679
  }
1699
1680
  }
1700
1681
  /**
1701
- * Merge images into accumulated array, avoiding duplicates
1682
+ * Remove a provider from cooldown (e.g., after successful request)
1702
1683
  */
1703
- _mergeImages(newImages) {
1704
- for (const img of newImages) {
1705
- const newUrl = img.image_url?.url;
1706
- const existingIndex = this.accumulatedImages.findIndex((existing) => {
1707
- const existingUrl = existing.image_url?.url;
1708
- if (newUrl && existingUrl) {
1709
- return existingUrl === newUrl;
1710
- }
1711
- if (img.index !== void 0 && existing.index !== void 0) {
1712
- return existing.index === img.index;
1713
- }
1714
- return false;
1715
- });
1716
- if (existingIndex === -1) {
1717
- this.accumulatedImages.push(img);
1718
- } else {
1719
- this.accumulatedImages[existingIndex] = img;
1720
- }
1721
- }
1722
- }
1723
- };
1724
-
1725
- // utils/torUtils.ts
1726
- var TOR_ONION_SUFFIX = ".onion";
1727
- var isTorContext = () => {
1728
- if (typeof window === "undefined") return false;
1729
- const hostname = window.location.hostname.toLowerCase();
1730
- return hostname.endsWith(TOR_ONION_SUFFIX);
1731
- };
1732
- var isOnionUrl = (url) => {
1733
- if (!url) return false;
1734
- const trimmed = url.trim().toLowerCase();
1735
- if (!trimmed) return false;
1736
- try {
1737
- const candidate = trimmed.startsWith("http") ? trimmed : `http://${trimmed}`;
1738
- return new URL(candidate).hostname.endsWith(TOR_ONION_SUFFIX);
1739
- } catch {
1740
- return trimmed.includes(TOR_ONION_SUFFIX);
1741
- }
1742
- };
1743
-
1744
- // client/ProviderManager.ts
1745
- function getImageResolutionFromDataUrl(dataUrl) {
1746
- try {
1747
- if (typeof dataUrl !== "string" || !dataUrl.startsWith("data:"))
1748
- return null;
1749
- const commaIdx = dataUrl.indexOf(",");
1750
- if (commaIdx === -1) return null;
1751
- const meta = dataUrl.slice(5, commaIdx);
1752
- const base64 = dataUrl.slice(commaIdx + 1);
1753
- const binary = typeof atob === "function" ? atob(base64) : Buffer.from(base64, "base64").toString("binary");
1754
- const len = binary.length;
1755
- const bytes = new Uint8Array(len);
1756
- for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i);
1757
- const isPNG = meta.includes("image/png");
1758
- const isJPEG = meta.includes("image/jpeg") || meta.includes("image/jpg");
1759
- if (isPNG) {
1760
- const sig = [137, 80, 78, 71, 13, 10, 26, 10];
1761
- for (let i = 0; i < sig.length; i++) {
1762
- if (bytes[i] !== sig[i]) return null;
1763
- }
1764
- const view = new DataView(
1765
- bytes.buffer,
1766
- bytes.byteOffset,
1767
- bytes.byteLength
1768
- );
1769
- const width = view.getUint32(16, false);
1770
- const height = view.getUint32(20, false);
1771
- if (width > 0 && height > 0) return { width, height };
1772
- return null;
1773
- }
1774
- if (isJPEG) {
1775
- let offset = 0;
1776
- if (bytes[offset++] !== 255 || bytes[offset++] !== 216) return null;
1777
- while (offset < bytes.length) {
1778
- while (offset < bytes.length && bytes[offset] !== 255) offset++;
1779
- if (offset + 1 >= bytes.length) break;
1780
- while (bytes[offset] === 255) offset++;
1781
- const marker = bytes[offset++];
1782
- if (marker === 216 || marker === 217) continue;
1783
- if (offset + 1 >= bytes.length) break;
1784
- const length = bytes[offset] << 8 | bytes[offset + 1];
1785
- offset += 2;
1786
- if (marker === 192 || marker === 194) {
1787
- if (length < 7 || offset + length - 2 > bytes.length) return null;
1788
- const precision = bytes[offset];
1789
- const height = bytes[offset + 1] << 8 | bytes[offset + 2];
1790
- const width = bytes[offset + 3] << 8 | bytes[offset + 4];
1791
- if (precision > 0 && width > 0 && height > 0)
1792
- return { width, height };
1793
- return null;
1794
- } else {
1795
- offset += length - 2;
1796
- }
1797
- }
1798
- return null;
1799
- }
1800
- return null;
1801
- } catch {
1802
- return null;
1803
- }
1804
- }
1805
- function calculateImageTokens(width, height, detail = "auto") {
1806
- if (detail === "low") return 85;
1807
- let w = width;
1808
- let h = height;
1809
- if (w > 2048 || h > 2048) {
1810
- const aspectRatio = w / h;
1811
- if (w > h) {
1812
- w = 2048;
1813
- h = Math.floor(w / aspectRatio);
1814
- } else {
1815
- h = 2048;
1816
- w = Math.floor(h * aspectRatio);
1684
+ removeFromCooldown(baseUrl) {
1685
+ this.providersOnCoolDown = this.providersOnCoolDown.filter(
1686
+ ([url]) => url !== baseUrl
1687
+ );
1688
+ if (this.store) {
1689
+ this.store.getState().removeProviderFromCooldown(baseUrl);
1817
1690
  }
1818
1691
  }
1819
- if (w > 768 || h > 768) {
1820
- const aspectRatio = w / h;
1821
- if (w > h) {
1822
- w = 768;
1823
- h = Math.floor(w / aspectRatio);
1824
- } else {
1825
- h = 768;
1826
- w = Math.floor(h * aspectRatio);
1692
+ /**
1693
+ * Clear all cooldown tracking
1694
+ */
1695
+ clearCooldowns() {
1696
+ this.providersOnCoolDown = [];
1697
+ if (this.store) {
1698
+ this.store.getState().clearProvidersOnCooldown();
1827
1699
  }
1828
1700
  }
1829
- const tilesWidth = Math.floor((w + 511) / 512);
1830
- const tilesHeight = Math.floor((h + 511) / 512);
1831
- const numTiles = tilesWidth * tilesHeight;
1832
- return 85 + 170 * numTiles;
1833
- }
1834
- function isInsecureHttpUrl(url) {
1835
- return url.startsWith("http://");
1836
- }
1837
- var ProviderManager = class _ProviderManager {
1838
- constructor(providerRegistry, store, logger) {
1839
- this.providerRegistry = providerRegistry;
1840
- this.instanceId = `pm_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
1841
- this.logger = (logger ?? consoleLogger).child(`ProviderManager:${this.instanceId}`);
1842
- if (store) {
1843
- this.store = store;
1844
- this.hydrateFromStore();
1701
+ /**
1702
+ * Clear all failure tracking (lastFailed timestamps)
1703
+ */
1704
+ clearFailureHistory() {
1705
+ this.lastFailed.clear();
1706
+ if (this.store) {
1707
+ this.store.getState().setLastFailed({});
1845
1708
  }
1846
1709
  }
1847
- providerRegistry;
1848
- failedProviders = /* @__PURE__ */ new Set();
1849
- /** Track when each provider last failed (provider URL -> timestamp) */
1850
- lastFailed = /* @__PURE__ */ new Map();
1851
- /** Providers on cooldown: [provider_url, cooldown_started_timestamp][] */
1852
- providersOnCoolDown = [];
1853
- /** Cooldown duration in milliseconds (42 seconds) */
1854
- static COOLDOWN_DURATION_MS = 42 * 1e3;
1855
- /** Optional persistent store for failure tracking */
1856
- store = null;
1857
- /** Instance ID for debugging */
1858
- instanceId;
1859
- logger;
1860
1710
  /**
1861
- * Hydrate in-memory state from persistent store
1711
+ * Check if a provider has failed
1862
1712
  */
1863
- hydrateFromStore() {
1864
- if (!this.store) return;
1865
- const state = this.store.getState();
1866
- this.failedProviders = new Set(state.failedProviders);
1867
- this.lastFailed = new Map(Object.entries(state.lastFailed));
1868
- const now = Date.now();
1869
- this.providersOnCoolDown = state.providersOnCooldown.filter(
1870
- (entry) => now - entry.timestamp < _ProviderManager.COOLDOWN_DURATION_MS
1871
- ).map((entry) => [entry.baseUrl, entry.timestamp]);
1872
- this.logger.log(`Hydrated from store: failedProviders=${this.failedProviders.size} lastFailed=${this.lastFailed.size} providersOnCooldown=${this.providersOnCoolDown.length}`);
1713
+ hasFailed(baseUrl) {
1714
+ return this.failedProviders.has(baseUrl);
1873
1715
  }
1874
1716
  /**
1875
- * Get instance ID for debugging
1717
+ * Get a copy of the failed providers set
1876
1718
  */
1877
- getInstanceId() {
1878
- return this.instanceId;
1719
+ getFailedProviders() {
1720
+ return new Set(this.failedProviders);
1879
1721
  }
1880
1722
  /**
1881
- * Clean up expired cooldown entries
1882
- * Also removes the provider from failedProviders so it can be retried
1723
+ * Find the next best provider for a model
1724
+ * @param modelId The model ID to find a provider for
1725
+ * @param currentBaseUrl The current provider to exclude
1726
+ * @returns The best provider URL or null if none available
1883
1727
  */
1884
- cleanupExpiredCooldowns() {
1885
- const now = Date.now();
1886
- const before = this.providersOnCoolDown.length;
1887
- this.providersOnCoolDown = this.providersOnCoolDown.filter(
1888
- ([url, timestamp]) => {
1889
- const age = now - timestamp;
1890
- const isExpired = age >= _ProviderManager.COOLDOWN_DURATION_MS;
1891
- if (isExpired) {
1892
- this.logger.log(`Removing expired cooldown for ${url} (age: ${age}ms)`);
1893
- this.failedProviders.delete(url);
1894
- if (this.store) {
1895
- this.store.getState().removeFailedProvider(url);
1896
- }
1728
+ findNextBestProvider(modelId, currentBaseUrl) {
1729
+ try {
1730
+ const torMode = isTorContext();
1731
+ const disabledProviders = new Set(
1732
+ this.providerRegistry.getDisabledProviders()
1733
+ );
1734
+ this.logger.log(`findNextBestProvider: model=${modelId} disabled=${[...disabledProviders].length} onCooldown=${this.providersOnCoolDown.length}`);
1735
+ const allProviders = this.providerRegistry.getAllProvidersModels();
1736
+ this.logger.log(`findNextBestProvider: total providers=${Object.keys(allProviders).length}`);
1737
+ const candidates = [];
1738
+ for (const [baseUrl, models] of Object.entries(allProviders)) {
1739
+ if (baseUrl === currentBaseUrl) {
1740
+ continue;
1897
1741
  }
1898
- return !isExpired;
1742
+ if (disabledProviders.has(baseUrl)) {
1743
+ continue;
1744
+ }
1745
+ if (this.isOnCooldown(baseUrl)) {
1746
+ continue;
1747
+ }
1748
+ if (!torMode && isOnionUrl(baseUrl)) {
1749
+ continue;
1750
+ }
1751
+ const model = models.find((m) => m.id === modelId);
1752
+ if (!model) {
1753
+ continue;
1754
+ }
1755
+ const cost = model.sats_pricing?.completion ?? 0;
1756
+ candidates.push({ baseUrl, model, cost });
1899
1757
  }
1900
- );
1901
- const after = this.providersOnCoolDown.length;
1902
- if (before !== after) {
1903
- this.logger.log(`Cleaned up ${before - after} expired cooldown(s), ${after} remaining`);
1758
+ candidates.sort((a, b) => a.cost - b.cost);
1759
+ if (candidates.length > 0) {
1760
+ return candidates[0].baseUrl;
1761
+ } else {
1762
+ return null;
1763
+ }
1764
+ } catch (error) {
1765
+ this.logger.error("findNextBestProvider error:", error);
1766
+ return null;
1904
1767
  }
1905
1768
  }
1906
1769
  /**
1907
- * Get the cooldown duration in milliseconds
1770
+ * Find the best model for a provider
1771
+ * Useful when switching providers and need to find equivalent model
1908
1772
  */
1909
- getCooldownDurationMs() {
1910
- return _ProviderManager.COOLDOWN_DURATION_MS;
1773
+ async getModelForProvider(baseUrl, modelId) {
1774
+ const models = this.providerRegistry.getModelsForProvider(baseUrl);
1775
+ const exactMatch = models.find((m) => m.id === modelId);
1776
+ if (exactMatch) return exactMatch;
1777
+ const providerInfo = await this.providerRegistry.getProviderInfo(baseUrl);
1778
+ if (providerInfo?.version && /^0\.1\./.test(providerInfo.version)) {
1779
+ const suffix = modelId.split("/").pop();
1780
+ const suffixMatch = models.find((m) => m.id === suffix);
1781
+ if (suffixMatch) return suffixMatch;
1782
+ }
1783
+ return null;
1911
1784
  }
1912
1785
  /**
1913
- * Check if a provider is currently on cooldown
1914
- */
1915
- isOnCooldown(baseUrl) {
1916
- this.cleanupExpiredCooldowns();
1917
- const result = this.providersOnCoolDown.some(([url]) => url === baseUrl);
1918
- return result;
1919
- }
1920
- /**
1921
- * Get all providers currently on cooldown
1922
- */
1923
- getProvidersOnCooldown() {
1924
- this.cleanupExpiredCooldowns();
1925
- return [...this.providersOnCoolDown];
1926
- }
1927
- /**
1928
- * Reset the failed providers list
1929
- */
1930
- resetFailedProviders() {
1931
- this.failedProviders.clear();
1932
- if (this.store) {
1933
- this.store.getState().setFailedProviders([]);
1934
- }
1935
- }
1936
- /**
1937
- * Get the last failed timestamp for a provider
1938
- */
1939
- getLastFailed(baseUrl) {
1940
- return this.lastFailed.get(baseUrl);
1941
- }
1942
- /**
1943
- * Get all providers with their last failed timestamps
1944
- */
1945
- getAllLastFailed() {
1946
- return new Map(this.lastFailed);
1947
- }
1948
- /**
1949
- * Mark a provider as failed
1950
- * If a provider fails twice within 5 minutes, it's added to cooldown
1951
- */
1952
- markFailed(baseUrl) {
1953
- const now = Date.now();
1954
- const lastFailure = this.lastFailed.get(baseUrl);
1955
- this.logger.log(`markFailed: ${baseUrl} lastFailure=${lastFailure} now=${now}`);
1956
- if (lastFailure !== void 0) {
1957
- const timeSinceLastFailure = now - lastFailure;
1958
- this.logger.log(`markFailed: timeSinceLastFailure=${timeSinceLastFailure}ms withinCooldown=${timeSinceLastFailure < _ProviderManager.COOLDOWN_DURATION_MS}`);
1959
- }
1960
- this.lastFailed.set(baseUrl, now);
1961
- this.failedProviders.add(baseUrl);
1962
- if (this.store) {
1963
- this.store.getState().setLastFailedTimestamp(baseUrl, now);
1964
- this.store.getState().addFailedProvider(baseUrl);
1965
- }
1966
- this.logger.log(`markFailed: updated ${baseUrl} to ${now}, failedProviders=${this.failedProviders.size}`);
1967
- if (lastFailure !== void 0 && now - lastFailure < _ProviderManager.COOLDOWN_DURATION_MS) {
1968
- this.logger.log(`markFailed: second failure within cooldown window for ${baseUrl}`);
1969
- if (!this.isOnCooldown(baseUrl)) {
1970
- this.providersOnCoolDown.push([baseUrl, now]);
1971
- if (this.store) {
1972
- this.store.getState().addProviderOnCooldown(baseUrl, now);
1973
- }
1974
- this.logger.log(`markFailed: ${baseUrl} added to cooldown`);
1975
- } else {
1976
- this.logger.log(`markFailed: ${baseUrl} already on cooldown`);
1977
- }
1978
- } else {
1979
- if (lastFailure === void 0) {
1980
- this.logger.log(`markFailed: first failure for ${baseUrl}`);
1981
- } else {
1982
- this.logger.log(`markFailed: failure outside cooldown window for ${baseUrl} (${now - lastFailure}ms ago)`);
1983
- }
1984
- }
1985
- }
1986
- /**
1987
- * Remove a provider from cooldown (e.g., after successful request)
1988
- */
1989
- removeFromCooldown(baseUrl) {
1990
- this.providersOnCoolDown = this.providersOnCoolDown.filter(
1991
- ([url]) => url !== baseUrl
1992
- );
1993
- if (this.store) {
1994
- this.store.getState().removeProviderFromCooldown(baseUrl);
1995
- }
1996
- }
1997
- /**
1998
- * Clear all cooldown tracking
1999
- */
2000
- clearCooldowns() {
2001
- this.providersOnCoolDown = [];
2002
- if (this.store) {
2003
- this.store.getState().clearProvidersOnCooldown();
2004
- }
2005
- }
2006
- /**
2007
- * Clear all failure tracking (lastFailed timestamps)
2008
- */
2009
- clearFailureHistory() {
2010
- this.lastFailed.clear();
2011
- if (this.store) {
2012
- this.store.getState().setLastFailed({});
2013
- }
2014
- }
2015
- /**
2016
- * Check if a provider has failed
2017
- */
2018
- hasFailed(baseUrl) {
2019
- return this.failedProviders.has(baseUrl);
2020
- }
2021
- /**
2022
- * Get a copy of the failed providers set
2023
- */
2024
- getFailedProviders() {
2025
- return new Set(this.failedProviders);
2026
- }
2027
- /**
2028
- * Find the next best provider for a model
2029
- * @param modelId The model ID to find a provider for
2030
- * @param currentBaseUrl The current provider to exclude
2031
- * @returns The best provider URL or null if none available
2032
- */
2033
- findNextBestProvider(modelId, currentBaseUrl) {
2034
- try {
2035
- const torMode = isTorContext();
2036
- const disabledProviders = new Set(
2037
- this.providerRegistry.getDisabledProviders()
2038
- );
2039
- this.logger.log(`findNextBestProvider: model=${modelId} disabled=${[...disabledProviders].length} onCooldown=${this.providersOnCoolDown.length}`);
2040
- const allProviders = this.providerRegistry.getAllProvidersModels();
2041
- this.logger.log(`findNextBestProvider: total providers=${Object.keys(allProviders).length}`);
2042
- const candidates = [];
2043
- for (const [baseUrl, models] of Object.entries(allProviders)) {
2044
- if (baseUrl === currentBaseUrl) {
2045
- continue;
2046
- }
2047
- if (disabledProviders.has(baseUrl)) {
2048
- continue;
2049
- }
2050
- if (this.isOnCooldown(baseUrl)) {
2051
- continue;
2052
- }
2053
- if (!torMode && (isOnionUrl(baseUrl) || isInsecureHttpUrl(baseUrl))) {
2054
- continue;
2055
- }
2056
- const model = models.find((m) => m.id === modelId);
2057
- if (!model) {
2058
- continue;
2059
- }
2060
- const cost = model.sats_pricing?.completion ?? 0;
2061
- candidates.push({ baseUrl, model, cost });
2062
- }
2063
- candidates.sort((a, b) => a.cost - b.cost);
2064
- if (candidates.length > 0) {
2065
- return candidates[0].baseUrl;
2066
- } else {
2067
- return null;
2068
- }
2069
- } catch (error) {
2070
- this.logger.error("findNextBestProvider error:", error);
2071
- return null;
2072
- }
2073
- }
2074
- /**
2075
- * Find the best model for a provider
2076
- * Useful when switching providers and need to find equivalent model
2077
- */
2078
- async getModelForProvider(baseUrl, modelId) {
2079
- const models = this.providerRegistry.getModelsForProvider(baseUrl);
2080
- const exactMatch = models.find((m) => m.id === modelId);
2081
- if (exactMatch) return exactMatch;
2082
- const providerInfo = await this.providerRegistry.getProviderInfo(baseUrl);
2083
- if (providerInfo?.version && /^0\.1\./.test(providerInfo.version)) {
2084
- const suffix = modelId.split("/").pop();
2085
- const suffixMatch = models.find((m) => m.id === suffix);
2086
- if (suffixMatch) return suffixMatch;
2087
- }
2088
- return null;
2089
- }
2090
- /**
2091
- * Get all available providers for a model
2092
- * Returns sorted list by price
1786
+ * Get all available providers for a model
1787
+ * Returns sorted list by price
2093
1788
  */
2094
1789
  getAllProvidersForModel(modelId) {
2095
1790
  const candidates = [];
@@ -2101,7 +1796,7 @@ var ProviderManager = class _ProviderManager {
2101
1796
  for (const [baseUrl, models] of Object.entries(allProviders)) {
2102
1797
  if (disabledProviders.has(baseUrl)) continue;
2103
1798
  if (this.isOnCooldown(baseUrl)) continue;
2104
- if (!torMode && (isOnionUrl(baseUrl) || isInsecureHttpUrl(baseUrl)))
1799
+ if (!torMode && isOnionUrl(baseUrl))
2105
1800
  continue;
2106
1801
  const model = models.find((m) => m.id === modelId);
2107
1802
  if (!model) continue;
@@ -2116,16 +1811,18 @@ var ProviderManager = class _ProviderManager {
2116
1811
  getProviderPriceRankingForModel(modelId, options = {}) {
2117
1812
  const includeDisabled = options.includeDisabled ?? false;
2118
1813
  const torMode = options.torMode ?? false;
2119
- const disabledProviders = new Set(
2120
- this.providerRegistry.getDisabledProviders()
2121
- );
1814
+ const disabledProviderList = this.providerRegistry.getDisabledProviders();
1815
+ const disabledProviders = new Set(disabledProviderList);
1816
+ if (disabledProviderList.length > 0) {
1817
+ this.logger.log(`getProviderPriceRankingForModel: disabled providers (${disabledProviderList.length}): ${disabledProviderList.join(", ")}`);
1818
+ }
2122
1819
  const allModels = this.providerRegistry.getAllProvidersModels();
2123
1820
  const results = [];
2124
1821
  for (const [baseUrl, models] of Object.entries(allModels)) {
2125
1822
  if (!includeDisabled && disabledProviders.has(baseUrl)) continue;
2126
1823
  if (this.isOnCooldown(baseUrl)) continue;
2127
1824
  if (torMode && !baseUrl.includes(".onion")) continue;
2128
- if (!torMode && (baseUrl.includes(".onion") || isInsecureHttpUrl(baseUrl)))
1825
+ if (!torMode && baseUrl.includes(".onion"))
2129
1826
  continue;
2130
1827
  const match = models.find((model) => model.id === modelId);
2131
1828
  if (!match?.sats_pricing) continue;
@@ -2145,12 +1842,20 @@ var ProviderManager = class _ProviderManager {
2145
1842
  totalPerMillion
2146
1843
  });
2147
1844
  }
2148
- return results.sort((a, b) => {
1845
+ results.sort((a, b) => {
2149
1846
  if (a.totalPerMillion !== b.totalPerMillion) {
2150
1847
  return a.totalPerMillion - b.totalPerMillion;
2151
1848
  }
2152
1849
  return a.baseUrl.localeCompare(b.baseUrl);
2153
1850
  });
1851
+ if (results.length > 0) {
1852
+ 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");
1853
+ this.logger.log(`getProviderPriceRankingForModel: ${modelId} ranking (${results.length} providers):
1854
+ ${ranking}`);
1855
+ } else {
1856
+ this.logger.log(`getProviderPriceRankingForModel: ${modelId} no providers found`);
1857
+ }
1858
+ return results;
2154
1859
  }
2155
1860
  /**
2156
1861
  * Get best-priced provider for a specific model
@@ -2338,92 +2043,6 @@ var createMemoryDriver = (seed) => {
2338
2043
  };
2339
2044
  };
2340
2045
 
2341
- // storage/drivers/sqlite.ts
2342
- var isBun = () => {
2343
- return typeof process.versions.bun !== "undefined";
2344
- };
2345
- var cachedDbModule = null;
2346
- var loadDatabase = async (dbPath) => {
2347
- if (isBun()) {
2348
- throw new Error(
2349
- "SQLite driver not supported in Bun. Use createBunSqliteDriver() instead."
2350
- );
2351
- }
2352
- try {
2353
- if (!cachedDbModule) {
2354
- cachedDbModule = (await import('better-sqlite3')).default;
2355
- }
2356
- return new cachedDbModule(dbPath);
2357
- } catch (error) {
2358
- throw new Error(
2359
- `better-sqlite3 is required for sqlite storage. Install it to use sqlite storage. (${error})`
2360
- );
2361
- }
2362
- };
2363
- var createSqliteDriver = (options = {}) => {
2364
- const dbPath = options.dbPath || "routstr.sqlite";
2365
- const tableName = options.tableName || "sdk_storage";
2366
- let db;
2367
- let selectStmt;
2368
- let upsertStmt;
2369
- let deleteStmt;
2370
- const initDb = async () => {
2371
- if (!db) {
2372
- db = await loadDatabase(dbPath);
2373
- db.exec(
2374
- `CREATE TABLE IF NOT EXISTS ${tableName} (key TEXT PRIMARY KEY, value TEXT NOT NULL)`
2375
- );
2376
- selectStmt = db.prepare(`SELECT value FROM ${tableName} WHERE key = ?`);
2377
- upsertStmt = db.prepare(
2378
- `INSERT INTO ${tableName} (key, value) VALUES (?, ?)
2379
- ON CONFLICT(key) DO UPDATE SET value = excluded.value`
2380
- );
2381
- deleteStmt = db.prepare(`DELETE FROM ${tableName} WHERE key = ?`);
2382
- }
2383
- };
2384
- const ensureInit = async () => {
2385
- if (!db) {
2386
- await initDb();
2387
- }
2388
- };
2389
- return {
2390
- async getItem(key, defaultValue) {
2391
- try {
2392
- await ensureInit();
2393
- const row = selectStmt.get(key);
2394
- if (!row || typeof row.value !== "string") return defaultValue;
2395
- try {
2396
- return JSON.parse(row.value);
2397
- } catch (parseError) {
2398
- if (typeof defaultValue === "string") {
2399
- return row.value;
2400
- }
2401
- throw parseError;
2402
- }
2403
- } catch (error) {
2404
- console.error(`SQLite getItem failed for key "${key}":`, error);
2405
- return defaultValue;
2406
- }
2407
- },
2408
- async setItem(key, value) {
2409
- try {
2410
- await ensureInit();
2411
- upsertStmt.run(key, JSON.stringify(value));
2412
- } catch (error) {
2413
- console.error(`SQLite setItem failed for key "${key}":`, error);
2414
- }
2415
- },
2416
- async removeItem(key) {
2417
- try {
2418
- await ensureInit();
2419
- deleteStmt.run(key);
2420
- } catch (error) {
2421
- console.error(`SQLite removeItem failed for key "${key}":`, error);
2422
- }
2423
- }
2424
- };
2425
- };
2426
-
2427
2046
  // storage/keys.ts
2428
2047
  var SDK_STORAGE_KEYS = {
2429
2048
  MODELS_FROM_ALL_PROVIDERS: "modelsFromAllProviders",
@@ -2447,6 +2066,91 @@ var SDK_STORAGE_KEYS = {
2447
2066
  PROVIDERS_ON_COOLDOWN: "providers_on_cooldown"
2448
2067
  };
2449
2068
 
2069
+ // storage/usageTracking/aggregate.ts
2070
+ var pad2 = (n) => String(n).padStart(2, "0");
2071
+ var jsGroupKey = (entry, groupBy, tzOffsetMinutes) => {
2072
+ switch (groupBy) {
2073
+ case "modelId":
2074
+ return entry.modelId ?? null;
2075
+ case "baseUrl":
2076
+ return entry.baseUrl ?? null;
2077
+ case "client":
2078
+ return entry.client ?? null;
2079
+ case "sessionId":
2080
+ return entry.sessionId ?? null;
2081
+ case "provider":
2082
+ return entry.provider ?? null;
2083
+ case "day": {
2084
+ const d = new Date(entry.timestamp - tzOffsetMinutes * 6e4);
2085
+ return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;
2086
+ }
2087
+ case "hour": {
2088
+ const d = new Date(entry.timestamp - tzOffsetMinutes * 6e4);
2089
+ return pad2(d.getUTCHours());
2090
+ }
2091
+ }
2092
+ };
2093
+ var reduceAggregate = (entries, options = {}) => {
2094
+ const emptyRow = (group) => ({
2095
+ group,
2096
+ requests: 0,
2097
+ promptTokens: 0,
2098
+ completionTokens: 0,
2099
+ totalTokens: 0,
2100
+ cost: 0,
2101
+ satsCost: 0,
2102
+ baseMsats: 0,
2103
+ inputMsats: 0,
2104
+ outputMsats: 0,
2105
+ totalMsats: 0,
2106
+ totalUsd: 0,
2107
+ cacheReadInputTokens: 0,
2108
+ cacheCreationInputTokens: 0,
2109
+ cacheReadMsats: 0,
2110
+ cacheCreationMsats: 0
2111
+ });
2112
+ const accumulate = (row, entry) => {
2113
+ row.requests += 1;
2114
+ row.promptTokens += entry.promptTokens;
2115
+ row.completionTokens += entry.completionTokens;
2116
+ row.totalTokens += entry.totalTokens;
2117
+ row.cost += entry.cost;
2118
+ row.satsCost += entry.satsCost;
2119
+ row.baseMsats += entry.baseMsats ?? 0;
2120
+ row.inputMsats += entry.inputMsats ?? 0;
2121
+ row.outputMsats += entry.outputMsats ?? 0;
2122
+ row.totalMsats += entry.totalMsats ?? 0;
2123
+ row.totalUsd += entry.totalUsd ?? 0;
2124
+ row.cacheReadInputTokens += entry.cacheReadInputTokens ?? 0;
2125
+ row.cacheCreationInputTokens += entry.cacheCreationInputTokens ?? 0;
2126
+ row.cacheReadMsats += entry.cacheReadMsats ?? 0;
2127
+ row.cacheCreationMsats += entry.cacheCreationMsats ?? 0;
2128
+ };
2129
+ if (!options.groupBy) {
2130
+ const total = emptyRow(null);
2131
+ for (const entry of entries) accumulate(total, entry);
2132
+ return [total];
2133
+ }
2134
+ const tz = options.tzOffsetMinutes ?? 0;
2135
+ const groups = /* @__PURE__ */ new Map();
2136
+ for (const entry of entries) {
2137
+ const key = jsGroupKey(entry, options.groupBy, tz);
2138
+ let row = groups.get(key);
2139
+ if (!row) {
2140
+ row = emptyRow(key);
2141
+ groups.set(key, row);
2142
+ }
2143
+ accumulate(row, entry);
2144
+ }
2145
+ const rows = [...groups.values()];
2146
+ if (options.groupBy === "day" || options.groupBy === "hour") {
2147
+ rows.sort((a, b) => (a.group ?? "").localeCompare(b.group ?? ""));
2148
+ } else {
2149
+ rows.sort((a, b) => b.satsCost - a.satsCost);
2150
+ }
2151
+ return rows;
2152
+ };
2153
+
2450
2154
  // storage/usageTracking/indexedDB.ts
2451
2155
  var DEFAULT_DB_NAME = "routstr-sdk";
2452
2156
  var DEFAULT_STORE_NAME = "usage_tracking";
@@ -2458,9 +2162,10 @@ var openDatabase = (dbName, storeName) => {
2458
2162
  return Promise.reject(new Error("IndexedDB is not available"));
2459
2163
  }
2460
2164
  return new Promise((resolve, reject) => {
2461
- const request = indexedDB.open(dbName, 1);
2165
+ const request = indexedDB.open(dbName, 3);
2462
2166
  request.onupgradeneeded = () => {
2463
2167
  const db = request.result;
2168
+ const tx = request.transaction;
2464
2169
  if (!db.objectStoreNames.contains(storeName)) {
2465
2170
  const store = db.createObjectStore(storeName, { keyPath: "id" });
2466
2171
  store.createIndex("timestamp", "timestamp", { unique: false });
@@ -2468,10 +2173,25 @@ var openDatabase = (dbName, storeName) => {
2468
2173
  store.createIndex("baseUrl", "baseUrl", { unique: false });
2469
2174
  store.createIndex("sessionId", "sessionId", { unique: false });
2470
2175
  store.createIndex("client", "client", { unique: false });
2176
+ store.createIndex("provider", "provider", { unique: false });
2177
+ } else if (tx) {
2178
+ const store = tx.objectStore(storeName);
2179
+ if (!store.indexNames.contains("provider")) {
2180
+ store.createIndex("provider", "provider", { unique: false });
2181
+ }
2182
+ }
2183
+ if (storeName !== "sdk_storage" && !db.objectStoreNames.contains("sdk_storage")) {
2184
+ db.createObjectStore("sdk_storage");
2471
2185
  }
2472
2186
  };
2473
2187
  request.onsuccess = () => resolve(request.result);
2474
2188
  request.onerror = () => reject(request.error);
2189
+ request.onblocked = () => {
2190
+ console.warn(
2191
+ `[usageTracking IndexedDB] open blocked for "${dbName}" \u2014 close other tabs using this DB`
2192
+ );
2193
+ reject(new Error(`IndexedDB "${dbName}" blocked by another connection`));
2194
+ };
2475
2195
  });
2476
2196
  };
2477
2197
  var matchesFilters = (entry, options = {}) => {
@@ -2493,6 +2213,12 @@ var matchesFilters = (entry, options = {}) => {
2493
2213
  if (options.client && entry.client !== options.client) {
2494
2214
  return false;
2495
2215
  }
2216
+ if (options.clients && options.clients.length > 0 && (entry.client == null || !options.clients.includes(entry.client))) {
2217
+ return false;
2218
+ }
2219
+ if (options.provider && entry.provider !== options.provider) {
2220
+ return false;
2221
+ }
2496
2222
  return true;
2497
2223
  };
2498
2224
  var createIndexedDBUsageTrackingDriver = (options = {}) => {
@@ -2588,6 +2314,10 @@ var createIndexedDBUsageTrackingDriver = (options = {}) => {
2588
2314
  const results = await this.list(options2);
2589
2315
  return results.length;
2590
2316
  },
2317
+ async aggregate(options2 = {}) {
2318
+ const entries = await this.list(options2);
2319
+ return reduceAggregate(entries, options2);
2320
+ },
2591
2321
  async deleteOlderThan(timestamp) {
2592
2322
  await ensureMigrated();
2593
2323
  const db = await getDb();
@@ -2624,410 +2354,31 @@ var createIndexedDBUsageTrackingDriver = (options = {}) => {
2624
2354
  };
2625
2355
  };
2626
2356
 
2627
- // storage/usageTracking/sqlite.ts
2628
- var MIGRATION_MARKER_KEY2 = "usage_tracking_migration_v1";
2357
+ // storage/usageTracking/memory.ts
2629
2358
  var normalizeBaseUrl2 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
2630
- var isBun2 = () => {
2631
- return typeof process.versions.bun !== "undefined";
2632
- };
2633
- var cachedDbModule2 = null;
2634
- var loadDatabase2 = async (dbPath) => {
2635
- if (isBun2()) {
2636
- throw new Error(
2637
- "SQLite driver not supported in Bun. Use createMemoryDriver() instead."
2638
- );
2639
- }
2640
- try {
2641
- if (!cachedDbModule2) {
2642
- cachedDbModule2 = (await import('better-sqlite3')).default;
2643
- }
2644
- return new cachedDbModule2(dbPath);
2645
- } catch (error) {
2646
- throw new Error(
2647
- `better-sqlite3 is required for sqlite usage tracking. Install it to use sqlite storage. (${error})`
2648
- );
2359
+ var matchesFilters2 = (entry, options = {}) => {
2360
+ if (typeof options.before === "number" && entry.timestamp >= options.before) {
2361
+ return false;
2649
2362
  }
2650
- };
2651
- var buildWhereClause = (options = {}) => {
2652
- const clauses = [];
2653
- const params = [];
2654
- if (typeof options.before === "number") {
2655
- clauses.push("timestamp < ?");
2656
- params.push(options.before);
2363
+ if (typeof options.after === "number" && entry.timestamp <= options.after) {
2364
+ return false;
2657
2365
  }
2658
- if (typeof options.after === "number") {
2659
- clauses.push("timestamp > ?");
2660
- params.push(options.after);
2366
+ if (options.modelId && entry.modelId !== options.modelId) {
2367
+ return false;
2661
2368
  }
2662
- if (options.modelId) {
2663
- clauses.push("model_id = ?");
2664
- params.push(options.modelId);
2369
+ if (options.baseUrl && normalizeBaseUrl2(entry.baseUrl) !== normalizeBaseUrl2(options.baseUrl)) {
2370
+ return false;
2665
2371
  }
2666
- if (options.baseUrl) {
2667
- clauses.push("base_url = ?");
2668
- params.push(normalizeBaseUrl2(options.baseUrl));
2669
- }
2670
- if (options.sessionId) {
2671
- clauses.push("session_id = ?");
2672
- params.push(options.sessionId);
2673
- }
2674
- if (options.client) {
2675
- clauses.push("client = ?");
2676
- params.push(options.client);
2677
- }
2678
- return {
2679
- sql: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
2680
- params
2681
- };
2682
- };
2683
- var createSqliteUsageTrackingDriver = (options = {}) => {
2684
- const dbPath = options.dbPath || "routstr.sqlite";
2685
- const tableName = options.tableName || "usage_tracking";
2686
- const legacyStorageDriver = options.legacyStorageDriver;
2687
- let db;
2688
- let insertStmt;
2689
- let migrationComplete = false;
2690
- const initDb = async () => {
2691
- if (!db) {
2692
- db = await loadDatabase2(dbPath);
2693
- db.exec(`
2694
- CREATE TABLE IF NOT EXISTS ${tableName} (
2695
- id TEXT PRIMARY KEY,
2696
- timestamp INTEGER NOT NULL,
2697
- model_id TEXT NOT NULL,
2698
- base_url TEXT NOT NULL,
2699
- request_id TEXT NOT NULL,
2700
- cost REAL NOT NULL,
2701
- sats_cost REAL NOT NULL,
2702
- prompt_tokens INTEGER NOT NULL,
2703
- completion_tokens INTEGER NOT NULL,
2704
- total_tokens INTEGER NOT NULL,
2705
- client TEXT,
2706
- session_id TEXT,
2707
- tags TEXT
2708
- );
2709
- CREATE INDEX IF NOT EXISTS idx_${tableName}_timestamp ON ${tableName}(timestamp);
2710
- CREATE INDEX IF NOT EXISTS idx_${tableName}_model_id ON ${tableName}(model_id);
2711
- CREATE INDEX IF NOT EXISTS idx_${tableName}_base_url ON ${tableName}(base_url);
2712
- CREATE INDEX IF NOT EXISTS idx_${tableName}_session_id ON ${tableName}(session_id);
2713
- CREATE INDEX IF NOT EXISTS idx_${tableName}_client ON ${tableName}(client);
2714
- `);
2715
- insertStmt = db.prepare(`
2716
- INSERT OR REPLACE INTO ${tableName} (
2717
- id, timestamp, model_id, base_url, request_id,
2718
- cost, sats_cost, prompt_tokens, completion_tokens, total_tokens,
2719
- client, session_id, tags
2720
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2721
- `);
2722
- }
2723
- };
2724
- const ensureInit = async () => {
2725
- if (!db) {
2726
- await initDb();
2727
- }
2728
- };
2729
- const appendOne = (entry) => {
2730
- insertStmt.run(
2731
- entry.id,
2732
- entry.timestamp,
2733
- entry.modelId,
2734
- normalizeBaseUrl2(entry.baseUrl),
2735
- entry.requestId,
2736
- entry.cost,
2737
- entry.satsCost,
2738
- entry.promptTokens,
2739
- entry.completionTokens,
2740
- entry.totalTokens,
2741
- entry.client ?? null,
2742
- entry.sessionId ?? null,
2743
- JSON.stringify(entry.tags ?? [])
2744
- );
2745
- };
2746
- const ensureMigrated = async () => {
2747
- if (!legacyStorageDriver || migrationComplete) return;
2748
- const migrated = await legacyStorageDriver.getItem(
2749
- MIGRATION_MARKER_KEY2,
2750
- false
2751
- );
2752
- if (migrated) {
2753
- migrationComplete = true;
2754
- return;
2755
- }
2756
- const legacyEntries = await legacyStorageDriver.getItem(
2757
- SDK_STORAGE_KEYS.USAGE_TRACKING,
2758
- []
2759
- );
2760
- for (const entry of legacyEntries) {
2761
- appendOne(entry);
2762
- }
2763
- if (legacyEntries.length > 0) {
2764
- await legacyStorageDriver.removeItem(SDK_STORAGE_KEYS.USAGE_TRACKING);
2765
- }
2766
- await legacyStorageDriver.setItem(MIGRATION_MARKER_KEY2, true);
2767
- migrationComplete = true;
2768
- };
2769
- const mapRow = (row) => ({
2770
- id: row.id,
2771
- timestamp: row.timestamp,
2772
- modelId: row.model_id,
2773
- baseUrl: row.base_url,
2774
- requestId: row.request_id,
2775
- cost: row.cost,
2776
- satsCost: row.sats_cost,
2777
- promptTokens: row.prompt_tokens,
2778
- completionTokens: row.completion_tokens,
2779
- totalTokens: row.total_tokens,
2780
- client: row.client ?? void 0,
2781
- sessionId: row.session_id ?? void 0,
2782
- tags: typeof row.tags === "string" ? JSON.parse(row.tags) : void 0
2783
- });
2784
- return {
2785
- async migrate() {
2786
- await ensureInit();
2787
- await ensureMigrated();
2788
- },
2789
- async append(entry) {
2790
- await ensureInit();
2791
- await ensureMigrated();
2792
- appendOne(entry);
2793
- },
2794
- async appendMany(entries) {
2795
- await ensureInit();
2796
- await ensureMigrated();
2797
- for (const entry of entries) {
2798
- appendOne(entry);
2799
- }
2800
- },
2801
- async list(options2 = {}) {
2802
- await ensureInit();
2803
- await ensureMigrated();
2804
- const { sql, params } = buildWhereClause(options2);
2805
- const limitSql = typeof options2.limit === "number" ? " LIMIT ?" : "";
2806
- const stmt = db.prepare(
2807
- `SELECT * FROM ${tableName} ${sql} ORDER BY timestamp DESC${limitSql}`
2808
- );
2809
- const rows = stmt.all(
2810
- ...typeof options2.limit === "number" ? [...params, options2.limit] : params
2811
- );
2812
- return rows.map(mapRow);
2813
- },
2814
- async count(options2 = {}) {
2815
- await ensureInit();
2816
- await ensureMigrated();
2817
- const { sql, params } = buildWhereClause(options2);
2818
- const stmt = db.prepare(`SELECT COUNT(*) as count FROM ${tableName} ${sql}`);
2819
- const row = stmt.get(...params);
2820
- return Number(row?.count ?? 0);
2821
- },
2822
- async deleteOlderThan(timestamp) {
2823
- await ensureInit();
2824
- await ensureMigrated();
2825
- const stmt = db.prepare(`DELETE FROM ${tableName} WHERE timestamp < ?`);
2826
- const result = stmt.run(timestamp);
2827
- return result.changes;
2828
- },
2829
- async clear() {
2830
- await ensureInit();
2831
- await ensureMigrated();
2832
- db.prepare(`DELETE FROM ${tableName}`).run();
2833
- }
2834
- };
2835
- };
2836
-
2837
- // storage/usageTracking/bunSqlite.ts
2838
- var MIGRATION_MARKER_KEY3 = "usage_tracking_migration_v1";
2839
- var normalizeBaseUrl3 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
2840
- var buildWhereClause2 = (options = {}) => {
2841
- const clauses = [];
2842
- const params = [];
2843
- if (typeof options.before === "number") {
2844
- clauses.push("timestamp < ?");
2845
- params.push(options.before);
2846
- }
2847
- if (typeof options.after === "number") {
2848
- clauses.push("timestamp > ?");
2849
- params.push(options.after);
2850
- }
2851
- if (options.modelId) {
2852
- clauses.push("model_id = ?");
2853
- params.push(options.modelId);
2854
- }
2855
- if (options.baseUrl) {
2856
- clauses.push("base_url = ?");
2857
- params.push(normalizeBaseUrl3(options.baseUrl));
2858
- }
2859
- if (options.sessionId) {
2860
- clauses.push("session_id = ?");
2861
- params.push(options.sessionId);
2862
- }
2863
- if (options.client) {
2864
- clauses.push("client = ?");
2865
- params.push(options.client);
2866
- }
2867
- return {
2868
- sql: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
2869
- params
2870
- };
2871
- };
2872
- var createBunSqliteUsageTrackingDriver = (options = {}) => {
2873
- const dbPath = options.dbPath || "routstr.sqlite";
2874
- const tableName = options.tableName || "usage_tracking";
2875
- const legacyStorageDriver = options.legacyStorageDriver;
2876
- const SQLiteDatabase = options.sqlite?.Database;
2877
- let migrationPromise = null;
2878
- if (!SQLiteDatabase) {
2879
- throw new Error(
2880
- "Bun SQLite Database constructor is required. Pass { sqlite: { Database } } when creating the driver."
2881
- );
2882
- }
2883
- const db = new SQLiteDatabase(dbPath);
2884
- db.run(`
2885
- CREATE TABLE IF NOT EXISTS ${tableName} (
2886
- id TEXT PRIMARY KEY,
2887
- timestamp INTEGER NOT NULL,
2888
- model_id TEXT NOT NULL,
2889
- base_url TEXT NOT NULL,
2890
- request_id TEXT NOT NULL,
2891
- cost REAL NOT NULL,
2892
- sats_cost REAL NOT NULL,
2893
- prompt_tokens INTEGER NOT NULL,
2894
- completion_tokens INTEGER NOT NULL,
2895
- total_tokens INTEGER NOT NULL,
2896
- client TEXT,
2897
- session_id TEXT,
2898
- tags TEXT
2899
- )
2900
- `);
2901
- db.run(`CREATE INDEX IF NOT EXISTS idx_${tableName}_timestamp ON ${tableName}(timestamp)`);
2902
- db.run(`CREATE INDEX IF NOT EXISTS idx_${tableName}_model_id ON ${tableName}(model_id)`);
2903
- db.run(`CREATE INDEX IF NOT EXISTS idx_${tableName}_base_url ON ${tableName}(base_url)`);
2904
- const appendOne = (entry) => {
2905
- db.query(`
2906
- INSERT OR REPLACE INTO ${tableName} (
2907
- id, timestamp, model_id, base_url, request_id,
2908
- cost, sats_cost, prompt_tokens, completion_tokens, total_tokens,
2909
- client, session_id, tags
2910
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2911
- `).run(
2912
- entry.id,
2913
- entry.timestamp,
2914
- entry.modelId,
2915
- normalizeBaseUrl3(entry.baseUrl),
2916
- entry.requestId,
2917
- entry.cost,
2918
- entry.satsCost,
2919
- entry.promptTokens,
2920
- entry.completionTokens,
2921
- entry.totalTokens,
2922
- entry.client ?? null,
2923
- entry.sessionId ?? null,
2924
- JSON.stringify(entry.tags ?? [])
2925
- );
2926
- };
2927
- const mapRow = (row) => ({
2928
- id: row.id,
2929
- timestamp: row.timestamp,
2930
- modelId: row.model_id,
2931
- baseUrl: row.base_url,
2932
- requestId: row.request_id,
2933
- cost: row.cost,
2934
- satsCost: row.sats_cost,
2935
- promptTokens: row.prompt_tokens,
2936
- completionTokens: row.completion_tokens,
2937
- totalTokens: row.total_tokens,
2938
- client: row.client ?? void 0,
2939
- sessionId: row.session_id ?? void 0,
2940
- tags: typeof row.tags === "string" ? JSON.parse(row.tags) : void 0
2941
- });
2942
- const ensureMigrated = async () => {
2943
- if (!legacyStorageDriver) return;
2944
- if (!migrationPromise) {
2945
- migrationPromise = (async () => {
2946
- const migrated = await legacyStorageDriver.getItem(
2947
- MIGRATION_MARKER_KEY3,
2948
- false
2949
- );
2950
- if (migrated) return;
2951
- const legacyEntries = await legacyStorageDriver.getItem(
2952
- SDK_STORAGE_KEYS.USAGE_TRACKING,
2953
- []
2954
- );
2955
- if (legacyEntries.length > 0) {
2956
- for (const entry of legacyEntries) {
2957
- appendOne(entry);
2958
- }
2959
- await legacyStorageDriver.removeItem(SDK_STORAGE_KEYS.USAGE_TRACKING);
2960
- }
2961
- await legacyStorageDriver.setItem(MIGRATION_MARKER_KEY3, true);
2962
- })();
2963
- }
2964
- await migrationPromise;
2965
- };
2966
- return {
2967
- async migrate() {
2968
- await ensureMigrated();
2969
- },
2970
- async append(entry) {
2971
- await ensureMigrated();
2972
- appendOne(entry);
2973
- },
2974
- async appendMany(entries) {
2975
- await ensureMigrated();
2976
- for (const entry of entries) {
2977
- appendOne(entry);
2978
- }
2979
- },
2980
- async list(options2 = {}) {
2981
- await ensureMigrated();
2982
- const { sql, params } = buildWhereClause2(options2);
2983
- const limitSql = typeof options2.limit === "number" ? " LIMIT ?" : "";
2984
- const query = `SELECT * FROM ${tableName} ${sql} ORDER BY timestamp DESC${limitSql}`;
2985
- let rows;
2986
- if (typeof options2.limit === "number") {
2987
- rows = db.query(query).all(...params, options2.limit);
2988
- } else {
2989
- rows = db.query(query).all(...params);
2990
- }
2991
- return rows.map(mapRow);
2992
- },
2993
- async count(options2 = {}) {
2994
- const { sql, params } = buildWhereClause2(options2);
2995
- const query = `SELECT COUNT(*) as count FROM ${tableName} ${sql}`;
2996
- const row = db.query(query).get(...params);
2997
- return Number(row?.count ?? 0);
2998
- },
2999
- async deleteOlderThan(timestamp) {
3000
- await ensureMigrated();
3001
- const before = timestamp;
3002
- const result = db.query(`DELETE FROM ${tableName} WHERE timestamp < ?`).run(before);
3003
- return result.changes ?? 0;
3004
- },
3005
- async clear() {
3006
- await ensureMigrated();
3007
- db.query(`DELETE FROM ${tableName}`).run();
3008
- }
3009
- };
3010
- };
3011
-
3012
- // storage/usageTracking/memory.ts
3013
- var normalizeBaseUrl4 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
3014
- var matchesFilters2 = (entry, options = {}) => {
3015
- if (typeof options.before === "number" && entry.timestamp >= options.before) {
3016
- return false;
3017
- }
3018
- if (typeof options.after === "number" && entry.timestamp <= options.after) {
3019
- return false;
3020
- }
3021
- if (options.modelId && entry.modelId !== options.modelId) {
2372
+ if (options.sessionId && entry.sessionId !== options.sessionId) {
3022
2373
  return false;
3023
2374
  }
3024
- if (options.baseUrl && normalizeBaseUrl4(entry.baseUrl) !== normalizeBaseUrl4(options.baseUrl)) {
2375
+ if (options.client && entry.client !== options.client) {
3025
2376
  return false;
3026
2377
  }
3027
- if (options.sessionId && entry.sessionId !== options.sessionId) {
2378
+ if (options.clients && options.clients.length > 0 && (entry.client == null || !options.clients.includes(entry.client))) {
3028
2379
  return false;
3029
2380
  }
3030
- if (options.client && entry.client !== options.client) {
2381
+ if (options.provider && entry.provider !== options.provider) {
3031
2382
  return false;
3032
2383
  }
3033
2384
  return true;
@@ -3035,18 +2386,18 @@ var matchesFilters2 = (entry, options = {}) => {
3035
2386
  var createMemoryUsageTrackingDriver = (seed = []) => {
3036
2387
  const store = /* @__PURE__ */ new Map();
3037
2388
  for (const entry of seed) {
3038
- store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl4(entry.baseUrl) });
2389
+ store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl2(entry.baseUrl) });
3039
2390
  }
3040
2391
  return {
3041
2392
  async migrate() {
3042
2393
  return;
3043
2394
  },
3044
2395
  async append(entry) {
3045
- store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl4(entry.baseUrl) });
2396
+ store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl2(entry.baseUrl) });
3046
2397
  },
3047
2398
  async appendMany(entries) {
3048
2399
  for (const entry of entries) {
3049
- store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl4(entry.baseUrl) });
2400
+ store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl2(entry.baseUrl) });
3050
2401
  }
3051
2402
  },
3052
2403
  async list(options = {}) {
@@ -3059,6 +2410,10 @@ var createMemoryUsageTrackingDriver = (seed = []) => {
3059
2410
  async count(options = {}) {
3060
2411
  return (await this.list(options)).length;
3061
2412
  },
2413
+ async aggregate(options = {}) {
2414
+ const entries = [...store.values()].filter((entry) => matchesFilters2(entry, options));
2415
+ return reduceAggregate(entries, options);
2416
+ },
3062
2417
  async deleteOlderThan(timestamp) {
3063
2418
  let deleted = 0;
3064
2419
  for (const [id, entry] of store.entries()) {
@@ -3074,7 +2429,7 @@ var createMemoryUsageTrackingDriver = (seed = []) => {
3074
2429
  }
3075
2430
  };
3076
2431
  };
3077
- var normalizeBaseUrl5 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
2432
+ var normalizeBaseUrl3 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
3078
2433
  var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3079
2434
  modelsFromAllProviders: {},
3080
2435
  lastUsedModel: null,
@@ -3097,7 +2452,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3097
2452
  setModelsFromAllProviders: (value) => {
3098
2453
  const normalized = {};
3099
2454
  for (const [baseUrl, models] of Object.entries(value)) {
3100
- normalized[normalizeBaseUrl5(baseUrl)] = models;
2455
+ normalized[normalizeBaseUrl3(baseUrl)] = models;
3101
2456
  }
3102
2457
  void driver.setItem(
3103
2458
  SDK_STORAGE_KEYS.MODELS_FROM_ALL_PROVIDERS,
@@ -3110,7 +2465,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3110
2465
  set({ lastUsedModel: value });
3111
2466
  },
3112
2467
  setBaseUrlsList: (value) => {
3113
- const normalized = value.map((url) => normalizeBaseUrl5(url));
2468
+ const normalized = value.map((url) => normalizeBaseUrl3(url));
3114
2469
  void driver.setItem(SDK_STORAGE_KEYS.BASE_URLS_LIST, normalized);
3115
2470
  set({ baseUrlsList: normalized });
3116
2471
  },
@@ -3119,14 +2474,14 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3119
2474
  set({ lastBaseUrlsUpdate: value });
3120
2475
  },
3121
2476
  setDisabledProviders: (value) => {
3122
- const normalized = value.map((url) => normalizeBaseUrl5(url));
2477
+ const normalized = value.map((url) => normalizeBaseUrl3(url));
3123
2478
  void driver.setItem(SDK_STORAGE_KEYS.DISABLED_PROVIDERS, normalized);
3124
2479
  set({ disabledProviders: normalized });
3125
2480
  },
3126
2481
  setMintsFromAllProviders: (value) => {
3127
2482
  const normalized = {};
3128
2483
  for (const [baseUrl, mints] of Object.entries(value)) {
3129
- normalized[normalizeBaseUrl5(baseUrl)] = mints.map(
2484
+ normalized[normalizeBaseUrl3(baseUrl)] = mints.map(
3130
2485
  (mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint
3131
2486
  );
3132
2487
  }
@@ -3139,7 +2494,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3139
2494
  setInfoFromAllProviders: (value) => {
3140
2495
  const normalized = {};
3141
2496
  for (const [baseUrl, info] of Object.entries(value)) {
3142
- normalized[normalizeBaseUrl5(baseUrl)] = info;
2497
+ normalized[normalizeBaseUrl3(baseUrl)] = info;
3143
2498
  }
3144
2499
  void driver.setItem(SDK_STORAGE_KEYS.INFO_FROM_ALL_PROVIDERS, normalized);
3145
2500
  set({ infoFromAllProviders: normalized });
@@ -3147,7 +2502,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3147
2502
  setLastModelsUpdate: (value) => {
3148
2503
  const normalized = {};
3149
2504
  for (const [baseUrl, timestamp] of Object.entries(value)) {
3150
- normalized[normalizeBaseUrl5(baseUrl)] = timestamp;
2505
+ normalized[normalizeBaseUrl3(baseUrl)] = timestamp;
3151
2506
  }
3152
2507
  void driver.setItem(SDK_STORAGE_KEYS.LAST_MODELS_UPDATE, normalized);
3153
2508
  set({ lastModelsUpdate: normalized });
@@ -3157,7 +2512,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3157
2512
  const updates = typeof value === "function" ? value(state.apiKeys) : value;
3158
2513
  const normalized = updates.map((entry) => ({
3159
2514
  ...entry,
3160
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
2515
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
3161
2516
  balance: entry.balance ?? 0,
3162
2517
  lastUsed: entry.lastUsed ?? null
3163
2518
  }));
@@ -3169,7 +2524,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3169
2524
  set((state) => {
3170
2525
  const updates = typeof value === "function" ? value(state.childKeys) : value;
3171
2526
  const normalized = updates.map((entry) => ({
3172
- parentBaseUrl: normalizeBaseUrl5(entry.parentBaseUrl),
2527
+ parentBaseUrl: normalizeBaseUrl3(entry.parentBaseUrl),
3173
2528
  childKey: entry.childKey,
3174
2529
  balance: entry.balance ?? 0,
3175
2530
  balanceLimit: entry.balanceLimit,
@@ -3183,9 +2538,9 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3183
2538
  setXcashuTokens: (value) => {
3184
2539
  const normalized = {};
3185
2540
  for (const [baseUrl, tokens] of Object.entries(value)) {
3186
- normalized[normalizeBaseUrl5(baseUrl)] = tokens.map((entry) => ({
2541
+ normalized[normalizeBaseUrl3(baseUrl)] = tokens.map((entry) => ({
3187
2542
  ...entry,
3188
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
2543
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
3189
2544
  createdAt: entry.createdAt ?? Date.now(),
3190
2545
  tryCount: entry.tryCount ?? 0
3191
2546
  }));
@@ -3236,12 +2591,12 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3236
2591
  },
3237
2592
  // ========== Failure Tracking ==========
3238
2593
  setFailedProviders: (value) => {
3239
- const normalized = value.map((url) => normalizeBaseUrl5(url));
2594
+ const normalized = value.map((url) => normalizeBaseUrl3(url));
3240
2595
  void driver.setItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, normalized);
3241
2596
  set({ failedProviders: normalized });
3242
2597
  },
3243
2598
  addFailedProvider: (baseUrl) => {
3244
- const normalized = normalizeBaseUrl5(baseUrl);
2599
+ const normalized = normalizeBaseUrl3(baseUrl);
3245
2600
  const current = get().failedProviders;
3246
2601
  if (!current.includes(normalized)) {
3247
2602
  const updated = [...current, normalized];
@@ -3250,7 +2605,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3250
2605
  }
3251
2606
  },
3252
2607
  removeFailedProvider: (baseUrl) => {
3253
- const normalized = normalizeBaseUrl5(baseUrl);
2608
+ const normalized = normalizeBaseUrl3(baseUrl);
3254
2609
  const current = get().failedProviders;
3255
2610
  const updated = current.filter((url) => url !== normalized);
3256
2611
  void driver.setItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, updated);
@@ -3259,13 +2614,13 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3259
2614
  setLastFailed: (value) => {
3260
2615
  const normalized = {};
3261
2616
  for (const [baseUrl, timestamp] of Object.entries(value)) {
3262
- normalized[normalizeBaseUrl5(baseUrl)] = timestamp;
2617
+ normalized[normalizeBaseUrl3(baseUrl)] = timestamp;
3263
2618
  }
3264
2619
  void driver.setItem(SDK_STORAGE_KEYS.LAST_FAILED, normalized);
3265
2620
  set({ lastFailed: normalized });
3266
2621
  },
3267
2622
  setLastFailedTimestamp: (baseUrl, timestamp) => {
3268
- const normalized = normalizeBaseUrl5(baseUrl);
2623
+ const normalized = normalizeBaseUrl3(baseUrl);
3269
2624
  const current = get().lastFailed;
3270
2625
  const updated = { ...current, [normalized]: timestamp };
3271
2626
  void driver.setItem(SDK_STORAGE_KEYS.LAST_FAILED, updated);
@@ -3273,14 +2628,14 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3273
2628
  },
3274
2629
  setProvidersOnCooldown: (value) => {
3275
2630
  const normalized = value.map((entry) => ({
3276
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
2631
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
3277
2632
  timestamp: entry.timestamp
3278
2633
  }));
3279
2634
  void driver.setItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, normalized);
3280
2635
  set({ providersOnCooldown: normalized });
3281
2636
  },
3282
2637
  addProviderOnCooldown: (baseUrl, timestamp) => {
3283
- const normalized = normalizeBaseUrl5(baseUrl);
2638
+ const normalized = normalizeBaseUrl3(baseUrl);
3284
2639
  const current = get().providersOnCooldown;
3285
2640
  if (!current.some((entry) => entry.baseUrl === normalized)) {
3286
2641
  const updated = [...current, { baseUrl: normalized, timestamp }];
@@ -3289,7 +2644,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3289
2644
  }
3290
2645
  },
3291
2646
  removeProviderFromCooldown: (baseUrl) => {
3292
- const normalized = normalizeBaseUrl5(baseUrl);
2647
+ const normalized = normalizeBaseUrl3(baseUrl);
3293
2648
  const current = get().providersOnCooldown;
3294
2649
  const updated = current.filter((entry) => entry.baseUrl !== normalized);
3295
2650
  void driver.setItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, updated);
@@ -3360,40 +2715,40 @@ var hydrateStoreFromDriver = async (store, driver) => {
3360
2715
  ]);
3361
2716
  const modelsFromAllProviders = Object.fromEntries(
3362
2717
  Object.entries(rawModels).map(([baseUrl, models]) => [
3363
- normalizeBaseUrl5(baseUrl),
2718
+ normalizeBaseUrl3(baseUrl),
3364
2719
  models
3365
2720
  ])
3366
2721
  );
3367
- const baseUrlsList = rawBaseUrls.map((url) => normalizeBaseUrl5(url));
2722
+ const baseUrlsList = rawBaseUrls.map((url) => normalizeBaseUrl3(url));
3368
2723
  const disabledProviders = rawDisabledProviders.map(
3369
- (url) => normalizeBaseUrl5(url)
2724
+ (url) => normalizeBaseUrl3(url)
3370
2725
  );
3371
2726
  const mintsFromAllProviders = Object.fromEntries(
3372
2727
  Object.entries(rawMints).map(([baseUrl, mints]) => [
3373
- normalizeBaseUrl5(baseUrl),
2728
+ normalizeBaseUrl3(baseUrl),
3374
2729
  mints.map((mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint)
3375
2730
  ])
3376
2731
  );
3377
2732
  const infoFromAllProviders = Object.fromEntries(
3378
2733
  Object.entries(rawInfo).map(([baseUrl, info]) => [
3379
- normalizeBaseUrl5(baseUrl),
2734
+ normalizeBaseUrl3(baseUrl),
3380
2735
  info
3381
2736
  ])
3382
2737
  );
3383
2738
  const lastModelsUpdate = Object.fromEntries(
3384
2739
  Object.entries(rawLastModelsUpdate).map(([baseUrl, timestamp]) => [
3385
- normalizeBaseUrl5(baseUrl),
2740
+ normalizeBaseUrl3(baseUrl),
3386
2741
  timestamp
3387
2742
  ])
3388
2743
  );
3389
2744
  const apiKeys = rawApiKeys.map((entry) => ({
3390
2745
  ...entry,
3391
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
2746
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
3392
2747
  balance: entry.balance ?? 0,
3393
2748
  lastUsed: entry.lastUsed ?? null
3394
2749
  }));
3395
2750
  const childKeys = rawChildKeys.map((entry) => ({
3396
- parentBaseUrl: normalizeBaseUrl5(entry.parentBaseUrl),
2751
+ parentBaseUrl: normalizeBaseUrl3(entry.parentBaseUrl),
3397
2752
  childKey: entry.childKey,
3398
2753
  balance: entry.balance ?? 0,
3399
2754
  balanceLimit: entry.balanceLimit,
@@ -3402,9 +2757,9 @@ var hydrateStoreFromDriver = async (store, driver) => {
3402
2757
  }));
3403
2758
  const xcashuTokens = Object.fromEntries(
3404
2759
  Object.entries(rawXcashuTokens).map(([baseUrl, tokens]) => [
3405
- normalizeBaseUrl5(baseUrl),
2760
+ normalizeBaseUrl3(baseUrl),
3406
2761
  tokens.map((entry) => ({
3407
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
2762
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
3408
2763
  token: entry.token,
3409
2764
  createdAt: entry.createdAt ?? Date.now(),
3410
2765
  tryCount: entry.tryCount ?? 0
@@ -3425,16 +2780,16 @@ var hydrateStoreFromDriver = async (store, driver) => {
3425
2780
  lastUsed: entry.lastUsed ?? null
3426
2781
  }));
3427
2782
  const failedProviders = rawFailedProviders.map(
3428
- (url) => normalizeBaseUrl5(url)
2783
+ (url) => normalizeBaseUrl3(url)
3429
2784
  );
3430
2785
  const lastFailed = Object.fromEntries(
3431
2786
  Object.entries(rawLastFailed).map(([baseUrl, timestamp]) => [
3432
- normalizeBaseUrl5(baseUrl),
2787
+ normalizeBaseUrl3(baseUrl),
3433
2788
  timestamp
3434
2789
  ])
3435
2790
  );
3436
2791
  const providersOnCooldown = rawProvidersOnCooldown.map((entry) => ({
3437
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
2792
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
3438
2793
  timestamp: entry.timestamp
3439
2794
  }));
3440
2795
  store.setState({
@@ -3476,31 +2831,13 @@ var isBrowser2 = () => {
3476
2831
  return false;
3477
2832
  }
3478
2833
  };
3479
- var isNode = () => {
3480
- try {
3481
- return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
3482
- } catch {
3483
- return false;
3484
- }
3485
- };
3486
2834
  var defaultDriver = null;
3487
- var isBun3 = () => {
3488
- return typeof process.versions.bun !== "undefined";
3489
- };
3490
2835
  var getDefaultSdkDriver = () => {
3491
2836
  if (defaultDriver) return defaultDriver;
3492
2837
  if (isBrowser2()) {
3493
2838
  defaultDriver = localStorageDriver;
3494
2839
  return defaultDriver;
3495
2840
  }
3496
- if (isBun3()) {
3497
- defaultDriver = createMemoryDriver();
3498
- return defaultDriver;
3499
- }
3500
- if (isNode()) {
3501
- defaultDriver = createSqliteDriver();
3502
- return defaultDriver;
3503
- }
3504
2841
  defaultDriver = createMemoryDriver();
3505
2842
  return defaultDriver;
3506
2843
  };
@@ -3521,49 +2858,209 @@ var getDefaultUsageTrackingDriver = () => {
3521
2858
  });
3522
2859
  return defaultUsageTrackingDriver;
3523
2860
  }
3524
- if (isBun3()) {
3525
- defaultUsageTrackingDriver = createBunSqliteUsageTrackingDriver();
3526
- return defaultUsageTrackingDriver;
3527
- }
3528
- if (isNode()) {
3529
- defaultUsageTrackingDriver = createSqliteUsageTrackingDriver({
3530
- legacyStorageDriver: storageDriver
3531
- });
3532
- return defaultUsageTrackingDriver;
3533
- }
3534
2861
  defaultUsageTrackingDriver = createMemoryUsageTrackingDriver();
3535
2862
  return defaultUsageTrackingDriver;
3536
2863
  };
2864
+
2865
+ // client/usage.ts
2866
+ var numOrUndef = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
2867
+ function extractCostBreakdown(costObj) {
2868
+ if (!costObj || typeof costObj !== "object") return {};
2869
+ return {
2870
+ baseMsats: numOrUndef(costObj.base_msats),
2871
+ inputMsats: numOrUndef(costObj.input_msats),
2872
+ outputMsats: numOrUndef(costObj.output_msats),
2873
+ totalMsats: numOrUndef(costObj.total_msats),
2874
+ totalUsd: numOrUndef(costObj.total_usd),
2875
+ cacheReadInputTokens: numOrUndef(costObj.cache_read_input_tokens),
2876
+ cacheCreationInputTokens: numOrUndef(costObj.cache_creation_input_tokens),
2877
+ cacheReadMsats: numOrUndef(costObj.cache_read_msats),
2878
+ cacheCreationMsats: numOrUndef(costObj.cache_creation_msats),
2879
+ remainingBalanceMsats: numOrUndef(costObj.remaining_balance_msats)
2880
+ };
2881
+ }
2882
+ function extractUsageFromResponseBody(body, fallbackSatsCost = 0) {
2883
+ if (!body || typeof body !== "object") return null;
2884
+ const usage = body.usage;
2885
+ if (!usage || typeof usage !== "object") return null;
2886
+ const promptTokens = Number(usage.prompt_tokens ?? 0);
2887
+ const completionTokens = Number(usage.completion_tokens ?? 0);
2888
+ const totalTokens = Number(usage.total_tokens ?? 0);
2889
+ const costValue = usage.cost;
2890
+ let cost = 0;
2891
+ let satsCost = fallbackSatsCost;
2892
+ let breakdown = {};
2893
+ if (typeof costValue === "number") {
2894
+ cost = costValue;
2895
+ } else if (costValue && typeof costValue === "object") {
2896
+ const costObj = costValue;
2897
+ const totalUsd = costObj.total_usd;
2898
+ const totalMsats = costObj.total_msats;
2899
+ cost = typeof totalUsd === "number" ? totalUsd : 0;
2900
+ if (typeof totalMsats === "number") {
2901
+ satsCost = totalMsats / 1e3;
2902
+ }
2903
+ breakdown = extractCostBreakdown(costObj);
2904
+ }
2905
+ const provider = typeof body.provider === "string" ? body.provider : void 0;
2906
+ if (promptTokens === 0 && completionTokens === 0 && totalTokens === 0 && cost === 0 && satsCost === 0) {
2907
+ return null;
2908
+ }
2909
+ return {
2910
+ promptTokens,
2911
+ completionTokens,
2912
+ totalTokens,
2913
+ cost,
2914
+ satsCost,
2915
+ provider,
2916
+ ...breakdown
2917
+ };
2918
+ }
2919
+ function extractResponseId(body) {
2920
+ if (!body || typeof body !== "object") return void 0;
2921
+ const id = body.id;
2922
+ if (typeof id !== "string") return void 0;
2923
+ const trimmed = id.trim();
2924
+ return trimmed.length > 0 ? trimmed : void 0;
2925
+ }
2926
+ function extractUsageFromSSEJson(parsed, fallbackSatsCost = 0) {
2927
+ if (!parsed || typeof parsed !== "object") {
2928
+ return null;
2929
+ }
2930
+ const provider = typeof parsed.provider === "string" ? parsed.provider : void 0;
2931
+ if (!parsed.usage && parsed.cost && typeof parsed.cost === "object") {
2932
+ const costObj = parsed.cost;
2933
+ const msats2 = costObj.total_msats ?? 0;
2934
+ const cost2 = costObj.total_usd ?? 0;
2935
+ if (msats2 === 0 && cost2 === 0) return null;
2936
+ return {
2937
+ promptTokens: Number(costObj.input_tokens ?? 0),
2938
+ completionTokens: Number(costObj.output_tokens ?? 0),
2939
+ totalTokens: Number((costObj.input_tokens ?? 0) + (costObj.output_tokens ?? 0)),
2940
+ cost: Number(cost2),
2941
+ satsCost: msats2 > 0 ? msats2 / 1e3 : fallbackSatsCost,
2942
+ provider,
2943
+ ...extractCostBreakdown(costObj)
2944
+ };
2945
+ }
2946
+ if (!parsed.usage) {
2947
+ return null;
2948
+ }
2949
+ const usage = parsed.usage;
2950
+ const usageCost = usage.cost;
2951
+ let cost = 0;
2952
+ let msats = 0;
2953
+ let breakdown = {};
2954
+ if (typeof usageCost === "number") {
2955
+ cost = usageCost;
2956
+ } else if (usageCost && typeof usageCost === "object") {
2957
+ cost = usageCost.total_usd ?? 0;
2958
+ msats = usageCost.total_msats ?? 0;
2959
+ breakdown = extractCostBreakdown(usageCost);
2960
+ }
2961
+ const routstrCost = parsed.metadata?.routstr?.cost;
2962
+ if (routstrCost && typeof routstrCost === "object") {
2963
+ breakdown = { ...extractCostBreakdown(routstrCost), ...breakdown };
2964
+ }
2965
+ if (cost === 0) {
2966
+ cost = parsed.metadata?.routstr?.cost?.total_usd ?? 0;
2967
+ }
2968
+ if (msats === 0) {
2969
+ msats = parsed.metadata?.routstr?.cost?.total_msats ?? (typeof usage.cost_sats === "number" ? usage.cost_sats * 1e3 : 0);
2970
+ }
2971
+ const promptTokens = Number(usage.prompt_tokens ?? usage.input_tokens ?? 0);
2972
+ const completionTokens = Number(usage.completion_tokens ?? usage.output_tokens ?? 0);
2973
+ const totalTokens = Number(usage.total_tokens ?? promptTokens + completionTokens);
2974
+ const result = {
2975
+ promptTokens,
2976
+ completionTokens,
2977
+ totalTokens,
2978
+ cost: Number(cost ?? 0),
2979
+ satsCost: msats > 0 ? msats / 1e3 : fallbackSatsCost,
2980
+ provider,
2981
+ ...breakdown
2982
+ };
2983
+ if (result.promptTokens === 0 && result.completionTokens === 0 && result.totalTokens === 0 && result.cost === 0 && result.satsCost === 0) {
2984
+ return null;
2985
+ }
2986
+ return result;
2987
+ }
2988
+ function toUsageStats(usage) {
2989
+ if (!usage) return void 0;
2990
+ return {
2991
+ total_tokens: usage.totalTokens,
2992
+ prompt_tokens: usage.promptTokens,
2993
+ completion_tokens: usage.completionTokens,
2994
+ cost: usage.cost,
2995
+ sats_cost: usage.satsCost
2996
+ };
2997
+ }
3537
2998
  function mergeUsage(previous, next) {
3538
2999
  if (!previous) return next;
3000
+ const pickNum = (n, p) => typeof n === "number" && n > 0 ? n : p ?? n;
3539
3001
  return {
3540
3002
  promptTokens: next.promptTokens > 0 ? next.promptTokens : previous.promptTokens,
3541
3003
  completionTokens: next.completionTokens > 0 ? next.completionTokens : previous.completionTokens,
3542
3004
  totalTokens: next.totalTokens > 0 ? next.totalTokens : previous.totalTokens,
3543
3005
  cost: next.cost > 0 ? next.cost : previous.cost,
3544
- satsCost: next.satsCost > 0 ? next.satsCost : previous.satsCost
3545
- };
3546
- }
3547
- function hasUsageChanged(previous, next) {
3006
+ satsCost: next.satsCost > 0 ? next.satsCost : previous.satsCost,
3007
+ provider: next.provider ?? previous.provider,
3008
+ baseMsats: pickNum(next.baseMsats, previous.baseMsats),
3009
+ inputMsats: pickNum(next.inputMsats, previous.inputMsats),
3010
+ outputMsats: pickNum(next.outputMsats, previous.outputMsats),
3011
+ totalMsats: pickNum(next.totalMsats, previous.totalMsats),
3012
+ totalUsd: pickNum(next.totalUsd, previous.totalUsd),
3013
+ cacheReadInputTokens: pickNum(
3014
+ next.cacheReadInputTokens,
3015
+ previous.cacheReadInputTokens
3016
+ ),
3017
+ cacheCreationInputTokens: pickNum(
3018
+ next.cacheCreationInputTokens,
3019
+ previous.cacheCreationInputTokens
3020
+ ),
3021
+ cacheReadMsats: pickNum(next.cacheReadMsats, previous.cacheReadMsats),
3022
+ cacheCreationMsats: pickNum(
3023
+ next.cacheCreationMsats,
3024
+ previous.cacheCreationMsats
3025
+ ),
3026
+ remainingBalanceMsats: pickNum(
3027
+ next.remainingBalanceMsats,
3028
+ previous.remainingBalanceMsats
3029
+ )
3030
+ };
3031
+ }
3032
+ function hasUsageChanged(previous, next) {
3548
3033
  if (!previous) return true;
3549
- return previous.promptTokens !== next.promptTokens || previous.completionTokens !== next.completionTokens || previous.totalTokens !== next.totalTokens || previous.cost !== next.cost || previous.satsCost !== next.satsCost;
3034
+ 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;
3035
+ }
3036
+ function isInspectionComplete(responseIdCaptured, usage) {
3037
+ return responseIdCaptured && !!usage && usage.totalTokens > 0 && typeof usage.totalMsats === "number" && !!usage.provider;
3550
3038
  }
3551
- async function inspectSSEWebStream(stream, onUsage, onResponseId) {
3039
+ async function inspectSSEWebStream(stream, onUsage, onResponseId, options) {
3552
3040
  const reader = stream.getReader();
3553
3041
  const decoder = new TextDecoder("utf-8");
3554
3042
  let buffer = "";
3555
3043
  let capturedUsage = null;
3556
3044
  let capturedResponseId;
3557
3045
  let responseIdCaptured = false;
3046
+ let rawChunkSequence = 0;
3558
3047
  const inspectDataPayload = (jsonText) => {
3559
- if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
3048
+ const trimmed = jsonText.trim();
3049
+ if (!trimmed || trimmed === "[DONE]") {
3050
+ if (trimmed === "[DONE]") console.log("[routstr:sse] [DONE]");
3051
+ return;
3052
+ }
3053
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
3054
+ console.log("[routstr:sse] non-JSON payload:", trimmed.slice(0, 200));
3560
3055
  return;
3561
3056
  }
3562
- const trimmed = jsonText.trim();
3563
- if (!trimmed || trimmed === "[DONE]") return;
3564
- if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return;
3565
3057
  try {
3566
3058
  const data = JSON.parse(trimmed);
3059
+ console.log("[routstr:sse] chunk:", JSON.stringify(data));
3060
+ if (isInspectionComplete(responseIdCaptured, capturedUsage)) {
3061
+ console.log("[routstr:sse] (inspection already complete, skipping)");
3062
+ return;
3063
+ }
3567
3064
  if (!responseIdCaptured) {
3568
3065
  const responseId = data?.id;
3569
3066
  if (typeof responseId === "string" && responseId.trim().length > 0) {
@@ -3574,19 +3071,21 @@ async function inspectSSEWebStream(stream, onUsage, onResponseId) {
3574
3071
  }
3575
3072
  const usage = extractUsageFromSSEJson(data);
3576
3073
  if (usage) {
3074
+ console.log("[routstr:sse] \u2192 usage detected:", usage);
3577
3075
  const merged = mergeUsage(capturedUsage, usage);
3578
3076
  if (hasUsageChanged(capturedUsage, merged)) {
3579
3077
  capturedUsage = merged;
3078
+ console.log("[routstr:sse] \u2192 merged (changed):", merged);
3580
3079
  onUsage(merged);
3080
+ } else {
3081
+ console.log("[routstr:sse] \u2192 merged (no change)");
3581
3082
  }
3582
3083
  }
3583
3084
  } catch {
3085
+ console.log("[routstr:sse] failed to parse payload:", trimmed.slice(0, 200));
3584
3086
  }
3585
3087
  };
3586
3088
  const inspectEventBlock = (eventBlock) => {
3587
- if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
3588
- return;
3589
- }
3590
3089
  const lines = eventBlock.split(/\r?\n/);
3591
3090
  const dataParts = [];
3592
3091
  for (const line of lines) {
@@ -3615,7 +3114,9 @@ async function inspectSSEWebStream(stream, onUsage, onResponseId) {
3615
3114
  const { value, done } = await reader.read();
3616
3115
  if (done) break;
3617
3116
  if (value && value.byteLength > 0) {
3618
- buffer += decoder.decode(value, { stream: true });
3117
+ const text = decoder.decode(value, { stream: true });
3118
+ void options?.onRawChunk?.(value, rawChunkSequence++, text);
3119
+ buffer += text;
3619
3120
  drainBufferedEvents();
3620
3121
  }
3621
3122
  }
@@ -3644,14 +3145,22 @@ function createSSEParserTransform(onUsage, onResponseId) {
3644
3145
  let capturedUsage = null;
3645
3146
  let responseIdCaptured = false;
3646
3147
  const inspectDataPayload = (jsonText) => {
3647
- if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
3148
+ const trimmed = jsonText.trim();
3149
+ if (!trimmed || trimmed === "[DONE]") {
3150
+ if (trimmed === "[DONE]") console.log("[routstr:sse] [DONE]");
3151
+ return;
3152
+ }
3153
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
3154
+ console.log("[routstr:sse] non-JSON payload:", trimmed.slice(0, 200));
3648
3155
  return;
3649
3156
  }
3650
- const trimmed = jsonText.trim();
3651
- if (!trimmed || trimmed === "[DONE]") return;
3652
- if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return;
3653
3157
  try {
3654
3158
  const data = JSON.parse(trimmed);
3159
+ console.log("[routstr:sse] chunk:", JSON.stringify(data));
3160
+ if (isInspectionComplete(responseIdCaptured, capturedUsage)) {
3161
+ console.log("[routstr:sse] (inspection already complete, skipping)");
3162
+ return;
3163
+ }
3655
3164
  if (!responseIdCaptured) {
3656
3165
  const responseId = data?.id;
3657
3166
  if (typeof responseId === "string" && responseId.trim().length > 0) {
@@ -3661,19 +3170,21 @@ function createSSEParserTransform(onUsage, onResponseId) {
3661
3170
  }
3662
3171
  const usage = extractUsageFromSSEJson(data);
3663
3172
  if (usage) {
3173
+ console.log("[routstr:sse] \u2192 usage detected:", usage);
3664
3174
  const mergedUsage = mergeUsage(capturedUsage, usage);
3665
3175
  if (hasUsageChanged(capturedUsage, mergedUsage)) {
3666
3176
  capturedUsage = mergedUsage;
3177
+ console.log("[routstr:sse] \u2192 merged (changed):", mergedUsage);
3667
3178
  onUsage(mergedUsage);
3179
+ } else {
3180
+ console.log("[routstr:sse] \u2192 merged (no change)");
3668
3181
  }
3669
3182
  }
3670
3183
  } catch {
3184
+ console.log("[routstr:sse] failed to parse payload:", trimmed.slice(0, 200));
3671
3185
  }
3672
3186
  };
3673
3187
  const inspectEventBlock = (eventBlock) => {
3674
- if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
3675
- return;
3676
- }
3677
3188
  const lines = eventBlock.split(/\r?\n/);
3678
3189
  const dataParts = [];
3679
3190
  for (const line of lines) {
@@ -3746,11 +3257,11 @@ var RoutstrClient = class {
3746
3257
  this.balanceManager,
3747
3258
  this.logger
3748
3259
  );
3749
- this.streamProcessor = new StreamProcessor();
3750
3260
  this.alertLevel = alertLevel;
3751
3261
  this.mode = mode;
3752
3262
  this.usageTrackingDriver = options.usageTrackingDriver;
3753
3263
  this.sdkStore = options.sdkStore;
3264
+ this.requestResponseLogSink = options.requestResponseLogSink;
3754
3265
  this.providerManager = options.providerManager ?? new ProviderManager(providerRegistry, this.sdkStore, this.logger);
3755
3266
  }
3756
3267
  walletAdapter;
@@ -3758,7 +3269,6 @@ var RoutstrClient = class {
3758
3269
  providerRegistry;
3759
3270
  cashuSpender;
3760
3271
  balanceManager;
3761
- streamProcessor;
3762
3272
  providerManager;
3763
3273
  alertLevel;
3764
3274
  mode;
@@ -3766,6 +3276,7 @@ var RoutstrClient = class {
3766
3276
  usageTrackingDriver;
3767
3277
  sdkStore;
3768
3278
  logger;
3279
+ requestResponseLogSink;
3769
3280
  /**
3770
3281
  * Get the current client mode
3771
3282
  */
@@ -3956,6 +3467,7 @@ var RoutstrClient = class {
3956
3467
  let usagePromise = Promise.resolve({});
3957
3468
  if (contentType.includes("text/event-stream") && response.body) {
3958
3469
  const [clientStream, inspectStream] = response.body.tee();
3470
+ const requestResponseLogId = response.requestResponseLogId;
3959
3471
  processedResponse = new Response(clientStream, {
3960
3472
  status: response.status,
3961
3473
  statusText: response.statusText,
@@ -3963,6 +3475,7 @@ var RoutstrClient = class {
3963
3475
  });
3964
3476
  processedResponse.baseUrl = response.baseUrl;
3965
3477
  processedResponse.token = response.token;
3478
+ processedResponse.requestResponseLogId = requestResponseLogId;
3966
3479
  usagePromise = inspectSSEWebStream(
3967
3480
  inspectStream,
3968
3481
  (usage) => {
@@ -3972,8 +3485,23 @@ var RoutstrClient = class {
3972
3485
  (responseId) => {
3973
3486
  capturedResponseId = responseId;
3974
3487
  processedResponse.requestId = responseId;
3488
+ },
3489
+ {
3490
+ onRawChunk: (_chunk, sequence, text) => {
3491
+ void this.requestResponseLogSink?.logResponseChunk?.(
3492
+ requestResponseLogId,
3493
+ sequence,
3494
+ text
3495
+ );
3496
+ }
3975
3497
  }
3976
- );
3498
+ ).then(async (result) => {
3499
+ await this.requestResponseLogSink?.logResponseEnd?.(requestResponseLogId);
3500
+ return result;
3501
+ }).catch(async (error) => {
3502
+ await this.requestResponseLogSink?.logResponseError?.(requestResponseLogId, error);
3503
+ throw error;
3504
+ });
3977
3505
  processedResponse.usagePromise = usagePromise;
3978
3506
  }
3979
3507
  return {
@@ -4000,153 +3528,6 @@ var RoutstrClient = class {
4000
3528
  }
4001
3529
  return void 0;
4002
3530
  }
4003
- /**
4004
- * Fetch AI response with streaming
4005
- */
4006
- async fetchAIResponse(options, callbacks) {
4007
- const {
4008
- messageHistory,
4009
- selectedModel,
4010
- baseUrl,
4011
- mintUrl,
4012
- balance,
4013
- transactionHistory,
4014
- maxTokens,
4015
- headers
4016
- } = options;
4017
- const apiMessages = await this._convertMessages(messageHistory);
4018
- const requiredSats = this.providerManager.getRequiredSatsForModel(
4019
- selectedModel,
4020
- apiMessages,
4021
- maxTokens
4022
- );
4023
- try {
4024
- await this._checkBalance();
4025
- callbacks.onPaymentProcessing?.(true);
4026
- const spendResult = await this._spendToken({
4027
- mintUrl,
4028
- amount: requiredSats,
4029
- baseUrl
4030
- });
4031
- let token = spendResult.token;
4032
- let tokenBalance = spendResult.tokenBalance;
4033
- let tokenBalanceUnit = spendResult.tokenBalanceUnit;
4034
- let tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
4035
- let initialTokenBalanceUnknown = spendResult.tokenBalanceUnknown;
4036
- callbacks.onTokenCreated?.(this._getPendingCashuTokenAmount());
4037
- const baseHeaders = this._buildBaseHeaders(headers);
4038
- const requestHeaders = this._withAuthHeader(baseHeaders, token);
4039
- const providerInfo = await this.providerRegistry.getProviderInfo(baseUrl);
4040
- const providerVersion = providerInfo?.version ?? "";
4041
- let modelIdForRequest = selectedModel.id;
4042
- if (/^0\.1\./.test(providerVersion)) {
4043
- const newModel = await this.providerManager.getModelForProvider(
4044
- baseUrl,
4045
- selectedModel.id
4046
- );
4047
- modelIdForRequest = newModel?.id ?? selectedModel.id;
4048
- }
4049
- const body = {
4050
- model: modelIdForRequest,
4051
- messages: apiMessages,
4052
- stream: true
4053
- };
4054
- if (maxTokens !== void 0) {
4055
- body.max_tokens = maxTokens;
4056
- }
4057
- if (selectedModel?.name?.startsWith("OpenAI:")) {
4058
- body.tools = [{ type: "web_search" }];
4059
- }
4060
- const response = await this._makeRequest({
4061
- path: "/v1/chat/completions",
4062
- method: "POST",
4063
- body,
4064
- selectedModel,
4065
- baseUrl,
4066
- mintUrl,
4067
- token,
4068
- requiredSats,
4069
- maxTokens,
4070
- headers: requestHeaders,
4071
- baseHeaders
4072
- });
4073
- if (!response.body) {
4074
- throw new Error("Response body is not available");
4075
- }
4076
- if (response.status === 200) {
4077
- const baseUrlUsed = response.baseUrl || baseUrl;
4078
- const responseToken = response.token || token;
4079
- if (baseUrlUsed !== baseUrl || responseToken !== token) {
4080
- token = responseToken;
4081
- if (typeof response.initialTokenBalanceInSats === "number") {
4082
- tokenBalanceInSats = response.initialTokenBalanceInSats;
4083
- initialTokenBalanceUnknown = Boolean(
4084
- response.initialTokenBalanceUnknown
4085
- );
4086
- } else {
4087
- initialTokenBalanceUnknown = true;
4088
- }
4089
- }
4090
- const streamingResult = await this.streamProcessor.process(
4091
- response,
4092
- {
4093
- onContent: callbacks.onStreamingUpdate,
4094
- onThinking: callbacks.onThinkingUpdate
4095
- },
4096
- selectedModel.id
4097
- );
4098
- if (streamingResult.finish_reason === "content_filter") {
4099
- callbacks.onMessageAppend({
4100
- role: "assistant",
4101
- content: "Your request was denied due to content filtering."
4102
- });
4103
- } else if (streamingResult.content || streamingResult.images && streamingResult.images.length > 0) {
4104
- const message = await this._createAssistantMessage(streamingResult);
4105
- callbacks.onMessageAppend(message);
4106
- } else {
4107
- callbacks.onMessageAppend({
4108
- role: "system",
4109
- content: "The provider did not respond to this request."
4110
- });
4111
- }
4112
- callbacks.onStreamingUpdate("");
4113
- callbacks.onThinkingUpdate("");
4114
- const isApikeysEstimate = this.mode === "apikeys";
4115
- let satsSpent = await this._handlePostResponseBalanceUpdate({
4116
- token,
4117
- baseUrl: baseUrlUsed,
4118
- mintUrl,
4119
- initialTokenBalance: tokenBalanceInSats,
4120
- initialTokenBalanceUnknown,
4121
- fallbackSatsSpent: isApikeysEstimate ? this._getEstimatedCosts(selectedModel, streamingResult) : void 0,
4122
- response,
4123
- modelId: selectedModel.id,
4124
- usage: streamingResult.usage ? {
4125
- promptTokens: Number(streamingResult.usage.prompt_tokens ?? 0),
4126
- completionTokens: Number(
4127
- streamingResult.usage.completion_tokens ?? 0
4128
- ),
4129
- totalTokens: Number(streamingResult.usage.total_tokens ?? 0),
4130
- cost: Number(streamingResult.usage.cost ?? 0),
4131
- satsCost: Number(streamingResult.usage.sats_cost ?? 0)
4132
- } : void 0,
4133
- requestId: streamingResult.responseId
4134
- });
4135
- const estimatedCosts = this._getEstimatedCosts(
4136
- selectedModel,
4137
- streamingResult
4138
- );
4139
- const onLastMessageSatsUpdate = callbacks.onLastMessageSatsUpdate;
4140
- onLastMessageSatsUpdate?.(satsSpent, estimatedCosts);
4141
- } else {
4142
- throw new Error(`${response.status} ${response.statusText}`);
4143
- }
4144
- } catch (error) {
4145
- this._handleError(error, callbacks);
4146
- } finally {
4147
- callbacks.onPaymentProcessing?.(false);
4148
- }
4149
- }
4150
3531
  /**
4151
3532
  * Make the API request with failover support
4152
3533
  */
@@ -4154,16 +3535,30 @@ var RoutstrClient = class {
4154
3535
  const { path, method, body, baseUrl, token, headers } = params;
4155
3536
  try {
4156
3537
  const url = `${baseUrl.replace(/\/$/, "")}${path}`;
3538
+ const requestBodyText = body === void 0 || method === "GET" ? void 0 : JSON.stringify(body);
3539
+ const requestLogId = await this.requestResponseLogSink?.logRequest?.({
3540
+ method,
3541
+ url,
3542
+ path,
3543
+ baseUrl,
3544
+ headers,
3545
+ body,
3546
+ rawBody: requestBodyText
3547
+ });
4157
3548
  if (this.mode === "xcashu") this._log("DEBUG", "HEADERS,", headers);
4158
3549
  const response = await fetch(url, {
4159
3550
  method,
4160
3551
  headers,
4161
- body: body === void 0 || method === "GET" ? void 0 : JSON.stringify(body)
3552
+ body: requestBodyText
4162
3553
  });
4163
3554
  if (this.mode === "xcashu") this._log("DEBUG", "response,", response);
4164
3555
  response.baseUrl = baseUrl;
4165
3556
  response.token = token;
3557
+ response.requestResponseLogId = requestLogId;
3558
+ await this.requestResponseLogSink?.logResponseStart?.(requestLogId, response);
3559
+ const contentType = response.headers.get("content-type") || "";
4166
3560
  if (!response.ok) {
3561
+ void this.requestResponseLogSink?.logResponseBody?.(requestLogId, response.clone());
4167
3562
  const requestId = response.headers.get("x-routstr-request-id") || void 0;
4168
3563
  let bodyText;
4169
3564
  try {
@@ -4181,6 +3576,9 @@ var RoutstrClient = class {
4181
3576
  params.retryCount ?? 0
4182
3577
  );
4183
3578
  }
3579
+ if (!contentType.includes("text/event-stream")) {
3580
+ void this.requestResponseLogSink?.logResponseBody?.(requestLogId, response.clone());
3581
+ }
4184
3582
  return response;
4185
3583
  } catch (error) {
4186
3584
  if (isNetworkErrorMessage(error?.message || "")) {
@@ -4671,268 +4069,529 @@ var RoutstrClient = class {
4671
4069
  }
4672
4070
  }
4673
4071
  /**
4674
- * Convert messages for API format
4072
+ * Check wallet balance and throw if insufficient
4675
4073
  */
4676
- async _convertMessages(messages) {
4677
- return Promise.all(
4678
- messages.filter((m) => m.role !== "system").map(async (m) => ({
4679
- role: m.role,
4680
- content: typeof m.content === "string" ? m.content : m.content
4681
- }))
4682
- );
4074
+ async _checkBalance() {
4075
+ const balances = await this.walletAdapter.getBalances();
4076
+ const totalBalance = Object.values(balances).reduce((sum, v) => sum + v, 0);
4077
+ if (totalBalance <= 0) {
4078
+ throw new InsufficientBalanceError(1, 0);
4079
+ }
4683
4080
  }
4684
4081
  /**
4685
- * Create assistant message from streaming result
4082
+ * Spend a token using CashuSpender with standardized error handling
4686
4083
  */
4687
- async _createAssistantMessage(result) {
4688
- if (result.images && result.images.length > 0) {
4689
- const content = [];
4690
- if (result.content) {
4691
- content.push({
4692
- type: "text",
4693
- text: result.content,
4694
- thinking: result.thinking,
4695
- citations: result.citations,
4696
- annotations: result.annotations
4084
+ async _spendToken(params) {
4085
+ const { mintUrl, amount, baseUrl } = params;
4086
+ this._log(
4087
+ "DEBUG",
4088
+ `[RoutstrClient] _spendToken: mode=${this.mode}, amount=${amount}, baseUrl=${baseUrl}, mintUrl=${mintUrl}`
4089
+ );
4090
+ if (this.mode === "apikeys") {
4091
+ let parentApiKey = this.storageAdapter.getApiKey(baseUrl);
4092
+ if (!parentApiKey) {
4093
+ this._log(
4094
+ "DEBUG",
4095
+ `[RoutstrClient] _spendToken: No existing API key for ${baseUrl}, creating new one via Cashu`
4096
+ );
4097
+ const spendResult2 = await this.cashuSpender.spend({
4098
+ mintUrl,
4099
+ amount: amount * TOPUP_MARGIN,
4100
+ baseUrl: "",
4101
+ reuseToken: false
4697
4102
  });
4698
- }
4699
- for (const img of result.images) {
4700
- content.push({
4701
- type: "image_url",
4702
- image_url: {
4703
- url: img.image_url.url
4103
+ if (!spendResult2.token) {
4104
+ this._log(
4105
+ "ERROR",
4106
+ `[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error:`,
4107
+ spendResult2.error
4108
+ );
4109
+ throw new Error(
4110
+ `[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error: ${spendResult2.error}`
4111
+ );
4112
+ } else {
4113
+ this._log(
4114
+ "DEBUG",
4115
+ `[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult2.token}`
4116
+ );
4117
+ }
4118
+ this._log(
4119
+ "DEBUG",
4120
+ `[RoutstrClient] _spendToken: Created API key for ${baseUrl}, key preview: ${spendResult2.token}, balance: ${spendResult2.balance}`
4121
+ );
4122
+ try {
4123
+ this.storageAdapter.setApiKey(baseUrl, spendResult2.token);
4124
+ } catch (error) {
4125
+ if (error instanceof Error && error.message.includes("ApiKey already exists")) {
4126
+ const receiveResult = await this.cashuSpender.receiveToken(
4127
+ spendResult2.token
4128
+ );
4129
+ if (receiveResult.success) {
4130
+ this._log(
4131
+ "DEBUG",
4132
+ `[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${receiveResult.amount}`
4133
+ );
4134
+ } else {
4135
+ this._log(
4136
+ "DEBUG",
4137
+ `[RoutstrClient] _handleErrorResponse: Token restore failed: ${receiveResult.message}`
4138
+ );
4139
+ }
4140
+ this._log(
4141
+ "DEBUG",
4142
+ `[RoutstrClient] _spendToken: API key already exists for ${baseUrl}, using existing key`
4143
+ );
4144
+ } else {
4145
+ throw error;
4704
4146
  }
4705
- });
4147
+ }
4148
+ parentApiKey = this.storageAdapter.getApiKey(baseUrl);
4149
+ } else {
4150
+ this._log(
4151
+ "DEBUG",
4152
+ `[RoutstrClient] _spendToken: Using existing API key for ${baseUrl}, key preview: ${parentApiKey.key}`
4153
+ );
4706
4154
  }
4155
+ let tokenBalance = 0;
4156
+ let tokenBalanceUnit = "sat";
4157
+ let tokenBalanceUnknown = false;
4158
+ const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
4159
+ const distributionForBaseUrl = apiKeyDistribution.find(
4160
+ (d) => d.baseUrl === baseUrl
4161
+ );
4162
+ if (distributionForBaseUrl) {
4163
+ tokenBalance = distributionForBaseUrl.amount;
4164
+ }
4165
+ if (tokenBalance === 0 && parentApiKey) {
4166
+ try {
4167
+ const balanceInfo = await this.balanceManager.getTokenBalance(
4168
+ parentApiKey.key,
4169
+ baseUrl
4170
+ );
4171
+ tokenBalance = balanceInfo.amount;
4172
+ tokenBalanceUnit = balanceInfo.unit;
4173
+ tokenBalanceUnknown = Boolean(balanceInfo.balanceUnknown);
4174
+ } catch (e) {
4175
+ this._log("WARN", "Could not get initial API key balance:", e);
4176
+ }
4177
+ }
4178
+ this._log(
4179
+ "DEBUG",
4180
+ `[RoutstrClient] _spendToken: Returning token with balance=${tokenBalance} ${tokenBalanceUnit}`
4181
+ );
4707
4182
  return {
4708
- role: "assistant",
4709
- content
4183
+ token: parentApiKey?.key ?? "",
4184
+ tokenBalance,
4185
+ tokenBalanceUnit,
4186
+ tokenBalanceUnknown
4710
4187
  };
4711
4188
  }
4189
+ this._log(
4190
+ "DEBUG",
4191
+ `[RoutstrClient] _spendToken: Calling CashuSpender.spend for amount=${amount}, mintUrl=${mintUrl}, mode=${this.mode}`
4192
+ );
4193
+ const spendResult = await this.cashuSpender.spend({
4194
+ mintUrl,
4195
+ amount,
4196
+ baseUrl: "",
4197
+ reuseToken: false
4198
+ });
4199
+ if (!spendResult.token) {
4200
+ this._log(
4201
+ "ERROR",
4202
+ `[RoutstrClient] _spendToken: CashuSpender.spend failed, error:`,
4203
+ spendResult.error
4204
+ );
4205
+ } else {
4206
+ this._log(
4207
+ "DEBUG",
4208
+ `[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult.token}, balance: ${spendResult.balance} ${spendResult.unit ?? "sat"}`
4209
+ );
4210
+ this.storageAdapter.addXcashuToken(baseUrl, spendResult.token);
4211
+ }
4712
4212
  return {
4713
- role: "assistant",
4714
- content: result.content || ""
4213
+ token: spendResult.token,
4214
+ tokenBalance: spendResult.balance,
4215
+ tokenBalanceUnit: spendResult.unit ?? "sat",
4216
+ tokenBalanceUnknown: false
4217
+ };
4218
+ }
4219
+ /**
4220
+ * Build request headers with common defaults and dev mock controls
4221
+ */
4222
+ _buildBaseHeaders(additionalHeaders = {}, token) {
4223
+ const headers = {
4224
+ ...additionalHeaders,
4225
+ "Content-Type": "application/json"
4715
4226
  };
4227
+ return headers;
4716
4228
  }
4717
4229
  /**
4718
- * Calculate estimated costs from usage
4230
+ * Attach auth headers using the active client mode
4719
4231
  */
4720
- _getEstimatedCosts(selectedModel, streamingResult) {
4721
- let estimatedCosts = 0;
4722
- if (streamingResult.usage) {
4723
- const { completion_tokens, prompt_tokens } = streamingResult.usage;
4724
- if (completion_tokens !== void 0 && prompt_tokens !== void 0) {
4725
- estimatedCosts = (selectedModel.sats_pricing?.completion ?? 0) * completion_tokens + (selectedModel.sats_pricing?.prompt ?? 0) * prompt_tokens;
4232
+ _withAuthHeader(headers, token) {
4233
+ const nextHeaders = { ...headers };
4234
+ if (this.mode === "xcashu") {
4235
+ nextHeaders["X-Cashu"] = token;
4236
+ } else {
4237
+ nextHeaders["Authorization"] = `Bearer ${token}`;
4238
+ }
4239
+ return nextHeaders;
4240
+ }
4241
+ };
4242
+
4243
+ // client/StreamProcessor.ts
4244
+ var StreamProcessor = class {
4245
+ accumulatedContent = "";
4246
+ accumulatedThinking = "";
4247
+ accumulatedImages = [];
4248
+ isInThinking = false;
4249
+ isInContent = false;
4250
+ /**
4251
+ * Process a streaming response
4252
+ */
4253
+ async process(response, callbacks, modelId) {
4254
+ if (!response.body) {
4255
+ throw new Error("Response body is not available");
4256
+ }
4257
+ const reader = response.body.getReader();
4258
+ const decoder = new TextDecoder("utf-8");
4259
+ let buffer = "";
4260
+ this.accumulatedContent = "";
4261
+ this.accumulatedThinking = "";
4262
+ this.accumulatedImages = [];
4263
+ this.isInThinking = false;
4264
+ this.isInContent = false;
4265
+ let usage;
4266
+ let model;
4267
+ let finish_reason;
4268
+ let citations;
4269
+ let annotations;
4270
+ let responseId;
4271
+ try {
4272
+ while (true) {
4273
+ const { done, value } = await reader.read();
4274
+ if (done) {
4275
+ break;
4276
+ }
4277
+ const chunk = decoder.decode(value, { stream: true });
4278
+ buffer += chunk;
4279
+ const lines = buffer.split("\n");
4280
+ buffer = lines.pop() || "";
4281
+ for (const line of lines) {
4282
+ const parsed = this._parseLine(line);
4283
+ if (!parsed) continue;
4284
+ if (parsed.content) {
4285
+ this._handleContent(parsed.content, callbacks, modelId);
4286
+ }
4287
+ if (parsed.reasoning) {
4288
+ this._handleThinking(parsed.reasoning, callbacks);
4289
+ }
4290
+ if (parsed.usage) {
4291
+ usage = parsed.usage;
4292
+ }
4293
+ if (parsed.model) {
4294
+ model = parsed.model;
4295
+ }
4296
+ if (parsed.finish_reason) {
4297
+ finish_reason = parsed.finish_reason;
4298
+ }
4299
+ if (parsed.responseId) {
4300
+ responseId = parsed.responseId;
4301
+ }
4302
+ if (parsed.citations) {
4303
+ citations = parsed.citations;
4304
+ }
4305
+ if (parsed.annotations) {
4306
+ annotations = parsed.annotations;
4307
+ }
4308
+ if (parsed.images) {
4309
+ this._mergeImages(parsed.images);
4310
+ }
4311
+ }
4726
4312
  }
4313
+ } finally {
4314
+ reader.releaseLock();
4727
4315
  }
4728
- return estimatedCosts;
4316
+ return {
4317
+ content: this.accumulatedContent,
4318
+ thinking: this.accumulatedThinking || void 0,
4319
+ images: this.accumulatedImages.length > 0 ? this.accumulatedImages : void 0,
4320
+ usage,
4321
+ model,
4322
+ responseId,
4323
+ finish_reason,
4324
+ citations,
4325
+ annotations
4326
+ };
4729
4327
  }
4730
4328
  /**
4731
- * Get pending API key amount
4329
+ * Parse a single SSE line
4732
4330
  */
4733
- _getPendingCashuTokenAmount() {
4734
- const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
4735
- return apiKeyDistribution.reduce((total, item) => total + item.amount, 0);
4331
+ _parseLine(line) {
4332
+ if (!line.trim()) return null;
4333
+ if (!line.startsWith("data: ")) {
4334
+ return null;
4335
+ }
4336
+ const jsonData = line.slice(6);
4337
+ if (jsonData === "[DONE]") {
4338
+ return null;
4339
+ }
4340
+ try {
4341
+ const parsed = JSON.parse(jsonData);
4342
+ const result = {};
4343
+ if (parsed.choices?.[0]?.delta?.content) {
4344
+ result.content = parsed.choices[0].delta.content;
4345
+ }
4346
+ if (parsed.choices?.[0]?.delta?.reasoning) {
4347
+ result.reasoning = parsed.choices[0].delta.reasoning;
4348
+ }
4349
+ const extractedUsage = extractUsageFromSSEJson(parsed);
4350
+ if (extractedUsage) {
4351
+ result.usage = toUsageStats(extractedUsage);
4352
+ } else if (parsed.usage) {
4353
+ result.usage = {
4354
+ total_tokens: parsed.usage.total_tokens ?? parsed.usage.input_tokens + parsed.usage.output_tokens,
4355
+ prompt_tokens: parsed.usage.prompt_tokens ?? parsed.usage.input_tokens,
4356
+ completion_tokens: parsed.usage.completion_tokens ?? parsed.usage.output_tokens
4357
+ };
4358
+ }
4359
+ if (parsed.id) {
4360
+ result.responseId = parsed.id;
4361
+ }
4362
+ if (parsed.model) {
4363
+ result.model = parsed.model;
4364
+ }
4365
+ if (parsed.citations) {
4366
+ result.citations = parsed.citations;
4367
+ }
4368
+ if (parsed.annotations) {
4369
+ result.annotations = parsed.annotations;
4370
+ }
4371
+ if (parsed.choices?.[0]?.finish_reason) {
4372
+ result.finish_reason = parsed.choices[0].finish_reason;
4373
+ }
4374
+ const images = parsed.choices?.[0]?.message?.images || parsed.choices?.[0]?.delta?.images;
4375
+ if (images && Array.isArray(images)) {
4376
+ result.images = images;
4377
+ }
4378
+ return result;
4379
+ } catch {
4380
+ return null;
4381
+ }
4736
4382
  }
4737
4383
  /**
4738
- * Handle errors and notify callbacks
4384
+ * Handle content delta with thinking support
4739
4385
  */
4740
- _handleError(error, callbacks) {
4741
- this._log("ERROR", "[RoutstrClient] _handleError: Error occurred", error);
4742
- if (error instanceof Error) {
4743
- const isStreamError = error.message.includes("Error in input stream") || error.message.includes("Load failed");
4744
- const modifiedErrorMsg = isStreamError ? "AI stream was cut off, turn on Keep Active or please try again" : error.message;
4745
- this._log(
4746
- "ERROR",
4747
- `[RoutstrClient] _handleError: Error type=${error.constructor.name}, message=${modifiedErrorMsg}, isStreamError=${isStreamError}`
4748
- );
4749
- callbacks.onMessageAppend({
4750
- role: "system",
4751
- content: "Uncaught Error: " + modifiedErrorMsg + (this.alertLevel === "max" ? " | " + error.stack : "")
4752
- });
4386
+ _handleContent(content, callbacks, modelId) {
4387
+ if (this.isInThinking && !this.isInContent) {
4388
+ this.accumulatedThinking += "</thinking>";
4389
+ callbacks.onThinking(this.accumulatedThinking);
4390
+ this.isInThinking = false;
4391
+ this.isInContent = true;
4392
+ }
4393
+ if (modelId) {
4394
+ this._extractThinkingFromContent(content, callbacks);
4753
4395
  } else {
4754
- callbacks.onMessageAppend({
4755
- role: "system",
4756
- content: "Unknown Error: Please tag Routstr on Nostr and/or retry."
4757
- });
4396
+ this.accumulatedContent += content;
4758
4397
  }
4398
+ callbacks.onContent(this.accumulatedContent);
4759
4399
  }
4760
4400
  /**
4761
- * Check wallet balance and throw if insufficient
4401
+ * Handle thinking/reasoning content
4762
4402
  */
4763
- async _checkBalance() {
4764
- const balances = await this.walletAdapter.getBalances();
4765
- const totalBalance = Object.values(balances).reduce((sum, v) => sum + v, 0);
4766
- if (totalBalance <= 0) {
4767
- throw new InsufficientBalanceError(1, 0);
4403
+ _handleThinking(reasoning, callbacks) {
4404
+ if (!this.isInThinking) {
4405
+ this.accumulatedThinking += "<thinking> ";
4406
+ this.isInThinking = true;
4768
4407
  }
4408
+ this.accumulatedThinking += reasoning;
4409
+ callbacks.onThinking(this.accumulatedThinking);
4769
4410
  }
4770
4411
  /**
4771
- * Spend a token using CashuSpender with standardized error handling
4412
+ * Extract thinking blocks from content (for models with inline thinking)
4772
4413
  */
4773
- async _spendToken(params) {
4774
- const { mintUrl, amount, baseUrl } = params;
4775
- this._log(
4776
- "DEBUG",
4777
- `[RoutstrClient] _spendToken: mode=${this.mode}, amount=${amount}, baseUrl=${baseUrl}, mintUrl=${mintUrl}`
4778
- );
4779
- if (this.mode === "apikeys") {
4780
- let parentApiKey = this.storageAdapter.getApiKey(baseUrl);
4781
- if (!parentApiKey) {
4782
- this._log(
4783
- "DEBUG",
4784
- `[RoutstrClient] _spendToken: No existing API key for ${baseUrl}, creating new one via Cashu`
4785
- );
4786
- const spendResult2 = await this.cashuSpender.spend({
4787
- mintUrl,
4788
- amount: amount * TOPUP_MARGIN,
4789
- baseUrl: "",
4790
- reuseToken: false
4791
- });
4792
- if (!spendResult2.token) {
4793
- this._log(
4794
- "ERROR",
4795
- `[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error:`,
4796
- spendResult2.error
4797
- );
4798
- throw new Error(
4799
- `[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error: ${spendResult2.error}`
4800
- );
4801
- } else {
4802
- this._log(
4803
- "DEBUG",
4804
- `[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult2.token}`
4805
- );
4806
- }
4807
- this._log(
4808
- "DEBUG",
4809
- `[RoutstrClient] _spendToken: Created API key for ${baseUrl}, key preview: ${spendResult2.token}, balance: ${spendResult2.balance}`
4810
- );
4811
- try {
4812
- this.storageAdapter.setApiKey(baseUrl, spendResult2.token);
4813
- } catch (error) {
4814
- if (error instanceof Error && error.message.includes("ApiKey already exists")) {
4815
- const receiveResult = await this.cashuSpender.receiveToken(
4816
- spendResult2.token
4817
- );
4818
- if (receiveResult.success) {
4819
- this._log(
4820
- "DEBUG",
4821
- `[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${receiveResult.amount}`
4822
- );
4823
- } else {
4824
- this._log(
4825
- "DEBUG",
4826
- `[RoutstrClient] _handleErrorResponse: Token restore failed: ${receiveResult.message}`
4827
- );
4828
- }
4829
- this._log(
4830
- "DEBUG",
4831
- `[RoutstrClient] _spendToken: API key already exists for ${baseUrl}, using existing key`
4832
- );
4833
- } else {
4834
- throw error;
4835
- }
4414
+ _extractThinkingFromContent(content, callbacks) {
4415
+ const parts = content.split(/(<thinking>|<\/thinking>)/);
4416
+ for (const part of parts) {
4417
+ if (part === "<thinking>") {
4418
+ this.isInThinking = true;
4419
+ if (!this.accumulatedThinking.includes("<thinking>")) {
4420
+ this.accumulatedThinking += "<thinking> ";
4836
4421
  }
4837
- parentApiKey = this.storageAdapter.getApiKey(baseUrl);
4422
+ } else if (part === "</thinking>") {
4423
+ this.isInThinking = false;
4424
+ this.accumulatedThinking += "</thinking>";
4425
+ } else if (this.isInThinking) {
4426
+ this.accumulatedThinking += part;
4838
4427
  } else {
4839
- this._log(
4840
- "DEBUG",
4841
- `[RoutstrClient] _spendToken: Using existing API key for ${baseUrl}, key preview: ${parentApiKey.key}`
4842
- );
4843
- }
4844
- let tokenBalance = 0;
4845
- let tokenBalanceUnit = "sat";
4846
- let tokenBalanceUnknown = false;
4847
- const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
4848
- const distributionForBaseUrl = apiKeyDistribution.find(
4849
- (d) => d.baseUrl === baseUrl
4850
- );
4851
- if (distributionForBaseUrl) {
4852
- tokenBalance = distributionForBaseUrl.amount;
4428
+ this.accumulatedContent += part;
4853
4429
  }
4854
- if (tokenBalance === 0 && parentApiKey) {
4855
- try {
4856
- const balanceInfo = await this.balanceManager.getTokenBalance(
4857
- parentApiKey.key,
4858
- baseUrl
4859
- );
4860
- tokenBalance = balanceInfo.amount;
4861
- tokenBalanceUnit = balanceInfo.unit;
4862
- tokenBalanceUnknown = Boolean(balanceInfo.balanceUnknown);
4863
- } catch (e) {
4864
- this._log("WARN", "Could not get initial API key balance:", e);
4430
+ }
4431
+ }
4432
+ /**
4433
+ * Merge images into accumulated array, avoiding duplicates
4434
+ */
4435
+ _mergeImages(newImages) {
4436
+ for (const img of newImages) {
4437
+ const newUrl = img.image_url?.url;
4438
+ const existingIndex = this.accumulatedImages.findIndex((existing) => {
4439
+ const existingUrl = existing.image_url?.url;
4440
+ if (newUrl && existingUrl) {
4441
+ return existingUrl === newUrl;
4865
4442
  }
4443
+ if (img.index !== void 0 && existing.index !== void 0) {
4444
+ return existing.index === img.index;
4445
+ }
4446
+ return false;
4447
+ });
4448
+ if (existingIndex === -1) {
4449
+ this.accumulatedImages.push(img);
4450
+ } else {
4451
+ this.accumulatedImages[existingIndex] = img;
4866
4452
  }
4867
- this._log(
4868
- "DEBUG",
4869
- `[RoutstrClient] _spendToken: Returning token with balance=${tokenBalance} ${tokenBalanceUnit}`
4870
- );
4871
- return {
4872
- token: parentApiKey?.key ?? "",
4873
- tokenBalance,
4874
- tokenBalanceUnit,
4875
- tokenBalanceUnknown
4876
- };
4877
4453
  }
4878
- this._log(
4879
- "DEBUG",
4880
- `[RoutstrClient] _spendToken: Calling CashuSpender.spend for amount=${amount}, mintUrl=${mintUrl}, mode=${this.mode}`
4881
- );
4882
- const spendResult = await this.cashuSpender.spend({
4454
+ }
4455
+ };
4456
+
4457
+ // client/fetchAIResponse.ts
4458
+ async function fetchAIResponse(options, callbacks, deps) {
4459
+ const {
4460
+ messageHistory,
4461
+ selectedModel,
4462
+ baseUrl,
4463
+ mintUrl,
4464
+ maxTokens,
4465
+ headers
4466
+ } = options;
4467
+ try {
4468
+ const apiMessages = await convertMessages(messageHistory);
4469
+ callbacks.onPaymentProcessing?.(true);
4470
+ callbacks.onTokenCreated?.(deps.getPendingCashuTokenAmount?.() ?? 0);
4471
+ const body = {
4472
+ model: selectedModel.id,
4473
+ messages: apiMessages,
4474
+ stream: true
4475
+ };
4476
+ if (maxTokens !== void 0) {
4477
+ body.max_tokens = maxTokens;
4478
+ }
4479
+ if (selectedModel?.name?.startsWith("OpenAI:")) {
4480
+ body.tools = [{ type: "web_search" }];
4481
+ }
4482
+ const response = await deps.client.routeRequest({
4483
+ path: "/v1/chat/completions",
4484
+ method: "POST",
4485
+ body,
4486
+ headers,
4487
+ baseUrl,
4883
4488
  mintUrl,
4884
- amount,
4885
- baseUrl: "",
4886
- reuseToken: false
4489
+ modelId: selectedModel.id
4887
4490
  });
4888
- if (!spendResult.token) {
4889
- this._log(
4890
- "ERROR",
4891
- `[RoutstrClient] _spendToken: CashuSpender.spend failed, error:`,
4892
- spendResult.error
4893
- );
4491
+ if (!response.body) {
4492
+ throw new Error("Response body is not available");
4493
+ }
4494
+ if (response.status !== 200) {
4495
+ throw new Error(`${response.status} ${response.statusText}`);
4496
+ }
4497
+ const streamProcessor = new StreamProcessor();
4498
+ const streamingResult = await streamProcessor.process(
4499
+ response,
4500
+ {
4501
+ onContent: callbacks.onStreamingUpdate,
4502
+ onThinking: callbacks.onThinkingUpdate
4503
+ },
4504
+ selectedModel.id
4505
+ );
4506
+ if (streamingResult.finish_reason === "content_filter") {
4507
+ callbacks.onMessageAppend({
4508
+ role: "assistant",
4509
+ content: "Your request was denied due to content filtering."
4510
+ });
4511
+ } else if (streamingResult.content || streamingResult.images && streamingResult.images.length > 0) {
4512
+ const message = await createAssistantMessage(streamingResult);
4513
+ callbacks.onMessageAppend(message);
4894
4514
  } else {
4895
- this._log(
4896
- "DEBUG",
4897
- `[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult.token}, balance: ${spendResult.balance} ${spendResult.unit ?? "sat"}`
4898
- );
4899
- this.storageAdapter.addXcashuToken(baseUrl, spendResult.token);
4515
+ callbacks.onMessageAppend({
4516
+ role: "system",
4517
+ content: "The provider did not respond to this request."
4518
+ });
4900
4519
  }
4901
- return {
4902
- token: spendResult.token,
4903
- tokenBalance: spendResult.balance,
4904
- tokenBalanceUnit: spendResult.unit ?? "sat",
4905
- tokenBalanceUnknown: false
4906
- };
4520
+ callbacks.onStreamingUpdate("");
4521
+ callbacks.onThinkingUpdate("");
4522
+ } catch (error) {
4523
+ handleError(error, callbacks, deps.alertLevel, deps.logger);
4524
+ } finally {
4525
+ callbacks.onPaymentProcessing?.(false);
4907
4526
  }
4908
- /**
4909
- * Build request headers with common defaults and dev mock controls
4910
- */
4911
- _buildBaseHeaders(additionalHeaders = {}, token) {
4912
- const headers = {
4913
- ...additionalHeaders,
4914
- "Content-Type": "application/json"
4527
+ }
4528
+ async function convertMessages(messages) {
4529
+ return Promise.all(
4530
+ messages.filter((m) => m.role !== "system").map(async (m) => ({
4531
+ role: m.role,
4532
+ content: typeof m.content === "string" ? m.content : m.content
4533
+ }))
4534
+ );
4535
+ }
4536
+ async function createAssistantMessage(result) {
4537
+ if (result.images && result.images.length > 0) {
4538
+ const content = [];
4539
+ if (result.content) {
4540
+ content.push({
4541
+ type: "text",
4542
+ text: result.content,
4543
+ thinking: result.thinking,
4544
+ citations: result.citations,
4545
+ annotations: result.annotations
4546
+ });
4547
+ }
4548
+ for (const img of result.images) {
4549
+ content.push({
4550
+ type: "image_url",
4551
+ image_url: {
4552
+ url: img.image_url.url
4553
+ }
4554
+ });
4555
+ }
4556
+ return {
4557
+ role: "assistant",
4558
+ content
4915
4559
  };
4916
- return headers;
4917
4560
  }
4918
- /**
4919
- * Attach auth headers using the active client mode
4920
- */
4921
- _withAuthHeader(headers, token) {
4922
- const nextHeaders = { ...headers };
4923
- if (this.mode === "xcashu") {
4924
- nextHeaders["X-Cashu"] = token;
4925
- } else {
4926
- nextHeaders["Authorization"] = `Bearer ${token}`;
4927
- }
4928
- return nextHeaders;
4561
+ return {
4562
+ role: "assistant",
4563
+ content: result.content || ""
4564
+ };
4565
+ }
4566
+ function handleError(error, callbacks, alertLevel, logger) {
4567
+ logger.error("[fetchAIResponse] Error occurred", error);
4568
+ if (error instanceof Error) {
4569
+ const isStreamError = error.message.includes("Error in input stream") || error.message.includes("Load failed");
4570
+ const modifiedErrorMsg = isStreamError ? "AI stream was cut off, turn on Keep Active or please try again" : error.message;
4571
+ logger.error(
4572
+ `[fetchAIResponse] Error type=${error.constructor.name}, message=${modifiedErrorMsg}, isStreamError=${isStreamError}`
4573
+ );
4574
+ callbacks.onMessageAppend({
4575
+ role: "system",
4576
+ content: "Uncaught Error: " + modifiedErrorMsg + (alertLevel === "max" ? " | " + error.stack : "")
4577
+ });
4578
+ } else {
4579
+ callbacks.onMessageAppend({
4580
+ role: "system",
4581
+ content: "Unknown Error: Please tag Routstr on Nostr and/or retry."
4582
+ });
4929
4583
  }
4930
- };
4584
+ }
4931
4585
 
4932
4586
  exports.ProviderManager = ProviderManager;
4933
4587
  exports.RoutstrClient = RoutstrClient;
4934
4588
  exports.StreamProcessor = StreamProcessor;
4935
4589
  exports.createSSEParserTransform = createSSEParserTransform;
4590
+ exports.extractResponseId = extractResponseId;
4591
+ exports.extractUsageFromResponseBody = extractUsageFromResponseBody;
4592
+ exports.extractUsageFromSSEJson = extractUsageFromSSEJson;
4593
+ exports.fetchAIResponse = fetchAIResponse;
4936
4594
  exports.inspectSSEWebStream = inspectSSEWebStream;
4595
+ exports.toUsageStats = toUsageStats;
4937
4596
  //# sourceMappingURL=index.js.map
4938
4597
  //# sourceMappingURL=index.js.map