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