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