@routstr/sdk 0.3.8 → 0.3.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/browser.d.mts +12 -0
  2. package/dist/browser.d.ts +12 -0
  3. package/dist/browser.js +6278 -0
  4. package/dist/browser.js.map +1 -0
  5. package/dist/browser.mjs +6230 -0
  6. package/dist/browser.mjs.map +1 -0
  7. package/dist/bun.d.mts +29 -0
  8. package/dist/bun.d.ts +29 -0
  9. package/dist/bun.js +6586 -0
  10. package/dist/bun.js.map +1 -0
  11. package/dist/bun.mjs +6532 -0
  12. package/dist/bun.mjs.map +1 -0
  13. package/dist/bunSqlite-BMTseLIz.d.ts +18 -0
  14. package/dist/bunSqlite-D6AreVE2.d.mts +18 -0
  15. package/dist/client/index.d.mts +63 -41
  16. package/dist/client/index.d.ts +63 -41
  17. package/dist/client/index.js +1223 -1658
  18. package/dist/client/index.js.map +1 -1
  19. package/dist/client/index.mjs +1223 -1659
  20. package/dist/client/index.mjs.map +1 -1
  21. package/dist/discovery/index.d.mts +67 -3
  22. package/dist/discovery/index.d.ts +67 -3
  23. package/dist/discovery/index.js +242 -79
  24. package/dist/discovery/index.js.map +1 -1
  25. package/dist/discovery/index.mjs +242 -79
  26. package/dist/discovery/index.mjs.map +1 -1
  27. package/dist/index.d.mts +5 -4
  28. package/dist/index.d.ts +5 -4
  29. package/dist/index.js +1975 -2004
  30. package/dist/index.js.map +1 -1
  31. package/dist/index.mjs +1973 -2001
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/node.d.mts +22 -0
  34. package/dist/node.d.ts +22 -0
  35. package/dist/node.js +6651 -0
  36. package/dist/node.js.map +1 -0
  37. package/dist/node.mjs +6599 -0
  38. package/dist/node.mjs.map +1 -0
  39. package/dist/storage/bun.d.mts +16 -0
  40. package/dist/storage/bun.d.ts +16 -0
  41. package/dist/storage/bun.js +1801 -0
  42. package/dist/storage/bun.js.map +1 -0
  43. package/dist/storage/bun.mjs +1777 -0
  44. package/dist/storage/bun.mjs.map +1 -0
  45. package/dist/storage/index.d.mts +30 -30
  46. package/dist/storage/index.d.ts +30 -30
  47. package/dist/storage/index.js +393 -625
  48. package/dist/storage/index.js.map +1 -1
  49. package/dist/storage/index.mjs +392 -622
  50. package/dist/storage/index.mjs.map +1 -1
  51. package/dist/storage/node.d.mts +22 -0
  52. package/dist/storage/node.d.ts +22 -0
  53. package/dist/storage/node.js +1864 -0
  54. package/dist/storage/node.js.map +1 -0
  55. package/dist/storage/node.mjs +1842 -0
  56. package/dist/storage/node.mjs.map +1 -0
  57. package/dist/{store-C6dfj1cc.d.mts → store-BiuM2V9N.d.mts} +14 -0
  58. package/dist/{store-58VcEUoA.d.ts → store-C8MZlfuz.d.ts} +14 -0
  59. package/dist/wallet/index.d.mts +4 -0
  60. package/dist/wallet/index.d.ts +4 -0
  61. package/dist/wallet/index.js +11 -4
  62. package/dist/wallet/index.js.map +1 -1
  63. package/dist/wallet/index.mjs +11 -4
  64. package/dist/wallet/index.mjs.map +1 -1
  65. package/package.json +28 -2
package/dist/index.mjs CHANGED
@@ -145,7 +145,10 @@ var ModelManager = class _ModelManager {
145
145
  this.includeProviderUrls = config.includeProviderUrls || [];
146
146
  this.excludeProviderUrls = config.excludeProviderUrls || [];
147
147
  this.routstrPubkey = config.routstrPubkey || "4ad6fa2d16e2a9b576c863b4cf7404a70d4dc320c0c447d10ad6ff58993eacc8";
148
+ this.nostrRelays = config.nostrRelays;
148
149
  this.logger = (config.logger ?? consoleLogger).child("ModelManager");
150
+ this.eventStoreDbPath = config.eventStoreDbPath;
151
+ this.persistentEventDatabaseFactory = config.persistentEventDatabaseFactory;
149
152
  }
150
153
  adapter;
151
154
  cacheTTL;
@@ -153,8 +156,15 @@ var ModelManager = class _ModelManager {
153
156
  includeProviderUrls;
154
157
  excludeProviderUrls;
155
158
  routstrPubkey;
159
+ nostrRelays;
156
160
  logger;
157
161
  providerNodePubkeysByUrl = /* @__PURE__ */ new Map();
162
+ /** Persistent event store for relay-fetched events (null if not configured/initialized) */
163
+ eventStore = null;
164
+ eventStoreDb = null;
165
+ eventStoreInitPromise = null;
166
+ eventStoreDbPath;
167
+ persistentEventDatabaseFactory;
158
168
  /**
159
169
  * Get the list of bootstrapped provider base URLs
160
170
  * @returns Array of provider base URLs
@@ -162,6 +172,103 @@ var ModelManager = class _ModelManager {
162
172
  getBaseUrls() {
163
173
  return this.adapter.getBaseUrlsList();
164
174
  }
175
+ /**
176
+ * Lazily initialize the persistent event store.
177
+ * Returns null if no eventStoreDbPath was provided.
178
+ */
179
+ async ensureEventStore() {
180
+ if (!this.eventStoreDbPath) return null;
181
+ if (this.eventStore) return this.eventStore;
182
+ if (!this.eventStoreInitPromise) {
183
+ this.eventStoreInitPromise = (async () => {
184
+ try {
185
+ const db = await this.createPersistentEventDatabase();
186
+ this.eventStoreDb = db;
187
+ this.eventStore = new EventStore({ database: db });
188
+ this.initializeEventStoreMetadata();
189
+ this.logger.log(
190
+ `Persistent event store initialized at ${this.eventStoreDbPath}`
191
+ );
192
+ return this.eventStore;
193
+ } catch (error) {
194
+ this.eventStoreInitPromise = null;
195
+ throw new Error(
196
+ `Persistent Nostr event storage requires a runtime-specific database factory. Use @routstr/sdk/node, @routstr/sdk/bun, inject persistentEventDatabaseFactory, or omit eventStoreDbPath. (${error})`
197
+ );
198
+ }
199
+ })();
200
+ }
201
+ return this.eventStoreInitPromise;
202
+ }
203
+ /**
204
+ * Get the persistent event store, initializing it if configured.
205
+ * Returns null if no eventStoreDbPath was provided.
206
+ */
207
+ async getEventStore() {
208
+ return this.ensureEventStore();
209
+ }
210
+ async createPersistentEventDatabase() {
211
+ if (!this.eventStoreDbPath) {
212
+ throw new Error("eventStoreDbPath is required");
213
+ }
214
+ if (!this.persistentEventDatabaseFactory) {
215
+ throw new Error(
216
+ "persistentEventDatabaseFactory is required. Import ModelManager from @routstr/sdk/node or @routstr/sdk/bun for SQLite-backed persistent event storage."
217
+ );
218
+ }
219
+ return this.persistentEventDatabaseFactory(this.eventStoreDbPath);
220
+ }
221
+ /** Close the persistent event store database handle, if configured. */
222
+ closeEventStore() {
223
+ this.eventStoreDb?.close?.();
224
+ this.eventStore = null;
225
+ this.eventStoreDb = null;
226
+ this.eventStoreInitPromise = null;
227
+ }
228
+ initializeEventStoreMetadata() {
229
+ this.eventStoreDb?.db?.exec(
230
+ `CREATE TABLE IF NOT EXISTS routstr_event_cache_metadata (
231
+ event_id TEXT PRIMARY KEY,
232
+ fetched_at INTEGER NOT NULL
233
+ )`
234
+ );
235
+ }
236
+ markEventFetched(event, fetchedAt = Date.now()) {
237
+ const db = this.eventStoreDb?.db;
238
+ if (!db) return;
239
+ db.prepare(
240
+ `INSERT INTO routstr_event_cache_metadata (event_id, fetched_at)
241
+ VALUES (?, ?)
242
+ ON CONFLICT(event_id) DO UPDATE SET fetched_at = excluded.fetched_at`
243
+ ).run?.(event.id, fetchedAt);
244
+ }
245
+ getEventFetchedAt(event) {
246
+ const db = this.eventStoreDb?.db;
247
+ if (!db) return void 0;
248
+ const row = db.prepare(
249
+ `SELECT fetched_at FROM routstr_event_cache_metadata WHERE event_id = ?`
250
+ ).get?.(event.id);
251
+ return typeof row?.fetched_at === "number" ? row.fetched_at : void 0;
252
+ }
253
+ /**
254
+ * Check the persistent event store for fresh cached events.
255
+ * Returns events from SQLite if they were fetched within `maxAge`, otherwise
256
+ * returns empty array (caller should hit relays). Events without local fetch
257
+ * metadata fall back to Nostr created_at for backwards compatibility.
258
+ */
259
+ async getCachedNostrEvents(filter, maxAge, forceRefresh = false) {
260
+ const eventStore = await this.ensureEventStore();
261
+ if (forceRefresh) return [];
262
+ if (!eventStore) return [];
263
+ const timeline = eventStore.getTimeline(filter);
264
+ if (timeline.length === 0) return [];
265
+ const cutoff = Date.now() - maxAge;
266
+ const freshest = Math.max(
267
+ ...timeline.map((e) => this.getEventFetchedAt(e) ?? e.created_at * 1e3)
268
+ );
269
+ if (freshest < cutoff) return [];
270
+ return timeline;
271
+ }
165
272
  static async init(adapter, config = {}, options = {}) {
166
273
  const manager = new _ModelManager(adapter, config);
167
274
  const torMode = options.torMode ?? false;
@@ -190,19 +297,31 @@ var ModelManager = class _ModelManager {
190
297
  torMode
191
298
  );
192
299
  await this.fetchRoutstr21Models(forceRefresh);
193
- await this.syncReviewedProvidersFromNostr(filteredCachedUrls);
300
+ await this.syncReviewedProvidersFromNostr(
301
+ filteredCachedUrls,
302
+ this.providerNodePubkeysByUrl,
303
+ forceRefresh
304
+ );
194
305
  return filteredCachedUrls;
195
306
  }
196
307
  }
197
308
  }
198
309
  try {
199
- const nostrProviders = await this.bootstrapFromNostr(38421, torMode);
310
+ const nostrProviders = await this.bootstrapFromNostr(
311
+ 38421,
312
+ torMode,
313
+ forceRefresh
314
+ );
200
315
  if (nostrProviders.length > 0) {
201
316
  const filtered = this.filterBaseUrlsForTor(nostrProviders, torMode);
202
317
  this.adapter.setBaseUrlsList(filtered);
203
318
  this.adapter.setBaseUrlsLastUpdate(Date.now());
204
319
  await this.fetchRoutstr21Models(forceRefresh);
205
- await this.syncReviewedProvidersFromNostr(filtered);
320
+ await this.syncReviewedProvidersFromNostr(
321
+ filtered,
322
+ this.providerNodePubkeysByUrl,
323
+ forceRefresh
324
+ );
206
325
  return filtered;
207
326
  }
208
327
  } catch (e) {
@@ -211,42 +330,59 @@ var ModelManager = class _ModelManager {
211
330
  return this.bootstrapFromHttp(torMode, forceRefresh);
212
331
  }
213
332
  /**
214
- * Bootstrap providers from Nostr network (kind 30421)
333
+ * Resolve Nostr relay URLs for a given use case.
334
+ * Returns user-configured relays if set, otherwise the provided defaults.
335
+ */
336
+ getNostrRelays(defaults) {
337
+ return this.nostrRelays && this.nostrRelays.length > 0 ? this.nostrRelays : defaults;
338
+ }
339
+ /**
340
+ * Bootstrap providers from Nostr network (kind 38421)
215
341
  * @param kind The Nostr kind to fetch
216
342
  * @param torMode Whether running in Tor context
217
343
  * @returns Array of provider base URLs
218
344
  */
219
- async bootstrapFromNostr(kind, torMode) {
220
- const DEFAULT_RELAYS = [
345
+ async bootstrapFromNostr(kind, torMode, forceRefresh = false) {
346
+ const relays = this.getNostrRelays([
221
347
  "wss://relay.primal.net",
222
348
  "wss://nos.lol",
223
349
  "wss://relay.damus.io"
224
- ];
225
- const pool = new RelayPool();
226
- const localEventStore = new EventStore();
227
- const timeoutMs = 5e3;
228
- await new Promise((resolve) => {
229
- pool.req(DEFAULT_RELAYS, {
230
- kinds: [kind],
231
- limit: 100
232
- }).pipe(
233
- onlyEvents(),
234
- tap((event) => {
235
- localEventStore.add(event);
236
- })
237
- ).subscribe({
238
- complete: () => {
350
+ ]);
351
+ const cached = await this.getCachedNostrEvents(
352
+ { kinds: [kind] },
353
+ this.cacheTTL,
354
+ forceRefresh
355
+ );
356
+ let sessionEvents = cached;
357
+ if (cached.length === 0) {
358
+ const pool = new RelayPool();
359
+ const timeoutMs = 5e3;
360
+ await new Promise((resolve) => {
361
+ pool.req(relays, {
362
+ kinds: [kind],
363
+ limit: 100
364
+ }).pipe(
365
+ onlyEvents(),
366
+ tap((event) => {
367
+ sessionEvents.push(event);
368
+ this.eventStore?.add(event);
369
+ this.markEventFetched(event);
370
+ })
371
+ ).subscribe({
372
+ complete: () => {
373
+ resolve();
374
+ }
375
+ });
376
+ setTimeout(() => {
239
377
  resolve();
240
- }
378
+ }, timeoutMs);
241
379
  });
242
- setTimeout(() => {
243
- resolve();
244
- }, timeoutMs);
245
- });
246
- const timeline = localEventStore.getTimeline({ kinds: [kind] });
380
+ } else {
381
+ this.logger.log(`Using ${cached.length} cached kind ${kind} events from persistent store`);
382
+ }
247
383
  const bases = /* @__PURE__ */ new Set();
248
384
  this.providerNodePubkeysByUrl = /* @__PURE__ */ new Map();
249
- for (const event of timeline) {
385
+ for (const event of sessionEvents) {
250
386
  const eventUrls = [];
251
387
  for (const tag of event.tags) {
252
388
  if (tag[0] === "u" && typeof tag[1] === "string") {
@@ -354,7 +490,11 @@ var ModelManager = class _ModelManager {
354
490
  this.adapter.setBaseUrlsList(list);
355
491
  this.adapter.setBaseUrlsLastUpdate(Date.now());
356
492
  await this.fetchRoutstr21Models(forceRefresh);
357
- await this.syncReviewedProvidersFromNostr(list);
493
+ await this.syncReviewedProvidersFromNostr(
494
+ list,
495
+ this.providerNodePubkeysByUrl,
496
+ forceRefresh
497
+ );
358
498
  }
359
499
  return list;
360
500
  } catch (e) {
@@ -373,7 +513,7 @@ var ModelManager = class _ModelManager {
373
513
  * @param baseUrls Current provider base URLs to evaluate
374
514
  * @returns Array of provider base URLs disabled by the review set
375
515
  */
376
- async syncReviewedProvidersFromNostr(baseUrls = this.adapter.getBaseUrlsList(), providerNodes = this.providerNodePubkeysByUrl) {
516
+ async syncReviewedProvidersFromNostr(baseUrls = this.adapter.getBaseUrlsList(), providerNodes = this.providerNodePubkeysByUrl, forceRefresh = false) {
377
517
  if (baseUrls.length === 0) return [];
378
518
  if (!this.adapter.setDisabledProviders) {
379
519
  this.logger.warn(
@@ -381,30 +521,43 @@ var ModelManager = class _ModelManager {
381
521
  );
382
522
  return [];
383
523
  }
384
- const LGTM_RELAYS = [
385
- "wss://relay.primal.net",
386
- "wss://nos.lol",
387
- "wss://relay.damus.io",
388
- "wss://relay.routstr.com"
389
- ];
390
524
  const reviewedNodePubkeys = /* @__PURE__ */ new Set();
391
525
  {
392
- const pool = new RelayPool();
393
- const store = new EventStore();
394
- const timeoutMs = 5e3;
395
- await new Promise((resolve) => {
396
- pool.req(LGTM_RELAYS, {
397
- kinds: [38425],
398
- "#t": ["lgtm"],
399
- limit: 500,
400
- authors: [this.routstrPubkey]
401
- }).pipe(
402
- onlyEvents(),
403
- tap((event) => store.add(event))
404
- ).subscribe({ complete: () => resolve() });
405
- setTimeout(() => resolve(), timeoutMs);
406
- });
407
- for (const event of store.getTimeline({ kinds: [38425] })) {
526
+ const cached = await this.getCachedNostrEvents(
527
+ { kinds: [38425], "#t": ["lgtm"], authors: [this.routstrPubkey] },
528
+ this.cacheTTL,
529
+ forceRefresh
530
+ );
531
+ let sessionEvents = cached;
532
+ if (cached.length === 0) {
533
+ const lgtmRelays = this.getNostrRelays([
534
+ "wss://relay.primal.net",
535
+ "wss://nos.lol",
536
+ "wss://relay.damus.io",
537
+ "wss://relay.routstr.com"
538
+ ]);
539
+ const pool = new RelayPool();
540
+ const timeoutMs = 5e3;
541
+ await new Promise((resolve) => {
542
+ pool.req(lgtmRelays, {
543
+ kinds: [38425],
544
+ "#t": ["lgtm"],
545
+ limit: 500,
546
+ authors: [this.routstrPubkey]
547
+ }).pipe(
548
+ onlyEvents(),
549
+ tap((event) => {
550
+ sessionEvents.push(event);
551
+ this.eventStore?.add(event);
552
+ this.markEventFetched(event);
553
+ })
554
+ ).subscribe({ complete: () => resolve() });
555
+ setTimeout(() => resolve(), timeoutMs);
556
+ });
557
+ } else {
558
+ this.logger.log(`Using ${cached.length} cached kind 38425 events from persistent store`);
559
+ }
560
+ for (const event of sessionEvents) {
408
561
  const hasLgtmTag = event.tags.some(
409
562
  (tag) => tag[0] === "t" && tag[1]?.toLowerCase() === "lgtm"
410
563
  );
@@ -513,7 +666,7 @@ var ModelManager = class _ModelManager {
513
666
  if (this.isProviderDownError(error)) {
514
667
  this.logger.warn(`Provider ${base} is down right now.`);
515
668
  } else {
516
- this.logger.warn(`Failed to fetch models from ${base}:`, error);
669
+ this.logger.warn(`Provider ${base} unreachable: ${error.message}`);
517
670
  }
518
671
  this.adapter.setProviderLastUpdate(base, Date.now());
519
672
  return { success: false, base };
@@ -628,39 +781,49 @@ var ModelManager = class _ModelManager {
628
781
  return cachedModels;
629
782
  }
630
783
  }
631
- const DEFAULT_RELAYS = [
784
+ const relays = this.getNostrRelays([
632
785
  "wss://relay.damus.io",
633
786
  "wss://nos.lol",
634
787
  "wss://relay.routstr.com"
635
- ];
636
- const pool = new RelayPool();
637
- const localEventStore = new EventStore();
638
- const timeoutMs = 5e3;
639
- await new Promise((resolve) => {
640
- pool.req(DEFAULT_RELAYS, {
641
- kinds: [38423],
642
- "#d": ["routstr-21-models"],
643
- limit: 1,
644
- authors: [this.routstrPubkey]
645
- }).pipe(
646
- onlyEvents(),
647
- tap((event2) => {
648
- localEventStore.add(event2);
649
- })
650
- ).subscribe({
651
- complete: () => {
788
+ ]);
789
+ const cached = await this.getCachedNostrEvents(
790
+ { kinds: [38423], "#d": ["routstr-21-models"], authors: [this.routstrPubkey] },
791
+ this.cacheTTL,
792
+ forceRefresh
793
+ );
794
+ let sessionEvents = cached;
795
+ if (cached.length === 0) {
796
+ const pool = new RelayPool();
797
+ const timeoutMs = 5e3;
798
+ await new Promise((resolve) => {
799
+ pool.req(relays, {
800
+ kinds: [38423],
801
+ "#d": ["routstr-21-models"],
802
+ limit: 1,
803
+ authors: [this.routstrPubkey]
804
+ }).pipe(
805
+ onlyEvents(),
806
+ tap((event2) => {
807
+ sessionEvents.push(event2);
808
+ this.eventStore?.add(event2);
809
+ this.markEventFetched(event2);
810
+ })
811
+ ).subscribe({
812
+ complete: () => {
813
+ resolve();
814
+ }
815
+ });
816
+ setTimeout(() => {
652
817
  resolve();
653
- }
818
+ }, timeoutMs);
654
819
  });
655
- setTimeout(() => {
656
- resolve();
657
- }, timeoutMs);
658
- });
659
- const timeline = localEventStore.getTimeline({ kinds: [38423] });
660
- if (timeline.length === 0) {
820
+ } else {
821
+ this.logger.log(`Using ${cached.length} cached kind 38423 events from persistent store`);
822
+ }
823
+ if (sessionEvents.length === 0) {
661
824
  return cachedModels.length > 0 ? cachedModels : [];
662
825
  }
663
- const event = timeline[0];
826
+ const event = sessionEvents[0];
664
827
  try {
665
828
  const content = JSON.parse(event.content);
666
829
  const models = Array.isArray(content?.models) ? content.models : [];
@@ -1378,7 +1541,7 @@ var CashuSpender = class {
1378
1541
  });
1379
1542
  continue;
1380
1543
  }
1381
- if (balanceResult.amount >= 0) {
1544
+ if (balanceResult.amount >= 0 && !balanceResult.balanceUnknown) {
1382
1545
  const balanceSat = balanceResult.unit === "msat" ? Math.floor(balanceResult.amount / 1e3) : balanceResult.amount;
1383
1546
  this.storageAdapter.updateApiKeyBalance(
1384
1547
  apiKeyEntry.baseUrl,
@@ -2127,17 +2290,24 @@ var BalanceManager = class _BalanceManager {
2127
2290
  this.logger.warn("getTokenBalance: FAILED", data);
2128
2291
  const isInvalidApiKey = response.status === 401 && data?.detail?.error?.code === "invalid_api_key" && data?.detail?.error?.message?.includes("proofs already spent");
2129
2292
  return {
2130
- amount: -1,
2293
+ amount: 0,
2131
2294
  reserved: data.reserved ?? 0,
2132
2295
  unit: "msat",
2133
2296
  apiKey: data.api_key,
2134
- isInvalidApiKey
2297
+ isInvalidApiKey,
2298
+ balanceUnknown: true
2135
2299
  };
2136
2300
  }
2137
2301
  } catch (error) {
2138
2302
  this.logger.error("getTokenBalance error", error);
2139
2303
  }
2140
- return { amount: -1, reserved: 0, unit: "sat", apiKey: "" };
2304
+ return {
2305
+ amount: 0,
2306
+ reserved: 0,
2307
+ unit: "sat",
2308
+ apiKey: "",
2309
+ balanceUnknown: true
2310
+ };
2141
2311
  }
2142
2312
  /**
2143
2313
  * Handle topup errors with specific error types
@@ -2172,529 +2342,210 @@ var BalanceManager = class _BalanceManager {
2172
2342
  }
2173
2343
  };
2174
2344
 
2175
- // client/usage.ts
2176
- function extractUsageFromResponseBody(body, fallbackSatsCost = 0) {
2177
- if (!body || typeof body !== "object") return null;
2178
- const usage = body.usage;
2179
- if (!usage || typeof usage !== "object") return null;
2180
- const promptTokens = Number(usage.prompt_tokens ?? 0);
2181
- const completionTokens = Number(usage.completion_tokens ?? 0);
2182
- const totalTokens = Number(usage.total_tokens ?? 0);
2183
- const costValue = usage.cost;
2184
- let cost = 0;
2185
- let satsCost = fallbackSatsCost;
2186
- if (typeof costValue === "number") {
2187
- cost = costValue;
2188
- } else if (costValue && typeof costValue === "object") {
2189
- const costObj = costValue;
2190
- const totalUsd = costObj.total_usd;
2191
- const totalMsats = costObj.total_msats;
2192
- cost = typeof totalUsd === "number" ? totalUsd : 0;
2193
- if (typeof totalMsats === "number") {
2194
- satsCost = totalMsats / 1e3;
2195
- }
2196
- }
2197
- if (promptTokens === 0 && completionTokens === 0 && totalTokens === 0 && cost === 0 && satsCost === 0) {
2198
- return null;
2199
- }
2200
- return {
2201
- promptTokens,
2202
- completionTokens,
2203
- totalTokens,
2204
- cost,
2205
- satsCost
2206
- };
2207
- }
2208
- function extractResponseId(body) {
2209
- if (!body || typeof body !== "object") return void 0;
2210
- const id = body.id;
2211
- if (typeof id !== "string") return void 0;
2212
- const trimmed = id.trim();
2213
- return trimmed.length > 0 ? trimmed : void 0;
2214
- }
2215
- function extractUsageFromSSEJson(parsed, fallbackSatsCost = 0) {
2216
- if (!parsed || typeof parsed !== "object") {
2217
- return null;
2218
- }
2219
- if (!parsed.usage && parsed.cost && typeof parsed.cost === "object") {
2220
- const costObj = parsed.cost;
2221
- const msats2 = costObj.total_msats ?? 0;
2222
- const cost2 = costObj.total_usd ?? 0;
2223
- if (msats2 === 0 && cost2 === 0) return null;
2224
- return {
2225
- promptTokens: Number(costObj.input_tokens ?? 0),
2226
- completionTokens: Number(costObj.output_tokens ?? 0),
2227
- totalTokens: Number((costObj.input_tokens ?? 0) + (costObj.output_tokens ?? 0)),
2228
- cost: Number(cost2),
2229
- satsCost: msats2 > 0 ? msats2 / 1e3 : fallbackSatsCost
2230
- };
2231
- }
2232
- if (!parsed.usage) {
2233
- return null;
2234
- }
2235
- const usage = parsed.usage;
2236
- const usageCost = usage.cost;
2237
- let cost = 0;
2238
- let msats = 0;
2239
- if (typeof usageCost === "number") {
2240
- cost = usageCost;
2241
- } else if (usageCost && typeof usageCost === "object") {
2242
- cost = usageCost.total_usd ?? 0;
2243
- msats = usageCost.total_msats ?? 0;
2345
+ // utils/torUtils.ts
2346
+ var TOR_ONION_SUFFIX = ".onion";
2347
+ var isTorContext = () => {
2348
+ if (typeof window === "undefined") return false;
2349
+ const hostname = window.location.hostname.toLowerCase();
2350
+ return hostname.endsWith(TOR_ONION_SUFFIX);
2351
+ };
2352
+ var isOnionUrl = (url) => {
2353
+ if (!url) return false;
2354
+ const trimmed = url.trim().toLowerCase();
2355
+ if (!trimmed) return false;
2356
+ try {
2357
+ const candidate = trimmed.startsWith("http") ? trimmed : `http://${trimmed}`;
2358
+ return new URL(candidate).hostname.endsWith(TOR_ONION_SUFFIX);
2359
+ } catch {
2360
+ return trimmed.includes(TOR_ONION_SUFFIX);
2244
2361
  }
2245
- if (cost === 0) {
2246
- cost = parsed.metadata?.routstr?.cost?.total_usd ?? 0;
2362
+ };
2363
+ var shouldAllowHttp = (url, torMode) => {
2364
+ if (!url.startsWith("http://")) return true;
2365
+ if (url.includes("localhost") || url.includes("127.0.0.1")) return true;
2366
+ return torMode && isOnionUrl(url);
2367
+ };
2368
+ var normalizeProviderUrl = (url, torMode = false) => {
2369
+ if (!url || typeof url !== "string") return null;
2370
+ const trimmed = url.trim();
2371
+ if (!trimmed) return null;
2372
+ if (/^https?:\/\//i.test(trimmed)) {
2373
+ return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
2247
2374
  }
2248
- if (msats === 0) {
2249
- msats = parsed.metadata?.routstr?.cost?.total_msats ?? (typeof usage.cost_sats === "number" ? usage.cost_sats * 1e3 : 0);
2375
+ const useHttpForOnion = torMode && isOnionUrl(trimmed);
2376
+ const withProto = `${useHttpForOnion ? "http" : "https"}://${trimmed}`;
2377
+ return withProto.endsWith("/") ? withProto : `${withProto}/`;
2378
+ };
2379
+ var dedupePreserveOrder = (urls) => {
2380
+ const seen = /* @__PURE__ */ new Set();
2381
+ const out = [];
2382
+ for (const url of urls) {
2383
+ if (!seen.has(url)) {
2384
+ seen.add(url);
2385
+ out.push(url);
2386
+ }
2250
2387
  }
2251
- const promptTokens = Number(usage.prompt_tokens ?? usage.input_tokens ?? 0);
2252
- const completionTokens = Number(usage.completion_tokens ?? usage.output_tokens ?? 0);
2253
- const totalTokens = Number(usage.total_tokens ?? promptTokens + completionTokens);
2254
- const result = {
2255
- promptTokens,
2256
- completionTokens,
2257
- totalTokens,
2258
- cost: Number(cost ?? 0),
2259
- satsCost: msats > 0 ? msats / 1e3 : fallbackSatsCost
2260
- };
2261
- if (result.promptTokens === 0 && result.completionTokens === 0 && result.totalTokens === 0 && result.cost === 0 && result.satsCost === 0) {
2262
- return null;
2388
+ return out;
2389
+ };
2390
+ var getProviderEndpoints = (provider, torMode) => {
2391
+ const rawUrls = [
2392
+ provider.endpoint_url,
2393
+ ...Array.isArray(provider.endpoint_urls) ? provider.endpoint_urls : [],
2394
+ provider.onion_url,
2395
+ ...Array.isArray(provider.onion_urls) ? provider.onion_urls : []
2396
+ ];
2397
+ const normalized = rawUrls.map((value) => normalizeProviderUrl(value, torMode)).filter((value) => Boolean(value));
2398
+ const unique = dedupePreserveOrder(normalized).filter(
2399
+ (value) => shouldAllowHttp(value, torMode)
2400
+ );
2401
+ if (unique.length === 0) return [];
2402
+ const onion = unique.filter((value) => isOnionUrl(value));
2403
+ const clearnet = unique.filter((value) => !isOnionUrl(value));
2404
+ if (torMode) {
2405
+ return onion.length > 0 ? onion : clearnet;
2263
2406
  }
2264
- return result;
2265
- }
2266
- function toUsageStats(usage) {
2267
- if (!usage) return void 0;
2268
- return {
2269
- total_tokens: usage.totalTokens,
2270
- prompt_tokens: usage.promptTokens,
2271
- completion_tokens: usage.completionTokens,
2272
- cost: usage.cost,
2273
- sats_cost: usage.satsCost
2274
- };
2275
- }
2407
+ return clearnet;
2408
+ };
2409
+ var filterBaseUrlsForTor = (baseUrls, torMode) => {
2410
+ if (!Array.isArray(baseUrls)) return [];
2411
+ const normalized = baseUrls.map((value) => normalizeProviderUrl(value, torMode)).filter((value) => Boolean(value));
2412
+ const filtered = normalized.filter(
2413
+ (value) => torMode ? true : !isOnionUrl(value)
2414
+ );
2415
+ return dedupePreserveOrder(
2416
+ filtered.filter((value) => shouldAllowHttp(value, torMode))
2417
+ );
2418
+ };
2276
2419
 
2277
- // client/StreamProcessor.ts
2278
- var StreamProcessor = class {
2279
- accumulatedContent = "";
2280
- accumulatedThinking = "";
2281
- accumulatedImages = [];
2282
- isInThinking = false;
2283
- isInContent = false;
2284
- /**
2285
- * Process a streaming response
2286
- */
2287
- async process(response, callbacks, modelId) {
2288
- if (!response.body) {
2289
- throw new Error("Response body is not available");
2290
- }
2291
- const reader = response.body.getReader();
2292
- const decoder = new TextDecoder("utf-8");
2293
- let buffer = "";
2294
- this.accumulatedContent = "";
2295
- this.accumulatedThinking = "";
2296
- this.accumulatedImages = [];
2297
- this.isInThinking = false;
2298
- this.isInContent = false;
2299
- let usage;
2300
- let model;
2301
- let finish_reason;
2302
- let citations;
2303
- let annotations;
2304
- let responseId;
2305
- try {
2306
- while (true) {
2307
- const { done, value } = await reader.read();
2308
- if (done) {
2309
- break;
2310
- }
2311
- const chunk = decoder.decode(value, { stream: true });
2312
- buffer += chunk;
2313
- const lines = buffer.split("\n");
2314
- buffer = lines.pop() || "";
2315
- for (const line of lines) {
2316
- const parsed = this._parseLine(line);
2317
- if (!parsed) continue;
2318
- if (parsed.content) {
2319
- this._handleContent(parsed.content, callbacks, modelId);
2320
- }
2321
- if (parsed.reasoning) {
2322
- this._handleThinking(parsed.reasoning, callbacks);
2323
- }
2324
- if (parsed.usage) {
2325
- usage = parsed.usage;
2326
- }
2327
- if (parsed.model) {
2328
- model = parsed.model;
2329
- }
2330
- if (parsed.finish_reason) {
2331
- finish_reason = parsed.finish_reason;
2332
- }
2333
- if (parsed.responseId) {
2334
- responseId = parsed.responseId;
2335
- }
2336
- if (parsed.citations) {
2337
- citations = parsed.citations;
2338
- }
2339
- if (parsed.annotations) {
2340
- annotations = parsed.annotations;
2341
- }
2342
- if (parsed.images) {
2343
- this._mergeImages(parsed.images);
2344
- }
2345
- }
2346
- }
2347
- } finally {
2348
- reader.releaseLock();
2349
- }
2350
- return {
2351
- content: this.accumulatedContent,
2352
- thinking: this.accumulatedThinking || void 0,
2353
- images: this.accumulatedImages.length > 0 ? this.accumulatedImages : void 0,
2354
- usage,
2355
- model,
2356
- responseId,
2357
- finish_reason,
2358
- citations,
2359
- annotations
2360
- };
2361
- }
2362
- /**
2363
- * Parse a single SSE line
2364
- */
2365
- _parseLine(line) {
2366
- if (!line.trim()) return null;
2367
- if (!line.startsWith("data: ")) {
2420
+ // client/ProviderManager.ts
2421
+ function getImageResolutionFromDataUrl(dataUrl) {
2422
+ try {
2423
+ if (typeof dataUrl !== "string" || !dataUrl.startsWith("data:"))
2368
2424
  return null;
2369
- }
2370
- const jsonData = line.slice(6);
2371
- if (jsonData === "[DONE]") {
2425
+ const commaIdx = dataUrl.indexOf(",");
2426
+ if (commaIdx === -1) return null;
2427
+ const meta = dataUrl.slice(5, commaIdx);
2428
+ const base64 = dataUrl.slice(commaIdx + 1);
2429
+ const binary = typeof atob === "function" ? atob(base64) : Buffer.from(base64, "base64").toString("binary");
2430
+ const len = binary.length;
2431
+ const bytes = new Uint8Array(len);
2432
+ for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i);
2433
+ const isPNG = meta.includes("image/png");
2434
+ const isJPEG = meta.includes("image/jpeg") || meta.includes("image/jpg");
2435
+ if (isPNG) {
2436
+ const sig = [137, 80, 78, 71, 13, 10, 26, 10];
2437
+ for (let i = 0; i < sig.length; i++) {
2438
+ if (bytes[i] !== sig[i]) return null;
2439
+ }
2440
+ const view = new DataView(
2441
+ bytes.buffer,
2442
+ bytes.byteOffset,
2443
+ bytes.byteLength
2444
+ );
2445
+ const width = view.getUint32(16, false);
2446
+ const height = view.getUint32(20, false);
2447
+ if (width > 0 && height > 0) return { width, height };
2372
2448
  return null;
2373
2449
  }
2374
- try {
2375
- const parsed = JSON.parse(jsonData);
2376
- const result = {};
2377
- if (parsed.choices?.[0]?.delta?.content) {
2378
- result.content = parsed.choices[0].delta.content;
2379
- }
2380
- if (parsed.choices?.[0]?.delta?.reasoning) {
2381
- result.reasoning = parsed.choices[0].delta.reasoning;
2382
- }
2383
- const extractedUsage = extractUsageFromSSEJson(parsed);
2384
- if (extractedUsage) {
2385
- result.usage = toUsageStats(extractedUsage);
2386
- } else if (parsed.usage) {
2387
- result.usage = {
2388
- total_tokens: parsed.usage.total_tokens ?? parsed.usage.input_tokens + parsed.usage.output_tokens,
2389
- prompt_tokens: parsed.usage.prompt_tokens ?? parsed.usage.input_tokens,
2390
- completion_tokens: parsed.usage.completion_tokens ?? parsed.usage.output_tokens
2391
- };
2392
- }
2393
- if (parsed.id) {
2394
- result.responseId = parsed.id;
2395
- }
2396
- if (parsed.model) {
2397
- result.model = parsed.model;
2398
- }
2399
- if (parsed.citations) {
2400
- result.citations = parsed.citations;
2401
- }
2402
- if (parsed.annotations) {
2403
- result.annotations = parsed.annotations;
2404
- }
2405
- if (parsed.choices?.[0]?.finish_reason) {
2406
- result.finish_reason = parsed.choices[0].finish_reason;
2407
- }
2408
- const images = parsed.choices?.[0]?.message?.images || parsed.choices?.[0]?.delta?.images;
2409
- if (images && Array.isArray(images)) {
2410
- result.images = images;
2450
+ if (isJPEG) {
2451
+ let offset = 0;
2452
+ if (bytes[offset++] !== 255 || bytes[offset++] !== 216) return null;
2453
+ while (offset < bytes.length) {
2454
+ while (offset < bytes.length && bytes[offset] !== 255) offset++;
2455
+ if (offset + 1 >= bytes.length) break;
2456
+ while (bytes[offset] === 255) offset++;
2457
+ const marker = bytes[offset++];
2458
+ if (marker === 216 || marker === 217) continue;
2459
+ if (offset + 1 >= bytes.length) break;
2460
+ const length = bytes[offset] << 8 | bytes[offset + 1];
2461
+ offset += 2;
2462
+ if (marker === 192 || marker === 194) {
2463
+ if (length < 7 || offset + length - 2 > bytes.length) return null;
2464
+ const precision = bytes[offset];
2465
+ const height = bytes[offset + 1] << 8 | bytes[offset + 2];
2466
+ const width = bytes[offset + 3] << 8 | bytes[offset + 4];
2467
+ if (precision > 0 && width > 0 && height > 0)
2468
+ return { width, height };
2469
+ return null;
2470
+ } else {
2471
+ offset += length - 2;
2472
+ }
2411
2473
  }
2412
- return result;
2413
- } catch {
2414
2474
  return null;
2415
2475
  }
2476
+ return null;
2477
+ } catch {
2478
+ return null;
2416
2479
  }
2417
- /**
2418
- * Handle content delta with thinking support
2419
- */
2420
- _handleContent(content, callbacks, modelId) {
2421
- if (this.isInThinking && !this.isInContent) {
2422
- this.accumulatedThinking += "</thinking>";
2423
- callbacks.onThinking(this.accumulatedThinking);
2424
- this.isInThinking = false;
2425
- this.isInContent = true;
2480
+ }
2481
+ function calculateImageTokens(width, height, detail = "auto") {
2482
+ if (detail === "low") return 85;
2483
+ let w = width;
2484
+ let h = height;
2485
+ if (w > 2048 || h > 2048) {
2486
+ const aspectRatio = w / h;
2487
+ if (w > h) {
2488
+ w = 2048;
2489
+ h = Math.floor(w / aspectRatio);
2490
+ } else {
2491
+ h = 2048;
2492
+ w = Math.floor(h * aspectRatio);
2426
2493
  }
2427
- if (modelId) {
2428
- this._extractThinkingFromContent(content, callbacks);
2494
+ }
2495
+ if (w > 768 || h > 768) {
2496
+ const aspectRatio = w / h;
2497
+ if (w > h) {
2498
+ w = 768;
2499
+ h = Math.floor(w / aspectRatio);
2429
2500
  } else {
2430
- this.accumulatedContent += content;
2501
+ h = 768;
2502
+ w = Math.floor(h * aspectRatio);
2503
+ }
2504
+ }
2505
+ const tilesWidth = Math.floor((w + 511) / 512);
2506
+ const tilesHeight = Math.floor((h + 511) / 512);
2507
+ const numTiles = tilesWidth * tilesHeight;
2508
+ return 85 + 170 * numTiles;
2509
+ }
2510
+ var ProviderManager = class _ProviderManager {
2511
+ constructor(providerRegistry, store, logger) {
2512
+ this.providerRegistry = providerRegistry;
2513
+ this.instanceId = `pm_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
2514
+ this.logger = (logger ?? consoleLogger).child(`ProviderManager:${this.instanceId}`);
2515
+ if (store) {
2516
+ this.store = store;
2517
+ this.hydrateFromStore();
2431
2518
  }
2432
- callbacks.onContent(this.accumulatedContent);
2433
2519
  }
2520
+ providerRegistry;
2521
+ failedProviders = /* @__PURE__ */ new Set();
2522
+ /** Track when each provider last failed (provider URL -> timestamp) */
2523
+ lastFailed = /* @__PURE__ */ new Map();
2524
+ /** Providers on cooldown: [provider_url, cooldown_started_timestamp][] */
2525
+ providersOnCoolDown = [];
2526
+ /** Cooldown duration in milliseconds (42 seconds) */
2527
+ static COOLDOWN_DURATION_MS = 42 * 1e3;
2528
+ /** Optional persistent store for failure tracking */
2529
+ store = null;
2530
+ /** Instance ID for debugging */
2531
+ instanceId;
2532
+ logger;
2434
2533
  /**
2435
- * Handle thinking/reasoning content
2534
+ * Hydrate in-memory state from persistent store
2436
2535
  */
2437
- _handleThinking(reasoning, callbacks) {
2438
- if (!this.isInThinking) {
2439
- this.accumulatedThinking += "<thinking> ";
2440
- this.isInThinking = true;
2441
- }
2442
- this.accumulatedThinking += reasoning;
2443
- callbacks.onThinking(this.accumulatedThinking);
2536
+ hydrateFromStore() {
2537
+ if (!this.store) return;
2538
+ const state = this.store.getState();
2539
+ this.failedProviders = new Set(state.failedProviders);
2540
+ this.lastFailed = new Map(Object.entries(state.lastFailed));
2541
+ const now = Date.now();
2542
+ this.providersOnCoolDown = state.providersOnCooldown.filter(
2543
+ (entry) => now - entry.timestamp < _ProviderManager.COOLDOWN_DURATION_MS
2544
+ ).map((entry) => [entry.baseUrl, entry.timestamp]);
2545
+ this.logger.log(`Hydrated from store: failedProviders=${this.failedProviders.size} lastFailed=${this.lastFailed.size} providersOnCooldown=${this.providersOnCoolDown.length}`);
2444
2546
  }
2445
2547
  /**
2446
- * Extract thinking blocks from content (for models with inline thinking)
2447
- */
2448
- _extractThinkingFromContent(content, callbacks) {
2449
- const parts = content.split(/(<thinking>|<\/thinking>)/);
2450
- for (const part of parts) {
2451
- if (part === "<thinking>") {
2452
- this.isInThinking = true;
2453
- if (!this.accumulatedThinking.includes("<thinking>")) {
2454
- this.accumulatedThinking += "<thinking> ";
2455
- }
2456
- } else if (part === "</thinking>") {
2457
- this.isInThinking = false;
2458
- this.accumulatedThinking += "</thinking>";
2459
- } else if (this.isInThinking) {
2460
- this.accumulatedThinking += part;
2461
- } else {
2462
- this.accumulatedContent += part;
2463
- }
2464
- }
2465
- }
2466
- /**
2467
- * Merge images into accumulated array, avoiding duplicates
2468
- */
2469
- _mergeImages(newImages) {
2470
- for (const img of newImages) {
2471
- const newUrl = img.image_url?.url;
2472
- const existingIndex = this.accumulatedImages.findIndex((existing) => {
2473
- const existingUrl = existing.image_url?.url;
2474
- if (newUrl && existingUrl) {
2475
- return existingUrl === newUrl;
2476
- }
2477
- if (img.index !== void 0 && existing.index !== void 0) {
2478
- return existing.index === img.index;
2479
- }
2480
- return false;
2481
- });
2482
- if (existingIndex === -1) {
2483
- this.accumulatedImages.push(img);
2484
- } else {
2485
- this.accumulatedImages[existingIndex] = img;
2486
- }
2487
- }
2488
- }
2489
- };
2490
-
2491
- // utils/torUtils.ts
2492
- var TOR_ONION_SUFFIX = ".onion";
2493
- var isTorContext = () => {
2494
- if (typeof window === "undefined") return false;
2495
- const hostname = window.location.hostname.toLowerCase();
2496
- return hostname.endsWith(TOR_ONION_SUFFIX);
2497
- };
2498
- var isOnionUrl = (url) => {
2499
- if (!url) return false;
2500
- const trimmed = url.trim().toLowerCase();
2501
- if (!trimmed) return false;
2502
- try {
2503
- const candidate = trimmed.startsWith("http") ? trimmed : `http://${trimmed}`;
2504
- return new URL(candidate).hostname.endsWith(TOR_ONION_SUFFIX);
2505
- } catch {
2506
- return trimmed.includes(TOR_ONION_SUFFIX);
2507
- }
2508
- };
2509
- var shouldAllowHttp = (url, torMode) => {
2510
- if (!url.startsWith("http://")) return true;
2511
- if (url.includes("localhost") || url.includes("127.0.0.1")) return true;
2512
- return torMode && isOnionUrl(url);
2513
- };
2514
- var normalizeProviderUrl = (url, torMode = false) => {
2515
- if (!url || typeof url !== "string") return null;
2516
- const trimmed = url.trim();
2517
- if (!trimmed) return null;
2518
- if (/^https?:\/\//i.test(trimmed)) {
2519
- return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
2520
- }
2521
- const useHttpForOnion = torMode && isOnionUrl(trimmed);
2522
- const withProto = `${useHttpForOnion ? "http" : "https"}://${trimmed}`;
2523
- return withProto.endsWith("/") ? withProto : `${withProto}/`;
2524
- };
2525
- var dedupePreserveOrder = (urls) => {
2526
- const seen = /* @__PURE__ */ new Set();
2527
- const out = [];
2528
- for (const url of urls) {
2529
- if (!seen.has(url)) {
2530
- seen.add(url);
2531
- out.push(url);
2532
- }
2533
- }
2534
- return out;
2535
- };
2536
- var getProviderEndpoints = (provider, torMode) => {
2537
- const rawUrls = [
2538
- provider.endpoint_url,
2539
- ...Array.isArray(provider.endpoint_urls) ? provider.endpoint_urls : [],
2540
- provider.onion_url,
2541
- ...Array.isArray(provider.onion_urls) ? provider.onion_urls : []
2542
- ];
2543
- const normalized = rawUrls.map((value) => normalizeProviderUrl(value, torMode)).filter((value) => Boolean(value));
2544
- const unique = dedupePreserveOrder(normalized).filter(
2545
- (value) => shouldAllowHttp(value, torMode)
2546
- );
2547
- if (unique.length === 0) return [];
2548
- const onion = unique.filter((value) => isOnionUrl(value));
2549
- const clearnet = unique.filter((value) => !isOnionUrl(value));
2550
- if (torMode) {
2551
- return onion.length > 0 ? onion : clearnet;
2552
- }
2553
- return clearnet;
2554
- };
2555
- var filterBaseUrlsForTor = (baseUrls, torMode) => {
2556
- if (!Array.isArray(baseUrls)) return [];
2557
- const normalized = baseUrls.map((value) => normalizeProviderUrl(value, torMode)).filter((value) => Boolean(value));
2558
- const filtered = normalized.filter(
2559
- (value) => torMode ? true : !isOnionUrl(value)
2560
- );
2561
- return dedupePreserveOrder(
2562
- filtered.filter((value) => shouldAllowHttp(value, torMode))
2563
- );
2564
- };
2565
-
2566
- // client/ProviderManager.ts
2567
- function getImageResolutionFromDataUrl(dataUrl) {
2568
- try {
2569
- if (typeof dataUrl !== "string" || !dataUrl.startsWith("data:"))
2570
- return null;
2571
- const commaIdx = dataUrl.indexOf(",");
2572
- if (commaIdx === -1) return null;
2573
- const meta = dataUrl.slice(5, commaIdx);
2574
- const base64 = dataUrl.slice(commaIdx + 1);
2575
- const binary = typeof atob === "function" ? atob(base64) : Buffer.from(base64, "base64").toString("binary");
2576
- const len = binary.length;
2577
- const bytes = new Uint8Array(len);
2578
- for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i);
2579
- const isPNG = meta.includes("image/png");
2580
- const isJPEG = meta.includes("image/jpeg") || meta.includes("image/jpg");
2581
- if (isPNG) {
2582
- const sig = [137, 80, 78, 71, 13, 10, 26, 10];
2583
- for (let i = 0; i < sig.length; i++) {
2584
- if (bytes[i] !== sig[i]) return null;
2585
- }
2586
- const view = new DataView(
2587
- bytes.buffer,
2588
- bytes.byteOffset,
2589
- bytes.byteLength
2590
- );
2591
- const width = view.getUint32(16, false);
2592
- const height = view.getUint32(20, false);
2593
- if (width > 0 && height > 0) return { width, height };
2594
- return null;
2595
- }
2596
- if (isJPEG) {
2597
- let offset = 0;
2598
- if (bytes[offset++] !== 255 || bytes[offset++] !== 216) return null;
2599
- while (offset < bytes.length) {
2600
- while (offset < bytes.length && bytes[offset] !== 255) offset++;
2601
- if (offset + 1 >= bytes.length) break;
2602
- while (bytes[offset] === 255) offset++;
2603
- const marker = bytes[offset++];
2604
- if (marker === 216 || marker === 217) continue;
2605
- if (offset + 1 >= bytes.length) break;
2606
- const length = bytes[offset] << 8 | bytes[offset + 1];
2607
- offset += 2;
2608
- if (marker === 192 || marker === 194) {
2609
- if (length < 7 || offset + length - 2 > bytes.length) return null;
2610
- const precision = bytes[offset];
2611
- const height = bytes[offset + 1] << 8 | bytes[offset + 2];
2612
- const width = bytes[offset + 3] << 8 | bytes[offset + 4];
2613
- if (precision > 0 && width > 0 && height > 0)
2614
- return { width, height };
2615
- return null;
2616
- } else {
2617
- offset += length - 2;
2618
- }
2619
- }
2620
- return null;
2621
- }
2622
- return null;
2623
- } catch {
2624
- return null;
2625
- }
2626
- }
2627
- function calculateImageTokens(width, height, detail = "auto") {
2628
- if (detail === "low") return 85;
2629
- let w = width;
2630
- let h = height;
2631
- if (w > 2048 || h > 2048) {
2632
- const aspectRatio = w / h;
2633
- if (w > h) {
2634
- w = 2048;
2635
- h = Math.floor(w / aspectRatio);
2636
- } else {
2637
- h = 2048;
2638
- w = Math.floor(h * aspectRatio);
2639
- }
2640
- }
2641
- if (w > 768 || h > 768) {
2642
- const aspectRatio = w / h;
2643
- if (w > h) {
2644
- w = 768;
2645
- h = Math.floor(w / aspectRatio);
2646
- } else {
2647
- h = 768;
2648
- w = Math.floor(h * aspectRatio);
2649
- }
2650
- }
2651
- const tilesWidth = Math.floor((w + 511) / 512);
2652
- const tilesHeight = Math.floor((h + 511) / 512);
2653
- const numTiles = tilesWidth * tilesHeight;
2654
- return 85 + 170 * numTiles;
2655
- }
2656
- function isInsecureHttpUrl(url) {
2657
- return url.startsWith("http://");
2658
- }
2659
- var ProviderManager = class _ProviderManager {
2660
- constructor(providerRegistry, store, logger) {
2661
- this.providerRegistry = providerRegistry;
2662
- this.instanceId = `pm_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
2663
- this.logger = (logger ?? consoleLogger).child(`ProviderManager:${this.instanceId}`);
2664
- if (store) {
2665
- this.store = store;
2666
- this.hydrateFromStore();
2667
- }
2668
- }
2669
- providerRegistry;
2670
- failedProviders = /* @__PURE__ */ new Set();
2671
- /** Track when each provider last failed (provider URL -> timestamp) */
2672
- lastFailed = /* @__PURE__ */ new Map();
2673
- /** Providers on cooldown: [provider_url, cooldown_started_timestamp][] */
2674
- providersOnCoolDown = [];
2675
- /** Cooldown duration in milliseconds (42 seconds) */
2676
- static COOLDOWN_DURATION_MS = 42 * 1e3;
2677
- /** Optional persistent store for failure tracking */
2678
- store = null;
2679
- /** Instance ID for debugging */
2680
- instanceId;
2681
- logger;
2682
- /**
2683
- * Hydrate in-memory state from persistent store
2684
- */
2685
- hydrateFromStore() {
2686
- if (!this.store) return;
2687
- const state = this.store.getState();
2688
- this.failedProviders = new Set(state.failedProviders);
2689
- this.lastFailed = new Map(Object.entries(state.lastFailed));
2690
- const now = Date.now();
2691
- this.providersOnCoolDown = state.providersOnCooldown.filter(
2692
- (entry) => now - entry.timestamp < _ProviderManager.COOLDOWN_DURATION_MS
2693
- ).map((entry) => [entry.baseUrl, entry.timestamp]);
2694
- this.logger.log(`Hydrated from store: failedProviders=${this.failedProviders.size} lastFailed=${this.lastFailed.size} providersOnCooldown=${this.providersOnCoolDown.length}`);
2695
- }
2696
- /**
2697
- * Get instance ID for debugging
2548
+ * Get instance ID for debugging
2698
2549
  */
2699
2550
  getInstanceId() {
2700
2551
  return this.instanceId;
@@ -2872,7 +2723,7 @@ var ProviderManager = class _ProviderManager {
2872
2723
  if (this.isOnCooldown(baseUrl)) {
2873
2724
  continue;
2874
2725
  }
2875
- if (!torMode && (isOnionUrl(baseUrl) || isInsecureHttpUrl(baseUrl))) {
2726
+ if (!torMode && isOnionUrl(baseUrl)) {
2876
2727
  continue;
2877
2728
  }
2878
2729
  const model = models.find((m) => m.id === modelId);
@@ -2923,7 +2774,7 @@ var ProviderManager = class _ProviderManager {
2923
2774
  for (const [baseUrl, models] of Object.entries(allProviders)) {
2924
2775
  if (disabledProviders.has(baseUrl)) continue;
2925
2776
  if (this.isOnCooldown(baseUrl)) continue;
2926
- if (!torMode && (isOnionUrl(baseUrl) || isInsecureHttpUrl(baseUrl)))
2777
+ if (!torMode && isOnionUrl(baseUrl))
2927
2778
  continue;
2928
2779
  const model = models.find((m) => m.id === modelId);
2929
2780
  if (!model) continue;
@@ -2938,16 +2789,18 @@ var ProviderManager = class _ProviderManager {
2938
2789
  getProviderPriceRankingForModel(modelId, options = {}) {
2939
2790
  const includeDisabled = options.includeDisabled ?? false;
2940
2791
  const torMode = options.torMode ?? false;
2941
- const disabledProviders = new Set(
2942
- this.providerRegistry.getDisabledProviders()
2943
- );
2792
+ const disabledProviderList = this.providerRegistry.getDisabledProviders();
2793
+ const disabledProviders = new Set(disabledProviderList);
2794
+ if (disabledProviderList.length > 0) {
2795
+ this.logger.log(`getProviderPriceRankingForModel: disabled providers (${disabledProviderList.length}): ${disabledProviderList.join(", ")}`);
2796
+ }
2944
2797
  const allModels = this.providerRegistry.getAllProvidersModels();
2945
2798
  const results = [];
2946
2799
  for (const [baseUrl, models] of Object.entries(allModels)) {
2947
2800
  if (!includeDisabled && disabledProviders.has(baseUrl)) continue;
2948
2801
  if (this.isOnCooldown(baseUrl)) continue;
2949
2802
  if (torMode && !baseUrl.includes(".onion")) continue;
2950
- if (!torMode && (baseUrl.includes(".onion") || isInsecureHttpUrl(baseUrl)))
2803
+ if (!torMode && baseUrl.includes(".onion"))
2951
2804
  continue;
2952
2805
  const match = models.find((model) => model.id === modelId);
2953
2806
  if (!match?.sats_pricing) continue;
@@ -2967,12 +2820,20 @@ var ProviderManager = class _ProviderManager {
2967
2820
  totalPerMillion
2968
2821
  });
2969
2822
  }
2970
- return results.sort((a, b) => {
2823
+ results.sort((a, b) => {
2971
2824
  if (a.totalPerMillion !== b.totalPerMillion) {
2972
2825
  return a.totalPerMillion - b.totalPerMillion;
2973
2826
  }
2974
2827
  return a.baseUrl.localeCompare(b.baseUrl);
2975
2828
  });
2829
+ if (results.length > 0) {
2830
+ 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");
2831
+ this.logger.log(`getProviderPriceRankingForModel: ${modelId} ranking (${results.length} providers):
2832
+ ${ranking}`);
2833
+ } else {
2834
+ this.logger.log(`getProviderPriceRankingForModel: ${modelId} no providers found`);
2835
+ }
2836
+ return results;
2976
2837
  }
2977
2838
  /**
2978
2839
  * Get best-priced provider for a specific model
@@ -3165,141 +3026,6 @@ var createMemoryDriver = (seed) => {
3165
3026
  };
3166
3027
  };
3167
3028
 
3168
- // storage/drivers/sqlite.ts
3169
- var isBun = () => {
3170
- return typeof process.versions.bun !== "undefined";
3171
- };
3172
- var cachedDbModule = null;
3173
- var loadDatabase = async (dbPath) => {
3174
- if (isBun()) {
3175
- throw new Error(
3176
- "SQLite driver not supported in Bun. Use createBunSqliteDriver() instead."
3177
- );
3178
- }
3179
- try {
3180
- if (!cachedDbModule) {
3181
- cachedDbModule = (await import('better-sqlite3')).default;
3182
- }
3183
- return new cachedDbModule(dbPath);
3184
- } catch (error) {
3185
- throw new Error(
3186
- `better-sqlite3 is required for sqlite storage. Install it to use sqlite storage. (${error})`
3187
- );
3188
- }
3189
- };
3190
- var createSqliteDriver = (options = {}) => {
3191
- const dbPath = options.dbPath || "routstr.sqlite";
3192
- const tableName = options.tableName || "sdk_storage";
3193
- let db;
3194
- let selectStmt;
3195
- let upsertStmt;
3196
- let deleteStmt;
3197
- const initDb = async () => {
3198
- if (!db) {
3199
- db = await loadDatabase(dbPath);
3200
- db.exec(
3201
- `CREATE TABLE IF NOT EXISTS ${tableName} (key TEXT PRIMARY KEY, value TEXT NOT NULL)`
3202
- );
3203
- selectStmt = db.prepare(`SELECT value FROM ${tableName} WHERE key = ?`);
3204
- upsertStmt = db.prepare(
3205
- `INSERT INTO ${tableName} (key, value) VALUES (?, ?)
3206
- ON CONFLICT(key) DO UPDATE SET value = excluded.value`
3207
- );
3208
- deleteStmt = db.prepare(`DELETE FROM ${tableName} WHERE key = ?`);
3209
- }
3210
- };
3211
- const ensureInit = async () => {
3212
- if (!db) {
3213
- await initDb();
3214
- }
3215
- };
3216
- return {
3217
- async getItem(key, defaultValue) {
3218
- try {
3219
- await ensureInit();
3220
- const row = selectStmt.get(key);
3221
- if (!row || typeof row.value !== "string") return defaultValue;
3222
- try {
3223
- return JSON.parse(row.value);
3224
- } catch (parseError) {
3225
- if (typeof defaultValue === "string") {
3226
- return row.value;
3227
- }
3228
- throw parseError;
3229
- }
3230
- } catch (error) {
3231
- console.error(`SQLite getItem failed for key "${key}":`, error);
3232
- return defaultValue;
3233
- }
3234
- },
3235
- async setItem(key, value) {
3236
- try {
3237
- await ensureInit();
3238
- upsertStmt.run(key, JSON.stringify(value));
3239
- } catch (error) {
3240
- console.error(`SQLite setItem failed for key "${key}":`, error);
3241
- }
3242
- },
3243
- async removeItem(key) {
3244
- try {
3245
- await ensureInit();
3246
- deleteStmt.run(key);
3247
- } catch (error) {
3248
- console.error(`SQLite removeItem failed for key "${key}":`, error);
3249
- }
3250
- }
3251
- };
3252
- };
3253
- async function createBunSqliteDriver(dbPath, options) {
3254
- const logger = (options?.logger ?? consoleLogger).child("BunSqliteDriver");
3255
- const SQLite = (await import(
3256
- /* webpackIgnore: true */
3257
- 'bun:sqlite'
3258
- )).default;
3259
- const db = new SQLite(dbPath);
3260
- db.run(`
3261
- CREATE TABLE IF NOT EXISTS sdk_storage (
3262
- key TEXT PRIMARY KEY,
3263
- value TEXT NOT NULL
3264
- )
3265
- `);
3266
- return {
3267
- async getItem(key, defaultValue) {
3268
- try {
3269
- const row = db.query("SELECT value FROM sdk_storage WHERE key = ?").get(key);
3270
- if (!row || typeof row.value !== "string") return defaultValue;
3271
- try {
3272
- return JSON.parse(row.value);
3273
- } catch (parseError) {
3274
- if (typeof defaultValue === "string") {
3275
- return row.value;
3276
- }
3277
- throw parseError;
3278
- }
3279
- } catch (error) {
3280
- logger.error(`getItem failed for key "${key}":`, error);
3281
- return defaultValue;
3282
- }
3283
- },
3284
- async setItem(key, value) {
3285
- try {
3286
- db.query(
3287
- "INSERT INTO sdk_storage (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value"
3288
- ).run(key, JSON.stringify(value));
3289
- } catch (error) {
3290
- logger.error(`setItem failed for key "${key}":`, error);
3291
- }
3292
- },
3293
- async removeItem(key) {
3294
- try {
3295
- db.query("DELETE FROM sdk_storage WHERE key = ?").run(key);
3296
- } catch (error) {
3297
- logger.error(`removeItem failed for key "${key}":`, error);
3298
- }
3299
- }
3300
- };
3301
- }
3302
-
3303
3029
  // storage/drivers/indexedDB.ts
3304
3030
  var isBrowser = typeof indexedDB !== "undefined";
3305
3031
  var openDatabase = (dbName, storeName) => {
@@ -3307,15 +3033,32 @@ var openDatabase = (dbName, storeName) => {
3307
3033
  return Promise.reject(new Error("IndexedDB is not available"));
3308
3034
  }
3309
3035
  return new Promise((resolve, reject) => {
3310
- const request = indexedDB.open(dbName, 1);
3036
+ const request = indexedDB.open(dbName, 2);
3311
3037
  request.onupgradeneeded = () => {
3312
3038
  const db = request.result;
3313
3039
  if (!db.objectStoreNames.contains(storeName)) {
3314
3040
  db.createObjectStore(storeName);
3315
3041
  }
3042
+ if (storeName !== "usage_tracking" && !db.objectStoreNames.contains("usage_tracking")) {
3043
+ const utStore = db.createObjectStore("usage_tracking", { keyPath: "id" });
3044
+ utStore.createIndex("timestamp", "timestamp", { unique: false });
3045
+ utStore.createIndex("modelId", "modelId", { unique: false });
3046
+ utStore.createIndex("baseUrl", "baseUrl", { unique: false });
3047
+ utStore.createIndex("sessionId", "sessionId", { unique: false });
3048
+ utStore.createIndex("client", "client", { unique: false });
3049
+ }
3050
+ if (storeName !== "sdk_storage" && !db.objectStoreNames.contains("sdk_storage")) {
3051
+ db.createObjectStore("sdk_storage");
3052
+ }
3316
3053
  };
3317
3054
  request.onsuccess = () => resolve(request.result);
3318
3055
  request.onerror = () => reject(request.error);
3056
+ request.onblocked = () => {
3057
+ console.warn(
3058
+ `[IndexedDB driver] open blocked for "${dbName}" (store: "${storeName}") \u2014 close other tabs using this DB`
3059
+ );
3060
+ reject(new Error(`IndexedDB "${dbName}" blocked by another connection`));
3061
+ };
3319
3062
  });
3320
3063
  };
3321
3064
  var createIndexedDBDriver = (options = {}) => {
@@ -3428,9 +3171,10 @@ var openDatabase2 = (dbName, storeName) => {
3428
3171
  return Promise.reject(new Error("IndexedDB is not available"));
3429
3172
  }
3430
3173
  return new Promise((resolve, reject) => {
3431
- const request = indexedDB.open(dbName, 1);
3174
+ const request = indexedDB.open(dbName, 3);
3432
3175
  request.onupgradeneeded = () => {
3433
3176
  const db = request.result;
3177
+ const tx = request.transaction;
3434
3178
  if (!db.objectStoreNames.contains(storeName)) {
3435
3179
  const store = db.createObjectStore(storeName, { keyPath: "id" });
3436
3180
  store.createIndex("timestamp", "timestamp", { unique: false });
@@ -3438,10 +3182,25 @@ var openDatabase2 = (dbName, storeName) => {
3438
3182
  store.createIndex("baseUrl", "baseUrl", { unique: false });
3439
3183
  store.createIndex("sessionId", "sessionId", { unique: false });
3440
3184
  store.createIndex("client", "client", { unique: false });
3185
+ store.createIndex("provider", "provider", { unique: false });
3186
+ } else if (tx) {
3187
+ const store = tx.objectStore(storeName);
3188
+ if (!store.indexNames.contains("provider")) {
3189
+ store.createIndex("provider", "provider", { unique: false });
3190
+ }
3191
+ }
3192
+ if (storeName !== "sdk_storage" && !db.objectStoreNames.contains("sdk_storage")) {
3193
+ db.createObjectStore("sdk_storage");
3441
3194
  }
3442
3195
  };
3443
3196
  request.onsuccess = () => resolve(request.result);
3444
3197
  request.onerror = () => reject(request.error);
3198
+ request.onblocked = () => {
3199
+ console.warn(
3200
+ `[usageTracking IndexedDB] open blocked for "${dbName}" \u2014 close other tabs using this DB`
3201
+ );
3202
+ reject(new Error(`IndexedDB "${dbName}" blocked by another connection`));
3203
+ };
3445
3204
  });
3446
3205
  };
3447
3206
  var matchesFilters = (entry, options = {}) => {
@@ -3463,6 +3222,9 @@ var matchesFilters = (entry, options = {}) => {
3463
3222
  if (options.client && entry.client !== options.client) {
3464
3223
  return false;
3465
3224
  }
3225
+ if (options.provider && entry.provider !== options.provider) {
3226
+ return false;
3227
+ }
3466
3228
  return true;
3467
3229
  };
3468
3230
  var createIndexedDBUsageTrackingDriver = (options = {}) => {
@@ -3594,393 +3356,8 @@ var createIndexedDBUsageTrackingDriver = (options = {}) => {
3594
3356
  };
3595
3357
  };
3596
3358
 
3597
- // storage/usageTracking/sqlite.ts
3598
- var MIGRATION_MARKER_KEY2 = "usage_tracking_migration_v1";
3599
- var normalizeBaseUrl2 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
3600
- var isBun2 = () => {
3601
- return typeof process.versions.bun !== "undefined";
3602
- };
3603
- var cachedDbModule2 = null;
3604
- var loadDatabase2 = async (dbPath) => {
3605
- if (isBun2()) {
3606
- throw new Error(
3607
- "SQLite driver not supported in Bun. Use createMemoryDriver() instead."
3608
- );
3609
- }
3610
- try {
3611
- if (!cachedDbModule2) {
3612
- cachedDbModule2 = (await import('better-sqlite3')).default;
3613
- }
3614
- return new cachedDbModule2(dbPath);
3615
- } catch (error) {
3616
- throw new Error(
3617
- `better-sqlite3 is required for sqlite usage tracking. Install it to use sqlite storage. (${error})`
3618
- );
3619
- }
3620
- };
3621
- var buildWhereClause = (options = {}) => {
3622
- const clauses = [];
3623
- const params = [];
3624
- if (typeof options.before === "number") {
3625
- clauses.push("timestamp < ?");
3626
- params.push(options.before);
3627
- }
3628
- if (typeof options.after === "number") {
3629
- clauses.push("timestamp > ?");
3630
- params.push(options.after);
3631
- }
3632
- if (options.modelId) {
3633
- clauses.push("model_id = ?");
3634
- params.push(options.modelId);
3635
- }
3636
- if (options.baseUrl) {
3637
- clauses.push("base_url = ?");
3638
- params.push(normalizeBaseUrl2(options.baseUrl));
3639
- }
3640
- if (options.sessionId) {
3641
- clauses.push("session_id = ?");
3642
- params.push(options.sessionId);
3643
- }
3644
- if (options.client) {
3645
- clauses.push("client = ?");
3646
- params.push(options.client);
3647
- }
3648
- return {
3649
- sql: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
3650
- params
3651
- };
3652
- };
3653
- var createSqliteUsageTrackingDriver = (options = {}) => {
3654
- const dbPath = options.dbPath || "routstr.sqlite";
3655
- const tableName = options.tableName || "usage_tracking";
3656
- const legacyStorageDriver = options.legacyStorageDriver;
3657
- let db;
3658
- let insertStmt;
3659
- let migrationComplete = false;
3660
- const initDb = async () => {
3661
- if (!db) {
3662
- db = await loadDatabase2(dbPath);
3663
- db.exec(`
3664
- CREATE TABLE IF NOT EXISTS ${tableName} (
3665
- id TEXT PRIMARY KEY,
3666
- timestamp INTEGER NOT NULL,
3667
- model_id TEXT NOT NULL,
3668
- base_url TEXT NOT NULL,
3669
- request_id TEXT NOT NULL,
3670
- cost REAL NOT NULL,
3671
- sats_cost REAL NOT NULL,
3672
- prompt_tokens INTEGER NOT NULL,
3673
- completion_tokens INTEGER NOT NULL,
3674
- total_tokens INTEGER NOT NULL,
3675
- client TEXT,
3676
- session_id TEXT,
3677
- tags TEXT
3678
- );
3679
- CREATE INDEX IF NOT EXISTS idx_${tableName}_timestamp ON ${tableName}(timestamp);
3680
- CREATE INDEX IF NOT EXISTS idx_${tableName}_model_id ON ${tableName}(model_id);
3681
- CREATE INDEX IF NOT EXISTS idx_${tableName}_base_url ON ${tableName}(base_url);
3682
- CREATE INDEX IF NOT EXISTS idx_${tableName}_session_id ON ${tableName}(session_id);
3683
- CREATE INDEX IF NOT EXISTS idx_${tableName}_client ON ${tableName}(client);
3684
- `);
3685
- insertStmt = db.prepare(`
3686
- INSERT OR REPLACE INTO ${tableName} (
3687
- id, timestamp, model_id, base_url, request_id,
3688
- cost, sats_cost, prompt_tokens, completion_tokens, total_tokens,
3689
- client, session_id, tags
3690
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3691
- `);
3692
- }
3693
- };
3694
- const ensureInit = async () => {
3695
- if (!db) {
3696
- await initDb();
3697
- }
3698
- };
3699
- const appendOne = (entry) => {
3700
- insertStmt.run(
3701
- entry.id,
3702
- entry.timestamp,
3703
- entry.modelId,
3704
- normalizeBaseUrl2(entry.baseUrl),
3705
- entry.requestId,
3706
- entry.cost,
3707
- entry.satsCost,
3708
- entry.promptTokens,
3709
- entry.completionTokens,
3710
- entry.totalTokens,
3711
- entry.client ?? null,
3712
- entry.sessionId ?? null,
3713
- JSON.stringify(entry.tags ?? [])
3714
- );
3715
- };
3716
- const ensureMigrated = async () => {
3717
- if (!legacyStorageDriver || migrationComplete) return;
3718
- const migrated = await legacyStorageDriver.getItem(
3719
- MIGRATION_MARKER_KEY2,
3720
- false
3721
- );
3722
- if (migrated) {
3723
- migrationComplete = true;
3724
- return;
3725
- }
3726
- const legacyEntries = await legacyStorageDriver.getItem(
3727
- SDK_STORAGE_KEYS.USAGE_TRACKING,
3728
- []
3729
- );
3730
- for (const entry of legacyEntries) {
3731
- appendOne(entry);
3732
- }
3733
- if (legacyEntries.length > 0) {
3734
- await legacyStorageDriver.removeItem(SDK_STORAGE_KEYS.USAGE_TRACKING);
3735
- }
3736
- await legacyStorageDriver.setItem(MIGRATION_MARKER_KEY2, true);
3737
- migrationComplete = true;
3738
- };
3739
- const mapRow = (row) => ({
3740
- id: row.id,
3741
- timestamp: row.timestamp,
3742
- modelId: row.model_id,
3743
- baseUrl: row.base_url,
3744
- requestId: row.request_id,
3745
- cost: row.cost,
3746
- satsCost: row.sats_cost,
3747
- promptTokens: row.prompt_tokens,
3748
- completionTokens: row.completion_tokens,
3749
- totalTokens: row.total_tokens,
3750
- client: row.client ?? void 0,
3751
- sessionId: row.session_id ?? void 0,
3752
- tags: typeof row.tags === "string" ? JSON.parse(row.tags) : void 0
3753
- });
3754
- return {
3755
- async migrate() {
3756
- await ensureInit();
3757
- await ensureMigrated();
3758
- },
3759
- async append(entry) {
3760
- await ensureInit();
3761
- await ensureMigrated();
3762
- appendOne(entry);
3763
- },
3764
- async appendMany(entries) {
3765
- await ensureInit();
3766
- await ensureMigrated();
3767
- for (const entry of entries) {
3768
- appendOne(entry);
3769
- }
3770
- },
3771
- async list(options2 = {}) {
3772
- await ensureInit();
3773
- await ensureMigrated();
3774
- const { sql, params } = buildWhereClause(options2);
3775
- const limitSql = typeof options2.limit === "number" ? " LIMIT ?" : "";
3776
- const stmt = db.prepare(
3777
- `SELECT * FROM ${tableName} ${sql} ORDER BY timestamp DESC${limitSql}`
3778
- );
3779
- const rows = stmt.all(
3780
- ...typeof options2.limit === "number" ? [...params, options2.limit] : params
3781
- );
3782
- return rows.map(mapRow);
3783
- },
3784
- async count(options2 = {}) {
3785
- await ensureInit();
3786
- await ensureMigrated();
3787
- const { sql, params } = buildWhereClause(options2);
3788
- const stmt = db.prepare(`SELECT COUNT(*) as count FROM ${tableName} ${sql}`);
3789
- const row = stmt.get(...params);
3790
- return Number(row?.count ?? 0);
3791
- },
3792
- async deleteOlderThan(timestamp) {
3793
- await ensureInit();
3794
- await ensureMigrated();
3795
- const stmt = db.prepare(`DELETE FROM ${tableName} WHERE timestamp < ?`);
3796
- const result = stmt.run(timestamp);
3797
- return result.changes;
3798
- },
3799
- async clear() {
3800
- await ensureInit();
3801
- await ensureMigrated();
3802
- db.prepare(`DELETE FROM ${tableName}`).run();
3803
- }
3804
- };
3805
- };
3806
-
3807
- // storage/usageTracking/bunSqlite.ts
3808
- var MIGRATION_MARKER_KEY3 = "usage_tracking_migration_v1";
3809
- var normalizeBaseUrl3 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
3810
- var buildWhereClause2 = (options = {}) => {
3811
- const clauses = [];
3812
- const params = [];
3813
- if (typeof options.before === "number") {
3814
- clauses.push("timestamp < ?");
3815
- params.push(options.before);
3816
- }
3817
- if (typeof options.after === "number") {
3818
- clauses.push("timestamp > ?");
3819
- params.push(options.after);
3820
- }
3821
- if (options.modelId) {
3822
- clauses.push("model_id = ?");
3823
- params.push(options.modelId);
3824
- }
3825
- if (options.baseUrl) {
3826
- clauses.push("base_url = ?");
3827
- params.push(normalizeBaseUrl3(options.baseUrl));
3828
- }
3829
- if (options.sessionId) {
3830
- clauses.push("session_id = ?");
3831
- params.push(options.sessionId);
3832
- }
3833
- if (options.client) {
3834
- clauses.push("client = ?");
3835
- params.push(options.client);
3836
- }
3837
- return {
3838
- sql: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
3839
- params
3840
- };
3841
- };
3842
- var createBunSqliteUsageTrackingDriver = (options = {}) => {
3843
- const dbPath = options.dbPath || "routstr.sqlite";
3844
- const tableName = options.tableName || "usage_tracking";
3845
- const legacyStorageDriver = options.legacyStorageDriver;
3846
- const SQLiteDatabase = options.sqlite?.Database;
3847
- let migrationPromise = null;
3848
- if (!SQLiteDatabase) {
3849
- throw new Error(
3850
- "Bun SQLite Database constructor is required. Pass { sqlite: { Database } } when creating the driver."
3851
- );
3852
- }
3853
- const db = new SQLiteDatabase(dbPath);
3854
- db.run(`
3855
- CREATE TABLE IF NOT EXISTS ${tableName} (
3856
- id TEXT PRIMARY KEY,
3857
- timestamp INTEGER NOT NULL,
3858
- model_id TEXT NOT NULL,
3859
- base_url TEXT NOT NULL,
3860
- request_id TEXT NOT NULL,
3861
- cost REAL NOT NULL,
3862
- sats_cost REAL NOT NULL,
3863
- prompt_tokens INTEGER NOT NULL,
3864
- completion_tokens INTEGER NOT NULL,
3865
- total_tokens INTEGER NOT NULL,
3866
- client TEXT,
3867
- session_id TEXT,
3868
- tags TEXT
3869
- )
3870
- `);
3871
- db.run(`CREATE INDEX IF NOT EXISTS idx_${tableName}_timestamp ON ${tableName}(timestamp)`);
3872
- db.run(`CREATE INDEX IF NOT EXISTS idx_${tableName}_model_id ON ${tableName}(model_id)`);
3873
- db.run(`CREATE INDEX IF NOT EXISTS idx_${tableName}_base_url ON ${tableName}(base_url)`);
3874
- const appendOne = (entry) => {
3875
- db.query(`
3876
- INSERT OR REPLACE INTO ${tableName} (
3877
- id, timestamp, model_id, base_url, request_id,
3878
- cost, sats_cost, prompt_tokens, completion_tokens, total_tokens,
3879
- client, session_id, tags
3880
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3881
- `).run(
3882
- entry.id,
3883
- entry.timestamp,
3884
- entry.modelId,
3885
- normalizeBaseUrl3(entry.baseUrl),
3886
- entry.requestId,
3887
- entry.cost,
3888
- entry.satsCost,
3889
- entry.promptTokens,
3890
- entry.completionTokens,
3891
- entry.totalTokens,
3892
- entry.client ?? null,
3893
- entry.sessionId ?? null,
3894
- JSON.stringify(entry.tags ?? [])
3895
- );
3896
- };
3897
- const mapRow = (row) => ({
3898
- id: row.id,
3899
- timestamp: row.timestamp,
3900
- modelId: row.model_id,
3901
- baseUrl: row.base_url,
3902
- requestId: row.request_id,
3903
- cost: row.cost,
3904
- satsCost: row.sats_cost,
3905
- promptTokens: row.prompt_tokens,
3906
- completionTokens: row.completion_tokens,
3907
- totalTokens: row.total_tokens,
3908
- client: row.client ?? void 0,
3909
- sessionId: row.session_id ?? void 0,
3910
- tags: typeof row.tags === "string" ? JSON.parse(row.tags) : void 0
3911
- });
3912
- const ensureMigrated = async () => {
3913
- if (!legacyStorageDriver) return;
3914
- if (!migrationPromise) {
3915
- migrationPromise = (async () => {
3916
- const migrated = await legacyStorageDriver.getItem(
3917
- MIGRATION_MARKER_KEY3,
3918
- false
3919
- );
3920
- if (migrated) return;
3921
- const legacyEntries = await legacyStorageDriver.getItem(
3922
- SDK_STORAGE_KEYS.USAGE_TRACKING,
3923
- []
3924
- );
3925
- if (legacyEntries.length > 0) {
3926
- for (const entry of legacyEntries) {
3927
- appendOne(entry);
3928
- }
3929
- await legacyStorageDriver.removeItem(SDK_STORAGE_KEYS.USAGE_TRACKING);
3930
- }
3931
- await legacyStorageDriver.setItem(MIGRATION_MARKER_KEY3, true);
3932
- })();
3933
- }
3934
- await migrationPromise;
3935
- };
3936
- return {
3937
- async migrate() {
3938
- await ensureMigrated();
3939
- },
3940
- async append(entry) {
3941
- await ensureMigrated();
3942
- appendOne(entry);
3943
- },
3944
- async appendMany(entries) {
3945
- await ensureMigrated();
3946
- for (const entry of entries) {
3947
- appendOne(entry);
3948
- }
3949
- },
3950
- async list(options2 = {}) {
3951
- await ensureMigrated();
3952
- const { sql, params } = buildWhereClause2(options2);
3953
- const limitSql = typeof options2.limit === "number" ? " LIMIT ?" : "";
3954
- const query = `SELECT * FROM ${tableName} ${sql} ORDER BY timestamp DESC${limitSql}`;
3955
- let rows;
3956
- if (typeof options2.limit === "number") {
3957
- rows = db.query(query).all(...params, options2.limit);
3958
- } else {
3959
- rows = db.query(query).all(...params);
3960
- }
3961
- return rows.map(mapRow);
3962
- },
3963
- async count(options2 = {}) {
3964
- const { sql, params } = buildWhereClause2(options2);
3965
- const query = `SELECT COUNT(*) as count FROM ${tableName} ${sql}`;
3966
- const row = db.query(query).get(...params);
3967
- return Number(row?.count ?? 0);
3968
- },
3969
- async deleteOlderThan(timestamp) {
3970
- await ensureMigrated();
3971
- const before = timestamp;
3972
- const result = db.query(`DELETE FROM ${tableName} WHERE timestamp < ?`).run(before);
3973
- return result.changes ?? 0;
3974
- },
3975
- async clear() {
3976
- await ensureMigrated();
3977
- db.query(`DELETE FROM ${tableName}`).run();
3978
- }
3979
- };
3980
- };
3981
-
3982
3359
  // storage/usageTracking/memory.ts
3983
- var normalizeBaseUrl4 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
3360
+ var normalizeBaseUrl2 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
3984
3361
  var matchesFilters2 = (entry, options = {}) => {
3985
3362
  if (typeof options.before === "number" && entry.timestamp >= options.before) {
3986
3363
  return false;
@@ -3991,7 +3368,7 @@ var matchesFilters2 = (entry, options = {}) => {
3991
3368
  if (options.modelId && entry.modelId !== options.modelId) {
3992
3369
  return false;
3993
3370
  }
3994
- if (options.baseUrl && normalizeBaseUrl4(entry.baseUrl) !== normalizeBaseUrl4(options.baseUrl)) {
3371
+ if (options.baseUrl && normalizeBaseUrl2(entry.baseUrl) !== normalizeBaseUrl2(options.baseUrl)) {
3995
3372
  return false;
3996
3373
  }
3997
3374
  if (options.sessionId && entry.sessionId !== options.sessionId) {
@@ -4000,23 +3377,26 @@ var matchesFilters2 = (entry, options = {}) => {
4000
3377
  if (options.client && entry.client !== options.client) {
4001
3378
  return false;
4002
3379
  }
3380
+ if (options.provider && entry.provider !== options.provider) {
3381
+ return false;
3382
+ }
4003
3383
  return true;
4004
3384
  };
4005
3385
  var createMemoryUsageTrackingDriver = (seed = []) => {
4006
3386
  const store = /* @__PURE__ */ new Map();
4007
3387
  for (const entry of seed) {
4008
- store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl4(entry.baseUrl) });
3388
+ store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl2(entry.baseUrl) });
4009
3389
  }
4010
3390
  return {
4011
3391
  async migrate() {
4012
3392
  return;
4013
3393
  },
4014
3394
  async append(entry) {
4015
- store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl4(entry.baseUrl) });
3395
+ store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl2(entry.baseUrl) });
4016
3396
  },
4017
3397
  async appendMany(entries) {
4018
3398
  for (const entry of entries) {
4019
- store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl4(entry.baseUrl) });
3399
+ store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl2(entry.baseUrl) });
4020
3400
  }
4021
3401
  },
4022
3402
  async list(options = {}) {
@@ -4044,7 +3424,7 @@ var createMemoryUsageTrackingDriver = (seed = []) => {
4044
3424
  }
4045
3425
  };
4046
3426
  };
4047
- var normalizeBaseUrl5 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
3427
+ var normalizeBaseUrl3 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
4048
3428
  var createEmptyStore = (driver) => createStore((set, get) => ({
4049
3429
  modelsFromAllProviders: {},
4050
3430
  lastUsedModel: null,
@@ -4067,7 +3447,7 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
4067
3447
  setModelsFromAllProviders: (value) => {
4068
3448
  const normalized = {};
4069
3449
  for (const [baseUrl, models] of Object.entries(value)) {
4070
- normalized[normalizeBaseUrl5(baseUrl)] = models;
3450
+ normalized[normalizeBaseUrl3(baseUrl)] = models;
4071
3451
  }
4072
3452
  void driver.setItem(
4073
3453
  SDK_STORAGE_KEYS.MODELS_FROM_ALL_PROVIDERS,
@@ -4080,7 +3460,7 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
4080
3460
  set({ lastUsedModel: value });
4081
3461
  },
4082
3462
  setBaseUrlsList: (value) => {
4083
- const normalized = value.map((url) => normalizeBaseUrl5(url));
3463
+ const normalized = value.map((url) => normalizeBaseUrl3(url));
4084
3464
  void driver.setItem(SDK_STORAGE_KEYS.BASE_URLS_LIST, normalized);
4085
3465
  set({ baseUrlsList: normalized });
4086
3466
  },
@@ -4089,14 +3469,14 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
4089
3469
  set({ lastBaseUrlsUpdate: value });
4090
3470
  },
4091
3471
  setDisabledProviders: (value) => {
4092
- const normalized = value.map((url) => normalizeBaseUrl5(url));
3472
+ const normalized = value.map((url) => normalizeBaseUrl3(url));
4093
3473
  void driver.setItem(SDK_STORAGE_KEYS.DISABLED_PROVIDERS, normalized);
4094
3474
  set({ disabledProviders: normalized });
4095
3475
  },
4096
3476
  setMintsFromAllProviders: (value) => {
4097
3477
  const normalized = {};
4098
3478
  for (const [baseUrl, mints] of Object.entries(value)) {
4099
- normalized[normalizeBaseUrl5(baseUrl)] = mints.map(
3479
+ normalized[normalizeBaseUrl3(baseUrl)] = mints.map(
4100
3480
  (mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint
4101
3481
  );
4102
3482
  }
@@ -4109,7 +3489,7 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
4109
3489
  setInfoFromAllProviders: (value) => {
4110
3490
  const normalized = {};
4111
3491
  for (const [baseUrl, info] of Object.entries(value)) {
4112
- normalized[normalizeBaseUrl5(baseUrl)] = info;
3492
+ normalized[normalizeBaseUrl3(baseUrl)] = info;
4113
3493
  }
4114
3494
  void driver.setItem(SDK_STORAGE_KEYS.INFO_FROM_ALL_PROVIDERS, normalized);
4115
3495
  set({ infoFromAllProviders: normalized });
@@ -4117,7 +3497,7 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
4117
3497
  setLastModelsUpdate: (value) => {
4118
3498
  const normalized = {};
4119
3499
  for (const [baseUrl, timestamp] of Object.entries(value)) {
4120
- normalized[normalizeBaseUrl5(baseUrl)] = timestamp;
3500
+ normalized[normalizeBaseUrl3(baseUrl)] = timestamp;
4121
3501
  }
4122
3502
  void driver.setItem(SDK_STORAGE_KEYS.LAST_MODELS_UPDATE, normalized);
4123
3503
  set({ lastModelsUpdate: normalized });
@@ -4127,7 +3507,7 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
4127
3507
  const updates = typeof value === "function" ? value(state.apiKeys) : value;
4128
3508
  const normalized = updates.map((entry) => ({
4129
3509
  ...entry,
4130
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
3510
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
4131
3511
  balance: entry.balance ?? 0,
4132
3512
  lastUsed: entry.lastUsed ?? null
4133
3513
  }));
@@ -4139,7 +3519,7 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
4139
3519
  set((state) => {
4140
3520
  const updates = typeof value === "function" ? value(state.childKeys) : value;
4141
3521
  const normalized = updates.map((entry) => ({
4142
- parentBaseUrl: normalizeBaseUrl5(entry.parentBaseUrl),
3522
+ parentBaseUrl: normalizeBaseUrl3(entry.parentBaseUrl),
4143
3523
  childKey: entry.childKey,
4144
3524
  balance: entry.balance ?? 0,
4145
3525
  balanceLimit: entry.balanceLimit,
@@ -4153,9 +3533,9 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
4153
3533
  setXcashuTokens: (value) => {
4154
3534
  const normalized = {};
4155
3535
  for (const [baseUrl, tokens] of Object.entries(value)) {
4156
- normalized[normalizeBaseUrl5(baseUrl)] = tokens.map((entry) => ({
3536
+ normalized[normalizeBaseUrl3(baseUrl)] = tokens.map((entry) => ({
4157
3537
  ...entry,
4158
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
3538
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
4159
3539
  createdAt: entry.createdAt ?? Date.now(),
4160
3540
  tryCount: entry.tryCount ?? 0
4161
3541
  }));
@@ -4206,12 +3586,12 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
4206
3586
  },
4207
3587
  // ========== Failure Tracking ==========
4208
3588
  setFailedProviders: (value) => {
4209
- const normalized = value.map((url) => normalizeBaseUrl5(url));
3589
+ const normalized = value.map((url) => normalizeBaseUrl3(url));
4210
3590
  void driver.setItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, normalized);
4211
3591
  set({ failedProviders: normalized });
4212
3592
  },
4213
3593
  addFailedProvider: (baseUrl) => {
4214
- const normalized = normalizeBaseUrl5(baseUrl);
3594
+ const normalized = normalizeBaseUrl3(baseUrl);
4215
3595
  const current = get().failedProviders;
4216
3596
  if (!current.includes(normalized)) {
4217
3597
  const updated = [...current, normalized];
@@ -4220,7 +3600,7 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
4220
3600
  }
4221
3601
  },
4222
3602
  removeFailedProvider: (baseUrl) => {
4223
- const normalized = normalizeBaseUrl5(baseUrl);
3603
+ const normalized = normalizeBaseUrl3(baseUrl);
4224
3604
  const current = get().failedProviders;
4225
3605
  const updated = current.filter((url) => url !== normalized);
4226
3606
  void driver.setItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, updated);
@@ -4229,13 +3609,13 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
4229
3609
  setLastFailed: (value) => {
4230
3610
  const normalized = {};
4231
3611
  for (const [baseUrl, timestamp] of Object.entries(value)) {
4232
- normalized[normalizeBaseUrl5(baseUrl)] = timestamp;
3612
+ normalized[normalizeBaseUrl3(baseUrl)] = timestamp;
4233
3613
  }
4234
3614
  void driver.setItem(SDK_STORAGE_KEYS.LAST_FAILED, normalized);
4235
3615
  set({ lastFailed: normalized });
4236
3616
  },
4237
3617
  setLastFailedTimestamp: (baseUrl, timestamp) => {
4238
- const normalized = normalizeBaseUrl5(baseUrl);
3618
+ const normalized = normalizeBaseUrl3(baseUrl);
4239
3619
  const current = get().lastFailed;
4240
3620
  const updated = { ...current, [normalized]: timestamp };
4241
3621
  void driver.setItem(SDK_STORAGE_KEYS.LAST_FAILED, updated);
@@ -4243,14 +3623,14 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
4243
3623
  },
4244
3624
  setProvidersOnCooldown: (value) => {
4245
3625
  const normalized = value.map((entry) => ({
4246
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
3626
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
4247
3627
  timestamp: entry.timestamp
4248
3628
  }));
4249
3629
  void driver.setItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, normalized);
4250
3630
  set({ providersOnCooldown: normalized });
4251
3631
  },
4252
3632
  addProviderOnCooldown: (baseUrl, timestamp) => {
4253
- const normalized = normalizeBaseUrl5(baseUrl);
3633
+ const normalized = normalizeBaseUrl3(baseUrl);
4254
3634
  const current = get().providersOnCooldown;
4255
3635
  if (!current.some((entry) => entry.baseUrl === normalized)) {
4256
3636
  const updated = [...current, { baseUrl: normalized, timestamp }];
@@ -4259,7 +3639,7 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
4259
3639
  }
4260
3640
  },
4261
3641
  removeProviderFromCooldown: (baseUrl) => {
4262
- const normalized = normalizeBaseUrl5(baseUrl);
3642
+ const normalized = normalizeBaseUrl3(baseUrl);
4263
3643
  const current = get().providersOnCooldown;
4264
3644
  const updated = current.filter((entry) => entry.baseUrl !== normalized);
4265
3645
  void driver.setItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, updated);
@@ -4330,40 +3710,40 @@ var hydrateStoreFromDriver = async (store, driver) => {
4330
3710
  ]);
4331
3711
  const modelsFromAllProviders = Object.fromEntries(
4332
3712
  Object.entries(rawModels).map(([baseUrl, models]) => [
4333
- normalizeBaseUrl5(baseUrl),
3713
+ normalizeBaseUrl3(baseUrl),
4334
3714
  models
4335
3715
  ])
4336
3716
  );
4337
- const baseUrlsList = rawBaseUrls.map((url) => normalizeBaseUrl5(url));
3717
+ const baseUrlsList = rawBaseUrls.map((url) => normalizeBaseUrl3(url));
4338
3718
  const disabledProviders = rawDisabledProviders.map(
4339
- (url) => normalizeBaseUrl5(url)
3719
+ (url) => normalizeBaseUrl3(url)
4340
3720
  );
4341
3721
  const mintsFromAllProviders = Object.fromEntries(
4342
3722
  Object.entries(rawMints).map(([baseUrl, mints]) => [
4343
- normalizeBaseUrl5(baseUrl),
3723
+ normalizeBaseUrl3(baseUrl),
4344
3724
  mints.map((mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint)
4345
3725
  ])
4346
3726
  );
4347
3727
  const infoFromAllProviders = Object.fromEntries(
4348
3728
  Object.entries(rawInfo).map(([baseUrl, info]) => [
4349
- normalizeBaseUrl5(baseUrl),
3729
+ normalizeBaseUrl3(baseUrl),
4350
3730
  info
4351
3731
  ])
4352
3732
  );
4353
3733
  const lastModelsUpdate = Object.fromEntries(
4354
3734
  Object.entries(rawLastModelsUpdate).map(([baseUrl, timestamp]) => [
4355
- normalizeBaseUrl5(baseUrl),
3735
+ normalizeBaseUrl3(baseUrl),
4356
3736
  timestamp
4357
3737
  ])
4358
3738
  );
4359
3739
  const apiKeys = rawApiKeys.map((entry) => ({
4360
3740
  ...entry,
4361
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
3741
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
4362
3742
  balance: entry.balance ?? 0,
4363
3743
  lastUsed: entry.lastUsed ?? null
4364
3744
  }));
4365
3745
  const childKeys = rawChildKeys.map((entry) => ({
4366
- parentBaseUrl: normalizeBaseUrl5(entry.parentBaseUrl),
3746
+ parentBaseUrl: normalizeBaseUrl3(entry.parentBaseUrl),
4367
3747
  childKey: entry.childKey,
4368
3748
  balance: entry.balance ?? 0,
4369
3749
  balanceLimit: entry.balanceLimit,
@@ -4372,9 +3752,9 @@ var hydrateStoreFromDriver = async (store, driver) => {
4372
3752
  }));
4373
3753
  const xcashuTokens = Object.fromEntries(
4374
3754
  Object.entries(rawXcashuTokens).map(([baseUrl, tokens]) => [
4375
- normalizeBaseUrl5(baseUrl),
3755
+ normalizeBaseUrl3(baseUrl),
4376
3756
  tokens.map((entry) => ({
4377
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
3757
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
4378
3758
  token: entry.token,
4379
3759
  createdAt: entry.createdAt ?? Date.now(),
4380
3760
  tryCount: entry.tryCount ?? 0
@@ -4395,16 +3775,16 @@ var hydrateStoreFromDriver = async (store, driver) => {
4395
3775
  lastUsed: entry.lastUsed ?? null
4396
3776
  }));
4397
3777
  const failedProviders = rawFailedProviders.map(
4398
- (url) => normalizeBaseUrl5(url)
3778
+ (url) => normalizeBaseUrl3(url)
4399
3779
  );
4400
3780
  const lastFailed = Object.fromEntries(
4401
3781
  Object.entries(rawLastFailed).map(([baseUrl, timestamp]) => [
4402
- normalizeBaseUrl5(baseUrl),
3782
+ normalizeBaseUrl3(baseUrl),
4403
3783
  timestamp
4404
3784
  ])
4405
3785
  );
4406
3786
  const providersOnCooldown = rawProvidersOnCooldown.map((entry) => ({
4407
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
3787
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
4408
3788
  timestamp: entry.timestamp
4409
3789
  }));
4410
3790
  store.setState({
@@ -4445,12 +3825,12 @@ var createDiscoveryAdapterFromStore = (store) => ({
4445
3825
  getCachedProviderInfo: () => store.getState().infoFromAllProviders,
4446
3826
  setCachedProviderInfo: (info) => store.getState().setInfoFromAllProviders(info),
4447
3827
  getProviderLastUpdate: (baseUrl) => {
4448
- const normalized = normalizeBaseUrl5(baseUrl);
3828
+ const normalized = normalizeBaseUrl3(baseUrl);
4449
3829
  const timestamps = store.getState().lastModelsUpdate;
4450
3830
  return timestamps[normalized] || null;
4451
3831
  },
4452
3832
  setProviderLastUpdate: (baseUrl, timestamp) => {
4453
- const normalized = normalizeBaseUrl5(baseUrl);
3833
+ const normalized = normalizeBaseUrl3(baseUrl);
4454
3834
  const timestamps = { ...store.getState().lastModelsUpdate };
4455
3835
  timestamps[normalized] = timestamp;
4456
3836
  store.getState().setLastModelsUpdate(timestamps);
@@ -4479,24 +3859,24 @@ var createStorageAdapterFromStore = (store) => ({
4479
3859
  return Object.entries(distributionMap).map(([baseUrl, amt]) => ({ baseUrl, amount: amt })).sort((a, b) => b.amount - a.amount);
4480
3860
  },
4481
3861
  saveProviderInfo: (baseUrl, info) => {
4482
- const normalized = normalizeBaseUrl5(baseUrl);
3862
+ const normalized = normalizeBaseUrl3(baseUrl);
4483
3863
  const next = { ...store.getState().infoFromAllProviders };
4484
3864
  next[normalized] = info;
4485
3865
  store.getState().setInfoFromAllProviders(next);
4486
3866
  },
4487
3867
  getProviderInfo: (baseUrl) => {
4488
- const normalized = normalizeBaseUrl5(baseUrl);
3868
+ const normalized = normalizeBaseUrl3(baseUrl);
4489
3869
  return store.getState().infoFromAllProviders[normalized] || null;
4490
3870
  },
4491
3871
  // ========== API Keys (for apikeys mode) ==========
4492
3872
  getApiKey: (baseUrl) => {
4493
- const normalized = normalizeBaseUrl5(baseUrl);
3873
+ const normalized = normalizeBaseUrl3(baseUrl);
4494
3874
  const entry = store.getState().apiKeys.find((key) => key.baseUrl === normalized);
4495
3875
  if (!entry) return null;
4496
3876
  return entry;
4497
3877
  },
4498
3878
  setApiKey: (baseUrl, key) => {
4499
- const normalized = normalizeBaseUrl5(baseUrl);
3879
+ const normalized = normalizeBaseUrl3(baseUrl);
4500
3880
  const keys = store.getState().apiKeys;
4501
3881
  const existingIndex = keys.findIndex(
4502
3882
  (entry) => entry.baseUrl === normalized
@@ -4514,7 +3894,7 @@ var createStorageAdapterFromStore = (store) => ({
4514
3894
  store.getState().setApiKeys(next);
4515
3895
  },
4516
3896
  updateApiKeyBalance: (baseUrl, balance) => {
4517
- const normalized = normalizeBaseUrl5(baseUrl);
3897
+ const normalized = normalizeBaseUrl3(baseUrl);
4518
3898
  const keys = store.getState().apiKeys;
4519
3899
  const next = keys.map(
4520
3900
  (entry) => entry.baseUrl === normalized ? { ...entry, balance, lastUsed: Date.now() } : entry
@@ -4522,7 +3902,7 @@ var createStorageAdapterFromStore = (store) => ({
4522
3902
  store.getState().setApiKeys(next);
4523
3903
  },
4524
3904
  removeApiKey: (baseUrl) => {
4525
- const normalized = normalizeBaseUrl5(baseUrl);
3905
+ const normalized = normalizeBaseUrl3(baseUrl);
4526
3906
  const next = store.getState().apiKeys.filter((entry) => entry.baseUrl !== normalized);
4527
3907
  store.getState().setApiKeys(next);
4528
3908
  },
@@ -4536,7 +3916,7 @@ var createStorageAdapterFromStore = (store) => ({
4536
3916
  },
4537
3917
  // ========== Child Keys ==========
4538
3918
  getChildKey: (parentBaseUrl) => {
4539
- const normalized = normalizeBaseUrl5(parentBaseUrl);
3919
+ const normalized = normalizeBaseUrl3(parentBaseUrl);
4540
3920
  const entry = store.getState().childKeys.find((key) => key.parentBaseUrl === normalized);
4541
3921
  if (!entry) return null;
4542
3922
  return {
@@ -4549,7 +3929,7 @@ var createStorageAdapterFromStore = (store) => ({
4549
3929
  };
4550
3930
  },
4551
3931
  setChildKey: (parentBaseUrl, childKey, balance, validityDate, balanceLimit) => {
4552
- const normalized = normalizeBaseUrl5(parentBaseUrl);
3932
+ const normalized = normalizeBaseUrl3(parentBaseUrl);
4553
3933
  const keys = store.getState().childKeys;
4554
3934
  const existingIndex = keys.findIndex(
4555
3935
  (entry) => entry.parentBaseUrl === normalized
@@ -4580,7 +3960,7 @@ var createStorageAdapterFromStore = (store) => ({
4580
3960
  }
4581
3961
  },
4582
3962
  updateChildKeyBalance: (parentBaseUrl, balance) => {
4583
- const normalized = normalizeBaseUrl5(parentBaseUrl);
3963
+ const normalized = normalizeBaseUrl3(parentBaseUrl);
4584
3964
  const keys = store.getState().childKeys;
4585
3965
  const next = keys.map(
4586
3966
  (entry) => entry.parentBaseUrl === normalized ? { ...entry, balance } : entry
@@ -4588,7 +3968,7 @@ var createStorageAdapterFromStore = (store) => ({
4588
3968
  store.getState().setChildKeys(next);
4589
3969
  },
4590
3970
  removeChildKey: (parentBaseUrl) => {
4591
- const normalized = normalizeBaseUrl5(parentBaseUrl);
3971
+ const normalized = normalizeBaseUrl3(parentBaseUrl);
4592
3972
  const next = store.getState().childKeys.filter((entry) => entry.parentBaseUrl !== normalized);
4593
3973
  store.getState().setChildKeys(next);
4594
3974
  },
@@ -4613,11 +3993,11 @@ var createStorageAdapterFromStore = (store) => ({
4613
3993
  return store.getState().xcashuTokens;
4614
3994
  },
4615
3995
  getXcashuTokensForBaseUrl: (baseUrl) => {
4616
- const normalized = normalizeBaseUrl5(baseUrl);
3996
+ const normalized = normalizeBaseUrl3(baseUrl);
4617
3997
  return store.getState().xcashuTokens[normalized] || [];
4618
3998
  },
4619
3999
  addXcashuToken: (baseUrl, token) => {
4620
- const normalized = normalizeBaseUrl5(baseUrl);
4000
+ const normalized = normalizeBaseUrl3(baseUrl);
4621
4001
  const tokens = store.getState().xcashuTokens;
4622
4002
  const existing = tokens[normalized] || [];
4623
4003
  const next = { ...tokens };
@@ -4628,7 +4008,7 @@ var createStorageAdapterFromStore = (store) => ({
4628
4008
  store.getState().setXcashuTokens(next);
4629
4009
  },
4630
4010
  removeXcashuToken: (baseUrl, token) => {
4631
- const normalized = normalizeBaseUrl5(baseUrl);
4011
+ const normalized = normalizeBaseUrl3(baseUrl);
4632
4012
  const tokens = store.getState().xcashuTokens;
4633
4013
  const existing = tokens[normalized] || [];
4634
4014
  const next = { ...tokens };
@@ -4639,7 +4019,7 @@ var createStorageAdapterFromStore = (store) => ({
4639
4019
  store.getState().setXcashuTokens(next);
4640
4020
  },
4641
4021
  clearXcashuTokensForBaseUrl: (baseUrl) => {
4642
- const normalized = normalizeBaseUrl5(baseUrl);
4022
+ const normalized = normalizeBaseUrl3(baseUrl);
4643
4023
  const tokens = store.getState().xcashuTokens;
4644
4024
  const next = { ...tokens };
4645
4025
  delete next[normalized];
@@ -4653,16 +4033,16 @@ var createProviderRegistryFromStore = (store, logger) => {
4653
4033
  const log = (logger ?? consoleLogger).child("ProviderRegistry");
4654
4034
  return {
4655
4035
  getModelsForProvider: (baseUrl) => {
4656
- const normalized = normalizeBaseUrl5(baseUrl);
4036
+ const normalized = normalizeBaseUrl3(baseUrl);
4657
4037
  return store.getState().modelsFromAllProviders[normalized] || [];
4658
4038
  },
4659
4039
  getDisabledProviders: () => store.getState().disabledProviders,
4660
4040
  getProviderMints: (baseUrl) => {
4661
- const normalized = normalizeBaseUrl5(baseUrl);
4041
+ const normalized = normalizeBaseUrl3(baseUrl);
4662
4042
  return store.getState().mintsFromAllProviders[normalized] || [];
4663
4043
  },
4664
4044
  getProviderInfo: async (baseUrl) => {
4665
- const normalized = normalizeBaseUrl5(baseUrl);
4045
+ const normalized = normalizeBaseUrl3(baseUrl);
4666
4046
  const cached = store.getState().infoFromAllProviders[normalized];
4667
4047
  if (cached) return cached;
4668
4048
  try {
@@ -4684,39 +4064,292 @@ var createProviderRegistryFromStore = (store, logger) => {
4684
4064
  };
4685
4065
  };
4686
4066
 
4687
- // storage/index.ts
4688
- var isBrowser3 = () => {
4689
- try {
4690
- return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
4691
- } catch {
4692
- return false;
4067
+ // storage/shardedDiscoveryAdapter.ts
4068
+ var MODEL_KEY_PREFIX = "models:provider:";
4069
+ var MODEL_TS_KEY_PREFIX = "models:provider_timestamp:";
4070
+ var PROVIDER_INDEX_KEY = "models:provider_index";
4071
+ var MIGRATION_MARKER_KEY2 = "models_sharded_migration_v1";
4072
+ var encodeBaseUrl = (baseUrl) => encodeURIComponent(baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
4073
+ var modelKey = (baseUrl) => `${MODEL_KEY_PREFIX}${encodeBaseUrl(baseUrl)}`;
4074
+ var modelTsKey = (baseUrl) => `${MODEL_TS_KEY_PREFIX}${encodeBaseUrl(baseUrl)}`;
4075
+ var normalizeBaseUrl4 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
4076
+ var createShardedDiscoveryAdapter = async (options) => {
4077
+ const { driver } = options;
4078
+ const legacyModels = await driver.getItem(SDK_STORAGE_KEYS.MODELS_FROM_ALL_PROVIDERS, {});
4079
+ const legacyTimestamps = await driver.getItem(SDK_STORAGE_KEYS.LAST_MODELS_UPDATE, {});
4080
+ if (Object.keys(legacyModels).length > 0) {
4081
+ const migratedProviders = [];
4082
+ for (const [baseUrl, models] of Object.entries(legacyModels)) {
4083
+ const normalized = normalizeBaseUrl4(baseUrl);
4084
+ await driver.setItem(modelKey(normalized), models);
4085
+ const ts = legacyTimestamps[normalized] ?? Date.now();
4086
+ await driver.setItem(modelTsKey(normalized), ts);
4087
+ migratedProviders.push(normalized);
4088
+ }
4089
+ const existingIndex = await driver.getItem(
4090
+ PROVIDER_INDEX_KEY,
4091
+ []
4092
+ );
4093
+ const merged = [.../* @__PURE__ */ new Set([...existingIndex, ...migratedProviders])];
4094
+ await driver.setItem(PROVIDER_INDEX_KEY, merged);
4095
+ await driver.removeItem(SDK_STORAGE_KEYS.MODELS_FROM_ALL_PROVIDERS);
4096
+ await driver.removeItem(SDK_STORAGE_KEYS.LAST_MODELS_UPDATE);
4693
4097
  }
4694
- };
4695
- var isNode = () => {
4696
- try {
4697
- return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
4698
- } catch {
4699
- return false;
4098
+ await driver.setItem(MIGRATION_MARKER_KEY2, true);
4099
+ const [
4100
+ rawMints,
4101
+ rawInfo,
4102
+ lastUsedModel,
4103
+ rawDisabled,
4104
+ rawBaseUrls,
4105
+ lastBaseUrlsUpdate,
4106
+ rawRoutstr21Models,
4107
+ lastRoutstr21ModelsUpdate
4108
+ ] = await Promise.all([
4109
+ driver.getItem(
4110
+ SDK_STORAGE_KEYS.MINTS_FROM_ALL_PROVIDERS,
4111
+ {}
4112
+ ),
4113
+ driver.getItem(
4114
+ SDK_STORAGE_KEYS.INFO_FROM_ALL_PROVIDERS,
4115
+ {}
4116
+ ),
4117
+ driver.getItem(SDK_STORAGE_KEYS.LAST_USED_MODEL, null),
4118
+ driver.getItem(SDK_STORAGE_KEYS.DISABLED_PROVIDERS, []),
4119
+ driver.getItem(SDK_STORAGE_KEYS.BASE_URLS_LIST, []),
4120
+ driver.getItem(SDK_STORAGE_KEYS.LAST_BASE_URLS_UPDATE, null),
4121
+ driver.getItem(SDK_STORAGE_KEYS.ROUTSTR21_MODELS, []),
4122
+ driver.getItem(
4123
+ SDK_STORAGE_KEYS.LAST_ROUTSTR21_MODELS_UPDATE,
4124
+ null
4125
+ )
4126
+ ]);
4127
+ const modelsByBaseUrl = /* @__PURE__ */ new Map();
4128
+ const timestampsByBaseUrl = /* @__PURE__ */ new Map();
4129
+ const providerIndex = /* @__PURE__ */ new Set();
4130
+ const knownProviders = /* @__PURE__ */ new Set();
4131
+ for (const baseUrl of Object.keys(rawInfo)) {
4132
+ knownProviders.add(normalizeBaseUrl4(baseUrl));
4700
4133
  }
4701
- };
4702
- var defaultDriver = null;
4703
- var isBun3 = () => {
4704
- return typeof process.versions.bun !== "undefined";
4705
- };
4706
- var getDefaultSdkDriver = () => {
4134
+ for (const baseUrl of Object.keys(rawMints)) {
4135
+ knownProviders.add(normalizeBaseUrl4(baseUrl));
4136
+ }
4137
+ for (const baseUrl of rawBaseUrls) {
4138
+ knownProviders.add(normalizeBaseUrl4(baseUrl));
4139
+ }
4140
+ for (const baseUrl of rawDisabled) {
4141
+ knownProviders.add(normalizeBaseUrl4(baseUrl));
4142
+ }
4143
+ for (const baseUrl of Object.keys(legacyModels)) {
4144
+ knownProviders.add(normalizeBaseUrl4(baseUrl));
4145
+ }
4146
+ const indexProviders = await driver.getItem(
4147
+ PROVIDER_INDEX_KEY,
4148
+ []
4149
+ );
4150
+ for (const baseUrl of indexProviders) {
4151
+ const normalized = normalizeBaseUrl4(baseUrl);
4152
+ providerIndex.add(normalized);
4153
+ knownProviders.add(normalized);
4154
+ }
4155
+ for (const baseUrl of knownProviders) {
4156
+ const normalized = normalizeBaseUrl4(baseUrl);
4157
+ const models = await driver.getItem(
4158
+ modelKey(normalized),
4159
+ null
4160
+ );
4161
+ const ts = await driver.getItem(
4162
+ modelTsKey(normalized),
4163
+ null
4164
+ );
4165
+ if (models !== null) {
4166
+ modelsByBaseUrl.set(normalized, models);
4167
+ }
4168
+ if (ts !== null) {
4169
+ timestampsByBaseUrl.set(normalized, ts);
4170
+ }
4171
+ if (models !== null || ts !== null) {
4172
+ providerIndex.add(normalized);
4173
+ }
4174
+ }
4175
+ let mints = Object.fromEntries(
4176
+ Object.entries(rawMints).map(([baseUrl, mintList]) => [
4177
+ normalizeBaseUrl4(baseUrl),
4178
+ mintList.map((mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint)
4179
+ ])
4180
+ );
4181
+ let info = Object.fromEntries(
4182
+ Object.entries(rawInfo).map(([baseUrl, entry]) => [
4183
+ normalizeBaseUrl4(baseUrl),
4184
+ entry
4185
+ ])
4186
+ );
4187
+ let _lastUsedModel = lastUsedModel;
4188
+ let _disabledProviders = rawDisabled.map(normalizeBaseUrl4);
4189
+ let _baseUrlsList = rawBaseUrls.map(normalizeBaseUrl4);
4190
+ let _lastBaseUrlsUpdate = lastBaseUrlsUpdate;
4191
+ let _routstr21Models = rawRoutstr21Models;
4192
+ let _lastRoutstr21ModelsUpdate = lastRoutstr21ModelsUpdate;
4193
+ const persistProviderIndex = () => {
4194
+ void driver.setItem(PROVIDER_INDEX_KEY, [...providerIndex]);
4195
+ };
4196
+ return {
4197
+ // -- Models (sharded kv) --
4198
+ getCachedModels: () => {
4199
+ const result = {};
4200
+ for (const [baseUrl, models] of modelsByBaseUrl.entries()) {
4201
+ result[baseUrl] = models;
4202
+ }
4203
+ return result;
4204
+ },
4205
+ setCachedModels: (models) => {
4206
+ const nextKeys = new Set(
4207
+ Object.keys(models).map((baseUrl) => normalizeBaseUrl4(baseUrl))
4208
+ );
4209
+ for (const baseUrl of [...modelsByBaseUrl.keys()]) {
4210
+ if (!nextKeys.has(normalizeBaseUrl4(baseUrl))) {
4211
+ providerIndex.delete(baseUrl);
4212
+ modelsByBaseUrl.delete(baseUrl);
4213
+ timestampsByBaseUrl.delete(baseUrl);
4214
+ void driver.removeItem(modelKey(baseUrl));
4215
+ void driver.removeItem(modelTsKey(baseUrl));
4216
+ }
4217
+ }
4218
+ for (const [baseUrl, modelList] of Object.entries(models)) {
4219
+ const normalized = normalizeBaseUrl4(baseUrl);
4220
+ providerIndex.add(normalized);
4221
+ modelsByBaseUrl.set(normalized, modelList);
4222
+ const ts = timestampsByBaseUrl.get(normalized) ?? Date.now();
4223
+ timestampsByBaseUrl.set(normalized, ts);
4224
+ void driver.setItem(modelKey(normalized), modelList);
4225
+ void driver.setItem(modelTsKey(normalized), ts);
4226
+ }
4227
+ persistProviderIndex();
4228
+ },
4229
+ getProviderLastUpdate: (baseUrl) => {
4230
+ return timestampsByBaseUrl.get(normalizeBaseUrl4(baseUrl)) ?? null;
4231
+ },
4232
+ setProviderLastUpdate: (baseUrl, timestamp) => {
4233
+ const normalized = normalizeBaseUrl4(baseUrl);
4234
+ providerIndex.add(normalized);
4235
+ timestampsByBaseUrl.set(normalized, timestamp);
4236
+ void driver.setItem(modelTsKey(normalized), timestamp);
4237
+ persistProviderIndex();
4238
+ },
4239
+ // -- Mints (kv) --
4240
+ getCachedMints: () => mints,
4241
+ setCachedMints: (value) => {
4242
+ const normalized = {};
4243
+ for (const [baseUrl, mintList] of Object.entries(value)) {
4244
+ normalized[normalizeBaseUrl4(baseUrl)] = mintList.map(
4245
+ (mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint
4246
+ );
4247
+ }
4248
+ mints = normalized;
4249
+ void driver.setItem(SDK_STORAGE_KEYS.MINTS_FROM_ALL_PROVIDERS, normalized);
4250
+ },
4251
+ // -- Provider info (kv) --
4252
+ getCachedProviderInfo: () => info,
4253
+ setCachedProviderInfo: (value) => {
4254
+ const normalized = {};
4255
+ for (const [baseUrl, entry] of Object.entries(value)) {
4256
+ normalized[normalizeBaseUrl4(baseUrl)] = entry;
4257
+ }
4258
+ info = normalized;
4259
+ void driver.setItem(SDK_STORAGE_KEYS.INFO_FROM_ALL_PROVIDERS, normalized);
4260
+ },
4261
+ // -- Last used model (kv) --
4262
+ getLastUsedModel: () => _lastUsedModel,
4263
+ setLastUsedModel: (modelId) => {
4264
+ _lastUsedModel = modelId;
4265
+ void driver.setItem(SDK_STORAGE_KEYS.LAST_USED_MODEL, modelId);
4266
+ },
4267
+ // -- Disabled providers (kv) --
4268
+ getDisabledProviders: () => _disabledProviders,
4269
+ setDisabledProviders: (urls) => {
4270
+ const normalized = urls.map(normalizeBaseUrl4);
4271
+ _disabledProviders = normalized;
4272
+ void driver.setItem(SDK_STORAGE_KEYS.DISABLED_PROVIDERS, normalized);
4273
+ },
4274
+ // -- Base URLs (kv) --
4275
+ getBaseUrlsList: () => _baseUrlsList,
4276
+ getBaseUrlsLastUpdate: () => _lastBaseUrlsUpdate,
4277
+ setBaseUrlsList: (urls) => {
4278
+ const normalized = urls.map(normalizeBaseUrl4);
4279
+ _baseUrlsList = normalized;
4280
+ void driver.setItem(SDK_STORAGE_KEYS.BASE_URLS_LIST, normalized);
4281
+ },
4282
+ setBaseUrlsLastUpdate: (timestamp) => {
4283
+ _lastBaseUrlsUpdate = timestamp;
4284
+ void driver.setItem(SDK_STORAGE_KEYS.LAST_BASE_URLS_UPDATE, timestamp);
4285
+ },
4286
+ // -- Routstr21 models (kv) --
4287
+ getRoutstr21Models: () => _routstr21Models,
4288
+ setRoutstr21Models: (models) => {
4289
+ _routstr21Models = models;
4290
+ void driver.setItem(SDK_STORAGE_KEYS.ROUTSTR21_MODELS, models);
4291
+ },
4292
+ getRoutstr21ModelsLastUpdate: () => _lastRoutstr21ModelsUpdate,
4293
+ setRoutstr21ModelsLastUpdate: (timestamp) => {
4294
+ _lastRoutstr21ModelsUpdate = timestamp;
4295
+ void driver.setItem(
4296
+ SDK_STORAGE_KEYS.LAST_ROUTSTR21_MODELS_UPDATE,
4297
+ timestamp
4298
+ );
4299
+ }
4300
+ };
4301
+ };
4302
+ var createProviderRegistryFromDiscoveryAdapter = (adapter, logger) => {
4303
+ const log = (logger ?? consoleLogger).child("ProviderRegistry");
4304
+ return {
4305
+ getModelsForProvider: (baseUrl) => {
4306
+ const normalized = normalizeBaseUrl4(baseUrl);
4307
+ return adapter.getCachedModels()[normalized] || [];
4308
+ },
4309
+ getDisabledProviders: () => adapter.getDisabledProviders(),
4310
+ getProviderMints: (baseUrl) => {
4311
+ const normalized = normalizeBaseUrl4(baseUrl);
4312
+ return adapter.getCachedMints()[normalized] || [];
4313
+ },
4314
+ getProviderInfo: async (baseUrl) => {
4315
+ const normalized = normalizeBaseUrl4(baseUrl);
4316
+ const cached = adapter.getCachedProviderInfo()[normalized];
4317
+ if (cached) return cached;
4318
+ try {
4319
+ const response = await fetch(`${normalized}v1/info`);
4320
+ if (!response.ok) {
4321
+ throw new Error(`Failed ${response.status}`);
4322
+ }
4323
+ const info = await response.json();
4324
+ adapter.setCachedProviderInfo({
4325
+ ...adapter.getCachedProviderInfo(),
4326
+ [normalized]: info
4327
+ });
4328
+ return info;
4329
+ } catch (error) {
4330
+ log.warn(`Failed to fetch provider info from ${normalized}:`, error);
4331
+ return null;
4332
+ }
4333
+ },
4334
+ getAllProvidersModels: () => adapter.getCachedModels()
4335
+ };
4336
+ };
4337
+
4338
+ // storage/index.ts
4339
+ var isBrowser3 = () => {
4340
+ try {
4341
+ return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
4342
+ } catch {
4343
+ return false;
4344
+ }
4345
+ };
4346
+ var defaultDriver = null;
4347
+ var getDefaultSdkDriver = () => {
4707
4348
  if (defaultDriver) return defaultDriver;
4708
4349
  if (isBrowser3()) {
4709
4350
  defaultDriver = localStorageDriver;
4710
4351
  return defaultDriver;
4711
4352
  }
4712
- if (isBun3()) {
4713
- defaultDriver = createMemoryDriver();
4714
- return defaultDriver;
4715
- }
4716
- if (isNode()) {
4717
- defaultDriver = createSqliteDriver();
4718
- return defaultDriver;
4719
- }
4720
4353
  defaultDriver = createMemoryDriver();
4721
4354
  return defaultDriver;
4722
4355
  };
@@ -4737,38 +4370,195 @@ var getDefaultUsageTrackingDriver = () => {
4737
4370
  });
4738
4371
  return defaultUsageTrackingDriver;
4739
4372
  }
4740
- if (isBun3()) {
4741
- defaultUsageTrackingDriver = createBunSqliteUsageTrackingDriver();
4742
- return defaultUsageTrackingDriver;
4743
- }
4744
- if (isNode()) {
4745
- defaultUsageTrackingDriver = createSqliteUsageTrackingDriver({
4746
- legacyStorageDriver: storageDriver
4747
- });
4748
- return defaultUsageTrackingDriver;
4749
- }
4750
4373
  defaultUsageTrackingDriver = createMemoryUsageTrackingDriver();
4751
4374
  return defaultUsageTrackingDriver;
4752
4375
  };
4753
4376
  var setDefaultUsageTrackingDriver = (driver) => {
4754
4377
  defaultUsageTrackingDriver = driver;
4755
4378
  };
4756
- var getDefaultDiscoveryAdapter = async () => createDiscoveryAdapterFromStore(await getDefaultSdkStore());
4379
+ var defaultDiscoveryAdapter = null;
4380
+ var getDefaultDiscoveryAdapter = async () => {
4381
+ if (defaultDiscoveryAdapter) return defaultDiscoveryAdapter;
4382
+ const driver = getDefaultSdkDriver();
4383
+ defaultDiscoveryAdapter = await createShardedDiscoveryAdapter({ driver });
4384
+ return defaultDiscoveryAdapter;
4385
+ };
4757
4386
  var getDefaultStorageAdapter = async () => createStorageAdapterFromStore(await getDefaultSdkStore());
4758
- var getDefaultProviderRegistry = async () => createProviderRegistryFromStore(await getDefaultSdkStore());
4387
+ var getDefaultProviderRegistry = async () => createProviderRegistryFromDiscoveryAdapter(await getDefaultDiscoveryAdapter());
4388
+
4389
+ // client/usage.ts
4390
+ var numOrUndef = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
4391
+ function extractCostBreakdown(costObj) {
4392
+ if (!costObj || typeof costObj !== "object") return {};
4393
+ return {
4394
+ baseMsats: numOrUndef(costObj.base_msats),
4395
+ inputMsats: numOrUndef(costObj.input_msats),
4396
+ outputMsats: numOrUndef(costObj.output_msats),
4397
+ totalMsats: numOrUndef(costObj.total_msats),
4398
+ totalUsd: numOrUndef(costObj.total_usd),
4399
+ cacheReadInputTokens: numOrUndef(costObj.cache_read_input_tokens),
4400
+ cacheCreationInputTokens: numOrUndef(costObj.cache_creation_input_tokens),
4401
+ cacheReadMsats: numOrUndef(costObj.cache_read_msats),
4402
+ cacheCreationMsats: numOrUndef(costObj.cache_creation_msats),
4403
+ remainingBalanceMsats: numOrUndef(costObj.remaining_balance_msats)
4404
+ };
4405
+ }
4406
+ function extractUsageFromResponseBody(body, fallbackSatsCost = 0) {
4407
+ if (!body || typeof body !== "object") return null;
4408
+ const usage = body.usage;
4409
+ if (!usage || typeof usage !== "object") return null;
4410
+ const promptTokens = Number(usage.prompt_tokens ?? 0);
4411
+ const completionTokens = Number(usage.completion_tokens ?? 0);
4412
+ const totalTokens = Number(usage.total_tokens ?? 0);
4413
+ const costValue = usage.cost;
4414
+ let cost = 0;
4415
+ let satsCost = fallbackSatsCost;
4416
+ let breakdown = {};
4417
+ if (typeof costValue === "number") {
4418
+ cost = costValue;
4419
+ } else if (costValue && typeof costValue === "object") {
4420
+ const costObj = costValue;
4421
+ const totalUsd = costObj.total_usd;
4422
+ const totalMsats = costObj.total_msats;
4423
+ cost = typeof totalUsd === "number" ? totalUsd : 0;
4424
+ if (typeof totalMsats === "number") {
4425
+ satsCost = totalMsats / 1e3;
4426
+ }
4427
+ breakdown = extractCostBreakdown(costObj);
4428
+ }
4429
+ const provider = typeof body.provider === "string" ? body.provider : void 0;
4430
+ if (promptTokens === 0 && completionTokens === 0 && totalTokens === 0 && cost === 0 && satsCost === 0) {
4431
+ return null;
4432
+ }
4433
+ return {
4434
+ promptTokens,
4435
+ completionTokens,
4436
+ totalTokens,
4437
+ cost,
4438
+ satsCost,
4439
+ provider,
4440
+ ...breakdown
4441
+ };
4442
+ }
4443
+ function extractResponseId(body) {
4444
+ if (!body || typeof body !== "object") return void 0;
4445
+ const id = body.id;
4446
+ if (typeof id !== "string") return void 0;
4447
+ const trimmed = id.trim();
4448
+ return trimmed.length > 0 ? trimmed : void 0;
4449
+ }
4450
+ function extractUsageFromSSEJson(parsed, fallbackSatsCost = 0) {
4451
+ if (!parsed || typeof parsed !== "object") {
4452
+ return null;
4453
+ }
4454
+ const provider = typeof parsed.provider === "string" ? parsed.provider : void 0;
4455
+ if (!parsed.usage && parsed.cost && typeof parsed.cost === "object") {
4456
+ const costObj = parsed.cost;
4457
+ const msats2 = costObj.total_msats ?? 0;
4458
+ const cost2 = costObj.total_usd ?? 0;
4459
+ if (msats2 === 0 && cost2 === 0) return null;
4460
+ return {
4461
+ promptTokens: Number(costObj.input_tokens ?? 0),
4462
+ completionTokens: Number(costObj.output_tokens ?? 0),
4463
+ totalTokens: Number((costObj.input_tokens ?? 0) + (costObj.output_tokens ?? 0)),
4464
+ cost: Number(cost2),
4465
+ satsCost: msats2 > 0 ? msats2 / 1e3 : fallbackSatsCost,
4466
+ provider,
4467
+ ...extractCostBreakdown(costObj)
4468
+ };
4469
+ }
4470
+ if (!parsed.usage) {
4471
+ return null;
4472
+ }
4473
+ const usage = parsed.usage;
4474
+ const usageCost = usage.cost;
4475
+ let cost = 0;
4476
+ let msats = 0;
4477
+ let breakdown = {};
4478
+ if (typeof usageCost === "number") {
4479
+ cost = usageCost;
4480
+ } else if (usageCost && typeof usageCost === "object") {
4481
+ cost = usageCost.total_usd ?? 0;
4482
+ msats = usageCost.total_msats ?? 0;
4483
+ breakdown = extractCostBreakdown(usageCost);
4484
+ }
4485
+ const routstrCost = parsed.metadata?.routstr?.cost;
4486
+ if (routstrCost && typeof routstrCost === "object") {
4487
+ breakdown = { ...extractCostBreakdown(routstrCost), ...breakdown };
4488
+ }
4489
+ if (cost === 0) {
4490
+ cost = parsed.metadata?.routstr?.cost?.total_usd ?? 0;
4491
+ }
4492
+ if (msats === 0) {
4493
+ msats = parsed.metadata?.routstr?.cost?.total_msats ?? (typeof usage.cost_sats === "number" ? usage.cost_sats * 1e3 : 0);
4494
+ }
4495
+ const promptTokens = Number(usage.prompt_tokens ?? usage.input_tokens ?? 0);
4496
+ const completionTokens = Number(usage.completion_tokens ?? usage.output_tokens ?? 0);
4497
+ const totalTokens = Number(usage.total_tokens ?? promptTokens + completionTokens);
4498
+ const result = {
4499
+ promptTokens,
4500
+ completionTokens,
4501
+ totalTokens,
4502
+ cost: Number(cost ?? 0),
4503
+ satsCost: msats > 0 ? msats / 1e3 : fallbackSatsCost,
4504
+ provider,
4505
+ ...breakdown
4506
+ };
4507
+ if (result.promptTokens === 0 && result.completionTokens === 0 && result.totalTokens === 0 && result.cost === 0 && result.satsCost === 0) {
4508
+ return null;
4509
+ }
4510
+ return result;
4511
+ }
4512
+ function toUsageStats(usage) {
4513
+ if (!usage) return void 0;
4514
+ return {
4515
+ total_tokens: usage.totalTokens,
4516
+ prompt_tokens: usage.promptTokens,
4517
+ completion_tokens: usage.completionTokens,
4518
+ cost: usage.cost,
4519
+ sats_cost: usage.satsCost
4520
+ };
4521
+ }
4759
4522
  function mergeUsage(previous, next) {
4760
4523
  if (!previous) return next;
4524
+ const pickNum = (n, p) => typeof n === "number" && n > 0 ? n : p ?? n;
4761
4525
  return {
4762
4526
  promptTokens: next.promptTokens > 0 ? next.promptTokens : previous.promptTokens,
4763
4527
  completionTokens: next.completionTokens > 0 ? next.completionTokens : previous.completionTokens,
4764
4528
  totalTokens: next.totalTokens > 0 ? next.totalTokens : previous.totalTokens,
4765
4529
  cost: next.cost > 0 ? next.cost : previous.cost,
4766
- satsCost: next.satsCost > 0 ? next.satsCost : previous.satsCost
4530
+ satsCost: next.satsCost > 0 ? next.satsCost : previous.satsCost,
4531
+ provider: next.provider ?? previous.provider,
4532
+ baseMsats: pickNum(next.baseMsats, previous.baseMsats),
4533
+ inputMsats: pickNum(next.inputMsats, previous.inputMsats),
4534
+ outputMsats: pickNum(next.outputMsats, previous.outputMsats),
4535
+ totalMsats: pickNum(next.totalMsats, previous.totalMsats),
4536
+ totalUsd: pickNum(next.totalUsd, previous.totalUsd),
4537
+ cacheReadInputTokens: pickNum(
4538
+ next.cacheReadInputTokens,
4539
+ previous.cacheReadInputTokens
4540
+ ),
4541
+ cacheCreationInputTokens: pickNum(
4542
+ next.cacheCreationInputTokens,
4543
+ previous.cacheCreationInputTokens
4544
+ ),
4545
+ cacheReadMsats: pickNum(next.cacheReadMsats, previous.cacheReadMsats),
4546
+ cacheCreationMsats: pickNum(
4547
+ next.cacheCreationMsats,
4548
+ previous.cacheCreationMsats
4549
+ ),
4550
+ remainingBalanceMsats: pickNum(
4551
+ next.remainingBalanceMsats,
4552
+ previous.remainingBalanceMsats
4553
+ )
4767
4554
  };
4768
4555
  }
4769
4556
  function hasUsageChanged(previous, next) {
4770
4557
  if (!previous) return true;
4771
- return previous.promptTokens !== next.promptTokens || previous.completionTokens !== next.completionTokens || previous.totalTokens !== next.totalTokens || previous.cost !== next.cost || previous.satsCost !== next.satsCost;
4558
+ 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;
4559
+ }
4560
+ function isInspectionComplete(responseIdCaptured, usage) {
4561
+ return responseIdCaptured && !!usage && usage.totalTokens > 0 && typeof usage.totalMsats === "number" && !!usage.provider;
4772
4562
  }
4773
4563
  async function inspectSSEWebStream(stream, onUsage, onResponseId) {
4774
4564
  const reader = stream.getReader();
@@ -4778,14 +4568,22 @@ async function inspectSSEWebStream(stream, onUsage, onResponseId) {
4778
4568
  let capturedResponseId;
4779
4569
  let responseIdCaptured = false;
4780
4570
  const inspectDataPayload = (jsonText) => {
4781
- if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
4571
+ const trimmed = jsonText.trim();
4572
+ if (!trimmed || trimmed === "[DONE]") {
4573
+ if (trimmed === "[DONE]") console.log("[routstr:sse] [DONE]");
4574
+ return;
4575
+ }
4576
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
4577
+ console.log("[routstr:sse] non-JSON payload:", trimmed.slice(0, 200));
4782
4578
  return;
4783
4579
  }
4784
- const trimmed = jsonText.trim();
4785
- if (!trimmed || trimmed === "[DONE]") return;
4786
- if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return;
4787
4580
  try {
4788
4581
  const data = JSON.parse(trimmed);
4582
+ console.log("[routstr:sse] chunk:", JSON.stringify(data));
4583
+ if (isInspectionComplete(responseIdCaptured, capturedUsage)) {
4584
+ console.log("[routstr:sse] (inspection already complete, skipping)");
4585
+ return;
4586
+ }
4789
4587
  if (!responseIdCaptured) {
4790
4588
  const responseId = data?.id;
4791
4589
  if (typeof responseId === "string" && responseId.trim().length > 0) {
@@ -4796,19 +4594,21 @@ async function inspectSSEWebStream(stream, onUsage, onResponseId) {
4796
4594
  }
4797
4595
  const usage = extractUsageFromSSEJson(data);
4798
4596
  if (usage) {
4597
+ console.log("[routstr:sse] \u2192 usage detected:", usage);
4799
4598
  const merged = mergeUsage(capturedUsage, usage);
4800
4599
  if (hasUsageChanged(capturedUsage, merged)) {
4801
4600
  capturedUsage = merged;
4601
+ console.log("[routstr:sse] \u2192 merged (changed):", merged);
4802
4602
  onUsage(merged);
4603
+ } else {
4604
+ console.log("[routstr:sse] \u2192 merged (no change)");
4803
4605
  }
4804
4606
  }
4805
4607
  } catch {
4608
+ console.log("[routstr:sse] failed to parse payload:", trimmed.slice(0, 200));
4806
4609
  }
4807
4610
  };
4808
4611
  const inspectEventBlock = (eventBlock) => {
4809
- if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
4810
- return;
4811
- }
4812
4612
  const lines = eventBlock.split(/\r?\n/);
4813
4613
  const dataParts = [];
4814
4614
  for (const line of lines) {
@@ -4866,14 +4666,22 @@ function createSSEParserTransform(onUsage, onResponseId) {
4866
4666
  let capturedUsage = null;
4867
4667
  let responseIdCaptured = false;
4868
4668
  const inspectDataPayload = (jsonText) => {
4869
- if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
4669
+ const trimmed = jsonText.trim();
4670
+ if (!trimmed || trimmed === "[DONE]") {
4671
+ if (trimmed === "[DONE]") console.log("[routstr:sse] [DONE]");
4672
+ return;
4673
+ }
4674
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
4675
+ console.log("[routstr:sse] non-JSON payload:", trimmed.slice(0, 200));
4870
4676
  return;
4871
4677
  }
4872
- const trimmed = jsonText.trim();
4873
- if (!trimmed || trimmed === "[DONE]") return;
4874
- if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return;
4875
4678
  try {
4876
4679
  const data = JSON.parse(trimmed);
4680
+ console.log("[routstr:sse] chunk:", JSON.stringify(data));
4681
+ if (isInspectionComplete(responseIdCaptured, capturedUsage)) {
4682
+ console.log("[routstr:sse] (inspection already complete, skipping)");
4683
+ return;
4684
+ }
4877
4685
  if (!responseIdCaptured) {
4878
4686
  const responseId = data?.id;
4879
4687
  if (typeof responseId === "string" && responseId.trim().length > 0) {
@@ -4883,19 +4691,21 @@ function createSSEParserTransform(onUsage, onResponseId) {
4883
4691
  }
4884
4692
  const usage = extractUsageFromSSEJson(data);
4885
4693
  if (usage) {
4694
+ console.log("[routstr:sse] \u2192 usage detected:", usage);
4886
4695
  const mergedUsage = mergeUsage(capturedUsage, usage);
4887
4696
  if (hasUsageChanged(capturedUsage, mergedUsage)) {
4888
4697
  capturedUsage = mergedUsage;
4698
+ console.log("[routstr:sse] \u2192 merged (changed):", mergedUsage);
4889
4699
  onUsage(mergedUsage);
4700
+ } else {
4701
+ console.log("[routstr:sse] \u2192 merged (no change)");
4890
4702
  }
4891
4703
  }
4892
4704
  } catch {
4705
+ console.log("[routstr:sse] failed to parse payload:", trimmed.slice(0, 200));
4893
4706
  }
4894
4707
  };
4895
4708
  const inspectEventBlock = (eventBlock) => {
4896
- if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
4897
- return;
4898
- }
4899
4709
  const lines = eventBlock.split(/\r?\n/);
4900
4710
  const dataParts = [];
4901
4711
  for (const line of lines) {
@@ -4968,7 +4778,6 @@ var RoutstrClient = class {
4968
4778
  this.balanceManager,
4969
4779
  this.logger
4970
4780
  );
4971
- this.streamProcessor = new StreamProcessor();
4972
4781
  this.alertLevel = alertLevel;
4973
4782
  this.mode = mode;
4974
4783
  this.usageTrackingDriver = options.usageTrackingDriver;
@@ -4980,7 +4789,6 @@ var RoutstrClient = class {
4980
4789
  providerRegistry;
4981
4790
  cashuSpender;
4982
4791
  balanceManager;
4983
- streamProcessor;
4984
4792
  providerManager;
4985
4793
  alertLevel;
4986
4794
  mode;
@@ -5065,6 +4873,8 @@ var RoutstrClient = class {
5065
4873
  baseUrl: prepared.baseUrlUsed,
5066
4874
  mintUrl: params.mintUrl,
5067
4875
  initialTokenBalance: prepared.tokenBalanceInSats,
4876
+ initialTokenBalanceUnknown: prepared.tokenBalanceUnknown,
4877
+ fallbackSatsSpent: usage?.satsCost,
5068
4878
  response: prepared.response,
5069
4879
  modelId: prepared.modelId,
5070
4880
  usage,
@@ -5129,7 +4939,7 @@ var RoutstrClient = class {
5129
4939
  );
5130
4940
  }
5131
4941
  }
5132
- const { token, tokenBalance, tokenBalanceUnit } = await this._spendToken({
4942
+ const { token, tokenBalance, tokenBalanceUnit, tokenBalanceUnknown } = await this._spendToken({
5133
4943
  mintUrl,
5134
4944
  amount: requiredSats,
5135
4945
  baseUrl
@@ -5155,9 +4965,20 @@ var RoutstrClient = class {
5155
4965
  baseHeaders,
5156
4966
  selectedModel
5157
4967
  });
5158
- const tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
4968
+ let tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
4969
+ let initialTokenBalanceUnknown = tokenBalanceUnknown;
5159
4970
  const baseUrlUsed = response.baseUrl || baseUrl;
5160
4971
  const tokenUsed = response.token || token;
4972
+ if (baseUrlUsed !== baseUrl || tokenUsed !== token) {
4973
+ if (typeof response.initialTokenBalanceInSats === "number") {
4974
+ tokenBalanceInSats = response.initialTokenBalanceInSats;
4975
+ initialTokenBalanceUnknown = Boolean(
4976
+ response.initialTokenBalanceUnknown
4977
+ );
4978
+ } else {
4979
+ initialTokenBalanceUnknown = true;
4980
+ }
4981
+ }
5161
4982
  const contentType = response.headers.get("content-type") || "";
5162
4983
  let processedResponse = response;
5163
4984
  let capturedUsage;
@@ -5190,6 +5011,7 @@ var RoutstrClient = class {
5190
5011
  tokenUsed,
5191
5012
  baseUrlUsed,
5192
5013
  tokenBalanceInSats,
5014
+ tokenBalanceUnknown: initialTokenBalanceUnknown,
5193
5015
  modelId,
5194
5016
  capturedUsage,
5195
5017
  capturedResponseId,
@@ -5209,899 +5031,1049 @@ var RoutstrClient = class {
5209
5031
  return void 0;
5210
5032
  }
5211
5033
  /**
5212
- * Fetch AI response with streaming
5034
+ * Make the API request with failover support
5213
5035
  */
5214
- async fetchAIResponse(options, callbacks) {
5215
- const {
5216
- messageHistory,
5217
- selectedModel,
5218
- baseUrl,
5219
- mintUrl,
5220
- balance,
5221
- transactionHistory,
5222
- maxTokens,
5223
- headers
5224
- } = options;
5225
- const apiMessages = await this._convertMessages(messageHistory);
5226
- const requiredSats = this.providerManager.getRequiredSatsForModel(
5227
- selectedModel,
5228
- apiMessages,
5229
- maxTokens
5230
- );
5036
+ async _makeRequest(params) {
5037
+ const { path, method, body, baseUrl, token, headers } = params;
5231
5038
  try {
5232
- await this._checkBalance();
5233
- callbacks.onPaymentProcessing?.(true);
5234
- const spendResult = await this._spendToken({
5235
- mintUrl,
5236
- amount: requiredSats,
5237
- baseUrl
5039
+ const url = `${baseUrl.replace(/\/$/, "")}${path}`;
5040
+ if (this.mode === "xcashu") this._log("DEBUG", "HEADERS,", headers);
5041
+ const response = await fetch(url, {
5042
+ method,
5043
+ headers,
5044
+ body: body === void 0 || method === "GET" ? void 0 : JSON.stringify(body)
5238
5045
  });
5239
- let token = spendResult.token;
5240
- let tokenBalance = spendResult.tokenBalance;
5241
- let tokenBalanceUnit = spendResult.tokenBalanceUnit;
5242
- const tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
5243
- callbacks.onTokenCreated?.(this._getPendingCashuTokenAmount());
5244
- const baseHeaders = this._buildBaseHeaders(headers);
5245
- const requestHeaders = this._withAuthHeader(baseHeaders, token);
5246
- const providerInfo = await this.providerRegistry.getProviderInfo(baseUrl);
5247
- const providerVersion = providerInfo?.version ?? "";
5248
- let modelIdForRequest = selectedModel.id;
5249
- if (/^0\.1\./.test(providerVersion)) {
5250
- const newModel = await this.providerManager.getModelForProvider(
5251
- baseUrl,
5252
- selectedModel.id
5046
+ if (this.mode === "xcashu") this._log("DEBUG", "response,", response);
5047
+ response.baseUrl = baseUrl;
5048
+ response.token = token;
5049
+ if (!response.ok) {
5050
+ const requestId = response.headers.get("x-routstr-request-id") || void 0;
5051
+ let bodyText;
5052
+ try {
5053
+ bodyText = await response.text();
5054
+ } catch (e) {
5055
+ bodyText = void 0;
5056
+ }
5057
+ return await this._handleErrorResponse(
5058
+ params,
5059
+ token,
5060
+ response.status,
5061
+ requestId,
5062
+ this.mode === "xcashu" ? response.headers.get("x-cashu") ?? void 0 : void 0,
5063
+ bodyText,
5064
+ params.retryCount ?? 0
5253
5065
  );
5254
- modelIdForRequest = newModel?.id ?? selectedModel.id;
5255
5066
  }
5256
- const body = {
5257
- model: modelIdForRequest,
5258
- messages: apiMessages,
5259
- stream: true
5260
- };
5261
- if (maxTokens !== void 0) {
5262
- body.max_tokens = maxTokens;
5067
+ return response;
5068
+ } catch (error) {
5069
+ if (isNetworkErrorMessage(error?.message || "")) {
5070
+ return await this._handleErrorResponse(
5071
+ params,
5072
+ token,
5073
+ -1,
5074
+ // just for Network Error to skip all statuses
5075
+ void 0,
5076
+ void 0,
5077
+ void 0,
5078
+ params.retryCount ?? 0
5079
+ );
5263
5080
  }
5264
- if (selectedModel?.name?.startsWith("OpenAI:")) {
5265
- body.tools = [{ type: "web_search" }];
5081
+ throw error;
5082
+ }
5083
+ }
5084
+ /**
5085
+ * Handle error responses with failover
5086
+ */
5087
+ async _handleErrorResponse(params, token, status, requestId, xCashuRefundToken, responseBody, retryCount = 0) {
5088
+ const MAX_RETRIES_PER_PROVIDER = 2;
5089
+ const { path, method, body, selectedModel, baseUrl, mintUrl } = params;
5090
+ let tryNextProvider = false;
5091
+ const errorMessage = responseBody;
5092
+ this._log(
5093
+ "DEBUG",
5094
+ `[RoutstrClient] _handleErrorResponse: status=${status}, baseUrl=${baseUrl}, mode=${this.mode}, token preview=${token}, requestId=${requestId}, errorMessage=${errorMessage}`
5095
+ );
5096
+ this._log(
5097
+ "DEBUG",
5098
+ `[RoutstrClient] _handleErrorResponse: Attempting to receive/restore token for ${baseUrl}`
5099
+ );
5100
+ if (params.token.startsWith("cashu")) {
5101
+ const receiveResult = await this.cashuSpender.receiveToken(
5102
+ params.token
5103
+ );
5104
+ if (receiveResult.success) {
5105
+ this._log(
5106
+ "DEBUG",
5107
+ `[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${receiveResult.amount}`
5108
+ );
5109
+ tryNextProvider = true;
5110
+ } else {
5111
+ this._log(
5112
+ "DEBUG",
5113
+ `[RoutstrClient] _handleErrorResponse: Failed to receive token: ${receiveResult.message}`
5114
+ );
5266
5115
  }
5267
- const response = await this._makeRequest({
5268
- path: "/v1/chat/completions",
5269
- method: "POST",
5270
- body,
5271
- selectedModel,
5272
- baseUrl,
5273
- mintUrl,
5274
- token,
5275
- requiredSats,
5276
- maxTokens,
5277
- headers: requestHeaders,
5278
- baseHeaders
5279
- });
5280
- if (!response.body) {
5281
- throw new Error("Response body is not available");
5116
+ }
5117
+ if (this.mode === "xcashu") {
5118
+ if (xCashuRefundToken) {
5119
+ this._log(
5120
+ "DEBUG",
5121
+ `[RoutstrClient] _handleErrorResponse: Attempting to receive xcashu refund token, preview=${xCashuRefundToken.substring(0, 20)}...`
5122
+ );
5123
+ const receiveResult = await this.cashuSpender.receiveToken(xCashuRefundToken);
5124
+ if (receiveResult.success) {
5125
+ this._log(
5126
+ "DEBUG",
5127
+ `[RoutstrClient] _handleErrorResponse: xcashu refund received, amount=${receiveResult.amount}`
5128
+ );
5129
+ tryNextProvider = true;
5130
+ } else {
5131
+ this._log(
5132
+ "ERROR",
5133
+ `[xcashu] Failed to receive refund token: ${receiveResult.message}`
5134
+ );
5135
+ throw new ProviderError(
5136
+ baseUrl,
5137
+ status,
5138
+ "[xcashu] Failed to receive refund token",
5139
+ requestId
5140
+ );
5141
+ }
5142
+ } else {
5143
+ if (!tryNextProvider)
5144
+ throw new ProviderError(
5145
+ baseUrl,
5146
+ status,
5147
+ "[xcashu] Failed to receive refund token",
5148
+ requestId
5149
+ );
5282
5150
  }
5283
- if (response.status === 200) {
5284
- const baseUrlUsed = response.baseUrl || baseUrl;
5285
- const streamingResult = await this.streamProcessor.process(
5286
- response,
5287
- {
5288
- onContent: callbacks.onStreamingUpdate,
5289
- onThinking: callbacks.onThinkingUpdate
5290
- },
5291
- selectedModel.id
5151
+ }
5152
+ if (status === 402 && !tryNextProvider && this.mode === "apikeys") {
5153
+ this.storageAdapter.getApiKey(baseUrl);
5154
+ let topupAmount = params.requiredSats;
5155
+ try {
5156
+ const currentBalanceInfo = await this.balanceManager.getTokenBalance(
5157
+ params.token,
5158
+ baseUrl
5292
5159
  );
5293
- if (streamingResult.finish_reason === "content_filter") {
5294
- callbacks.onMessageAppend({
5295
- role: "assistant",
5296
- content: "Your request was denied due to content filtering."
5297
- });
5298
- } else if (streamingResult.content || streamingResult.images && streamingResult.images.length > 0) {
5299
- const message = await this._createAssistantMessage(streamingResult);
5300
- callbacks.onMessageAppend(message);
5160
+ if (currentBalanceInfo.balanceUnknown) {
5161
+ this._log(
5162
+ "DEBUG",
5163
+ `[RoutstrClient] _handleErrorResponse: Current balance unknown for ${baseUrl}; using default topup amount=${topupAmount}`
5164
+ );
5301
5165
  } else {
5302
- callbacks.onMessageAppend({
5303
- role: "system",
5304
- content: "The provider did not respond to this request."
5305
- });
5166
+ const currentBalance = currentBalanceInfo.unit === "msat" ? currentBalanceInfo.amount / 1e3 : currentBalanceInfo.amount;
5167
+ const reservedBalance = currentBalanceInfo.unit === "msat" ? (currentBalanceInfo.reserved ?? 0) / 1e3 : currentBalanceInfo.reserved ?? 0;
5168
+ const shortfall = Math.max(
5169
+ 0,
5170
+ params.requiredSats - currentBalance + reservedBalance
5171
+ );
5172
+ topupAmount = shortfall > 0.21 * params.requiredSats ? shortfall : 0.21 * params.requiredSats;
5173
+ this._log(
5174
+ "DEBUG",
5175
+ `The shortfall is: ${shortfall}. requiredSats: ${params.requiredSats}. Current Balance: ${currentBalance}. Reserved Balance: ${reservedBalance}. Available Balance: ${currentBalance - reservedBalance}`
5176
+ );
5306
5177
  }
5307
- callbacks.onStreamingUpdate("");
5308
- callbacks.onThinkingUpdate("");
5309
- const isApikeysEstimate = this.mode === "apikeys";
5310
- let satsSpent = await this._handlePostResponseBalanceUpdate({
5311
- token,
5312
- baseUrl: baseUrlUsed,
5313
- mintUrl,
5314
- initialTokenBalance: tokenBalanceInSats,
5315
- fallbackSatsSpent: isApikeysEstimate ? this._getEstimatedCosts(selectedModel, streamingResult) : void 0,
5316
- response,
5317
- modelId: selectedModel.id,
5318
- usage: streamingResult.usage ? {
5319
- promptTokens: Number(streamingResult.usage.prompt_tokens ?? 0),
5320
- completionTokens: Number(
5321
- streamingResult.usage.completion_tokens ?? 0
5322
- ),
5323
- totalTokens: Number(streamingResult.usage.total_tokens ?? 0),
5324
- cost: Number(streamingResult.usage.cost ?? 0),
5325
- satsCost: Number(streamingResult.usage.sats_cost ?? 0)
5326
- } : void 0,
5327
- requestId: streamingResult.responseId
5328
- });
5329
- const estimatedCosts = this._getEstimatedCosts(
5330
- selectedModel,
5331
- streamingResult
5178
+ } catch (e) {
5179
+ this._log(
5180
+ "WARN",
5181
+ "Could not get current token balance for topup calculation:",
5182
+ e
5332
5183
  );
5333
- const onLastMessageSatsUpdate = callbacks.onLastMessageSatsUpdate;
5334
- onLastMessageSatsUpdate?.(satsSpent, estimatedCosts);
5184
+ }
5185
+ const topupResult = await this.balanceManager.topUp({
5186
+ mintUrl,
5187
+ baseUrl,
5188
+ amount: topupAmount * TOPUP_MARGIN,
5189
+ token: params.token
5190
+ });
5191
+ this._log(
5192
+ "DEBUG",
5193
+ `[RoutstrClient] _handleErrorResponse: Topup result for ${baseUrl}: success=${topupResult.success}, message=${topupResult.message}`
5194
+ );
5195
+ if (!topupResult.success) {
5196
+ const message = topupResult.message || "";
5197
+ if (message.includes("Insufficient balance")) {
5198
+ const needMatch = message.match(/need (\d+)/);
5199
+ const haveMatch = message.match(/have (\d+)/);
5200
+ const required = needMatch ? parseInt(needMatch[1], 10) : params.requiredSats;
5201
+ const available = haveMatch ? parseInt(haveMatch[1], 10) : 0;
5202
+ this._log(
5203
+ "DEBUG",
5204
+ `[RoutstrClient] _handleErrorResponse: Insufficient balance, need=${required}, have=${available}`
5205
+ );
5206
+ throw new InsufficientBalanceError(
5207
+ required,
5208
+ available,
5209
+ 0,
5210
+ "",
5211
+ message
5212
+ );
5213
+ } else {
5214
+ this._log(
5215
+ "DEBUG",
5216
+ `[RoutstrClient] _handleErrorResponse: Topup failed with non-insufficient-balance error, will try next provider`
5217
+ );
5218
+ tryNextProvider = true;
5219
+ }
5335
5220
  } else {
5336
- throw new Error(`${response.status} ${response.statusText}`);
5221
+ this._log(
5222
+ "DEBUG",
5223
+ `[RoutstrClient] _handleErrorResponse: Topup successful, will retry with new token`
5224
+ );
5225
+ }
5226
+ if (!tryNextProvider) {
5227
+ if (retryCount < MAX_RETRIES_PER_PROVIDER) {
5228
+ this._log(
5229
+ "DEBUG",
5230
+ `[RoutstrClient] _handleErrorResponse: Retrying 402 (attempt ${retryCount + 1}/${MAX_RETRIES_PER_PROVIDER})`
5231
+ );
5232
+ return this._makeRequest({
5233
+ ...params,
5234
+ token: params.token,
5235
+ headers: this._withAuthHeader(params.baseHeaders, params.token),
5236
+ retryCount: retryCount + 1
5237
+ });
5238
+ } else {
5239
+ this._log(
5240
+ "DEBUG",
5241
+ `[RoutstrClient] _handleErrorResponse: 402 retry limit reached (${retryCount}/${MAX_RETRIES_PER_PROVIDER}), failing over to next provider`
5242
+ );
5243
+ tryNextProvider = true;
5244
+ }
5337
5245
  }
5338
- } catch (error) {
5339
- this._handleError(error, callbacks);
5340
- } finally {
5341
- callbacks.onPaymentProcessing?.(false);
5342
5246
  }
5343
- }
5344
- /**
5345
- * Make the API request with failover support
5346
- */
5347
- async _makeRequest(params) {
5348
- const { path, method, body, baseUrl, token, headers } = params;
5349
- try {
5350
- const url = `${baseUrl.replace(/\/$/, "")}${path}`;
5351
- if (this.mode === "xcashu") this._log("DEBUG", "HEADERS,", headers);
5352
- const response = await fetch(url, {
5353
- method,
5354
- headers,
5355
- body: body === void 0 || method === "GET" ? void 0 : JSON.stringify(body)
5356
- });
5357
- if (this.mode === "xcashu") this._log("DEBUG", "response,", response);
5358
- response.baseUrl = baseUrl;
5359
- response.token = token;
5360
- if (!response.ok) {
5361
- const requestId = response.headers.get("x-routstr-request-id") || void 0;
5362
- let bodyText;
5363
- try {
5364
- bodyText = await response.text();
5365
- } catch (e) {
5366
- bodyText = void 0;
5247
+ const isInsufficientBalance413 = status === 413 && responseBody?.includes("Insufficient balance");
5248
+ if (isInsufficientBalance413 && !tryNextProvider && this.mode === "apikeys") {
5249
+ let retryToken = params.token;
5250
+ try {
5251
+ const latestBalanceInfo = await this.balanceManager.getTokenBalance(
5252
+ params.token,
5253
+ baseUrl
5254
+ );
5255
+ if (latestBalanceInfo.isInvalidApiKey) {
5256
+ this._log(
5257
+ "DEBUG",
5258
+ `[RoutstrClient] _handleErrorResponse: Invalid API key (proofs already spent), removing for ${baseUrl}`
5259
+ );
5260
+ this.storageAdapter.removeApiKey(baseUrl);
5261
+ tryNextProvider = true;
5262
+ } else {
5263
+ const latestTokenBalance = latestBalanceInfo.balanceUnknown ? void 0 : latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
5264
+ if (latestBalanceInfo.apiKey) {
5265
+ const storedApiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
5266
+ if (storedApiKeyEntry?.key !== latestBalanceInfo.apiKey) {
5267
+ if (storedApiKeyEntry) {
5268
+ this.storageAdapter.removeApiKey(baseUrl);
5269
+ }
5270
+ this.storageAdapter.setApiKey(baseUrl, latestBalanceInfo.apiKey);
5271
+ }
5272
+ retryToken = latestBalanceInfo.apiKey;
5273
+ }
5274
+ if (latestTokenBalance !== void 0 && latestTokenBalance >= 0) {
5275
+ this.storageAdapter.updateApiKeyBalance(
5276
+ baseUrl,
5277
+ latestTokenBalance
5278
+ );
5279
+ }
5367
5280
  }
5368
- return await this._handleErrorResponse(
5369
- params,
5370
- token,
5371
- response.status,
5372
- requestId,
5373
- this.mode === "xcashu" ? response.headers.get("x-cashu") ?? void 0 : void 0,
5374
- bodyText,
5375
- params.retryCount ?? 0
5281
+ } catch (error) {
5282
+ this._log(
5283
+ "WARN",
5284
+ `[RoutstrClient] _handleErrorResponse: Failed to refresh API key after 413 insufficient balance for ${baseUrl}`,
5285
+ error
5376
5286
  );
5377
5287
  }
5378
- return response;
5379
- } catch (error) {
5380
- if (isNetworkErrorMessage(error?.message || "")) {
5381
- return await this._handleErrorResponse(
5382
- params,
5288
+ if (retryCount < MAX_RETRIES_PER_PROVIDER) {
5289
+ this._log(
5290
+ "DEBUG",
5291
+ `[RoutstrClient] _handleErrorResponse: Retrying 413 (attempt ${retryCount + 1}/${MAX_RETRIES_PER_PROVIDER})`
5292
+ );
5293
+ return this._makeRequest({
5294
+ ...params,
5295
+ token: retryToken,
5296
+ headers: this._withAuthHeader(params.baseHeaders, retryToken),
5297
+ retryCount: retryCount + 1
5298
+ });
5299
+ } else {
5300
+ this._log(
5301
+ "DEBUG",
5302
+ `[RoutstrClient] _handleErrorResponse: 413 retry limit reached (${retryCount}/${MAX_RETRIES_PER_PROVIDER}), failing over to next provider`
5303
+ );
5304
+ tryNextProvider = true;
5305
+ }
5306
+ }
5307
+ if (status === 401 && this.mode === "apikeys") {
5308
+ this._log(
5309
+ "DEBUG",
5310
+ `[RoutstrClient] _handleErrorResponse: Checking balance for ${baseUrl}, key preview=${token}`
5311
+ );
5312
+ const latestBalanceInfo = await this.balanceManager.getTokenBalance(
5313
+ token,
5314
+ baseUrl
5315
+ );
5316
+ if (latestBalanceInfo.isInvalidApiKey) {
5317
+ this.storageAdapter.removeApiKey(baseUrl);
5318
+ tryNextProvider = true;
5319
+ }
5320
+ }
5321
+ if ((status === 401 || status === 403 || status === 404 || status === 413 || status === 400 || status === 429 || status === 500 || status === 502 || status === 503 || status === 504 || status === 521) && !tryNextProvider) {
5322
+ this._log(
5323
+ "DEBUG",
5324
+ `[RoutstrClient] _handleErrorResponse: Status ${status} (${status === 429 ? "rate limited" : "auth/server error"}), attempting refund for ${baseUrl}, mode=${this.mode}`
5325
+ );
5326
+ if (this.mode === "apikeys") {
5327
+ this._log(
5328
+ "DEBUG",
5329
+ `[RoutstrClient] _handleErrorResponse: Attempting API key refund for ${baseUrl}, key preview=${token}`
5330
+ );
5331
+ const latestBalanceInfo = await this.balanceManager.getTokenBalance(
5383
5332
  token,
5384
- -1,
5385
- // just for Network Error to skip all statuses
5386
- void 0,
5387
- void 0,
5388
- void 0,
5389
- params.retryCount ?? 0
5333
+ baseUrl
5390
5334
  );
5335
+ this._log(
5336
+ "DEBUG",
5337
+ `[RoutstrClient] _handleErrorResponse: Initial API key balance: ${latestBalanceInfo.amount}`
5338
+ );
5339
+ const refundResult = await this.balanceManager.refundApiKey({
5340
+ mintUrl,
5341
+ baseUrl,
5342
+ apiKey: token,
5343
+ forceRefund: true
5344
+ });
5345
+ this._log(
5346
+ "DEBUG",
5347
+ `[RoutstrClient] _handleErrorResponse: API key refund result: success=${refundResult.success}, message=${refundResult.message}`
5348
+ );
5349
+ if (!refundResult.success && latestBalanceInfo.amount > 0 && !latestBalanceInfo.balanceUnknown) {
5350
+ throw new ProviderError(
5351
+ baseUrl,
5352
+ status,
5353
+ refundResult.message ?? "Unknown error"
5354
+ );
5355
+ }
5391
5356
  }
5392
- throw error;
5393
5357
  }
5358
+ this.providerManager.markFailed(baseUrl);
5359
+ this._log(
5360
+ "DEBUG",
5361
+ `[RoutstrClient] _handleErrorResponse: Marked provider ${baseUrl} as failed`
5362
+ );
5363
+ if (!selectedModel) {
5364
+ throw new ProviderError(
5365
+ baseUrl,
5366
+ status,
5367
+ "Funny, no selected model. HMM. "
5368
+ );
5369
+ }
5370
+ const nextProvider = this.providerManager.findNextBestProvider(
5371
+ selectedModel.id,
5372
+ baseUrl
5373
+ );
5374
+ if (nextProvider) {
5375
+ this._log(
5376
+ "DEBUG",
5377
+ `[RoutstrClient] _handleErrorResponse: Failing over to next provider: ${nextProvider}, model: ${selectedModel.id}`
5378
+ );
5379
+ const newModel = await this.providerManager.getModelForProvider(
5380
+ nextProvider,
5381
+ selectedModel.id
5382
+ ) ?? selectedModel;
5383
+ const messagesForPricing = Array.isArray(
5384
+ body?.messages
5385
+ ) ? body.messages : [];
5386
+ const newRequiredSats = this.providerManager.getRequiredSatsForModel(
5387
+ newModel,
5388
+ messagesForPricing,
5389
+ params.maxTokens
5390
+ );
5391
+ this._log(
5392
+ "DEBUG",
5393
+ `[RoutstrClient] _handleErrorResponse: Creating new token for failover provider ${nextProvider}, required sats: ${newRequiredSats}`
5394
+ );
5395
+ const spendResult = await this._spendToken({
5396
+ mintUrl,
5397
+ amount: newRequiredSats,
5398
+ baseUrl: nextProvider
5399
+ });
5400
+ const retryResponse = await this._makeRequest({
5401
+ ...params,
5402
+ path,
5403
+ method,
5404
+ body,
5405
+ baseUrl: nextProvider,
5406
+ selectedModel: newModel,
5407
+ token: spendResult.token,
5408
+ requiredSats: newRequiredSats,
5409
+ headers: this._withAuthHeader(params.baseHeaders, spendResult.token),
5410
+ retryCount: 0
5411
+ });
5412
+ retryResponse.initialTokenBalanceInSats = spendResult.tokenBalanceUnit === "msat" ? spendResult.tokenBalance / 1e3 : spendResult.tokenBalance;
5413
+ retryResponse.initialTokenBalanceUnknown = spendResult.tokenBalanceUnknown;
5414
+ return retryResponse;
5415
+ }
5416
+ throw new FailoverError(
5417
+ baseUrl,
5418
+ Array.from(this.providerManager.getFailedProviders())
5419
+ );
5394
5420
  }
5395
5421
  /**
5396
- * Handle error responses with failover
5422
+ * Handle post-response balance update for all modes
5397
5423
  */
5398
- async _handleErrorResponse(params, token, status, requestId, xCashuRefundToken, responseBody, retryCount = 0) {
5399
- const MAX_RETRIES_PER_PROVIDER = 2;
5400
- const { path, method, body, selectedModel, baseUrl, mintUrl } = params;
5401
- let tryNextProvider = false;
5402
- const errorMessage = responseBody;
5403
- this._log(
5404
- "DEBUG",
5405
- `[RoutstrClient] _handleErrorResponse: status=${status}, baseUrl=${baseUrl}, mode=${this.mode}, token preview=${token}, requestId=${requestId}, errorMessage=${errorMessage}`
5406
- );
5407
- this._log(
5408
- "DEBUG",
5409
- `[RoutstrClient] _handleErrorResponse: Attempting to receive/restore token for ${baseUrl}`
5410
- );
5411
- if (params.token.startsWith("cashu")) {
5412
- const receiveResult = await this.cashuSpender.receiveToken(
5413
- params.token
5414
- );
5415
- if (receiveResult.success) {
5416
- this._log(
5417
- "DEBUG",
5418
- `[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${receiveResult.amount}`
5419
- );
5420
- tryNextProvider = true;
5421
- } else {
5422
- this._log(
5423
- "DEBUG",
5424
- `[RoutstrClient] _handleErrorResponse: Failed to receive token: ${receiveResult.message}`
5425
- );
5426
- }
5427
- }
5428
- if (this.mode === "xcashu") {
5429
- if (xCashuRefundToken) {
5430
- this._log(
5431
- "DEBUG",
5432
- `[RoutstrClient] _handleErrorResponse: Attempting to receive xcashu refund token, preview=${xCashuRefundToken.substring(0, 20)}...`
5433
- );
5434
- const receiveResult = await this.cashuSpender.receiveToken(xCashuRefundToken);
5424
+ async _handlePostResponseBalanceUpdate(params) {
5425
+ const {
5426
+ token,
5427
+ baseUrl,
5428
+ mintUrl,
5429
+ initialTokenBalance,
5430
+ initialTokenBalanceUnknown,
5431
+ fallbackSatsSpent,
5432
+ response,
5433
+ modelId,
5434
+ usage,
5435
+ requestId,
5436
+ clientApiKey
5437
+ } = params;
5438
+ let satsSpent = initialTokenBalance;
5439
+ if (this.mode === "xcashu" && response) {
5440
+ const refundToken = response.headers.get("x-cashu") ?? void 0;
5441
+ if (refundToken) {
5442
+ const receiveResult = await this.cashuSpender.receiveToken(refundToken);
5435
5443
  if (receiveResult.success) {
5436
- this._log(
5437
- "DEBUG",
5438
- `[RoutstrClient] _handleErrorResponse: xcashu refund received, amount=${receiveResult.amount}`
5439
- );
5440
- tryNextProvider = true;
5444
+ this.storageAdapter.removeXcashuToken(baseUrl, token);
5445
+ satsSpent = initialTokenBalance - receiveResult.amount * (receiveResult.unit == "sat" ? 1 : 1e3);
5441
5446
  } else {
5442
5447
  this._log(
5443
5448
  "ERROR",
5444
5449
  `[xcashu] Failed to receive refund token: ${receiveResult.message}`
5445
5450
  );
5446
- throw new ProviderError(
5447
- baseUrl,
5448
- status,
5449
- "[xcashu] Failed to receive refund token",
5450
- requestId
5451
- );
5452
5451
  }
5453
- } else {
5454
- if (!tryNextProvider)
5455
- throw new ProviderError(
5456
- baseUrl,
5457
- status,
5458
- "[xcashu] Failed to receive refund token",
5459
- requestId
5460
- );
5461
5452
  }
5462
- }
5463
- if (status === 402 && !tryNextProvider && this.mode === "apikeys") {
5464
- this.storageAdapter.getApiKey(baseUrl);
5465
- let topupAmount = params.requiredSats;
5453
+ } else if (this.mode === "apikeys") {
5466
5454
  try {
5467
- const currentBalanceInfo = await this.balanceManager.getTokenBalance(
5468
- params.token,
5455
+ const latestBalanceInfo = await this.balanceManager.getTokenBalance(
5456
+ token,
5469
5457
  baseUrl
5470
5458
  );
5471
- const currentBalance = currentBalanceInfo.unit === "msat" ? currentBalanceInfo.amount / 1e3 : currentBalanceInfo.amount;
5472
- const reservedBalance = currentBalanceInfo.unit === "msat" ? (currentBalanceInfo.reserved ?? 0) / 1e3 : currentBalanceInfo.reserved ?? 0;
5473
- const shortfall = Math.max(0, params.requiredSats - currentBalance + reservedBalance);
5474
- topupAmount = shortfall > 0.21 * params.requiredSats ? shortfall : 0.21 * params.requiredSats;
5475
5459
  this._log(
5476
5460
  "DEBUG",
5477
- `The shortfall is: ${shortfall}. requiredSats: ${params.requiredSats}. Current Balance: ${currentBalance}. Reserved Balance: ${reservedBalance}. Available Balance: ${currentBalance - reservedBalance}`
5461
+ "LATEST Balance",
5462
+ latestBalanceInfo.amount,
5463
+ latestBalanceInfo.reserved,
5464
+ latestBalanceInfo.apiKey,
5465
+ baseUrl
5478
5466
  );
5467
+ const latestTokenBalance = latestBalanceInfo.balanceUnknown ? void 0 : latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
5468
+ const storedApiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
5469
+ if (storedApiKeyEntry?.key.startsWith("cashu") && latestBalanceInfo.apiKey) {
5470
+ this.storageAdapter.removeApiKey(baseUrl);
5471
+ this.storageAdapter.setApiKey(baseUrl, latestBalanceInfo.apiKey);
5472
+ }
5473
+ if (latestTokenBalance !== void 0) {
5474
+ this.storageAdapter.updateApiKeyBalance(baseUrl, latestTokenBalance);
5475
+ }
5476
+ satsSpent = latestTokenBalance !== void 0 && !initialTokenBalanceUnknown ? Math.max(0, initialTokenBalance - latestTokenBalance) : fallbackSatsSpent ?? usage?.satsCost ?? 0;
5479
5477
  } catch (e) {
5480
- this._log(
5481
- "WARN",
5482
- "Could not get current token balance for topup calculation:",
5483
- e
5484
- );
5478
+ this._log("WARN", "Could not get updated API key balance:", e);
5479
+ satsSpent = fallbackSatsSpent ?? usage?.satsCost ?? 0;
5485
5480
  }
5486
- const topupResult = await this.balanceManager.topUp({
5487
- mintUrl,
5488
- baseUrl,
5489
- amount: topupAmount * TOPUP_MARGIN,
5490
- token: params.token
5491
- });
5492
- this._log(
5493
- "DEBUG",
5494
- `[RoutstrClient] _handleErrorResponse: Topup result for ${baseUrl}: success=${topupResult.success}, message=${topupResult.message}`
5495
- );
5496
- if (!topupResult.success) {
5497
- const message = topupResult.message || "";
5498
- if (message.includes("Insufficient balance")) {
5499
- const needMatch = message.match(/need (\d+)/);
5500
- const haveMatch = message.match(/have (\d+)/);
5501
- const required = needMatch ? parseInt(needMatch[1], 10) : params.requiredSats;
5502
- const available = haveMatch ? parseInt(haveMatch[1], 10) : 0;
5503
- this._log(
5504
- "DEBUG",
5505
- `[RoutstrClient] _handleErrorResponse: Insufficient balance, need=${required}, have=${available}`
5506
- );
5507
- throw new InsufficientBalanceError(
5508
- required,
5509
- available,
5510
- 0,
5511
- "",
5512
- message
5513
- );
5481
+ }
5482
+ await this._trackResponseUsage({
5483
+ token,
5484
+ baseUrl,
5485
+ response,
5486
+ modelId,
5487
+ satsSpent,
5488
+ usage,
5489
+ requestId,
5490
+ clientApiKey
5491
+ });
5492
+ (async () => {
5493
+ })();
5494
+ return satsSpent;
5495
+ }
5496
+ async _trackResponseUsage(params) {
5497
+ const {
5498
+ token,
5499
+ baseUrl,
5500
+ response,
5501
+ modelId,
5502
+ satsSpent,
5503
+ usage: providedUsage,
5504
+ requestId: providedRequestId,
5505
+ clientApiKey
5506
+ } = params;
5507
+ if (!response || !modelId) {
5508
+ return;
5509
+ }
5510
+ try {
5511
+ let usage = providedUsage;
5512
+ let requestId = providedRequestId;
5513
+ if (!usage || !requestId) {
5514
+ const contentType = response.headers.get("content-type") || "";
5515
+ if (contentType.includes("text/event-stream")) {
5516
+ usage = usage ?? response.usage;
5517
+ requestId = requestId ?? response.requestId ?? response.headers.get("x-routstr-request-id") ?? void 0;
5518
+ if (!usage) {
5519
+ return;
5520
+ }
5514
5521
  } else {
5515
- this._log(
5516
- "DEBUG",
5517
- `[RoutstrClient] _handleErrorResponse: Topup failed with non-insufficient-balance error, will try next provider`
5518
- );
5519
- tryNextProvider = true;
5522
+ const cloned = response.clone();
5523
+ const responseBody = await cloned.json();
5524
+ usage = usage ?? extractUsageFromResponseBody(responseBody, satsSpent) ?? void 0;
5525
+ requestId = requestId ?? extractResponseId(responseBody) ?? response.headers.get("x-routstr-request-id") ?? void 0;
5520
5526
  }
5521
- } else {
5527
+ }
5528
+ if (!usage) {
5529
+ return;
5530
+ }
5531
+ const finalRequestId = requestId || "unknown";
5532
+ const store = this.sdkStore ?? await getDefaultSdkStore();
5533
+ const state = store.getState();
5534
+ const matchKey = clientApiKey ?? token;
5535
+ const matchingClient = state.clientIds.find(
5536
+ (client) => client.apiKey === matchKey
5537
+ );
5538
+ const entryId = finalRequestId === "unknown" ? `req-${Date.now()}-${modelId}` : finalRequestId;
5539
+ const usageTracking = this.usageTrackingDriver ?? getDefaultUsageTrackingDriver();
5540
+ const entry = {
5541
+ id: entryId,
5542
+ timestamp: Date.now(),
5543
+ modelId,
5544
+ baseUrl,
5545
+ requestId: finalRequestId,
5546
+ client: matchingClient?.clientId,
5547
+ ...usage
5548
+ };
5549
+ if (this.mode === "xcashu") {
5550
+ entry.satsCost = satsSpent;
5551
+ }
5552
+ await usageTracking.append(entry);
5553
+ } catch (error) {
5554
+ }
5555
+ }
5556
+ /**
5557
+ * Check wallet balance and throw if insufficient
5558
+ */
5559
+ async _checkBalance() {
5560
+ const balances = await this.walletAdapter.getBalances();
5561
+ const totalBalance = Object.values(balances).reduce((sum, v) => sum + v, 0);
5562
+ if (totalBalance <= 0) {
5563
+ throw new InsufficientBalanceError(1, 0);
5564
+ }
5565
+ }
5566
+ /**
5567
+ * Spend a token using CashuSpender with standardized error handling
5568
+ */
5569
+ async _spendToken(params) {
5570
+ const { mintUrl, amount, baseUrl } = params;
5571
+ this._log(
5572
+ "DEBUG",
5573
+ `[RoutstrClient] _spendToken: mode=${this.mode}, amount=${amount}, baseUrl=${baseUrl}, mintUrl=${mintUrl}`
5574
+ );
5575
+ if (this.mode === "apikeys") {
5576
+ let parentApiKey = this.storageAdapter.getApiKey(baseUrl);
5577
+ if (!parentApiKey) {
5522
5578
  this._log(
5523
5579
  "DEBUG",
5524
- `[RoutstrClient] _handleErrorResponse: Topup successful, will retry with new token`
5580
+ `[RoutstrClient] _spendToken: No existing API key for ${baseUrl}, creating new one via Cashu`
5525
5581
  );
5526
- }
5527
- if (!tryNextProvider) {
5528
- if (retryCount < MAX_RETRIES_PER_PROVIDER) {
5582
+ const spendResult2 = await this.cashuSpender.spend({
5583
+ mintUrl,
5584
+ amount: amount * TOPUP_MARGIN,
5585
+ baseUrl: "",
5586
+ reuseToken: false
5587
+ });
5588
+ if (!spendResult2.token) {
5529
5589
  this._log(
5530
- "DEBUG",
5531
- `[RoutstrClient] _handleErrorResponse: Retrying 402 (attempt ${retryCount + 1}/${MAX_RETRIES_PER_PROVIDER})`
5590
+ "ERROR",
5591
+ `[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error:`,
5592
+ spendResult2.error
5593
+ );
5594
+ throw new Error(
5595
+ `[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error: ${spendResult2.error}`
5532
5596
  );
5533
- return this._makeRequest({
5534
- ...params,
5535
- token: params.token,
5536
- headers: this._withAuthHeader(params.baseHeaders, params.token),
5537
- retryCount: retryCount + 1
5538
- });
5539
5597
  } else {
5540
5598
  this._log(
5541
5599
  "DEBUG",
5542
- `[RoutstrClient] _handleErrorResponse: 402 retry limit reached (${retryCount}/${MAX_RETRIES_PER_PROVIDER}), failing over to next provider`
5600
+ `[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult2.token}`
5543
5601
  );
5544
- tryNextProvider = true;
5545
5602
  }
5546
- }
5547
- }
5548
- const isInsufficientBalance413 = status === 413 && responseBody?.includes("Insufficient balance");
5549
- if (isInsufficientBalance413 && !tryNextProvider && this.mode === "apikeys") {
5550
- let retryToken = params.token;
5551
- try {
5552
- const latestBalanceInfo = await this.balanceManager.getTokenBalance(
5553
- params.token,
5554
- baseUrl
5603
+ this._log(
5604
+ "DEBUG",
5605
+ `[RoutstrClient] _spendToken: Created API key for ${baseUrl}, key preview: ${spendResult2.token}, balance: ${spendResult2.balance}`
5555
5606
  );
5556
- if (latestBalanceInfo.isInvalidApiKey) {
5557
- this._log(
5558
- "DEBUG",
5559
- `[RoutstrClient] _handleErrorResponse: Invalid API key (proofs already spent), removing for ${baseUrl}`
5560
- );
5561
- this.storageAdapter.removeApiKey(baseUrl);
5562
- tryNextProvider = true;
5563
- } else {
5564
- const latestTokenBalance = latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
5565
- if (latestBalanceInfo.apiKey) {
5566
- const storedApiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
5567
- if (storedApiKeyEntry?.key !== latestBalanceInfo.apiKey) {
5568
- if (storedApiKeyEntry) {
5569
- this.storageAdapter.removeApiKey(baseUrl);
5570
- }
5571
- this.storageAdapter.setApiKey(baseUrl, latestBalanceInfo.apiKey);
5607
+ try {
5608
+ this.storageAdapter.setApiKey(baseUrl, spendResult2.token);
5609
+ } catch (error) {
5610
+ if (error instanceof Error && error.message.includes("ApiKey already exists")) {
5611
+ const receiveResult = await this.cashuSpender.receiveToken(
5612
+ spendResult2.token
5613
+ );
5614
+ if (receiveResult.success) {
5615
+ this._log(
5616
+ "DEBUG",
5617
+ `[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${receiveResult.amount}`
5618
+ );
5619
+ } else {
5620
+ this._log(
5621
+ "DEBUG",
5622
+ `[RoutstrClient] _handleErrorResponse: Token restore failed: ${receiveResult.message}`
5623
+ );
5572
5624
  }
5573
- retryToken = latestBalanceInfo.apiKey;
5574
- }
5575
- if (latestTokenBalance >= 0) {
5576
- this.storageAdapter.updateApiKeyBalance(
5577
- baseUrl,
5578
- latestTokenBalance
5625
+ this._log(
5626
+ "DEBUG",
5627
+ `[RoutstrClient] _spendToken: API key already exists for ${baseUrl}, using existing key`
5579
5628
  );
5629
+ } else {
5630
+ throw error;
5580
5631
  }
5581
5632
  }
5582
- } catch (error) {
5583
- this._log(
5584
- "WARN",
5585
- `[RoutstrClient] _handleErrorResponse: Failed to refresh API key after 413 insufficient balance for ${baseUrl}`,
5586
- error
5587
- );
5588
- }
5589
- if (retryCount < MAX_RETRIES_PER_PROVIDER) {
5590
- this._log(
5591
- "DEBUG",
5592
- `[RoutstrClient] _handleErrorResponse: Retrying 413 (attempt ${retryCount + 1}/${MAX_RETRIES_PER_PROVIDER})`
5593
- );
5594
- return this._makeRequest({
5595
- ...params,
5596
- token: retryToken,
5597
- headers: this._withAuthHeader(params.baseHeaders, retryToken),
5598
- retryCount: retryCount + 1
5599
- });
5633
+ parentApiKey = this.storageAdapter.getApiKey(baseUrl);
5600
5634
  } else {
5601
5635
  this._log(
5602
5636
  "DEBUG",
5603
- `[RoutstrClient] _handleErrorResponse: 413 retry limit reached (${retryCount}/${MAX_RETRIES_PER_PROVIDER}), failing over to next provider`
5637
+ `[RoutstrClient] _spendToken: Using existing API key for ${baseUrl}, key preview: ${parentApiKey.key}`
5604
5638
  );
5605
- tryNextProvider = true;
5606
5639
  }
5607
- }
5608
- if (status === 401 && this.mode === "apikeys") {
5609
- this._log(
5610
- "DEBUG",
5611
- `[RoutstrClient] _handleErrorResponse: Checking balance for ${baseUrl}, key preview=${token}`
5612
- );
5613
- const latestBalanceInfo = await this.balanceManager.getTokenBalance(
5614
- token,
5615
- baseUrl
5640
+ let tokenBalance = 0;
5641
+ let tokenBalanceUnit = "sat";
5642
+ let tokenBalanceUnknown = false;
5643
+ const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
5644
+ const distributionForBaseUrl = apiKeyDistribution.find(
5645
+ (d) => d.baseUrl === baseUrl
5616
5646
  );
5617
- if (latestBalanceInfo.isInvalidApiKey) {
5618
- this.storageAdapter.removeApiKey(baseUrl);
5619
- tryNextProvider = true;
5647
+ if (distributionForBaseUrl) {
5648
+ tokenBalance = distributionForBaseUrl.amount;
5620
5649
  }
5621
- }
5622
- if ((status === 401 || status === 403 || status === 404 || status === 413 || status === 400 || status === 429 || status === 500 || status === 502 || status === 503 || status === 504 || status === 521) && !tryNextProvider) {
5623
- this._log(
5624
- "DEBUG",
5625
- `[RoutstrClient] _handleErrorResponse: Status ${status} (${status === 429 ? "rate limited" : "auth/server error"}), attempting refund for ${baseUrl}, mode=${this.mode}`
5626
- );
5627
- if (this.mode === "apikeys") {
5628
- this._log(
5629
- "DEBUG",
5630
- `[RoutstrClient] _handleErrorResponse: Attempting API key refund for ${baseUrl}, key preview=${token}`
5631
- );
5632
- const latestBalanceInfo = await this.balanceManager.getTokenBalance(
5633
- token,
5634
- baseUrl
5635
- );
5636
- this._log(
5637
- "DEBUG",
5638
- `[RoutstrClient] _handleErrorResponse: Initial API key balance: ${latestBalanceInfo.amount}`
5639
- );
5640
- const refundResult = await this.balanceManager.refundApiKey({
5641
- mintUrl,
5642
- baseUrl,
5643
- apiKey: token,
5644
- forceRefund: true
5645
- });
5646
- this._log(
5647
- "DEBUG",
5648
- `[RoutstrClient] _handleErrorResponse: API key refund result: success=${refundResult.success}, message=${refundResult.message}`
5649
- );
5650
- if (!refundResult.success && latestBalanceInfo.amount > 0) {
5651
- throw new ProviderError(
5652
- baseUrl,
5653
- status,
5654
- refundResult.message ?? "Unknown error"
5650
+ if (tokenBalance === 0 && parentApiKey) {
5651
+ try {
5652
+ const balanceInfo = await this.balanceManager.getTokenBalance(
5653
+ parentApiKey.key,
5654
+ baseUrl
5655
5655
  );
5656
+ tokenBalance = balanceInfo.amount;
5657
+ tokenBalanceUnit = balanceInfo.unit;
5658
+ tokenBalanceUnknown = Boolean(balanceInfo.balanceUnknown);
5659
+ } catch (e) {
5660
+ this._log("WARN", "Could not get initial API key balance:", e);
5656
5661
  }
5657
5662
  }
5663
+ this._log(
5664
+ "DEBUG",
5665
+ `[RoutstrClient] _spendToken: Returning token with balance=${tokenBalance} ${tokenBalanceUnit}`
5666
+ );
5667
+ return {
5668
+ token: parentApiKey?.key ?? "",
5669
+ tokenBalance,
5670
+ tokenBalanceUnit,
5671
+ tokenBalanceUnknown
5672
+ };
5658
5673
  }
5659
- this.providerManager.markFailed(baseUrl);
5660
5674
  this._log(
5661
5675
  "DEBUG",
5662
- `[RoutstrClient] _handleErrorResponse: Marked provider ${baseUrl} as failed`
5663
- );
5664
- if (!selectedModel) {
5665
- throw new ProviderError(
5666
- baseUrl,
5667
- status,
5668
- "Funny, no selected model. HMM. "
5669
- );
5670
- }
5671
- const nextProvider = this.providerManager.findNextBestProvider(
5672
- selectedModel.id,
5673
- baseUrl
5676
+ `[RoutstrClient] _spendToken: Calling CashuSpender.spend for amount=${amount}, mintUrl=${mintUrl}, mode=${this.mode}`
5674
5677
  );
5675
- if (nextProvider) {
5678
+ const spendResult = await this.cashuSpender.spend({
5679
+ mintUrl,
5680
+ amount,
5681
+ baseUrl: "",
5682
+ reuseToken: false
5683
+ });
5684
+ if (!spendResult.token) {
5676
5685
  this._log(
5677
- "DEBUG",
5678
- `[RoutstrClient] _handleErrorResponse: Failing over to next provider: ${nextProvider}, model: ${selectedModel.id}`
5679
- );
5680
- const newModel = await this.providerManager.getModelForProvider(
5681
- nextProvider,
5682
- selectedModel.id
5683
- ) ?? selectedModel;
5684
- const messagesForPricing = Array.isArray(
5685
- body?.messages
5686
- ) ? body.messages : [];
5687
- const newRequiredSats = this.providerManager.getRequiredSatsForModel(
5688
- newModel,
5689
- messagesForPricing,
5690
- params.maxTokens
5686
+ "ERROR",
5687
+ `[RoutstrClient] _spendToken: CashuSpender.spend failed, error:`,
5688
+ spendResult.error
5691
5689
  );
5690
+ } else {
5692
5691
  this._log(
5693
5692
  "DEBUG",
5694
- `[RoutstrClient] _handleErrorResponse: Creating new token for failover provider ${nextProvider}, required sats: ${newRequiredSats}`
5693
+ `[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult.token}, balance: ${spendResult.balance} ${spendResult.unit ?? "sat"}`
5695
5694
  );
5696
- const spendResult = await this._spendToken({
5697
- mintUrl,
5698
- amount: newRequiredSats,
5699
- baseUrl: nextProvider
5700
- });
5701
- return this._makeRequest({
5702
- ...params,
5703
- path,
5704
- method,
5705
- body,
5706
- baseUrl: nextProvider,
5707
- selectedModel: newModel,
5708
- token: spendResult.token,
5709
- requiredSats: newRequiredSats,
5710
- headers: this._withAuthHeader(params.baseHeaders, spendResult.token),
5711
- retryCount: 0
5712
- });
5695
+ this.storageAdapter.addXcashuToken(baseUrl, spendResult.token);
5713
5696
  }
5714
- throw new FailoverError(
5715
- baseUrl,
5716
- Array.from(this.providerManager.getFailedProviders())
5717
- );
5697
+ return {
5698
+ token: spendResult.token,
5699
+ tokenBalance: spendResult.balance,
5700
+ tokenBalanceUnit: spendResult.unit ?? "sat",
5701
+ tokenBalanceUnknown: false
5702
+ };
5703
+ }
5704
+ /**
5705
+ * Build request headers with common defaults and dev mock controls
5706
+ */
5707
+ _buildBaseHeaders(additionalHeaders = {}, token) {
5708
+ const headers = {
5709
+ ...additionalHeaders,
5710
+ "Content-Type": "application/json"
5711
+ };
5712
+ return headers;
5713
+ }
5714
+ /**
5715
+ * Attach auth headers using the active client mode
5716
+ */
5717
+ _withAuthHeader(headers, token) {
5718
+ const nextHeaders = { ...headers };
5719
+ if (this.mode === "xcashu") {
5720
+ nextHeaders["X-Cashu"] = token;
5721
+ } else {
5722
+ nextHeaders["Authorization"] = `Bearer ${token}`;
5723
+ }
5724
+ return nextHeaders;
5718
5725
  }
5726
+ };
5727
+
5728
+ // client/StreamProcessor.ts
5729
+ var StreamProcessor = class {
5730
+ accumulatedContent = "";
5731
+ accumulatedThinking = "";
5732
+ accumulatedImages = [];
5733
+ isInThinking = false;
5734
+ isInContent = false;
5719
5735
  /**
5720
- * Handle post-response balance update for all modes
5736
+ * Process a streaming response
5721
5737
  */
5722
- async _handlePostResponseBalanceUpdate(params) {
5723
- const {
5724
- token,
5725
- baseUrl,
5726
- mintUrl,
5727
- initialTokenBalance,
5728
- fallbackSatsSpent,
5729
- response,
5730
- modelId,
5731
- usage,
5732
- requestId,
5733
- clientApiKey
5734
- } = params;
5735
- let satsSpent = initialTokenBalance;
5736
- if (this.mode === "xcashu" && response) {
5737
- const refundToken = response.headers.get("x-cashu") ?? void 0;
5738
- if (refundToken) {
5739
- const receiveResult = await this.cashuSpender.receiveToken(refundToken);
5740
- if (receiveResult.success) {
5741
- this.storageAdapter.removeXcashuToken(baseUrl, token);
5742
- satsSpent = initialTokenBalance - receiveResult.amount * (receiveResult.unit == "sat" ? 1 : 1e3);
5743
- } else {
5744
- this._log(
5745
- "ERROR",
5746
- `[xcashu] Failed to receive refund token: ${receiveResult.message}`
5747
- );
5738
+ async process(response, callbacks, modelId) {
5739
+ if (!response.body) {
5740
+ throw new Error("Response body is not available");
5741
+ }
5742
+ const reader = response.body.getReader();
5743
+ const decoder = new TextDecoder("utf-8");
5744
+ let buffer = "";
5745
+ this.accumulatedContent = "";
5746
+ this.accumulatedThinking = "";
5747
+ this.accumulatedImages = [];
5748
+ this.isInThinking = false;
5749
+ this.isInContent = false;
5750
+ let usage;
5751
+ let model;
5752
+ let finish_reason;
5753
+ let citations;
5754
+ let annotations;
5755
+ let responseId;
5756
+ try {
5757
+ while (true) {
5758
+ const { done, value } = await reader.read();
5759
+ if (done) {
5760
+ break;
5748
5761
  }
5749
- }
5750
- } else if (this.mode === "apikeys") {
5751
- try {
5752
- const latestBalanceInfo = await this.balanceManager.getTokenBalance(
5753
- token,
5754
- baseUrl
5755
- );
5756
- this._log(
5757
- "DEBUG",
5758
- "LATEST Balance",
5759
- latestBalanceInfo.amount,
5760
- latestBalanceInfo.reserved,
5761
- latestBalanceInfo.apiKey,
5762
- baseUrl
5763
- );
5764
- const latestTokenBalance = latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
5765
- const storedApiKeyEntry = this.storageAdapter.getApiKey(baseUrl);
5766
- if (storedApiKeyEntry?.key.startsWith("cashu") && latestBalanceInfo.apiKey) {
5767
- this.storageAdapter.removeApiKey(baseUrl);
5768
- this.storageAdapter.setApiKey(baseUrl, latestBalanceInfo.apiKey);
5762
+ const chunk = decoder.decode(value, { stream: true });
5763
+ buffer += chunk;
5764
+ const lines = buffer.split("\n");
5765
+ buffer = lines.pop() || "";
5766
+ for (const line of lines) {
5767
+ const parsed = this._parseLine(line);
5768
+ if (!parsed) continue;
5769
+ if (parsed.content) {
5770
+ this._handleContent(parsed.content, callbacks, modelId);
5771
+ }
5772
+ if (parsed.reasoning) {
5773
+ this._handleThinking(parsed.reasoning, callbacks);
5774
+ }
5775
+ if (parsed.usage) {
5776
+ usage = parsed.usage;
5777
+ }
5778
+ if (parsed.model) {
5779
+ model = parsed.model;
5780
+ }
5781
+ if (parsed.finish_reason) {
5782
+ finish_reason = parsed.finish_reason;
5783
+ }
5784
+ if (parsed.responseId) {
5785
+ responseId = parsed.responseId;
5786
+ }
5787
+ if (parsed.citations) {
5788
+ citations = parsed.citations;
5789
+ }
5790
+ if (parsed.annotations) {
5791
+ annotations = parsed.annotations;
5792
+ }
5793
+ if (parsed.images) {
5794
+ this._mergeImages(parsed.images);
5795
+ }
5769
5796
  }
5770
- this.storageAdapter.updateApiKeyBalance(baseUrl, latestTokenBalance);
5771
- satsSpent = initialTokenBalance - latestTokenBalance;
5772
- } catch (e) {
5773
- this._log("WARN", "Could not get updated API key balance:", e);
5774
- satsSpent = fallbackSatsSpent ?? initialTokenBalance;
5775
5797
  }
5798
+ } finally {
5799
+ reader.releaseLock();
5776
5800
  }
5777
- await this._trackResponseUsage({
5778
- token,
5779
- baseUrl,
5780
- response,
5781
- modelId,
5782
- satsSpent,
5801
+ return {
5802
+ content: this.accumulatedContent,
5803
+ thinking: this.accumulatedThinking || void 0,
5804
+ images: this.accumulatedImages.length > 0 ? this.accumulatedImages : void 0,
5783
5805
  usage,
5784
- requestId,
5785
- clientApiKey
5786
- });
5787
- (async () => {
5788
- })();
5789
- return satsSpent;
5806
+ model,
5807
+ responseId,
5808
+ finish_reason,
5809
+ citations,
5810
+ annotations
5811
+ };
5790
5812
  }
5791
- async _trackResponseUsage(params) {
5792
- const {
5793
- token,
5794
- baseUrl,
5795
- response,
5796
- modelId,
5797
- satsSpent,
5798
- usage: providedUsage,
5799
- requestId: providedRequestId,
5800
- clientApiKey
5801
- } = params;
5802
- if (!response || !modelId) {
5803
- return;
5813
+ /**
5814
+ * Parse a single SSE line
5815
+ */
5816
+ _parseLine(line) {
5817
+ if (!line.trim()) return null;
5818
+ if (!line.startsWith("data: ")) {
5819
+ return null;
5820
+ }
5821
+ const jsonData = line.slice(6);
5822
+ if (jsonData === "[DONE]") {
5823
+ return null;
5804
5824
  }
5805
5825
  try {
5806
- let usage = providedUsage;
5807
- let requestId = providedRequestId;
5808
- if (!usage || !requestId) {
5809
- const contentType = response.headers.get("content-type") || "";
5810
- if (contentType.includes("text/event-stream")) {
5811
- usage = usage ?? response.usage;
5812
- requestId = requestId ?? response.requestId ?? response.headers.get("x-routstr-request-id") ?? void 0;
5813
- if (!usage) {
5814
- return;
5815
- }
5816
- } else {
5817
- const cloned = response.clone();
5818
- const responseBody = await cloned.json();
5819
- usage = usage ?? extractUsageFromResponseBody(responseBody, satsSpent) ?? void 0;
5820
- requestId = requestId ?? extractResponseId(responseBody) ?? response.headers.get("x-routstr-request-id") ?? void 0;
5821
- }
5826
+ const parsed = JSON.parse(jsonData);
5827
+ const result = {};
5828
+ if (parsed.choices?.[0]?.delta?.content) {
5829
+ result.content = parsed.choices[0].delta.content;
5822
5830
  }
5823
- if (!usage) {
5824
- return;
5831
+ if (parsed.choices?.[0]?.delta?.reasoning) {
5832
+ result.reasoning = parsed.choices[0].delta.reasoning;
5825
5833
  }
5826
- const finalRequestId = requestId || "unknown";
5827
- const store = this.sdkStore ?? await getDefaultSdkStore();
5828
- const state = store.getState();
5829
- const matchKey = clientApiKey ?? token;
5830
- const matchingClient = state.clientIds.find(
5831
- (client) => client.apiKey === matchKey
5832
- );
5833
- const entryId = finalRequestId === "unknown" ? `req-${Date.now()}-${modelId}` : finalRequestId;
5834
- const usageTracking = this.usageTrackingDriver ?? getDefaultUsageTrackingDriver();
5835
- const entry = {
5836
- id: entryId,
5837
- timestamp: Date.now(),
5838
- modelId,
5839
- baseUrl,
5840
- requestId: finalRequestId,
5841
- client: matchingClient?.clientId,
5842
- ...usage
5843
- };
5844
- if (this.mode === "xcashu") {
5845
- entry.satsCost = satsSpent;
5834
+ const extractedUsage = extractUsageFromSSEJson(parsed);
5835
+ if (extractedUsage) {
5836
+ result.usage = toUsageStats(extractedUsage);
5837
+ } else if (parsed.usage) {
5838
+ result.usage = {
5839
+ total_tokens: parsed.usage.total_tokens ?? parsed.usage.input_tokens + parsed.usage.output_tokens,
5840
+ prompt_tokens: parsed.usage.prompt_tokens ?? parsed.usage.input_tokens,
5841
+ completion_tokens: parsed.usage.completion_tokens ?? parsed.usage.output_tokens
5842
+ };
5846
5843
  }
5847
- await usageTracking.append(entry);
5848
- } catch (error) {
5844
+ if (parsed.id) {
5845
+ result.responseId = parsed.id;
5846
+ }
5847
+ if (parsed.model) {
5848
+ result.model = parsed.model;
5849
+ }
5850
+ if (parsed.citations) {
5851
+ result.citations = parsed.citations;
5852
+ }
5853
+ if (parsed.annotations) {
5854
+ result.annotations = parsed.annotations;
5855
+ }
5856
+ if (parsed.choices?.[0]?.finish_reason) {
5857
+ result.finish_reason = parsed.choices[0].finish_reason;
5858
+ }
5859
+ const images = parsed.choices?.[0]?.message?.images || parsed.choices?.[0]?.delta?.images;
5860
+ if (images && Array.isArray(images)) {
5861
+ result.images = images;
5862
+ }
5863
+ return result;
5864
+ } catch {
5865
+ return null;
5849
5866
  }
5850
5867
  }
5851
5868
  /**
5852
- * Convert messages for API format
5853
- */
5854
- async _convertMessages(messages) {
5855
- return Promise.all(
5856
- messages.filter((m) => m.role !== "system").map(async (m) => ({
5857
- role: m.role,
5858
- content: typeof m.content === "string" ? m.content : m.content
5859
- }))
5860
- );
5861
- }
5862
- /**
5863
- * Create assistant message from streaming result
5869
+ * Handle content delta with thinking support
5864
5870
  */
5865
- async _createAssistantMessage(result) {
5866
- if (result.images && result.images.length > 0) {
5867
- const content = [];
5868
- if (result.content) {
5869
- content.push({
5870
- type: "text",
5871
- text: result.content,
5872
- thinking: result.thinking,
5873
- citations: result.citations,
5874
- annotations: result.annotations
5875
- });
5876
- }
5877
- for (const img of result.images) {
5878
- content.push({
5879
- type: "image_url",
5880
- image_url: {
5881
- url: img.image_url.url
5882
- }
5883
- });
5884
- }
5885
- return {
5886
- role: "assistant",
5887
- content
5888
- };
5871
+ _handleContent(content, callbacks, modelId) {
5872
+ if (this.isInThinking && !this.isInContent) {
5873
+ this.accumulatedThinking += "</thinking>";
5874
+ callbacks.onThinking(this.accumulatedThinking);
5875
+ this.isInThinking = false;
5876
+ this.isInContent = true;
5877
+ }
5878
+ if (modelId) {
5879
+ this._extractThinkingFromContent(content, callbacks);
5880
+ } else {
5881
+ this.accumulatedContent += content;
5889
5882
  }
5890
- return {
5891
- role: "assistant",
5892
- content: result.content || ""
5893
- };
5883
+ callbacks.onContent(this.accumulatedContent);
5894
5884
  }
5895
5885
  /**
5896
- * Calculate estimated costs from usage
5886
+ * Handle thinking/reasoning content
5897
5887
  */
5898
- _getEstimatedCosts(selectedModel, streamingResult) {
5899
- let estimatedCosts = 0;
5900
- if (streamingResult.usage) {
5901
- const { completion_tokens, prompt_tokens } = streamingResult.usage;
5902
- if (completion_tokens !== void 0 && prompt_tokens !== void 0) {
5903
- estimatedCosts = (selectedModel.sats_pricing?.completion ?? 0) * completion_tokens + (selectedModel.sats_pricing?.prompt ?? 0) * prompt_tokens;
5904
- }
5888
+ _handleThinking(reasoning, callbacks) {
5889
+ if (!this.isInThinking) {
5890
+ this.accumulatedThinking += "<thinking> ";
5891
+ this.isInThinking = true;
5905
5892
  }
5906
- return estimatedCosts;
5893
+ this.accumulatedThinking += reasoning;
5894
+ callbacks.onThinking(this.accumulatedThinking);
5907
5895
  }
5908
5896
  /**
5909
- * Get pending API key amount
5897
+ * Extract thinking blocks from content (for models with inline thinking)
5910
5898
  */
5911
- _getPendingCashuTokenAmount() {
5912
- const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
5913
- return apiKeyDistribution.reduce((total, item) => total + item.amount, 0);
5899
+ _extractThinkingFromContent(content, callbacks) {
5900
+ const parts = content.split(/(<thinking>|<\/thinking>)/);
5901
+ for (const part of parts) {
5902
+ if (part === "<thinking>") {
5903
+ this.isInThinking = true;
5904
+ if (!this.accumulatedThinking.includes("<thinking>")) {
5905
+ this.accumulatedThinking += "<thinking> ";
5906
+ }
5907
+ } else if (part === "</thinking>") {
5908
+ this.isInThinking = false;
5909
+ this.accumulatedThinking += "</thinking>";
5910
+ } else if (this.isInThinking) {
5911
+ this.accumulatedThinking += part;
5912
+ } else {
5913
+ this.accumulatedContent += part;
5914
+ }
5915
+ }
5914
5916
  }
5915
5917
  /**
5916
- * Handle errors and notify callbacks
5918
+ * Merge images into accumulated array, avoiding duplicates
5917
5919
  */
5918
- _handleError(error, callbacks) {
5919
- this._log("ERROR", "[RoutstrClient] _handleError: Error occurred", error);
5920
- if (error instanceof Error) {
5921
- const isStreamError = error.message.includes("Error in input stream") || error.message.includes("Load failed");
5922
- const modifiedErrorMsg = isStreamError ? "AI stream was cut off, turn on Keep Active or please try again" : error.message;
5923
- this._log(
5924
- "ERROR",
5925
- `[RoutstrClient] _handleError: Error type=${error.constructor.name}, message=${modifiedErrorMsg}, isStreamError=${isStreamError}`
5926
- );
5920
+ _mergeImages(newImages) {
5921
+ for (const img of newImages) {
5922
+ const newUrl = img.image_url?.url;
5923
+ const existingIndex = this.accumulatedImages.findIndex((existing) => {
5924
+ const existingUrl = existing.image_url?.url;
5925
+ if (newUrl && existingUrl) {
5926
+ return existingUrl === newUrl;
5927
+ }
5928
+ if (img.index !== void 0 && existing.index !== void 0) {
5929
+ return existing.index === img.index;
5930
+ }
5931
+ return false;
5932
+ });
5933
+ if (existingIndex === -1) {
5934
+ this.accumulatedImages.push(img);
5935
+ } else {
5936
+ this.accumulatedImages[existingIndex] = img;
5937
+ }
5938
+ }
5939
+ }
5940
+ };
5941
+
5942
+ // client/fetchAIResponse.ts
5943
+ async function fetchAIResponse(options, callbacks, deps) {
5944
+ const {
5945
+ messageHistory,
5946
+ selectedModel,
5947
+ baseUrl,
5948
+ mintUrl,
5949
+ maxTokens,
5950
+ headers
5951
+ } = options;
5952
+ try {
5953
+ const apiMessages = await convertMessages(messageHistory);
5954
+ callbacks.onPaymentProcessing?.(true);
5955
+ callbacks.onTokenCreated?.(deps.getPendingCashuTokenAmount?.() ?? 0);
5956
+ const providerInfo = await deps.providerRegistry.getProviderInfo(baseUrl);
5957
+ const providerVersion = providerInfo?.version ?? "";
5958
+ let modelIdForRequest = selectedModel.id;
5959
+ if (/^0\.1\./.test(providerVersion)) {
5960
+ const newModel = await deps.client.getProviderManager().getModelForProvider(baseUrl, selectedModel.id);
5961
+ modelIdForRequest = newModel?.id ?? selectedModel.id;
5962
+ }
5963
+ const body = {
5964
+ model: modelIdForRequest,
5965
+ messages: apiMessages,
5966
+ stream: true
5967
+ };
5968
+ if (maxTokens !== void 0) {
5969
+ body.max_tokens = maxTokens;
5970
+ }
5971
+ if (selectedModel?.name?.startsWith("OpenAI:")) {
5972
+ body.tools = [{ type: "web_search" }];
5973
+ }
5974
+ const response = await deps.client.routeRequest({
5975
+ path: "/v1/chat/completions",
5976
+ method: "POST",
5977
+ body,
5978
+ headers,
5979
+ baseUrl,
5980
+ mintUrl,
5981
+ modelId: selectedModel.id
5982
+ });
5983
+ if (!response.body) {
5984
+ throw new Error("Response body is not available");
5985
+ }
5986
+ if (response.status !== 200) {
5987
+ throw new Error(`${response.status} ${response.statusText}`);
5988
+ }
5989
+ const streamProcessor = new StreamProcessor();
5990
+ const streamingResult = await streamProcessor.process(
5991
+ response,
5992
+ {
5993
+ onContent: callbacks.onStreamingUpdate,
5994
+ onThinking: callbacks.onThinkingUpdate
5995
+ },
5996
+ selectedModel.id
5997
+ );
5998
+ if (streamingResult.finish_reason === "content_filter") {
5927
5999
  callbacks.onMessageAppend({
5928
- role: "system",
5929
- content: "Uncaught Error: " + modifiedErrorMsg + (this.alertLevel === "max" ? " | " + error.stack : "")
6000
+ role: "assistant",
6001
+ content: "Your request was denied due to content filtering."
5930
6002
  });
6003
+ } else if (streamingResult.content || streamingResult.images && streamingResult.images.length > 0) {
6004
+ const message = await createAssistantMessage(streamingResult);
6005
+ callbacks.onMessageAppend(message);
5931
6006
  } else {
5932
6007
  callbacks.onMessageAppend({
5933
6008
  role: "system",
5934
- content: "Unknown Error: Please tag Routstr on Nostr and/or retry."
6009
+ content: "The provider did not respond to this request."
5935
6010
  });
5936
6011
  }
6012
+ callbacks.onStreamingUpdate("");
6013
+ callbacks.onThinkingUpdate("");
6014
+ } catch (error) {
6015
+ handleError(error, callbacks, deps.alertLevel, deps.logger);
6016
+ } finally {
6017
+ callbacks.onPaymentProcessing?.(false);
5937
6018
  }
5938
- /**
5939
- * Check wallet balance and throw if insufficient
5940
- */
5941
- async _checkBalance() {
5942
- const balances = await this.walletAdapter.getBalances();
5943
- const totalBalance = Object.values(balances).reduce((sum, v) => sum + v, 0);
5944
- if (totalBalance <= 0) {
5945
- throw new InsufficientBalanceError(1, 0);
6019
+ }
6020
+ async function convertMessages(messages) {
6021
+ return Promise.all(
6022
+ messages.filter((m) => m.role !== "system").map(async (m) => ({
6023
+ role: m.role,
6024
+ content: typeof m.content === "string" ? m.content : m.content
6025
+ }))
6026
+ );
6027
+ }
6028
+ async function createAssistantMessage(result) {
6029
+ if (result.images && result.images.length > 0) {
6030
+ const content = [];
6031
+ if (result.content) {
6032
+ content.push({
6033
+ type: "text",
6034
+ text: result.content,
6035
+ thinking: result.thinking,
6036
+ citations: result.citations,
6037
+ annotations: result.annotations
6038
+ });
5946
6039
  }
5947
- }
5948
- /**
5949
- * Spend a token using CashuSpender with standardized error handling
5950
- */
5951
- async _spendToken(params) {
5952
- const { mintUrl, amount, baseUrl } = params;
5953
- this._log(
5954
- "DEBUG",
5955
- `[RoutstrClient] _spendToken: mode=${this.mode}, amount=${amount}, baseUrl=${baseUrl}, mintUrl=${mintUrl}`
5956
- );
5957
- if (this.mode === "apikeys") {
5958
- let parentApiKey = this.storageAdapter.getApiKey(baseUrl);
5959
- if (!parentApiKey) {
5960
- this._log(
5961
- "DEBUG",
5962
- `[RoutstrClient] _spendToken: No existing API key for ${baseUrl}, creating new one via Cashu`
5963
- );
5964
- const spendResult2 = await this.cashuSpender.spend({
5965
- mintUrl,
5966
- amount: amount * TOPUP_MARGIN,
5967
- baseUrl: "",
5968
- reuseToken: false
5969
- });
5970
- if (!spendResult2.token) {
5971
- this._log(
5972
- "ERROR",
5973
- `[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error:`,
5974
- spendResult2.error
5975
- );
5976
- throw new Error(
5977
- `[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error: ${spendResult2.error}`
5978
- );
5979
- } else {
5980
- this._log(
5981
- "DEBUG",
5982
- `[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult2.token}`
5983
- );
5984
- }
5985
- this._log(
5986
- "DEBUG",
5987
- `[RoutstrClient] _spendToken: Created API key for ${baseUrl}, key preview: ${spendResult2.token}, balance: ${spendResult2.balance}`
5988
- );
5989
- try {
5990
- this.storageAdapter.setApiKey(baseUrl, spendResult2.token);
5991
- } catch (error) {
5992
- if (error instanceof Error && error.message.includes("ApiKey already exists")) {
5993
- const receiveResult = await this.cashuSpender.receiveToken(
5994
- spendResult2.token
5995
- );
5996
- if (receiveResult.success) {
5997
- this._log(
5998
- "DEBUG",
5999
- `[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${receiveResult.amount}`
6000
- );
6001
- } else {
6002
- this._log(
6003
- "DEBUG",
6004
- `[RoutstrClient] _handleErrorResponse: Token restore failed: ${receiveResult.message}`
6005
- );
6006
- }
6007
- this._log(
6008
- "DEBUG",
6009
- `[RoutstrClient] _spendToken: API key already exists for ${baseUrl}, using existing key`
6010
- );
6011
- } else {
6012
- throw error;
6013
- }
6014
- }
6015
- parentApiKey = this.storageAdapter.getApiKey(baseUrl);
6016
- } else {
6017
- this._log(
6018
- "DEBUG",
6019
- `[RoutstrClient] _spendToken: Using existing API key for ${baseUrl}, key preview: ${parentApiKey.key}`
6020
- );
6021
- }
6022
- let tokenBalance = 0;
6023
- let tokenBalanceUnit = "sat";
6024
- const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
6025
- const distributionForBaseUrl = apiKeyDistribution.find(
6026
- (d) => d.baseUrl === baseUrl
6027
- );
6028
- if (distributionForBaseUrl) {
6029
- tokenBalance = distributionForBaseUrl.amount;
6030
- }
6031
- if (tokenBalance === 0 && parentApiKey) {
6032
- try {
6033
- const balanceInfo = await this.balanceManager.getTokenBalance(
6034
- parentApiKey.key,
6035
- baseUrl
6036
- );
6037
- tokenBalance = balanceInfo.amount;
6038
- tokenBalanceUnit = balanceInfo.unit;
6039
- } catch (e) {
6040
- this._log("WARN", "Could not get initial API key balance:", e);
6040
+ for (const img of result.images) {
6041
+ content.push({
6042
+ type: "image_url",
6043
+ image_url: {
6044
+ url: img.image_url.url
6041
6045
  }
6042
- }
6043
- this._log(
6044
- "DEBUG",
6045
- `[RoutstrClient] _spendToken: Returning token with balance=${tokenBalance} ${tokenBalanceUnit}`
6046
- );
6047
- return {
6048
- token: parentApiKey?.key ?? "",
6049
- tokenBalance,
6050
- tokenBalanceUnit
6051
- };
6052
- }
6053
- this._log(
6054
- "DEBUG",
6055
- `[RoutstrClient] _spendToken: Calling CashuSpender.spend for amount=${amount}, mintUrl=${mintUrl}, mode=${this.mode}`
6056
- );
6057
- const spendResult = await this.cashuSpender.spend({
6058
- mintUrl,
6059
- amount,
6060
- baseUrl: "",
6061
- reuseToken: false
6062
- });
6063
- if (!spendResult.token) {
6064
- this._log(
6065
- "ERROR",
6066
- `[RoutstrClient] _spendToken: CashuSpender.spend failed, error:`,
6067
- spendResult.error
6068
- );
6069
- } else {
6070
- this._log(
6071
- "DEBUG",
6072
- `[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult.token}, balance: ${spendResult.balance} ${spendResult.unit ?? "sat"}`
6073
- );
6074
- this.storageAdapter.addXcashuToken(baseUrl, spendResult.token);
6046
+ });
6075
6047
  }
6076
6048
  return {
6077
- token: spendResult.token,
6078
- tokenBalance: spendResult.balance,
6079
- tokenBalanceUnit: spendResult.unit ?? "sat"
6080
- };
6081
- }
6082
- /**
6083
- * Build request headers with common defaults and dev mock controls
6084
- */
6085
- _buildBaseHeaders(additionalHeaders = {}, token) {
6086
- const headers = {
6087
- ...additionalHeaders,
6088
- "Content-Type": "application/json"
6049
+ role: "assistant",
6050
+ content
6089
6051
  };
6090
- return headers;
6091
6052
  }
6092
- /**
6093
- * Attach auth headers using the active client mode
6094
- */
6095
- _withAuthHeader(headers, token) {
6096
- const nextHeaders = { ...headers };
6097
- if (this.mode === "xcashu") {
6098
- nextHeaders["X-Cashu"] = token;
6099
- } else {
6100
- nextHeaders["Authorization"] = `Bearer ${token}`;
6101
- }
6102
- return nextHeaders;
6053
+ return {
6054
+ role: "assistant",
6055
+ content: result.content || ""
6056
+ };
6057
+ }
6058
+ function handleError(error, callbacks, alertLevel, logger) {
6059
+ logger.error("[fetchAIResponse] Error occurred", error);
6060
+ if (error instanceof Error) {
6061
+ const isStreamError = error.message.includes("Error in input stream") || error.message.includes("Load failed");
6062
+ const modifiedErrorMsg = isStreamError ? "AI stream was cut off, turn on Keep Active or please try again" : error.message;
6063
+ logger.error(
6064
+ `[fetchAIResponse] Error type=${error.constructor.name}, message=${modifiedErrorMsg}, isStreamError=${isStreamError}`
6065
+ );
6066
+ callbacks.onMessageAppend({
6067
+ role: "system",
6068
+ content: "Uncaught Error: " + modifiedErrorMsg + (alertLevel === "max" ? " | " + error.stack : "")
6069
+ });
6070
+ } else {
6071
+ callbacks.onMessageAppend({
6072
+ role: "system",
6073
+ content: "Unknown Error: Please tag Routstr on Nostr and/or retry."
6074
+ });
6103
6075
  }
6104
- };
6076
+ }
6105
6077
 
6106
6078
  // routeRequests.ts
6107
6079
  async function resolveRouteRequestContext(options) {
@@ -6253,6 +6225,6 @@ function extractStream(requestBody) {
6253
6225
  return typeof stream === "boolean" ? stream : void 0;
6254
6226
  }
6255
6227
 
6256
- 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, createProviderRegistryFromStore, createSSEParserTransform, createSdkStore, createSqliteDriver, createSqliteUsageTrackingDriver, createStorageAdapterFromStore, filterBaseUrlsForTor, getDefaultDiscoveryAdapter, getDefaultProviderRegistry, getDefaultSdkDriver, getDefaultSdkStore, getDefaultStorageAdapter, getDefaultUsageTrackingDriver, getProviderEndpoints, inspectSSEWebStream, isOnionUrl, isTorContext, localStorageDriver, noopLogger, normalizeProviderUrl, routeRequests, setDefaultUsageTrackingDriver };
6228
+ 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, fetchAIResponse, filterBaseUrlsForTor, getDefaultDiscoveryAdapter, getDefaultProviderRegistry, getDefaultSdkDriver, getDefaultSdkStore, getDefaultStorageAdapter, getDefaultUsageTrackingDriver, getProviderEndpoints, inspectSSEWebStream, isOnionUrl, isTorContext, localStorageDriver, noopLogger, normalizeProviderUrl, routeRequests, setDefaultUsageTrackingDriver };
6257
6229
  //# sourceMappingURL=index.mjs.map
6258
6230
  //# sourceMappingURL=index.mjs.map