@routstr/sdk 0.2.4 → 0.2.5

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 (38) hide show
  1. package/README.md +9 -0
  2. package/dist/client/index.d.mts +21 -8
  3. package/dist/client/index.d.ts +21 -8
  4. package/dist/client/index.js +1379 -54
  5. package/dist/client/index.js.map +1 -1
  6. package/dist/client/index.mjs +1379 -55
  7. package/dist/client/index.mjs.map +1 -1
  8. package/dist/discovery/index.d.mts +2 -2
  9. package/dist/discovery/index.d.ts +2 -2
  10. package/dist/discovery/index.js +1 -4
  11. package/dist/discovery/index.js.map +1 -1
  12. package/dist/discovery/index.mjs +1 -4
  13. package/dist/discovery/index.mjs.map +1 -1
  14. package/dist/index.d.mts +15 -19
  15. package/dist/index.d.ts +15 -19
  16. package/dist/index.js +2671 -1872
  17. package/dist/index.js.map +1 -1
  18. package/dist/index.mjs +2666 -1873
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/{interfaces-DGdP8fQp.d.mts → interfaces-BWJJTCXO.d.mts} +1 -1
  21. package/dist/{interfaces-CC0LT9p9.d.ts → interfaces-BxDEka72.d.ts} +1 -1
  22. package/dist/{interfaces-B85Wx7ni.d.mts → interfaces-C6Dr6hKy.d.mts} +1 -1
  23. package/dist/{interfaces-BVNyAmKu.d.ts → interfaces-CluftN4z.d.ts} +1 -1
  24. package/dist/storage/index.d.mts +56 -34
  25. package/dist/storage/index.d.ts +56 -34
  26. package/dist/storage/index.js +500 -51
  27. package/dist/storage/index.js.map +1 -1
  28. package/dist/storage/index.mjs +497 -52
  29. package/dist/storage/index.mjs.map +1 -1
  30. package/dist/{types-BlHjmWRK.d.mts → types-BYj_8c5c.d.mts} +3 -0
  31. package/dist/{types-BlHjmWRK.d.ts → types-BYj_8c5c.d.ts} +3 -0
  32. package/dist/wallet/index.d.mts +9 -5
  33. package/dist/wallet/index.d.ts +9 -5
  34. package/dist/wallet/index.js +27 -7
  35. package/dist/wallet/index.js.map +1 -1
  36. package/dist/wallet/index.mjs +27 -7
  37. package/dist/wallet/index.mjs.map +1 -1
  38. package/package.json +1 -1
@@ -1,5 +1,16 @@
1
1
  'use strict';
2
2
 
3
+ var vanilla = require('zustand/vanilla');
4
+ var cashuTs = require('@cashu/cashu-ts');
5
+ var stream = require('stream');
6
+
7
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
8
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
9
+ }) : x)(function(x) {
10
+ if (typeof require !== "undefined") return require.apply(this, arguments);
11
+ throw Error('Dynamic require of "' + x + '" is not supported');
12
+ });
13
+
3
14
  // core/errors.ts
4
15
  var InsufficientBalanceError = class extends Error {
5
16
  constructor(required, available, maxMintBalance = 0, maxMintUrl = "", customMessage) {
@@ -468,7 +479,7 @@ var CashuSpender = class {
468
479
  /**
469
480
  * Refund specific providers without retrying spend
470
481
  */
471
- async refundProviders(baseUrls, mintUrl, refundApiKeys = false) {
482
+ async refundProviders(baseUrls, mintUrl, refundApiKeys = false, forceRefund) {
472
483
  const results = [];
473
484
  const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
474
485
  const toRefund = pendingDistribution.filter(
@@ -518,7 +529,8 @@ var CashuSpender = class {
518
529
  const refundResult = await this.balanceManager.refundApiKey({
519
530
  mintUrl,
520
531
  baseUrl: apiKeyEntry.baseUrl,
521
- apiKey: apiKeyEntryFull.key
532
+ apiKey: apiKeyEntryFull.key,
533
+ forceRefund
522
534
  });
523
535
  if (refundResult.success) {
524
536
  this.storageAdapter.updateApiKeyBalance(apiKeyEntry.baseUrl, 0);
@@ -691,12 +703,29 @@ var BalanceManager = class {
691
703
  }
692
704
  /**
693
705
  * Refund API key balance - convert remaining API key balance to cashu token
706
+ * @param options - Refund options including forceRefund flag
707
+ * @returns Refund result
694
708
  */
695
709
  async refundApiKey(options) {
696
- const { mintUrl, baseUrl, apiKey } = options;
710
+ const { mintUrl, baseUrl, apiKey, forceRefund } = options;
697
711
  if (!apiKey) {
698
712
  return { success: false, message: "No API key to refund" };
699
713
  }
714
+ if (!forceRefund) {
715
+ const apiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
716
+ if (apiKeyEntry?.lastUsed) {
717
+ const fiveMinutesAgo = Date.now() - 5 * 60 * 1e3;
718
+ if (apiKeyEntry.lastUsed > fiveMinutesAgo) {
719
+ console.log(
720
+ `[BalanceManager] Skipping refund for ${baseUrl} - used ${Math.round((Date.now() - apiKeyEntry.lastUsed) / 1e3)}s ago`
721
+ );
722
+ return {
723
+ success: false,
724
+ message: "API key was used recently, skipping refund"
725
+ };
726
+ }
727
+ }
728
+ }
700
729
  let fetchResult;
701
730
  try {
702
731
  fetchResult = await this._fetchRefundTokenWithApiKey(baseUrl, apiKey);
@@ -888,8 +917,8 @@ var BalanceManager = class {
888
917
  const refundableProviderBalance = Object.entries(
889
918
  balanceState.providerBalances
890
919
  ).filter(([providerBaseUrl]) => providerBaseUrl !== baseUrl).reduce((sum, [, value]) => sum + value, 0);
891
- if (totalMintBalance + targetProviderBalance < adjustedAmount && totalMintBalance + targetProviderBalance + refundableProviderBalance >= adjustedAmount && retryCount < 1) {
892
- await this._refundOtherProvidersForTopUp(baseUrl, mintUrl);
920
+ if (totalMintBalance + targetProviderBalance < adjustedAmount && totalMintBalance + targetProviderBalance + refundableProviderBalance >= adjustedAmount && retryCount < 2) {
921
+ await this._refundOtherProvidersForTopUp(baseUrl, mintUrl, retryCount);
893
922
  return this.createProviderToken({
894
923
  ...options,
895
924
  retryCount: retryCount + 1
@@ -1042,9 +1071,10 @@ var BalanceManager = class {
1042
1071
  }
1043
1072
  return candidates;
1044
1073
  }
1045
- async _refundOtherProvidersForTopUp(baseUrl, mintUrl) {
1074
+ async _refundOtherProvidersForTopUp(baseUrl, mintUrl, retryCount) {
1046
1075
  const pendingDistribution = this.storageAdapter.getCachedTokenDistribution();
1047
1076
  const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
1077
+ const forceRefund = retryCount >= 2;
1048
1078
  const toRefund = pendingDistribution.filter(
1049
1079
  (pending) => pending.baseUrl !== baseUrl
1050
1080
  );
@@ -1085,7 +1115,8 @@ var BalanceManager = class {
1085
1115
  const result = await this.refundApiKey({
1086
1116
  mintUrl,
1087
1117
  baseUrl: apiKeyEntry.baseUrl,
1088
- apiKey: fullApiKeyEntry.key
1118
+ apiKey: fullApiKeyEntry.key,
1119
+ forceRefund
1089
1120
  });
1090
1121
  return { baseUrl: apiKeyEntry.baseUrl, success: result.success };
1091
1122
  })
@@ -1339,6 +1370,77 @@ var BalanceManager = class {
1339
1370
  }
1340
1371
  };
1341
1372
 
1373
+ // client/usage.ts
1374
+ function extractUsageFromResponseBody(body, fallbackSatsCost = 0) {
1375
+ if (!body || typeof body !== "object") return null;
1376
+ const usage = body.usage;
1377
+ if (!usage || typeof usage !== "object") return null;
1378
+ const promptTokens = Number(usage.prompt_tokens ?? 0);
1379
+ const completionTokens = Number(usage.completion_tokens ?? 0);
1380
+ const totalTokens = Number(usage.total_tokens ?? 0);
1381
+ const costValue = usage.cost;
1382
+ let cost = 0;
1383
+ let satsCost = fallbackSatsCost;
1384
+ if (typeof costValue === "number") {
1385
+ cost = costValue;
1386
+ } else if (costValue && typeof costValue === "object") {
1387
+ const costObj = costValue;
1388
+ const totalUsd = costObj.total_usd;
1389
+ const totalMsats = costObj.total_msats;
1390
+ cost = typeof totalUsd === "number" ? totalUsd : 0;
1391
+ if (typeof totalMsats === "number") {
1392
+ satsCost = totalMsats / 1e3;
1393
+ }
1394
+ }
1395
+ if (promptTokens === 0 && completionTokens === 0 && totalTokens === 0 && cost === 0 && satsCost === 0) {
1396
+ return null;
1397
+ }
1398
+ return {
1399
+ promptTokens,
1400
+ completionTokens,
1401
+ totalTokens,
1402
+ cost,
1403
+ satsCost
1404
+ };
1405
+ }
1406
+ function extractResponseId(body) {
1407
+ if (!body || typeof body !== "object") return void 0;
1408
+ const id = body.id;
1409
+ if (typeof id !== "string") return void 0;
1410
+ const trimmed = id.trim();
1411
+ return trimmed.length > 0 ? trimmed : void 0;
1412
+ }
1413
+ function extractUsageFromSSEJson(parsed, fallbackSatsCost = 0) {
1414
+ if (!parsed || typeof parsed !== "object" || !parsed.usage) {
1415
+ return null;
1416
+ }
1417
+ const usage = parsed.usage;
1418
+ const usageCost = usage.cost;
1419
+ const cost = typeof usageCost === "number" ? usageCost : usageCost?.total_usd ?? parsed.metadata?.routstr?.cost?.total_usd ?? 0;
1420
+ const msats = parsed.metadata?.routstr?.cost?.total_msats ?? (typeof usage.cost_sats === "number" ? usage.cost_sats * 1e3 : 0);
1421
+ const result = {
1422
+ promptTokens: Number(usage.prompt_tokens ?? 0),
1423
+ completionTokens: Number(usage.completion_tokens ?? 0),
1424
+ totalTokens: Number(usage.total_tokens ?? 0),
1425
+ cost: Number(cost ?? 0),
1426
+ satsCost: msats > 0 ? msats / 1e3 : fallbackSatsCost
1427
+ };
1428
+ if (result.promptTokens === 0 && result.completionTokens === 0 && result.totalTokens === 0 && result.cost === 0 && result.satsCost === 0) {
1429
+ return null;
1430
+ }
1431
+ return result;
1432
+ }
1433
+ function toUsageStats(usage) {
1434
+ if (!usage) return void 0;
1435
+ return {
1436
+ total_tokens: usage.totalTokens,
1437
+ prompt_tokens: usage.promptTokens,
1438
+ completion_tokens: usage.completionTokens,
1439
+ cost: usage.cost,
1440
+ sats_cost: usage.satsCost
1441
+ };
1442
+ }
1443
+
1342
1444
  // client/StreamProcessor.ts
1343
1445
  var StreamProcessor = class {
1344
1446
  accumulatedContent = "";
@@ -1366,6 +1468,7 @@ var StreamProcessor = class {
1366
1468
  let finish_reason;
1367
1469
  let citations;
1368
1470
  let annotations;
1471
+ let responseId;
1369
1472
  try {
1370
1473
  while (true) {
1371
1474
  const { done, value } = await reader.read();
@@ -1394,6 +1497,9 @@ var StreamProcessor = class {
1394
1497
  if (parsed.finish_reason) {
1395
1498
  finish_reason = parsed.finish_reason;
1396
1499
  }
1500
+ if (parsed.responseId) {
1501
+ responseId = parsed.responseId;
1502
+ }
1397
1503
  if (parsed.citations) {
1398
1504
  citations = parsed.citations;
1399
1505
  }
@@ -1414,6 +1520,7 @@ var StreamProcessor = class {
1414
1520
  images: this.accumulatedImages.length > 0 ? this.accumulatedImages : void 0,
1415
1521
  usage,
1416
1522
  model,
1523
+ responseId,
1417
1524
  finish_reason,
1418
1525
  citations,
1419
1526
  annotations
@@ -1441,12 +1548,15 @@ var StreamProcessor = class {
1441
1548
  result.reasoning = parsed.choices[0].delta.reasoning;
1442
1549
  }
1443
1550
  if (parsed.usage) {
1444
- result.usage = {
1551
+ result.usage = toUsageStats(extractUsageFromSSEJson(parsed)) ?? {
1445
1552
  total_tokens: parsed.usage.total_tokens,
1446
1553
  prompt_tokens: parsed.usage.prompt_tokens,
1447
1554
  completion_tokens: parsed.usage.completion_tokens
1448
1555
  };
1449
1556
  }
1557
+ if (parsed.id) {
1558
+ result.responseId = parsed.id;
1559
+ }
1450
1560
  if (parsed.model) {
1451
1561
  result.model = parsed.model;
1452
1562
  }
@@ -1838,7 +1948,6 @@ var ProviderManager = class _ProviderManager {
1838
1948
  * Get providers for a model sorted by prompt+completion pricing
1839
1949
  */
1840
1950
  getProviderPriceRankingForModel(modelId, options = {}) {
1841
- const normalizedId = this.normalizeModelId(modelId);
1842
1951
  const includeDisabled = options.includeDisabled ?? false;
1843
1952
  const torMode = options.torMode ?? false;
1844
1953
  const disabledProviders = new Set(
@@ -1852,9 +1961,7 @@ var ProviderManager = class _ProviderManager {
1852
1961
  if (torMode && !baseUrl.includes(".onion")) continue;
1853
1962
  if (!torMode && (baseUrl.includes(".onion") || isInsecureHttpUrl(baseUrl)))
1854
1963
  continue;
1855
- const match = models.find(
1856
- (model) => this.normalizeModelId(model.id) === normalizedId
1857
- );
1964
+ const match = models.find((model) => model.id === modelId);
1858
1965
  if (!match?.sats_pricing) continue;
1859
1966
  const prompt = match.sats_pricing.prompt;
1860
1967
  const completion = match.sats_pricing.completion;
@@ -1967,7 +2074,1058 @@ var ProviderManager = class _ProviderManager {
1967
2074
  }
1968
2075
  };
1969
2076
 
1970
- // client/RoutstrClient.ts
2077
+ // storage/drivers/localStorage.ts
2078
+ var canUseLocalStorage = () => {
2079
+ return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
2080
+ };
2081
+ var isQuotaExceeded = (error) => {
2082
+ const e = error;
2083
+ return !!e && (e?.name === "QuotaExceededError" || e?.code === 22 || e?.code === 1014);
2084
+ };
2085
+ var NON_CRITICAL_KEYS = /* @__PURE__ */ new Set(["modelsFromAllProviders"]);
2086
+ var localStorageDriver = {
2087
+ async getItem(key, defaultValue) {
2088
+ if (!canUseLocalStorage()) return defaultValue;
2089
+ try {
2090
+ const item = window.localStorage.getItem(key);
2091
+ if (item === null) return defaultValue;
2092
+ try {
2093
+ return JSON.parse(item);
2094
+ } catch (parseError) {
2095
+ if (typeof defaultValue === "string") {
2096
+ return item;
2097
+ }
2098
+ throw parseError;
2099
+ }
2100
+ } catch (error) {
2101
+ console.error(`Error retrieving item with key "${key}":`, error);
2102
+ if (canUseLocalStorage()) {
2103
+ try {
2104
+ window.localStorage.removeItem(key);
2105
+ } catch (removeError) {
2106
+ console.error(
2107
+ `Error removing corrupted item with key "${key}":`,
2108
+ removeError
2109
+ );
2110
+ }
2111
+ }
2112
+ return defaultValue;
2113
+ }
2114
+ },
2115
+ async setItem(key, value) {
2116
+ if (!canUseLocalStorage()) return;
2117
+ try {
2118
+ window.localStorage.setItem(key, JSON.stringify(value));
2119
+ } catch (error) {
2120
+ if (isQuotaExceeded(error)) {
2121
+ if (NON_CRITICAL_KEYS.has(key)) {
2122
+ console.warn(
2123
+ `Storage quota exceeded; skipping non-critical key "${key}".`
2124
+ );
2125
+ return;
2126
+ }
2127
+ try {
2128
+ window.localStorage.removeItem("modelsFromAllProviders");
2129
+ } catch {
2130
+ }
2131
+ try {
2132
+ window.localStorage.setItem(key, JSON.stringify(value));
2133
+ return;
2134
+ } catch (retryError) {
2135
+ console.warn(
2136
+ `Storage quota exceeded; unable to persist key "${key}" after cleanup attempt.`,
2137
+ retryError
2138
+ );
2139
+ return;
2140
+ }
2141
+ }
2142
+ console.error(`Error storing item with key "${key}":`, error);
2143
+ }
2144
+ },
2145
+ async removeItem(key) {
2146
+ if (!canUseLocalStorage()) return;
2147
+ try {
2148
+ window.localStorage.removeItem(key);
2149
+ } catch (error) {
2150
+ console.error(`Error removing item with key "${key}":`, error);
2151
+ }
2152
+ }
2153
+ };
2154
+
2155
+ // storage/drivers/memory.ts
2156
+ var createMemoryDriver = (seed) => {
2157
+ const store = /* @__PURE__ */ new Map();
2158
+ return {
2159
+ async getItem(key, defaultValue) {
2160
+ const item = store.get(key);
2161
+ if (item === void 0) return defaultValue;
2162
+ try {
2163
+ return JSON.parse(item);
2164
+ } catch (parseError) {
2165
+ if (typeof defaultValue === "string") {
2166
+ return item;
2167
+ }
2168
+ throw parseError;
2169
+ }
2170
+ },
2171
+ async setItem(key, value) {
2172
+ store.set(key, JSON.stringify(value));
2173
+ },
2174
+ async removeItem(key) {
2175
+ store.delete(key);
2176
+ }
2177
+ };
2178
+ };
2179
+
2180
+ // storage/drivers/sqlite.ts
2181
+ var isBun = () => {
2182
+ return typeof process.versions.bun !== "undefined";
2183
+ };
2184
+ var createDatabase = (dbPath) => {
2185
+ if (isBun()) {
2186
+ throw new Error(
2187
+ "SQLite driver not supported in Bun. Use createMemoryDriver() instead."
2188
+ );
2189
+ }
2190
+ let Database = null;
2191
+ try {
2192
+ Database = __require("better-sqlite3");
2193
+ } catch (error) {
2194
+ throw new Error(
2195
+ `better-sqlite3 is required for sqlite storage. Install it to use sqlite storage. (${error})`
2196
+ );
2197
+ }
2198
+ return new Database(dbPath);
2199
+ };
2200
+ var createSqliteDriver = (options = {}) => {
2201
+ const dbPath = options.dbPath || "routstr.sqlite";
2202
+ const tableName = options.tableName || "sdk_storage";
2203
+ const db = createDatabase(dbPath);
2204
+ db.exec(
2205
+ `CREATE TABLE IF NOT EXISTS ${tableName} (key TEXT PRIMARY KEY, value TEXT NOT NULL)`
2206
+ );
2207
+ const selectStmt = db.prepare(`SELECT value FROM ${tableName} WHERE key = ?`);
2208
+ const upsertStmt = db.prepare(
2209
+ `INSERT INTO ${tableName} (key, value) VALUES (?, ?)
2210
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`
2211
+ );
2212
+ const deleteStmt = db.prepare(`DELETE FROM ${tableName} WHERE key = ?`);
2213
+ return {
2214
+ async getItem(key, defaultValue) {
2215
+ try {
2216
+ const row = selectStmt.get(key);
2217
+ if (!row || typeof row.value !== "string") return defaultValue;
2218
+ try {
2219
+ return JSON.parse(row.value);
2220
+ } catch (parseError) {
2221
+ if (typeof defaultValue === "string") {
2222
+ return row.value;
2223
+ }
2224
+ throw parseError;
2225
+ }
2226
+ } catch (error) {
2227
+ console.error(`SQLite getItem failed for key "${key}":`, error);
2228
+ return defaultValue;
2229
+ }
2230
+ },
2231
+ async setItem(key, value) {
2232
+ try {
2233
+ upsertStmt.run(key, JSON.stringify(value));
2234
+ } catch (error) {
2235
+ console.error(`SQLite setItem failed for key "${key}":`, error);
2236
+ }
2237
+ },
2238
+ async removeItem(key) {
2239
+ try {
2240
+ deleteStmt.run(key);
2241
+ } catch (error) {
2242
+ console.error(`SQLite removeItem failed for key "${key}":`, error);
2243
+ }
2244
+ }
2245
+ };
2246
+ };
2247
+
2248
+ // storage/keys.ts
2249
+ var SDK_STORAGE_KEYS = {
2250
+ MODELS_FROM_ALL_PROVIDERS: "modelsFromAllProviders",
2251
+ LAST_USED_MODEL: "lastUsedModel",
2252
+ BASE_URLS_LIST: "base_urls_list",
2253
+ DISABLED_PROVIDERS: "disabled_providers",
2254
+ MINTS_FROM_ALL_PROVIDERS: "mints_from_all_providers",
2255
+ INFO_FROM_ALL_PROVIDERS: "info_from_all_providers",
2256
+ LAST_MODELS_UPDATE: "lastModelsUpdate",
2257
+ LAST_BASE_URLS_UPDATE: "lastBaseUrlsUpdate",
2258
+ LOCAL_CASHU_TOKENS: "local_cashu_tokens",
2259
+ API_KEYS: "api_keys",
2260
+ CHILD_KEYS: "child_keys",
2261
+ ROUTSTR21_MODELS: "routstr21Models",
2262
+ LAST_ROUTSTR21_MODELS_UPDATE: "lastRoutstr21ModelsUpdate",
2263
+ CACHED_RECEIVE_TOKENS: "cached_receive_tokens",
2264
+ USAGE_TRACKING: "usage_tracking",
2265
+ CLIENT_IDS: "client_ids"
2266
+ };
2267
+
2268
+ // storage/usageTracking/indexedDB.ts
2269
+ var DEFAULT_DB_NAME = "routstr-sdk";
2270
+ var DEFAULT_STORE_NAME = "usage_tracking";
2271
+ var MIGRATION_MARKER_KEY = "usage_tracking_migration_v1";
2272
+ var isBrowser = typeof indexedDB !== "undefined";
2273
+ var normalizeBaseUrl = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
2274
+ var openDatabase = (dbName, storeName) => {
2275
+ if (!isBrowser) {
2276
+ return Promise.reject(new Error("IndexedDB is not available"));
2277
+ }
2278
+ return new Promise((resolve, reject) => {
2279
+ const request = indexedDB.open(dbName, 1);
2280
+ request.onupgradeneeded = () => {
2281
+ const db = request.result;
2282
+ if (!db.objectStoreNames.contains(storeName)) {
2283
+ const store = db.createObjectStore(storeName, { keyPath: "id" });
2284
+ store.createIndex("timestamp", "timestamp", { unique: false });
2285
+ store.createIndex("modelId", "modelId", { unique: false });
2286
+ store.createIndex("baseUrl", "baseUrl", { unique: false });
2287
+ store.createIndex("sessionId", "sessionId", { unique: false });
2288
+ store.createIndex("client", "client", { unique: false });
2289
+ }
2290
+ };
2291
+ request.onsuccess = () => resolve(request.result);
2292
+ request.onerror = () => reject(request.error);
2293
+ });
2294
+ };
2295
+ var matchesFilters = (entry, options = {}) => {
2296
+ if (typeof options.before === "number" && entry.timestamp >= options.before) {
2297
+ return false;
2298
+ }
2299
+ if (typeof options.after === "number" && entry.timestamp <= options.after) {
2300
+ return false;
2301
+ }
2302
+ if (options.modelId && entry.modelId !== options.modelId) {
2303
+ return false;
2304
+ }
2305
+ if (options.baseUrl && normalizeBaseUrl(entry.baseUrl) !== normalizeBaseUrl(options.baseUrl)) {
2306
+ return false;
2307
+ }
2308
+ if (options.sessionId && entry.sessionId !== options.sessionId) {
2309
+ return false;
2310
+ }
2311
+ if (options.client && entry.client !== options.client) {
2312
+ return false;
2313
+ }
2314
+ return true;
2315
+ };
2316
+ var createIndexedDBUsageTrackingDriver = (options = {}) => {
2317
+ const dbName = options.dbName || DEFAULT_DB_NAME;
2318
+ const storeName = options.storeName || DEFAULT_STORE_NAME;
2319
+ const legacyStorageDriver = options.legacyStorageDriver;
2320
+ let dbPromise = null;
2321
+ let migrationPromise = null;
2322
+ const getDb = () => {
2323
+ if (!dbPromise) {
2324
+ dbPromise = openDatabase(dbName, storeName);
2325
+ }
2326
+ return dbPromise;
2327
+ };
2328
+ const putMany = async (entries) => {
2329
+ if (entries.length === 0) return;
2330
+ const db = await getDb();
2331
+ await new Promise((resolve, reject) => {
2332
+ const tx = db.transaction(storeName, "readwrite");
2333
+ const store = tx.objectStore(storeName);
2334
+ for (const entry of entries) {
2335
+ store.put({ ...entry, baseUrl: normalizeBaseUrl(entry.baseUrl) });
2336
+ }
2337
+ tx.oncomplete = () => resolve();
2338
+ tx.onerror = () => reject(tx.error);
2339
+ });
2340
+ };
2341
+ const ensureMigrated = async () => {
2342
+ if (!legacyStorageDriver) return;
2343
+ if (!migrationPromise) {
2344
+ migrationPromise = (async () => {
2345
+ const migrated = await legacyStorageDriver.getItem(
2346
+ MIGRATION_MARKER_KEY,
2347
+ false
2348
+ );
2349
+ if (migrated) return;
2350
+ const legacyEntries = await legacyStorageDriver.getItem(
2351
+ SDK_STORAGE_KEYS.USAGE_TRACKING,
2352
+ []
2353
+ );
2354
+ if (legacyEntries.length > 0) {
2355
+ await putMany(legacyEntries);
2356
+ await legacyStorageDriver.removeItem(SDK_STORAGE_KEYS.USAGE_TRACKING);
2357
+ }
2358
+ await legacyStorageDriver.setItem(MIGRATION_MARKER_KEY, true);
2359
+ })();
2360
+ }
2361
+ await migrationPromise;
2362
+ };
2363
+ return {
2364
+ async migrate() {
2365
+ await ensureMigrated();
2366
+ },
2367
+ async append(entry) {
2368
+ await ensureMigrated();
2369
+ await putMany([entry]);
2370
+ },
2371
+ async appendMany(entries) {
2372
+ await ensureMigrated();
2373
+ await putMany(entries);
2374
+ },
2375
+ async list(options2 = {}) {
2376
+ await ensureMigrated();
2377
+ const db = await getDb();
2378
+ return new Promise((resolve, reject) => {
2379
+ const tx = db.transaction(storeName, "readonly");
2380
+ const store = tx.objectStore(storeName);
2381
+ const index = store.index("timestamp");
2382
+ const direction = "prev";
2383
+ const request = index.openCursor(null, direction);
2384
+ const results = [];
2385
+ const limit = options2.limit;
2386
+ request.onsuccess = () => {
2387
+ const cursor = request.result;
2388
+ if (!cursor) {
2389
+ resolve(results);
2390
+ return;
2391
+ }
2392
+ const value = cursor.value;
2393
+ if (matchesFilters(value, options2)) {
2394
+ results.push(value);
2395
+ if (typeof limit === "number" && results.length >= limit) {
2396
+ resolve(results);
2397
+ return;
2398
+ }
2399
+ }
2400
+ cursor.continue();
2401
+ };
2402
+ request.onerror = () => reject(request.error);
2403
+ });
2404
+ },
2405
+ async count(options2 = {}) {
2406
+ const results = await this.list(options2);
2407
+ return results.length;
2408
+ },
2409
+ async deleteOlderThan(timestamp) {
2410
+ await ensureMigrated();
2411
+ const db = await getDb();
2412
+ return new Promise((resolve, reject) => {
2413
+ const tx = db.transaction(storeName, "readwrite");
2414
+ const store = tx.objectStore(storeName);
2415
+ const index = store.index("timestamp");
2416
+ const range = IDBKeyRange.upperBound(timestamp, true);
2417
+ const request = index.openCursor(range);
2418
+ let deleted = 0;
2419
+ request.onsuccess = () => {
2420
+ const cursor = request.result;
2421
+ if (!cursor) {
2422
+ resolve(deleted);
2423
+ return;
2424
+ }
2425
+ deleted += 1;
2426
+ cursor.delete();
2427
+ cursor.continue();
2428
+ };
2429
+ request.onerror = () => reject(request.error);
2430
+ });
2431
+ },
2432
+ async clear() {
2433
+ await ensureMigrated();
2434
+ const db = await getDb();
2435
+ await new Promise((resolve, reject) => {
2436
+ const tx = db.transaction(storeName, "readwrite");
2437
+ tx.objectStore(storeName).clear();
2438
+ tx.oncomplete = () => resolve();
2439
+ tx.onerror = () => reject(tx.error);
2440
+ });
2441
+ }
2442
+ };
2443
+ };
2444
+
2445
+ // storage/usageTracking/sqlite.ts
2446
+ var MIGRATION_MARKER_KEY2 = "usage_tracking_migration_v1";
2447
+ var normalizeBaseUrl2 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
2448
+ var isBun2 = () => {
2449
+ return typeof process.versions.bun !== "undefined";
2450
+ };
2451
+ var createDatabase2 = (dbPath) => {
2452
+ if (isBun2()) {
2453
+ throw new Error(
2454
+ "SQLite driver not supported in Bun. Use createMemoryDriver() instead."
2455
+ );
2456
+ }
2457
+ let Database = null;
2458
+ try {
2459
+ Database = __require("better-sqlite3");
2460
+ } catch (error) {
2461
+ throw new Error(
2462
+ `better-sqlite3 is required for sqlite usage tracking. Install it to use sqlite storage. (${error})`
2463
+ );
2464
+ }
2465
+ return new Database(dbPath);
2466
+ };
2467
+ var buildWhereClause = (options = {}) => {
2468
+ const clauses = [];
2469
+ const params = [];
2470
+ if (typeof options.before === "number") {
2471
+ clauses.push("timestamp < ?");
2472
+ params.push(options.before);
2473
+ }
2474
+ if (typeof options.after === "number") {
2475
+ clauses.push("timestamp > ?");
2476
+ params.push(options.after);
2477
+ }
2478
+ if (options.modelId) {
2479
+ clauses.push("model_id = ?");
2480
+ params.push(options.modelId);
2481
+ }
2482
+ if (options.baseUrl) {
2483
+ clauses.push("base_url = ?");
2484
+ params.push(normalizeBaseUrl2(options.baseUrl));
2485
+ }
2486
+ if (options.sessionId) {
2487
+ clauses.push("session_id = ?");
2488
+ params.push(options.sessionId);
2489
+ }
2490
+ if (options.client) {
2491
+ clauses.push("client = ?");
2492
+ params.push(options.client);
2493
+ }
2494
+ return {
2495
+ sql: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
2496
+ params
2497
+ };
2498
+ };
2499
+ var createSqliteUsageTrackingDriver = (options = {}) => {
2500
+ const dbPath = options.dbPath || "routstr.sqlite";
2501
+ const tableName = options.tableName || "usage_tracking";
2502
+ const db = createDatabase2(dbPath);
2503
+ const legacyStorageDriver = options.legacyStorageDriver;
2504
+ db.exec(`
2505
+ CREATE TABLE IF NOT EXISTS ${tableName} (
2506
+ id TEXT PRIMARY KEY,
2507
+ timestamp INTEGER NOT NULL,
2508
+ model_id TEXT NOT NULL,
2509
+ base_url TEXT NOT NULL,
2510
+ request_id TEXT NOT NULL,
2511
+ cost REAL NOT NULL,
2512
+ sats_cost REAL NOT NULL,
2513
+ prompt_tokens INTEGER NOT NULL,
2514
+ completion_tokens INTEGER NOT NULL,
2515
+ total_tokens INTEGER NOT NULL,
2516
+ client TEXT,
2517
+ session_id TEXT,
2518
+ tags TEXT
2519
+ );
2520
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_timestamp ON ${tableName}(timestamp);
2521
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_model_id ON ${tableName}(model_id);
2522
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_base_url ON ${tableName}(base_url);
2523
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_session_id ON ${tableName}(session_id);
2524
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_client ON ${tableName}(client);
2525
+ `);
2526
+ const insertStmt = db.prepare(`
2527
+ INSERT OR REPLACE INTO ${tableName} (
2528
+ id, timestamp, model_id, base_url, request_id,
2529
+ cost, sats_cost, prompt_tokens, completion_tokens, total_tokens,
2530
+ client, session_id, tags
2531
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2532
+ `);
2533
+ let migrationComplete = false;
2534
+ const appendOne = (entry) => {
2535
+ insertStmt.run(
2536
+ entry.id,
2537
+ entry.timestamp,
2538
+ entry.modelId,
2539
+ normalizeBaseUrl2(entry.baseUrl),
2540
+ entry.requestId,
2541
+ entry.cost,
2542
+ entry.satsCost,
2543
+ entry.promptTokens,
2544
+ entry.completionTokens,
2545
+ entry.totalTokens,
2546
+ entry.client ?? null,
2547
+ entry.sessionId ?? null,
2548
+ JSON.stringify(entry.tags ?? [])
2549
+ );
2550
+ };
2551
+ const ensureMigrated = async () => {
2552
+ if (!legacyStorageDriver || migrationComplete) return;
2553
+ const migrated = await legacyStorageDriver.getItem(
2554
+ MIGRATION_MARKER_KEY2,
2555
+ false
2556
+ );
2557
+ if (migrated) {
2558
+ migrationComplete = true;
2559
+ return;
2560
+ }
2561
+ const legacyEntries = await legacyStorageDriver.getItem(
2562
+ SDK_STORAGE_KEYS.USAGE_TRACKING,
2563
+ []
2564
+ );
2565
+ for (const entry of legacyEntries) {
2566
+ appendOne(entry);
2567
+ }
2568
+ if (legacyEntries.length > 0) {
2569
+ await legacyStorageDriver.removeItem(SDK_STORAGE_KEYS.USAGE_TRACKING);
2570
+ }
2571
+ await legacyStorageDriver.setItem(MIGRATION_MARKER_KEY2, true);
2572
+ migrationComplete = true;
2573
+ };
2574
+ const mapRow = (row) => ({
2575
+ id: row.id,
2576
+ timestamp: row.timestamp,
2577
+ modelId: row.model_id,
2578
+ baseUrl: row.base_url,
2579
+ requestId: row.request_id,
2580
+ cost: row.cost,
2581
+ satsCost: row.sats_cost,
2582
+ promptTokens: row.prompt_tokens,
2583
+ completionTokens: row.completion_tokens,
2584
+ totalTokens: row.total_tokens,
2585
+ client: row.client ?? void 0,
2586
+ sessionId: row.session_id ?? void 0,
2587
+ tags: typeof row.tags === "string" ? JSON.parse(row.tags) : void 0
2588
+ });
2589
+ return {
2590
+ async migrate() {
2591
+ await ensureMigrated();
2592
+ },
2593
+ async append(entry) {
2594
+ await ensureMigrated();
2595
+ appendOne(entry);
2596
+ },
2597
+ async appendMany(entries) {
2598
+ await ensureMigrated();
2599
+ for (const entry of entries) {
2600
+ appendOne(entry);
2601
+ }
2602
+ },
2603
+ async list(options2 = {}) {
2604
+ await ensureMigrated();
2605
+ const { sql, params } = buildWhereClause(options2);
2606
+ const limitSql = typeof options2.limit === "number" ? " LIMIT ?" : "";
2607
+ const stmt = db.prepare(
2608
+ `SELECT * FROM ${tableName} ${sql} ORDER BY timestamp DESC${limitSql}`
2609
+ );
2610
+ const rows = stmt.all(
2611
+ ...typeof options2.limit === "number" ? [...params, options2.limit] : params
2612
+ );
2613
+ return rows.map(mapRow);
2614
+ },
2615
+ async count(options2 = {}) {
2616
+ await ensureMigrated();
2617
+ const { sql, params } = buildWhereClause(options2);
2618
+ const stmt = db.prepare(`SELECT COUNT(*) as count FROM ${tableName} ${sql}`);
2619
+ const row = stmt.get(...params);
2620
+ return Number(row?.count ?? 0);
2621
+ },
2622
+ async deleteOlderThan(timestamp) {
2623
+ await ensureMigrated();
2624
+ const stmt = db.prepare(`DELETE FROM ${tableName} WHERE timestamp < ?`);
2625
+ const result = stmt.run(timestamp);
2626
+ return result.changes;
2627
+ },
2628
+ async clear() {
2629
+ await ensureMigrated();
2630
+ db.prepare(`DELETE FROM ${tableName}`).run();
2631
+ }
2632
+ };
2633
+ };
2634
+
2635
+ // storage/usageTracking/memory.ts
2636
+ var normalizeBaseUrl3 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
2637
+ var matchesFilters2 = (entry, options = {}) => {
2638
+ if (typeof options.before === "number" && entry.timestamp >= options.before) {
2639
+ return false;
2640
+ }
2641
+ if (typeof options.after === "number" && entry.timestamp <= options.after) {
2642
+ return false;
2643
+ }
2644
+ if (options.modelId && entry.modelId !== options.modelId) {
2645
+ return false;
2646
+ }
2647
+ if (options.baseUrl && normalizeBaseUrl3(entry.baseUrl) !== normalizeBaseUrl3(options.baseUrl)) {
2648
+ return false;
2649
+ }
2650
+ if (options.sessionId && entry.sessionId !== options.sessionId) {
2651
+ return false;
2652
+ }
2653
+ if (options.client && entry.client !== options.client) {
2654
+ return false;
2655
+ }
2656
+ return true;
2657
+ };
2658
+ var createMemoryUsageTrackingDriver = (seed = []) => {
2659
+ const store = /* @__PURE__ */ new Map();
2660
+ for (const entry of seed) {
2661
+ store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl3(entry.baseUrl) });
2662
+ }
2663
+ return {
2664
+ async migrate() {
2665
+ return;
2666
+ },
2667
+ async append(entry) {
2668
+ store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl3(entry.baseUrl) });
2669
+ },
2670
+ async appendMany(entries) {
2671
+ for (const entry of entries) {
2672
+ store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl3(entry.baseUrl) });
2673
+ }
2674
+ },
2675
+ async list(options = {}) {
2676
+ const entries = [...store.values()].filter((entry) => matchesFilters2(entry, options)).sort((a, b) => b.timestamp - a.timestamp);
2677
+ if (typeof options.limit === "number") {
2678
+ return entries.slice(0, options.limit);
2679
+ }
2680
+ return entries;
2681
+ },
2682
+ async count(options = {}) {
2683
+ return (await this.list(options)).length;
2684
+ },
2685
+ async deleteOlderThan(timestamp) {
2686
+ let deleted = 0;
2687
+ for (const [id, entry] of store.entries()) {
2688
+ if (entry.timestamp < timestamp) {
2689
+ store.delete(id);
2690
+ deleted += 1;
2691
+ }
2692
+ }
2693
+ return deleted;
2694
+ },
2695
+ async clear() {
2696
+ store.clear();
2697
+ }
2698
+ };
2699
+ };
2700
+ var normalizeBaseUrl4 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
2701
+ var getCashuTokenBalance = (token) => {
2702
+ try {
2703
+ const decoded = cashuTs.getDecodedToken(token);
2704
+ const unitDivisor = decoded.unit === "msat" ? 1e3 : 1;
2705
+ let sum = 0;
2706
+ for (const proof of decoded.proofs) {
2707
+ sum += proof.amount / unitDivisor;
2708
+ }
2709
+ return sum;
2710
+ } catch {
2711
+ return 0;
2712
+ }
2713
+ };
2714
+ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
2715
+ modelsFromAllProviders: {},
2716
+ lastUsedModel: null,
2717
+ baseUrlsList: [],
2718
+ lastBaseUrlsUpdate: null,
2719
+ disabledProviders: [],
2720
+ mintsFromAllProviders: {},
2721
+ infoFromAllProviders: {},
2722
+ lastModelsUpdate: {},
2723
+ cachedTokens: [],
2724
+ apiKeys: [],
2725
+ childKeys: [],
2726
+ routstr21Models: [],
2727
+ lastRoutstr21ModelsUpdate: null,
2728
+ cachedReceiveTokens: [],
2729
+ clientIds: [],
2730
+ setModelsFromAllProviders: (value) => {
2731
+ const normalized = {};
2732
+ for (const [baseUrl, models] of Object.entries(value)) {
2733
+ normalized[normalizeBaseUrl4(baseUrl)] = models;
2734
+ }
2735
+ void driver.setItem(
2736
+ SDK_STORAGE_KEYS.MODELS_FROM_ALL_PROVIDERS,
2737
+ normalized
2738
+ );
2739
+ set({ modelsFromAllProviders: normalized });
2740
+ },
2741
+ setLastUsedModel: (value) => {
2742
+ void driver.setItem(SDK_STORAGE_KEYS.LAST_USED_MODEL, value);
2743
+ set({ lastUsedModel: value });
2744
+ },
2745
+ setBaseUrlsList: (value) => {
2746
+ const normalized = value.map((url) => normalizeBaseUrl4(url));
2747
+ void driver.setItem(SDK_STORAGE_KEYS.BASE_URLS_LIST, normalized);
2748
+ set({ baseUrlsList: normalized });
2749
+ },
2750
+ setBaseUrlsLastUpdate: (value) => {
2751
+ void driver.setItem(SDK_STORAGE_KEYS.LAST_BASE_URLS_UPDATE, value);
2752
+ set({ lastBaseUrlsUpdate: value });
2753
+ },
2754
+ setDisabledProviders: (value) => {
2755
+ const normalized = value.map((url) => normalizeBaseUrl4(url));
2756
+ void driver.setItem(SDK_STORAGE_KEYS.DISABLED_PROVIDERS, normalized);
2757
+ set({ disabledProviders: normalized });
2758
+ },
2759
+ setMintsFromAllProviders: (value) => {
2760
+ const normalized = {};
2761
+ for (const [baseUrl, mints] of Object.entries(value)) {
2762
+ normalized[normalizeBaseUrl4(baseUrl)] = mints.map(
2763
+ (mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint
2764
+ );
2765
+ }
2766
+ void driver.setItem(
2767
+ SDK_STORAGE_KEYS.MINTS_FROM_ALL_PROVIDERS,
2768
+ normalized
2769
+ );
2770
+ set({ mintsFromAllProviders: normalized });
2771
+ },
2772
+ setInfoFromAllProviders: (value) => {
2773
+ const normalized = {};
2774
+ for (const [baseUrl, info] of Object.entries(value)) {
2775
+ normalized[normalizeBaseUrl4(baseUrl)] = info;
2776
+ }
2777
+ void driver.setItem(SDK_STORAGE_KEYS.INFO_FROM_ALL_PROVIDERS, normalized);
2778
+ set({ infoFromAllProviders: normalized });
2779
+ },
2780
+ setLastModelsUpdate: (value) => {
2781
+ const normalized = {};
2782
+ for (const [baseUrl, timestamp] of Object.entries(value)) {
2783
+ normalized[normalizeBaseUrl4(baseUrl)] = timestamp;
2784
+ }
2785
+ void driver.setItem(SDK_STORAGE_KEYS.LAST_MODELS_UPDATE, normalized);
2786
+ set({ lastModelsUpdate: normalized });
2787
+ },
2788
+ setCachedTokens: (value) => {
2789
+ set((state) => {
2790
+ const updates = typeof value === "function" ? value(state.cachedTokens) : value;
2791
+ const normalized = updates.map((entry) => ({
2792
+ ...entry,
2793
+ baseUrl: normalizeBaseUrl4(entry.baseUrl),
2794
+ balance: typeof entry.balance === "number" ? entry.balance : getCashuTokenBalance(entry.token),
2795
+ lastUsed: entry.lastUsed ?? null
2796
+ }));
2797
+ void driver.setItem(SDK_STORAGE_KEYS.LOCAL_CASHU_TOKENS, normalized);
2798
+ return { cachedTokens: normalized };
2799
+ });
2800
+ },
2801
+ setApiKeys: (value) => {
2802
+ set((state) => {
2803
+ const updates = typeof value === "function" ? value(state.apiKeys) : value;
2804
+ const normalized = updates.map((entry) => ({
2805
+ ...entry,
2806
+ baseUrl: normalizeBaseUrl4(entry.baseUrl),
2807
+ balance: entry.balance ?? 0,
2808
+ lastUsed: entry.lastUsed ?? null
2809
+ }));
2810
+ void driver.setItem(SDK_STORAGE_KEYS.API_KEYS, normalized);
2811
+ return { apiKeys: normalized };
2812
+ });
2813
+ },
2814
+ setChildKeys: (value) => {
2815
+ set((state) => {
2816
+ const updates = typeof value === "function" ? value(state.childKeys) : value;
2817
+ const normalized = updates.map((entry) => ({
2818
+ parentBaseUrl: normalizeBaseUrl4(entry.parentBaseUrl),
2819
+ childKey: entry.childKey,
2820
+ balance: entry.balance ?? 0,
2821
+ balanceLimit: entry.balanceLimit,
2822
+ validityDate: entry.validityDate,
2823
+ createdAt: entry.createdAt ?? Date.now()
2824
+ }));
2825
+ void driver.setItem(SDK_STORAGE_KEYS.CHILD_KEYS, normalized);
2826
+ return { childKeys: normalized };
2827
+ });
2828
+ },
2829
+ setRoutstr21Models: (value) => {
2830
+ void driver.setItem(SDK_STORAGE_KEYS.ROUTSTR21_MODELS, value);
2831
+ set({ routstr21Models: value });
2832
+ },
2833
+ setRoutstr21ModelsLastUpdate: (value) => {
2834
+ void driver.setItem(SDK_STORAGE_KEYS.LAST_ROUTSTR21_MODELS_UPDATE, value);
2835
+ set({ lastRoutstr21ModelsUpdate: value });
2836
+ },
2837
+ setCachedReceiveTokens: (value) => {
2838
+ const normalized = value.map((entry) => ({
2839
+ token: entry.token,
2840
+ amount: entry.amount,
2841
+ unit: entry.unit || "sat",
2842
+ createdAt: entry.createdAt ?? Date.now()
2843
+ }));
2844
+ void driver.setItem(SDK_STORAGE_KEYS.CACHED_RECEIVE_TOKENS, normalized);
2845
+ set({ cachedReceiveTokens: normalized });
2846
+ },
2847
+ setClientIds: (value) => {
2848
+ set((state) => {
2849
+ const updates = typeof value === "function" ? value(state.clientIds) : value;
2850
+ const normalized = updates.map((entry) => ({
2851
+ ...entry,
2852
+ createdAt: entry.createdAt ?? Date.now(),
2853
+ lastUsed: entry.lastUsed ?? null
2854
+ }));
2855
+ void driver.setItem(SDK_STORAGE_KEYS.CLIENT_IDS, normalized);
2856
+ return { clientIds: normalized };
2857
+ });
2858
+ }
2859
+ }));
2860
+ var hydrateStoreFromDriver = async (store, driver) => {
2861
+ const [
2862
+ rawModels,
2863
+ lastUsedModel,
2864
+ rawBaseUrls,
2865
+ lastBaseUrlsUpdate,
2866
+ rawDisabledProviders,
2867
+ rawMints,
2868
+ rawInfo,
2869
+ rawLastModelsUpdate,
2870
+ rawCachedTokens,
2871
+ rawApiKeys,
2872
+ rawChildKeys,
2873
+ rawRoutstr21Models,
2874
+ rawLastRoutstr21ModelsUpdate,
2875
+ rawCachedReceiveTokens,
2876
+ rawClientIds
2877
+ ] = await Promise.all([
2878
+ driver.getItem(
2879
+ SDK_STORAGE_KEYS.MODELS_FROM_ALL_PROVIDERS,
2880
+ {}
2881
+ ),
2882
+ driver.getItem(SDK_STORAGE_KEYS.LAST_USED_MODEL, null),
2883
+ driver.getItem(SDK_STORAGE_KEYS.BASE_URLS_LIST, []),
2884
+ driver.getItem(SDK_STORAGE_KEYS.LAST_BASE_URLS_UPDATE, null),
2885
+ driver.getItem(SDK_STORAGE_KEYS.DISABLED_PROVIDERS, []),
2886
+ driver.getItem(
2887
+ SDK_STORAGE_KEYS.MINTS_FROM_ALL_PROVIDERS,
2888
+ {}
2889
+ ),
2890
+ driver.getItem(
2891
+ SDK_STORAGE_KEYS.INFO_FROM_ALL_PROVIDERS,
2892
+ {}
2893
+ ),
2894
+ driver.getItem(
2895
+ SDK_STORAGE_KEYS.LAST_MODELS_UPDATE,
2896
+ {}
2897
+ ),
2898
+ driver.getItem(SDK_STORAGE_KEYS.LOCAL_CASHU_TOKENS, []),
2899
+ driver.getItem(SDK_STORAGE_KEYS.API_KEYS, []),
2900
+ driver.getItem(SDK_STORAGE_KEYS.CHILD_KEYS, []),
2901
+ driver.getItem(SDK_STORAGE_KEYS.ROUTSTR21_MODELS, []),
2902
+ driver.getItem(
2903
+ SDK_STORAGE_KEYS.LAST_ROUTSTR21_MODELS_UPDATE,
2904
+ null
2905
+ ),
2906
+ driver.getItem(SDK_STORAGE_KEYS.CACHED_RECEIVE_TOKENS, []),
2907
+ driver.getItem(SDK_STORAGE_KEYS.CLIENT_IDS, [])
2908
+ ]);
2909
+ const modelsFromAllProviders = Object.fromEntries(
2910
+ Object.entries(rawModels).map(([baseUrl, models]) => [
2911
+ normalizeBaseUrl4(baseUrl),
2912
+ models
2913
+ ])
2914
+ );
2915
+ const baseUrlsList = rawBaseUrls.map((url) => normalizeBaseUrl4(url));
2916
+ const disabledProviders = rawDisabledProviders.map(
2917
+ (url) => normalizeBaseUrl4(url)
2918
+ );
2919
+ const mintsFromAllProviders = Object.fromEntries(
2920
+ Object.entries(rawMints).map(([baseUrl, mints]) => [
2921
+ normalizeBaseUrl4(baseUrl),
2922
+ mints.map((mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint)
2923
+ ])
2924
+ );
2925
+ const infoFromAllProviders = Object.fromEntries(
2926
+ Object.entries(rawInfo).map(([baseUrl, info]) => [
2927
+ normalizeBaseUrl4(baseUrl),
2928
+ info
2929
+ ])
2930
+ );
2931
+ const lastModelsUpdate = Object.fromEntries(
2932
+ Object.entries(rawLastModelsUpdate).map(([baseUrl, timestamp]) => [
2933
+ normalizeBaseUrl4(baseUrl),
2934
+ timestamp
2935
+ ])
2936
+ );
2937
+ const cachedTokens = rawCachedTokens.map((entry) => ({
2938
+ ...entry,
2939
+ baseUrl: normalizeBaseUrl4(entry.baseUrl),
2940
+ balance: typeof entry.balance === "number" ? entry.balance : getCashuTokenBalance(entry.token),
2941
+ lastUsed: entry.lastUsed ?? null
2942
+ }));
2943
+ const apiKeys = rawApiKeys.map((entry) => ({
2944
+ ...entry,
2945
+ baseUrl: normalizeBaseUrl4(entry.baseUrl),
2946
+ balance: entry.balance ?? 0,
2947
+ lastUsed: entry.lastUsed ?? null
2948
+ }));
2949
+ const childKeys = rawChildKeys.map((entry) => ({
2950
+ parentBaseUrl: normalizeBaseUrl4(entry.parentBaseUrl),
2951
+ childKey: entry.childKey,
2952
+ balance: entry.balance ?? 0,
2953
+ balanceLimit: entry.balanceLimit,
2954
+ validityDate: entry.validityDate,
2955
+ createdAt: entry.createdAt ?? Date.now()
2956
+ }));
2957
+ const routstr21Models = rawRoutstr21Models;
2958
+ const lastRoutstr21ModelsUpdate = rawLastRoutstr21ModelsUpdate;
2959
+ const cachedReceiveTokens = rawCachedReceiveTokens?.map((entry) => ({
2960
+ token: entry.token,
2961
+ amount: entry.amount,
2962
+ unit: entry.unit || "sat",
2963
+ createdAt: entry.createdAt ?? Date.now()
2964
+ }));
2965
+ const clientIds = rawClientIds.map((entry) => ({
2966
+ ...entry,
2967
+ createdAt: entry.createdAt ?? Date.now(),
2968
+ lastUsed: entry.lastUsed ?? null
2969
+ }));
2970
+ store.setState({
2971
+ modelsFromAllProviders,
2972
+ lastUsedModel,
2973
+ baseUrlsList,
2974
+ lastBaseUrlsUpdate,
2975
+ disabledProviders,
2976
+ mintsFromAllProviders,
2977
+ infoFromAllProviders,
2978
+ lastModelsUpdate,
2979
+ cachedTokens,
2980
+ apiKeys,
2981
+ childKeys,
2982
+ routstr21Models,
2983
+ lastRoutstr21ModelsUpdate,
2984
+ cachedReceiveTokens,
2985
+ clientIds
2986
+ });
2987
+ };
2988
+ var createSdkStore = ({
2989
+ driver
2990
+ }) => {
2991
+ const store = createEmptyStore(driver);
2992
+ return {
2993
+ store,
2994
+ hydrate: hydrateStoreFromDriver(store, driver)
2995
+ };
2996
+ };
2997
+
2998
+ // storage/index.ts
2999
+ var isBrowser2 = () => {
3000
+ try {
3001
+ return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
3002
+ } catch {
3003
+ return false;
3004
+ }
3005
+ };
3006
+ var isNode = () => {
3007
+ try {
3008
+ return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
3009
+ } catch {
3010
+ return false;
3011
+ }
3012
+ };
3013
+ var defaultDriver = null;
3014
+ var isBun3 = () => {
3015
+ return typeof process.versions.bun !== "undefined";
3016
+ };
3017
+ var getDefaultSdkDriver = () => {
3018
+ if (defaultDriver) return defaultDriver;
3019
+ if (isBrowser2()) {
3020
+ defaultDriver = localStorageDriver;
3021
+ return defaultDriver;
3022
+ }
3023
+ if (isBun3()) {
3024
+ defaultDriver = createMemoryDriver();
3025
+ return defaultDriver;
3026
+ }
3027
+ if (isNode()) {
3028
+ defaultDriver = createSqliteDriver();
3029
+ return defaultDriver;
3030
+ }
3031
+ defaultDriver = createMemoryDriver();
3032
+ return defaultDriver;
3033
+ };
3034
+ var defaultStore = null;
3035
+ var defaultUsageTrackingDriver = null;
3036
+ var getDefaultSdkStore = () => {
3037
+ if (!defaultStore) {
3038
+ defaultStore = createSdkStore({ driver: getDefaultSdkDriver() });
3039
+ }
3040
+ return defaultStore.hydrate.then(() => defaultStore.store);
3041
+ };
3042
+ var getDefaultUsageTrackingDriver = () => {
3043
+ if (defaultUsageTrackingDriver) return defaultUsageTrackingDriver;
3044
+ const storageDriver = getDefaultSdkDriver();
3045
+ if (isBrowser2()) {
3046
+ defaultUsageTrackingDriver = createIndexedDBUsageTrackingDriver({
3047
+ legacyStorageDriver: storageDriver
3048
+ });
3049
+ return defaultUsageTrackingDriver;
3050
+ }
3051
+ if (isBun3()) {
3052
+ defaultUsageTrackingDriver = createMemoryUsageTrackingDriver();
3053
+ return defaultUsageTrackingDriver;
3054
+ }
3055
+ if (isNode()) {
3056
+ defaultUsageTrackingDriver = createSqliteUsageTrackingDriver({
3057
+ legacyStorageDriver: storageDriver
3058
+ });
3059
+ return defaultUsageTrackingDriver;
3060
+ }
3061
+ defaultUsageTrackingDriver = createMemoryUsageTrackingDriver();
3062
+ return defaultUsageTrackingDriver;
3063
+ };
3064
+ function createSSEParserTransform(onUsage, onResponseId) {
3065
+ let buffer = "";
3066
+ const maybeCaptureUsageFromJson = (jsonText) => {
3067
+ try {
3068
+ const data = JSON.parse(jsonText);
3069
+ const responseId = data.id;
3070
+ if (typeof responseId === "string" && responseId.trim().length > 0) {
3071
+ onResponseId?.(responseId.trim());
3072
+ }
3073
+ const usage = extractUsageFromSSEJson(data);
3074
+ if (usage) {
3075
+ onUsage(usage);
3076
+ }
3077
+ } catch {
3078
+ }
3079
+ };
3080
+ const processLine = (self, line) => {
3081
+ const trimmed = line.trim();
3082
+ if (!trimmed) {
3083
+ return;
3084
+ }
3085
+ if (trimmed === "data: [DONE]" || trimmed === "[DONE]") {
3086
+ self.push("data: [DONE]\n\n");
3087
+ return;
3088
+ }
3089
+ if (trimmed.startsWith("data:")) {
3090
+ const dataStr = trimmed.startsWith("data: ") ? trimmed.slice(6) : trimmed.slice(5).trimStart();
3091
+ if (dataStr === "[DONE]") {
3092
+ self.push("data: [DONE]\n\n");
3093
+ return;
3094
+ }
3095
+ maybeCaptureUsageFromJson(dataStr);
3096
+ self.push(`data: ${dataStr}
3097
+
3098
+ `);
3099
+ return;
3100
+ }
3101
+ if (trimmed.startsWith("{")) {
3102
+ maybeCaptureUsageFromJson(trimmed);
3103
+ self.push(`data: ${trimmed}
3104
+
3105
+ `);
3106
+ return;
3107
+ }
3108
+ self.push(line + "\n");
3109
+ };
3110
+ return new stream.Transform({
3111
+ transform(chunk, encoding, callback) {
3112
+ buffer += chunk.toString();
3113
+ const lines = buffer.split(/\r?\n/);
3114
+ buffer = lines.pop() || "";
3115
+ for (const line of lines) {
3116
+ processLine(this, line);
3117
+ }
3118
+ callback();
3119
+ },
3120
+ flush(callback) {
3121
+ if (buffer.trim()) {
3122
+ processLine(this, buffer);
3123
+ }
3124
+ buffer = "";
3125
+ callback();
3126
+ }
3127
+ });
3128
+ }
1971
3129
  var TOPUP_MARGIN = 1.2;
1972
3130
  var RoutstrClient = class {
1973
3131
  constructor(walletAdapter, storageAdapter, providerRegistry, alertLevel, mode = "xcashu") {
@@ -1988,7 +3146,13 @@ var RoutstrClient = class {
1988
3146
  this.streamProcessor = new StreamProcessor();
1989
3147
  this.providerManager = new ProviderManager(providerRegistry);
1990
3148
  this.alertLevel = alertLevel;
1991
- this.mode = mode;
3149
+ if (mode === "lazyrefund") {
3150
+ this.mode = "apikeys";
3151
+ } else if (mode === "apikeys") {
3152
+ this.mode = "lazyrefund";
3153
+ } else {
3154
+ this.mode = mode;
3155
+ }
1992
3156
  }
1993
3157
  cashuSpender;
1994
3158
  balanceManager;
@@ -2062,6 +3226,80 @@ var RoutstrClient = class {
2062
3226
  * requests and get responses back.
2063
3227
  */
2064
3228
  async routeRequest(params) {
3229
+ const prepared = await this._prepareRoutedRequest(params);
3230
+ const satsSpent = await this._handlePostResponseBalanceUpdate({
3231
+ token: prepared.tokenUsed,
3232
+ baseUrl: prepared.baseUrlUsed,
3233
+ initialTokenBalance: prepared.tokenBalanceInSats,
3234
+ response: prepared.response,
3235
+ modelId: prepared.modelId,
3236
+ usage: prepared.capturedUsage,
3237
+ requestId: prepared.capturedResponseId
3238
+ });
3239
+ prepared.response.satsSpent = satsSpent;
3240
+ prepared.response.usage = prepared.capturedUsage;
3241
+ prepared.response.requestId = prepared.capturedResponseId;
3242
+ return prepared.response;
3243
+ }
3244
+ async routeRequestToNodeResponse(params) {
3245
+ const { res } = params;
3246
+ const prepared = await this._prepareRoutedRequest(params);
3247
+ res.statusCode = prepared.response.status;
3248
+ prepared.response.headers.forEach((value, key) => {
3249
+ res.setHeader(key, value);
3250
+ });
3251
+ const body = prepared.response.body;
3252
+ if (!body) {
3253
+ const satsSpent = await this._handlePostResponseBalanceUpdate({
3254
+ token: prepared.tokenUsed,
3255
+ baseUrl: prepared.baseUrlUsed,
3256
+ initialTokenBalance: prepared.tokenBalanceInSats,
3257
+ response: prepared.response,
3258
+ modelId: prepared.modelId,
3259
+ usage: prepared.capturedUsage,
3260
+ requestId: prepared.capturedResponseId
3261
+ });
3262
+ prepared.response.satsSpent = satsSpent;
3263
+ res.end();
3264
+ return;
3265
+ }
3266
+ const nodeReadable = stream.Readable.fromWeb(body);
3267
+ await new Promise((resolve, reject) => {
3268
+ let settled = false;
3269
+ const finish = async () => {
3270
+ if (settled) return;
3271
+ settled = true;
3272
+ try {
3273
+ const satsSpent = await this._handlePostResponseBalanceUpdate({
3274
+ token: prepared.tokenUsed,
3275
+ baseUrl: prepared.baseUrlUsed,
3276
+ initialTokenBalance: prepared.tokenBalanceInSats,
3277
+ response: prepared.response,
3278
+ modelId: prepared.modelId,
3279
+ usage: prepared.capturedUsage,
3280
+ requestId: prepared.capturedResponseId
3281
+ });
3282
+ prepared.response.satsSpent = satsSpent;
3283
+ prepared.response.usage = prepared.capturedUsage;
3284
+ prepared.response.requestId = prepared.capturedResponseId;
3285
+ resolve();
3286
+ } catch (error) {
3287
+ reject(error);
3288
+ }
3289
+ };
3290
+ const fail = (error) => {
3291
+ if (settled) return;
3292
+ settled = true;
3293
+ reject(error);
3294
+ };
3295
+ res.once("finish", finish);
3296
+ res.once("close", finish);
3297
+ res.once("error", fail);
3298
+ nodeReadable.once("error", fail);
3299
+ nodeReadable.pipe(res);
3300
+ });
3301
+ }
3302
+ async _prepareRoutedRequest(params) {
2065
3303
  const {
2066
3304
  path,
2067
3305
  method,
@@ -2117,13 +3355,43 @@ var RoutstrClient = class {
2117
3355
  const tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
2118
3356
  const baseUrlUsed = response.baseUrl || baseUrl;
2119
3357
  const tokenUsed = response.token || token;
2120
- await this._handlePostResponseBalanceUpdate({
2121
- token: tokenUsed,
2122
- baseUrl: baseUrlUsed,
2123
- initialTokenBalance: tokenBalanceInSats,
2124
- response
2125
- });
2126
- return response;
3358
+ const contentType = response.headers.get("content-type") || "";
3359
+ let processedResponse = response;
3360
+ let capturedUsage;
3361
+ let capturedResponseId;
3362
+ if (contentType.includes("text/event-stream") && response.body) {
3363
+ const nodeReadable = stream.Readable.fromWeb(response.body);
3364
+ const sseParser = createSSEParserTransform(
3365
+ (usage) => {
3366
+ capturedUsage = usage;
3367
+ processedResponse.usage = usage;
3368
+ },
3369
+ (responseId) => {
3370
+ capturedResponseId = responseId;
3371
+ processedResponse.requestId = responseId;
3372
+ }
3373
+ );
3374
+ const transformed = nodeReadable.pipe(sseParser, { end: true });
3375
+ const webStream = stream.Readable.toWeb(
3376
+ transformed
3377
+ );
3378
+ processedResponse = new Response(webStream, {
3379
+ status: response.status,
3380
+ statusText: response.statusText,
3381
+ headers: response.headers
3382
+ });
3383
+ processedResponse.baseUrl = response.baseUrl;
3384
+ processedResponse.token = response.token;
3385
+ }
3386
+ return {
3387
+ response: processedResponse,
3388
+ tokenUsed,
3389
+ baseUrlUsed,
3390
+ tokenBalanceInSats,
3391
+ modelId,
3392
+ capturedUsage,
3393
+ capturedResponseId
3394
+ };
2127
3395
  }
2128
3396
  /**
2129
3397
  * Fetch AI response with streaming
@@ -2230,7 +3498,18 @@ var RoutstrClient = class {
2230
3498
  baseUrl: baseUrlUsed,
2231
3499
  initialTokenBalance: tokenBalanceInSats,
2232
3500
  fallbackSatsSpent: isApikeysEstimate ? this._getEstimatedCosts(selectedModel, streamingResult) : void 0,
2233
- response
3501
+ response,
3502
+ modelId: selectedModel.id,
3503
+ usage: streamingResult.usage ? {
3504
+ promptTokens: Number(streamingResult.usage.prompt_tokens ?? 0),
3505
+ completionTokens: Number(
3506
+ streamingResult.usage.completion_tokens ?? 0
3507
+ ),
3508
+ totalTokens: Number(streamingResult.usage.total_tokens ?? 0),
3509
+ cost: Number(streamingResult.usage.cost ?? 0),
3510
+ satsCost: Number(streamingResult.usage.sats_cost ?? 0)
3511
+ } : void 0,
3512
+ requestId: streamingResult.responseId
2234
3513
  });
2235
3514
  const estimatedCosts = this._getEstimatedCosts(
2236
3515
  selectedModel,
@@ -2647,7 +3926,16 @@ var RoutstrClient = class {
2647
3926
  * Handle post-response balance update for all modes
2648
3927
  */
2649
3928
  async _handlePostResponseBalanceUpdate(params) {
2650
- const { token, baseUrl, initialTokenBalance, fallbackSatsSpent, response } = params;
3929
+ const {
3930
+ token,
3931
+ baseUrl,
3932
+ initialTokenBalance,
3933
+ fallbackSatsSpent,
3934
+ response,
3935
+ modelId,
3936
+ usage,
3937
+ requestId
3938
+ } = params;
2651
3939
  let satsSpent = initialTokenBalance;
2652
3940
  if (this.mode === "xcashu" && response) {
2653
3941
  const refundToken = response.headers.get("x-cashu") ?? void 0;
@@ -2694,8 +3982,75 @@ var RoutstrClient = class {
2694
3982
  satsSpent = fallbackSatsSpent ?? initialTokenBalance;
2695
3983
  }
2696
3984
  }
3985
+ await this._trackResponseUsage({
3986
+ token,
3987
+ baseUrl,
3988
+ response,
3989
+ modelId,
3990
+ satsSpent,
3991
+ usage,
3992
+ requestId
3993
+ });
2697
3994
  return satsSpent;
2698
3995
  }
3996
+ async _trackResponseUsage(params) {
3997
+ const {
3998
+ token,
3999
+ baseUrl,
4000
+ response,
4001
+ modelId,
4002
+ satsSpent,
4003
+ usage: providedUsage,
4004
+ requestId: providedRequestId
4005
+ } = params;
4006
+ if (!response || !modelId) {
4007
+ return;
4008
+ }
4009
+ try {
4010
+ let usage = providedUsage;
4011
+ let requestId = providedRequestId;
4012
+ if (!usage || !requestId) {
4013
+ const contentType = response.headers.get("content-type") || "";
4014
+ if (contentType.includes("text/event-stream")) {
4015
+ usage = usage ?? response.usage;
4016
+ requestId = requestId ?? response.requestId ?? response.headers.get("x-routstr-request-id") ?? void 0;
4017
+ if (!usage) {
4018
+ return;
4019
+ }
4020
+ } else {
4021
+ const cloned = response.clone();
4022
+ const responseBody = await cloned.json();
4023
+ usage = usage ?? extractUsageFromResponseBody(responseBody, satsSpent) ?? void 0;
4024
+ requestId = requestId ?? extractResponseId(responseBody) ?? response.headers.get("x-routstr-request-id") ?? void 0;
4025
+ }
4026
+ }
4027
+ if (!usage) {
4028
+ return;
4029
+ }
4030
+ const finalRequestId = requestId || "unknown";
4031
+ const store = await getDefaultSdkStore();
4032
+ const state = store.getState();
4033
+ const matchingClient = state.clientIds.find(
4034
+ (client) => client.apiKey === token
4035
+ );
4036
+ const entryId = finalRequestId === "unknown" ? `req-${Date.now()}-${modelId}` : finalRequestId;
4037
+ const usageTracking = getDefaultUsageTrackingDriver();
4038
+ const entry = {
4039
+ id: entryId,
4040
+ timestamp: Date.now(),
4041
+ modelId,
4042
+ baseUrl,
4043
+ requestId: finalRequestId,
4044
+ client: matchingClient?.clientId,
4045
+ ...usage
4046
+ };
4047
+ if (this.mode === "xcashu") {
4048
+ entry.satsCost = satsSpent;
4049
+ }
4050
+ await usageTracking.append(entry);
4051
+ } catch (error) {
4052
+ }
4053
+ }
2699
4054
  /**
2700
4055
  * Convert messages for API format
2701
4056
  */
@@ -2740,37 +4095,6 @@ var RoutstrClient = class {
2740
4095
  content: result.content || ""
2741
4096
  };
2742
4097
  }
2743
- /**
2744
- * Create a child key for a parent API key via the provider's API
2745
- * POST /v1/balance/child-key
2746
- */
2747
- async _createChildKey(baseUrl, parentApiKey, options) {
2748
- const response = await fetch(`${baseUrl}v1/balance/child-key`, {
2749
- method: "POST",
2750
- headers: {
2751
- "Content-Type": "application/json",
2752
- Authorization: `Bearer ${parentApiKey}`
2753
- },
2754
- body: JSON.stringify({
2755
- count: options?.count ?? 1,
2756
- balance_limit: options?.balanceLimit,
2757
- balance_limit_reset: options?.balanceLimitReset,
2758
- validity_date: options?.validityDate
2759
- })
2760
- });
2761
- if (!response.ok) {
2762
- throw new Error(
2763
- `Failed to create child key: ${response.status} ${await response.text()}`
2764
- );
2765
- }
2766
- const data = await response.json();
2767
- return {
2768
- childKey: data.api_keys?.[0],
2769
- balance: data.balance ?? 0,
2770
- balanceLimit: data.balance_limit,
2771
- validityDate: data.validity_date
2772
- };
2773
- }
2774
4098
  /**
2775
4099
  * Calculate estimated costs from usage
2776
4100
  */
@@ -2984,5 +4308,6 @@ var RoutstrClient = class {
2984
4308
  exports.ProviderManager = ProviderManager;
2985
4309
  exports.RoutstrClient = RoutstrClient;
2986
4310
  exports.StreamProcessor = StreamProcessor;
4311
+ exports.createSSEParserTransform = createSSEParserTransform;
2987
4312
  //# sourceMappingURL=index.js.map
2988
4313
  //# sourceMappingURL=index.js.map