@routstr/sdk 0.3.9 → 0.3.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/browser.d.mts +12 -0
  2. package/dist/browser.d.ts +12 -0
  3. package/dist/browser.js +6278 -0
  4. package/dist/browser.js.map +1 -0
  5. package/dist/browser.mjs +6230 -0
  6. package/dist/browser.mjs.map +1 -0
  7. package/dist/bun.d.mts +29 -0
  8. package/dist/bun.d.ts +29 -0
  9. package/dist/bun.js +6586 -0
  10. package/dist/bun.js.map +1 -0
  11. package/dist/bun.mjs +6532 -0
  12. package/dist/bun.mjs.map +1 -0
  13. package/dist/bunSqlite-BMTseLIz.d.ts +18 -0
  14. package/dist/bunSqlite-D6AreVE2.d.mts +18 -0
  15. package/dist/client/index.d.mts +63 -41
  16. package/dist/client/index.d.ts +63 -41
  17. package/dist/client/index.js +801 -1291
  18. package/dist/client/index.js.map +1 -1
  19. package/dist/client/index.mjs +801 -1292
  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 +28 -21
  24. package/dist/discovery/index.js.map +1 -1
  25. package/dist/discovery/index.mjs +28 -21
  26. package/dist/discovery/index.mjs.map +1 -1
  27. package/dist/index.d.mts +4 -4
  28. package/dist/index.d.ts +4 -4
  29. package/dist/index.js +1045 -1564
  30. package/dist/index.js.map +1 -1
  31. package/dist/index.mjs +1045 -1561
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/node.d.mts +22 -0
  34. package/dist/node.d.ts +22 -0
  35. package/dist/node.js +6651 -0
  36. package/dist/node.js.map +1 -0
  37. package/dist/node.mjs +6599 -0
  38. package/dist/node.mjs.map +1 -0
  39. package/dist/storage/bun.d.mts +16 -0
  40. package/dist/storage/bun.d.ts +16 -0
  41. package/dist/storage/bun.js +1801 -0
  42. package/dist/storage/bun.js.map +1 -0
  43. package/dist/storage/bun.mjs +1777 -0
  44. package/dist/storage/bun.mjs.map +1 -0
  45. package/dist/storage/index.d.mts +4 -30
  46. package/dist/storage/index.d.ts +4 -30
  47. package/dist/storage/index.js +139 -650
  48. package/dist/storage/index.js.map +1 -1
  49. package/dist/storage/index.mjs +140 -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 +1864 -0
  54. package/dist/storage/node.js.map +1 -0
  55. package/dist/storage/node.mjs +1842 -0
  56. package/dist/storage/node.mjs.map +1 -0
  57. package/dist/{store-C6dfj1cc.d.mts → store-BiuM2V9N.d.mts} +14 -0
  58. package/dist/{store-58VcEUoA.d.ts → store-C8MZlfuz.d.ts} +14 -0
  59. package/package.json +26 -1
@@ -1406,322 +1406,6 @@ var BalanceManager = class _BalanceManager {
1406
1406
  }
1407
1407
  };
1408
1408
 
1409
- // client/usage.ts
1410
- function extractUsageFromResponseBody(body, fallbackSatsCost = 0) {
1411
- if (!body || typeof body !== "object") return null;
1412
- const usage = body.usage;
1413
- if (!usage || typeof usage !== "object") return null;
1414
- const promptTokens = Number(usage.prompt_tokens ?? 0);
1415
- const completionTokens = Number(usage.completion_tokens ?? 0);
1416
- const totalTokens = Number(usage.total_tokens ?? 0);
1417
- const costValue = usage.cost;
1418
- let cost = 0;
1419
- let satsCost = fallbackSatsCost;
1420
- if (typeof costValue === "number") {
1421
- cost = costValue;
1422
- } else if (costValue && typeof costValue === "object") {
1423
- const costObj = costValue;
1424
- const totalUsd = costObj.total_usd;
1425
- const totalMsats = costObj.total_msats;
1426
- cost = typeof totalUsd === "number" ? totalUsd : 0;
1427
- if (typeof totalMsats === "number") {
1428
- satsCost = totalMsats / 1e3;
1429
- }
1430
- }
1431
- if (promptTokens === 0 && completionTokens === 0 && totalTokens === 0 && cost === 0 && satsCost === 0) {
1432
- return null;
1433
- }
1434
- return {
1435
- promptTokens,
1436
- completionTokens,
1437
- totalTokens,
1438
- cost,
1439
- satsCost
1440
- };
1441
- }
1442
- function extractResponseId(body) {
1443
- if (!body || typeof body !== "object") return void 0;
1444
- const id = body.id;
1445
- if (typeof id !== "string") return void 0;
1446
- const trimmed = id.trim();
1447
- return trimmed.length > 0 ? trimmed : void 0;
1448
- }
1449
- function extractUsageFromSSEJson(parsed, fallbackSatsCost = 0) {
1450
- if (!parsed || typeof parsed !== "object") {
1451
- return null;
1452
- }
1453
- if (!parsed.usage && parsed.cost && typeof parsed.cost === "object") {
1454
- const costObj = parsed.cost;
1455
- const msats2 = costObj.total_msats ?? 0;
1456
- const cost2 = costObj.total_usd ?? 0;
1457
- if (msats2 === 0 && cost2 === 0) return null;
1458
- return {
1459
- promptTokens: Number(costObj.input_tokens ?? 0),
1460
- completionTokens: Number(costObj.output_tokens ?? 0),
1461
- totalTokens: Number((costObj.input_tokens ?? 0) + (costObj.output_tokens ?? 0)),
1462
- cost: Number(cost2),
1463
- satsCost: msats2 > 0 ? msats2 / 1e3 : fallbackSatsCost
1464
- };
1465
- }
1466
- if (!parsed.usage) {
1467
- return null;
1468
- }
1469
- const usage = parsed.usage;
1470
- const usageCost = usage.cost;
1471
- let cost = 0;
1472
- let msats = 0;
1473
- if (typeof usageCost === "number") {
1474
- cost = usageCost;
1475
- } else if (usageCost && typeof usageCost === "object") {
1476
- cost = usageCost.total_usd ?? 0;
1477
- msats = usageCost.total_msats ?? 0;
1478
- }
1479
- if (cost === 0) {
1480
- cost = parsed.metadata?.routstr?.cost?.total_usd ?? 0;
1481
- }
1482
- if (msats === 0) {
1483
- msats = parsed.metadata?.routstr?.cost?.total_msats ?? (typeof usage.cost_sats === "number" ? usage.cost_sats * 1e3 : 0);
1484
- }
1485
- const promptTokens = Number(usage.prompt_tokens ?? usage.input_tokens ?? 0);
1486
- const completionTokens = Number(usage.completion_tokens ?? usage.output_tokens ?? 0);
1487
- const totalTokens = Number(usage.total_tokens ?? promptTokens + completionTokens);
1488
- const result = {
1489
- promptTokens,
1490
- completionTokens,
1491
- totalTokens,
1492
- cost: Number(cost ?? 0),
1493
- satsCost: msats > 0 ? msats / 1e3 : fallbackSatsCost
1494
- };
1495
- if (result.promptTokens === 0 && result.completionTokens === 0 && result.totalTokens === 0 && result.cost === 0 && result.satsCost === 0) {
1496
- return null;
1497
- }
1498
- return result;
1499
- }
1500
- function toUsageStats(usage) {
1501
- if (!usage) return void 0;
1502
- return {
1503
- total_tokens: usage.totalTokens,
1504
- prompt_tokens: usage.promptTokens,
1505
- completion_tokens: usage.completionTokens,
1506
- cost: usage.cost,
1507
- sats_cost: usage.satsCost
1508
- };
1509
- }
1510
-
1511
- // client/StreamProcessor.ts
1512
- var StreamProcessor = class {
1513
- accumulatedContent = "";
1514
- accumulatedThinking = "";
1515
- accumulatedImages = [];
1516
- isInThinking = false;
1517
- isInContent = false;
1518
- /**
1519
- * Process a streaming response
1520
- */
1521
- async process(response, callbacks, modelId) {
1522
- if (!response.body) {
1523
- throw new Error("Response body is not available");
1524
- }
1525
- const reader = response.body.getReader();
1526
- const decoder = new TextDecoder("utf-8");
1527
- let buffer = "";
1528
- this.accumulatedContent = "";
1529
- this.accumulatedThinking = "";
1530
- this.accumulatedImages = [];
1531
- this.isInThinking = false;
1532
- this.isInContent = false;
1533
- let usage;
1534
- let model;
1535
- let finish_reason;
1536
- let citations;
1537
- let annotations;
1538
- let responseId;
1539
- try {
1540
- while (true) {
1541
- const { done, value } = await reader.read();
1542
- if (done) {
1543
- break;
1544
- }
1545
- const chunk = decoder.decode(value, { stream: true });
1546
- buffer += chunk;
1547
- const lines = buffer.split("\n");
1548
- buffer = lines.pop() || "";
1549
- for (const line of lines) {
1550
- const parsed = this._parseLine(line);
1551
- if (!parsed) continue;
1552
- if (parsed.content) {
1553
- this._handleContent(parsed.content, callbacks, modelId);
1554
- }
1555
- if (parsed.reasoning) {
1556
- this._handleThinking(parsed.reasoning, callbacks);
1557
- }
1558
- if (parsed.usage) {
1559
- usage = parsed.usage;
1560
- }
1561
- if (parsed.model) {
1562
- model = parsed.model;
1563
- }
1564
- if (parsed.finish_reason) {
1565
- finish_reason = parsed.finish_reason;
1566
- }
1567
- if (parsed.responseId) {
1568
- responseId = parsed.responseId;
1569
- }
1570
- if (parsed.citations) {
1571
- citations = parsed.citations;
1572
- }
1573
- if (parsed.annotations) {
1574
- annotations = parsed.annotations;
1575
- }
1576
- if (parsed.images) {
1577
- this._mergeImages(parsed.images);
1578
- }
1579
- }
1580
- }
1581
- } finally {
1582
- reader.releaseLock();
1583
- }
1584
- return {
1585
- content: this.accumulatedContent,
1586
- thinking: this.accumulatedThinking || void 0,
1587
- images: this.accumulatedImages.length > 0 ? this.accumulatedImages : void 0,
1588
- usage,
1589
- model,
1590
- responseId,
1591
- finish_reason,
1592
- citations,
1593
- annotations
1594
- };
1595
- }
1596
- /**
1597
- * Parse a single SSE line
1598
- */
1599
- _parseLine(line) {
1600
- if (!line.trim()) return null;
1601
- if (!line.startsWith("data: ")) {
1602
- return null;
1603
- }
1604
- const jsonData = line.slice(6);
1605
- if (jsonData === "[DONE]") {
1606
- return null;
1607
- }
1608
- try {
1609
- const parsed = JSON.parse(jsonData);
1610
- const result = {};
1611
- if (parsed.choices?.[0]?.delta?.content) {
1612
- result.content = parsed.choices[0].delta.content;
1613
- }
1614
- if (parsed.choices?.[0]?.delta?.reasoning) {
1615
- result.reasoning = parsed.choices[0].delta.reasoning;
1616
- }
1617
- const extractedUsage = extractUsageFromSSEJson(parsed);
1618
- if (extractedUsage) {
1619
- result.usage = toUsageStats(extractedUsage);
1620
- } else if (parsed.usage) {
1621
- result.usage = {
1622
- total_tokens: parsed.usage.total_tokens ?? parsed.usage.input_tokens + parsed.usage.output_tokens,
1623
- prompt_tokens: parsed.usage.prompt_tokens ?? parsed.usage.input_tokens,
1624
- completion_tokens: parsed.usage.completion_tokens ?? parsed.usage.output_tokens
1625
- };
1626
- }
1627
- if (parsed.id) {
1628
- result.responseId = parsed.id;
1629
- }
1630
- if (parsed.model) {
1631
- result.model = parsed.model;
1632
- }
1633
- if (parsed.citations) {
1634
- result.citations = parsed.citations;
1635
- }
1636
- if (parsed.annotations) {
1637
- result.annotations = parsed.annotations;
1638
- }
1639
- if (parsed.choices?.[0]?.finish_reason) {
1640
- result.finish_reason = parsed.choices[0].finish_reason;
1641
- }
1642
- const images = parsed.choices?.[0]?.message?.images || parsed.choices?.[0]?.delta?.images;
1643
- if (images && Array.isArray(images)) {
1644
- result.images = images;
1645
- }
1646
- return result;
1647
- } catch {
1648
- return null;
1649
- }
1650
- }
1651
- /**
1652
- * Handle content delta with thinking support
1653
- */
1654
- _handleContent(content, callbacks, modelId) {
1655
- if (this.isInThinking && !this.isInContent) {
1656
- this.accumulatedThinking += "</thinking>";
1657
- callbacks.onThinking(this.accumulatedThinking);
1658
- this.isInThinking = false;
1659
- this.isInContent = true;
1660
- }
1661
- if (modelId) {
1662
- this._extractThinkingFromContent(content, callbacks);
1663
- } else {
1664
- this.accumulatedContent += content;
1665
- }
1666
- callbacks.onContent(this.accumulatedContent);
1667
- }
1668
- /**
1669
- * Handle thinking/reasoning content
1670
- */
1671
- _handleThinking(reasoning, callbacks) {
1672
- if (!this.isInThinking) {
1673
- this.accumulatedThinking += "<thinking> ";
1674
- this.isInThinking = true;
1675
- }
1676
- this.accumulatedThinking += reasoning;
1677
- callbacks.onThinking(this.accumulatedThinking);
1678
- }
1679
- /**
1680
- * Extract thinking blocks from content (for models with inline thinking)
1681
- */
1682
- _extractThinkingFromContent(content, callbacks) {
1683
- const parts = content.split(/(<thinking>|<\/thinking>)/);
1684
- for (const part of parts) {
1685
- if (part === "<thinking>") {
1686
- this.isInThinking = true;
1687
- if (!this.accumulatedThinking.includes("<thinking>")) {
1688
- this.accumulatedThinking += "<thinking> ";
1689
- }
1690
- } else if (part === "</thinking>") {
1691
- this.isInThinking = false;
1692
- this.accumulatedThinking += "</thinking>";
1693
- } else if (this.isInThinking) {
1694
- this.accumulatedThinking += part;
1695
- } else {
1696
- this.accumulatedContent += part;
1697
- }
1698
- }
1699
- }
1700
- /**
1701
- * Merge images into accumulated array, avoiding duplicates
1702
- */
1703
- _mergeImages(newImages) {
1704
- for (const img of newImages) {
1705
- const newUrl = img.image_url?.url;
1706
- const existingIndex = this.accumulatedImages.findIndex((existing) => {
1707
- const existingUrl = existing.image_url?.url;
1708
- if (newUrl && existingUrl) {
1709
- return existingUrl === newUrl;
1710
- }
1711
- if (img.index !== void 0 && existing.index !== void 0) {
1712
- return existing.index === img.index;
1713
- }
1714
- return false;
1715
- });
1716
- if (existingIndex === -1) {
1717
- this.accumulatedImages.push(img);
1718
- } else {
1719
- this.accumulatedImages[existingIndex] = img;
1720
- }
1721
- }
1722
- }
1723
- };
1724
-
1725
1409
  // utils/torUtils.ts
1726
1410
  var TOR_ONION_SUFFIX = ".onion";
1727
1411
  var isTorContext = () => {
@@ -1831,9 +1515,6 @@ function calculateImageTokens(width, height, detail = "auto") {
1831
1515
  const numTiles = tilesWidth * tilesHeight;
1832
1516
  return 85 + 170 * numTiles;
1833
1517
  }
1834
- function isInsecureHttpUrl(url) {
1835
- return url.startsWith("http://");
1836
- }
1837
1518
  var ProviderManager = class _ProviderManager {
1838
1519
  constructor(providerRegistry, store, logger) {
1839
1520
  this.providerRegistry = providerRegistry;
@@ -2050,7 +1731,7 @@ var ProviderManager = class _ProviderManager {
2050
1731
  if (this.isOnCooldown(baseUrl)) {
2051
1732
  continue;
2052
1733
  }
2053
- if (!torMode && (isOnionUrl(baseUrl) || isInsecureHttpUrl(baseUrl))) {
1734
+ if (!torMode && isOnionUrl(baseUrl)) {
2054
1735
  continue;
2055
1736
  }
2056
1737
  const model = models.find((m) => m.id === modelId);
@@ -2101,7 +1782,7 @@ var ProviderManager = class _ProviderManager {
2101
1782
  for (const [baseUrl, models] of Object.entries(allProviders)) {
2102
1783
  if (disabledProviders.has(baseUrl)) continue;
2103
1784
  if (this.isOnCooldown(baseUrl)) continue;
2104
- if (!torMode && (isOnionUrl(baseUrl) || isInsecureHttpUrl(baseUrl)))
1785
+ if (!torMode && isOnionUrl(baseUrl))
2105
1786
  continue;
2106
1787
  const model = models.find((m) => m.id === modelId);
2107
1788
  if (!model) continue;
@@ -2116,16 +1797,18 @@ var ProviderManager = class _ProviderManager {
2116
1797
  getProviderPriceRankingForModel(modelId, options = {}) {
2117
1798
  const includeDisabled = options.includeDisabled ?? false;
2118
1799
  const torMode = options.torMode ?? false;
2119
- const disabledProviders = new Set(
2120
- this.providerRegistry.getDisabledProviders()
2121
- );
1800
+ const disabledProviderList = this.providerRegistry.getDisabledProviders();
1801
+ const disabledProviders = new Set(disabledProviderList);
1802
+ if (disabledProviderList.length > 0) {
1803
+ this.logger.log(`getProviderPriceRankingForModel: disabled providers (${disabledProviderList.length}): ${disabledProviderList.join(", ")}`);
1804
+ }
2122
1805
  const allModels = this.providerRegistry.getAllProvidersModels();
2123
1806
  const results = [];
2124
1807
  for (const [baseUrl, models] of Object.entries(allModels)) {
2125
1808
  if (!includeDisabled && disabledProviders.has(baseUrl)) continue;
2126
1809
  if (this.isOnCooldown(baseUrl)) continue;
2127
1810
  if (torMode && !baseUrl.includes(".onion")) continue;
2128
- if (!torMode && (baseUrl.includes(".onion") || isInsecureHttpUrl(baseUrl)))
1811
+ if (!torMode && baseUrl.includes(".onion"))
2129
1812
  continue;
2130
1813
  const match = models.find((model) => model.id === modelId);
2131
1814
  if (!match?.sats_pricing) continue;
@@ -2145,12 +1828,20 @@ var ProviderManager = class _ProviderManager {
2145
1828
  totalPerMillion
2146
1829
  });
2147
1830
  }
2148
- return results.sort((a, b) => {
1831
+ results.sort((a, b) => {
2149
1832
  if (a.totalPerMillion !== b.totalPerMillion) {
2150
1833
  return a.totalPerMillion - b.totalPerMillion;
2151
1834
  }
2152
1835
  return a.baseUrl.localeCompare(b.baseUrl);
2153
1836
  });
1837
+ if (results.length > 0) {
1838
+ const ranking = results.map((r, i) => ` ${i + 1}. ${r.baseUrl} total=${r.totalPerMillion.toFixed(2)} sats/M (prompt=${r.promptPerMillion.toFixed(2)} completion=${r.completionPerMillion.toFixed(2)})`).join("\n");
1839
+ this.logger.log(`getProviderPriceRankingForModel: ${modelId} ranking (${results.length} providers):
1840
+ ${ranking}`);
1841
+ } else {
1842
+ this.logger.log(`getProviderPriceRankingForModel: ${modelId} no providers found`);
1843
+ }
1844
+ return results;
2154
1845
  }
2155
1846
  /**
2156
1847
  * Get best-priced provider for a specific model
@@ -2338,92 +2029,6 @@ var createMemoryDriver = (seed) => {
2338
2029
  };
2339
2030
  };
2340
2031
 
2341
- // storage/drivers/sqlite.ts
2342
- var isBun = () => {
2343
- return typeof process.versions.bun !== "undefined";
2344
- };
2345
- var cachedDbModule = null;
2346
- var loadDatabase = async (dbPath) => {
2347
- if (isBun()) {
2348
- throw new Error(
2349
- "SQLite driver not supported in Bun. Use createBunSqliteDriver() instead."
2350
- );
2351
- }
2352
- try {
2353
- if (!cachedDbModule) {
2354
- cachedDbModule = (await import('better-sqlite3')).default;
2355
- }
2356
- return new cachedDbModule(dbPath);
2357
- } catch (error) {
2358
- throw new Error(
2359
- `better-sqlite3 is required for sqlite storage. Install it to use sqlite storage. (${error})`
2360
- );
2361
- }
2362
- };
2363
- var createSqliteDriver = (options = {}) => {
2364
- const dbPath = options.dbPath || "routstr.sqlite";
2365
- const tableName = options.tableName || "sdk_storage";
2366
- let db;
2367
- let selectStmt;
2368
- let upsertStmt;
2369
- let deleteStmt;
2370
- const initDb = async () => {
2371
- if (!db) {
2372
- db = await loadDatabase(dbPath);
2373
- db.exec(
2374
- `CREATE TABLE IF NOT EXISTS ${tableName} (key TEXT PRIMARY KEY, value TEXT NOT NULL)`
2375
- );
2376
- selectStmt = db.prepare(`SELECT value FROM ${tableName} WHERE key = ?`);
2377
- upsertStmt = db.prepare(
2378
- `INSERT INTO ${tableName} (key, value) VALUES (?, ?)
2379
- ON CONFLICT(key) DO UPDATE SET value = excluded.value`
2380
- );
2381
- deleteStmt = db.prepare(`DELETE FROM ${tableName} WHERE key = ?`);
2382
- }
2383
- };
2384
- const ensureInit = async () => {
2385
- if (!db) {
2386
- await initDb();
2387
- }
2388
- };
2389
- return {
2390
- async getItem(key, defaultValue) {
2391
- try {
2392
- await ensureInit();
2393
- const row = selectStmt.get(key);
2394
- if (!row || typeof row.value !== "string") return defaultValue;
2395
- try {
2396
- return JSON.parse(row.value);
2397
- } catch (parseError) {
2398
- if (typeof defaultValue === "string") {
2399
- return row.value;
2400
- }
2401
- throw parseError;
2402
- }
2403
- } catch (error) {
2404
- console.error(`SQLite getItem failed for key "${key}":`, error);
2405
- return defaultValue;
2406
- }
2407
- },
2408
- async setItem(key, value) {
2409
- try {
2410
- await ensureInit();
2411
- upsertStmt.run(key, JSON.stringify(value));
2412
- } catch (error) {
2413
- console.error(`SQLite setItem failed for key "${key}":`, error);
2414
- }
2415
- },
2416
- async removeItem(key) {
2417
- try {
2418
- await ensureInit();
2419
- deleteStmt.run(key);
2420
- } catch (error) {
2421
- console.error(`SQLite removeItem failed for key "${key}":`, error);
2422
- }
2423
- }
2424
- };
2425
- };
2426
-
2427
2032
  // storage/keys.ts
2428
2033
  var SDK_STORAGE_KEYS = {
2429
2034
  MODELS_FROM_ALL_PROVIDERS: "modelsFromAllProviders",
@@ -2458,9 +2063,10 @@ var openDatabase = (dbName, storeName) => {
2458
2063
  return Promise.reject(new Error("IndexedDB is not available"));
2459
2064
  }
2460
2065
  return new Promise((resolve, reject) => {
2461
- const request = indexedDB.open(dbName, 1);
2066
+ const request = indexedDB.open(dbName, 3);
2462
2067
  request.onupgradeneeded = () => {
2463
2068
  const db = request.result;
2069
+ const tx = request.transaction;
2464
2070
  if (!db.objectStoreNames.contains(storeName)) {
2465
2071
  const store = db.createObjectStore(storeName, { keyPath: "id" });
2466
2072
  store.createIndex("timestamp", "timestamp", { unique: false });
@@ -2468,10 +2074,25 @@ var openDatabase = (dbName, storeName) => {
2468
2074
  store.createIndex("baseUrl", "baseUrl", { unique: false });
2469
2075
  store.createIndex("sessionId", "sessionId", { unique: false });
2470
2076
  store.createIndex("client", "client", { unique: false });
2077
+ store.createIndex("provider", "provider", { unique: false });
2078
+ } else if (tx) {
2079
+ const store = tx.objectStore(storeName);
2080
+ if (!store.indexNames.contains("provider")) {
2081
+ store.createIndex("provider", "provider", { unique: false });
2082
+ }
2083
+ }
2084
+ if (storeName !== "sdk_storage" && !db.objectStoreNames.contains("sdk_storage")) {
2085
+ db.createObjectStore("sdk_storage");
2471
2086
  }
2472
2087
  };
2473
2088
  request.onsuccess = () => resolve(request.result);
2474
2089
  request.onerror = () => reject(request.error);
2090
+ request.onblocked = () => {
2091
+ console.warn(
2092
+ `[usageTracking IndexedDB] open blocked for "${dbName}" \u2014 close other tabs using this DB`
2093
+ );
2094
+ reject(new Error(`IndexedDB "${dbName}" blocked by another connection`));
2095
+ };
2475
2096
  });
2476
2097
  };
2477
2098
  var matchesFilters = (entry, options = {}) => {
@@ -2493,6 +2114,9 @@ var matchesFilters = (entry, options = {}) => {
2493
2114
  if (options.client && entry.client !== options.client) {
2494
2115
  return false;
2495
2116
  }
2117
+ if (options.provider && entry.provider !== options.provider) {
2118
+ return false;
2119
+ }
2496
2120
  return true;
2497
2121
  };
2498
2122
  var createIndexedDBUsageTrackingDriver = (options = {}) => {
@@ -2624,393 +2248,8 @@ var createIndexedDBUsageTrackingDriver = (options = {}) => {
2624
2248
  };
2625
2249
  };
2626
2250
 
2627
- // storage/usageTracking/sqlite.ts
2628
- var MIGRATION_MARKER_KEY2 = "usage_tracking_migration_v1";
2629
- var normalizeBaseUrl2 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
2630
- var isBun2 = () => {
2631
- return typeof process.versions.bun !== "undefined";
2632
- };
2633
- var cachedDbModule2 = null;
2634
- var loadDatabase2 = async (dbPath) => {
2635
- if (isBun2()) {
2636
- throw new Error(
2637
- "SQLite driver not supported in Bun. Use createMemoryDriver() instead."
2638
- );
2639
- }
2640
- try {
2641
- if (!cachedDbModule2) {
2642
- cachedDbModule2 = (await import('better-sqlite3')).default;
2643
- }
2644
- return new cachedDbModule2(dbPath);
2645
- } catch (error) {
2646
- throw new Error(
2647
- `better-sqlite3 is required for sqlite usage tracking. Install it to use sqlite storage. (${error})`
2648
- );
2649
- }
2650
- };
2651
- var buildWhereClause = (options = {}) => {
2652
- const clauses = [];
2653
- const params = [];
2654
- if (typeof options.before === "number") {
2655
- clauses.push("timestamp < ?");
2656
- params.push(options.before);
2657
- }
2658
- if (typeof options.after === "number") {
2659
- clauses.push("timestamp > ?");
2660
- params.push(options.after);
2661
- }
2662
- if (options.modelId) {
2663
- clauses.push("model_id = ?");
2664
- params.push(options.modelId);
2665
- }
2666
- if (options.baseUrl) {
2667
- clauses.push("base_url = ?");
2668
- params.push(normalizeBaseUrl2(options.baseUrl));
2669
- }
2670
- if (options.sessionId) {
2671
- clauses.push("session_id = ?");
2672
- params.push(options.sessionId);
2673
- }
2674
- if (options.client) {
2675
- clauses.push("client = ?");
2676
- params.push(options.client);
2677
- }
2678
- return {
2679
- sql: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
2680
- params
2681
- };
2682
- };
2683
- var createSqliteUsageTrackingDriver = (options = {}) => {
2684
- const dbPath = options.dbPath || "routstr.sqlite";
2685
- const tableName = options.tableName || "usage_tracking";
2686
- const legacyStorageDriver = options.legacyStorageDriver;
2687
- let db;
2688
- let insertStmt;
2689
- let migrationComplete = false;
2690
- const initDb = async () => {
2691
- if (!db) {
2692
- db = await loadDatabase2(dbPath);
2693
- db.exec(`
2694
- CREATE TABLE IF NOT EXISTS ${tableName} (
2695
- id TEXT PRIMARY KEY,
2696
- timestamp INTEGER NOT NULL,
2697
- model_id TEXT NOT NULL,
2698
- base_url TEXT NOT NULL,
2699
- request_id TEXT NOT NULL,
2700
- cost REAL NOT NULL,
2701
- sats_cost REAL NOT NULL,
2702
- prompt_tokens INTEGER NOT NULL,
2703
- completion_tokens INTEGER NOT NULL,
2704
- total_tokens INTEGER NOT NULL,
2705
- client TEXT,
2706
- session_id TEXT,
2707
- tags TEXT
2708
- );
2709
- CREATE INDEX IF NOT EXISTS idx_${tableName}_timestamp ON ${tableName}(timestamp);
2710
- CREATE INDEX IF NOT EXISTS idx_${tableName}_model_id ON ${tableName}(model_id);
2711
- CREATE INDEX IF NOT EXISTS idx_${tableName}_base_url ON ${tableName}(base_url);
2712
- CREATE INDEX IF NOT EXISTS idx_${tableName}_session_id ON ${tableName}(session_id);
2713
- CREATE INDEX IF NOT EXISTS idx_${tableName}_client ON ${tableName}(client);
2714
- `);
2715
- insertStmt = db.prepare(`
2716
- INSERT OR REPLACE INTO ${tableName} (
2717
- id, timestamp, model_id, base_url, request_id,
2718
- cost, sats_cost, prompt_tokens, completion_tokens, total_tokens,
2719
- client, session_id, tags
2720
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2721
- `);
2722
- }
2723
- };
2724
- const ensureInit = async () => {
2725
- if (!db) {
2726
- await initDb();
2727
- }
2728
- };
2729
- const appendOne = (entry) => {
2730
- insertStmt.run(
2731
- entry.id,
2732
- entry.timestamp,
2733
- entry.modelId,
2734
- normalizeBaseUrl2(entry.baseUrl),
2735
- entry.requestId,
2736
- entry.cost,
2737
- entry.satsCost,
2738
- entry.promptTokens,
2739
- entry.completionTokens,
2740
- entry.totalTokens,
2741
- entry.client ?? null,
2742
- entry.sessionId ?? null,
2743
- JSON.stringify(entry.tags ?? [])
2744
- );
2745
- };
2746
- const ensureMigrated = async () => {
2747
- if (!legacyStorageDriver || migrationComplete) return;
2748
- const migrated = await legacyStorageDriver.getItem(
2749
- MIGRATION_MARKER_KEY2,
2750
- false
2751
- );
2752
- if (migrated) {
2753
- migrationComplete = true;
2754
- return;
2755
- }
2756
- const legacyEntries = await legacyStorageDriver.getItem(
2757
- SDK_STORAGE_KEYS.USAGE_TRACKING,
2758
- []
2759
- );
2760
- for (const entry of legacyEntries) {
2761
- appendOne(entry);
2762
- }
2763
- if (legacyEntries.length > 0) {
2764
- await legacyStorageDriver.removeItem(SDK_STORAGE_KEYS.USAGE_TRACKING);
2765
- }
2766
- await legacyStorageDriver.setItem(MIGRATION_MARKER_KEY2, true);
2767
- migrationComplete = true;
2768
- };
2769
- const mapRow = (row) => ({
2770
- id: row.id,
2771
- timestamp: row.timestamp,
2772
- modelId: row.model_id,
2773
- baseUrl: row.base_url,
2774
- requestId: row.request_id,
2775
- cost: row.cost,
2776
- satsCost: row.sats_cost,
2777
- promptTokens: row.prompt_tokens,
2778
- completionTokens: row.completion_tokens,
2779
- totalTokens: row.total_tokens,
2780
- client: row.client ?? void 0,
2781
- sessionId: row.session_id ?? void 0,
2782
- tags: typeof row.tags === "string" ? JSON.parse(row.tags) : void 0
2783
- });
2784
- return {
2785
- async migrate() {
2786
- await ensureInit();
2787
- await ensureMigrated();
2788
- },
2789
- async append(entry) {
2790
- await ensureInit();
2791
- await ensureMigrated();
2792
- appendOne(entry);
2793
- },
2794
- async appendMany(entries) {
2795
- await ensureInit();
2796
- await ensureMigrated();
2797
- for (const entry of entries) {
2798
- appendOne(entry);
2799
- }
2800
- },
2801
- async list(options2 = {}) {
2802
- await ensureInit();
2803
- await ensureMigrated();
2804
- const { sql, params } = buildWhereClause(options2);
2805
- const limitSql = typeof options2.limit === "number" ? " LIMIT ?" : "";
2806
- const stmt = db.prepare(
2807
- `SELECT * FROM ${tableName} ${sql} ORDER BY timestamp DESC${limitSql}`
2808
- );
2809
- const rows = stmt.all(
2810
- ...typeof options2.limit === "number" ? [...params, options2.limit] : params
2811
- );
2812
- return rows.map(mapRow);
2813
- },
2814
- async count(options2 = {}) {
2815
- await ensureInit();
2816
- await ensureMigrated();
2817
- const { sql, params } = buildWhereClause(options2);
2818
- const stmt = db.prepare(`SELECT COUNT(*) as count FROM ${tableName} ${sql}`);
2819
- const row = stmt.get(...params);
2820
- return Number(row?.count ?? 0);
2821
- },
2822
- async deleteOlderThan(timestamp) {
2823
- await ensureInit();
2824
- await ensureMigrated();
2825
- const stmt = db.prepare(`DELETE FROM ${tableName} WHERE timestamp < ?`);
2826
- const result = stmt.run(timestamp);
2827
- return result.changes;
2828
- },
2829
- async clear() {
2830
- await ensureInit();
2831
- await ensureMigrated();
2832
- db.prepare(`DELETE FROM ${tableName}`).run();
2833
- }
2834
- };
2835
- };
2836
-
2837
- // storage/usageTracking/bunSqlite.ts
2838
- var MIGRATION_MARKER_KEY3 = "usage_tracking_migration_v1";
2839
- var normalizeBaseUrl3 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
2840
- var buildWhereClause2 = (options = {}) => {
2841
- const clauses = [];
2842
- const params = [];
2843
- if (typeof options.before === "number") {
2844
- clauses.push("timestamp < ?");
2845
- params.push(options.before);
2846
- }
2847
- if (typeof options.after === "number") {
2848
- clauses.push("timestamp > ?");
2849
- params.push(options.after);
2850
- }
2851
- if (options.modelId) {
2852
- clauses.push("model_id = ?");
2853
- params.push(options.modelId);
2854
- }
2855
- if (options.baseUrl) {
2856
- clauses.push("base_url = ?");
2857
- params.push(normalizeBaseUrl3(options.baseUrl));
2858
- }
2859
- if (options.sessionId) {
2860
- clauses.push("session_id = ?");
2861
- params.push(options.sessionId);
2862
- }
2863
- if (options.client) {
2864
- clauses.push("client = ?");
2865
- params.push(options.client);
2866
- }
2867
- return {
2868
- sql: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
2869
- params
2870
- };
2871
- };
2872
- var createBunSqliteUsageTrackingDriver = (options = {}) => {
2873
- const dbPath = options.dbPath || "routstr.sqlite";
2874
- const tableName = options.tableName || "usage_tracking";
2875
- const legacyStorageDriver = options.legacyStorageDriver;
2876
- const SQLiteDatabase = options.sqlite?.Database;
2877
- let migrationPromise = null;
2878
- if (!SQLiteDatabase) {
2879
- throw new Error(
2880
- "Bun SQLite Database constructor is required. Pass { sqlite: { Database } } when creating the driver."
2881
- );
2882
- }
2883
- const db = new SQLiteDatabase(dbPath);
2884
- db.run(`
2885
- CREATE TABLE IF NOT EXISTS ${tableName} (
2886
- id TEXT PRIMARY KEY,
2887
- timestamp INTEGER NOT NULL,
2888
- model_id TEXT NOT NULL,
2889
- base_url TEXT NOT NULL,
2890
- request_id TEXT NOT NULL,
2891
- cost REAL NOT NULL,
2892
- sats_cost REAL NOT NULL,
2893
- prompt_tokens INTEGER NOT NULL,
2894
- completion_tokens INTEGER NOT NULL,
2895
- total_tokens INTEGER NOT NULL,
2896
- client TEXT,
2897
- session_id TEXT,
2898
- tags TEXT
2899
- )
2900
- `);
2901
- db.run(`CREATE INDEX IF NOT EXISTS idx_${tableName}_timestamp ON ${tableName}(timestamp)`);
2902
- db.run(`CREATE INDEX IF NOT EXISTS idx_${tableName}_model_id ON ${tableName}(model_id)`);
2903
- db.run(`CREATE INDEX IF NOT EXISTS idx_${tableName}_base_url ON ${tableName}(base_url)`);
2904
- const appendOne = (entry) => {
2905
- db.query(`
2906
- INSERT OR REPLACE INTO ${tableName} (
2907
- id, timestamp, model_id, base_url, request_id,
2908
- cost, sats_cost, prompt_tokens, completion_tokens, total_tokens,
2909
- client, session_id, tags
2910
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2911
- `).run(
2912
- entry.id,
2913
- entry.timestamp,
2914
- entry.modelId,
2915
- normalizeBaseUrl3(entry.baseUrl),
2916
- entry.requestId,
2917
- entry.cost,
2918
- entry.satsCost,
2919
- entry.promptTokens,
2920
- entry.completionTokens,
2921
- entry.totalTokens,
2922
- entry.client ?? null,
2923
- entry.sessionId ?? null,
2924
- JSON.stringify(entry.tags ?? [])
2925
- );
2926
- };
2927
- const mapRow = (row) => ({
2928
- id: row.id,
2929
- timestamp: row.timestamp,
2930
- modelId: row.model_id,
2931
- baseUrl: row.base_url,
2932
- requestId: row.request_id,
2933
- cost: row.cost,
2934
- satsCost: row.sats_cost,
2935
- promptTokens: row.prompt_tokens,
2936
- completionTokens: row.completion_tokens,
2937
- totalTokens: row.total_tokens,
2938
- client: row.client ?? void 0,
2939
- sessionId: row.session_id ?? void 0,
2940
- tags: typeof row.tags === "string" ? JSON.parse(row.tags) : void 0
2941
- });
2942
- const ensureMigrated = async () => {
2943
- if (!legacyStorageDriver) return;
2944
- if (!migrationPromise) {
2945
- migrationPromise = (async () => {
2946
- const migrated = await legacyStorageDriver.getItem(
2947
- MIGRATION_MARKER_KEY3,
2948
- false
2949
- );
2950
- if (migrated) return;
2951
- const legacyEntries = await legacyStorageDriver.getItem(
2952
- SDK_STORAGE_KEYS.USAGE_TRACKING,
2953
- []
2954
- );
2955
- if (legacyEntries.length > 0) {
2956
- for (const entry of legacyEntries) {
2957
- appendOne(entry);
2958
- }
2959
- await legacyStorageDriver.removeItem(SDK_STORAGE_KEYS.USAGE_TRACKING);
2960
- }
2961
- await legacyStorageDriver.setItem(MIGRATION_MARKER_KEY3, true);
2962
- })();
2963
- }
2964
- await migrationPromise;
2965
- };
2966
- return {
2967
- async migrate() {
2968
- await ensureMigrated();
2969
- },
2970
- async append(entry) {
2971
- await ensureMigrated();
2972
- appendOne(entry);
2973
- },
2974
- async appendMany(entries) {
2975
- await ensureMigrated();
2976
- for (const entry of entries) {
2977
- appendOne(entry);
2978
- }
2979
- },
2980
- async list(options2 = {}) {
2981
- await ensureMigrated();
2982
- const { sql, params } = buildWhereClause2(options2);
2983
- const limitSql = typeof options2.limit === "number" ? " LIMIT ?" : "";
2984
- const query = `SELECT * FROM ${tableName} ${sql} ORDER BY timestamp DESC${limitSql}`;
2985
- let rows;
2986
- if (typeof options2.limit === "number") {
2987
- rows = db.query(query).all(...params, options2.limit);
2988
- } else {
2989
- rows = db.query(query).all(...params);
2990
- }
2991
- return rows.map(mapRow);
2992
- },
2993
- async count(options2 = {}) {
2994
- const { sql, params } = buildWhereClause2(options2);
2995
- const query = `SELECT COUNT(*) as count FROM ${tableName} ${sql}`;
2996
- const row = db.query(query).get(...params);
2997
- return Number(row?.count ?? 0);
2998
- },
2999
- async deleteOlderThan(timestamp) {
3000
- await ensureMigrated();
3001
- const before = timestamp;
3002
- const result = db.query(`DELETE FROM ${tableName} WHERE timestamp < ?`).run(before);
3003
- return result.changes ?? 0;
3004
- },
3005
- async clear() {
3006
- await ensureMigrated();
3007
- db.query(`DELETE FROM ${tableName}`).run();
3008
- }
3009
- };
3010
- };
3011
-
3012
2251
  // storage/usageTracking/memory.ts
3013
- var normalizeBaseUrl4 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
2252
+ var normalizeBaseUrl2 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
3014
2253
  var matchesFilters2 = (entry, options = {}) => {
3015
2254
  if (typeof options.before === "number" && entry.timestamp >= options.before) {
3016
2255
  return false;
@@ -3021,7 +2260,7 @@ var matchesFilters2 = (entry, options = {}) => {
3021
2260
  if (options.modelId && entry.modelId !== options.modelId) {
3022
2261
  return false;
3023
2262
  }
3024
- if (options.baseUrl && normalizeBaseUrl4(entry.baseUrl) !== normalizeBaseUrl4(options.baseUrl)) {
2263
+ if (options.baseUrl && normalizeBaseUrl2(entry.baseUrl) !== normalizeBaseUrl2(options.baseUrl)) {
3025
2264
  return false;
3026
2265
  }
3027
2266
  if (options.sessionId && entry.sessionId !== options.sessionId) {
@@ -3030,23 +2269,26 @@ var matchesFilters2 = (entry, options = {}) => {
3030
2269
  if (options.client && entry.client !== options.client) {
3031
2270
  return false;
3032
2271
  }
2272
+ if (options.provider && entry.provider !== options.provider) {
2273
+ return false;
2274
+ }
3033
2275
  return true;
3034
2276
  };
3035
2277
  var createMemoryUsageTrackingDriver = (seed = []) => {
3036
2278
  const store = /* @__PURE__ */ new Map();
3037
2279
  for (const entry of seed) {
3038
- store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl4(entry.baseUrl) });
2280
+ store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl2(entry.baseUrl) });
3039
2281
  }
3040
2282
  return {
3041
2283
  async migrate() {
3042
2284
  return;
3043
2285
  },
3044
2286
  async append(entry) {
3045
- store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl4(entry.baseUrl) });
2287
+ store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl2(entry.baseUrl) });
3046
2288
  },
3047
2289
  async appendMany(entries) {
3048
2290
  for (const entry of entries) {
3049
- store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl4(entry.baseUrl) });
2291
+ store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl2(entry.baseUrl) });
3050
2292
  }
3051
2293
  },
3052
2294
  async list(options = {}) {
@@ -3074,7 +2316,7 @@ var createMemoryUsageTrackingDriver = (seed = []) => {
3074
2316
  }
3075
2317
  };
3076
2318
  };
3077
- var normalizeBaseUrl5 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
2319
+ var normalizeBaseUrl3 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
3078
2320
  var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3079
2321
  modelsFromAllProviders: {},
3080
2322
  lastUsedModel: null,
@@ -3097,7 +2339,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3097
2339
  setModelsFromAllProviders: (value) => {
3098
2340
  const normalized = {};
3099
2341
  for (const [baseUrl, models] of Object.entries(value)) {
3100
- normalized[normalizeBaseUrl5(baseUrl)] = models;
2342
+ normalized[normalizeBaseUrl3(baseUrl)] = models;
3101
2343
  }
3102
2344
  void driver.setItem(
3103
2345
  SDK_STORAGE_KEYS.MODELS_FROM_ALL_PROVIDERS,
@@ -3110,7 +2352,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3110
2352
  set({ lastUsedModel: value });
3111
2353
  },
3112
2354
  setBaseUrlsList: (value) => {
3113
- const normalized = value.map((url) => normalizeBaseUrl5(url));
2355
+ const normalized = value.map((url) => normalizeBaseUrl3(url));
3114
2356
  void driver.setItem(SDK_STORAGE_KEYS.BASE_URLS_LIST, normalized);
3115
2357
  set({ baseUrlsList: normalized });
3116
2358
  },
@@ -3119,14 +2361,14 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3119
2361
  set({ lastBaseUrlsUpdate: value });
3120
2362
  },
3121
2363
  setDisabledProviders: (value) => {
3122
- const normalized = value.map((url) => normalizeBaseUrl5(url));
2364
+ const normalized = value.map((url) => normalizeBaseUrl3(url));
3123
2365
  void driver.setItem(SDK_STORAGE_KEYS.DISABLED_PROVIDERS, normalized);
3124
2366
  set({ disabledProviders: normalized });
3125
2367
  },
3126
2368
  setMintsFromAllProviders: (value) => {
3127
2369
  const normalized = {};
3128
2370
  for (const [baseUrl, mints] of Object.entries(value)) {
3129
- normalized[normalizeBaseUrl5(baseUrl)] = mints.map(
2371
+ normalized[normalizeBaseUrl3(baseUrl)] = mints.map(
3130
2372
  (mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint
3131
2373
  );
3132
2374
  }
@@ -3139,7 +2381,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3139
2381
  setInfoFromAllProviders: (value) => {
3140
2382
  const normalized = {};
3141
2383
  for (const [baseUrl, info] of Object.entries(value)) {
3142
- normalized[normalizeBaseUrl5(baseUrl)] = info;
2384
+ normalized[normalizeBaseUrl3(baseUrl)] = info;
3143
2385
  }
3144
2386
  void driver.setItem(SDK_STORAGE_KEYS.INFO_FROM_ALL_PROVIDERS, normalized);
3145
2387
  set({ infoFromAllProviders: normalized });
@@ -3147,7 +2389,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3147
2389
  setLastModelsUpdate: (value) => {
3148
2390
  const normalized = {};
3149
2391
  for (const [baseUrl, timestamp] of Object.entries(value)) {
3150
- normalized[normalizeBaseUrl5(baseUrl)] = timestamp;
2392
+ normalized[normalizeBaseUrl3(baseUrl)] = timestamp;
3151
2393
  }
3152
2394
  void driver.setItem(SDK_STORAGE_KEYS.LAST_MODELS_UPDATE, normalized);
3153
2395
  set({ lastModelsUpdate: normalized });
@@ -3157,7 +2399,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3157
2399
  const updates = typeof value === "function" ? value(state.apiKeys) : value;
3158
2400
  const normalized = updates.map((entry) => ({
3159
2401
  ...entry,
3160
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
2402
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
3161
2403
  balance: entry.balance ?? 0,
3162
2404
  lastUsed: entry.lastUsed ?? null
3163
2405
  }));
@@ -3169,7 +2411,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3169
2411
  set((state) => {
3170
2412
  const updates = typeof value === "function" ? value(state.childKeys) : value;
3171
2413
  const normalized = updates.map((entry) => ({
3172
- parentBaseUrl: normalizeBaseUrl5(entry.parentBaseUrl),
2414
+ parentBaseUrl: normalizeBaseUrl3(entry.parentBaseUrl),
3173
2415
  childKey: entry.childKey,
3174
2416
  balance: entry.balance ?? 0,
3175
2417
  balanceLimit: entry.balanceLimit,
@@ -3183,9 +2425,9 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3183
2425
  setXcashuTokens: (value) => {
3184
2426
  const normalized = {};
3185
2427
  for (const [baseUrl, tokens] of Object.entries(value)) {
3186
- normalized[normalizeBaseUrl5(baseUrl)] = tokens.map((entry) => ({
2428
+ normalized[normalizeBaseUrl3(baseUrl)] = tokens.map((entry) => ({
3187
2429
  ...entry,
3188
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
2430
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
3189
2431
  createdAt: entry.createdAt ?? Date.now(),
3190
2432
  tryCount: entry.tryCount ?? 0
3191
2433
  }));
@@ -3236,12 +2478,12 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3236
2478
  },
3237
2479
  // ========== Failure Tracking ==========
3238
2480
  setFailedProviders: (value) => {
3239
- const normalized = value.map((url) => normalizeBaseUrl5(url));
2481
+ const normalized = value.map((url) => normalizeBaseUrl3(url));
3240
2482
  void driver.setItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, normalized);
3241
2483
  set({ failedProviders: normalized });
3242
2484
  },
3243
2485
  addFailedProvider: (baseUrl) => {
3244
- const normalized = normalizeBaseUrl5(baseUrl);
2486
+ const normalized = normalizeBaseUrl3(baseUrl);
3245
2487
  const current = get().failedProviders;
3246
2488
  if (!current.includes(normalized)) {
3247
2489
  const updated = [...current, normalized];
@@ -3250,7 +2492,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3250
2492
  }
3251
2493
  },
3252
2494
  removeFailedProvider: (baseUrl) => {
3253
- const normalized = normalizeBaseUrl5(baseUrl);
2495
+ const normalized = normalizeBaseUrl3(baseUrl);
3254
2496
  const current = get().failedProviders;
3255
2497
  const updated = current.filter((url) => url !== normalized);
3256
2498
  void driver.setItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, updated);
@@ -3259,13 +2501,13 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3259
2501
  setLastFailed: (value) => {
3260
2502
  const normalized = {};
3261
2503
  for (const [baseUrl, timestamp] of Object.entries(value)) {
3262
- normalized[normalizeBaseUrl5(baseUrl)] = timestamp;
2504
+ normalized[normalizeBaseUrl3(baseUrl)] = timestamp;
3263
2505
  }
3264
2506
  void driver.setItem(SDK_STORAGE_KEYS.LAST_FAILED, normalized);
3265
2507
  set({ lastFailed: normalized });
3266
2508
  },
3267
2509
  setLastFailedTimestamp: (baseUrl, timestamp) => {
3268
- const normalized = normalizeBaseUrl5(baseUrl);
2510
+ const normalized = normalizeBaseUrl3(baseUrl);
3269
2511
  const current = get().lastFailed;
3270
2512
  const updated = { ...current, [normalized]: timestamp };
3271
2513
  void driver.setItem(SDK_STORAGE_KEYS.LAST_FAILED, updated);
@@ -3273,14 +2515,14 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3273
2515
  },
3274
2516
  setProvidersOnCooldown: (value) => {
3275
2517
  const normalized = value.map((entry) => ({
3276
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
2518
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
3277
2519
  timestamp: entry.timestamp
3278
2520
  }));
3279
2521
  void driver.setItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, normalized);
3280
2522
  set({ providersOnCooldown: normalized });
3281
2523
  },
3282
2524
  addProviderOnCooldown: (baseUrl, timestamp) => {
3283
- const normalized = normalizeBaseUrl5(baseUrl);
2525
+ const normalized = normalizeBaseUrl3(baseUrl);
3284
2526
  const current = get().providersOnCooldown;
3285
2527
  if (!current.some((entry) => entry.baseUrl === normalized)) {
3286
2528
  const updated = [...current, { baseUrl: normalized, timestamp }];
@@ -3289,7 +2531,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
3289
2531
  }
3290
2532
  },
3291
2533
  removeProviderFromCooldown: (baseUrl) => {
3292
- const normalized = normalizeBaseUrl5(baseUrl);
2534
+ const normalized = normalizeBaseUrl3(baseUrl);
3293
2535
  const current = get().providersOnCooldown;
3294
2536
  const updated = current.filter((entry) => entry.baseUrl !== normalized);
3295
2537
  void driver.setItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, updated);
@@ -3360,40 +2602,40 @@ var hydrateStoreFromDriver = async (store, driver) => {
3360
2602
  ]);
3361
2603
  const modelsFromAllProviders = Object.fromEntries(
3362
2604
  Object.entries(rawModels).map(([baseUrl, models]) => [
3363
- normalizeBaseUrl5(baseUrl),
2605
+ normalizeBaseUrl3(baseUrl),
3364
2606
  models
3365
2607
  ])
3366
2608
  );
3367
- const baseUrlsList = rawBaseUrls.map((url) => normalizeBaseUrl5(url));
2609
+ const baseUrlsList = rawBaseUrls.map((url) => normalizeBaseUrl3(url));
3368
2610
  const disabledProviders = rawDisabledProviders.map(
3369
- (url) => normalizeBaseUrl5(url)
2611
+ (url) => normalizeBaseUrl3(url)
3370
2612
  );
3371
2613
  const mintsFromAllProviders = Object.fromEntries(
3372
2614
  Object.entries(rawMints).map(([baseUrl, mints]) => [
3373
- normalizeBaseUrl5(baseUrl),
2615
+ normalizeBaseUrl3(baseUrl),
3374
2616
  mints.map((mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint)
3375
2617
  ])
3376
2618
  );
3377
2619
  const infoFromAllProviders = Object.fromEntries(
3378
2620
  Object.entries(rawInfo).map(([baseUrl, info]) => [
3379
- normalizeBaseUrl5(baseUrl),
2621
+ normalizeBaseUrl3(baseUrl),
3380
2622
  info
3381
2623
  ])
3382
2624
  );
3383
2625
  const lastModelsUpdate = Object.fromEntries(
3384
2626
  Object.entries(rawLastModelsUpdate).map(([baseUrl, timestamp]) => [
3385
- normalizeBaseUrl5(baseUrl),
2627
+ normalizeBaseUrl3(baseUrl),
3386
2628
  timestamp
3387
2629
  ])
3388
2630
  );
3389
2631
  const apiKeys = rawApiKeys.map((entry) => ({
3390
2632
  ...entry,
3391
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
2633
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
3392
2634
  balance: entry.balance ?? 0,
3393
2635
  lastUsed: entry.lastUsed ?? null
3394
2636
  }));
3395
2637
  const childKeys = rawChildKeys.map((entry) => ({
3396
- parentBaseUrl: normalizeBaseUrl5(entry.parentBaseUrl),
2638
+ parentBaseUrl: normalizeBaseUrl3(entry.parentBaseUrl),
3397
2639
  childKey: entry.childKey,
3398
2640
  balance: entry.balance ?? 0,
3399
2641
  balanceLimit: entry.balanceLimit,
@@ -3402,9 +2644,9 @@ var hydrateStoreFromDriver = async (store, driver) => {
3402
2644
  }));
3403
2645
  const xcashuTokens = Object.fromEntries(
3404
2646
  Object.entries(rawXcashuTokens).map(([baseUrl, tokens]) => [
3405
- normalizeBaseUrl5(baseUrl),
2647
+ normalizeBaseUrl3(baseUrl),
3406
2648
  tokens.map((entry) => ({
3407
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
2649
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
3408
2650
  token: entry.token,
3409
2651
  createdAt: entry.createdAt ?? Date.now(),
3410
2652
  tryCount: entry.tryCount ?? 0
@@ -3425,16 +2667,16 @@ var hydrateStoreFromDriver = async (store, driver) => {
3425
2667
  lastUsed: entry.lastUsed ?? null
3426
2668
  }));
3427
2669
  const failedProviders = rawFailedProviders.map(
3428
- (url) => normalizeBaseUrl5(url)
2670
+ (url) => normalizeBaseUrl3(url)
3429
2671
  );
3430
2672
  const lastFailed = Object.fromEntries(
3431
2673
  Object.entries(rawLastFailed).map(([baseUrl, timestamp]) => [
3432
- normalizeBaseUrl5(baseUrl),
2674
+ normalizeBaseUrl3(baseUrl),
3433
2675
  timestamp
3434
2676
  ])
3435
2677
  );
3436
2678
  const providersOnCooldown = rawProvidersOnCooldown.map((entry) => ({
3437
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
2679
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
3438
2680
  timestamp: entry.timestamp
3439
2681
  }));
3440
2682
  store.setState({
@@ -3476,77 +2718,210 @@ var isBrowser2 = () => {
3476
2718
  return false;
3477
2719
  }
3478
2720
  };
3479
- var isNode = () => {
3480
- try {
3481
- return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
3482
- } catch {
3483
- return false;
3484
- }
3485
- };
3486
2721
  var defaultDriver = null;
3487
- var isBun3 = () => {
3488
- return typeof process.versions.bun !== "undefined";
3489
- };
3490
2722
  var getDefaultSdkDriver = () => {
3491
2723
  if (defaultDriver) return defaultDriver;
3492
2724
  if (isBrowser2()) {
3493
2725
  defaultDriver = localStorageDriver;
3494
2726
  return defaultDriver;
3495
2727
  }
3496
- if (isBun3()) {
3497
- defaultDriver = createMemoryDriver();
3498
- return defaultDriver;
2728
+ defaultDriver = createMemoryDriver();
2729
+ return defaultDriver;
2730
+ };
2731
+ var defaultStore = null;
2732
+ var defaultUsageTrackingDriver = null;
2733
+ var getDefaultSdkStore = () => {
2734
+ if (!defaultStore) {
2735
+ defaultStore = createSdkStore({ driver: getDefaultSdkDriver() });
2736
+ }
2737
+ return defaultStore.hydrate.then(() => defaultStore.store);
2738
+ };
2739
+ var getDefaultUsageTrackingDriver = () => {
2740
+ if (defaultUsageTrackingDriver) return defaultUsageTrackingDriver;
2741
+ const storageDriver = getDefaultSdkDriver();
2742
+ if (isBrowser2()) {
2743
+ defaultUsageTrackingDriver = createIndexedDBUsageTrackingDriver({
2744
+ legacyStorageDriver: storageDriver
2745
+ });
2746
+ return defaultUsageTrackingDriver;
2747
+ }
2748
+ defaultUsageTrackingDriver = createMemoryUsageTrackingDriver();
2749
+ return defaultUsageTrackingDriver;
2750
+ };
2751
+
2752
+ // client/usage.ts
2753
+ var numOrUndef = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
2754
+ function extractCostBreakdown(costObj) {
2755
+ if (!costObj || typeof costObj !== "object") return {};
2756
+ return {
2757
+ baseMsats: numOrUndef(costObj.base_msats),
2758
+ inputMsats: numOrUndef(costObj.input_msats),
2759
+ outputMsats: numOrUndef(costObj.output_msats),
2760
+ totalMsats: numOrUndef(costObj.total_msats),
2761
+ totalUsd: numOrUndef(costObj.total_usd),
2762
+ cacheReadInputTokens: numOrUndef(costObj.cache_read_input_tokens),
2763
+ cacheCreationInputTokens: numOrUndef(costObj.cache_creation_input_tokens),
2764
+ cacheReadMsats: numOrUndef(costObj.cache_read_msats),
2765
+ cacheCreationMsats: numOrUndef(costObj.cache_creation_msats),
2766
+ remainingBalanceMsats: numOrUndef(costObj.remaining_balance_msats)
2767
+ };
2768
+ }
2769
+ function extractUsageFromResponseBody(body, fallbackSatsCost = 0) {
2770
+ if (!body || typeof body !== "object") return null;
2771
+ const usage = body.usage;
2772
+ if (!usage || typeof usage !== "object") return null;
2773
+ const promptTokens = Number(usage.prompt_tokens ?? 0);
2774
+ const completionTokens = Number(usage.completion_tokens ?? 0);
2775
+ const totalTokens = Number(usage.total_tokens ?? 0);
2776
+ const costValue = usage.cost;
2777
+ let cost = 0;
2778
+ let satsCost = fallbackSatsCost;
2779
+ let breakdown = {};
2780
+ if (typeof costValue === "number") {
2781
+ cost = costValue;
2782
+ } else if (costValue && typeof costValue === "object") {
2783
+ const costObj = costValue;
2784
+ const totalUsd = costObj.total_usd;
2785
+ const totalMsats = costObj.total_msats;
2786
+ cost = typeof totalUsd === "number" ? totalUsd : 0;
2787
+ if (typeof totalMsats === "number") {
2788
+ satsCost = totalMsats / 1e3;
2789
+ }
2790
+ breakdown = extractCostBreakdown(costObj);
2791
+ }
2792
+ const provider = typeof body.provider === "string" ? body.provider : void 0;
2793
+ if (promptTokens === 0 && completionTokens === 0 && totalTokens === 0 && cost === 0 && satsCost === 0) {
2794
+ return null;
2795
+ }
2796
+ return {
2797
+ promptTokens,
2798
+ completionTokens,
2799
+ totalTokens,
2800
+ cost,
2801
+ satsCost,
2802
+ provider,
2803
+ ...breakdown
2804
+ };
2805
+ }
2806
+ function extractResponseId(body) {
2807
+ if (!body || typeof body !== "object") return void 0;
2808
+ const id = body.id;
2809
+ if (typeof id !== "string") return void 0;
2810
+ const trimmed = id.trim();
2811
+ return trimmed.length > 0 ? trimmed : void 0;
2812
+ }
2813
+ function extractUsageFromSSEJson(parsed, fallbackSatsCost = 0) {
2814
+ if (!parsed || typeof parsed !== "object") {
2815
+ return null;
2816
+ }
2817
+ const provider = typeof parsed.provider === "string" ? parsed.provider : void 0;
2818
+ if (!parsed.usage && parsed.cost && typeof parsed.cost === "object") {
2819
+ const costObj = parsed.cost;
2820
+ const msats2 = costObj.total_msats ?? 0;
2821
+ const cost2 = costObj.total_usd ?? 0;
2822
+ if (msats2 === 0 && cost2 === 0) return null;
2823
+ return {
2824
+ promptTokens: Number(costObj.input_tokens ?? 0),
2825
+ completionTokens: Number(costObj.output_tokens ?? 0),
2826
+ totalTokens: Number((costObj.input_tokens ?? 0) + (costObj.output_tokens ?? 0)),
2827
+ cost: Number(cost2),
2828
+ satsCost: msats2 > 0 ? msats2 / 1e3 : fallbackSatsCost,
2829
+ provider,
2830
+ ...extractCostBreakdown(costObj)
2831
+ };
3499
2832
  }
3500
- if (isNode()) {
3501
- defaultDriver = createSqliteDriver();
3502
- return defaultDriver;
2833
+ if (!parsed.usage) {
2834
+ return null;
3503
2835
  }
3504
- defaultDriver = createMemoryDriver();
3505
- return defaultDriver;
3506
- };
3507
- var defaultStore = null;
3508
- var defaultUsageTrackingDriver = null;
3509
- var getDefaultSdkStore = () => {
3510
- if (!defaultStore) {
3511
- defaultStore = createSdkStore({ driver: getDefaultSdkDriver() });
2836
+ const usage = parsed.usage;
2837
+ const usageCost = usage.cost;
2838
+ let cost = 0;
2839
+ let msats = 0;
2840
+ let breakdown = {};
2841
+ if (typeof usageCost === "number") {
2842
+ cost = usageCost;
2843
+ } else if (usageCost && typeof usageCost === "object") {
2844
+ cost = usageCost.total_usd ?? 0;
2845
+ msats = usageCost.total_msats ?? 0;
2846
+ breakdown = extractCostBreakdown(usageCost);
3512
2847
  }
3513
- return defaultStore.hydrate.then(() => defaultStore.store);
3514
- };
3515
- var getDefaultUsageTrackingDriver = () => {
3516
- if (defaultUsageTrackingDriver) return defaultUsageTrackingDriver;
3517
- const storageDriver = getDefaultSdkDriver();
3518
- if (isBrowser2()) {
3519
- defaultUsageTrackingDriver = createIndexedDBUsageTrackingDriver({
3520
- legacyStorageDriver: storageDriver
3521
- });
3522
- return defaultUsageTrackingDriver;
2848
+ const routstrCost = parsed.metadata?.routstr?.cost;
2849
+ if (routstrCost && typeof routstrCost === "object") {
2850
+ breakdown = { ...extractCostBreakdown(routstrCost), ...breakdown };
3523
2851
  }
3524
- if (isBun3()) {
3525
- defaultUsageTrackingDriver = createBunSqliteUsageTrackingDriver();
3526
- return defaultUsageTrackingDriver;
2852
+ if (cost === 0) {
2853
+ cost = parsed.metadata?.routstr?.cost?.total_usd ?? 0;
3527
2854
  }
3528
- if (isNode()) {
3529
- defaultUsageTrackingDriver = createSqliteUsageTrackingDriver({
3530
- legacyStorageDriver: storageDriver
3531
- });
3532
- return defaultUsageTrackingDriver;
2855
+ if (msats === 0) {
2856
+ msats = parsed.metadata?.routstr?.cost?.total_msats ?? (typeof usage.cost_sats === "number" ? usage.cost_sats * 1e3 : 0);
3533
2857
  }
3534
- defaultUsageTrackingDriver = createMemoryUsageTrackingDriver();
3535
- return defaultUsageTrackingDriver;
3536
- };
2858
+ const promptTokens = Number(usage.prompt_tokens ?? usage.input_tokens ?? 0);
2859
+ const completionTokens = Number(usage.completion_tokens ?? usage.output_tokens ?? 0);
2860
+ const totalTokens = Number(usage.total_tokens ?? promptTokens + completionTokens);
2861
+ const result = {
2862
+ promptTokens,
2863
+ completionTokens,
2864
+ totalTokens,
2865
+ cost: Number(cost ?? 0),
2866
+ satsCost: msats > 0 ? msats / 1e3 : fallbackSatsCost,
2867
+ provider,
2868
+ ...breakdown
2869
+ };
2870
+ if (result.promptTokens === 0 && result.completionTokens === 0 && result.totalTokens === 0 && result.cost === 0 && result.satsCost === 0) {
2871
+ return null;
2872
+ }
2873
+ return result;
2874
+ }
2875
+ function toUsageStats(usage) {
2876
+ if (!usage) return void 0;
2877
+ return {
2878
+ total_tokens: usage.totalTokens,
2879
+ prompt_tokens: usage.promptTokens,
2880
+ completion_tokens: usage.completionTokens,
2881
+ cost: usage.cost,
2882
+ sats_cost: usage.satsCost
2883
+ };
2884
+ }
3537
2885
  function mergeUsage(previous, next) {
3538
2886
  if (!previous) return next;
2887
+ const pickNum = (n, p) => typeof n === "number" && n > 0 ? n : p ?? n;
3539
2888
  return {
3540
2889
  promptTokens: next.promptTokens > 0 ? next.promptTokens : previous.promptTokens,
3541
2890
  completionTokens: next.completionTokens > 0 ? next.completionTokens : previous.completionTokens,
3542
2891
  totalTokens: next.totalTokens > 0 ? next.totalTokens : previous.totalTokens,
3543
2892
  cost: next.cost > 0 ? next.cost : previous.cost,
3544
- satsCost: next.satsCost > 0 ? next.satsCost : previous.satsCost
2893
+ satsCost: next.satsCost > 0 ? next.satsCost : previous.satsCost,
2894
+ provider: next.provider ?? previous.provider,
2895
+ baseMsats: pickNum(next.baseMsats, previous.baseMsats),
2896
+ inputMsats: pickNum(next.inputMsats, previous.inputMsats),
2897
+ outputMsats: pickNum(next.outputMsats, previous.outputMsats),
2898
+ totalMsats: pickNum(next.totalMsats, previous.totalMsats),
2899
+ totalUsd: pickNum(next.totalUsd, previous.totalUsd),
2900
+ cacheReadInputTokens: pickNum(
2901
+ next.cacheReadInputTokens,
2902
+ previous.cacheReadInputTokens
2903
+ ),
2904
+ cacheCreationInputTokens: pickNum(
2905
+ next.cacheCreationInputTokens,
2906
+ previous.cacheCreationInputTokens
2907
+ ),
2908
+ cacheReadMsats: pickNum(next.cacheReadMsats, previous.cacheReadMsats),
2909
+ cacheCreationMsats: pickNum(
2910
+ next.cacheCreationMsats,
2911
+ previous.cacheCreationMsats
2912
+ ),
2913
+ remainingBalanceMsats: pickNum(
2914
+ next.remainingBalanceMsats,
2915
+ previous.remainingBalanceMsats
2916
+ )
3545
2917
  };
3546
2918
  }
3547
2919
  function hasUsageChanged(previous, next) {
3548
2920
  if (!previous) return true;
3549
- return previous.promptTokens !== next.promptTokens || previous.completionTokens !== next.completionTokens || previous.totalTokens !== next.totalTokens || previous.cost !== next.cost || previous.satsCost !== next.satsCost;
2921
+ return previous.promptTokens !== next.promptTokens || previous.completionTokens !== next.completionTokens || previous.totalTokens !== next.totalTokens || previous.cost !== next.cost || previous.satsCost !== next.satsCost || previous.provider !== next.provider || previous.totalMsats !== next.totalMsats || previous.remainingBalanceMsats !== next.remainingBalanceMsats;
2922
+ }
2923
+ function isInspectionComplete(responseIdCaptured, usage) {
2924
+ return responseIdCaptured && !!usage && usage.totalTokens > 0 && typeof usage.totalMsats === "number" && !!usage.provider;
3550
2925
  }
3551
2926
  async function inspectSSEWebStream(stream, onUsage, onResponseId) {
3552
2927
  const reader = stream.getReader();
@@ -3556,14 +2931,22 @@ async function inspectSSEWebStream(stream, onUsage, onResponseId) {
3556
2931
  let capturedResponseId;
3557
2932
  let responseIdCaptured = false;
3558
2933
  const inspectDataPayload = (jsonText) => {
3559
- if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
2934
+ const trimmed = jsonText.trim();
2935
+ if (!trimmed || trimmed === "[DONE]") {
2936
+ if (trimmed === "[DONE]") console.log("[routstr:sse] [DONE]");
2937
+ return;
2938
+ }
2939
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
2940
+ console.log("[routstr:sse] non-JSON payload:", trimmed.slice(0, 200));
3560
2941
  return;
3561
2942
  }
3562
- const trimmed = jsonText.trim();
3563
- if (!trimmed || trimmed === "[DONE]") return;
3564
- if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return;
3565
2943
  try {
3566
2944
  const data = JSON.parse(trimmed);
2945
+ console.log("[routstr:sse] chunk:", JSON.stringify(data));
2946
+ if (isInspectionComplete(responseIdCaptured, capturedUsage)) {
2947
+ console.log("[routstr:sse] (inspection already complete, skipping)");
2948
+ return;
2949
+ }
3567
2950
  if (!responseIdCaptured) {
3568
2951
  const responseId = data?.id;
3569
2952
  if (typeof responseId === "string" && responseId.trim().length > 0) {
@@ -3574,19 +2957,21 @@ async function inspectSSEWebStream(stream, onUsage, onResponseId) {
3574
2957
  }
3575
2958
  const usage = extractUsageFromSSEJson(data);
3576
2959
  if (usage) {
2960
+ console.log("[routstr:sse] \u2192 usage detected:", usage);
3577
2961
  const merged = mergeUsage(capturedUsage, usage);
3578
2962
  if (hasUsageChanged(capturedUsage, merged)) {
3579
2963
  capturedUsage = merged;
2964
+ console.log("[routstr:sse] \u2192 merged (changed):", merged);
3580
2965
  onUsage(merged);
2966
+ } else {
2967
+ console.log("[routstr:sse] \u2192 merged (no change)");
3581
2968
  }
3582
2969
  }
3583
2970
  } catch {
2971
+ console.log("[routstr:sse] failed to parse payload:", trimmed.slice(0, 200));
3584
2972
  }
3585
2973
  };
3586
2974
  const inspectEventBlock = (eventBlock) => {
3587
- if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
3588
- return;
3589
- }
3590
2975
  const lines = eventBlock.split(/\r?\n/);
3591
2976
  const dataParts = [];
3592
2977
  for (const line of lines) {
@@ -3644,14 +3029,22 @@ function createSSEParserTransform(onUsage, onResponseId) {
3644
3029
  let capturedUsage = null;
3645
3030
  let responseIdCaptured = false;
3646
3031
  const inspectDataPayload = (jsonText) => {
3647
- if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
3032
+ const trimmed = jsonText.trim();
3033
+ if (!trimmed || trimmed === "[DONE]") {
3034
+ if (trimmed === "[DONE]") console.log("[routstr:sse] [DONE]");
3035
+ return;
3036
+ }
3037
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
3038
+ console.log("[routstr:sse] non-JSON payload:", trimmed.slice(0, 200));
3648
3039
  return;
3649
3040
  }
3650
- const trimmed = jsonText.trim();
3651
- if (!trimmed || trimmed === "[DONE]") return;
3652
- if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return;
3653
3041
  try {
3654
3042
  const data = JSON.parse(trimmed);
3043
+ console.log("[routstr:sse] chunk:", JSON.stringify(data));
3044
+ if (isInspectionComplete(responseIdCaptured, capturedUsage)) {
3045
+ console.log("[routstr:sse] (inspection already complete, skipping)");
3046
+ return;
3047
+ }
3655
3048
  if (!responseIdCaptured) {
3656
3049
  const responseId = data?.id;
3657
3050
  if (typeof responseId === "string" && responseId.trim().length > 0) {
@@ -3661,19 +3054,21 @@ function createSSEParserTransform(onUsage, onResponseId) {
3661
3054
  }
3662
3055
  const usage = extractUsageFromSSEJson(data);
3663
3056
  if (usage) {
3057
+ console.log("[routstr:sse] \u2192 usage detected:", usage);
3664
3058
  const mergedUsage = mergeUsage(capturedUsage, usage);
3665
3059
  if (hasUsageChanged(capturedUsage, mergedUsage)) {
3666
3060
  capturedUsage = mergedUsage;
3061
+ console.log("[routstr:sse] \u2192 merged (changed):", mergedUsage);
3667
3062
  onUsage(mergedUsage);
3063
+ } else {
3064
+ console.log("[routstr:sse] \u2192 merged (no change)");
3668
3065
  }
3669
3066
  }
3670
3067
  } catch {
3068
+ console.log("[routstr:sse] failed to parse payload:", trimmed.slice(0, 200));
3671
3069
  }
3672
3070
  };
3673
3071
  const inspectEventBlock = (eventBlock) => {
3674
- if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
3675
- return;
3676
- }
3677
3072
  const lines = eventBlock.split(/\r?\n/);
3678
3073
  const dataParts = [];
3679
3074
  for (const line of lines) {
@@ -3746,7 +3141,6 @@ var RoutstrClient = class {
3746
3141
  this.balanceManager,
3747
3142
  this.logger
3748
3143
  );
3749
- this.streamProcessor = new StreamProcessor();
3750
3144
  this.alertLevel = alertLevel;
3751
3145
  this.mode = mode;
3752
3146
  this.usageTrackingDriver = options.usageTrackingDriver;
@@ -3758,7 +3152,6 @@ var RoutstrClient = class {
3758
3152
  providerRegistry;
3759
3153
  cashuSpender;
3760
3154
  balanceManager;
3761
- streamProcessor;
3762
3155
  providerManager;
3763
3156
  alertLevel;
3764
3157
  mode;
@@ -4000,153 +3393,6 @@ var RoutstrClient = class {
4000
3393
  }
4001
3394
  return void 0;
4002
3395
  }
4003
- /**
4004
- * Fetch AI response with streaming
4005
- */
4006
- async fetchAIResponse(options, callbacks) {
4007
- const {
4008
- messageHistory,
4009
- selectedModel,
4010
- baseUrl,
4011
- mintUrl,
4012
- balance,
4013
- transactionHistory,
4014
- maxTokens,
4015
- headers
4016
- } = options;
4017
- const apiMessages = await this._convertMessages(messageHistory);
4018
- const requiredSats = this.providerManager.getRequiredSatsForModel(
4019
- selectedModel,
4020
- apiMessages,
4021
- maxTokens
4022
- );
4023
- try {
4024
- await this._checkBalance();
4025
- callbacks.onPaymentProcessing?.(true);
4026
- const spendResult = await this._spendToken({
4027
- mintUrl,
4028
- amount: requiredSats,
4029
- baseUrl
4030
- });
4031
- let token = spendResult.token;
4032
- let tokenBalance = spendResult.tokenBalance;
4033
- let tokenBalanceUnit = spendResult.tokenBalanceUnit;
4034
- let tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
4035
- let initialTokenBalanceUnknown = spendResult.tokenBalanceUnknown;
4036
- callbacks.onTokenCreated?.(this._getPendingCashuTokenAmount());
4037
- const baseHeaders = this._buildBaseHeaders(headers);
4038
- const requestHeaders = this._withAuthHeader(baseHeaders, token);
4039
- const providerInfo = await this.providerRegistry.getProviderInfo(baseUrl);
4040
- const providerVersion = providerInfo?.version ?? "";
4041
- let modelIdForRequest = selectedModel.id;
4042
- if (/^0\.1\./.test(providerVersion)) {
4043
- const newModel = await this.providerManager.getModelForProvider(
4044
- baseUrl,
4045
- selectedModel.id
4046
- );
4047
- modelIdForRequest = newModel?.id ?? selectedModel.id;
4048
- }
4049
- const body = {
4050
- model: modelIdForRequest,
4051
- messages: apiMessages,
4052
- stream: true
4053
- };
4054
- if (maxTokens !== void 0) {
4055
- body.max_tokens = maxTokens;
4056
- }
4057
- if (selectedModel?.name?.startsWith("OpenAI:")) {
4058
- body.tools = [{ type: "web_search" }];
4059
- }
4060
- const response = await this._makeRequest({
4061
- path: "/v1/chat/completions",
4062
- method: "POST",
4063
- body,
4064
- selectedModel,
4065
- baseUrl,
4066
- mintUrl,
4067
- token,
4068
- requiredSats,
4069
- maxTokens,
4070
- headers: requestHeaders,
4071
- baseHeaders
4072
- });
4073
- if (!response.body) {
4074
- throw new Error("Response body is not available");
4075
- }
4076
- if (response.status === 200) {
4077
- const baseUrlUsed = response.baseUrl || baseUrl;
4078
- const responseToken = response.token || token;
4079
- if (baseUrlUsed !== baseUrl || responseToken !== token) {
4080
- token = responseToken;
4081
- if (typeof response.initialTokenBalanceInSats === "number") {
4082
- tokenBalanceInSats = response.initialTokenBalanceInSats;
4083
- initialTokenBalanceUnknown = Boolean(
4084
- response.initialTokenBalanceUnknown
4085
- );
4086
- } else {
4087
- initialTokenBalanceUnknown = true;
4088
- }
4089
- }
4090
- const streamingResult = await this.streamProcessor.process(
4091
- response,
4092
- {
4093
- onContent: callbacks.onStreamingUpdate,
4094
- onThinking: callbacks.onThinkingUpdate
4095
- },
4096
- selectedModel.id
4097
- );
4098
- if (streamingResult.finish_reason === "content_filter") {
4099
- callbacks.onMessageAppend({
4100
- role: "assistant",
4101
- content: "Your request was denied due to content filtering."
4102
- });
4103
- } else if (streamingResult.content || streamingResult.images && streamingResult.images.length > 0) {
4104
- const message = await this._createAssistantMessage(streamingResult);
4105
- callbacks.onMessageAppend(message);
4106
- } else {
4107
- callbacks.onMessageAppend({
4108
- role: "system",
4109
- content: "The provider did not respond to this request."
4110
- });
4111
- }
4112
- callbacks.onStreamingUpdate("");
4113
- callbacks.onThinkingUpdate("");
4114
- const isApikeysEstimate = this.mode === "apikeys";
4115
- let satsSpent = await this._handlePostResponseBalanceUpdate({
4116
- token,
4117
- baseUrl: baseUrlUsed,
4118
- mintUrl,
4119
- initialTokenBalance: tokenBalanceInSats,
4120
- initialTokenBalanceUnknown,
4121
- fallbackSatsSpent: isApikeysEstimate ? this._getEstimatedCosts(selectedModel, streamingResult) : void 0,
4122
- response,
4123
- modelId: selectedModel.id,
4124
- usage: streamingResult.usage ? {
4125
- promptTokens: Number(streamingResult.usage.prompt_tokens ?? 0),
4126
- completionTokens: Number(
4127
- streamingResult.usage.completion_tokens ?? 0
4128
- ),
4129
- totalTokens: Number(streamingResult.usage.total_tokens ?? 0),
4130
- cost: Number(streamingResult.usage.cost ?? 0),
4131
- satsCost: Number(streamingResult.usage.sats_cost ?? 0)
4132
- } : void 0,
4133
- requestId: streamingResult.responseId
4134
- });
4135
- const estimatedCosts = this._getEstimatedCosts(
4136
- selectedModel,
4137
- streamingResult
4138
- );
4139
- const onLastMessageSatsUpdate = callbacks.onLastMessageSatsUpdate;
4140
- onLastMessageSatsUpdate?.(satsSpent, estimatedCosts);
4141
- } else {
4142
- throw new Error(`${response.status} ${response.statusText}`);
4143
- }
4144
- } catch (error) {
4145
- this._handleError(error, callbacks);
4146
- } finally {
4147
- callbacks.onPaymentProcessing?.(false);
4148
- }
4149
- }
4150
3396
  /**
4151
3397
  * Make the API request with failover support
4152
3398
  */
@@ -4635,304 +3881,568 @@ var RoutstrClient = class {
4635
3881
  if (!usage) {
4636
3882
  return;
4637
3883
  }
4638
- } else {
4639
- const cloned = response.clone();
4640
- const responseBody = await cloned.json();
4641
- usage = usage ?? extractUsageFromResponseBody(responseBody, satsSpent) ?? void 0;
4642
- requestId = requestId ?? extractResponseId(responseBody) ?? response.headers.get("x-routstr-request-id") ?? void 0;
3884
+ } else {
3885
+ const cloned = response.clone();
3886
+ const responseBody = await cloned.json();
3887
+ usage = usage ?? extractUsageFromResponseBody(responseBody, satsSpent) ?? void 0;
3888
+ requestId = requestId ?? extractResponseId(responseBody) ?? response.headers.get("x-routstr-request-id") ?? void 0;
3889
+ }
3890
+ }
3891
+ if (!usage) {
3892
+ return;
3893
+ }
3894
+ const finalRequestId = requestId || "unknown";
3895
+ const store = this.sdkStore ?? await getDefaultSdkStore();
3896
+ const state = store.getState();
3897
+ const matchKey = clientApiKey ?? token;
3898
+ const matchingClient = state.clientIds.find(
3899
+ (client) => client.apiKey === matchKey
3900
+ );
3901
+ const entryId = finalRequestId === "unknown" ? `req-${Date.now()}-${modelId}` : finalRequestId;
3902
+ const usageTracking = this.usageTrackingDriver ?? getDefaultUsageTrackingDriver();
3903
+ const entry = {
3904
+ id: entryId,
3905
+ timestamp: Date.now(),
3906
+ modelId,
3907
+ baseUrl,
3908
+ requestId: finalRequestId,
3909
+ client: matchingClient?.clientId,
3910
+ ...usage
3911
+ };
3912
+ if (this.mode === "xcashu") {
3913
+ entry.satsCost = satsSpent;
3914
+ }
3915
+ await usageTracking.append(entry);
3916
+ } catch (error) {
3917
+ }
3918
+ }
3919
+ /**
3920
+ * Check wallet balance and throw if insufficient
3921
+ */
3922
+ async _checkBalance() {
3923
+ const balances = await this.walletAdapter.getBalances();
3924
+ const totalBalance = Object.values(balances).reduce((sum, v) => sum + v, 0);
3925
+ if (totalBalance <= 0) {
3926
+ throw new InsufficientBalanceError(1, 0);
3927
+ }
3928
+ }
3929
+ /**
3930
+ * Spend a token using CashuSpender with standardized error handling
3931
+ */
3932
+ async _spendToken(params) {
3933
+ const { mintUrl, amount, baseUrl } = params;
3934
+ this._log(
3935
+ "DEBUG",
3936
+ `[RoutstrClient] _spendToken: mode=${this.mode}, amount=${amount}, baseUrl=${baseUrl}, mintUrl=${mintUrl}`
3937
+ );
3938
+ if (this.mode === "apikeys") {
3939
+ let parentApiKey = this.storageAdapter.getApiKey(baseUrl);
3940
+ if (!parentApiKey) {
3941
+ this._log(
3942
+ "DEBUG",
3943
+ `[RoutstrClient] _spendToken: No existing API key for ${baseUrl}, creating new one via Cashu`
3944
+ );
3945
+ const spendResult2 = await this.cashuSpender.spend({
3946
+ mintUrl,
3947
+ amount: amount * TOPUP_MARGIN,
3948
+ baseUrl: "",
3949
+ reuseToken: false
3950
+ });
3951
+ if (!spendResult2.token) {
3952
+ this._log(
3953
+ "ERROR",
3954
+ `[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error:`,
3955
+ spendResult2.error
3956
+ );
3957
+ throw new Error(
3958
+ `[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error: ${spendResult2.error}`
3959
+ );
3960
+ } else {
3961
+ this._log(
3962
+ "DEBUG",
3963
+ `[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult2.token}`
3964
+ );
3965
+ }
3966
+ this._log(
3967
+ "DEBUG",
3968
+ `[RoutstrClient] _spendToken: Created API key for ${baseUrl}, key preview: ${spendResult2.token}, balance: ${spendResult2.balance}`
3969
+ );
3970
+ try {
3971
+ this.storageAdapter.setApiKey(baseUrl, spendResult2.token);
3972
+ } catch (error) {
3973
+ if (error instanceof Error && error.message.includes("ApiKey already exists")) {
3974
+ const receiveResult = await this.cashuSpender.receiveToken(
3975
+ spendResult2.token
3976
+ );
3977
+ if (receiveResult.success) {
3978
+ this._log(
3979
+ "DEBUG",
3980
+ `[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${receiveResult.amount}`
3981
+ );
3982
+ } else {
3983
+ this._log(
3984
+ "DEBUG",
3985
+ `[RoutstrClient] _handleErrorResponse: Token restore failed: ${receiveResult.message}`
3986
+ );
3987
+ }
3988
+ this._log(
3989
+ "DEBUG",
3990
+ `[RoutstrClient] _spendToken: API key already exists for ${baseUrl}, using existing key`
3991
+ );
3992
+ } else {
3993
+ throw error;
3994
+ }
4643
3995
  }
3996
+ parentApiKey = this.storageAdapter.getApiKey(baseUrl);
3997
+ } else {
3998
+ this._log(
3999
+ "DEBUG",
4000
+ `[RoutstrClient] _spendToken: Using existing API key for ${baseUrl}, key preview: ${parentApiKey.key}`
4001
+ );
4644
4002
  }
4645
- if (!usage) {
4646
- return;
4003
+ let tokenBalance = 0;
4004
+ let tokenBalanceUnit = "sat";
4005
+ let tokenBalanceUnknown = false;
4006
+ const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
4007
+ const distributionForBaseUrl = apiKeyDistribution.find(
4008
+ (d) => d.baseUrl === baseUrl
4009
+ );
4010
+ if (distributionForBaseUrl) {
4011
+ tokenBalance = distributionForBaseUrl.amount;
4647
4012
  }
4648
- const finalRequestId = requestId || "unknown";
4649
- const store = this.sdkStore ?? await getDefaultSdkStore();
4650
- const state = store.getState();
4651
- const matchKey = clientApiKey ?? token;
4652
- const matchingClient = state.clientIds.find(
4653
- (client) => client.apiKey === matchKey
4013
+ if (tokenBalance === 0 && parentApiKey) {
4014
+ try {
4015
+ const balanceInfo = await this.balanceManager.getTokenBalance(
4016
+ parentApiKey.key,
4017
+ baseUrl
4018
+ );
4019
+ tokenBalance = balanceInfo.amount;
4020
+ tokenBalanceUnit = balanceInfo.unit;
4021
+ tokenBalanceUnknown = Boolean(balanceInfo.balanceUnknown);
4022
+ } catch (e) {
4023
+ this._log("WARN", "Could not get initial API key balance:", e);
4024
+ }
4025
+ }
4026
+ this._log(
4027
+ "DEBUG",
4028
+ `[RoutstrClient] _spendToken: Returning token with balance=${tokenBalance} ${tokenBalanceUnit}`
4654
4029
  );
4655
- const entryId = finalRequestId === "unknown" ? `req-${Date.now()}-${modelId}` : finalRequestId;
4656
- const usageTracking = this.usageTrackingDriver ?? getDefaultUsageTrackingDriver();
4657
- const entry = {
4658
- id: entryId,
4659
- timestamp: Date.now(),
4660
- modelId,
4661
- baseUrl,
4662
- requestId: finalRequestId,
4663
- client: matchingClient?.clientId,
4664
- ...usage
4030
+ return {
4031
+ token: parentApiKey?.key ?? "",
4032
+ tokenBalance,
4033
+ tokenBalanceUnit,
4034
+ tokenBalanceUnknown
4665
4035
  };
4666
- if (this.mode === "xcashu") {
4667
- entry.satsCost = satsSpent;
4668
- }
4669
- await usageTracking.append(entry);
4670
- } catch (error) {
4671
4036
  }
4037
+ this._log(
4038
+ "DEBUG",
4039
+ `[RoutstrClient] _spendToken: Calling CashuSpender.spend for amount=${amount}, mintUrl=${mintUrl}, mode=${this.mode}`
4040
+ );
4041
+ const spendResult = await this.cashuSpender.spend({
4042
+ mintUrl,
4043
+ amount,
4044
+ baseUrl: "",
4045
+ reuseToken: false
4046
+ });
4047
+ if (!spendResult.token) {
4048
+ this._log(
4049
+ "ERROR",
4050
+ `[RoutstrClient] _spendToken: CashuSpender.spend failed, error:`,
4051
+ spendResult.error
4052
+ );
4053
+ } else {
4054
+ this._log(
4055
+ "DEBUG",
4056
+ `[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult.token}, balance: ${spendResult.balance} ${spendResult.unit ?? "sat"}`
4057
+ );
4058
+ this.storageAdapter.addXcashuToken(baseUrl, spendResult.token);
4059
+ }
4060
+ return {
4061
+ token: spendResult.token,
4062
+ tokenBalance: spendResult.balance,
4063
+ tokenBalanceUnit: spendResult.unit ?? "sat",
4064
+ tokenBalanceUnknown: false
4065
+ };
4672
4066
  }
4673
4067
  /**
4674
- * Convert messages for API format
4068
+ * Build request headers with common defaults and dev mock controls
4675
4069
  */
4676
- async _convertMessages(messages) {
4677
- return Promise.all(
4678
- messages.filter((m) => m.role !== "system").map(async (m) => ({
4679
- role: m.role,
4680
- content: typeof m.content === "string" ? m.content : m.content
4681
- }))
4682
- );
4070
+ _buildBaseHeaders(additionalHeaders = {}, token) {
4071
+ const headers = {
4072
+ ...additionalHeaders,
4073
+ "Content-Type": "application/json"
4074
+ };
4075
+ return headers;
4683
4076
  }
4684
4077
  /**
4685
- * Create assistant message from streaming result
4078
+ * Attach auth headers using the active client mode
4686
4079
  */
4687
- async _createAssistantMessage(result) {
4688
- if (result.images && result.images.length > 0) {
4689
- const content = [];
4690
- if (result.content) {
4691
- content.push({
4692
- type: "text",
4693
- text: result.content,
4694
- thinking: result.thinking,
4695
- citations: result.citations,
4696
- annotations: result.annotations
4697
- });
4698
- }
4699
- for (const img of result.images) {
4700
- content.push({
4701
- type: "image_url",
4702
- image_url: {
4703
- url: img.image_url.url
4704
- }
4705
- });
4706
- }
4707
- return {
4708
- role: "assistant",
4709
- content
4710
- };
4080
+ _withAuthHeader(headers, token) {
4081
+ const nextHeaders = { ...headers };
4082
+ if (this.mode === "xcashu") {
4083
+ nextHeaders["X-Cashu"] = token;
4084
+ } else {
4085
+ nextHeaders["Authorization"] = `Bearer ${token}`;
4711
4086
  }
4712
- return {
4713
- role: "assistant",
4714
- content: result.content || ""
4715
- };
4087
+ return nextHeaders;
4716
4088
  }
4089
+ };
4090
+
4091
+ // client/StreamProcessor.ts
4092
+ var StreamProcessor = class {
4093
+ accumulatedContent = "";
4094
+ accumulatedThinking = "";
4095
+ accumulatedImages = [];
4096
+ isInThinking = false;
4097
+ isInContent = false;
4717
4098
  /**
4718
- * Calculate estimated costs from usage
4099
+ * Process a streaming response
4719
4100
  */
4720
- _getEstimatedCosts(selectedModel, streamingResult) {
4721
- let estimatedCosts = 0;
4722
- if (streamingResult.usage) {
4723
- const { completion_tokens, prompt_tokens } = streamingResult.usage;
4724
- if (completion_tokens !== void 0 && prompt_tokens !== void 0) {
4725
- estimatedCosts = (selectedModel.sats_pricing?.completion ?? 0) * completion_tokens + (selectedModel.sats_pricing?.prompt ?? 0) * prompt_tokens;
4101
+ async process(response, callbacks, modelId) {
4102
+ if (!response.body) {
4103
+ throw new Error("Response body is not available");
4104
+ }
4105
+ const reader = response.body.getReader();
4106
+ const decoder = new TextDecoder("utf-8");
4107
+ let buffer = "";
4108
+ this.accumulatedContent = "";
4109
+ this.accumulatedThinking = "";
4110
+ this.accumulatedImages = [];
4111
+ this.isInThinking = false;
4112
+ this.isInContent = false;
4113
+ let usage;
4114
+ let model;
4115
+ let finish_reason;
4116
+ let citations;
4117
+ let annotations;
4118
+ let responseId;
4119
+ try {
4120
+ while (true) {
4121
+ const { done, value } = await reader.read();
4122
+ if (done) {
4123
+ break;
4124
+ }
4125
+ const chunk = decoder.decode(value, { stream: true });
4126
+ buffer += chunk;
4127
+ const lines = buffer.split("\n");
4128
+ buffer = lines.pop() || "";
4129
+ for (const line of lines) {
4130
+ const parsed = this._parseLine(line);
4131
+ if (!parsed) continue;
4132
+ if (parsed.content) {
4133
+ this._handleContent(parsed.content, callbacks, modelId);
4134
+ }
4135
+ if (parsed.reasoning) {
4136
+ this._handleThinking(parsed.reasoning, callbacks);
4137
+ }
4138
+ if (parsed.usage) {
4139
+ usage = parsed.usage;
4140
+ }
4141
+ if (parsed.model) {
4142
+ model = parsed.model;
4143
+ }
4144
+ if (parsed.finish_reason) {
4145
+ finish_reason = parsed.finish_reason;
4146
+ }
4147
+ if (parsed.responseId) {
4148
+ responseId = parsed.responseId;
4149
+ }
4150
+ if (parsed.citations) {
4151
+ citations = parsed.citations;
4152
+ }
4153
+ if (parsed.annotations) {
4154
+ annotations = parsed.annotations;
4155
+ }
4156
+ if (parsed.images) {
4157
+ this._mergeImages(parsed.images);
4158
+ }
4159
+ }
4726
4160
  }
4161
+ } finally {
4162
+ reader.releaseLock();
4727
4163
  }
4728
- return estimatedCosts;
4164
+ return {
4165
+ content: this.accumulatedContent,
4166
+ thinking: this.accumulatedThinking || void 0,
4167
+ images: this.accumulatedImages.length > 0 ? this.accumulatedImages : void 0,
4168
+ usage,
4169
+ model,
4170
+ responseId,
4171
+ finish_reason,
4172
+ citations,
4173
+ annotations
4174
+ };
4729
4175
  }
4730
4176
  /**
4731
- * Get pending API key amount
4177
+ * Parse a single SSE line
4732
4178
  */
4733
- _getPendingCashuTokenAmount() {
4734
- const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
4735
- return apiKeyDistribution.reduce((total, item) => total + item.amount, 0);
4179
+ _parseLine(line) {
4180
+ if (!line.trim()) return null;
4181
+ if (!line.startsWith("data: ")) {
4182
+ return null;
4183
+ }
4184
+ const jsonData = line.slice(6);
4185
+ if (jsonData === "[DONE]") {
4186
+ return null;
4187
+ }
4188
+ try {
4189
+ const parsed = JSON.parse(jsonData);
4190
+ const result = {};
4191
+ if (parsed.choices?.[0]?.delta?.content) {
4192
+ result.content = parsed.choices[0].delta.content;
4193
+ }
4194
+ if (parsed.choices?.[0]?.delta?.reasoning) {
4195
+ result.reasoning = parsed.choices[0].delta.reasoning;
4196
+ }
4197
+ const extractedUsage = extractUsageFromSSEJson(parsed);
4198
+ if (extractedUsage) {
4199
+ result.usage = toUsageStats(extractedUsage);
4200
+ } else if (parsed.usage) {
4201
+ result.usage = {
4202
+ total_tokens: parsed.usage.total_tokens ?? parsed.usage.input_tokens + parsed.usage.output_tokens,
4203
+ prompt_tokens: parsed.usage.prompt_tokens ?? parsed.usage.input_tokens,
4204
+ completion_tokens: parsed.usage.completion_tokens ?? parsed.usage.output_tokens
4205
+ };
4206
+ }
4207
+ if (parsed.id) {
4208
+ result.responseId = parsed.id;
4209
+ }
4210
+ if (parsed.model) {
4211
+ result.model = parsed.model;
4212
+ }
4213
+ if (parsed.citations) {
4214
+ result.citations = parsed.citations;
4215
+ }
4216
+ if (parsed.annotations) {
4217
+ result.annotations = parsed.annotations;
4218
+ }
4219
+ if (parsed.choices?.[0]?.finish_reason) {
4220
+ result.finish_reason = parsed.choices[0].finish_reason;
4221
+ }
4222
+ const images = parsed.choices?.[0]?.message?.images || parsed.choices?.[0]?.delta?.images;
4223
+ if (images && Array.isArray(images)) {
4224
+ result.images = images;
4225
+ }
4226
+ return result;
4227
+ } catch {
4228
+ return null;
4229
+ }
4736
4230
  }
4737
4231
  /**
4738
- * Handle errors and notify callbacks
4232
+ * Handle content delta with thinking support
4739
4233
  */
4740
- _handleError(error, callbacks) {
4741
- this._log("ERROR", "[RoutstrClient] _handleError: Error occurred", error);
4742
- if (error instanceof Error) {
4743
- const isStreamError = error.message.includes("Error in input stream") || error.message.includes("Load failed");
4744
- const modifiedErrorMsg = isStreamError ? "AI stream was cut off, turn on Keep Active or please try again" : error.message;
4745
- this._log(
4746
- "ERROR",
4747
- `[RoutstrClient] _handleError: Error type=${error.constructor.name}, message=${modifiedErrorMsg}, isStreamError=${isStreamError}`
4748
- );
4749
- callbacks.onMessageAppend({
4750
- role: "system",
4751
- content: "Uncaught Error: " + modifiedErrorMsg + (this.alertLevel === "max" ? " | " + error.stack : "")
4752
- });
4234
+ _handleContent(content, callbacks, modelId) {
4235
+ if (this.isInThinking && !this.isInContent) {
4236
+ this.accumulatedThinking += "</thinking>";
4237
+ callbacks.onThinking(this.accumulatedThinking);
4238
+ this.isInThinking = false;
4239
+ this.isInContent = true;
4240
+ }
4241
+ if (modelId) {
4242
+ this._extractThinkingFromContent(content, callbacks);
4753
4243
  } else {
4754
- callbacks.onMessageAppend({
4755
- role: "system",
4756
- content: "Unknown Error: Please tag Routstr on Nostr and/or retry."
4757
- });
4244
+ this.accumulatedContent += content;
4758
4245
  }
4246
+ callbacks.onContent(this.accumulatedContent);
4759
4247
  }
4760
4248
  /**
4761
- * Check wallet balance and throw if insufficient
4249
+ * Handle thinking/reasoning content
4762
4250
  */
4763
- async _checkBalance() {
4764
- const balances = await this.walletAdapter.getBalances();
4765
- const totalBalance = Object.values(balances).reduce((sum, v) => sum + v, 0);
4766
- if (totalBalance <= 0) {
4767
- throw new InsufficientBalanceError(1, 0);
4251
+ _handleThinking(reasoning, callbacks) {
4252
+ if (!this.isInThinking) {
4253
+ this.accumulatedThinking += "<thinking> ";
4254
+ this.isInThinking = true;
4768
4255
  }
4256
+ this.accumulatedThinking += reasoning;
4257
+ callbacks.onThinking(this.accumulatedThinking);
4769
4258
  }
4770
4259
  /**
4771
- * Spend a token using CashuSpender with standardized error handling
4260
+ * Extract thinking blocks from content (for models with inline thinking)
4772
4261
  */
4773
- async _spendToken(params) {
4774
- const { mintUrl, amount, baseUrl } = params;
4775
- this._log(
4776
- "DEBUG",
4777
- `[RoutstrClient] _spendToken: mode=${this.mode}, amount=${amount}, baseUrl=${baseUrl}, mintUrl=${mintUrl}`
4778
- );
4779
- if (this.mode === "apikeys") {
4780
- let parentApiKey = this.storageAdapter.getApiKey(baseUrl);
4781
- if (!parentApiKey) {
4782
- this._log(
4783
- "DEBUG",
4784
- `[RoutstrClient] _spendToken: No existing API key for ${baseUrl}, creating new one via Cashu`
4785
- );
4786
- const spendResult2 = await this.cashuSpender.spend({
4787
- mintUrl,
4788
- amount: amount * TOPUP_MARGIN,
4789
- baseUrl: "",
4790
- reuseToken: false
4791
- });
4792
- if (!spendResult2.token) {
4793
- this._log(
4794
- "ERROR",
4795
- `[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error:`,
4796
- spendResult2.error
4797
- );
4798
- throw new Error(
4799
- `[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error: ${spendResult2.error}`
4800
- );
4801
- } else {
4802
- this._log(
4803
- "DEBUG",
4804
- `[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult2.token}`
4805
- );
4806
- }
4807
- this._log(
4808
- "DEBUG",
4809
- `[RoutstrClient] _spendToken: Created API key for ${baseUrl}, key preview: ${spendResult2.token}, balance: ${spendResult2.balance}`
4810
- );
4811
- try {
4812
- this.storageAdapter.setApiKey(baseUrl, spendResult2.token);
4813
- } catch (error) {
4814
- if (error instanceof Error && error.message.includes("ApiKey already exists")) {
4815
- const receiveResult = await this.cashuSpender.receiveToken(
4816
- spendResult2.token
4817
- );
4818
- if (receiveResult.success) {
4819
- this._log(
4820
- "DEBUG",
4821
- `[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${receiveResult.amount}`
4822
- );
4823
- } else {
4824
- this._log(
4825
- "DEBUG",
4826
- `[RoutstrClient] _handleErrorResponse: Token restore failed: ${receiveResult.message}`
4827
- );
4828
- }
4829
- this._log(
4830
- "DEBUG",
4831
- `[RoutstrClient] _spendToken: API key already exists for ${baseUrl}, using existing key`
4832
- );
4833
- } else {
4834
- throw error;
4835
- }
4262
+ _extractThinkingFromContent(content, callbacks) {
4263
+ const parts = content.split(/(<thinking>|<\/thinking>)/);
4264
+ for (const part of parts) {
4265
+ if (part === "<thinking>") {
4266
+ this.isInThinking = true;
4267
+ if (!this.accumulatedThinking.includes("<thinking>")) {
4268
+ this.accumulatedThinking += "<thinking> ";
4836
4269
  }
4837
- parentApiKey = this.storageAdapter.getApiKey(baseUrl);
4270
+ } else if (part === "</thinking>") {
4271
+ this.isInThinking = false;
4272
+ this.accumulatedThinking += "</thinking>";
4273
+ } else if (this.isInThinking) {
4274
+ this.accumulatedThinking += part;
4838
4275
  } else {
4839
- this._log(
4840
- "DEBUG",
4841
- `[RoutstrClient] _spendToken: Using existing API key for ${baseUrl}, key preview: ${parentApiKey.key}`
4842
- );
4843
- }
4844
- let tokenBalance = 0;
4845
- let tokenBalanceUnit = "sat";
4846
- let tokenBalanceUnknown = false;
4847
- const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
4848
- const distributionForBaseUrl = apiKeyDistribution.find(
4849
- (d) => d.baseUrl === baseUrl
4850
- );
4851
- if (distributionForBaseUrl) {
4852
- tokenBalance = distributionForBaseUrl.amount;
4276
+ this.accumulatedContent += part;
4853
4277
  }
4854
- if (tokenBalance === 0 && parentApiKey) {
4855
- try {
4856
- const balanceInfo = await this.balanceManager.getTokenBalance(
4857
- parentApiKey.key,
4858
- baseUrl
4859
- );
4860
- tokenBalance = balanceInfo.amount;
4861
- tokenBalanceUnit = balanceInfo.unit;
4862
- tokenBalanceUnknown = Boolean(balanceInfo.balanceUnknown);
4863
- } catch (e) {
4864
- this._log("WARN", "Could not get initial API key balance:", e);
4278
+ }
4279
+ }
4280
+ /**
4281
+ * Merge images into accumulated array, avoiding duplicates
4282
+ */
4283
+ _mergeImages(newImages) {
4284
+ for (const img of newImages) {
4285
+ const newUrl = img.image_url?.url;
4286
+ const existingIndex = this.accumulatedImages.findIndex((existing) => {
4287
+ const existingUrl = existing.image_url?.url;
4288
+ if (newUrl && existingUrl) {
4289
+ return existingUrl === newUrl;
4865
4290
  }
4291
+ if (img.index !== void 0 && existing.index !== void 0) {
4292
+ return existing.index === img.index;
4293
+ }
4294
+ return false;
4295
+ });
4296
+ if (existingIndex === -1) {
4297
+ this.accumulatedImages.push(img);
4298
+ } else {
4299
+ this.accumulatedImages[existingIndex] = img;
4866
4300
  }
4867
- this._log(
4868
- "DEBUG",
4869
- `[RoutstrClient] _spendToken: Returning token with balance=${tokenBalance} ${tokenBalanceUnit}`
4870
- );
4871
- return {
4872
- token: parentApiKey?.key ?? "",
4873
- tokenBalance,
4874
- tokenBalanceUnit,
4875
- tokenBalanceUnknown
4876
- };
4877
4301
  }
4878
- this._log(
4879
- "DEBUG",
4880
- `[RoutstrClient] _spendToken: Calling CashuSpender.spend for amount=${amount}, mintUrl=${mintUrl}, mode=${this.mode}`
4881
- );
4882
- const spendResult = await this.cashuSpender.spend({
4302
+ }
4303
+ };
4304
+
4305
+ // client/fetchAIResponse.ts
4306
+ async function fetchAIResponse(options, callbacks, deps) {
4307
+ const {
4308
+ messageHistory,
4309
+ selectedModel,
4310
+ baseUrl,
4311
+ mintUrl,
4312
+ maxTokens,
4313
+ headers
4314
+ } = options;
4315
+ try {
4316
+ const apiMessages = await convertMessages(messageHistory);
4317
+ callbacks.onPaymentProcessing?.(true);
4318
+ callbacks.onTokenCreated?.(deps.getPendingCashuTokenAmount?.() ?? 0);
4319
+ const providerInfo = await deps.providerRegistry.getProviderInfo(baseUrl);
4320
+ const providerVersion = providerInfo?.version ?? "";
4321
+ let modelIdForRequest = selectedModel.id;
4322
+ if (/^0\.1\./.test(providerVersion)) {
4323
+ const newModel = await deps.client.getProviderManager().getModelForProvider(baseUrl, selectedModel.id);
4324
+ modelIdForRequest = newModel?.id ?? selectedModel.id;
4325
+ }
4326
+ const body = {
4327
+ model: modelIdForRequest,
4328
+ messages: apiMessages,
4329
+ stream: true
4330
+ };
4331
+ if (maxTokens !== void 0) {
4332
+ body.max_tokens = maxTokens;
4333
+ }
4334
+ if (selectedModel?.name?.startsWith("OpenAI:")) {
4335
+ body.tools = [{ type: "web_search" }];
4336
+ }
4337
+ const response = await deps.client.routeRequest({
4338
+ path: "/v1/chat/completions",
4339
+ method: "POST",
4340
+ body,
4341
+ headers,
4342
+ baseUrl,
4883
4343
  mintUrl,
4884
- amount,
4885
- baseUrl: "",
4886
- reuseToken: false
4344
+ modelId: selectedModel.id
4887
4345
  });
4888
- if (!spendResult.token) {
4889
- this._log(
4890
- "ERROR",
4891
- `[RoutstrClient] _spendToken: CashuSpender.spend failed, error:`,
4892
- spendResult.error
4893
- );
4346
+ if (!response.body) {
4347
+ throw new Error("Response body is not available");
4348
+ }
4349
+ if (response.status !== 200) {
4350
+ throw new Error(`${response.status} ${response.statusText}`);
4351
+ }
4352
+ const streamProcessor = new StreamProcessor();
4353
+ const streamingResult = await streamProcessor.process(
4354
+ response,
4355
+ {
4356
+ onContent: callbacks.onStreamingUpdate,
4357
+ onThinking: callbacks.onThinkingUpdate
4358
+ },
4359
+ selectedModel.id
4360
+ );
4361
+ if (streamingResult.finish_reason === "content_filter") {
4362
+ callbacks.onMessageAppend({
4363
+ role: "assistant",
4364
+ content: "Your request was denied due to content filtering."
4365
+ });
4366
+ } else if (streamingResult.content || streamingResult.images && streamingResult.images.length > 0) {
4367
+ const message = await createAssistantMessage(streamingResult);
4368
+ callbacks.onMessageAppend(message);
4894
4369
  } else {
4895
- this._log(
4896
- "DEBUG",
4897
- `[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult.token}, balance: ${spendResult.balance} ${spendResult.unit ?? "sat"}`
4898
- );
4899
- this.storageAdapter.addXcashuToken(baseUrl, spendResult.token);
4370
+ callbacks.onMessageAppend({
4371
+ role: "system",
4372
+ content: "The provider did not respond to this request."
4373
+ });
4900
4374
  }
4901
- return {
4902
- token: spendResult.token,
4903
- tokenBalance: spendResult.balance,
4904
- tokenBalanceUnit: spendResult.unit ?? "sat",
4905
- tokenBalanceUnknown: false
4906
- };
4375
+ callbacks.onStreamingUpdate("");
4376
+ callbacks.onThinkingUpdate("");
4377
+ } catch (error) {
4378
+ handleError(error, callbacks, deps.alertLevel, deps.logger);
4379
+ } finally {
4380
+ callbacks.onPaymentProcessing?.(false);
4907
4381
  }
4908
- /**
4909
- * Build request headers with common defaults and dev mock controls
4910
- */
4911
- _buildBaseHeaders(additionalHeaders = {}, token) {
4912
- const headers = {
4913
- ...additionalHeaders,
4914
- "Content-Type": "application/json"
4382
+ }
4383
+ async function convertMessages(messages) {
4384
+ return Promise.all(
4385
+ messages.filter((m) => m.role !== "system").map(async (m) => ({
4386
+ role: m.role,
4387
+ content: typeof m.content === "string" ? m.content : m.content
4388
+ }))
4389
+ );
4390
+ }
4391
+ async function createAssistantMessage(result) {
4392
+ if (result.images && result.images.length > 0) {
4393
+ const content = [];
4394
+ if (result.content) {
4395
+ content.push({
4396
+ type: "text",
4397
+ text: result.content,
4398
+ thinking: result.thinking,
4399
+ citations: result.citations,
4400
+ annotations: result.annotations
4401
+ });
4402
+ }
4403
+ for (const img of result.images) {
4404
+ content.push({
4405
+ type: "image_url",
4406
+ image_url: {
4407
+ url: img.image_url.url
4408
+ }
4409
+ });
4410
+ }
4411
+ return {
4412
+ role: "assistant",
4413
+ content
4915
4414
  };
4916
- return headers;
4917
4415
  }
4918
- /**
4919
- * Attach auth headers using the active client mode
4920
- */
4921
- _withAuthHeader(headers, token) {
4922
- const nextHeaders = { ...headers };
4923
- if (this.mode === "xcashu") {
4924
- nextHeaders["X-Cashu"] = token;
4925
- } else {
4926
- nextHeaders["Authorization"] = `Bearer ${token}`;
4927
- }
4928
- return nextHeaders;
4416
+ return {
4417
+ role: "assistant",
4418
+ content: result.content || ""
4419
+ };
4420
+ }
4421
+ function handleError(error, callbacks, alertLevel, logger) {
4422
+ logger.error("[fetchAIResponse] Error occurred", error);
4423
+ if (error instanceof Error) {
4424
+ const isStreamError = error.message.includes("Error in input stream") || error.message.includes("Load failed");
4425
+ const modifiedErrorMsg = isStreamError ? "AI stream was cut off, turn on Keep Active or please try again" : error.message;
4426
+ logger.error(
4427
+ `[fetchAIResponse] Error type=${error.constructor.name}, message=${modifiedErrorMsg}, isStreamError=${isStreamError}`
4428
+ );
4429
+ callbacks.onMessageAppend({
4430
+ role: "system",
4431
+ content: "Uncaught Error: " + modifiedErrorMsg + (alertLevel === "max" ? " | " + error.stack : "")
4432
+ });
4433
+ } else {
4434
+ callbacks.onMessageAppend({
4435
+ role: "system",
4436
+ content: "Unknown Error: Please tag Routstr on Nostr and/or retry."
4437
+ });
4929
4438
  }
4930
- };
4439
+ }
4931
4440
 
4932
4441
  exports.ProviderManager = ProviderManager;
4933
4442
  exports.RoutstrClient = RoutstrClient;
4934
4443
  exports.StreamProcessor = StreamProcessor;
4935
4444
  exports.createSSEParserTransform = createSSEParserTransform;
4445
+ exports.fetchAIResponse = fetchAIResponse;
4936
4446
  exports.inspectSSEWebStream = inspectSSEWebStream;
4937
4447
  //# sourceMappingURL=index.js.map
4938
4448
  //# sourceMappingURL=index.js.map