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