@routstr/sdk 0.3.9 → 0.3.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/browser.d.mts +12 -0
  2. package/dist/browser.d.ts +12 -0
  3. package/dist/browser.js +6413 -0
  4. package/dist/browser.js.map +1 -0
  5. package/dist/browser.mjs +6361 -0
  6. package/dist/browser.mjs.map +1 -0
  7. package/dist/bun.d.mts +29 -0
  8. package/dist/bun.d.ts +29 -0
  9. package/dist/bun.js +6791 -0
  10. package/dist/bun.js.map +1 -0
  11. package/dist/bun.mjs +6733 -0
  12. package/dist/bun.mjs.map +1 -0
  13. package/dist/bunSqlite-BmXWNc25.d.ts +18 -0
  14. package/dist/bunSqlite-Bro9efsl.d.mts +18 -0
  15. package/dist/client/index.d.mts +85 -42
  16. package/dist/client/index.d.ts +85 -42
  17. package/dist/client/index.js +1243 -1584
  18. package/dist/client/index.js.map +1 -1
  19. package/dist/client/index.mjs +1239 -1585
  20. package/dist/client/index.mjs.map +1 -1
  21. package/dist/discovery/index.d.mts +33 -3
  22. package/dist/discovery/index.d.ts +33 -3
  23. package/dist/discovery/index.js +30 -31
  24. package/dist/discovery/index.js.map +1 -1
  25. package/dist/discovery/index.mjs +30 -31
  26. package/dist/discovery/index.mjs.map +1 -1
  27. package/dist/index.d.mts +9 -7
  28. package/dist/index.d.ts +9 -7
  29. package/dist/index.js +1264 -1648
  30. package/dist/index.js.map +1 -1
  31. package/dist/index.mjs +1260 -1645
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/node.d.mts +22 -0
  34. package/dist/node.d.ts +22 -0
  35. package/dist/node.js +6857 -0
  36. package/dist/node.js.map +1 -0
  37. package/dist/node.mjs +6801 -0
  38. package/dist/node.mjs.map +1 -0
  39. package/dist/storage/bun.d.mts +16 -0
  40. package/dist/storage/bun.d.ts +16 -0
  41. package/dist/storage/bun.js +1970 -0
  42. package/dist/storage/bun.js.map +1 -0
  43. package/dist/storage/bun.mjs +1946 -0
  44. package/dist/storage/bun.mjs.map +1 -0
  45. package/dist/storage/index.d.mts +4 -30
  46. package/dist/storage/index.d.ts +4 -30
  47. package/dist/storage/index.js +238 -650
  48. package/dist/storage/index.js.map +1 -1
  49. package/dist/storage/index.mjs +239 -647
  50. package/dist/storage/index.mjs.map +1 -1
  51. package/dist/storage/node.d.mts +22 -0
  52. package/dist/storage/node.d.ts +22 -0
  53. package/dist/storage/node.js +2034 -0
  54. package/dist/storage/node.js.map +1 -0
  55. package/dist/storage/node.mjs +2012 -0
  56. package/dist/storage/node.mjs.map +1 -0
  57. package/dist/{store-58VcEUoA.d.ts → store-CAQLSbEj.d.ts} +52 -1
  58. package/dist/{store-C6dfj1cc.d.mts → store-CuXwe5Rg.d.mts} +52 -1
  59. package/dist/wallet/index.js +38 -24
  60. package/dist/wallet/index.js.map +1 -1
  61. package/dist/wallet/index.mjs +38 -24
  62. package/dist/wallet/index.mjs.map +1 -1
  63. package/package.json +26 -1
package/dist/index.js CHANGED
@@ -139,9 +139,11 @@ var MintDiscoveryError = class extends Error {
139
139
  }
140
140
  baseUrl;
141
141
  };
142
- function isBunRuntime() {
143
- return typeof Bun !== "undefined";
144
- }
142
+ var DEFAULT_NOSTR_RELAYS = [
143
+ "wss://relay.damus.io",
144
+ "wss://nos.lol",
145
+ "wss://relay.routstr.com"
146
+ ];
145
147
  var ModelManager = class _ModelManager {
146
148
  constructor(adapter, config = {}) {
147
149
  this.adapter = adapter;
@@ -150,8 +152,10 @@ var ModelManager = class _ModelManager {
150
152
  this.includeProviderUrls = config.includeProviderUrls || [];
151
153
  this.excludeProviderUrls = config.excludeProviderUrls || [];
152
154
  this.routstrPubkey = config.routstrPubkey || "4ad6fa2d16e2a9b576c863b4cf7404a70d4dc320c0c447d10ad6ff58993eacc8";
155
+ this.nostrRelays = config.nostrRelays;
153
156
  this.logger = (config.logger ?? consoleLogger).child("ModelManager");
154
157
  this.eventStoreDbPath = config.eventStoreDbPath;
158
+ this.persistentEventDatabaseFactory = config.persistentEventDatabaseFactory;
155
159
  }
156
160
  adapter;
157
161
  cacheTTL;
@@ -159,6 +163,7 @@ var ModelManager = class _ModelManager {
159
163
  includeProviderUrls;
160
164
  excludeProviderUrls;
161
165
  routstrPubkey;
166
+ nostrRelays;
162
167
  logger;
163
168
  providerNodePubkeysByUrl = /* @__PURE__ */ new Map();
164
169
  /** Persistent event store for relay-fetched events (null if not configured/initialized) */
@@ -166,6 +171,7 @@ var ModelManager = class _ModelManager {
166
171
  eventStoreDb = null;
167
172
  eventStoreInitPromise = null;
168
173
  eventStoreDbPath;
174
+ persistentEventDatabaseFactory;
169
175
  /**
170
176
  * Get the list of bootstrapped provider base URLs
171
177
  * @returns Array of provider base URLs
@@ -194,7 +200,7 @@ var ModelManager = class _ModelManager {
194
200
  } catch (error) {
195
201
  this.eventStoreInitPromise = null;
196
202
  throw new Error(
197
- `applesauce-sqlite with a supported SQLite driver is required for persistent Nostr event storage. Bun uses bun:sqlite; Node.js uses better-sqlite3. Install optional dependencies or omit eventStoreDbPath. (${error})`
203
+ `Persistent Nostr event storage requires a runtime-specific database factory. Use @routstr/sdk/node, @routstr/sdk/bun, inject persistentEventDatabaseFactory, or omit eventStoreDbPath. (${error})`
198
204
  );
199
205
  }
200
206
  })();
@@ -209,16 +215,15 @@ var ModelManager = class _ModelManager {
209
215
  return this.ensureEventStore();
210
216
  }
211
217
  async createPersistentEventDatabase() {
212
- if (isBunRuntime()) {
213
- const { BunSqliteEventDatabase } = await import('applesauce-sqlite/bun');
214
- return new BunSqliteEventDatabase(
215
- this.eventStoreDbPath
218
+ if (!this.eventStoreDbPath) {
219
+ throw new Error("eventStoreDbPath is required");
220
+ }
221
+ if (!this.persistentEventDatabaseFactory) {
222
+ throw new Error(
223
+ "persistentEventDatabaseFactory is required. Import ModelManager from @routstr/sdk/node or @routstr/sdk/bun for SQLite-backed persistent event storage."
216
224
  );
217
225
  }
218
- const { BetterSqlite3EventDatabase } = await import('applesauce-sqlite/better-sqlite3');
219
- return new BetterSqlite3EventDatabase(
220
- this.eventStoreDbPath
221
- );
226
+ return this.persistentEventDatabaseFactory(this.eventStoreDbPath);
222
227
  }
223
228
  /** Close the persistent event store database handle, if configured. */
224
229
  closeEventStore() {
@@ -331,6 +336,13 @@ var ModelManager = class _ModelManager {
331
336
  }
332
337
  return this.bootstrapFromHttp(torMode, forceRefresh);
333
338
  }
339
+ /**
340
+ * Resolve Nostr relay URLs.
341
+ * Returns user-configured relays if set, otherwise the shared defaults.
342
+ */
343
+ getNostrRelays() {
344
+ return this.nostrRelays && this.nostrRelays.length > 0 ? this.nostrRelays : DEFAULT_NOSTR_RELAYS;
345
+ }
334
346
  /**
335
347
  * Bootstrap providers from Nostr network (kind 38421)
336
348
  * @param kind The Nostr kind to fetch
@@ -338,11 +350,7 @@ var ModelManager = class _ModelManager {
338
350
  * @returns Array of provider base URLs
339
351
  */
340
352
  async bootstrapFromNostr(kind, torMode, forceRefresh = false) {
341
- const DEFAULT_RELAYS = [
342
- "wss://relay.primal.net",
343
- "wss://nos.lol",
344
- "wss://relay.damus.io"
345
- ];
353
+ const relays = this.getNostrRelays();
346
354
  const cached = await this.getCachedNostrEvents(
347
355
  { kinds: [kind] },
348
356
  this.cacheTTL,
@@ -353,7 +361,7 @@ var ModelManager = class _ModelManager {
353
361
  const pool = new applesauceRelay.RelayPool();
354
362
  const timeoutMs = 5e3;
355
363
  await new Promise((resolve) => {
356
- pool.req(DEFAULT_RELAYS, {
364
+ pool.req(relays, {
357
365
  kinds: [kind],
358
366
  limit: 100
359
367
  }).pipe(
@@ -525,16 +533,11 @@ var ModelManager = class _ModelManager {
525
533
  );
526
534
  let sessionEvents = cached;
527
535
  if (cached.length === 0) {
528
- const LGTM_RELAYS = [
529
- "wss://relay.primal.net",
530
- "wss://nos.lol",
531
- "wss://relay.damus.io",
532
- "wss://relay.routstr.com"
533
- ];
536
+ const lgtmRelays = this.getNostrRelays();
534
537
  const pool = new applesauceRelay.RelayPool();
535
538
  const timeoutMs = 5e3;
536
539
  await new Promise((resolve) => {
537
- pool.req(LGTM_RELAYS, {
540
+ pool.req(lgtmRelays, {
538
541
  kinds: [38425],
539
542
  "#t": ["lgtm"],
540
543
  limit: 500,
@@ -776,11 +779,7 @@ var ModelManager = class _ModelManager {
776
779
  return cachedModels;
777
780
  }
778
781
  }
779
- const DEFAULT_RELAYS = [
780
- "wss://relay.damus.io",
781
- "wss://nos.lol",
782
- "wss://relay.routstr.com"
783
- ];
782
+ const relays = this.getNostrRelays();
784
783
  const cached = await this.getCachedNostrEvents(
785
784
  { kinds: [38423], "#d": ["routstr-21-models"], authors: [this.routstrPubkey] },
786
785
  this.cacheTTL,
@@ -791,7 +790,7 @@ var ModelManager = class _ModelManager {
791
790
  const pool = new applesauceRelay.RelayPool();
792
791
  const timeoutMs = 5e3;
793
792
  await new Promise((resolve) => {
794
- pool.req(DEFAULT_RELAYS, {
793
+ pool.req(relays, {
795
794
  kinds: [38423],
796
795
  "#d": ["routstr-21-models"],
797
796
  limit: 1,
@@ -1992,8 +1991,8 @@ var BalanceManager = class _BalanceManager {
1992
1991
  const refundableProviderBalance = Object.entries(
1993
1992
  balanceState.providerBalances
1994
1993
  ).filter(([providerBaseUrl]) => providerBaseUrl !== baseUrl).reduce((sum, [, value]) => sum + value, 0);
1995
- if (totalMintBalance + targetProviderBalance < adjustedAmount && totalMintBalance + targetProviderBalance + refundableProviderBalance >= adjustedAmount && retryCount < 2) {
1996
- await this._refundOtherProvidersForTopUp(baseUrl, mintUrl, retryCount);
1994
+ if (totalMintBalance + targetProviderBalance < adjustedAmount && totalMintBalance + targetProviderBalance + refundableProviderBalance >= adjustedAmount && retryCount < 3) {
1995
+ await this._refundOtherProvidersForTopUp(baseUrl, mintUrl, retryCount, adjustedAmount);
1997
1996
  return this.createProviderToken({
1998
1997
  ...options,
1999
1998
  retryCount: retryCount + 1
@@ -2132,33 +2131,47 @@ var BalanceManager = class _BalanceManager {
2132
2131
  }
2133
2132
  return candidates;
2134
2133
  }
2135
- async _refundOtherProvidersForTopUp(baseUrl, mintUrl, retryCount) {
2134
+ async _refundOtherProvidersForTopUp(baseUrl, mintUrl, retryCount, requiredAmount) {
2136
2135
  const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
2137
2136
  const forceRefund = retryCount >= 2;
2138
- const apiKeysToRefund = apiKeyDistribution.filter(
2139
- (apiKey) => apiKey.baseUrl !== baseUrl && apiKey.amount > 0
2140
- );
2141
- const apiKeyRefundResults = await Promise.allSettled(
2142
- apiKeysToRefund.map(async (apiKeyEntry) => {
2143
- const fullApiKeyEntry = this.storageAdapter.getApiKey(
2144
- apiKeyEntry.baseUrl
2145
- );
2146
- if (!fullApiKeyEntry) {
2147
- return { baseUrl: apiKeyEntry.baseUrl, success: false };
2148
- }
2149
- const result = await this.refundApiKey({
2137
+ const candidates = apiKeyDistribution.filter((apiKey) => apiKey.baseUrl !== baseUrl && apiKey.amount > 0).map((apiKey) => {
2138
+ const full = this.storageAdapter.getApiKey(apiKey.baseUrl);
2139
+ return {
2140
+ baseUrl: apiKey.baseUrl,
2141
+ amount: apiKey.amount,
2142
+ lastUsed: full?.lastUsed ?? 0,
2143
+ key: full?.key
2144
+ };
2145
+ }).filter((c) => c.key != null).sort((a, b) => a.lastUsed - b.lastUsed);
2146
+ if (candidates.length === 0) return;
2147
+ if (forceRefund) {
2148
+ for (const candidate of candidates) {
2149
+ await this.refundApiKey({
2150
2150
  mintUrl,
2151
- baseUrl: apiKeyEntry.baseUrl,
2152
- apiKey: fullApiKeyEntry.key,
2153
- forceRefund
2151
+ baseUrl: candidate.baseUrl,
2152
+ apiKey: candidate.key,
2153
+ forceRefund: true
2154
2154
  });
2155
- return { baseUrl: apiKeyEntry.baseUrl, success: result.success };
2156
- })
2157
- );
2158
- for (const result of apiKeyRefundResults) {
2159
- if (result.status === "fulfilled" && result.value.success) {
2160
- this.storageAdapter.updateApiKeyBalance(result.value.baseUrl, 0);
2155
+ const newState = await this.getBalanceState();
2156
+ const newAvailable = (newState.mintBalances[mintUrl] || 0) + (newState.providerBalances[baseUrl] || 0);
2157
+ if (newAvailable >= requiredAmount) {
2158
+ this.logger.log(
2159
+ `_refundOtherProvidersForTopUp: freed enough balance (${newAvailable} >= ${requiredAmount}), stopping early`
2160
+ );
2161
+ return;
2162
+ }
2161
2163
  }
2164
+ } else {
2165
+ await Promise.allSettled(
2166
+ candidates.map(
2167
+ (candidate) => this.refundApiKey({
2168
+ mintUrl,
2169
+ baseUrl: candidate.baseUrl,
2170
+ apiKey: candidate.key,
2171
+ forceRefund: false
2172
+ })
2173
+ )
2174
+ );
2162
2175
  }
2163
2176
  }
2164
2177
  /**
@@ -2337,561 +2350,242 @@ var BalanceManager = class _BalanceManager {
2337
2350
  }
2338
2351
  };
2339
2352
 
2340
- // client/usage.ts
2341
- function extractUsageFromResponseBody(body, fallbackSatsCost = 0) {
2342
- if (!body || typeof body !== "object") return null;
2343
- const usage = body.usage;
2344
- if (!usage || typeof usage !== "object") return null;
2345
- const promptTokens = Number(usage.prompt_tokens ?? 0);
2346
- const completionTokens = Number(usage.completion_tokens ?? 0);
2347
- const totalTokens = Number(usage.total_tokens ?? 0);
2348
- const costValue = usage.cost;
2349
- let cost = 0;
2350
- let satsCost = fallbackSatsCost;
2351
- if (typeof costValue === "number") {
2352
- cost = costValue;
2353
- } else if (costValue && typeof costValue === "object") {
2354
- const costObj = costValue;
2355
- const totalUsd = costObj.total_usd;
2356
- const totalMsats = costObj.total_msats;
2357
- cost = typeof totalUsd === "number" ? totalUsd : 0;
2358
- if (typeof totalMsats === "number") {
2359
- satsCost = totalMsats / 1e3;
2360
- }
2353
+ // utils/torUtils.ts
2354
+ var TOR_ONION_SUFFIX = ".onion";
2355
+ var isTorContext = () => {
2356
+ if (typeof window === "undefined") return false;
2357
+ const hostname = window.location.hostname.toLowerCase();
2358
+ return hostname.endsWith(TOR_ONION_SUFFIX);
2359
+ };
2360
+ var isOnionUrl = (url) => {
2361
+ if (!url) return false;
2362
+ const trimmed = url.trim().toLowerCase();
2363
+ if (!trimmed) return false;
2364
+ try {
2365
+ const candidate = trimmed.startsWith("http") ? trimmed : `http://${trimmed}`;
2366
+ return new URL(candidate).hostname.endsWith(TOR_ONION_SUFFIX);
2367
+ } catch {
2368
+ return trimmed.includes(TOR_ONION_SUFFIX);
2361
2369
  }
2362
- if (promptTokens === 0 && completionTokens === 0 && totalTokens === 0 && cost === 0 && satsCost === 0) {
2363
- return null;
2370
+ };
2371
+ var shouldAllowHttp = (url, torMode) => {
2372
+ if (!url.startsWith("http://")) return true;
2373
+ if (url.includes("localhost") || url.includes("127.0.0.1")) return true;
2374
+ return torMode && isOnionUrl(url);
2375
+ };
2376
+ var normalizeProviderUrl = (url, torMode = false) => {
2377
+ if (!url || typeof url !== "string") return null;
2378
+ const trimmed = url.trim();
2379
+ if (!trimmed) return null;
2380
+ if (/^https?:\/\//i.test(trimmed)) {
2381
+ return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
2364
2382
  }
2365
- return {
2366
- promptTokens,
2367
- completionTokens,
2368
- totalTokens,
2369
- cost,
2370
- satsCost
2371
- };
2372
- }
2373
- function extractResponseId(body) {
2374
- if (!body || typeof body !== "object") return void 0;
2375
- const id = body.id;
2376
- if (typeof id !== "string") return void 0;
2377
- const trimmed = id.trim();
2378
- return trimmed.length > 0 ? trimmed : void 0;
2379
- }
2380
- function extractUsageFromSSEJson(parsed, fallbackSatsCost = 0) {
2381
- if (!parsed || typeof parsed !== "object") {
2382
- return null;
2383
+ const useHttpForOnion = torMode && isOnionUrl(trimmed);
2384
+ const withProto = `${useHttpForOnion ? "http" : "https"}://${trimmed}`;
2385
+ return withProto.endsWith("/") ? withProto : `${withProto}/`;
2386
+ };
2387
+ var dedupePreserveOrder = (urls) => {
2388
+ const seen = /* @__PURE__ */ new Set();
2389
+ const out = [];
2390
+ for (const url of urls) {
2391
+ if (!seen.has(url)) {
2392
+ seen.add(url);
2393
+ out.push(url);
2394
+ }
2383
2395
  }
2384
- if (!parsed.usage && parsed.cost && typeof parsed.cost === "object") {
2385
- const costObj = parsed.cost;
2386
- const msats2 = costObj.total_msats ?? 0;
2387
- const cost2 = costObj.total_usd ?? 0;
2388
- if (msats2 === 0 && cost2 === 0) return null;
2389
- return {
2390
- promptTokens: Number(costObj.input_tokens ?? 0),
2391
- completionTokens: Number(costObj.output_tokens ?? 0),
2392
- totalTokens: Number((costObj.input_tokens ?? 0) + (costObj.output_tokens ?? 0)),
2393
- cost: Number(cost2),
2394
- satsCost: msats2 > 0 ? msats2 / 1e3 : fallbackSatsCost
2395
- };
2396
+ return out;
2397
+ };
2398
+ var getProviderEndpoints = (provider, torMode) => {
2399
+ const rawUrls = [
2400
+ provider.endpoint_url,
2401
+ ...Array.isArray(provider.endpoint_urls) ? provider.endpoint_urls : [],
2402
+ provider.onion_url,
2403
+ ...Array.isArray(provider.onion_urls) ? provider.onion_urls : []
2404
+ ];
2405
+ const normalized = rawUrls.map((value) => normalizeProviderUrl(value, torMode)).filter((value) => Boolean(value));
2406
+ const unique = dedupePreserveOrder(normalized).filter(
2407
+ (value) => shouldAllowHttp(value, torMode)
2408
+ );
2409
+ if (unique.length === 0) return [];
2410
+ const onion = unique.filter((value) => isOnionUrl(value));
2411
+ const clearnet = unique.filter((value) => !isOnionUrl(value));
2412
+ if (torMode) {
2413
+ return onion.length > 0 ? onion : clearnet;
2396
2414
  }
2397
- if (!parsed.usage) {
2415
+ return clearnet;
2416
+ };
2417
+ var filterBaseUrlsForTor = (baseUrls, torMode) => {
2418
+ if (!Array.isArray(baseUrls)) return [];
2419
+ const normalized = baseUrls.map((value) => normalizeProviderUrl(value, torMode)).filter((value) => Boolean(value));
2420
+ const filtered = normalized.filter(
2421
+ (value) => torMode ? true : !isOnionUrl(value)
2422
+ );
2423
+ return dedupePreserveOrder(
2424
+ filtered.filter((value) => shouldAllowHttp(value, torMode))
2425
+ );
2426
+ };
2427
+
2428
+ // client/ProviderManager.ts
2429
+ function getImageResolutionFromDataUrl(dataUrl) {
2430
+ try {
2431
+ if (typeof dataUrl !== "string" || !dataUrl.startsWith("data:"))
2432
+ return null;
2433
+ const commaIdx = dataUrl.indexOf(",");
2434
+ if (commaIdx === -1) return null;
2435
+ const meta = dataUrl.slice(5, commaIdx);
2436
+ const base64 = dataUrl.slice(commaIdx + 1);
2437
+ const binary = typeof atob === "function" ? atob(base64) : Buffer.from(base64, "base64").toString("binary");
2438
+ const len = binary.length;
2439
+ const bytes = new Uint8Array(len);
2440
+ for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i);
2441
+ const isPNG = meta.includes("image/png");
2442
+ const isJPEG = meta.includes("image/jpeg") || meta.includes("image/jpg");
2443
+ if (isPNG) {
2444
+ const sig = [137, 80, 78, 71, 13, 10, 26, 10];
2445
+ for (let i = 0; i < sig.length; i++) {
2446
+ if (bytes[i] !== sig[i]) return null;
2447
+ }
2448
+ const view = new DataView(
2449
+ bytes.buffer,
2450
+ bytes.byteOffset,
2451
+ bytes.byteLength
2452
+ );
2453
+ const width = view.getUint32(16, false);
2454
+ const height = view.getUint32(20, false);
2455
+ if (width > 0 && height > 0) return { width, height };
2456
+ return null;
2457
+ }
2458
+ if (isJPEG) {
2459
+ let offset = 0;
2460
+ if (bytes[offset++] !== 255 || bytes[offset++] !== 216) return null;
2461
+ while (offset < bytes.length) {
2462
+ while (offset < bytes.length && bytes[offset] !== 255) offset++;
2463
+ if (offset + 1 >= bytes.length) break;
2464
+ while (bytes[offset] === 255) offset++;
2465
+ const marker = bytes[offset++];
2466
+ if (marker === 216 || marker === 217) continue;
2467
+ if (offset + 1 >= bytes.length) break;
2468
+ const length = bytes[offset] << 8 | bytes[offset + 1];
2469
+ offset += 2;
2470
+ if (marker === 192 || marker === 194) {
2471
+ if (length < 7 || offset + length - 2 > bytes.length) return null;
2472
+ const precision = bytes[offset];
2473
+ const height = bytes[offset + 1] << 8 | bytes[offset + 2];
2474
+ const width = bytes[offset + 3] << 8 | bytes[offset + 4];
2475
+ if (precision > 0 && width > 0 && height > 0)
2476
+ return { width, height };
2477
+ return null;
2478
+ } else {
2479
+ offset += length - 2;
2480
+ }
2481
+ }
2482
+ return null;
2483
+ }
2398
2484
  return null;
2399
- }
2400
- const usage = parsed.usage;
2401
- const usageCost = usage.cost;
2402
- let cost = 0;
2403
- let msats = 0;
2404
- if (typeof usageCost === "number") {
2405
- cost = usageCost;
2406
- } else if (usageCost && typeof usageCost === "object") {
2407
- cost = usageCost.total_usd ?? 0;
2408
- msats = usageCost.total_msats ?? 0;
2409
- }
2410
- if (cost === 0) {
2411
- cost = parsed.metadata?.routstr?.cost?.total_usd ?? 0;
2412
- }
2413
- if (msats === 0) {
2414
- msats = parsed.metadata?.routstr?.cost?.total_msats ?? (typeof usage.cost_sats === "number" ? usage.cost_sats * 1e3 : 0);
2415
- }
2416
- const promptTokens = Number(usage.prompt_tokens ?? usage.input_tokens ?? 0);
2417
- const completionTokens = Number(usage.completion_tokens ?? usage.output_tokens ?? 0);
2418
- const totalTokens = Number(usage.total_tokens ?? promptTokens + completionTokens);
2419
- const result = {
2420
- promptTokens,
2421
- completionTokens,
2422
- totalTokens,
2423
- cost: Number(cost ?? 0),
2424
- satsCost: msats > 0 ? msats / 1e3 : fallbackSatsCost
2425
- };
2426
- if (result.promptTokens === 0 && result.completionTokens === 0 && result.totalTokens === 0 && result.cost === 0 && result.satsCost === 0) {
2485
+ } catch {
2427
2486
  return null;
2428
2487
  }
2429
- return result;
2430
- }
2431
- function toUsageStats(usage) {
2432
- if (!usage) return void 0;
2433
- return {
2434
- total_tokens: usage.totalTokens,
2435
- prompt_tokens: usage.promptTokens,
2436
- completion_tokens: usage.completionTokens,
2437
- cost: usage.cost,
2438
- sats_cost: usage.satsCost
2439
- };
2440
2488
  }
2441
-
2442
- // client/StreamProcessor.ts
2443
- var StreamProcessor = class {
2444
- accumulatedContent = "";
2445
- accumulatedThinking = "";
2446
- accumulatedImages = [];
2447
- isInThinking = false;
2448
- isInContent = false;
2449
- /**
2450
- * Process a streaming response
2451
- */
2452
- async process(response, callbacks, modelId) {
2453
- if (!response.body) {
2454
- throw new Error("Response body is not available");
2455
- }
2456
- const reader = response.body.getReader();
2457
- const decoder = new TextDecoder("utf-8");
2458
- let buffer = "";
2459
- this.accumulatedContent = "";
2460
- this.accumulatedThinking = "";
2461
- this.accumulatedImages = [];
2462
- this.isInThinking = false;
2463
- this.isInContent = false;
2464
- let usage;
2465
- let model;
2466
- let finish_reason;
2467
- let citations;
2468
- let annotations;
2469
- let responseId;
2470
- try {
2471
- while (true) {
2472
- const { done, value } = await reader.read();
2473
- if (done) {
2474
- break;
2475
- }
2476
- const chunk = decoder.decode(value, { stream: true });
2477
- buffer += chunk;
2478
- const lines = buffer.split("\n");
2479
- buffer = lines.pop() || "";
2480
- for (const line of lines) {
2481
- const parsed = this._parseLine(line);
2482
- if (!parsed) continue;
2483
- if (parsed.content) {
2484
- this._handleContent(parsed.content, callbacks, modelId);
2485
- }
2486
- if (parsed.reasoning) {
2487
- this._handleThinking(parsed.reasoning, callbacks);
2488
- }
2489
- if (parsed.usage) {
2490
- usage = parsed.usage;
2491
- }
2492
- if (parsed.model) {
2493
- model = parsed.model;
2494
- }
2495
- if (parsed.finish_reason) {
2496
- finish_reason = parsed.finish_reason;
2497
- }
2498
- if (parsed.responseId) {
2499
- responseId = parsed.responseId;
2500
- }
2501
- if (parsed.citations) {
2502
- citations = parsed.citations;
2503
- }
2504
- if (parsed.annotations) {
2505
- annotations = parsed.annotations;
2506
- }
2507
- if (parsed.images) {
2508
- this._mergeImages(parsed.images);
2509
- }
2510
- }
2511
- }
2512
- } finally {
2513
- reader.releaseLock();
2489
+ function calculateImageTokens(width, height, detail = "auto") {
2490
+ if (detail === "low") return 85;
2491
+ let w = width;
2492
+ let h = height;
2493
+ if (w > 2048 || h > 2048) {
2494
+ const aspectRatio = w / h;
2495
+ if (w > h) {
2496
+ w = 2048;
2497
+ h = Math.floor(w / aspectRatio);
2498
+ } else {
2499
+ h = 2048;
2500
+ w = Math.floor(h * aspectRatio);
2514
2501
  }
2515
- return {
2516
- content: this.accumulatedContent,
2517
- thinking: this.accumulatedThinking || void 0,
2518
- images: this.accumulatedImages.length > 0 ? this.accumulatedImages : void 0,
2519
- usage,
2520
- model,
2521
- responseId,
2522
- finish_reason,
2523
- citations,
2524
- annotations
2525
- };
2526
2502
  }
2527
- /**
2528
- * Parse a single SSE line
2529
- */
2530
- _parseLine(line) {
2531
- if (!line.trim()) return null;
2532
- if (!line.startsWith("data: ")) {
2533
- return null;
2534
- }
2535
- const jsonData = line.slice(6);
2536
- if (jsonData === "[DONE]") {
2537
- return null;
2503
+ if (w > 768 || h > 768) {
2504
+ const aspectRatio = w / h;
2505
+ if (w > h) {
2506
+ w = 768;
2507
+ h = Math.floor(w / aspectRatio);
2508
+ } else {
2509
+ h = 768;
2510
+ w = Math.floor(h * aspectRatio);
2538
2511
  }
2539
- try {
2540
- const parsed = JSON.parse(jsonData);
2541
- const result = {};
2542
- if (parsed.choices?.[0]?.delta?.content) {
2543
- result.content = parsed.choices[0].delta.content;
2544
- }
2545
- if (parsed.choices?.[0]?.delta?.reasoning) {
2546
- result.reasoning = parsed.choices[0].delta.reasoning;
2547
- }
2548
- const extractedUsage = extractUsageFromSSEJson(parsed);
2549
- if (extractedUsage) {
2550
- result.usage = toUsageStats(extractedUsage);
2551
- } else if (parsed.usage) {
2552
- result.usage = {
2553
- total_tokens: parsed.usage.total_tokens ?? parsed.usage.input_tokens + parsed.usage.output_tokens,
2554
- prompt_tokens: parsed.usage.prompt_tokens ?? parsed.usage.input_tokens,
2555
- completion_tokens: parsed.usage.completion_tokens ?? parsed.usage.output_tokens
2556
- };
2557
- }
2558
- if (parsed.id) {
2559
- result.responseId = parsed.id;
2560
- }
2561
- if (parsed.model) {
2562
- result.model = parsed.model;
2563
- }
2564
- if (parsed.citations) {
2565
- result.citations = parsed.citations;
2566
- }
2567
- if (parsed.annotations) {
2568
- result.annotations = parsed.annotations;
2569
- }
2570
- if (parsed.choices?.[0]?.finish_reason) {
2571
- result.finish_reason = parsed.choices[0].finish_reason;
2572
- }
2573
- const images = parsed.choices?.[0]?.message?.images || parsed.choices?.[0]?.delta?.images;
2574
- if (images && Array.isArray(images)) {
2575
- result.images = images;
2576
- }
2577
- return result;
2578
- } catch {
2579
- return null;
2512
+ }
2513
+ const tilesWidth = Math.floor((w + 511) / 512);
2514
+ const tilesHeight = Math.floor((h + 511) / 512);
2515
+ const numTiles = tilesWidth * tilesHeight;
2516
+ return 85 + 170 * numTiles;
2517
+ }
2518
+ var ProviderManager = class _ProviderManager {
2519
+ constructor(providerRegistry, store, logger) {
2520
+ this.providerRegistry = providerRegistry;
2521
+ this.instanceId = `pm_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
2522
+ this.logger = (logger ?? consoleLogger).child(`ProviderManager:${this.instanceId}`);
2523
+ if (store) {
2524
+ this.store = store;
2525
+ this.hydrateFromStore();
2580
2526
  }
2581
2527
  }
2528
+ providerRegistry;
2529
+ failedProviders = /* @__PURE__ */ new Set();
2530
+ /** Track when each provider last failed (provider URL -> timestamp) */
2531
+ lastFailed = /* @__PURE__ */ new Map();
2532
+ /** Providers on cooldown: [provider_url, cooldown_started_timestamp][] */
2533
+ providersOnCoolDown = [];
2534
+ /** Cooldown duration in milliseconds (42 seconds) */
2535
+ static COOLDOWN_DURATION_MS = 42 * 1e3;
2536
+ /** Optional persistent store for failure tracking */
2537
+ store = null;
2538
+ /** Instance ID for debugging */
2539
+ instanceId;
2540
+ logger;
2582
2541
  /**
2583
- * Handle content delta with thinking support
2542
+ * Hydrate in-memory state from persistent store
2584
2543
  */
2585
- _handleContent(content, callbacks, modelId) {
2586
- if (this.isInThinking && !this.isInContent) {
2587
- this.accumulatedThinking += "</thinking>";
2588
- callbacks.onThinking(this.accumulatedThinking);
2589
- this.isInThinking = false;
2590
- this.isInContent = true;
2591
- }
2592
- if (modelId) {
2593
- this._extractThinkingFromContent(content, callbacks);
2594
- } else {
2595
- this.accumulatedContent += content;
2596
- }
2597
- callbacks.onContent(this.accumulatedContent);
2544
+ hydrateFromStore() {
2545
+ if (!this.store) return;
2546
+ const state = this.store.getState();
2547
+ this.failedProviders = new Set(state.failedProviders);
2548
+ this.lastFailed = new Map(Object.entries(state.lastFailed));
2549
+ const now = Date.now();
2550
+ this.providersOnCoolDown = state.providersOnCooldown.filter(
2551
+ (entry) => now - entry.timestamp < _ProviderManager.COOLDOWN_DURATION_MS
2552
+ ).map((entry) => [entry.baseUrl, entry.timestamp]);
2553
+ this.logger.log(`Hydrated from store: failedProviders=${this.failedProviders.size} lastFailed=${this.lastFailed.size} providersOnCooldown=${this.providersOnCoolDown.length}`);
2598
2554
  }
2599
2555
  /**
2600
- * Handle thinking/reasoning content
2556
+ * Get instance ID for debugging
2601
2557
  */
2602
- _handleThinking(reasoning, callbacks) {
2603
- if (!this.isInThinking) {
2604
- this.accumulatedThinking += "<thinking> ";
2605
- this.isInThinking = true;
2606
- }
2607
- this.accumulatedThinking += reasoning;
2608
- callbacks.onThinking(this.accumulatedThinking);
2558
+ getInstanceId() {
2559
+ return this.instanceId;
2609
2560
  }
2610
2561
  /**
2611
- * Extract thinking blocks from content (for models with inline thinking)
2562
+ * Clean up expired cooldown entries
2563
+ * Also removes the provider from failedProviders so it can be retried
2612
2564
  */
2613
- _extractThinkingFromContent(content, callbacks) {
2614
- const parts = content.split(/(<thinking>|<\/thinking>)/);
2615
- for (const part of parts) {
2616
- if (part === "<thinking>") {
2617
- this.isInThinking = true;
2618
- if (!this.accumulatedThinking.includes("<thinking>")) {
2619
- this.accumulatedThinking += "<thinking> ";
2565
+ cleanupExpiredCooldowns() {
2566
+ const now = Date.now();
2567
+ const before = this.providersOnCoolDown.length;
2568
+ this.providersOnCoolDown = this.providersOnCoolDown.filter(
2569
+ ([url, timestamp]) => {
2570
+ const age = now - timestamp;
2571
+ const isExpired = age >= _ProviderManager.COOLDOWN_DURATION_MS;
2572
+ if (isExpired) {
2573
+ this.logger.log(`Removing expired cooldown for ${url} (age: ${age}ms)`);
2574
+ this.failedProviders.delete(url);
2575
+ if (this.store) {
2576
+ this.store.getState().removeFailedProvider(url);
2577
+ }
2620
2578
  }
2621
- } else if (part === "</thinking>") {
2622
- this.isInThinking = false;
2623
- this.accumulatedThinking += "</thinking>";
2624
- } else if (this.isInThinking) {
2625
- this.accumulatedThinking += part;
2626
- } else {
2627
- this.accumulatedContent += part;
2579
+ return !isExpired;
2628
2580
  }
2581
+ );
2582
+ const after = this.providersOnCoolDown.length;
2583
+ if (before !== after) {
2584
+ this.logger.log(`Cleaned up ${before - after} expired cooldown(s), ${after} remaining`);
2629
2585
  }
2630
2586
  }
2631
2587
  /**
2632
- * Merge images into accumulated array, avoiding duplicates
2633
- */
2634
- _mergeImages(newImages) {
2635
- for (const img of newImages) {
2636
- const newUrl = img.image_url?.url;
2637
- const existingIndex = this.accumulatedImages.findIndex((existing) => {
2638
- const existingUrl = existing.image_url?.url;
2639
- if (newUrl && existingUrl) {
2640
- return existingUrl === newUrl;
2641
- }
2642
- if (img.index !== void 0 && existing.index !== void 0) {
2643
- return existing.index === img.index;
2644
- }
2645
- return false;
2646
- });
2647
- if (existingIndex === -1) {
2648
- this.accumulatedImages.push(img);
2649
- } else {
2650
- this.accumulatedImages[existingIndex] = img;
2651
- }
2652
- }
2653
- }
2654
- };
2655
-
2656
- // utils/torUtils.ts
2657
- var TOR_ONION_SUFFIX = ".onion";
2658
- var isTorContext = () => {
2659
- if (typeof window === "undefined") return false;
2660
- const hostname = window.location.hostname.toLowerCase();
2661
- return hostname.endsWith(TOR_ONION_SUFFIX);
2662
- };
2663
- var isOnionUrl = (url) => {
2664
- if (!url) return false;
2665
- const trimmed = url.trim().toLowerCase();
2666
- if (!trimmed) return false;
2667
- try {
2668
- const candidate = trimmed.startsWith("http") ? trimmed : `http://${trimmed}`;
2669
- return new URL(candidate).hostname.endsWith(TOR_ONION_SUFFIX);
2670
- } catch {
2671
- return trimmed.includes(TOR_ONION_SUFFIX);
2672
- }
2673
- };
2674
- var shouldAllowHttp = (url, torMode) => {
2675
- if (!url.startsWith("http://")) return true;
2676
- if (url.includes("localhost") || url.includes("127.0.0.1")) return true;
2677
- return torMode && isOnionUrl(url);
2678
- };
2679
- var normalizeProviderUrl = (url, torMode = false) => {
2680
- if (!url || typeof url !== "string") return null;
2681
- const trimmed = url.trim();
2682
- if (!trimmed) return null;
2683
- if (/^https?:\/\//i.test(trimmed)) {
2684
- return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
2685
- }
2686
- const useHttpForOnion = torMode && isOnionUrl(trimmed);
2687
- const withProto = `${useHttpForOnion ? "http" : "https"}://${trimmed}`;
2688
- return withProto.endsWith("/") ? withProto : `${withProto}/`;
2689
- };
2690
- var dedupePreserveOrder = (urls) => {
2691
- const seen = /* @__PURE__ */ new Set();
2692
- const out = [];
2693
- for (const url of urls) {
2694
- if (!seen.has(url)) {
2695
- seen.add(url);
2696
- out.push(url);
2697
- }
2698
- }
2699
- return out;
2700
- };
2701
- var getProviderEndpoints = (provider, torMode) => {
2702
- const rawUrls = [
2703
- provider.endpoint_url,
2704
- ...Array.isArray(provider.endpoint_urls) ? provider.endpoint_urls : [],
2705
- provider.onion_url,
2706
- ...Array.isArray(provider.onion_urls) ? provider.onion_urls : []
2707
- ];
2708
- const normalized = rawUrls.map((value) => normalizeProviderUrl(value, torMode)).filter((value) => Boolean(value));
2709
- const unique = dedupePreserveOrder(normalized).filter(
2710
- (value) => shouldAllowHttp(value, torMode)
2711
- );
2712
- if (unique.length === 0) return [];
2713
- const onion = unique.filter((value) => isOnionUrl(value));
2714
- const clearnet = unique.filter((value) => !isOnionUrl(value));
2715
- if (torMode) {
2716
- return onion.length > 0 ? onion : clearnet;
2717
- }
2718
- return clearnet;
2719
- };
2720
- var filterBaseUrlsForTor = (baseUrls, torMode) => {
2721
- if (!Array.isArray(baseUrls)) return [];
2722
- const normalized = baseUrls.map((value) => normalizeProviderUrl(value, torMode)).filter((value) => Boolean(value));
2723
- const filtered = normalized.filter(
2724
- (value) => torMode ? true : !isOnionUrl(value)
2725
- );
2726
- return dedupePreserveOrder(
2727
- filtered.filter((value) => shouldAllowHttp(value, torMode))
2728
- );
2729
- };
2730
-
2731
- // client/ProviderManager.ts
2732
- function getImageResolutionFromDataUrl(dataUrl) {
2733
- try {
2734
- if (typeof dataUrl !== "string" || !dataUrl.startsWith("data:"))
2735
- return null;
2736
- const commaIdx = dataUrl.indexOf(",");
2737
- if (commaIdx === -1) return null;
2738
- const meta = dataUrl.slice(5, commaIdx);
2739
- const base64 = dataUrl.slice(commaIdx + 1);
2740
- const binary = typeof atob === "function" ? atob(base64) : Buffer.from(base64, "base64").toString("binary");
2741
- const len = binary.length;
2742
- const bytes = new Uint8Array(len);
2743
- for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i);
2744
- const isPNG = meta.includes("image/png");
2745
- const isJPEG = meta.includes("image/jpeg") || meta.includes("image/jpg");
2746
- if (isPNG) {
2747
- const sig = [137, 80, 78, 71, 13, 10, 26, 10];
2748
- for (let i = 0; i < sig.length; i++) {
2749
- if (bytes[i] !== sig[i]) return null;
2750
- }
2751
- const view = new DataView(
2752
- bytes.buffer,
2753
- bytes.byteOffset,
2754
- bytes.byteLength
2755
- );
2756
- const width = view.getUint32(16, false);
2757
- const height = view.getUint32(20, false);
2758
- if (width > 0 && height > 0) return { width, height };
2759
- return null;
2760
- }
2761
- if (isJPEG) {
2762
- let offset = 0;
2763
- if (bytes[offset++] !== 255 || bytes[offset++] !== 216) return null;
2764
- while (offset < bytes.length) {
2765
- while (offset < bytes.length && bytes[offset] !== 255) offset++;
2766
- if (offset + 1 >= bytes.length) break;
2767
- while (bytes[offset] === 255) offset++;
2768
- const marker = bytes[offset++];
2769
- if (marker === 216 || marker === 217) continue;
2770
- if (offset + 1 >= bytes.length) break;
2771
- const length = bytes[offset] << 8 | bytes[offset + 1];
2772
- offset += 2;
2773
- if (marker === 192 || marker === 194) {
2774
- if (length < 7 || offset + length - 2 > bytes.length) return null;
2775
- const precision = bytes[offset];
2776
- const height = bytes[offset + 1] << 8 | bytes[offset + 2];
2777
- const width = bytes[offset + 3] << 8 | bytes[offset + 4];
2778
- if (precision > 0 && width > 0 && height > 0)
2779
- return { width, height };
2780
- return null;
2781
- } else {
2782
- offset += length - 2;
2783
- }
2784
- }
2785
- return null;
2786
- }
2787
- return null;
2788
- } catch {
2789
- return null;
2790
- }
2791
- }
2792
- function calculateImageTokens(width, height, detail = "auto") {
2793
- if (detail === "low") return 85;
2794
- let w = width;
2795
- let h = height;
2796
- if (w > 2048 || h > 2048) {
2797
- const aspectRatio = w / h;
2798
- if (w > h) {
2799
- w = 2048;
2800
- h = Math.floor(w / aspectRatio);
2801
- } else {
2802
- h = 2048;
2803
- w = Math.floor(h * aspectRatio);
2804
- }
2805
- }
2806
- if (w > 768 || h > 768) {
2807
- const aspectRatio = w / h;
2808
- if (w > h) {
2809
- w = 768;
2810
- h = Math.floor(w / aspectRatio);
2811
- } else {
2812
- h = 768;
2813
- w = Math.floor(h * aspectRatio);
2814
- }
2815
- }
2816
- const tilesWidth = Math.floor((w + 511) / 512);
2817
- const tilesHeight = Math.floor((h + 511) / 512);
2818
- const numTiles = tilesWidth * tilesHeight;
2819
- return 85 + 170 * numTiles;
2820
- }
2821
- function isInsecureHttpUrl(url) {
2822
- return url.startsWith("http://");
2823
- }
2824
- var ProviderManager = class _ProviderManager {
2825
- constructor(providerRegistry, store, logger) {
2826
- this.providerRegistry = providerRegistry;
2827
- this.instanceId = `pm_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
2828
- this.logger = (logger ?? consoleLogger).child(`ProviderManager:${this.instanceId}`);
2829
- if (store) {
2830
- this.store = store;
2831
- this.hydrateFromStore();
2832
- }
2833
- }
2834
- providerRegistry;
2835
- failedProviders = /* @__PURE__ */ new Set();
2836
- /** Track when each provider last failed (provider URL -> timestamp) */
2837
- lastFailed = /* @__PURE__ */ new Map();
2838
- /** Providers on cooldown: [provider_url, cooldown_started_timestamp][] */
2839
- providersOnCoolDown = [];
2840
- /** Cooldown duration in milliseconds (42 seconds) */
2841
- static COOLDOWN_DURATION_MS = 42 * 1e3;
2842
- /** Optional persistent store for failure tracking */
2843
- store = null;
2844
- /** Instance ID for debugging */
2845
- instanceId;
2846
- logger;
2847
- /**
2848
- * Hydrate in-memory state from persistent store
2849
- */
2850
- hydrateFromStore() {
2851
- if (!this.store) return;
2852
- const state = this.store.getState();
2853
- this.failedProviders = new Set(state.failedProviders);
2854
- this.lastFailed = new Map(Object.entries(state.lastFailed));
2855
- const now = Date.now();
2856
- this.providersOnCoolDown = state.providersOnCooldown.filter(
2857
- (entry) => now - entry.timestamp < _ProviderManager.COOLDOWN_DURATION_MS
2858
- ).map((entry) => [entry.baseUrl, entry.timestamp]);
2859
- this.logger.log(`Hydrated from store: failedProviders=${this.failedProviders.size} lastFailed=${this.lastFailed.size} providersOnCooldown=${this.providersOnCoolDown.length}`);
2860
- }
2861
- /**
2862
- * Get instance ID for debugging
2863
- */
2864
- getInstanceId() {
2865
- return this.instanceId;
2866
- }
2867
- /**
2868
- * Clean up expired cooldown entries
2869
- * Also removes the provider from failedProviders so it can be retried
2870
- */
2871
- cleanupExpiredCooldowns() {
2872
- const now = Date.now();
2873
- const before = this.providersOnCoolDown.length;
2874
- this.providersOnCoolDown = this.providersOnCoolDown.filter(
2875
- ([url, timestamp]) => {
2876
- const age = now - timestamp;
2877
- const isExpired = age >= _ProviderManager.COOLDOWN_DURATION_MS;
2878
- if (isExpired) {
2879
- this.logger.log(`Removing expired cooldown for ${url} (age: ${age}ms)`);
2880
- this.failedProviders.delete(url);
2881
- if (this.store) {
2882
- this.store.getState().removeFailedProvider(url);
2883
- }
2884
- }
2885
- return !isExpired;
2886
- }
2887
- );
2888
- const after = this.providersOnCoolDown.length;
2889
- if (before !== after) {
2890
- this.logger.log(`Cleaned up ${before - after} expired cooldown(s), ${after} remaining`);
2891
- }
2892
- }
2893
- /**
2894
- * Get the cooldown duration in milliseconds
2588
+ * Get the cooldown duration in milliseconds
2895
2589
  */
2896
2590
  getCooldownDurationMs() {
2897
2591
  return _ProviderManager.COOLDOWN_DURATION_MS;
@@ -3037,7 +2731,7 @@ var ProviderManager = class _ProviderManager {
3037
2731
  if (this.isOnCooldown(baseUrl)) {
3038
2732
  continue;
3039
2733
  }
3040
- if (!torMode && (isOnionUrl(baseUrl) || isInsecureHttpUrl(baseUrl))) {
2734
+ if (!torMode && isOnionUrl(baseUrl)) {
3041
2735
  continue;
3042
2736
  }
3043
2737
  const model = models.find((m) => m.id === modelId);
@@ -3088,7 +2782,7 @@ var ProviderManager = class _ProviderManager {
3088
2782
  for (const [baseUrl, models] of Object.entries(allProviders)) {
3089
2783
  if (disabledProviders.has(baseUrl)) continue;
3090
2784
  if (this.isOnCooldown(baseUrl)) continue;
3091
- if (!torMode && (isOnionUrl(baseUrl) || isInsecureHttpUrl(baseUrl)))
2785
+ if (!torMode && isOnionUrl(baseUrl))
3092
2786
  continue;
3093
2787
  const model = models.find((m) => m.id === modelId);
3094
2788
  if (!model) continue;
@@ -3103,16 +2797,18 @@ var ProviderManager = class _ProviderManager {
3103
2797
  getProviderPriceRankingForModel(modelId, options = {}) {
3104
2798
  const includeDisabled = options.includeDisabled ?? false;
3105
2799
  const torMode = options.torMode ?? false;
3106
- const disabledProviders = new Set(
3107
- this.providerRegistry.getDisabledProviders()
3108
- );
2800
+ const disabledProviderList = this.providerRegistry.getDisabledProviders();
2801
+ const disabledProviders = new Set(disabledProviderList);
2802
+ if (disabledProviderList.length > 0) {
2803
+ this.logger.log(`getProviderPriceRankingForModel: disabled providers (${disabledProviderList.length}): ${disabledProviderList.join(", ")}`);
2804
+ }
3109
2805
  const allModels = this.providerRegistry.getAllProvidersModels();
3110
2806
  const results = [];
3111
2807
  for (const [baseUrl, models] of Object.entries(allModels)) {
3112
2808
  if (!includeDisabled && disabledProviders.has(baseUrl)) continue;
3113
2809
  if (this.isOnCooldown(baseUrl)) continue;
3114
2810
  if (torMode && !baseUrl.includes(".onion")) continue;
3115
- if (!torMode && (baseUrl.includes(".onion") || isInsecureHttpUrl(baseUrl)))
2811
+ if (!torMode && baseUrl.includes(".onion"))
3116
2812
  continue;
3117
2813
  const match = models.find((model) => model.id === modelId);
3118
2814
  if (!match?.sats_pricing) continue;
@@ -3132,12 +2828,20 @@ var ProviderManager = class _ProviderManager {
3132
2828
  totalPerMillion
3133
2829
  });
3134
2830
  }
3135
- return results.sort((a, b) => {
2831
+ results.sort((a, b) => {
3136
2832
  if (a.totalPerMillion !== b.totalPerMillion) {
3137
2833
  return a.totalPerMillion - b.totalPerMillion;
3138
2834
  }
3139
2835
  return a.baseUrl.localeCompare(b.baseUrl);
3140
2836
  });
2837
+ if (results.length > 0) {
2838
+ 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");
2839
+ this.logger.log(`getProviderPriceRankingForModel: ${modelId} ranking (${results.length} providers):
2840
+ ${ranking}`);
2841
+ } else {
2842
+ this.logger.log(`getProviderPriceRankingForModel: ${modelId} no providers found`);
2843
+ }
2844
+ return results;
3141
2845
  }
3142
2846
  /**
3143
2847
  * Get best-priced provider for a specific model
@@ -3330,141 +3034,6 @@ var createMemoryDriver = (seed) => {
3330
3034
  };
3331
3035
  };
3332
3036
 
3333
- // storage/drivers/sqlite.ts
3334
- var isBun = () => {
3335
- return typeof process.versions.bun !== "undefined";
3336
- };
3337
- var cachedDbModule = null;
3338
- var loadDatabase = async (dbPath) => {
3339
- if (isBun()) {
3340
- throw new Error(
3341
- "SQLite driver not supported in Bun. Use createBunSqliteDriver() instead."
3342
- );
3343
- }
3344
- try {
3345
- if (!cachedDbModule) {
3346
- cachedDbModule = (await import('better-sqlite3')).default;
3347
- }
3348
- return new cachedDbModule(dbPath);
3349
- } catch (error) {
3350
- throw new Error(
3351
- `better-sqlite3 is required for sqlite storage. Install it to use sqlite storage. (${error})`
3352
- );
3353
- }
3354
- };
3355
- var createSqliteDriver = (options = {}) => {
3356
- const dbPath = options.dbPath || "routstr.sqlite";
3357
- const tableName = options.tableName || "sdk_storage";
3358
- let db;
3359
- let selectStmt;
3360
- let upsertStmt;
3361
- let deleteStmt;
3362
- const initDb = async () => {
3363
- if (!db) {
3364
- db = await loadDatabase(dbPath);
3365
- db.exec(
3366
- `CREATE TABLE IF NOT EXISTS ${tableName} (key TEXT PRIMARY KEY, value TEXT NOT NULL)`
3367
- );
3368
- selectStmt = db.prepare(`SELECT value FROM ${tableName} WHERE key = ?`);
3369
- upsertStmt = db.prepare(
3370
- `INSERT INTO ${tableName} (key, value) VALUES (?, ?)
3371
- ON CONFLICT(key) DO UPDATE SET value = excluded.value`
3372
- );
3373
- deleteStmt = db.prepare(`DELETE FROM ${tableName} WHERE key = ?`);
3374
- }
3375
- };
3376
- const ensureInit = async () => {
3377
- if (!db) {
3378
- await initDb();
3379
- }
3380
- };
3381
- return {
3382
- async getItem(key, defaultValue) {
3383
- try {
3384
- await ensureInit();
3385
- const row = selectStmt.get(key);
3386
- if (!row || typeof row.value !== "string") return defaultValue;
3387
- try {
3388
- return JSON.parse(row.value);
3389
- } catch (parseError) {
3390
- if (typeof defaultValue === "string") {
3391
- return row.value;
3392
- }
3393
- throw parseError;
3394
- }
3395
- } catch (error) {
3396
- console.error(`SQLite getItem failed for key "${key}":`, error);
3397
- return defaultValue;
3398
- }
3399
- },
3400
- async setItem(key, value) {
3401
- try {
3402
- await ensureInit();
3403
- upsertStmt.run(key, JSON.stringify(value));
3404
- } catch (error) {
3405
- console.error(`SQLite setItem failed for key "${key}":`, error);
3406
- }
3407
- },
3408
- async removeItem(key) {
3409
- try {
3410
- await ensureInit();
3411
- deleteStmt.run(key);
3412
- } catch (error) {
3413
- console.error(`SQLite removeItem failed for key "${key}":`, error);
3414
- }
3415
- }
3416
- };
3417
- };
3418
- async function createBunSqliteDriver(dbPath, options) {
3419
- const logger = (options?.logger ?? consoleLogger).child("BunSqliteDriver");
3420
- const SQLite = (await import(
3421
- /* webpackIgnore: true */
3422
- 'bun:sqlite'
3423
- )).default;
3424
- const db = new SQLite(dbPath);
3425
- db.run(`
3426
- CREATE TABLE IF NOT EXISTS sdk_storage (
3427
- key TEXT PRIMARY KEY,
3428
- value TEXT NOT NULL
3429
- )
3430
- `);
3431
- return {
3432
- async getItem(key, defaultValue) {
3433
- try {
3434
- const row = db.query("SELECT value FROM sdk_storage WHERE key = ?").get(key);
3435
- if (!row || typeof row.value !== "string") return defaultValue;
3436
- try {
3437
- return JSON.parse(row.value);
3438
- } catch (parseError) {
3439
- if (typeof defaultValue === "string") {
3440
- return row.value;
3441
- }
3442
- throw parseError;
3443
- }
3444
- } catch (error) {
3445
- logger.error(`getItem failed for key "${key}":`, error);
3446
- return defaultValue;
3447
- }
3448
- },
3449
- async setItem(key, value) {
3450
- try {
3451
- db.query(
3452
- "INSERT INTO sdk_storage (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value"
3453
- ).run(key, JSON.stringify(value));
3454
- } catch (error) {
3455
- logger.error(`setItem failed for key "${key}":`, error);
3456
- }
3457
- },
3458
- async removeItem(key) {
3459
- try {
3460
- db.query("DELETE FROM sdk_storage WHERE key = ?").run(key);
3461
- } catch (error) {
3462
- logger.error(`removeItem failed for key "${key}":`, error);
3463
- }
3464
- }
3465
- };
3466
- }
3467
-
3468
3037
  // storage/drivers/indexedDB.ts
3469
3038
  var isBrowser = typeof indexedDB !== "undefined";
3470
3039
  var openDatabase = (dbName, storeName) => {
@@ -3472,15 +3041,32 @@ var openDatabase = (dbName, storeName) => {
3472
3041
  return Promise.reject(new Error("IndexedDB is not available"));
3473
3042
  }
3474
3043
  return new Promise((resolve, reject) => {
3475
- const request = indexedDB.open(dbName, 1);
3044
+ const request = indexedDB.open(dbName, 2);
3476
3045
  request.onupgradeneeded = () => {
3477
3046
  const db = request.result;
3478
3047
  if (!db.objectStoreNames.contains(storeName)) {
3479
3048
  db.createObjectStore(storeName);
3480
3049
  }
3050
+ if (storeName !== "usage_tracking" && !db.objectStoreNames.contains("usage_tracking")) {
3051
+ const utStore = db.createObjectStore("usage_tracking", { keyPath: "id" });
3052
+ utStore.createIndex("timestamp", "timestamp", { unique: false });
3053
+ utStore.createIndex("modelId", "modelId", { unique: false });
3054
+ utStore.createIndex("baseUrl", "baseUrl", { unique: false });
3055
+ utStore.createIndex("sessionId", "sessionId", { unique: false });
3056
+ utStore.createIndex("client", "client", { unique: false });
3057
+ }
3058
+ if (storeName !== "sdk_storage" && !db.objectStoreNames.contains("sdk_storage")) {
3059
+ db.createObjectStore("sdk_storage");
3060
+ }
3481
3061
  };
3482
3062
  request.onsuccess = () => resolve(request.result);
3483
3063
  request.onerror = () => reject(request.error);
3064
+ request.onblocked = () => {
3065
+ console.warn(
3066
+ `[IndexedDB driver] open blocked for "${dbName}" (store: "${storeName}") \u2014 close other tabs using this DB`
3067
+ );
3068
+ reject(new Error(`IndexedDB "${dbName}" blocked by another connection`));
3069
+ };
3484
3070
  });
3485
3071
  };
3486
3072
  var createIndexedDBDriver = (options = {}) => {
@@ -3582,6 +3168,91 @@ var SDK_STORAGE_KEYS = {
3582
3168
  PROVIDERS_ON_COOLDOWN: "providers_on_cooldown"
3583
3169
  };
3584
3170
 
3171
+ // storage/usageTracking/aggregate.ts
3172
+ var pad2 = (n) => String(n).padStart(2, "0");
3173
+ var jsGroupKey = (entry, groupBy, tzOffsetMinutes) => {
3174
+ switch (groupBy) {
3175
+ case "modelId":
3176
+ return entry.modelId ?? null;
3177
+ case "baseUrl":
3178
+ return entry.baseUrl ?? null;
3179
+ case "client":
3180
+ return entry.client ?? null;
3181
+ case "sessionId":
3182
+ return entry.sessionId ?? null;
3183
+ case "provider":
3184
+ return entry.provider ?? null;
3185
+ case "day": {
3186
+ const d = new Date(entry.timestamp - tzOffsetMinutes * 6e4);
3187
+ return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;
3188
+ }
3189
+ case "hour": {
3190
+ const d = new Date(entry.timestamp - tzOffsetMinutes * 6e4);
3191
+ return pad2(d.getUTCHours());
3192
+ }
3193
+ }
3194
+ };
3195
+ var reduceAggregate = (entries, options = {}) => {
3196
+ const emptyRow = (group) => ({
3197
+ group,
3198
+ requests: 0,
3199
+ promptTokens: 0,
3200
+ completionTokens: 0,
3201
+ totalTokens: 0,
3202
+ cost: 0,
3203
+ satsCost: 0,
3204
+ baseMsats: 0,
3205
+ inputMsats: 0,
3206
+ outputMsats: 0,
3207
+ totalMsats: 0,
3208
+ totalUsd: 0,
3209
+ cacheReadInputTokens: 0,
3210
+ cacheCreationInputTokens: 0,
3211
+ cacheReadMsats: 0,
3212
+ cacheCreationMsats: 0
3213
+ });
3214
+ const accumulate = (row, entry) => {
3215
+ row.requests += 1;
3216
+ row.promptTokens += entry.promptTokens;
3217
+ row.completionTokens += entry.completionTokens;
3218
+ row.totalTokens += entry.totalTokens;
3219
+ row.cost += entry.cost;
3220
+ row.satsCost += entry.satsCost;
3221
+ row.baseMsats += entry.baseMsats ?? 0;
3222
+ row.inputMsats += entry.inputMsats ?? 0;
3223
+ row.outputMsats += entry.outputMsats ?? 0;
3224
+ row.totalMsats += entry.totalMsats ?? 0;
3225
+ row.totalUsd += entry.totalUsd ?? 0;
3226
+ row.cacheReadInputTokens += entry.cacheReadInputTokens ?? 0;
3227
+ row.cacheCreationInputTokens += entry.cacheCreationInputTokens ?? 0;
3228
+ row.cacheReadMsats += entry.cacheReadMsats ?? 0;
3229
+ row.cacheCreationMsats += entry.cacheCreationMsats ?? 0;
3230
+ };
3231
+ if (!options.groupBy) {
3232
+ const total = emptyRow(null);
3233
+ for (const entry of entries) accumulate(total, entry);
3234
+ return [total];
3235
+ }
3236
+ const tz = options.tzOffsetMinutes ?? 0;
3237
+ const groups = /* @__PURE__ */ new Map();
3238
+ for (const entry of entries) {
3239
+ const key = jsGroupKey(entry, options.groupBy, tz);
3240
+ let row = groups.get(key);
3241
+ if (!row) {
3242
+ row = emptyRow(key);
3243
+ groups.set(key, row);
3244
+ }
3245
+ accumulate(row, entry);
3246
+ }
3247
+ const rows = [...groups.values()];
3248
+ if (options.groupBy === "day" || options.groupBy === "hour") {
3249
+ rows.sort((a, b) => (a.group ?? "").localeCompare(b.group ?? ""));
3250
+ } else {
3251
+ rows.sort((a, b) => b.satsCost - a.satsCost);
3252
+ }
3253
+ return rows;
3254
+ };
3255
+
3585
3256
  // storage/usageTracking/indexedDB.ts
3586
3257
  var DEFAULT_DB_NAME = "routstr-sdk";
3587
3258
  var DEFAULT_STORE_NAME = "usage_tracking";
@@ -3593,9 +3264,10 @@ var openDatabase2 = (dbName, storeName) => {
3593
3264
  return Promise.reject(new Error("IndexedDB is not available"));
3594
3265
  }
3595
3266
  return new Promise((resolve, reject) => {
3596
- const request = indexedDB.open(dbName, 1);
3267
+ const request = indexedDB.open(dbName, 3);
3597
3268
  request.onupgradeneeded = () => {
3598
3269
  const db = request.result;
3270
+ const tx = request.transaction;
3599
3271
  if (!db.objectStoreNames.contains(storeName)) {
3600
3272
  const store = db.createObjectStore(storeName, { keyPath: "id" });
3601
3273
  store.createIndex("timestamp", "timestamp", { unique: false });
@@ -3603,10 +3275,25 @@ var openDatabase2 = (dbName, storeName) => {
3603
3275
  store.createIndex("baseUrl", "baseUrl", { unique: false });
3604
3276
  store.createIndex("sessionId", "sessionId", { unique: false });
3605
3277
  store.createIndex("client", "client", { unique: false });
3278
+ store.createIndex("provider", "provider", { unique: false });
3279
+ } else if (tx) {
3280
+ const store = tx.objectStore(storeName);
3281
+ if (!store.indexNames.contains("provider")) {
3282
+ store.createIndex("provider", "provider", { unique: false });
3283
+ }
3284
+ }
3285
+ if (storeName !== "sdk_storage" && !db.objectStoreNames.contains("sdk_storage")) {
3286
+ db.createObjectStore("sdk_storage");
3606
3287
  }
3607
3288
  };
3608
3289
  request.onsuccess = () => resolve(request.result);
3609
3290
  request.onerror = () => reject(request.error);
3291
+ request.onblocked = () => {
3292
+ console.warn(
3293
+ `[usageTracking IndexedDB] open blocked for "${dbName}" \u2014 close other tabs using this DB`
3294
+ );
3295
+ reject(new Error(`IndexedDB "${dbName}" blocked by another connection`));
3296
+ };
3610
3297
  });
3611
3298
  };
3612
3299
  var matchesFilters = (entry, options = {}) => {
@@ -3628,6 +3315,12 @@ var matchesFilters = (entry, options = {}) => {
3628
3315
  if (options.client && entry.client !== options.client) {
3629
3316
  return false;
3630
3317
  }
3318
+ if (options.clients && options.clients.length > 0 && (entry.client == null || !options.clients.includes(entry.client))) {
3319
+ return false;
3320
+ }
3321
+ if (options.provider && entry.provider !== options.provider) {
3322
+ return false;
3323
+ }
3631
3324
  return true;
3632
3325
  };
3633
3326
  var createIndexedDBUsageTrackingDriver = (options = {}) => {
@@ -3723,6 +3416,10 @@ var createIndexedDBUsageTrackingDriver = (options = {}) => {
3723
3416
  const results = await this.list(options2);
3724
3417
  return results.length;
3725
3418
  },
3419
+ async aggregate(options2 = {}) {
3420
+ const entries = await this.list(options2);
3421
+ return reduceAggregate(entries, options2);
3422
+ },
3726
3423
  async deleteOlderThan(timestamp) {
3727
3424
  await ensureMigrated();
3728
3425
  const db = await getDb();
@@ -3759,393 +3456,8 @@ var createIndexedDBUsageTrackingDriver = (options = {}) => {
3759
3456
  };
3760
3457
  };
3761
3458
 
3762
- // storage/usageTracking/sqlite.ts
3763
- var MIGRATION_MARKER_KEY2 = "usage_tracking_migration_v1";
3764
- var normalizeBaseUrl2 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
3765
- var isBun2 = () => {
3766
- return typeof process.versions.bun !== "undefined";
3767
- };
3768
- var cachedDbModule2 = null;
3769
- var loadDatabase2 = async (dbPath) => {
3770
- if (isBun2()) {
3771
- throw new Error(
3772
- "SQLite driver not supported in Bun. Use createMemoryDriver() instead."
3773
- );
3774
- }
3775
- try {
3776
- if (!cachedDbModule2) {
3777
- cachedDbModule2 = (await import('better-sqlite3')).default;
3778
- }
3779
- return new cachedDbModule2(dbPath);
3780
- } catch (error) {
3781
- throw new Error(
3782
- `better-sqlite3 is required for sqlite usage tracking. Install it to use sqlite storage. (${error})`
3783
- );
3784
- }
3785
- };
3786
- var buildWhereClause = (options = {}) => {
3787
- const clauses = [];
3788
- const params = [];
3789
- if (typeof options.before === "number") {
3790
- clauses.push("timestamp < ?");
3791
- params.push(options.before);
3792
- }
3793
- if (typeof options.after === "number") {
3794
- clauses.push("timestamp > ?");
3795
- params.push(options.after);
3796
- }
3797
- if (options.modelId) {
3798
- clauses.push("model_id = ?");
3799
- params.push(options.modelId);
3800
- }
3801
- if (options.baseUrl) {
3802
- clauses.push("base_url = ?");
3803
- params.push(normalizeBaseUrl2(options.baseUrl));
3804
- }
3805
- if (options.sessionId) {
3806
- clauses.push("session_id = ?");
3807
- params.push(options.sessionId);
3808
- }
3809
- if (options.client) {
3810
- clauses.push("client = ?");
3811
- params.push(options.client);
3812
- }
3813
- return {
3814
- sql: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
3815
- params
3816
- };
3817
- };
3818
- var createSqliteUsageTrackingDriver = (options = {}) => {
3819
- const dbPath = options.dbPath || "routstr.sqlite";
3820
- const tableName = options.tableName || "usage_tracking";
3821
- const legacyStorageDriver = options.legacyStorageDriver;
3822
- let db;
3823
- let insertStmt;
3824
- let migrationComplete = false;
3825
- const initDb = async () => {
3826
- if (!db) {
3827
- db = await loadDatabase2(dbPath);
3828
- db.exec(`
3829
- CREATE TABLE IF NOT EXISTS ${tableName} (
3830
- id TEXT PRIMARY KEY,
3831
- timestamp INTEGER NOT NULL,
3832
- model_id TEXT NOT NULL,
3833
- base_url TEXT NOT NULL,
3834
- request_id TEXT NOT NULL,
3835
- cost REAL NOT NULL,
3836
- sats_cost REAL NOT NULL,
3837
- prompt_tokens INTEGER NOT NULL,
3838
- completion_tokens INTEGER NOT NULL,
3839
- total_tokens INTEGER NOT NULL,
3840
- client TEXT,
3841
- session_id TEXT,
3842
- tags TEXT
3843
- );
3844
- CREATE INDEX IF NOT EXISTS idx_${tableName}_timestamp ON ${tableName}(timestamp);
3845
- CREATE INDEX IF NOT EXISTS idx_${tableName}_model_id ON ${tableName}(model_id);
3846
- CREATE INDEX IF NOT EXISTS idx_${tableName}_base_url ON ${tableName}(base_url);
3847
- CREATE INDEX IF NOT EXISTS idx_${tableName}_session_id ON ${tableName}(session_id);
3848
- CREATE INDEX IF NOT EXISTS idx_${tableName}_client ON ${tableName}(client);
3849
- `);
3850
- insertStmt = db.prepare(`
3851
- INSERT OR REPLACE INTO ${tableName} (
3852
- id, timestamp, model_id, base_url, request_id,
3853
- cost, sats_cost, prompt_tokens, completion_tokens, total_tokens,
3854
- client, session_id, tags
3855
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3856
- `);
3857
- }
3858
- };
3859
- const ensureInit = async () => {
3860
- if (!db) {
3861
- await initDb();
3862
- }
3863
- };
3864
- const appendOne = (entry) => {
3865
- insertStmt.run(
3866
- entry.id,
3867
- entry.timestamp,
3868
- entry.modelId,
3869
- normalizeBaseUrl2(entry.baseUrl),
3870
- entry.requestId,
3871
- entry.cost,
3872
- entry.satsCost,
3873
- entry.promptTokens,
3874
- entry.completionTokens,
3875
- entry.totalTokens,
3876
- entry.client ?? null,
3877
- entry.sessionId ?? null,
3878
- JSON.stringify(entry.tags ?? [])
3879
- );
3880
- };
3881
- const ensureMigrated = async () => {
3882
- if (!legacyStorageDriver || migrationComplete) return;
3883
- const migrated = await legacyStorageDriver.getItem(
3884
- MIGRATION_MARKER_KEY2,
3885
- false
3886
- );
3887
- if (migrated) {
3888
- migrationComplete = true;
3889
- return;
3890
- }
3891
- const legacyEntries = await legacyStorageDriver.getItem(
3892
- SDK_STORAGE_KEYS.USAGE_TRACKING,
3893
- []
3894
- );
3895
- for (const entry of legacyEntries) {
3896
- appendOne(entry);
3897
- }
3898
- if (legacyEntries.length > 0) {
3899
- await legacyStorageDriver.removeItem(SDK_STORAGE_KEYS.USAGE_TRACKING);
3900
- }
3901
- await legacyStorageDriver.setItem(MIGRATION_MARKER_KEY2, true);
3902
- migrationComplete = true;
3903
- };
3904
- const mapRow = (row) => ({
3905
- id: row.id,
3906
- timestamp: row.timestamp,
3907
- modelId: row.model_id,
3908
- baseUrl: row.base_url,
3909
- requestId: row.request_id,
3910
- cost: row.cost,
3911
- satsCost: row.sats_cost,
3912
- promptTokens: row.prompt_tokens,
3913
- completionTokens: row.completion_tokens,
3914
- totalTokens: row.total_tokens,
3915
- client: row.client ?? void 0,
3916
- sessionId: row.session_id ?? void 0,
3917
- tags: typeof row.tags === "string" ? JSON.parse(row.tags) : void 0
3918
- });
3919
- return {
3920
- async migrate() {
3921
- await ensureInit();
3922
- await ensureMigrated();
3923
- },
3924
- async append(entry) {
3925
- await ensureInit();
3926
- await ensureMigrated();
3927
- appendOne(entry);
3928
- },
3929
- async appendMany(entries) {
3930
- await ensureInit();
3931
- await ensureMigrated();
3932
- for (const entry of entries) {
3933
- appendOne(entry);
3934
- }
3935
- },
3936
- async list(options2 = {}) {
3937
- await ensureInit();
3938
- await ensureMigrated();
3939
- const { sql, params } = buildWhereClause(options2);
3940
- const limitSql = typeof options2.limit === "number" ? " LIMIT ?" : "";
3941
- const stmt = db.prepare(
3942
- `SELECT * FROM ${tableName} ${sql} ORDER BY timestamp DESC${limitSql}`
3943
- );
3944
- const rows = stmt.all(
3945
- ...typeof options2.limit === "number" ? [...params, options2.limit] : params
3946
- );
3947
- return rows.map(mapRow);
3948
- },
3949
- async count(options2 = {}) {
3950
- await ensureInit();
3951
- await ensureMigrated();
3952
- const { sql, params } = buildWhereClause(options2);
3953
- const stmt = db.prepare(`SELECT COUNT(*) as count FROM ${tableName} ${sql}`);
3954
- const row = stmt.get(...params);
3955
- return Number(row?.count ?? 0);
3956
- },
3957
- async deleteOlderThan(timestamp) {
3958
- await ensureInit();
3959
- await ensureMigrated();
3960
- const stmt = db.prepare(`DELETE FROM ${tableName} WHERE timestamp < ?`);
3961
- const result = stmt.run(timestamp);
3962
- return result.changes;
3963
- },
3964
- async clear() {
3965
- await ensureInit();
3966
- await ensureMigrated();
3967
- db.prepare(`DELETE FROM ${tableName}`).run();
3968
- }
3969
- };
3970
- };
3971
-
3972
- // storage/usageTracking/bunSqlite.ts
3973
- var MIGRATION_MARKER_KEY3 = "usage_tracking_migration_v1";
3974
- var normalizeBaseUrl3 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
3975
- var buildWhereClause2 = (options = {}) => {
3976
- const clauses = [];
3977
- const params = [];
3978
- if (typeof options.before === "number") {
3979
- clauses.push("timestamp < ?");
3980
- params.push(options.before);
3981
- }
3982
- if (typeof options.after === "number") {
3983
- clauses.push("timestamp > ?");
3984
- params.push(options.after);
3985
- }
3986
- if (options.modelId) {
3987
- clauses.push("model_id = ?");
3988
- params.push(options.modelId);
3989
- }
3990
- if (options.baseUrl) {
3991
- clauses.push("base_url = ?");
3992
- params.push(normalizeBaseUrl3(options.baseUrl));
3993
- }
3994
- if (options.sessionId) {
3995
- clauses.push("session_id = ?");
3996
- params.push(options.sessionId);
3997
- }
3998
- if (options.client) {
3999
- clauses.push("client = ?");
4000
- params.push(options.client);
4001
- }
4002
- return {
4003
- sql: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
4004
- params
4005
- };
4006
- };
4007
- var createBunSqliteUsageTrackingDriver = (options = {}) => {
4008
- const dbPath = options.dbPath || "routstr.sqlite";
4009
- const tableName = options.tableName || "usage_tracking";
4010
- const legacyStorageDriver = options.legacyStorageDriver;
4011
- const SQLiteDatabase = options.sqlite?.Database;
4012
- let migrationPromise = null;
4013
- if (!SQLiteDatabase) {
4014
- throw new Error(
4015
- "Bun SQLite Database constructor is required. Pass { sqlite: { Database } } when creating the driver."
4016
- );
4017
- }
4018
- const db = new SQLiteDatabase(dbPath);
4019
- db.run(`
4020
- CREATE TABLE IF NOT EXISTS ${tableName} (
4021
- id TEXT PRIMARY KEY,
4022
- timestamp INTEGER NOT NULL,
4023
- model_id TEXT NOT NULL,
4024
- base_url TEXT NOT NULL,
4025
- request_id TEXT NOT NULL,
4026
- cost REAL NOT NULL,
4027
- sats_cost REAL NOT NULL,
4028
- prompt_tokens INTEGER NOT NULL,
4029
- completion_tokens INTEGER NOT NULL,
4030
- total_tokens INTEGER NOT NULL,
4031
- client TEXT,
4032
- session_id TEXT,
4033
- tags TEXT
4034
- )
4035
- `);
4036
- db.run(`CREATE INDEX IF NOT EXISTS idx_${tableName}_timestamp ON ${tableName}(timestamp)`);
4037
- db.run(`CREATE INDEX IF NOT EXISTS idx_${tableName}_model_id ON ${tableName}(model_id)`);
4038
- db.run(`CREATE INDEX IF NOT EXISTS idx_${tableName}_base_url ON ${tableName}(base_url)`);
4039
- const appendOne = (entry) => {
4040
- db.query(`
4041
- INSERT OR REPLACE INTO ${tableName} (
4042
- id, timestamp, model_id, base_url, request_id,
4043
- cost, sats_cost, prompt_tokens, completion_tokens, total_tokens,
4044
- client, session_id, tags
4045
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
4046
- `).run(
4047
- entry.id,
4048
- entry.timestamp,
4049
- entry.modelId,
4050
- normalizeBaseUrl3(entry.baseUrl),
4051
- entry.requestId,
4052
- entry.cost,
4053
- entry.satsCost,
4054
- entry.promptTokens,
4055
- entry.completionTokens,
4056
- entry.totalTokens,
4057
- entry.client ?? null,
4058
- entry.sessionId ?? null,
4059
- JSON.stringify(entry.tags ?? [])
4060
- );
4061
- };
4062
- const mapRow = (row) => ({
4063
- id: row.id,
4064
- timestamp: row.timestamp,
4065
- modelId: row.model_id,
4066
- baseUrl: row.base_url,
4067
- requestId: row.request_id,
4068
- cost: row.cost,
4069
- satsCost: row.sats_cost,
4070
- promptTokens: row.prompt_tokens,
4071
- completionTokens: row.completion_tokens,
4072
- totalTokens: row.total_tokens,
4073
- client: row.client ?? void 0,
4074
- sessionId: row.session_id ?? void 0,
4075
- tags: typeof row.tags === "string" ? JSON.parse(row.tags) : void 0
4076
- });
4077
- const ensureMigrated = async () => {
4078
- if (!legacyStorageDriver) return;
4079
- if (!migrationPromise) {
4080
- migrationPromise = (async () => {
4081
- const migrated = await legacyStorageDriver.getItem(
4082
- MIGRATION_MARKER_KEY3,
4083
- false
4084
- );
4085
- if (migrated) return;
4086
- const legacyEntries = await legacyStorageDriver.getItem(
4087
- SDK_STORAGE_KEYS.USAGE_TRACKING,
4088
- []
4089
- );
4090
- if (legacyEntries.length > 0) {
4091
- for (const entry of legacyEntries) {
4092
- appendOne(entry);
4093
- }
4094
- await legacyStorageDriver.removeItem(SDK_STORAGE_KEYS.USAGE_TRACKING);
4095
- }
4096
- await legacyStorageDriver.setItem(MIGRATION_MARKER_KEY3, true);
4097
- })();
4098
- }
4099
- await migrationPromise;
4100
- };
4101
- return {
4102
- async migrate() {
4103
- await ensureMigrated();
4104
- },
4105
- async append(entry) {
4106
- await ensureMigrated();
4107
- appendOne(entry);
4108
- },
4109
- async appendMany(entries) {
4110
- await ensureMigrated();
4111
- for (const entry of entries) {
4112
- appendOne(entry);
4113
- }
4114
- },
4115
- async list(options2 = {}) {
4116
- await ensureMigrated();
4117
- const { sql, params } = buildWhereClause2(options2);
4118
- const limitSql = typeof options2.limit === "number" ? " LIMIT ?" : "";
4119
- const query = `SELECT * FROM ${tableName} ${sql} ORDER BY timestamp DESC${limitSql}`;
4120
- let rows;
4121
- if (typeof options2.limit === "number") {
4122
- rows = db.query(query).all(...params, options2.limit);
4123
- } else {
4124
- rows = db.query(query).all(...params);
4125
- }
4126
- return rows.map(mapRow);
4127
- },
4128
- async count(options2 = {}) {
4129
- const { sql, params } = buildWhereClause2(options2);
4130
- const query = `SELECT COUNT(*) as count FROM ${tableName} ${sql}`;
4131
- const row = db.query(query).get(...params);
4132
- return Number(row?.count ?? 0);
4133
- },
4134
- async deleteOlderThan(timestamp) {
4135
- await ensureMigrated();
4136
- const before = timestamp;
4137
- const result = db.query(`DELETE FROM ${tableName} WHERE timestamp < ?`).run(before);
4138
- return result.changes ?? 0;
4139
- },
4140
- async clear() {
4141
- await ensureMigrated();
4142
- db.query(`DELETE FROM ${tableName}`).run();
4143
- }
4144
- };
4145
- };
4146
-
4147
3459
  // storage/usageTracking/memory.ts
4148
- var normalizeBaseUrl4 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
3460
+ var normalizeBaseUrl2 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
4149
3461
  var matchesFilters2 = (entry, options = {}) => {
4150
3462
  if (typeof options.before === "number" && entry.timestamp >= options.before) {
4151
3463
  return false;
@@ -4156,7 +3468,7 @@ var matchesFilters2 = (entry, options = {}) => {
4156
3468
  if (options.modelId && entry.modelId !== options.modelId) {
4157
3469
  return false;
4158
3470
  }
4159
- if (options.baseUrl && normalizeBaseUrl4(entry.baseUrl) !== normalizeBaseUrl4(options.baseUrl)) {
3471
+ if (options.baseUrl && normalizeBaseUrl2(entry.baseUrl) !== normalizeBaseUrl2(options.baseUrl)) {
4160
3472
  return false;
4161
3473
  }
4162
3474
  if (options.sessionId && entry.sessionId !== options.sessionId) {
@@ -4165,23 +3477,29 @@ var matchesFilters2 = (entry, options = {}) => {
4165
3477
  if (options.client && entry.client !== options.client) {
4166
3478
  return false;
4167
3479
  }
3480
+ if (options.clients && options.clients.length > 0 && (entry.client == null || !options.clients.includes(entry.client))) {
3481
+ return false;
3482
+ }
3483
+ if (options.provider && entry.provider !== options.provider) {
3484
+ return false;
3485
+ }
4168
3486
  return true;
4169
3487
  };
4170
3488
  var createMemoryUsageTrackingDriver = (seed = []) => {
4171
3489
  const store = /* @__PURE__ */ new Map();
4172
3490
  for (const entry of seed) {
4173
- store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl4(entry.baseUrl) });
3491
+ store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl2(entry.baseUrl) });
4174
3492
  }
4175
3493
  return {
4176
3494
  async migrate() {
4177
3495
  return;
4178
3496
  },
4179
3497
  async append(entry) {
4180
- store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl4(entry.baseUrl) });
3498
+ store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl2(entry.baseUrl) });
4181
3499
  },
4182
3500
  async appendMany(entries) {
4183
3501
  for (const entry of entries) {
4184
- store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl4(entry.baseUrl) });
3502
+ store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl2(entry.baseUrl) });
4185
3503
  }
4186
3504
  },
4187
3505
  async list(options = {}) {
@@ -4194,6 +3512,10 @@ var createMemoryUsageTrackingDriver = (seed = []) => {
4194
3512
  async count(options = {}) {
4195
3513
  return (await this.list(options)).length;
4196
3514
  },
3515
+ async aggregate(options = {}) {
3516
+ const entries = [...store.values()].filter((entry) => matchesFilters2(entry, options));
3517
+ return reduceAggregate(entries, options);
3518
+ },
4197
3519
  async deleteOlderThan(timestamp) {
4198
3520
  let deleted = 0;
4199
3521
  for (const [id, entry] of store.entries()) {
@@ -4209,7 +3531,7 @@ var createMemoryUsageTrackingDriver = (seed = []) => {
4209
3531
  }
4210
3532
  };
4211
3533
  };
4212
- var normalizeBaseUrl5 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
3534
+ var normalizeBaseUrl3 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
4213
3535
  var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
4214
3536
  modelsFromAllProviders: {},
4215
3537
  lastUsedModel: null,
@@ -4232,7 +3554,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
4232
3554
  setModelsFromAllProviders: (value) => {
4233
3555
  const normalized = {};
4234
3556
  for (const [baseUrl, models] of Object.entries(value)) {
4235
- normalized[normalizeBaseUrl5(baseUrl)] = models;
3557
+ normalized[normalizeBaseUrl3(baseUrl)] = models;
4236
3558
  }
4237
3559
  void driver.setItem(
4238
3560
  SDK_STORAGE_KEYS.MODELS_FROM_ALL_PROVIDERS,
@@ -4245,7 +3567,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
4245
3567
  set({ lastUsedModel: value });
4246
3568
  },
4247
3569
  setBaseUrlsList: (value) => {
4248
- const normalized = value.map((url) => normalizeBaseUrl5(url));
3570
+ const normalized = value.map((url) => normalizeBaseUrl3(url));
4249
3571
  void driver.setItem(SDK_STORAGE_KEYS.BASE_URLS_LIST, normalized);
4250
3572
  set({ baseUrlsList: normalized });
4251
3573
  },
@@ -4254,14 +3576,14 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
4254
3576
  set({ lastBaseUrlsUpdate: value });
4255
3577
  },
4256
3578
  setDisabledProviders: (value) => {
4257
- const normalized = value.map((url) => normalizeBaseUrl5(url));
3579
+ const normalized = value.map((url) => normalizeBaseUrl3(url));
4258
3580
  void driver.setItem(SDK_STORAGE_KEYS.DISABLED_PROVIDERS, normalized);
4259
3581
  set({ disabledProviders: normalized });
4260
3582
  },
4261
3583
  setMintsFromAllProviders: (value) => {
4262
3584
  const normalized = {};
4263
3585
  for (const [baseUrl, mints] of Object.entries(value)) {
4264
- normalized[normalizeBaseUrl5(baseUrl)] = mints.map(
3586
+ normalized[normalizeBaseUrl3(baseUrl)] = mints.map(
4265
3587
  (mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint
4266
3588
  );
4267
3589
  }
@@ -4274,7 +3596,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
4274
3596
  setInfoFromAllProviders: (value) => {
4275
3597
  const normalized = {};
4276
3598
  for (const [baseUrl, info] of Object.entries(value)) {
4277
- normalized[normalizeBaseUrl5(baseUrl)] = info;
3599
+ normalized[normalizeBaseUrl3(baseUrl)] = info;
4278
3600
  }
4279
3601
  void driver.setItem(SDK_STORAGE_KEYS.INFO_FROM_ALL_PROVIDERS, normalized);
4280
3602
  set({ infoFromAllProviders: normalized });
@@ -4282,7 +3604,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
4282
3604
  setLastModelsUpdate: (value) => {
4283
3605
  const normalized = {};
4284
3606
  for (const [baseUrl, timestamp] of Object.entries(value)) {
4285
- normalized[normalizeBaseUrl5(baseUrl)] = timestamp;
3607
+ normalized[normalizeBaseUrl3(baseUrl)] = timestamp;
4286
3608
  }
4287
3609
  void driver.setItem(SDK_STORAGE_KEYS.LAST_MODELS_UPDATE, normalized);
4288
3610
  set({ lastModelsUpdate: normalized });
@@ -4292,7 +3614,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
4292
3614
  const updates = typeof value === "function" ? value(state.apiKeys) : value;
4293
3615
  const normalized = updates.map((entry) => ({
4294
3616
  ...entry,
4295
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
3617
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
4296
3618
  balance: entry.balance ?? 0,
4297
3619
  lastUsed: entry.lastUsed ?? null
4298
3620
  }));
@@ -4304,7 +3626,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
4304
3626
  set((state) => {
4305
3627
  const updates = typeof value === "function" ? value(state.childKeys) : value;
4306
3628
  const normalized = updates.map((entry) => ({
4307
- parentBaseUrl: normalizeBaseUrl5(entry.parentBaseUrl),
3629
+ parentBaseUrl: normalizeBaseUrl3(entry.parentBaseUrl),
4308
3630
  childKey: entry.childKey,
4309
3631
  balance: entry.balance ?? 0,
4310
3632
  balanceLimit: entry.balanceLimit,
@@ -4318,9 +3640,9 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
4318
3640
  setXcashuTokens: (value) => {
4319
3641
  const normalized = {};
4320
3642
  for (const [baseUrl, tokens] of Object.entries(value)) {
4321
- normalized[normalizeBaseUrl5(baseUrl)] = tokens.map((entry) => ({
3643
+ normalized[normalizeBaseUrl3(baseUrl)] = tokens.map((entry) => ({
4322
3644
  ...entry,
4323
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
3645
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
4324
3646
  createdAt: entry.createdAt ?? Date.now(),
4325
3647
  tryCount: entry.tryCount ?? 0
4326
3648
  }));
@@ -4371,12 +3693,12 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
4371
3693
  },
4372
3694
  // ========== Failure Tracking ==========
4373
3695
  setFailedProviders: (value) => {
4374
- const normalized = value.map((url) => normalizeBaseUrl5(url));
3696
+ const normalized = value.map((url) => normalizeBaseUrl3(url));
4375
3697
  void driver.setItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, normalized);
4376
3698
  set({ failedProviders: normalized });
4377
3699
  },
4378
3700
  addFailedProvider: (baseUrl) => {
4379
- const normalized = normalizeBaseUrl5(baseUrl);
3701
+ const normalized = normalizeBaseUrl3(baseUrl);
4380
3702
  const current = get().failedProviders;
4381
3703
  if (!current.includes(normalized)) {
4382
3704
  const updated = [...current, normalized];
@@ -4385,7 +3707,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
4385
3707
  }
4386
3708
  },
4387
3709
  removeFailedProvider: (baseUrl) => {
4388
- const normalized = normalizeBaseUrl5(baseUrl);
3710
+ const normalized = normalizeBaseUrl3(baseUrl);
4389
3711
  const current = get().failedProviders;
4390
3712
  const updated = current.filter((url) => url !== normalized);
4391
3713
  void driver.setItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, updated);
@@ -4394,13 +3716,13 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
4394
3716
  setLastFailed: (value) => {
4395
3717
  const normalized = {};
4396
3718
  for (const [baseUrl, timestamp] of Object.entries(value)) {
4397
- normalized[normalizeBaseUrl5(baseUrl)] = timestamp;
3719
+ normalized[normalizeBaseUrl3(baseUrl)] = timestamp;
4398
3720
  }
4399
3721
  void driver.setItem(SDK_STORAGE_KEYS.LAST_FAILED, normalized);
4400
3722
  set({ lastFailed: normalized });
4401
3723
  },
4402
3724
  setLastFailedTimestamp: (baseUrl, timestamp) => {
4403
- const normalized = normalizeBaseUrl5(baseUrl);
3725
+ const normalized = normalizeBaseUrl3(baseUrl);
4404
3726
  const current = get().lastFailed;
4405
3727
  const updated = { ...current, [normalized]: timestamp };
4406
3728
  void driver.setItem(SDK_STORAGE_KEYS.LAST_FAILED, updated);
@@ -4408,14 +3730,14 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
4408
3730
  },
4409
3731
  setProvidersOnCooldown: (value) => {
4410
3732
  const normalized = value.map((entry) => ({
4411
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
3733
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
4412
3734
  timestamp: entry.timestamp
4413
3735
  }));
4414
3736
  void driver.setItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, normalized);
4415
3737
  set({ providersOnCooldown: normalized });
4416
3738
  },
4417
3739
  addProviderOnCooldown: (baseUrl, timestamp) => {
4418
- const normalized = normalizeBaseUrl5(baseUrl);
3740
+ const normalized = normalizeBaseUrl3(baseUrl);
4419
3741
  const current = get().providersOnCooldown;
4420
3742
  if (!current.some((entry) => entry.baseUrl === normalized)) {
4421
3743
  const updated = [...current, { baseUrl: normalized, timestamp }];
@@ -4424,7 +3746,7 @@ var createEmptyStore = (driver) => vanilla.createStore((set, get) => ({
4424
3746
  }
4425
3747
  },
4426
3748
  removeProviderFromCooldown: (baseUrl) => {
4427
- const normalized = normalizeBaseUrl5(baseUrl);
3749
+ const normalized = normalizeBaseUrl3(baseUrl);
4428
3750
  const current = get().providersOnCooldown;
4429
3751
  const updated = current.filter((entry) => entry.baseUrl !== normalized);
4430
3752
  void driver.setItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, updated);
@@ -4495,40 +3817,40 @@ var hydrateStoreFromDriver = async (store, driver) => {
4495
3817
  ]);
4496
3818
  const modelsFromAllProviders = Object.fromEntries(
4497
3819
  Object.entries(rawModels).map(([baseUrl, models]) => [
4498
- normalizeBaseUrl5(baseUrl),
3820
+ normalizeBaseUrl3(baseUrl),
4499
3821
  models
4500
3822
  ])
4501
3823
  );
4502
- const baseUrlsList = rawBaseUrls.map((url) => normalizeBaseUrl5(url));
3824
+ const baseUrlsList = rawBaseUrls.map((url) => normalizeBaseUrl3(url));
4503
3825
  const disabledProviders = rawDisabledProviders.map(
4504
- (url) => normalizeBaseUrl5(url)
3826
+ (url) => normalizeBaseUrl3(url)
4505
3827
  );
4506
3828
  const mintsFromAllProviders = Object.fromEntries(
4507
3829
  Object.entries(rawMints).map(([baseUrl, mints]) => [
4508
- normalizeBaseUrl5(baseUrl),
3830
+ normalizeBaseUrl3(baseUrl),
4509
3831
  mints.map((mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint)
4510
3832
  ])
4511
3833
  );
4512
3834
  const infoFromAllProviders = Object.fromEntries(
4513
3835
  Object.entries(rawInfo).map(([baseUrl, info]) => [
4514
- normalizeBaseUrl5(baseUrl),
3836
+ normalizeBaseUrl3(baseUrl),
4515
3837
  info
4516
3838
  ])
4517
3839
  );
4518
3840
  const lastModelsUpdate = Object.fromEntries(
4519
3841
  Object.entries(rawLastModelsUpdate).map(([baseUrl, timestamp]) => [
4520
- normalizeBaseUrl5(baseUrl),
3842
+ normalizeBaseUrl3(baseUrl),
4521
3843
  timestamp
4522
3844
  ])
4523
3845
  );
4524
3846
  const apiKeys = rawApiKeys.map((entry) => ({
4525
3847
  ...entry,
4526
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
3848
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
4527
3849
  balance: entry.balance ?? 0,
4528
3850
  lastUsed: entry.lastUsed ?? null
4529
3851
  }));
4530
3852
  const childKeys = rawChildKeys.map((entry) => ({
4531
- parentBaseUrl: normalizeBaseUrl5(entry.parentBaseUrl),
3853
+ parentBaseUrl: normalizeBaseUrl3(entry.parentBaseUrl),
4532
3854
  childKey: entry.childKey,
4533
3855
  balance: entry.balance ?? 0,
4534
3856
  balanceLimit: entry.balanceLimit,
@@ -4537,9 +3859,9 @@ var hydrateStoreFromDriver = async (store, driver) => {
4537
3859
  }));
4538
3860
  const xcashuTokens = Object.fromEntries(
4539
3861
  Object.entries(rawXcashuTokens).map(([baseUrl, tokens]) => [
4540
- normalizeBaseUrl5(baseUrl),
3862
+ normalizeBaseUrl3(baseUrl),
4541
3863
  tokens.map((entry) => ({
4542
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
3864
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
4543
3865
  token: entry.token,
4544
3866
  createdAt: entry.createdAt ?? Date.now(),
4545
3867
  tryCount: entry.tryCount ?? 0
@@ -4560,16 +3882,16 @@ var hydrateStoreFromDriver = async (store, driver) => {
4560
3882
  lastUsed: entry.lastUsed ?? null
4561
3883
  }));
4562
3884
  const failedProviders = rawFailedProviders.map(
4563
- (url) => normalizeBaseUrl5(url)
3885
+ (url) => normalizeBaseUrl3(url)
4564
3886
  );
4565
3887
  const lastFailed = Object.fromEntries(
4566
3888
  Object.entries(rawLastFailed).map(([baseUrl, timestamp]) => [
4567
- normalizeBaseUrl5(baseUrl),
3889
+ normalizeBaseUrl3(baseUrl),
4568
3890
  timestamp
4569
3891
  ])
4570
3892
  );
4571
3893
  const providersOnCooldown = rawProvidersOnCooldown.map((entry) => ({
4572
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
3894
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
4573
3895
  timestamp: entry.timestamp
4574
3896
  }));
4575
3897
  store.setState({
@@ -4610,12 +3932,12 @@ var createDiscoveryAdapterFromStore = (store) => ({
4610
3932
  getCachedProviderInfo: () => store.getState().infoFromAllProviders,
4611
3933
  setCachedProviderInfo: (info) => store.getState().setInfoFromAllProviders(info),
4612
3934
  getProviderLastUpdate: (baseUrl) => {
4613
- const normalized = normalizeBaseUrl5(baseUrl);
3935
+ const normalized = normalizeBaseUrl3(baseUrl);
4614
3936
  const timestamps = store.getState().lastModelsUpdate;
4615
3937
  return timestamps[normalized] || null;
4616
3938
  },
4617
3939
  setProviderLastUpdate: (baseUrl, timestamp) => {
4618
- const normalized = normalizeBaseUrl5(baseUrl);
3940
+ const normalized = normalizeBaseUrl3(baseUrl);
4619
3941
  const timestamps = { ...store.getState().lastModelsUpdate };
4620
3942
  timestamps[normalized] = timestamp;
4621
3943
  store.getState().setLastModelsUpdate(timestamps);
@@ -4644,24 +3966,24 @@ var createStorageAdapterFromStore = (store) => ({
4644
3966
  return Object.entries(distributionMap).map(([baseUrl, amt]) => ({ baseUrl, amount: amt })).sort((a, b) => b.amount - a.amount);
4645
3967
  },
4646
3968
  saveProviderInfo: (baseUrl, info) => {
4647
- const normalized = normalizeBaseUrl5(baseUrl);
3969
+ const normalized = normalizeBaseUrl3(baseUrl);
4648
3970
  const next = { ...store.getState().infoFromAllProviders };
4649
3971
  next[normalized] = info;
4650
3972
  store.getState().setInfoFromAllProviders(next);
4651
3973
  },
4652
3974
  getProviderInfo: (baseUrl) => {
4653
- const normalized = normalizeBaseUrl5(baseUrl);
3975
+ const normalized = normalizeBaseUrl3(baseUrl);
4654
3976
  return store.getState().infoFromAllProviders[normalized] || null;
4655
3977
  },
4656
3978
  // ========== API Keys (for apikeys mode) ==========
4657
3979
  getApiKey: (baseUrl) => {
4658
- const normalized = normalizeBaseUrl5(baseUrl);
3980
+ const normalized = normalizeBaseUrl3(baseUrl);
4659
3981
  const entry = store.getState().apiKeys.find((key) => key.baseUrl === normalized);
4660
3982
  if (!entry) return null;
4661
3983
  return entry;
4662
3984
  },
4663
3985
  setApiKey: (baseUrl, key) => {
4664
- const normalized = normalizeBaseUrl5(baseUrl);
3986
+ const normalized = normalizeBaseUrl3(baseUrl);
4665
3987
  const keys = store.getState().apiKeys;
4666
3988
  const existingIndex = keys.findIndex(
4667
3989
  (entry) => entry.baseUrl === normalized
@@ -4679,7 +4001,7 @@ var createStorageAdapterFromStore = (store) => ({
4679
4001
  store.getState().setApiKeys(next);
4680
4002
  },
4681
4003
  updateApiKeyBalance: (baseUrl, balance) => {
4682
- const normalized = normalizeBaseUrl5(baseUrl);
4004
+ const normalized = normalizeBaseUrl3(baseUrl);
4683
4005
  const keys = store.getState().apiKeys;
4684
4006
  const next = keys.map(
4685
4007
  (entry) => entry.baseUrl === normalized ? { ...entry, balance, lastUsed: Date.now() } : entry
@@ -4687,7 +4009,7 @@ var createStorageAdapterFromStore = (store) => ({
4687
4009
  store.getState().setApiKeys(next);
4688
4010
  },
4689
4011
  removeApiKey: (baseUrl) => {
4690
- const normalized = normalizeBaseUrl5(baseUrl);
4012
+ const normalized = normalizeBaseUrl3(baseUrl);
4691
4013
  const next = store.getState().apiKeys.filter((entry) => entry.baseUrl !== normalized);
4692
4014
  store.getState().setApiKeys(next);
4693
4015
  },
@@ -4701,7 +4023,7 @@ var createStorageAdapterFromStore = (store) => ({
4701
4023
  },
4702
4024
  // ========== Child Keys ==========
4703
4025
  getChildKey: (parentBaseUrl) => {
4704
- const normalized = normalizeBaseUrl5(parentBaseUrl);
4026
+ const normalized = normalizeBaseUrl3(parentBaseUrl);
4705
4027
  const entry = store.getState().childKeys.find((key) => key.parentBaseUrl === normalized);
4706
4028
  if (!entry) return null;
4707
4029
  return {
@@ -4714,7 +4036,7 @@ var createStorageAdapterFromStore = (store) => ({
4714
4036
  };
4715
4037
  },
4716
4038
  setChildKey: (parentBaseUrl, childKey, balance, validityDate, balanceLimit) => {
4717
- const normalized = normalizeBaseUrl5(parentBaseUrl);
4039
+ const normalized = normalizeBaseUrl3(parentBaseUrl);
4718
4040
  const keys = store.getState().childKeys;
4719
4041
  const existingIndex = keys.findIndex(
4720
4042
  (entry) => entry.parentBaseUrl === normalized
@@ -4745,7 +4067,7 @@ var createStorageAdapterFromStore = (store) => ({
4745
4067
  }
4746
4068
  },
4747
4069
  updateChildKeyBalance: (parentBaseUrl, balance) => {
4748
- const normalized = normalizeBaseUrl5(parentBaseUrl);
4070
+ const normalized = normalizeBaseUrl3(parentBaseUrl);
4749
4071
  const keys = store.getState().childKeys;
4750
4072
  const next = keys.map(
4751
4073
  (entry) => entry.parentBaseUrl === normalized ? { ...entry, balance } : entry
@@ -4753,7 +4075,7 @@ var createStorageAdapterFromStore = (store) => ({
4753
4075
  store.getState().setChildKeys(next);
4754
4076
  },
4755
4077
  removeChildKey: (parentBaseUrl) => {
4756
- const normalized = normalizeBaseUrl5(parentBaseUrl);
4078
+ const normalized = normalizeBaseUrl3(parentBaseUrl);
4757
4079
  const next = store.getState().childKeys.filter((entry) => entry.parentBaseUrl !== normalized);
4758
4080
  store.getState().setChildKeys(next);
4759
4081
  },
@@ -4778,11 +4100,11 @@ var createStorageAdapterFromStore = (store) => ({
4778
4100
  return store.getState().xcashuTokens;
4779
4101
  },
4780
4102
  getXcashuTokensForBaseUrl: (baseUrl) => {
4781
- const normalized = normalizeBaseUrl5(baseUrl);
4103
+ const normalized = normalizeBaseUrl3(baseUrl);
4782
4104
  return store.getState().xcashuTokens[normalized] || [];
4783
4105
  },
4784
4106
  addXcashuToken: (baseUrl, token) => {
4785
- const normalized = normalizeBaseUrl5(baseUrl);
4107
+ const normalized = normalizeBaseUrl3(baseUrl);
4786
4108
  const tokens = store.getState().xcashuTokens;
4787
4109
  const existing = tokens[normalized] || [];
4788
4110
  const next = { ...tokens };
@@ -4793,7 +4115,7 @@ var createStorageAdapterFromStore = (store) => ({
4793
4115
  store.getState().setXcashuTokens(next);
4794
4116
  },
4795
4117
  removeXcashuToken: (baseUrl, token) => {
4796
- const normalized = normalizeBaseUrl5(baseUrl);
4118
+ const normalized = normalizeBaseUrl3(baseUrl);
4797
4119
  const tokens = store.getState().xcashuTokens;
4798
4120
  const existing = tokens[normalized] || [];
4799
4121
  const next = { ...tokens };
@@ -4804,7 +4126,7 @@ var createStorageAdapterFromStore = (store) => ({
4804
4126
  store.getState().setXcashuTokens(next);
4805
4127
  },
4806
4128
  clearXcashuTokensForBaseUrl: (baseUrl) => {
4807
- const normalized = normalizeBaseUrl5(baseUrl);
4129
+ const normalized = normalizeBaseUrl3(baseUrl);
4808
4130
  const tokens = store.getState().xcashuTokens;
4809
4131
  const next = { ...tokens };
4810
4132
  delete next[normalized];
@@ -4818,16 +4140,16 @@ var createProviderRegistryFromStore = (store, logger) => {
4818
4140
  const log = (logger ?? consoleLogger).child("ProviderRegistry");
4819
4141
  return {
4820
4142
  getModelsForProvider: (baseUrl) => {
4821
- const normalized = normalizeBaseUrl5(baseUrl);
4143
+ const normalized = normalizeBaseUrl3(baseUrl);
4822
4144
  return store.getState().modelsFromAllProviders[normalized] || [];
4823
4145
  },
4824
4146
  getDisabledProviders: () => store.getState().disabledProviders,
4825
4147
  getProviderMints: (baseUrl) => {
4826
- const normalized = normalizeBaseUrl5(baseUrl);
4148
+ const normalized = normalizeBaseUrl3(baseUrl);
4827
4149
  return store.getState().mintsFromAllProviders[normalized] || [];
4828
4150
  },
4829
4151
  getProviderInfo: async (baseUrl) => {
4830
- const normalized = normalizeBaseUrl5(baseUrl);
4152
+ const normalized = normalizeBaseUrl3(baseUrl);
4831
4153
  const cached = store.getState().infoFromAllProviders[normalized];
4832
4154
  if (cached) return cached;
4833
4155
  try {
@@ -4853,11 +4175,11 @@ var createProviderRegistryFromStore = (store, logger) => {
4853
4175
  var MODEL_KEY_PREFIX = "models:provider:";
4854
4176
  var MODEL_TS_KEY_PREFIX = "models:provider_timestamp:";
4855
4177
  var PROVIDER_INDEX_KEY = "models:provider_index";
4856
- var MIGRATION_MARKER_KEY4 = "models_sharded_migration_v1";
4178
+ var MIGRATION_MARKER_KEY2 = "models_sharded_migration_v1";
4857
4179
  var encodeBaseUrl = (baseUrl) => encodeURIComponent(baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
4858
4180
  var modelKey = (baseUrl) => `${MODEL_KEY_PREFIX}${encodeBaseUrl(baseUrl)}`;
4859
4181
  var modelTsKey = (baseUrl) => `${MODEL_TS_KEY_PREFIX}${encodeBaseUrl(baseUrl)}`;
4860
- var normalizeBaseUrl6 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
4182
+ var normalizeBaseUrl4 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
4861
4183
  var createShardedDiscoveryAdapter = async (options) => {
4862
4184
  const { driver } = options;
4863
4185
  const legacyModels = await driver.getItem(SDK_STORAGE_KEYS.MODELS_FROM_ALL_PROVIDERS, {});
@@ -4865,7 +4187,7 @@ var createShardedDiscoveryAdapter = async (options) => {
4865
4187
  if (Object.keys(legacyModels).length > 0) {
4866
4188
  const migratedProviders = [];
4867
4189
  for (const [baseUrl, models] of Object.entries(legacyModels)) {
4868
- const normalized = normalizeBaseUrl6(baseUrl);
4190
+ const normalized = normalizeBaseUrl4(baseUrl);
4869
4191
  await driver.setItem(modelKey(normalized), models);
4870
4192
  const ts = legacyTimestamps[normalized] ?? Date.now();
4871
4193
  await driver.setItem(modelTsKey(normalized), ts);
@@ -4880,7 +4202,7 @@ var createShardedDiscoveryAdapter = async (options) => {
4880
4202
  await driver.removeItem(SDK_STORAGE_KEYS.MODELS_FROM_ALL_PROVIDERS);
4881
4203
  await driver.removeItem(SDK_STORAGE_KEYS.LAST_MODELS_UPDATE);
4882
4204
  }
4883
- await driver.setItem(MIGRATION_MARKER_KEY4, true);
4205
+ await driver.setItem(MIGRATION_MARKER_KEY2, true);
4884
4206
  const [
4885
4207
  rawMints,
4886
4208
  rawInfo,
@@ -4914,31 +4236,31 @@ var createShardedDiscoveryAdapter = async (options) => {
4914
4236
  const providerIndex = /* @__PURE__ */ new Set();
4915
4237
  const knownProviders = /* @__PURE__ */ new Set();
4916
4238
  for (const baseUrl of Object.keys(rawInfo)) {
4917
- knownProviders.add(normalizeBaseUrl6(baseUrl));
4239
+ knownProviders.add(normalizeBaseUrl4(baseUrl));
4918
4240
  }
4919
4241
  for (const baseUrl of Object.keys(rawMints)) {
4920
- knownProviders.add(normalizeBaseUrl6(baseUrl));
4242
+ knownProviders.add(normalizeBaseUrl4(baseUrl));
4921
4243
  }
4922
4244
  for (const baseUrl of rawBaseUrls) {
4923
- knownProviders.add(normalizeBaseUrl6(baseUrl));
4245
+ knownProviders.add(normalizeBaseUrl4(baseUrl));
4924
4246
  }
4925
4247
  for (const baseUrl of rawDisabled) {
4926
- knownProviders.add(normalizeBaseUrl6(baseUrl));
4248
+ knownProviders.add(normalizeBaseUrl4(baseUrl));
4927
4249
  }
4928
4250
  for (const baseUrl of Object.keys(legacyModels)) {
4929
- knownProviders.add(normalizeBaseUrl6(baseUrl));
4251
+ knownProviders.add(normalizeBaseUrl4(baseUrl));
4930
4252
  }
4931
4253
  const indexProviders = await driver.getItem(
4932
4254
  PROVIDER_INDEX_KEY,
4933
4255
  []
4934
4256
  );
4935
4257
  for (const baseUrl of indexProviders) {
4936
- const normalized = normalizeBaseUrl6(baseUrl);
4258
+ const normalized = normalizeBaseUrl4(baseUrl);
4937
4259
  providerIndex.add(normalized);
4938
4260
  knownProviders.add(normalized);
4939
4261
  }
4940
4262
  for (const baseUrl of knownProviders) {
4941
- const normalized = normalizeBaseUrl6(baseUrl);
4263
+ const normalized = normalizeBaseUrl4(baseUrl);
4942
4264
  const models = await driver.getItem(
4943
4265
  modelKey(normalized),
4944
4266
  null
@@ -4959,19 +4281,19 @@ var createShardedDiscoveryAdapter = async (options) => {
4959
4281
  }
4960
4282
  let mints = Object.fromEntries(
4961
4283
  Object.entries(rawMints).map(([baseUrl, mintList]) => [
4962
- normalizeBaseUrl6(baseUrl),
4284
+ normalizeBaseUrl4(baseUrl),
4963
4285
  mintList.map((mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint)
4964
4286
  ])
4965
4287
  );
4966
4288
  let info = Object.fromEntries(
4967
4289
  Object.entries(rawInfo).map(([baseUrl, entry]) => [
4968
- normalizeBaseUrl6(baseUrl),
4290
+ normalizeBaseUrl4(baseUrl),
4969
4291
  entry
4970
4292
  ])
4971
4293
  );
4972
4294
  let _lastUsedModel = lastUsedModel;
4973
- let _disabledProviders = rawDisabled.map(normalizeBaseUrl6);
4974
- let _baseUrlsList = rawBaseUrls.map(normalizeBaseUrl6);
4295
+ let _disabledProviders = rawDisabled.map(normalizeBaseUrl4);
4296
+ let _baseUrlsList = rawBaseUrls.map(normalizeBaseUrl4);
4975
4297
  let _lastBaseUrlsUpdate = lastBaseUrlsUpdate;
4976
4298
  let _routstr21Models = rawRoutstr21Models;
4977
4299
  let _lastRoutstr21ModelsUpdate = lastRoutstr21ModelsUpdate;
@@ -4989,10 +4311,10 @@ var createShardedDiscoveryAdapter = async (options) => {
4989
4311
  },
4990
4312
  setCachedModels: (models) => {
4991
4313
  const nextKeys = new Set(
4992
- Object.keys(models).map((baseUrl) => normalizeBaseUrl6(baseUrl))
4314
+ Object.keys(models).map((baseUrl) => normalizeBaseUrl4(baseUrl))
4993
4315
  );
4994
4316
  for (const baseUrl of [...modelsByBaseUrl.keys()]) {
4995
- if (!nextKeys.has(normalizeBaseUrl6(baseUrl))) {
4317
+ if (!nextKeys.has(normalizeBaseUrl4(baseUrl))) {
4996
4318
  providerIndex.delete(baseUrl);
4997
4319
  modelsByBaseUrl.delete(baseUrl);
4998
4320
  timestampsByBaseUrl.delete(baseUrl);
@@ -5001,7 +4323,7 @@ var createShardedDiscoveryAdapter = async (options) => {
5001
4323
  }
5002
4324
  }
5003
4325
  for (const [baseUrl, modelList] of Object.entries(models)) {
5004
- const normalized = normalizeBaseUrl6(baseUrl);
4326
+ const normalized = normalizeBaseUrl4(baseUrl);
5005
4327
  providerIndex.add(normalized);
5006
4328
  modelsByBaseUrl.set(normalized, modelList);
5007
4329
  const ts = timestampsByBaseUrl.get(normalized) ?? Date.now();
@@ -5012,10 +4334,10 @@ var createShardedDiscoveryAdapter = async (options) => {
5012
4334
  persistProviderIndex();
5013
4335
  },
5014
4336
  getProviderLastUpdate: (baseUrl) => {
5015
- return timestampsByBaseUrl.get(normalizeBaseUrl6(baseUrl)) ?? null;
4337
+ return timestampsByBaseUrl.get(normalizeBaseUrl4(baseUrl)) ?? null;
5016
4338
  },
5017
4339
  setProviderLastUpdate: (baseUrl, timestamp) => {
5018
- const normalized = normalizeBaseUrl6(baseUrl);
4340
+ const normalized = normalizeBaseUrl4(baseUrl);
5019
4341
  providerIndex.add(normalized);
5020
4342
  timestampsByBaseUrl.set(normalized, timestamp);
5021
4343
  void driver.setItem(modelTsKey(normalized), timestamp);
@@ -5026,7 +4348,7 @@ var createShardedDiscoveryAdapter = async (options) => {
5026
4348
  setCachedMints: (value) => {
5027
4349
  const normalized = {};
5028
4350
  for (const [baseUrl, mintList] of Object.entries(value)) {
5029
- normalized[normalizeBaseUrl6(baseUrl)] = mintList.map(
4351
+ normalized[normalizeBaseUrl4(baseUrl)] = mintList.map(
5030
4352
  (mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint
5031
4353
  );
5032
4354
  }
@@ -5038,7 +4360,7 @@ var createShardedDiscoveryAdapter = async (options) => {
5038
4360
  setCachedProviderInfo: (value) => {
5039
4361
  const normalized = {};
5040
4362
  for (const [baseUrl, entry] of Object.entries(value)) {
5041
- normalized[normalizeBaseUrl6(baseUrl)] = entry;
4363
+ normalized[normalizeBaseUrl4(baseUrl)] = entry;
5042
4364
  }
5043
4365
  info = normalized;
5044
4366
  void driver.setItem(SDK_STORAGE_KEYS.INFO_FROM_ALL_PROVIDERS, normalized);
@@ -5052,7 +4374,7 @@ var createShardedDiscoveryAdapter = async (options) => {
5052
4374
  // -- Disabled providers (kv) --
5053
4375
  getDisabledProviders: () => _disabledProviders,
5054
4376
  setDisabledProviders: (urls) => {
5055
- const normalized = urls.map(normalizeBaseUrl6);
4377
+ const normalized = urls.map(normalizeBaseUrl4);
5056
4378
  _disabledProviders = normalized;
5057
4379
  void driver.setItem(SDK_STORAGE_KEYS.DISABLED_PROVIDERS, normalized);
5058
4380
  },
@@ -5060,7 +4382,7 @@ var createShardedDiscoveryAdapter = async (options) => {
5060
4382
  getBaseUrlsList: () => _baseUrlsList,
5061
4383
  getBaseUrlsLastUpdate: () => _lastBaseUrlsUpdate,
5062
4384
  setBaseUrlsList: (urls) => {
5063
- const normalized = urls.map(normalizeBaseUrl6);
4385
+ const normalized = urls.map(normalizeBaseUrl4);
5064
4386
  _baseUrlsList = normalized;
5065
4387
  void driver.setItem(SDK_STORAGE_KEYS.BASE_URLS_LIST, normalized);
5066
4388
  },
@@ -5088,16 +4410,16 @@ var createProviderRegistryFromDiscoveryAdapter = (adapter, logger) => {
5088
4410
  const log = (logger ?? consoleLogger).child("ProviderRegistry");
5089
4411
  return {
5090
4412
  getModelsForProvider: (baseUrl) => {
5091
- const normalized = normalizeBaseUrl6(baseUrl);
4413
+ const normalized = normalizeBaseUrl4(baseUrl);
5092
4414
  return adapter.getCachedModels()[normalized] || [];
5093
4415
  },
5094
4416
  getDisabledProviders: () => adapter.getDisabledProviders(),
5095
4417
  getProviderMints: (baseUrl) => {
5096
- const normalized = normalizeBaseUrl6(baseUrl);
4418
+ const normalized = normalizeBaseUrl4(baseUrl);
5097
4419
  return adapter.getCachedMints()[normalized] || [];
5098
4420
  },
5099
4421
  getProviderInfo: async (baseUrl) => {
5100
- const normalized = normalizeBaseUrl6(baseUrl);
4422
+ const normalized = normalizeBaseUrl4(baseUrl);
5101
4423
  const cached = adapter.getCachedProviderInfo()[normalized];
5102
4424
  if (cached) return cached;
5103
4425
  try {
@@ -5128,31 +4450,13 @@ var isBrowser3 = () => {
5128
4450
  return false;
5129
4451
  }
5130
4452
  };
5131
- var isNode = () => {
5132
- try {
5133
- return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
5134
- } catch {
5135
- return false;
5136
- }
5137
- };
5138
4453
  var defaultDriver = null;
5139
- var isBun3 = () => {
5140
- return typeof process.versions.bun !== "undefined";
5141
- };
5142
4454
  var getDefaultSdkDriver = () => {
5143
4455
  if (defaultDriver) return defaultDriver;
5144
4456
  if (isBrowser3()) {
5145
4457
  defaultDriver = localStorageDriver;
5146
4458
  return defaultDriver;
5147
4459
  }
5148
- if (isBun3()) {
5149
- defaultDriver = createMemoryDriver();
5150
- return defaultDriver;
5151
- }
5152
- if (isNode()) {
5153
- defaultDriver = createSqliteDriver();
5154
- return defaultDriver;
5155
- }
5156
4460
  defaultDriver = createMemoryDriver();
5157
4461
  return defaultDriver;
5158
4462
  };
@@ -5173,16 +4477,6 @@ var getDefaultUsageTrackingDriver = () => {
5173
4477
  });
5174
4478
  return defaultUsageTrackingDriver;
5175
4479
  }
5176
- if (isBun3()) {
5177
- defaultUsageTrackingDriver = createBunSqliteUsageTrackingDriver();
5178
- return defaultUsageTrackingDriver;
5179
- }
5180
- if (isNode()) {
5181
- defaultUsageTrackingDriver = createSqliteUsageTrackingDriver({
5182
- legacyStorageDriver: storageDriver
5183
- });
5184
- return defaultUsageTrackingDriver;
5185
- }
5186
4480
  defaultUsageTrackingDriver = createMemoryUsageTrackingDriver();
5187
4481
  return defaultUsageTrackingDriver;
5188
4482
  };
@@ -5198,36 +4492,206 @@ var getDefaultDiscoveryAdapter = async () => {
5198
4492
  };
5199
4493
  var getDefaultStorageAdapter = async () => createStorageAdapterFromStore(await getDefaultSdkStore());
5200
4494
  var getDefaultProviderRegistry = async () => createProviderRegistryFromDiscoveryAdapter(await getDefaultDiscoveryAdapter());
5201
- function mergeUsage(previous, next) {
5202
- if (!previous) return next;
4495
+
4496
+ // client/usage.ts
4497
+ var numOrUndef = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
4498
+ function extractCostBreakdown(costObj) {
4499
+ if (!costObj || typeof costObj !== "object") return {};
5203
4500
  return {
5204
- promptTokens: next.promptTokens > 0 ? next.promptTokens : previous.promptTokens,
5205
- completionTokens: next.completionTokens > 0 ? next.completionTokens : previous.completionTokens,
5206
- totalTokens: next.totalTokens > 0 ? next.totalTokens : previous.totalTokens,
5207
- cost: next.cost > 0 ? next.cost : previous.cost,
5208
- satsCost: next.satsCost > 0 ? next.satsCost : previous.satsCost
4501
+ baseMsats: numOrUndef(costObj.base_msats),
4502
+ inputMsats: numOrUndef(costObj.input_msats),
4503
+ outputMsats: numOrUndef(costObj.output_msats),
4504
+ totalMsats: numOrUndef(costObj.total_msats),
4505
+ totalUsd: numOrUndef(costObj.total_usd),
4506
+ cacheReadInputTokens: numOrUndef(costObj.cache_read_input_tokens),
4507
+ cacheCreationInputTokens: numOrUndef(costObj.cache_creation_input_tokens),
4508
+ cacheReadMsats: numOrUndef(costObj.cache_read_msats),
4509
+ cacheCreationMsats: numOrUndef(costObj.cache_creation_msats),
4510
+ remainingBalanceMsats: numOrUndef(costObj.remaining_balance_msats)
5209
4511
  };
5210
4512
  }
5211
- function hasUsageChanged(previous, next) {
5212
- if (!previous) return true;
5213
- return previous.promptTokens !== next.promptTokens || previous.completionTokens !== next.completionTokens || previous.totalTokens !== next.totalTokens || previous.cost !== next.cost || previous.satsCost !== next.satsCost;
5214
- }
5215
- async function inspectSSEWebStream(stream, onUsage, onResponseId) {
5216
- const reader = stream.getReader();
5217
- const decoder = new TextDecoder("utf-8");
5218
- let buffer = "";
5219
- let capturedUsage = null;
5220
- let capturedResponseId;
4513
+ function extractUsageFromResponseBody(body, fallbackSatsCost = 0) {
4514
+ if (!body || typeof body !== "object") return null;
4515
+ const usage = body.usage;
4516
+ if (!usage || typeof usage !== "object") return null;
4517
+ const promptTokens = Number(usage.prompt_tokens ?? 0);
4518
+ const completionTokens = Number(usage.completion_tokens ?? 0);
4519
+ const totalTokens = Number(usage.total_tokens ?? 0);
4520
+ const costValue = usage.cost;
4521
+ let cost = 0;
4522
+ let satsCost = fallbackSatsCost;
4523
+ let breakdown = {};
4524
+ if (typeof costValue === "number") {
4525
+ cost = costValue;
4526
+ } else if (costValue && typeof costValue === "object") {
4527
+ const costObj = costValue;
4528
+ const totalUsd = costObj.total_usd;
4529
+ const totalMsats = costObj.total_msats;
4530
+ cost = typeof totalUsd === "number" ? totalUsd : 0;
4531
+ if (typeof totalMsats === "number") {
4532
+ satsCost = totalMsats / 1e3;
4533
+ }
4534
+ breakdown = extractCostBreakdown(costObj);
4535
+ }
4536
+ const provider = typeof body.provider === "string" ? body.provider : void 0;
4537
+ if (promptTokens === 0 && completionTokens === 0 && totalTokens === 0 && cost === 0 && satsCost === 0) {
4538
+ return null;
4539
+ }
4540
+ return {
4541
+ promptTokens,
4542
+ completionTokens,
4543
+ totalTokens,
4544
+ cost,
4545
+ satsCost,
4546
+ provider,
4547
+ ...breakdown
4548
+ };
4549
+ }
4550
+ function extractResponseId(body) {
4551
+ if (!body || typeof body !== "object") return void 0;
4552
+ const id = body.id;
4553
+ if (typeof id !== "string") return void 0;
4554
+ const trimmed = id.trim();
4555
+ return trimmed.length > 0 ? trimmed : void 0;
4556
+ }
4557
+ function extractUsageFromSSEJson(parsed, fallbackSatsCost = 0) {
4558
+ if (!parsed || typeof parsed !== "object") {
4559
+ return null;
4560
+ }
4561
+ const provider = typeof parsed.provider === "string" ? parsed.provider : void 0;
4562
+ if (!parsed.usage && parsed.cost && typeof parsed.cost === "object") {
4563
+ const costObj = parsed.cost;
4564
+ const msats2 = costObj.total_msats ?? 0;
4565
+ const cost2 = costObj.total_usd ?? 0;
4566
+ if (msats2 === 0 && cost2 === 0) return null;
4567
+ return {
4568
+ promptTokens: Number(costObj.input_tokens ?? 0),
4569
+ completionTokens: Number(costObj.output_tokens ?? 0),
4570
+ totalTokens: Number((costObj.input_tokens ?? 0) + (costObj.output_tokens ?? 0)),
4571
+ cost: Number(cost2),
4572
+ satsCost: msats2 > 0 ? msats2 / 1e3 : fallbackSatsCost,
4573
+ provider,
4574
+ ...extractCostBreakdown(costObj)
4575
+ };
4576
+ }
4577
+ if (!parsed.usage) {
4578
+ return null;
4579
+ }
4580
+ const usage = parsed.usage;
4581
+ const usageCost = usage.cost;
4582
+ let cost = 0;
4583
+ let msats = 0;
4584
+ let breakdown = {};
4585
+ if (typeof usageCost === "number") {
4586
+ cost = usageCost;
4587
+ } else if (usageCost && typeof usageCost === "object") {
4588
+ cost = usageCost.total_usd ?? 0;
4589
+ msats = usageCost.total_msats ?? 0;
4590
+ breakdown = extractCostBreakdown(usageCost);
4591
+ }
4592
+ const routstrCost = parsed.metadata?.routstr?.cost;
4593
+ if (routstrCost && typeof routstrCost === "object") {
4594
+ breakdown = { ...extractCostBreakdown(routstrCost), ...breakdown };
4595
+ }
4596
+ if (cost === 0) {
4597
+ cost = parsed.metadata?.routstr?.cost?.total_usd ?? 0;
4598
+ }
4599
+ if (msats === 0) {
4600
+ msats = parsed.metadata?.routstr?.cost?.total_msats ?? (typeof usage.cost_sats === "number" ? usage.cost_sats * 1e3 : 0);
4601
+ }
4602
+ const promptTokens = Number(usage.prompt_tokens ?? usage.input_tokens ?? 0);
4603
+ const completionTokens = Number(usage.completion_tokens ?? usage.output_tokens ?? 0);
4604
+ const totalTokens = Number(usage.total_tokens ?? promptTokens + completionTokens);
4605
+ const result = {
4606
+ promptTokens,
4607
+ completionTokens,
4608
+ totalTokens,
4609
+ cost: Number(cost ?? 0),
4610
+ satsCost: msats > 0 ? msats / 1e3 : fallbackSatsCost,
4611
+ provider,
4612
+ ...breakdown
4613
+ };
4614
+ if (result.promptTokens === 0 && result.completionTokens === 0 && result.totalTokens === 0 && result.cost === 0 && result.satsCost === 0) {
4615
+ return null;
4616
+ }
4617
+ return result;
4618
+ }
4619
+ function toUsageStats(usage) {
4620
+ if (!usage) return void 0;
4621
+ return {
4622
+ total_tokens: usage.totalTokens,
4623
+ prompt_tokens: usage.promptTokens,
4624
+ completion_tokens: usage.completionTokens,
4625
+ cost: usage.cost,
4626
+ sats_cost: usage.satsCost
4627
+ };
4628
+ }
4629
+ function mergeUsage(previous, next) {
4630
+ if (!previous) return next;
4631
+ const pickNum = (n, p) => typeof n === "number" && n > 0 ? n : p ?? n;
4632
+ return {
4633
+ promptTokens: next.promptTokens > 0 ? next.promptTokens : previous.promptTokens,
4634
+ completionTokens: next.completionTokens > 0 ? next.completionTokens : previous.completionTokens,
4635
+ totalTokens: next.totalTokens > 0 ? next.totalTokens : previous.totalTokens,
4636
+ cost: next.cost > 0 ? next.cost : previous.cost,
4637
+ satsCost: next.satsCost > 0 ? next.satsCost : previous.satsCost,
4638
+ provider: next.provider ?? previous.provider,
4639
+ baseMsats: pickNum(next.baseMsats, previous.baseMsats),
4640
+ inputMsats: pickNum(next.inputMsats, previous.inputMsats),
4641
+ outputMsats: pickNum(next.outputMsats, previous.outputMsats),
4642
+ totalMsats: pickNum(next.totalMsats, previous.totalMsats),
4643
+ totalUsd: pickNum(next.totalUsd, previous.totalUsd),
4644
+ cacheReadInputTokens: pickNum(
4645
+ next.cacheReadInputTokens,
4646
+ previous.cacheReadInputTokens
4647
+ ),
4648
+ cacheCreationInputTokens: pickNum(
4649
+ next.cacheCreationInputTokens,
4650
+ previous.cacheCreationInputTokens
4651
+ ),
4652
+ cacheReadMsats: pickNum(next.cacheReadMsats, previous.cacheReadMsats),
4653
+ cacheCreationMsats: pickNum(
4654
+ next.cacheCreationMsats,
4655
+ previous.cacheCreationMsats
4656
+ ),
4657
+ remainingBalanceMsats: pickNum(
4658
+ next.remainingBalanceMsats,
4659
+ previous.remainingBalanceMsats
4660
+ )
4661
+ };
4662
+ }
4663
+ function hasUsageChanged(previous, next) {
4664
+ if (!previous) return true;
4665
+ 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;
4666
+ }
4667
+ function isInspectionComplete(responseIdCaptured, usage) {
4668
+ return responseIdCaptured && !!usage && usage.totalTokens > 0 && typeof usage.totalMsats === "number" && !!usage.provider;
4669
+ }
4670
+ async function inspectSSEWebStream(stream, onUsage, onResponseId, options) {
4671
+ const reader = stream.getReader();
4672
+ const decoder = new TextDecoder("utf-8");
4673
+ let buffer = "";
4674
+ let capturedUsage = null;
4675
+ let capturedResponseId;
5221
4676
  let responseIdCaptured = false;
4677
+ let rawChunkSequence = 0;
5222
4678
  const inspectDataPayload = (jsonText) => {
5223
- if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
4679
+ const trimmed = jsonText.trim();
4680
+ if (!trimmed || trimmed === "[DONE]") {
4681
+ if (trimmed === "[DONE]") console.log("[routstr:sse] [DONE]");
4682
+ return;
4683
+ }
4684
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
4685
+ console.log("[routstr:sse] non-JSON payload:", trimmed.slice(0, 200));
5224
4686
  return;
5225
4687
  }
5226
- const trimmed = jsonText.trim();
5227
- if (!trimmed || trimmed === "[DONE]") return;
5228
- if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return;
5229
4688
  try {
5230
4689
  const data = JSON.parse(trimmed);
4690
+ console.log("[routstr:sse] chunk:", JSON.stringify(data));
4691
+ if (isInspectionComplete(responseIdCaptured, capturedUsage)) {
4692
+ console.log("[routstr:sse] (inspection already complete, skipping)");
4693
+ return;
4694
+ }
5231
4695
  if (!responseIdCaptured) {
5232
4696
  const responseId = data?.id;
5233
4697
  if (typeof responseId === "string" && responseId.trim().length > 0) {
@@ -5238,19 +4702,21 @@ async function inspectSSEWebStream(stream, onUsage, onResponseId) {
5238
4702
  }
5239
4703
  const usage = extractUsageFromSSEJson(data);
5240
4704
  if (usage) {
4705
+ console.log("[routstr:sse] \u2192 usage detected:", usage);
5241
4706
  const merged = mergeUsage(capturedUsage, usage);
5242
4707
  if (hasUsageChanged(capturedUsage, merged)) {
5243
4708
  capturedUsage = merged;
4709
+ console.log("[routstr:sse] \u2192 merged (changed):", merged);
5244
4710
  onUsage(merged);
4711
+ } else {
4712
+ console.log("[routstr:sse] \u2192 merged (no change)");
5245
4713
  }
5246
4714
  }
5247
4715
  } catch {
4716
+ console.log("[routstr:sse] failed to parse payload:", trimmed.slice(0, 200));
5248
4717
  }
5249
4718
  };
5250
4719
  const inspectEventBlock = (eventBlock) => {
5251
- if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
5252
- return;
5253
- }
5254
4720
  const lines = eventBlock.split(/\r?\n/);
5255
4721
  const dataParts = [];
5256
4722
  for (const line of lines) {
@@ -5279,7 +4745,9 @@ async function inspectSSEWebStream(stream, onUsage, onResponseId) {
5279
4745
  const { value, done } = await reader.read();
5280
4746
  if (done) break;
5281
4747
  if (value && value.byteLength > 0) {
5282
- buffer += decoder.decode(value, { stream: true });
4748
+ const text = decoder.decode(value, { stream: true });
4749
+ void options?.onRawChunk?.(value, rawChunkSequence++, text);
4750
+ buffer += text;
5283
4751
  drainBufferedEvents();
5284
4752
  }
5285
4753
  }
@@ -5308,14 +4776,22 @@ function createSSEParserTransform(onUsage, onResponseId) {
5308
4776
  let capturedUsage = null;
5309
4777
  let responseIdCaptured = false;
5310
4778
  const inspectDataPayload = (jsonText) => {
5311
- if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
4779
+ const trimmed = jsonText.trim();
4780
+ if (!trimmed || trimmed === "[DONE]") {
4781
+ if (trimmed === "[DONE]") console.log("[routstr:sse] [DONE]");
4782
+ return;
4783
+ }
4784
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
4785
+ console.log("[routstr:sse] non-JSON payload:", trimmed.slice(0, 200));
5312
4786
  return;
5313
4787
  }
5314
- const trimmed = jsonText.trim();
5315
- if (!trimmed || trimmed === "[DONE]") return;
5316
- if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return;
5317
4788
  try {
5318
4789
  const data = JSON.parse(trimmed);
4790
+ console.log("[routstr:sse] chunk:", JSON.stringify(data));
4791
+ if (isInspectionComplete(responseIdCaptured, capturedUsage)) {
4792
+ console.log("[routstr:sse] (inspection already complete, skipping)");
4793
+ return;
4794
+ }
5319
4795
  if (!responseIdCaptured) {
5320
4796
  const responseId = data?.id;
5321
4797
  if (typeof responseId === "string" && responseId.trim().length > 0) {
@@ -5325,19 +4801,21 @@ function createSSEParserTransform(onUsage, onResponseId) {
5325
4801
  }
5326
4802
  const usage = extractUsageFromSSEJson(data);
5327
4803
  if (usage) {
4804
+ console.log("[routstr:sse] \u2192 usage detected:", usage);
5328
4805
  const mergedUsage = mergeUsage(capturedUsage, usage);
5329
4806
  if (hasUsageChanged(capturedUsage, mergedUsage)) {
5330
4807
  capturedUsage = mergedUsage;
4808
+ console.log("[routstr:sse] \u2192 merged (changed):", mergedUsage);
5331
4809
  onUsage(mergedUsage);
4810
+ } else {
4811
+ console.log("[routstr:sse] \u2192 merged (no change)");
5332
4812
  }
5333
4813
  }
5334
4814
  } catch {
4815
+ console.log("[routstr:sse] failed to parse payload:", trimmed.slice(0, 200));
5335
4816
  }
5336
4817
  };
5337
4818
  const inspectEventBlock = (eventBlock) => {
5338
- if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
5339
- return;
5340
- }
5341
4819
  const lines = eventBlock.split(/\r?\n/);
5342
4820
  const dataParts = [];
5343
4821
  for (const line of lines) {
@@ -5410,11 +4888,11 @@ var RoutstrClient = class {
5410
4888
  this.balanceManager,
5411
4889
  this.logger
5412
4890
  );
5413
- this.streamProcessor = new StreamProcessor();
5414
4891
  this.alertLevel = alertLevel;
5415
4892
  this.mode = mode;
5416
4893
  this.usageTrackingDriver = options.usageTrackingDriver;
5417
4894
  this.sdkStore = options.sdkStore;
4895
+ this.requestResponseLogSink = options.requestResponseLogSink;
5418
4896
  this.providerManager = options.providerManager ?? new ProviderManager(providerRegistry, this.sdkStore, this.logger);
5419
4897
  }
5420
4898
  walletAdapter;
@@ -5422,7 +4900,6 @@ var RoutstrClient = class {
5422
4900
  providerRegistry;
5423
4901
  cashuSpender;
5424
4902
  balanceManager;
5425
- streamProcessor;
5426
4903
  providerManager;
5427
4904
  alertLevel;
5428
4905
  mode;
@@ -5430,6 +4907,7 @@ var RoutstrClient = class {
5430
4907
  usageTrackingDriver;
5431
4908
  sdkStore;
5432
4909
  logger;
4910
+ requestResponseLogSink;
5433
4911
  /**
5434
4912
  * Get the current client mode
5435
4913
  */
@@ -5620,6 +5098,7 @@ var RoutstrClient = class {
5620
5098
  let usagePromise = Promise.resolve({});
5621
5099
  if (contentType.includes("text/event-stream") && response.body) {
5622
5100
  const [clientStream, inspectStream] = response.body.tee();
5101
+ const requestResponseLogId = response.requestResponseLogId;
5623
5102
  processedResponse = new Response(clientStream, {
5624
5103
  status: response.status,
5625
5104
  statusText: response.statusText,
@@ -5627,6 +5106,7 @@ var RoutstrClient = class {
5627
5106
  });
5628
5107
  processedResponse.baseUrl = response.baseUrl;
5629
5108
  processedResponse.token = response.token;
5109
+ processedResponse.requestResponseLogId = requestResponseLogId;
5630
5110
  usagePromise = inspectSSEWebStream(
5631
5111
  inspectStream,
5632
5112
  (usage) => {
@@ -5636,8 +5116,23 @@ var RoutstrClient = class {
5636
5116
  (responseId) => {
5637
5117
  capturedResponseId = responseId;
5638
5118
  processedResponse.requestId = responseId;
5119
+ },
5120
+ {
5121
+ onRawChunk: (_chunk, sequence, text) => {
5122
+ void this.requestResponseLogSink?.logResponseChunk?.(
5123
+ requestResponseLogId,
5124
+ sequence,
5125
+ text
5126
+ );
5127
+ }
5639
5128
  }
5640
- );
5129
+ ).then(async (result) => {
5130
+ await this.requestResponseLogSink?.logResponseEnd?.(requestResponseLogId);
5131
+ return result;
5132
+ }).catch(async (error) => {
5133
+ await this.requestResponseLogSink?.logResponseError?.(requestResponseLogId, error);
5134
+ throw error;
5135
+ });
5641
5136
  processedResponse.usagePromise = usagePromise;
5642
5137
  }
5643
5138
  return {
@@ -5664,153 +5159,6 @@ var RoutstrClient = class {
5664
5159
  }
5665
5160
  return void 0;
5666
5161
  }
5667
- /**
5668
- * Fetch AI response with streaming
5669
- */
5670
- async fetchAIResponse(options, callbacks) {
5671
- const {
5672
- messageHistory,
5673
- selectedModel,
5674
- baseUrl,
5675
- mintUrl,
5676
- balance,
5677
- transactionHistory,
5678
- maxTokens,
5679
- headers
5680
- } = options;
5681
- const apiMessages = await this._convertMessages(messageHistory);
5682
- const requiredSats = this.providerManager.getRequiredSatsForModel(
5683
- selectedModel,
5684
- apiMessages,
5685
- maxTokens
5686
- );
5687
- try {
5688
- await this._checkBalance();
5689
- callbacks.onPaymentProcessing?.(true);
5690
- const spendResult = await this._spendToken({
5691
- mintUrl,
5692
- amount: requiredSats,
5693
- baseUrl
5694
- });
5695
- let token = spendResult.token;
5696
- let tokenBalance = spendResult.tokenBalance;
5697
- let tokenBalanceUnit = spendResult.tokenBalanceUnit;
5698
- let tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
5699
- let initialTokenBalanceUnknown = spendResult.tokenBalanceUnknown;
5700
- callbacks.onTokenCreated?.(this._getPendingCashuTokenAmount());
5701
- const baseHeaders = this._buildBaseHeaders(headers);
5702
- const requestHeaders = this._withAuthHeader(baseHeaders, token);
5703
- const providerInfo = await this.providerRegistry.getProviderInfo(baseUrl);
5704
- const providerVersion = providerInfo?.version ?? "";
5705
- let modelIdForRequest = selectedModel.id;
5706
- if (/^0\.1\./.test(providerVersion)) {
5707
- const newModel = await this.providerManager.getModelForProvider(
5708
- baseUrl,
5709
- selectedModel.id
5710
- );
5711
- modelIdForRequest = newModel?.id ?? selectedModel.id;
5712
- }
5713
- const body = {
5714
- model: modelIdForRequest,
5715
- messages: apiMessages,
5716
- stream: true
5717
- };
5718
- if (maxTokens !== void 0) {
5719
- body.max_tokens = maxTokens;
5720
- }
5721
- if (selectedModel?.name?.startsWith("OpenAI:")) {
5722
- body.tools = [{ type: "web_search" }];
5723
- }
5724
- const response = await this._makeRequest({
5725
- path: "/v1/chat/completions",
5726
- method: "POST",
5727
- body,
5728
- selectedModel,
5729
- baseUrl,
5730
- mintUrl,
5731
- token,
5732
- requiredSats,
5733
- maxTokens,
5734
- headers: requestHeaders,
5735
- baseHeaders
5736
- });
5737
- if (!response.body) {
5738
- throw new Error("Response body is not available");
5739
- }
5740
- if (response.status === 200) {
5741
- const baseUrlUsed = response.baseUrl || baseUrl;
5742
- const responseToken = response.token || token;
5743
- if (baseUrlUsed !== baseUrl || responseToken !== token) {
5744
- token = responseToken;
5745
- if (typeof response.initialTokenBalanceInSats === "number") {
5746
- tokenBalanceInSats = response.initialTokenBalanceInSats;
5747
- initialTokenBalanceUnknown = Boolean(
5748
- response.initialTokenBalanceUnknown
5749
- );
5750
- } else {
5751
- initialTokenBalanceUnknown = true;
5752
- }
5753
- }
5754
- const streamingResult = await this.streamProcessor.process(
5755
- response,
5756
- {
5757
- onContent: callbacks.onStreamingUpdate,
5758
- onThinking: callbacks.onThinkingUpdate
5759
- },
5760
- selectedModel.id
5761
- );
5762
- if (streamingResult.finish_reason === "content_filter") {
5763
- callbacks.onMessageAppend({
5764
- role: "assistant",
5765
- content: "Your request was denied due to content filtering."
5766
- });
5767
- } else if (streamingResult.content || streamingResult.images && streamingResult.images.length > 0) {
5768
- const message = await this._createAssistantMessage(streamingResult);
5769
- callbacks.onMessageAppend(message);
5770
- } else {
5771
- callbacks.onMessageAppend({
5772
- role: "system",
5773
- content: "The provider did not respond to this request."
5774
- });
5775
- }
5776
- callbacks.onStreamingUpdate("");
5777
- callbacks.onThinkingUpdate("");
5778
- const isApikeysEstimate = this.mode === "apikeys";
5779
- let satsSpent = await this._handlePostResponseBalanceUpdate({
5780
- token,
5781
- baseUrl: baseUrlUsed,
5782
- mintUrl,
5783
- initialTokenBalance: tokenBalanceInSats,
5784
- initialTokenBalanceUnknown,
5785
- fallbackSatsSpent: isApikeysEstimate ? this._getEstimatedCosts(selectedModel, streamingResult) : void 0,
5786
- response,
5787
- modelId: selectedModel.id,
5788
- usage: streamingResult.usage ? {
5789
- promptTokens: Number(streamingResult.usage.prompt_tokens ?? 0),
5790
- completionTokens: Number(
5791
- streamingResult.usage.completion_tokens ?? 0
5792
- ),
5793
- totalTokens: Number(streamingResult.usage.total_tokens ?? 0),
5794
- cost: Number(streamingResult.usage.cost ?? 0),
5795
- satsCost: Number(streamingResult.usage.sats_cost ?? 0)
5796
- } : void 0,
5797
- requestId: streamingResult.responseId
5798
- });
5799
- const estimatedCosts = this._getEstimatedCosts(
5800
- selectedModel,
5801
- streamingResult
5802
- );
5803
- const onLastMessageSatsUpdate = callbacks.onLastMessageSatsUpdate;
5804
- onLastMessageSatsUpdate?.(satsSpent, estimatedCosts);
5805
- } else {
5806
- throw new Error(`${response.status} ${response.statusText}`);
5807
- }
5808
- } catch (error) {
5809
- this._handleError(error, callbacks);
5810
- } finally {
5811
- callbacks.onPaymentProcessing?.(false);
5812
- }
5813
- }
5814
5162
  /**
5815
5163
  * Make the API request with failover support
5816
5164
  */
@@ -5818,16 +5166,30 @@ var RoutstrClient = class {
5818
5166
  const { path, method, body, baseUrl, token, headers } = params;
5819
5167
  try {
5820
5168
  const url = `${baseUrl.replace(/\/$/, "")}${path}`;
5169
+ const requestBodyText = body === void 0 || method === "GET" ? void 0 : JSON.stringify(body);
5170
+ const requestLogId = await this.requestResponseLogSink?.logRequest?.({
5171
+ method,
5172
+ url,
5173
+ path,
5174
+ baseUrl,
5175
+ headers,
5176
+ body,
5177
+ rawBody: requestBodyText
5178
+ });
5821
5179
  if (this.mode === "xcashu") this._log("DEBUG", "HEADERS,", headers);
5822
5180
  const response = await fetch(url, {
5823
5181
  method,
5824
5182
  headers,
5825
- body: body === void 0 || method === "GET" ? void 0 : JSON.stringify(body)
5183
+ body: requestBodyText
5826
5184
  });
5827
5185
  if (this.mode === "xcashu") this._log("DEBUG", "response,", response);
5828
5186
  response.baseUrl = baseUrl;
5829
5187
  response.token = token;
5188
+ response.requestResponseLogId = requestLogId;
5189
+ await this.requestResponseLogSink?.logResponseStart?.(requestLogId, response);
5190
+ const contentType = response.headers.get("content-type") || "";
5830
5191
  if (!response.ok) {
5192
+ void this.requestResponseLogSink?.logResponseBody?.(requestLogId, response.clone());
5831
5193
  const requestId = response.headers.get("x-routstr-request-id") || void 0;
5832
5194
  let bodyText;
5833
5195
  try {
@@ -5845,6 +5207,9 @@ var RoutstrClient = class {
5845
5207
  params.retryCount ?? 0
5846
5208
  );
5847
5209
  }
5210
+ if (!contentType.includes("text/event-stream")) {
5211
+ void this.requestResponseLogSink?.logResponseBody?.(requestLogId, response.clone());
5212
+ }
5848
5213
  return response;
5849
5214
  } catch (error) {
5850
5215
  if (isNetworkErrorMessage(error?.message || "")) {
@@ -6335,263 +5700,519 @@ var RoutstrClient = class {
6335
5700
  }
6336
5701
  }
6337
5702
  /**
6338
- * Convert messages for API format
5703
+ * Check wallet balance and throw if insufficient
6339
5704
  */
6340
- async _convertMessages(messages) {
6341
- return Promise.all(
6342
- messages.filter((m) => m.role !== "system").map(async (m) => ({
6343
- role: m.role,
6344
- content: typeof m.content === "string" ? m.content : m.content
6345
- }))
6346
- );
5705
+ async _checkBalance() {
5706
+ const balances = await this.walletAdapter.getBalances();
5707
+ const totalBalance = Object.values(balances).reduce((sum, v) => sum + v, 0);
5708
+ if (totalBalance <= 0) {
5709
+ throw new InsufficientBalanceError(1, 0);
5710
+ }
6347
5711
  }
6348
5712
  /**
6349
- * Create assistant message from streaming result
5713
+ * Spend a token using CashuSpender with standardized error handling
6350
5714
  */
6351
- async _createAssistantMessage(result) {
6352
- if (result.images && result.images.length > 0) {
6353
- const content = [];
6354
- if (result.content) {
6355
- content.push({
6356
- type: "text",
6357
- text: result.content,
6358
- thinking: result.thinking,
6359
- citations: result.citations,
6360
- annotations: result.annotations
6361
- });
6362
- }
6363
- for (const img of result.images) {
6364
- content.push({
6365
- type: "image_url",
6366
- image_url: {
6367
- url: img.image_url.url
6368
- }
5715
+ async _spendToken(params) {
5716
+ const { mintUrl, amount, baseUrl } = params;
5717
+ this._log(
5718
+ "DEBUG",
5719
+ `[RoutstrClient] _spendToken: mode=${this.mode}, amount=${amount}, baseUrl=${baseUrl}, mintUrl=${mintUrl}`
5720
+ );
5721
+ if (this.mode === "apikeys") {
5722
+ let parentApiKey = this.storageAdapter.getApiKey(baseUrl);
5723
+ if (!parentApiKey) {
5724
+ this._log(
5725
+ "DEBUG",
5726
+ `[RoutstrClient] _spendToken: No existing API key for ${baseUrl}, creating new one via Cashu`
5727
+ );
5728
+ const spendResult2 = await this.cashuSpender.spend({
5729
+ mintUrl,
5730
+ amount: amount * TOPUP_MARGIN,
5731
+ baseUrl: "",
5732
+ reuseToken: false
6369
5733
  });
6370
- }
6371
- return {
6372
- role: "assistant",
6373
- content
6374
- };
5734
+ if (!spendResult2.token) {
5735
+ this._log(
5736
+ "ERROR",
5737
+ `[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error:`,
5738
+ spendResult2.error
5739
+ );
5740
+ throw new Error(
5741
+ `[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error: ${spendResult2.error}`
5742
+ );
5743
+ } else {
5744
+ this._log(
5745
+ "DEBUG",
5746
+ `[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult2.token}`
5747
+ );
5748
+ }
5749
+ this._log(
5750
+ "DEBUG",
5751
+ `[RoutstrClient] _spendToken: Created API key for ${baseUrl}, key preview: ${spendResult2.token}, balance: ${spendResult2.balance}`
5752
+ );
5753
+ try {
5754
+ this.storageAdapter.setApiKey(baseUrl, spendResult2.token);
5755
+ } catch (error) {
5756
+ if (error instanceof Error && error.message.includes("ApiKey already exists")) {
5757
+ const receiveResult = await this.cashuSpender.receiveToken(
5758
+ spendResult2.token
5759
+ );
5760
+ if (receiveResult.success) {
5761
+ this._log(
5762
+ "DEBUG",
5763
+ `[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${receiveResult.amount}`
5764
+ );
5765
+ } else {
5766
+ this._log(
5767
+ "DEBUG",
5768
+ `[RoutstrClient] _handleErrorResponse: Token restore failed: ${receiveResult.message}`
5769
+ );
5770
+ }
5771
+ this._log(
5772
+ "DEBUG",
5773
+ `[RoutstrClient] _spendToken: API key already exists for ${baseUrl}, using existing key`
5774
+ );
5775
+ } else {
5776
+ throw error;
5777
+ }
5778
+ }
5779
+ parentApiKey = this.storageAdapter.getApiKey(baseUrl);
5780
+ } else {
5781
+ this._log(
5782
+ "DEBUG",
5783
+ `[RoutstrClient] _spendToken: Using existing API key for ${baseUrl}, key preview: ${parentApiKey.key}`
5784
+ );
5785
+ }
5786
+ let tokenBalance = 0;
5787
+ let tokenBalanceUnit = "sat";
5788
+ let tokenBalanceUnknown = false;
5789
+ const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
5790
+ const distributionForBaseUrl = apiKeyDistribution.find(
5791
+ (d) => d.baseUrl === baseUrl
5792
+ );
5793
+ if (distributionForBaseUrl) {
5794
+ tokenBalance = distributionForBaseUrl.amount;
5795
+ }
5796
+ if (tokenBalance === 0 && parentApiKey) {
5797
+ try {
5798
+ const balanceInfo = await this.balanceManager.getTokenBalance(
5799
+ parentApiKey.key,
5800
+ baseUrl
5801
+ );
5802
+ tokenBalance = balanceInfo.amount;
5803
+ tokenBalanceUnit = balanceInfo.unit;
5804
+ tokenBalanceUnknown = Boolean(balanceInfo.balanceUnknown);
5805
+ } catch (e) {
5806
+ this._log("WARN", "Could not get initial API key balance:", e);
5807
+ }
5808
+ }
5809
+ this._log(
5810
+ "DEBUG",
5811
+ `[RoutstrClient] _spendToken: Returning token with balance=${tokenBalance} ${tokenBalanceUnit}`
5812
+ );
5813
+ return {
5814
+ token: parentApiKey?.key ?? "",
5815
+ tokenBalance,
5816
+ tokenBalanceUnit,
5817
+ tokenBalanceUnknown
5818
+ };
5819
+ }
5820
+ this._log(
5821
+ "DEBUG",
5822
+ `[RoutstrClient] _spendToken: Calling CashuSpender.spend for amount=${amount}, mintUrl=${mintUrl}, mode=${this.mode}`
5823
+ );
5824
+ const spendResult = await this.cashuSpender.spend({
5825
+ mintUrl,
5826
+ amount,
5827
+ baseUrl: "",
5828
+ reuseToken: false
5829
+ });
5830
+ if (!spendResult.token) {
5831
+ this._log(
5832
+ "ERROR",
5833
+ `[RoutstrClient] _spendToken: CashuSpender.spend failed, error:`,
5834
+ spendResult.error
5835
+ );
5836
+ } else {
5837
+ this._log(
5838
+ "DEBUG",
5839
+ `[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult.token}, balance: ${spendResult.balance} ${spendResult.unit ?? "sat"}`
5840
+ );
5841
+ this.storageAdapter.addXcashuToken(baseUrl, spendResult.token);
5842
+ }
5843
+ return {
5844
+ token: spendResult.token,
5845
+ tokenBalance: spendResult.balance,
5846
+ tokenBalanceUnit: spendResult.unit ?? "sat",
5847
+ tokenBalanceUnknown: false
5848
+ };
5849
+ }
5850
+ /**
5851
+ * Build request headers with common defaults and dev mock controls
5852
+ */
5853
+ _buildBaseHeaders(additionalHeaders = {}, token) {
5854
+ const headers = {
5855
+ ...additionalHeaders,
5856
+ "Content-Type": "application/json"
5857
+ };
5858
+ return headers;
5859
+ }
5860
+ /**
5861
+ * Attach auth headers using the active client mode
5862
+ */
5863
+ _withAuthHeader(headers, token) {
5864
+ const nextHeaders = { ...headers };
5865
+ if (this.mode === "xcashu") {
5866
+ nextHeaders["X-Cashu"] = token;
5867
+ } else {
5868
+ nextHeaders["Authorization"] = `Bearer ${token}`;
5869
+ }
5870
+ return nextHeaders;
5871
+ }
5872
+ };
5873
+
5874
+ // client/StreamProcessor.ts
5875
+ var StreamProcessor = class {
5876
+ accumulatedContent = "";
5877
+ accumulatedThinking = "";
5878
+ accumulatedImages = [];
5879
+ isInThinking = false;
5880
+ isInContent = false;
5881
+ /**
5882
+ * Process a streaming response
5883
+ */
5884
+ async process(response, callbacks, modelId) {
5885
+ if (!response.body) {
5886
+ throw new Error("Response body is not available");
5887
+ }
5888
+ const reader = response.body.getReader();
5889
+ const decoder = new TextDecoder("utf-8");
5890
+ let buffer = "";
5891
+ this.accumulatedContent = "";
5892
+ this.accumulatedThinking = "";
5893
+ this.accumulatedImages = [];
5894
+ this.isInThinking = false;
5895
+ this.isInContent = false;
5896
+ let usage;
5897
+ let model;
5898
+ let finish_reason;
5899
+ let citations;
5900
+ let annotations;
5901
+ let responseId;
5902
+ try {
5903
+ while (true) {
5904
+ const { done, value } = await reader.read();
5905
+ if (done) {
5906
+ break;
5907
+ }
5908
+ const chunk = decoder.decode(value, { stream: true });
5909
+ buffer += chunk;
5910
+ const lines = buffer.split("\n");
5911
+ buffer = lines.pop() || "";
5912
+ for (const line of lines) {
5913
+ const parsed = this._parseLine(line);
5914
+ if (!parsed) continue;
5915
+ if (parsed.content) {
5916
+ this._handleContent(parsed.content, callbacks, modelId);
5917
+ }
5918
+ if (parsed.reasoning) {
5919
+ this._handleThinking(parsed.reasoning, callbacks);
5920
+ }
5921
+ if (parsed.usage) {
5922
+ usage = parsed.usage;
5923
+ }
5924
+ if (parsed.model) {
5925
+ model = parsed.model;
5926
+ }
5927
+ if (parsed.finish_reason) {
5928
+ finish_reason = parsed.finish_reason;
5929
+ }
5930
+ if (parsed.responseId) {
5931
+ responseId = parsed.responseId;
5932
+ }
5933
+ if (parsed.citations) {
5934
+ citations = parsed.citations;
5935
+ }
5936
+ if (parsed.annotations) {
5937
+ annotations = parsed.annotations;
5938
+ }
5939
+ if (parsed.images) {
5940
+ this._mergeImages(parsed.images);
5941
+ }
5942
+ }
5943
+ }
5944
+ } finally {
5945
+ reader.releaseLock();
5946
+ }
5947
+ return {
5948
+ content: this.accumulatedContent,
5949
+ thinking: this.accumulatedThinking || void 0,
5950
+ images: this.accumulatedImages.length > 0 ? this.accumulatedImages : void 0,
5951
+ usage,
5952
+ model,
5953
+ responseId,
5954
+ finish_reason,
5955
+ citations,
5956
+ annotations
5957
+ };
5958
+ }
5959
+ /**
5960
+ * Parse a single SSE line
5961
+ */
5962
+ _parseLine(line) {
5963
+ if (!line.trim()) return null;
5964
+ if (!line.startsWith("data: ")) {
5965
+ return null;
5966
+ }
5967
+ const jsonData = line.slice(6);
5968
+ if (jsonData === "[DONE]") {
5969
+ return null;
5970
+ }
5971
+ try {
5972
+ const parsed = JSON.parse(jsonData);
5973
+ const result = {};
5974
+ if (parsed.choices?.[0]?.delta?.content) {
5975
+ result.content = parsed.choices[0].delta.content;
5976
+ }
5977
+ if (parsed.choices?.[0]?.delta?.reasoning) {
5978
+ result.reasoning = parsed.choices[0].delta.reasoning;
5979
+ }
5980
+ const extractedUsage = extractUsageFromSSEJson(parsed);
5981
+ if (extractedUsage) {
5982
+ result.usage = toUsageStats(extractedUsage);
5983
+ } else if (parsed.usage) {
5984
+ result.usage = {
5985
+ total_tokens: parsed.usage.total_tokens ?? parsed.usage.input_tokens + parsed.usage.output_tokens,
5986
+ prompt_tokens: parsed.usage.prompt_tokens ?? parsed.usage.input_tokens,
5987
+ completion_tokens: parsed.usage.completion_tokens ?? parsed.usage.output_tokens
5988
+ };
5989
+ }
5990
+ if (parsed.id) {
5991
+ result.responseId = parsed.id;
5992
+ }
5993
+ if (parsed.model) {
5994
+ result.model = parsed.model;
5995
+ }
5996
+ if (parsed.citations) {
5997
+ result.citations = parsed.citations;
5998
+ }
5999
+ if (parsed.annotations) {
6000
+ result.annotations = parsed.annotations;
6001
+ }
6002
+ if (parsed.choices?.[0]?.finish_reason) {
6003
+ result.finish_reason = parsed.choices[0].finish_reason;
6004
+ }
6005
+ const images = parsed.choices?.[0]?.message?.images || parsed.choices?.[0]?.delta?.images;
6006
+ if (images && Array.isArray(images)) {
6007
+ result.images = images;
6008
+ }
6009
+ return result;
6010
+ } catch {
6011
+ return null;
6012
+ }
6013
+ }
6014
+ /**
6015
+ * Handle content delta with thinking support
6016
+ */
6017
+ _handleContent(content, callbacks, modelId) {
6018
+ if (this.isInThinking && !this.isInContent) {
6019
+ this.accumulatedThinking += "</thinking>";
6020
+ callbacks.onThinking(this.accumulatedThinking);
6021
+ this.isInThinking = false;
6022
+ this.isInContent = true;
6023
+ }
6024
+ if (modelId) {
6025
+ this._extractThinkingFromContent(content, callbacks);
6026
+ } else {
6027
+ this.accumulatedContent += content;
6028
+ }
6029
+ callbacks.onContent(this.accumulatedContent);
6030
+ }
6031
+ /**
6032
+ * Handle thinking/reasoning content
6033
+ */
6034
+ _handleThinking(reasoning, callbacks) {
6035
+ if (!this.isInThinking) {
6036
+ this.accumulatedThinking += "<thinking> ";
6037
+ this.isInThinking = true;
6375
6038
  }
6376
- return {
6377
- role: "assistant",
6378
- content: result.content || ""
6379
- };
6039
+ this.accumulatedThinking += reasoning;
6040
+ callbacks.onThinking(this.accumulatedThinking);
6380
6041
  }
6381
6042
  /**
6382
- * Calculate estimated costs from usage
6043
+ * Extract thinking blocks from content (for models with inline thinking)
6383
6044
  */
6384
- _getEstimatedCosts(selectedModel, streamingResult) {
6385
- let estimatedCosts = 0;
6386
- if (streamingResult.usage) {
6387
- const { completion_tokens, prompt_tokens } = streamingResult.usage;
6388
- if (completion_tokens !== void 0 && prompt_tokens !== void 0) {
6389
- estimatedCosts = (selectedModel.sats_pricing?.completion ?? 0) * completion_tokens + (selectedModel.sats_pricing?.prompt ?? 0) * prompt_tokens;
6045
+ _extractThinkingFromContent(content, callbacks) {
6046
+ const parts = content.split(/(<thinking>|<\/thinking>)/);
6047
+ for (const part of parts) {
6048
+ if (part === "<thinking>") {
6049
+ this.isInThinking = true;
6050
+ if (!this.accumulatedThinking.includes("<thinking>")) {
6051
+ this.accumulatedThinking += "<thinking> ";
6052
+ }
6053
+ } else if (part === "</thinking>") {
6054
+ this.isInThinking = false;
6055
+ this.accumulatedThinking += "</thinking>";
6056
+ } else if (this.isInThinking) {
6057
+ this.accumulatedThinking += part;
6058
+ } else {
6059
+ this.accumulatedContent += part;
6390
6060
  }
6391
6061
  }
6392
- return estimatedCosts;
6393
6062
  }
6394
6063
  /**
6395
- * Get pending API key amount
6064
+ * Merge images into accumulated array, avoiding duplicates
6396
6065
  */
6397
- _getPendingCashuTokenAmount() {
6398
- const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
6399
- return apiKeyDistribution.reduce((total, item) => total + item.amount, 0);
6066
+ _mergeImages(newImages) {
6067
+ for (const img of newImages) {
6068
+ const newUrl = img.image_url?.url;
6069
+ const existingIndex = this.accumulatedImages.findIndex((existing) => {
6070
+ const existingUrl = existing.image_url?.url;
6071
+ if (newUrl && existingUrl) {
6072
+ return existingUrl === newUrl;
6073
+ }
6074
+ if (img.index !== void 0 && existing.index !== void 0) {
6075
+ return existing.index === img.index;
6076
+ }
6077
+ return false;
6078
+ });
6079
+ if (existingIndex === -1) {
6080
+ this.accumulatedImages.push(img);
6081
+ } else {
6082
+ this.accumulatedImages[existingIndex] = img;
6083
+ }
6084
+ }
6400
6085
  }
6401
- /**
6402
- * Handle errors and notify callbacks
6403
- */
6404
- _handleError(error, callbacks) {
6405
- this._log("ERROR", "[RoutstrClient] _handleError: Error occurred", error);
6406
- if (error instanceof Error) {
6407
- const isStreamError = error.message.includes("Error in input stream") || error.message.includes("Load failed");
6408
- const modifiedErrorMsg = isStreamError ? "AI stream was cut off, turn on Keep Active or please try again" : error.message;
6409
- this._log(
6410
- "ERROR",
6411
- `[RoutstrClient] _handleError: Error type=${error.constructor.name}, message=${modifiedErrorMsg}, isStreamError=${isStreamError}`
6412
- );
6086
+ };
6087
+
6088
+ // client/fetchAIResponse.ts
6089
+ async function fetchAIResponse(options, callbacks, deps) {
6090
+ const {
6091
+ messageHistory,
6092
+ selectedModel,
6093
+ baseUrl,
6094
+ mintUrl,
6095
+ maxTokens,
6096
+ headers
6097
+ } = options;
6098
+ try {
6099
+ const apiMessages = await convertMessages(messageHistory);
6100
+ callbacks.onPaymentProcessing?.(true);
6101
+ callbacks.onTokenCreated?.(deps.getPendingCashuTokenAmount?.() ?? 0);
6102
+ const body = {
6103
+ model: selectedModel.id,
6104
+ messages: apiMessages,
6105
+ stream: true
6106
+ };
6107
+ if (maxTokens !== void 0) {
6108
+ body.max_tokens = maxTokens;
6109
+ }
6110
+ if (selectedModel?.name?.startsWith("OpenAI:")) {
6111
+ body.tools = [{ type: "web_search" }];
6112
+ }
6113
+ const response = await deps.client.routeRequest({
6114
+ path: "/v1/chat/completions",
6115
+ method: "POST",
6116
+ body,
6117
+ headers,
6118
+ baseUrl,
6119
+ mintUrl,
6120
+ modelId: selectedModel.id
6121
+ });
6122
+ if (!response.body) {
6123
+ throw new Error("Response body is not available");
6124
+ }
6125
+ if (response.status !== 200) {
6126
+ throw new Error(`${response.status} ${response.statusText}`);
6127
+ }
6128
+ const streamProcessor = new StreamProcessor();
6129
+ const streamingResult = await streamProcessor.process(
6130
+ response,
6131
+ {
6132
+ onContent: callbacks.onStreamingUpdate,
6133
+ onThinking: callbacks.onThinkingUpdate
6134
+ },
6135
+ selectedModel.id
6136
+ );
6137
+ if (streamingResult.finish_reason === "content_filter") {
6413
6138
  callbacks.onMessageAppend({
6414
- role: "system",
6415
- content: "Uncaught Error: " + modifiedErrorMsg + (this.alertLevel === "max" ? " | " + error.stack : "")
6139
+ role: "assistant",
6140
+ content: "Your request was denied due to content filtering."
6416
6141
  });
6142
+ } else if (streamingResult.content || streamingResult.images && streamingResult.images.length > 0) {
6143
+ const message = await createAssistantMessage(streamingResult);
6144
+ callbacks.onMessageAppend(message);
6417
6145
  } else {
6418
6146
  callbacks.onMessageAppend({
6419
6147
  role: "system",
6420
- content: "Unknown Error: Please tag Routstr on Nostr and/or retry."
6148
+ content: "The provider did not respond to this request."
6421
6149
  });
6422
6150
  }
6151
+ callbacks.onStreamingUpdate("");
6152
+ callbacks.onThinkingUpdate("");
6153
+ } catch (error) {
6154
+ handleError(error, callbacks, deps.alertLevel, deps.logger);
6155
+ } finally {
6156
+ callbacks.onPaymentProcessing?.(false);
6423
6157
  }
6424
- /**
6425
- * Check wallet balance and throw if insufficient
6426
- */
6427
- async _checkBalance() {
6428
- const balances = await this.walletAdapter.getBalances();
6429
- const totalBalance = Object.values(balances).reduce((sum, v) => sum + v, 0);
6430
- if (totalBalance <= 0) {
6431
- throw new InsufficientBalanceError(1, 0);
6158
+ }
6159
+ async function convertMessages(messages) {
6160
+ return Promise.all(
6161
+ messages.filter((m) => m.role !== "system").map(async (m) => ({
6162
+ role: m.role,
6163
+ content: typeof m.content === "string" ? m.content : m.content
6164
+ }))
6165
+ );
6166
+ }
6167
+ async function createAssistantMessage(result) {
6168
+ if (result.images && result.images.length > 0) {
6169
+ const content = [];
6170
+ if (result.content) {
6171
+ content.push({
6172
+ type: "text",
6173
+ text: result.content,
6174
+ thinking: result.thinking,
6175
+ citations: result.citations,
6176
+ annotations: result.annotations
6177
+ });
6432
6178
  }
6433
- }
6434
- /**
6435
- * Spend a token using CashuSpender with standardized error handling
6436
- */
6437
- async _spendToken(params) {
6438
- const { mintUrl, amount, baseUrl } = params;
6439
- this._log(
6440
- "DEBUG",
6441
- `[RoutstrClient] _spendToken: mode=${this.mode}, amount=${amount}, baseUrl=${baseUrl}, mintUrl=${mintUrl}`
6442
- );
6443
- if (this.mode === "apikeys") {
6444
- let parentApiKey = this.storageAdapter.getApiKey(baseUrl);
6445
- if (!parentApiKey) {
6446
- this._log(
6447
- "DEBUG",
6448
- `[RoutstrClient] _spendToken: No existing API key for ${baseUrl}, creating new one via Cashu`
6449
- );
6450
- const spendResult2 = await this.cashuSpender.spend({
6451
- mintUrl,
6452
- amount: amount * TOPUP_MARGIN,
6453
- baseUrl: "",
6454
- reuseToken: false
6455
- });
6456
- if (!spendResult2.token) {
6457
- this._log(
6458
- "ERROR",
6459
- `[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error:`,
6460
- spendResult2.error
6461
- );
6462
- throw new Error(
6463
- `[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error: ${spendResult2.error}`
6464
- );
6465
- } else {
6466
- this._log(
6467
- "DEBUG",
6468
- `[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult2.token}`
6469
- );
6470
- }
6471
- this._log(
6472
- "DEBUG",
6473
- `[RoutstrClient] _spendToken: Created API key for ${baseUrl}, key preview: ${spendResult2.token}, balance: ${spendResult2.balance}`
6474
- );
6475
- try {
6476
- this.storageAdapter.setApiKey(baseUrl, spendResult2.token);
6477
- } catch (error) {
6478
- if (error instanceof Error && error.message.includes("ApiKey already exists")) {
6479
- const receiveResult = await this.cashuSpender.receiveToken(
6480
- spendResult2.token
6481
- );
6482
- if (receiveResult.success) {
6483
- this._log(
6484
- "DEBUG",
6485
- `[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${receiveResult.amount}`
6486
- );
6487
- } else {
6488
- this._log(
6489
- "DEBUG",
6490
- `[RoutstrClient] _handleErrorResponse: Token restore failed: ${receiveResult.message}`
6491
- );
6492
- }
6493
- this._log(
6494
- "DEBUG",
6495
- `[RoutstrClient] _spendToken: API key already exists for ${baseUrl}, using existing key`
6496
- );
6497
- } else {
6498
- throw error;
6499
- }
6500
- }
6501
- parentApiKey = this.storageAdapter.getApiKey(baseUrl);
6502
- } else {
6503
- this._log(
6504
- "DEBUG",
6505
- `[RoutstrClient] _spendToken: Using existing API key for ${baseUrl}, key preview: ${parentApiKey.key}`
6506
- );
6507
- }
6508
- let tokenBalance = 0;
6509
- let tokenBalanceUnit = "sat";
6510
- let tokenBalanceUnknown = false;
6511
- const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
6512
- const distributionForBaseUrl = apiKeyDistribution.find(
6513
- (d) => d.baseUrl === baseUrl
6514
- );
6515
- if (distributionForBaseUrl) {
6516
- tokenBalance = distributionForBaseUrl.amount;
6517
- }
6518
- if (tokenBalance === 0 && parentApiKey) {
6519
- try {
6520
- const balanceInfo = await this.balanceManager.getTokenBalance(
6521
- parentApiKey.key,
6522
- baseUrl
6523
- );
6524
- tokenBalance = balanceInfo.amount;
6525
- tokenBalanceUnit = balanceInfo.unit;
6526
- tokenBalanceUnknown = Boolean(balanceInfo.balanceUnknown);
6527
- } catch (e) {
6528
- this._log("WARN", "Could not get initial API key balance:", e);
6179
+ for (const img of result.images) {
6180
+ content.push({
6181
+ type: "image_url",
6182
+ image_url: {
6183
+ url: img.image_url.url
6529
6184
  }
6530
- }
6531
- this._log(
6532
- "DEBUG",
6533
- `[RoutstrClient] _spendToken: Returning token with balance=${tokenBalance} ${tokenBalanceUnit}`
6534
- );
6535
- return {
6536
- token: parentApiKey?.key ?? "",
6537
- tokenBalance,
6538
- tokenBalanceUnit,
6539
- tokenBalanceUnknown
6540
- };
6541
- }
6542
- this._log(
6543
- "DEBUG",
6544
- `[RoutstrClient] _spendToken: Calling CashuSpender.spend for amount=${amount}, mintUrl=${mintUrl}, mode=${this.mode}`
6545
- );
6546
- const spendResult = await this.cashuSpender.spend({
6547
- mintUrl,
6548
- amount,
6549
- baseUrl: "",
6550
- reuseToken: false
6551
- });
6552
- if (!spendResult.token) {
6553
- this._log(
6554
- "ERROR",
6555
- `[RoutstrClient] _spendToken: CashuSpender.spend failed, error:`,
6556
- spendResult.error
6557
- );
6558
- } else {
6559
- this._log(
6560
- "DEBUG",
6561
- `[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult.token}, balance: ${spendResult.balance} ${spendResult.unit ?? "sat"}`
6562
- );
6563
- this.storageAdapter.addXcashuToken(baseUrl, spendResult.token);
6185
+ });
6564
6186
  }
6565
6187
  return {
6566
- token: spendResult.token,
6567
- tokenBalance: spendResult.balance,
6568
- tokenBalanceUnit: spendResult.unit ?? "sat",
6569
- tokenBalanceUnknown: false
6570
- };
6571
- }
6572
- /**
6573
- * Build request headers with common defaults and dev mock controls
6574
- */
6575
- _buildBaseHeaders(additionalHeaders = {}, token) {
6576
- const headers = {
6577
- ...additionalHeaders,
6578
- "Content-Type": "application/json"
6188
+ role: "assistant",
6189
+ content
6579
6190
  };
6580
- return headers;
6581
6191
  }
6582
- /**
6583
- * Attach auth headers using the active client mode
6584
- */
6585
- _withAuthHeader(headers, token) {
6586
- const nextHeaders = { ...headers };
6587
- if (this.mode === "xcashu") {
6588
- nextHeaders["X-Cashu"] = token;
6589
- } else {
6590
- nextHeaders["Authorization"] = `Bearer ${token}`;
6591
- }
6592
- return nextHeaders;
6192
+ return {
6193
+ role: "assistant",
6194
+ content: result.content || ""
6195
+ };
6196
+ }
6197
+ function handleError(error, callbacks, alertLevel, logger) {
6198
+ logger.error("[fetchAIResponse] Error occurred", error);
6199
+ if (error instanceof Error) {
6200
+ const isStreamError = error.message.includes("Error in input stream") || error.message.includes("Load failed");
6201
+ const modifiedErrorMsg = isStreamError ? "AI stream was cut off, turn on Keep Active or please try again" : error.message;
6202
+ logger.error(
6203
+ `[fetchAIResponse] Error type=${error.constructor.name}, message=${modifiedErrorMsg}, isStreamError=${isStreamError}`
6204
+ );
6205
+ callbacks.onMessageAppend({
6206
+ role: "system",
6207
+ content: "Uncaught Error: " + modifiedErrorMsg + (alertLevel === "max" ? " | " + error.stack : "")
6208
+ });
6209
+ } else {
6210
+ callbacks.onMessageAppend({
6211
+ role: "system",
6212
+ content: "Unknown Error: Please tag Routstr on Nostr and/or retry."
6213
+ });
6593
6214
  }
6594
- };
6215
+ }
6595
6216
 
6596
6217
  // routeRequests.ts
6597
6218
  async function resolveRouteRequestContext(options) {
@@ -6614,7 +6235,8 @@ async function resolveRouteRequestContext(options) {
6614
6235
  usageTrackingDriver,
6615
6236
  sdkStore,
6616
6237
  providerManager: providedProviderManager,
6617
- logger
6238
+ logger,
6239
+ requestResponseLogSink
6618
6240
  } = options;
6619
6241
  let modelManager;
6620
6242
  let providers;
@@ -6663,15 +6285,8 @@ async function resolveRouteRequestContext(options) {
6663
6285
  baseUrl = cheapest.baseUrl;
6664
6286
  selectedModel = cheapest.model;
6665
6287
  }
6666
- const balances = await walletAdapter.getBalances();
6667
- const totalBalance = Object.values(balances).reduce((sum, v) => sum + v, 0);
6668
- if (totalBalance <= 0) {
6669
- throw new Error(
6670
- "Wallet balance is empty. Add a mint and fund it before making requests."
6671
- );
6672
- }
6673
6288
  const providerMints = providerRegistry.getProviderMints(baseUrl);
6674
- const mintUrl = walletAdapter.getActiveMintUrl() || providerMints[0] || Object.keys(balances)[0];
6289
+ const mintUrl = walletAdapter.getActiveMintUrl() || providerMints[0] || Object.keys(await walletAdapter.getBalances())[0];
6675
6290
  if (!mintUrl) {
6676
6291
  throw new Error("No mint configured in wallet");
6677
6292
  }
@@ -6681,7 +6296,7 @@ async function resolveRouteRequestContext(options) {
6681
6296
  providerRegistry,
6682
6297
  "min",
6683
6298
  mode,
6684
- { usageTrackingDriver, sdkStore, providerManager, logger }
6299
+ { usageTrackingDriver, sdkStore, providerManager, logger, requestResponseLogSink }
6685
6300
  );
6686
6301
  const maxTokens = extractMaxTokens(requestBody);
6687
6302
  const stream = extractStream(requestBody);
@@ -6762,8 +6377,6 @@ exports.StreamProcessor = StreamProcessor;
6762
6377
  exports.StreamingError = StreamingError;
6763
6378
  exports.TokenOperationError = TokenOperationError;
6764
6379
  exports.consoleLogger = consoleLogger;
6765
- exports.createBunSqliteDriver = createBunSqliteDriver;
6766
- exports.createBunSqliteUsageTrackingDriver = createBunSqliteUsageTrackingDriver;
6767
6380
  exports.createDiscoveryAdapterFromStore = createDiscoveryAdapterFromStore;
6768
6381
  exports.createIndexedDBDriver = createIndexedDBDriver;
6769
6382
  exports.createIndexedDBUsageTrackingDriver = createIndexedDBUsageTrackingDriver;
@@ -6774,9 +6387,11 @@ exports.createProviderRegistryFromStore = createProviderRegistryFromStore;
6774
6387
  exports.createSSEParserTransform = createSSEParserTransform;
6775
6388
  exports.createSdkStore = createSdkStore;
6776
6389
  exports.createShardedDiscoveryAdapter = createShardedDiscoveryAdapter;
6777
- exports.createSqliteDriver = createSqliteDriver;
6778
- exports.createSqliteUsageTrackingDriver = createSqliteUsageTrackingDriver;
6779
6390
  exports.createStorageAdapterFromStore = createStorageAdapterFromStore;
6391
+ exports.extractResponseId = extractResponseId;
6392
+ exports.extractUsageFromResponseBody = extractUsageFromResponseBody;
6393
+ exports.extractUsageFromSSEJson = extractUsageFromSSEJson;
6394
+ exports.fetchAIResponse = fetchAIResponse;
6780
6395
  exports.filterBaseUrlsForTor = filterBaseUrlsForTor;
6781
6396
  exports.getDefaultDiscoveryAdapter = getDefaultDiscoveryAdapter;
6782
6397
  exports.getDefaultProviderRegistry = getDefaultProviderRegistry;
@@ -6793,5 +6408,6 @@ exports.noopLogger = noopLogger;
6793
6408
  exports.normalizeProviderUrl = normalizeProviderUrl;
6794
6409
  exports.routeRequests = routeRequests;
6795
6410
  exports.setDefaultUsageTrackingDriver = setDefaultUsageTrackingDriver;
6411
+ exports.toUsageStats = toUsageStats;
6796
6412
  //# sourceMappingURL=index.js.map
6797
6413
  //# sourceMappingURL=index.js.map