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