@routstr/sdk 0.3.9 → 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 (59) 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 +801 -1291
  18. package/dist/client/index.js.map +1 -1
  19. package/dist/client/index.mjs +801 -1292
  20. package/dist/client/index.mjs.map +1 -1
  21. package/dist/discovery/index.d.mts +33 -3
  22. package/dist/discovery/index.d.ts +33 -3
  23. package/dist/discovery/index.js +28 -21
  24. package/dist/discovery/index.js.map +1 -1
  25. package/dist/discovery/index.mjs +28 -21
  26. package/dist/discovery/index.mjs.map +1 -1
  27. package/dist/index.d.mts +4 -4
  28. package/dist/index.d.ts +4 -4
  29. package/dist/index.js +1045 -1564
  30. package/dist/index.js.map +1 -1
  31. package/dist/index.mjs +1045 -1561
  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 +4 -30
  46. package/dist/storage/index.d.ts +4 -30
  47. package/dist/storage/index.js +139 -650
  48. package/dist/storage/index.js.map +1 -1
  49. package/dist/storage/index.mjs +140 -647
  50. package/dist/storage/index.mjs.map +1 -1
  51. package/dist/storage/node.d.mts +22 -0
  52. package/dist/storage/node.d.ts +22 -0
  53. package/dist/storage/node.js +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/package.json +26 -1
package/dist/index.mjs CHANGED
@@ -137,9 +137,6 @@ var MintDiscoveryError = class extends Error {
137
137
  }
138
138
  baseUrl;
139
139
  };
140
- function isBunRuntime() {
141
- return typeof Bun !== "undefined";
142
- }
143
140
  var ModelManager = class _ModelManager {
144
141
  constructor(adapter, config = {}) {
145
142
  this.adapter = adapter;
@@ -148,8 +145,10 @@ var ModelManager = class _ModelManager {
148
145
  this.includeProviderUrls = config.includeProviderUrls || [];
149
146
  this.excludeProviderUrls = config.excludeProviderUrls || [];
150
147
  this.routstrPubkey = config.routstrPubkey || "4ad6fa2d16e2a9b576c863b4cf7404a70d4dc320c0c447d10ad6ff58993eacc8";
148
+ this.nostrRelays = config.nostrRelays;
151
149
  this.logger = (config.logger ?? consoleLogger).child("ModelManager");
152
150
  this.eventStoreDbPath = config.eventStoreDbPath;
151
+ this.persistentEventDatabaseFactory = config.persistentEventDatabaseFactory;
153
152
  }
154
153
  adapter;
155
154
  cacheTTL;
@@ -157,6 +156,7 @@ var ModelManager = class _ModelManager {
157
156
  includeProviderUrls;
158
157
  excludeProviderUrls;
159
158
  routstrPubkey;
159
+ nostrRelays;
160
160
  logger;
161
161
  providerNodePubkeysByUrl = /* @__PURE__ */ new Map();
162
162
  /** Persistent event store for relay-fetched events (null if not configured/initialized) */
@@ -164,6 +164,7 @@ var ModelManager = class _ModelManager {
164
164
  eventStoreDb = null;
165
165
  eventStoreInitPromise = null;
166
166
  eventStoreDbPath;
167
+ persistentEventDatabaseFactory;
167
168
  /**
168
169
  * Get the list of bootstrapped provider base URLs
169
170
  * @returns Array of provider base URLs
@@ -192,7 +193,7 @@ var ModelManager = class _ModelManager {
192
193
  } catch (error) {
193
194
  this.eventStoreInitPromise = null;
194
195
  throw new Error(
195
- `applesauce-sqlite with a supported SQLite driver is required for persistent Nostr event storage. Bun uses bun:sqlite; Node.js uses better-sqlite3. Install optional dependencies or omit eventStoreDbPath. (${error})`
196
+ `Persistent Nostr event storage requires a runtime-specific database factory. Use @routstr/sdk/node, @routstr/sdk/bun, inject persistentEventDatabaseFactory, or omit eventStoreDbPath. (${error})`
196
197
  );
197
198
  }
198
199
  })();
@@ -207,16 +208,15 @@ var ModelManager = class _ModelManager {
207
208
  return this.ensureEventStore();
208
209
  }
209
210
  async createPersistentEventDatabase() {
210
- if (isBunRuntime()) {
211
- const { BunSqliteEventDatabase } = await import('applesauce-sqlite/bun');
212
- return new BunSqliteEventDatabase(
213
- this.eventStoreDbPath
211
+ if (!this.eventStoreDbPath) {
212
+ throw new Error("eventStoreDbPath is required");
213
+ }
214
+ if (!this.persistentEventDatabaseFactory) {
215
+ throw new Error(
216
+ "persistentEventDatabaseFactory is required. Import ModelManager from @routstr/sdk/node or @routstr/sdk/bun for SQLite-backed persistent event storage."
214
217
  );
215
218
  }
216
- const { BetterSqlite3EventDatabase } = await import('applesauce-sqlite/better-sqlite3');
217
- return new BetterSqlite3EventDatabase(
218
- this.eventStoreDbPath
219
- );
219
+ return this.persistentEventDatabaseFactory(this.eventStoreDbPath);
220
220
  }
221
221
  /** Close the persistent event store database handle, if configured. */
222
222
  closeEventStore() {
@@ -329,6 +329,13 @@ var ModelManager = class _ModelManager {
329
329
  }
330
330
  return this.bootstrapFromHttp(torMode, forceRefresh);
331
331
  }
332
+ /**
333
+ * Resolve Nostr relay URLs for a given use case.
334
+ * Returns user-configured relays if set, otherwise the provided defaults.
335
+ */
336
+ getNostrRelays(defaults) {
337
+ return this.nostrRelays && this.nostrRelays.length > 0 ? this.nostrRelays : defaults;
338
+ }
332
339
  /**
333
340
  * Bootstrap providers from Nostr network (kind 38421)
334
341
  * @param kind The Nostr kind to fetch
@@ -336,11 +343,11 @@ var ModelManager = class _ModelManager {
336
343
  * @returns Array of provider base URLs
337
344
  */
338
345
  async bootstrapFromNostr(kind, torMode, forceRefresh = false) {
339
- const DEFAULT_RELAYS = [
346
+ const relays = this.getNostrRelays([
340
347
  "wss://relay.primal.net",
341
348
  "wss://nos.lol",
342
349
  "wss://relay.damus.io"
343
- ];
350
+ ]);
344
351
  const cached = await this.getCachedNostrEvents(
345
352
  { kinds: [kind] },
346
353
  this.cacheTTL,
@@ -351,7 +358,7 @@ var ModelManager = class _ModelManager {
351
358
  const pool = new RelayPool();
352
359
  const timeoutMs = 5e3;
353
360
  await new Promise((resolve) => {
354
- pool.req(DEFAULT_RELAYS, {
361
+ pool.req(relays, {
355
362
  kinds: [kind],
356
363
  limit: 100
357
364
  }).pipe(
@@ -523,16 +530,16 @@ var ModelManager = class _ModelManager {
523
530
  );
524
531
  let sessionEvents = cached;
525
532
  if (cached.length === 0) {
526
- const LGTM_RELAYS = [
533
+ const lgtmRelays = this.getNostrRelays([
527
534
  "wss://relay.primal.net",
528
535
  "wss://nos.lol",
529
536
  "wss://relay.damus.io",
530
537
  "wss://relay.routstr.com"
531
- ];
538
+ ]);
532
539
  const pool = new RelayPool();
533
540
  const timeoutMs = 5e3;
534
541
  await new Promise((resolve) => {
535
- pool.req(LGTM_RELAYS, {
542
+ pool.req(lgtmRelays, {
536
543
  kinds: [38425],
537
544
  "#t": ["lgtm"],
538
545
  limit: 500,
@@ -774,11 +781,11 @@ var ModelManager = class _ModelManager {
774
781
  return cachedModels;
775
782
  }
776
783
  }
777
- const DEFAULT_RELAYS = [
784
+ const relays = this.getNostrRelays([
778
785
  "wss://relay.damus.io",
779
786
  "wss://nos.lol",
780
787
  "wss://relay.routstr.com"
781
- ];
788
+ ]);
782
789
  const cached = await this.getCachedNostrEvents(
783
790
  { kinds: [38423], "#d": ["routstr-21-models"], authors: [this.routstrPubkey] },
784
791
  this.cacheTTL,
@@ -789,7 +796,7 @@ var ModelManager = class _ModelManager {
789
796
  const pool = new RelayPool();
790
797
  const timeoutMs = 5e3;
791
798
  await new Promise((resolve) => {
792
- pool.req(DEFAULT_RELAYS, {
799
+ pool.req(relays, {
793
800
  kinds: [38423],
794
801
  "#d": ["routstr-21-models"],
795
802
  limit: 1,
@@ -2335,470 +2342,154 @@ var BalanceManager = class _BalanceManager {
2335
2342
  }
2336
2343
  };
2337
2344
 
2338
- // client/usage.ts
2339
- function extractUsageFromResponseBody(body, fallbackSatsCost = 0) {
2340
- if (!body || typeof body !== "object") return null;
2341
- const usage = body.usage;
2342
- if (!usage || typeof usage !== "object") return null;
2343
- const promptTokens = Number(usage.prompt_tokens ?? 0);
2344
- const completionTokens = Number(usage.completion_tokens ?? 0);
2345
- const totalTokens = Number(usage.total_tokens ?? 0);
2346
- const costValue = usage.cost;
2347
- let cost = 0;
2348
- let satsCost = fallbackSatsCost;
2349
- if (typeof costValue === "number") {
2350
- cost = costValue;
2351
- } else if (costValue && typeof costValue === "object") {
2352
- const costObj = costValue;
2353
- const totalUsd = costObj.total_usd;
2354
- const totalMsats = costObj.total_msats;
2355
- cost = typeof totalUsd === "number" ? totalUsd : 0;
2356
- if (typeof totalMsats === "number") {
2357
- satsCost = totalMsats / 1e3;
2358
- }
2345
+ // utils/torUtils.ts
2346
+ var TOR_ONION_SUFFIX = ".onion";
2347
+ var isTorContext = () => {
2348
+ if (typeof window === "undefined") return false;
2349
+ const hostname = window.location.hostname.toLowerCase();
2350
+ return hostname.endsWith(TOR_ONION_SUFFIX);
2351
+ };
2352
+ var isOnionUrl = (url) => {
2353
+ if (!url) return false;
2354
+ const trimmed = url.trim().toLowerCase();
2355
+ if (!trimmed) return false;
2356
+ try {
2357
+ const candidate = trimmed.startsWith("http") ? trimmed : `http://${trimmed}`;
2358
+ return new URL(candidate).hostname.endsWith(TOR_ONION_SUFFIX);
2359
+ } catch {
2360
+ return trimmed.includes(TOR_ONION_SUFFIX);
2359
2361
  }
2360
- if (promptTokens === 0 && completionTokens === 0 && totalTokens === 0 && cost === 0 && satsCost === 0) {
2361
- return null;
2362
+ };
2363
+ var shouldAllowHttp = (url, torMode) => {
2364
+ if (!url.startsWith("http://")) return true;
2365
+ if (url.includes("localhost") || url.includes("127.0.0.1")) return true;
2366
+ return torMode && isOnionUrl(url);
2367
+ };
2368
+ var normalizeProviderUrl = (url, torMode = false) => {
2369
+ if (!url || typeof url !== "string") return null;
2370
+ const trimmed = url.trim();
2371
+ if (!trimmed) return null;
2372
+ if (/^https?:\/\//i.test(trimmed)) {
2373
+ return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
2362
2374
  }
2363
- return {
2364
- promptTokens,
2365
- completionTokens,
2366
- totalTokens,
2367
- cost,
2368
- satsCost
2369
- };
2370
- }
2371
- function extractResponseId(body) {
2372
- if (!body || typeof body !== "object") return void 0;
2373
- const id = body.id;
2374
- if (typeof id !== "string") return void 0;
2375
- const trimmed = id.trim();
2376
- return trimmed.length > 0 ? trimmed : void 0;
2377
- }
2378
- function extractUsageFromSSEJson(parsed, fallbackSatsCost = 0) {
2379
- if (!parsed || typeof parsed !== "object") {
2380
- return null;
2375
+ const useHttpForOnion = torMode && isOnionUrl(trimmed);
2376
+ const withProto = `${useHttpForOnion ? "http" : "https"}://${trimmed}`;
2377
+ return withProto.endsWith("/") ? withProto : `${withProto}/`;
2378
+ };
2379
+ var dedupePreserveOrder = (urls) => {
2380
+ const seen = /* @__PURE__ */ new Set();
2381
+ const out = [];
2382
+ for (const url of urls) {
2383
+ if (!seen.has(url)) {
2384
+ seen.add(url);
2385
+ out.push(url);
2386
+ }
2381
2387
  }
2382
- if (!parsed.usage && parsed.cost && typeof parsed.cost === "object") {
2383
- const costObj = parsed.cost;
2384
- const msats2 = costObj.total_msats ?? 0;
2385
- const cost2 = costObj.total_usd ?? 0;
2386
- if (msats2 === 0 && cost2 === 0) return null;
2387
- return {
2388
- promptTokens: Number(costObj.input_tokens ?? 0),
2389
- completionTokens: Number(costObj.output_tokens ?? 0),
2390
- totalTokens: Number((costObj.input_tokens ?? 0) + (costObj.output_tokens ?? 0)),
2391
- cost: Number(cost2),
2392
- satsCost: msats2 > 0 ? msats2 / 1e3 : fallbackSatsCost
2393
- };
2388
+ return out;
2389
+ };
2390
+ var getProviderEndpoints = (provider, torMode) => {
2391
+ const rawUrls = [
2392
+ provider.endpoint_url,
2393
+ ...Array.isArray(provider.endpoint_urls) ? provider.endpoint_urls : [],
2394
+ provider.onion_url,
2395
+ ...Array.isArray(provider.onion_urls) ? provider.onion_urls : []
2396
+ ];
2397
+ const normalized = rawUrls.map((value) => normalizeProviderUrl(value, torMode)).filter((value) => Boolean(value));
2398
+ const unique = dedupePreserveOrder(normalized).filter(
2399
+ (value) => shouldAllowHttp(value, torMode)
2400
+ );
2401
+ if (unique.length === 0) return [];
2402
+ const onion = unique.filter((value) => isOnionUrl(value));
2403
+ const clearnet = unique.filter((value) => !isOnionUrl(value));
2404
+ if (torMode) {
2405
+ return onion.length > 0 ? onion : clearnet;
2394
2406
  }
2395
- if (!parsed.usage) {
2407
+ return clearnet;
2408
+ };
2409
+ var filterBaseUrlsForTor = (baseUrls, torMode) => {
2410
+ if (!Array.isArray(baseUrls)) return [];
2411
+ const normalized = baseUrls.map((value) => normalizeProviderUrl(value, torMode)).filter((value) => Boolean(value));
2412
+ const filtered = normalized.filter(
2413
+ (value) => torMode ? true : !isOnionUrl(value)
2414
+ );
2415
+ return dedupePreserveOrder(
2416
+ filtered.filter((value) => shouldAllowHttp(value, torMode))
2417
+ );
2418
+ };
2419
+
2420
+ // client/ProviderManager.ts
2421
+ function getImageResolutionFromDataUrl(dataUrl) {
2422
+ try {
2423
+ if (typeof dataUrl !== "string" || !dataUrl.startsWith("data:"))
2424
+ return null;
2425
+ const commaIdx = dataUrl.indexOf(",");
2426
+ if (commaIdx === -1) return null;
2427
+ const meta = dataUrl.slice(5, commaIdx);
2428
+ const base64 = dataUrl.slice(commaIdx + 1);
2429
+ const binary = typeof atob === "function" ? atob(base64) : Buffer.from(base64, "base64").toString("binary");
2430
+ const len = binary.length;
2431
+ const bytes = new Uint8Array(len);
2432
+ for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i);
2433
+ const isPNG = meta.includes("image/png");
2434
+ const isJPEG = meta.includes("image/jpeg") || meta.includes("image/jpg");
2435
+ if (isPNG) {
2436
+ const sig = [137, 80, 78, 71, 13, 10, 26, 10];
2437
+ for (let i = 0; i < sig.length; i++) {
2438
+ if (bytes[i] !== sig[i]) return null;
2439
+ }
2440
+ const view = new DataView(
2441
+ bytes.buffer,
2442
+ bytes.byteOffset,
2443
+ bytes.byteLength
2444
+ );
2445
+ const width = view.getUint32(16, false);
2446
+ const height = view.getUint32(20, false);
2447
+ if (width > 0 && height > 0) return { width, height };
2448
+ return null;
2449
+ }
2450
+ if (isJPEG) {
2451
+ let offset = 0;
2452
+ if (bytes[offset++] !== 255 || bytes[offset++] !== 216) return null;
2453
+ while (offset < bytes.length) {
2454
+ while (offset < bytes.length && bytes[offset] !== 255) offset++;
2455
+ if (offset + 1 >= bytes.length) break;
2456
+ while (bytes[offset] === 255) offset++;
2457
+ const marker = bytes[offset++];
2458
+ if (marker === 216 || marker === 217) continue;
2459
+ if (offset + 1 >= bytes.length) break;
2460
+ const length = bytes[offset] << 8 | bytes[offset + 1];
2461
+ offset += 2;
2462
+ if (marker === 192 || marker === 194) {
2463
+ if (length < 7 || offset + length - 2 > bytes.length) return null;
2464
+ const precision = bytes[offset];
2465
+ const height = bytes[offset + 1] << 8 | bytes[offset + 2];
2466
+ const width = bytes[offset + 3] << 8 | bytes[offset + 4];
2467
+ if (precision > 0 && width > 0 && height > 0)
2468
+ return { width, height };
2469
+ return null;
2470
+ } else {
2471
+ offset += length - 2;
2472
+ }
2473
+ }
2474
+ return null;
2475
+ }
2396
2476
  return null;
2397
- }
2398
- const usage = parsed.usage;
2399
- const usageCost = usage.cost;
2400
- let cost = 0;
2401
- let msats = 0;
2402
- if (typeof usageCost === "number") {
2403
- cost = usageCost;
2404
- } else if (usageCost && typeof usageCost === "object") {
2405
- cost = usageCost.total_usd ?? 0;
2406
- msats = usageCost.total_msats ?? 0;
2407
- }
2408
- if (cost === 0) {
2409
- cost = parsed.metadata?.routstr?.cost?.total_usd ?? 0;
2410
- }
2411
- if (msats === 0) {
2412
- msats = parsed.metadata?.routstr?.cost?.total_msats ?? (typeof usage.cost_sats === "number" ? usage.cost_sats * 1e3 : 0);
2413
- }
2414
- const promptTokens = Number(usage.prompt_tokens ?? usage.input_tokens ?? 0);
2415
- const completionTokens = Number(usage.completion_tokens ?? usage.output_tokens ?? 0);
2416
- const totalTokens = Number(usage.total_tokens ?? promptTokens + completionTokens);
2417
- const result = {
2418
- promptTokens,
2419
- completionTokens,
2420
- totalTokens,
2421
- cost: Number(cost ?? 0),
2422
- satsCost: msats > 0 ? msats / 1e3 : fallbackSatsCost
2423
- };
2424
- if (result.promptTokens === 0 && result.completionTokens === 0 && result.totalTokens === 0 && result.cost === 0 && result.satsCost === 0) {
2477
+ } catch {
2425
2478
  return null;
2426
2479
  }
2427
- return result;
2428
- }
2429
- function toUsageStats(usage) {
2430
- if (!usage) return void 0;
2431
- return {
2432
- total_tokens: usage.totalTokens,
2433
- prompt_tokens: usage.promptTokens,
2434
- completion_tokens: usage.completionTokens,
2435
- cost: usage.cost,
2436
- sats_cost: usage.satsCost
2437
- };
2438
2480
  }
2439
-
2440
- // client/StreamProcessor.ts
2441
- var StreamProcessor = class {
2442
- accumulatedContent = "";
2443
- accumulatedThinking = "";
2444
- accumulatedImages = [];
2445
- isInThinking = false;
2446
- isInContent = false;
2447
- /**
2448
- * Process a streaming response
2449
- */
2450
- async process(response, callbacks, modelId) {
2451
- if (!response.body) {
2452
- throw new Error("Response body is not available");
2453
- }
2454
- const reader = response.body.getReader();
2455
- const decoder = new TextDecoder("utf-8");
2456
- let buffer = "";
2457
- this.accumulatedContent = "";
2458
- this.accumulatedThinking = "";
2459
- this.accumulatedImages = [];
2460
- this.isInThinking = false;
2461
- this.isInContent = false;
2462
- let usage;
2463
- let model;
2464
- let finish_reason;
2465
- let citations;
2466
- let annotations;
2467
- let responseId;
2468
- try {
2469
- while (true) {
2470
- const { done, value } = await reader.read();
2471
- if (done) {
2472
- break;
2473
- }
2474
- const chunk = decoder.decode(value, { stream: true });
2475
- buffer += chunk;
2476
- const lines = buffer.split("\n");
2477
- buffer = lines.pop() || "";
2478
- for (const line of lines) {
2479
- const parsed = this._parseLine(line);
2480
- if (!parsed) continue;
2481
- if (parsed.content) {
2482
- this._handleContent(parsed.content, callbacks, modelId);
2483
- }
2484
- if (parsed.reasoning) {
2485
- this._handleThinking(parsed.reasoning, callbacks);
2486
- }
2487
- if (parsed.usage) {
2488
- usage = parsed.usage;
2489
- }
2490
- if (parsed.model) {
2491
- model = parsed.model;
2492
- }
2493
- if (parsed.finish_reason) {
2494
- finish_reason = parsed.finish_reason;
2495
- }
2496
- if (parsed.responseId) {
2497
- responseId = parsed.responseId;
2498
- }
2499
- if (parsed.citations) {
2500
- citations = parsed.citations;
2501
- }
2502
- if (parsed.annotations) {
2503
- annotations = parsed.annotations;
2504
- }
2505
- if (parsed.images) {
2506
- this._mergeImages(parsed.images);
2507
- }
2508
- }
2509
- }
2510
- } finally {
2511
- reader.releaseLock();
2512
- }
2513
- return {
2514
- content: this.accumulatedContent,
2515
- thinking: this.accumulatedThinking || void 0,
2516
- images: this.accumulatedImages.length > 0 ? this.accumulatedImages : void 0,
2517
- usage,
2518
- model,
2519
- responseId,
2520
- finish_reason,
2521
- citations,
2522
- annotations
2523
- };
2524
- }
2525
- /**
2526
- * Parse a single SSE line
2527
- */
2528
- _parseLine(line) {
2529
- if (!line.trim()) return null;
2530
- if (!line.startsWith("data: ")) {
2531
- return null;
2532
- }
2533
- const jsonData = line.slice(6);
2534
- if (jsonData === "[DONE]") {
2535
- return null;
2536
- }
2537
- try {
2538
- const parsed = JSON.parse(jsonData);
2539
- const result = {};
2540
- if (parsed.choices?.[0]?.delta?.content) {
2541
- result.content = parsed.choices[0].delta.content;
2542
- }
2543
- if (parsed.choices?.[0]?.delta?.reasoning) {
2544
- result.reasoning = parsed.choices[0].delta.reasoning;
2545
- }
2546
- const extractedUsage = extractUsageFromSSEJson(parsed);
2547
- if (extractedUsage) {
2548
- result.usage = toUsageStats(extractedUsage);
2549
- } else if (parsed.usage) {
2550
- result.usage = {
2551
- total_tokens: parsed.usage.total_tokens ?? parsed.usage.input_tokens + parsed.usage.output_tokens,
2552
- prompt_tokens: parsed.usage.prompt_tokens ?? parsed.usage.input_tokens,
2553
- completion_tokens: parsed.usage.completion_tokens ?? parsed.usage.output_tokens
2554
- };
2555
- }
2556
- if (parsed.id) {
2557
- result.responseId = parsed.id;
2558
- }
2559
- if (parsed.model) {
2560
- result.model = parsed.model;
2561
- }
2562
- if (parsed.citations) {
2563
- result.citations = parsed.citations;
2564
- }
2565
- if (parsed.annotations) {
2566
- result.annotations = parsed.annotations;
2567
- }
2568
- if (parsed.choices?.[0]?.finish_reason) {
2569
- result.finish_reason = parsed.choices[0].finish_reason;
2570
- }
2571
- const images = parsed.choices?.[0]?.message?.images || parsed.choices?.[0]?.delta?.images;
2572
- if (images && Array.isArray(images)) {
2573
- result.images = images;
2574
- }
2575
- return result;
2576
- } catch {
2577
- return null;
2578
- }
2579
- }
2580
- /**
2581
- * Handle content delta with thinking support
2582
- */
2583
- _handleContent(content, callbacks, modelId) {
2584
- if (this.isInThinking && !this.isInContent) {
2585
- this.accumulatedThinking += "</thinking>";
2586
- callbacks.onThinking(this.accumulatedThinking);
2587
- this.isInThinking = false;
2588
- this.isInContent = true;
2589
- }
2590
- if (modelId) {
2591
- this._extractThinkingFromContent(content, callbacks);
2592
- } else {
2593
- this.accumulatedContent += content;
2594
- }
2595
- callbacks.onContent(this.accumulatedContent);
2596
- }
2597
- /**
2598
- * Handle thinking/reasoning content
2599
- */
2600
- _handleThinking(reasoning, callbacks) {
2601
- if (!this.isInThinking) {
2602
- this.accumulatedThinking += "<thinking> ";
2603
- this.isInThinking = true;
2604
- }
2605
- this.accumulatedThinking += reasoning;
2606
- callbacks.onThinking(this.accumulatedThinking);
2607
- }
2608
- /**
2609
- * Extract thinking blocks from content (for models with inline thinking)
2610
- */
2611
- _extractThinkingFromContent(content, callbacks) {
2612
- const parts = content.split(/(<thinking>|<\/thinking>)/);
2613
- for (const part of parts) {
2614
- if (part === "<thinking>") {
2615
- this.isInThinking = true;
2616
- if (!this.accumulatedThinking.includes("<thinking>")) {
2617
- this.accumulatedThinking += "<thinking> ";
2618
- }
2619
- } else if (part === "</thinking>") {
2620
- this.isInThinking = false;
2621
- this.accumulatedThinking += "</thinking>";
2622
- } else if (this.isInThinking) {
2623
- this.accumulatedThinking += part;
2624
- } else {
2625
- this.accumulatedContent += part;
2626
- }
2627
- }
2628
- }
2629
- /**
2630
- * Merge images into accumulated array, avoiding duplicates
2631
- */
2632
- _mergeImages(newImages) {
2633
- for (const img of newImages) {
2634
- const newUrl = img.image_url?.url;
2635
- const existingIndex = this.accumulatedImages.findIndex((existing) => {
2636
- const existingUrl = existing.image_url?.url;
2637
- if (newUrl && existingUrl) {
2638
- return existingUrl === newUrl;
2639
- }
2640
- if (img.index !== void 0 && existing.index !== void 0) {
2641
- return existing.index === img.index;
2642
- }
2643
- return false;
2644
- });
2645
- if (existingIndex === -1) {
2646
- this.accumulatedImages.push(img);
2647
- } else {
2648
- this.accumulatedImages[existingIndex] = img;
2649
- }
2650
- }
2651
- }
2652
- };
2653
-
2654
- // utils/torUtils.ts
2655
- var TOR_ONION_SUFFIX = ".onion";
2656
- var isTorContext = () => {
2657
- if (typeof window === "undefined") return false;
2658
- const hostname = window.location.hostname.toLowerCase();
2659
- return hostname.endsWith(TOR_ONION_SUFFIX);
2660
- };
2661
- var isOnionUrl = (url) => {
2662
- if (!url) return false;
2663
- const trimmed = url.trim().toLowerCase();
2664
- if (!trimmed) return false;
2665
- try {
2666
- const candidate = trimmed.startsWith("http") ? trimmed : `http://${trimmed}`;
2667
- return new URL(candidate).hostname.endsWith(TOR_ONION_SUFFIX);
2668
- } catch {
2669
- return trimmed.includes(TOR_ONION_SUFFIX);
2670
- }
2671
- };
2672
- var shouldAllowHttp = (url, torMode) => {
2673
- if (!url.startsWith("http://")) return true;
2674
- if (url.includes("localhost") || url.includes("127.0.0.1")) return true;
2675
- return torMode && isOnionUrl(url);
2676
- };
2677
- var normalizeProviderUrl = (url, torMode = false) => {
2678
- if (!url || typeof url !== "string") return null;
2679
- const trimmed = url.trim();
2680
- if (!trimmed) return null;
2681
- if (/^https?:\/\//i.test(trimmed)) {
2682
- return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
2683
- }
2684
- const useHttpForOnion = torMode && isOnionUrl(trimmed);
2685
- const withProto = `${useHttpForOnion ? "http" : "https"}://${trimmed}`;
2686
- return withProto.endsWith("/") ? withProto : `${withProto}/`;
2687
- };
2688
- var dedupePreserveOrder = (urls) => {
2689
- const seen = /* @__PURE__ */ new Set();
2690
- const out = [];
2691
- for (const url of urls) {
2692
- if (!seen.has(url)) {
2693
- seen.add(url);
2694
- out.push(url);
2695
- }
2696
- }
2697
- return out;
2698
- };
2699
- var getProviderEndpoints = (provider, torMode) => {
2700
- const rawUrls = [
2701
- provider.endpoint_url,
2702
- ...Array.isArray(provider.endpoint_urls) ? provider.endpoint_urls : [],
2703
- provider.onion_url,
2704
- ...Array.isArray(provider.onion_urls) ? provider.onion_urls : []
2705
- ];
2706
- const normalized = rawUrls.map((value) => normalizeProviderUrl(value, torMode)).filter((value) => Boolean(value));
2707
- const unique = dedupePreserveOrder(normalized).filter(
2708
- (value) => shouldAllowHttp(value, torMode)
2709
- );
2710
- if (unique.length === 0) return [];
2711
- const onion = unique.filter((value) => isOnionUrl(value));
2712
- const clearnet = unique.filter((value) => !isOnionUrl(value));
2713
- if (torMode) {
2714
- return onion.length > 0 ? onion : clearnet;
2715
- }
2716
- return clearnet;
2717
- };
2718
- var filterBaseUrlsForTor = (baseUrls, torMode) => {
2719
- if (!Array.isArray(baseUrls)) return [];
2720
- const normalized = baseUrls.map((value) => normalizeProviderUrl(value, torMode)).filter((value) => Boolean(value));
2721
- const filtered = normalized.filter(
2722
- (value) => torMode ? true : !isOnionUrl(value)
2723
- );
2724
- return dedupePreserveOrder(
2725
- filtered.filter((value) => shouldAllowHttp(value, torMode))
2726
- );
2727
- };
2728
-
2729
- // client/ProviderManager.ts
2730
- function getImageResolutionFromDataUrl(dataUrl) {
2731
- try {
2732
- if (typeof dataUrl !== "string" || !dataUrl.startsWith("data:"))
2733
- return null;
2734
- const commaIdx = dataUrl.indexOf(",");
2735
- if (commaIdx === -1) return null;
2736
- const meta = dataUrl.slice(5, commaIdx);
2737
- const base64 = dataUrl.slice(commaIdx + 1);
2738
- const binary = typeof atob === "function" ? atob(base64) : Buffer.from(base64, "base64").toString("binary");
2739
- const len = binary.length;
2740
- const bytes = new Uint8Array(len);
2741
- for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i);
2742
- const isPNG = meta.includes("image/png");
2743
- const isJPEG = meta.includes("image/jpeg") || meta.includes("image/jpg");
2744
- if (isPNG) {
2745
- const sig = [137, 80, 78, 71, 13, 10, 26, 10];
2746
- for (let i = 0; i < sig.length; i++) {
2747
- if (bytes[i] !== sig[i]) return null;
2748
- }
2749
- const view = new DataView(
2750
- bytes.buffer,
2751
- bytes.byteOffset,
2752
- bytes.byteLength
2753
- );
2754
- const width = view.getUint32(16, false);
2755
- const height = view.getUint32(20, false);
2756
- if (width > 0 && height > 0) return { width, height };
2757
- return null;
2758
- }
2759
- if (isJPEG) {
2760
- let offset = 0;
2761
- if (bytes[offset++] !== 255 || bytes[offset++] !== 216) return null;
2762
- while (offset < bytes.length) {
2763
- while (offset < bytes.length && bytes[offset] !== 255) offset++;
2764
- if (offset + 1 >= bytes.length) break;
2765
- while (bytes[offset] === 255) offset++;
2766
- const marker = bytes[offset++];
2767
- if (marker === 216 || marker === 217) continue;
2768
- if (offset + 1 >= bytes.length) break;
2769
- const length = bytes[offset] << 8 | bytes[offset + 1];
2770
- offset += 2;
2771
- if (marker === 192 || marker === 194) {
2772
- if (length < 7 || offset + length - 2 > bytes.length) return null;
2773
- const precision = bytes[offset];
2774
- const height = bytes[offset + 1] << 8 | bytes[offset + 2];
2775
- const width = bytes[offset + 3] << 8 | bytes[offset + 4];
2776
- if (precision > 0 && width > 0 && height > 0)
2777
- return { width, height };
2778
- return null;
2779
- } else {
2780
- offset += length - 2;
2781
- }
2782
- }
2783
- return null;
2784
- }
2785
- return null;
2786
- } catch {
2787
- return null;
2788
- }
2789
- }
2790
- function calculateImageTokens(width, height, detail = "auto") {
2791
- if (detail === "low") return 85;
2792
- let w = width;
2793
- let h = height;
2794
- if (w > 2048 || h > 2048) {
2795
- const aspectRatio = w / h;
2796
- if (w > h) {
2797
- w = 2048;
2798
- h = Math.floor(w / aspectRatio);
2799
- } else {
2800
- h = 2048;
2801
- w = Math.floor(h * aspectRatio);
2481
+ function calculateImageTokens(width, height, detail = "auto") {
2482
+ if (detail === "low") return 85;
2483
+ let w = width;
2484
+ let h = height;
2485
+ if (w > 2048 || h > 2048) {
2486
+ const aspectRatio = w / h;
2487
+ if (w > h) {
2488
+ w = 2048;
2489
+ h = Math.floor(w / aspectRatio);
2490
+ } else {
2491
+ h = 2048;
2492
+ w = Math.floor(h * aspectRatio);
2802
2493
  }
2803
2494
  }
2804
2495
  if (w > 768 || h > 768) {
@@ -2816,9 +2507,6 @@ function calculateImageTokens(width, height, detail = "auto") {
2816
2507
  const numTiles = tilesWidth * tilesHeight;
2817
2508
  return 85 + 170 * numTiles;
2818
2509
  }
2819
- function isInsecureHttpUrl(url) {
2820
- return url.startsWith("http://");
2821
- }
2822
2510
  var ProviderManager = class _ProviderManager {
2823
2511
  constructor(providerRegistry, store, logger) {
2824
2512
  this.providerRegistry = providerRegistry;
@@ -3035,7 +2723,7 @@ var ProviderManager = class _ProviderManager {
3035
2723
  if (this.isOnCooldown(baseUrl)) {
3036
2724
  continue;
3037
2725
  }
3038
- if (!torMode && (isOnionUrl(baseUrl) || isInsecureHttpUrl(baseUrl))) {
2726
+ if (!torMode && isOnionUrl(baseUrl)) {
3039
2727
  continue;
3040
2728
  }
3041
2729
  const model = models.find((m) => m.id === modelId);
@@ -3086,7 +2774,7 @@ var ProviderManager = class _ProviderManager {
3086
2774
  for (const [baseUrl, models] of Object.entries(allProviders)) {
3087
2775
  if (disabledProviders.has(baseUrl)) continue;
3088
2776
  if (this.isOnCooldown(baseUrl)) continue;
3089
- if (!torMode && (isOnionUrl(baseUrl) || isInsecureHttpUrl(baseUrl)))
2777
+ if (!torMode && isOnionUrl(baseUrl))
3090
2778
  continue;
3091
2779
  const model = models.find((m) => m.id === modelId);
3092
2780
  if (!model) continue;
@@ -3101,16 +2789,18 @@ var ProviderManager = class _ProviderManager {
3101
2789
  getProviderPriceRankingForModel(modelId, options = {}) {
3102
2790
  const includeDisabled = options.includeDisabled ?? false;
3103
2791
  const torMode = options.torMode ?? false;
3104
- const disabledProviders = new Set(
3105
- this.providerRegistry.getDisabledProviders()
3106
- );
2792
+ const disabledProviderList = this.providerRegistry.getDisabledProviders();
2793
+ const disabledProviders = new Set(disabledProviderList);
2794
+ if (disabledProviderList.length > 0) {
2795
+ this.logger.log(`getProviderPriceRankingForModel: disabled providers (${disabledProviderList.length}): ${disabledProviderList.join(", ")}`);
2796
+ }
3107
2797
  const allModels = this.providerRegistry.getAllProvidersModels();
3108
2798
  const results = [];
3109
2799
  for (const [baseUrl, models] of Object.entries(allModels)) {
3110
2800
  if (!includeDisabled && disabledProviders.has(baseUrl)) continue;
3111
2801
  if (this.isOnCooldown(baseUrl)) continue;
3112
2802
  if (torMode && !baseUrl.includes(".onion")) continue;
3113
- if (!torMode && (baseUrl.includes(".onion") || isInsecureHttpUrl(baseUrl)))
2803
+ if (!torMode && baseUrl.includes(".onion"))
3114
2804
  continue;
3115
2805
  const match = models.find((model) => model.id === modelId);
3116
2806
  if (!match?.sats_pricing) continue;
@@ -3130,12 +2820,20 @@ var ProviderManager = class _ProviderManager {
3130
2820
  totalPerMillion
3131
2821
  });
3132
2822
  }
3133
- return results.sort((a, b) => {
2823
+ results.sort((a, b) => {
3134
2824
  if (a.totalPerMillion !== b.totalPerMillion) {
3135
2825
  return a.totalPerMillion - b.totalPerMillion;
3136
2826
  }
3137
2827
  return a.baseUrl.localeCompare(b.baseUrl);
3138
2828
  });
2829
+ if (results.length > 0) {
2830
+ const ranking = results.map((r, i) => ` ${i + 1}. ${r.baseUrl} total=${r.totalPerMillion.toFixed(2)} sats/M (prompt=${r.promptPerMillion.toFixed(2)} completion=${r.completionPerMillion.toFixed(2)})`).join("\n");
2831
+ this.logger.log(`getProviderPriceRankingForModel: ${modelId} ranking (${results.length} providers):
2832
+ ${ranking}`);
2833
+ } else {
2834
+ this.logger.log(`getProviderPriceRankingForModel: ${modelId} no providers found`);
2835
+ }
2836
+ return results;
3139
2837
  }
3140
2838
  /**
3141
2839
  * Get best-priced provider for a specific model
@@ -3321,147 +3019,12 @@ var createMemoryDriver = (seed) => {
3321
3019
  },
3322
3020
  async setItem(key, value) {
3323
3021
  store.set(key, JSON.stringify(value));
3324
- },
3325
- async removeItem(key) {
3326
- store.delete(key);
3327
- }
3328
- };
3329
- };
3330
-
3331
- // storage/drivers/sqlite.ts
3332
- var isBun = () => {
3333
- return typeof process.versions.bun !== "undefined";
3334
- };
3335
- var cachedDbModule = null;
3336
- var loadDatabase = async (dbPath) => {
3337
- if (isBun()) {
3338
- throw new Error(
3339
- "SQLite driver not supported in Bun. Use createBunSqliteDriver() instead."
3340
- );
3341
- }
3342
- try {
3343
- if (!cachedDbModule) {
3344
- cachedDbModule = (await import('better-sqlite3')).default;
3345
- }
3346
- return new cachedDbModule(dbPath);
3347
- } catch (error) {
3348
- throw new Error(
3349
- `better-sqlite3 is required for sqlite storage. Install it to use sqlite storage. (${error})`
3350
- );
3351
- }
3352
- };
3353
- var createSqliteDriver = (options = {}) => {
3354
- const dbPath = options.dbPath || "routstr.sqlite";
3355
- const tableName = options.tableName || "sdk_storage";
3356
- let db;
3357
- let selectStmt;
3358
- let upsertStmt;
3359
- let deleteStmt;
3360
- const initDb = async () => {
3361
- if (!db) {
3362
- db = await loadDatabase(dbPath);
3363
- db.exec(
3364
- `CREATE TABLE IF NOT EXISTS ${tableName} (key TEXT PRIMARY KEY, value TEXT NOT NULL)`
3365
- );
3366
- selectStmt = db.prepare(`SELECT value FROM ${tableName} WHERE key = ?`);
3367
- upsertStmt = db.prepare(
3368
- `INSERT INTO ${tableName} (key, value) VALUES (?, ?)
3369
- ON CONFLICT(key) DO UPDATE SET value = excluded.value`
3370
- );
3371
- deleteStmt = db.prepare(`DELETE FROM ${tableName} WHERE key = ?`);
3372
- }
3373
- };
3374
- const ensureInit = async () => {
3375
- if (!db) {
3376
- await initDb();
3377
- }
3378
- };
3379
- return {
3380
- async getItem(key, defaultValue) {
3381
- try {
3382
- await ensureInit();
3383
- const row = selectStmt.get(key);
3384
- if (!row || typeof row.value !== "string") return defaultValue;
3385
- try {
3386
- return JSON.parse(row.value);
3387
- } catch (parseError) {
3388
- if (typeof defaultValue === "string") {
3389
- return row.value;
3390
- }
3391
- throw parseError;
3392
- }
3393
- } catch (error) {
3394
- console.error(`SQLite getItem failed for key "${key}":`, error);
3395
- return defaultValue;
3396
- }
3397
- },
3398
- async setItem(key, value) {
3399
- try {
3400
- await ensureInit();
3401
- upsertStmt.run(key, JSON.stringify(value));
3402
- } catch (error) {
3403
- console.error(`SQLite setItem failed for key "${key}":`, error);
3404
- }
3405
- },
3406
- async removeItem(key) {
3407
- try {
3408
- await ensureInit();
3409
- deleteStmt.run(key);
3410
- } catch (error) {
3411
- console.error(`SQLite removeItem failed for key "${key}":`, error);
3412
- }
3413
- }
3414
- };
3415
- };
3416
- async function createBunSqliteDriver(dbPath, options) {
3417
- const logger = (options?.logger ?? consoleLogger).child("BunSqliteDriver");
3418
- const SQLite = (await import(
3419
- /* webpackIgnore: true */
3420
- 'bun:sqlite'
3421
- )).default;
3422
- const db = new SQLite(dbPath);
3423
- db.run(`
3424
- CREATE TABLE IF NOT EXISTS sdk_storage (
3425
- key TEXT PRIMARY KEY,
3426
- value TEXT NOT NULL
3427
- )
3428
- `);
3429
- return {
3430
- async getItem(key, defaultValue) {
3431
- try {
3432
- const row = db.query("SELECT value FROM sdk_storage WHERE key = ?").get(key);
3433
- if (!row || typeof row.value !== "string") return defaultValue;
3434
- try {
3435
- return JSON.parse(row.value);
3436
- } catch (parseError) {
3437
- if (typeof defaultValue === "string") {
3438
- return row.value;
3439
- }
3440
- throw parseError;
3441
- }
3442
- } catch (error) {
3443
- logger.error(`getItem failed for key "${key}":`, error);
3444
- return defaultValue;
3445
- }
3446
- },
3447
- async setItem(key, value) {
3448
- try {
3449
- db.query(
3450
- "INSERT INTO sdk_storage (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value"
3451
- ).run(key, JSON.stringify(value));
3452
- } catch (error) {
3453
- logger.error(`setItem failed for key "${key}":`, error);
3454
- }
3455
- },
3456
- async removeItem(key) {
3457
- try {
3458
- db.query("DELETE FROM sdk_storage WHERE key = ?").run(key);
3459
- } catch (error) {
3460
- logger.error(`removeItem failed for key "${key}":`, error);
3461
- }
3022
+ },
3023
+ async removeItem(key) {
3024
+ store.delete(key);
3462
3025
  }
3463
3026
  };
3464
- }
3027
+ };
3465
3028
 
3466
3029
  // storage/drivers/indexedDB.ts
3467
3030
  var isBrowser = typeof indexedDB !== "undefined";
@@ -3470,15 +3033,32 @@ var openDatabase = (dbName, storeName) => {
3470
3033
  return Promise.reject(new Error("IndexedDB is not available"));
3471
3034
  }
3472
3035
  return new Promise((resolve, reject) => {
3473
- const request = indexedDB.open(dbName, 1);
3036
+ const request = indexedDB.open(dbName, 2);
3474
3037
  request.onupgradeneeded = () => {
3475
3038
  const db = request.result;
3476
3039
  if (!db.objectStoreNames.contains(storeName)) {
3477
3040
  db.createObjectStore(storeName);
3478
3041
  }
3042
+ if (storeName !== "usage_tracking" && !db.objectStoreNames.contains("usage_tracking")) {
3043
+ const utStore = db.createObjectStore("usage_tracking", { keyPath: "id" });
3044
+ utStore.createIndex("timestamp", "timestamp", { unique: false });
3045
+ utStore.createIndex("modelId", "modelId", { unique: false });
3046
+ utStore.createIndex("baseUrl", "baseUrl", { unique: false });
3047
+ utStore.createIndex("sessionId", "sessionId", { unique: false });
3048
+ utStore.createIndex("client", "client", { unique: false });
3049
+ }
3050
+ if (storeName !== "sdk_storage" && !db.objectStoreNames.contains("sdk_storage")) {
3051
+ db.createObjectStore("sdk_storage");
3052
+ }
3479
3053
  };
3480
3054
  request.onsuccess = () => resolve(request.result);
3481
3055
  request.onerror = () => reject(request.error);
3056
+ request.onblocked = () => {
3057
+ console.warn(
3058
+ `[IndexedDB driver] open blocked for "${dbName}" (store: "${storeName}") \u2014 close other tabs using this DB`
3059
+ );
3060
+ reject(new Error(`IndexedDB "${dbName}" blocked by another connection`));
3061
+ };
3482
3062
  });
3483
3063
  };
3484
3064
  var createIndexedDBDriver = (options = {}) => {
@@ -3591,9 +3171,10 @@ var openDatabase2 = (dbName, storeName) => {
3591
3171
  return Promise.reject(new Error("IndexedDB is not available"));
3592
3172
  }
3593
3173
  return new Promise((resolve, reject) => {
3594
- const request = indexedDB.open(dbName, 1);
3174
+ const request = indexedDB.open(dbName, 3);
3595
3175
  request.onupgradeneeded = () => {
3596
3176
  const db = request.result;
3177
+ const tx = request.transaction;
3597
3178
  if (!db.objectStoreNames.contains(storeName)) {
3598
3179
  const store = db.createObjectStore(storeName, { keyPath: "id" });
3599
3180
  store.createIndex("timestamp", "timestamp", { unique: false });
@@ -3601,10 +3182,25 @@ var openDatabase2 = (dbName, storeName) => {
3601
3182
  store.createIndex("baseUrl", "baseUrl", { unique: false });
3602
3183
  store.createIndex("sessionId", "sessionId", { unique: false });
3603
3184
  store.createIndex("client", "client", { unique: false });
3185
+ store.createIndex("provider", "provider", { unique: false });
3186
+ } else if (tx) {
3187
+ const store = tx.objectStore(storeName);
3188
+ if (!store.indexNames.contains("provider")) {
3189
+ store.createIndex("provider", "provider", { unique: false });
3190
+ }
3191
+ }
3192
+ if (storeName !== "sdk_storage" && !db.objectStoreNames.contains("sdk_storage")) {
3193
+ db.createObjectStore("sdk_storage");
3604
3194
  }
3605
3195
  };
3606
3196
  request.onsuccess = () => resolve(request.result);
3607
3197
  request.onerror = () => reject(request.error);
3198
+ request.onblocked = () => {
3199
+ console.warn(
3200
+ `[usageTracking IndexedDB] open blocked for "${dbName}" \u2014 close other tabs using this DB`
3201
+ );
3202
+ reject(new Error(`IndexedDB "${dbName}" blocked by another connection`));
3203
+ };
3608
3204
  });
3609
3205
  };
3610
3206
  var matchesFilters = (entry, options = {}) => {
@@ -3626,6 +3222,9 @@ var matchesFilters = (entry, options = {}) => {
3626
3222
  if (options.client && entry.client !== options.client) {
3627
3223
  return false;
3628
3224
  }
3225
+ if (options.provider && entry.provider !== options.provider) {
3226
+ return false;
3227
+ }
3629
3228
  return true;
3630
3229
  };
3631
3230
  var createIndexedDBUsageTrackingDriver = (options = {}) => {
@@ -3757,393 +3356,8 @@ var createIndexedDBUsageTrackingDriver = (options = {}) => {
3757
3356
  };
3758
3357
  };
3759
3358
 
3760
- // storage/usageTracking/sqlite.ts
3761
- var MIGRATION_MARKER_KEY2 = "usage_tracking_migration_v1";
3762
- var normalizeBaseUrl2 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
3763
- var isBun2 = () => {
3764
- return typeof process.versions.bun !== "undefined";
3765
- };
3766
- var cachedDbModule2 = null;
3767
- var loadDatabase2 = async (dbPath) => {
3768
- if (isBun2()) {
3769
- throw new Error(
3770
- "SQLite driver not supported in Bun. Use createMemoryDriver() instead."
3771
- );
3772
- }
3773
- try {
3774
- if (!cachedDbModule2) {
3775
- cachedDbModule2 = (await import('better-sqlite3')).default;
3776
- }
3777
- return new cachedDbModule2(dbPath);
3778
- } catch (error) {
3779
- throw new Error(
3780
- `better-sqlite3 is required for sqlite usage tracking. Install it to use sqlite storage. (${error})`
3781
- );
3782
- }
3783
- };
3784
- var buildWhereClause = (options = {}) => {
3785
- const clauses = [];
3786
- const params = [];
3787
- if (typeof options.before === "number") {
3788
- clauses.push("timestamp < ?");
3789
- params.push(options.before);
3790
- }
3791
- if (typeof options.after === "number") {
3792
- clauses.push("timestamp > ?");
3793
- params.push(options.after);
3794
- }
3795
- if (options.modelId) {
3796
- clauses.push("model_id = ?");
3797
- params.push(options.modelId);
3798
- }
3799
- if (options.baseUrl) {
3800
- clauses.push("base_url = ?");
3801
- params.push(normalizeBaseUrl2(options.baseUrl));
3802
- }
3803
- if (options.sessionId) {
3804
- clauses.push("session_id = ?");
3805
- params.push(options.sessionId);
3806
- }
3807
- if (options.client) {
3808
- clauses.push("client = ?");
3809
- params.push(options.client);
3810
- }
3811
- return {
3812
- sql: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
3813
- params
3814
- };
3815
- };
3816
- var createSqliteUsageTrackingDriver = (options = {}) => {
3817
- const dbPath = options.dbPath || "routstr.sqlite";
3818
- const tableName = options.tableName || "usage_tracking";
3819
- const legacyStorageDriver = options.legacyStorageDriver;
3820
- let db;
3821
- let insertStmt;
3822
- let migrationComplete = false;
3823
- const initDb = async () => {
3824
- if (!db) {
3825
- db = await loadDatabase2(dbPath);
3826
- db.exec(`
3827
- CREATE TABLE IF NOT EXISTS ${tableName} (
3828
- id TEXT PRIMARY KEY,
3829
- timestamp INTEGER NOT NULL,
3830
- model_id TEXT NOT NULL,
3831
- base_url TEXT NOT NULL,
3832
- request_id TEXT NOT NULL,
3833
- cost REAL NOT NULL,
3834
- sats_cost REAL NOT NULL,
3835
- prompt_tokens INTEGER NOT NULL,
3836
- completion_tokens INTEGER NOT NULL,
3837
- total_tokens INTEGER NOT NULL,
3838
- client TEXT,
3839
- session_id TEXT,
3840
- tags TEXT
3841
- );
3842
- CREATE INDEX IF NOT EXISTS idx_${tableName}_timestamp ON ${tableName}(timestamp);
3843
- CREATE INDEX IF NOT EXISTS idx_${tableName}_model_id ON ${tableName}(model_id);
3844
- CREATE INDEX IF NOT EXISTS idx_${tableName}_base_url ON ${tableName}(base_url);
3845
- CREATE INDEX IF NOT EXISTS idx_${tableName}_session_id ON ${tableName}(session_id);
3846
- CREATE INDEX IF NOT EXISTS idx_${tableName}_client ON ${tableName}(client);
3847
- `);
3848
- insertStmt = db.prepare(`
3849
- INSERT OR REPLACE INTO ${tableName} (
3850
- id, timestamp, model_id, base_url, request_id,
3851
- cost, sats_cost, prompt_tokens, completion_tokens, total_tokens,
3852
- client, session_id, tags
3853
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3854
- `);
3855
- }
3856
- };
3857
- const ensureInit = async () => {
3858
- if (!db) {
3859
- await initDb();
3860
- }
3861
- };
3862
- const appendOne = (entry) => {
3863
- insertStmt.run(
3864
- entry.id,
3865
- entry.timestamp,
3866
- entry.modelId,
3867
- normalizeBaseUrl2(entry.baseUrl),
3868
- entry.requestId,
3869
- entry.cost,
3870
- entry.satsCost,
3871
- entry.promptTokens,
3872
- entry.completionTokens,
3873
- entry.totalTokens,
3874
- entry.client ?? null,
3875
- entry.sessionId ?? null,
3876
- JSON.stringify(entry.tags ?? [])
3877
- );
3878
- };
3879
- const ensureMigrated = async () => {
3880
- if (!legacyStorageDriver || migrationComplete) return;
3881
- const migrated = await legacyStorageDriver.getItem(
3882
- MIGRATION_MARKER_KEY2,
3883
- false
3884
- );
3885
- if (migrated) {
3886
- migrationComplete = true;
3887
- return;
3888
- }
3889
- const legacyEntries = await legacyStorageDriver.getItem(
3890
- SDK_STORAGE_KEYS.USAGE_TRACKING,
3891
- []
3892
- );
3893
- for (const entry of legacyEntries) {
3894
- appendOne(entry);
3895
- }
3896
- if (legacyEntries.length > 0) {
3897
- await legacyStorageDriver.removeItem(SDK_STORAGE_KEYS.USAGE_TRACKING);
3898
- }
3899
- await legacyStorageDriver.setItem(MIGRATION_MARKER_KEY2, true);
3900
- migrationComplete = true;
3901
- };
3902
- const mapRow = (row) => ({
3903
- id: row.id,
3904
- timestamp: row.timestamp,
3905
- modelId: row.model_id,
3906
- baseUrl: row.base_url,
3907
- requestId: row.request_id,
3908
- cost: row.cost,
3909
- satsCost: row.sats_cost,
3910
- promptTokens: row.prompt_tokens,
3911
- completionTokens: row.completion_tokens,
3912
- totalTokens: row.total_tokens,
3913
- client: row.client ?? void 0,
3914
- sessionId: row.session_id ?? void 0,
3915
- tags: typeof row.tags === "string" ? JSON.parse(row.tags) : void 0
3916
- });
3917
- return {
3918
- async migrate() {
3919
- await ensureInit();
3920
- await ensureMigrated();
3921
- },
3922
- async append(entry) {
3923
- await ensureInit();
3924
- await ensureMigrated();
3925
- appendOne(entry);
3926
- },
3927
- async appendMany(entries) {
3928
- await ensureInit();
3929
- await ensureMigrated();
3930
- for (const entry of entries) {
3931
- appendOne(entry);
3932
- }
3933
- },
3934
- async list(options2 = {}) {
3935
- await ensureInit();
3936
- await ensureMigrated();
3937
- const { sql, params } = buildWhereClause(options2);
3938
- const limitSql = typeof options2.limit === "number" ? " LIMIT ?" : "";
3939
- const stmt = db.prepare(
3940
- `SELECT * FROM ${tableName} ${sql} ORDER BY timestamp DESC${limitSql}`
3941
- );
3942
- const rows = stmt.all(
3943
- ...typeof options2.limit === "number" ? [...params, options2.limit] : params
3944
- );
3945
- return rows.map(mapRow);
3946
- },
3947
- async count(options2 = {}) {
3948
- await ensureInit();
3949
- await ensureMigrated();
3950
- const { sql, params } = buildWhereClause(options2);
3951
- const stmt = db.prepare(`SELECT COUNT(*) as count FROM ${tableName} ${sql}`);
3952
- const row = stmt.get(...params);
3953
- return Number(row?.count ?? 0);
3954
- },
3955
- async deleteOlderThan(timestamp) {
3956
- await ensureInit();
3957
- await ensureMigrated();
3958
- const stmt = db.prepare(`DELETE FROM ${tableName} WHERE timestamp < ?`);
3959
- const result = stmt.run(timestamp);
3960
- return result.changes;
3961
- },
3962
- async clear() {
3963
- await ensureInit();
3964
- await ensureMigrated();
3965
- db.prepare(`DELETE FROM ${tableName}`).run();
3966
- }
3967
- };
3968
- };
3969
-
3970
- // storage/usageTracking/bunSqlite.ts
3971
- var MIGRATION_MARKER_KEY3 = "usage_tracking_migration_v1";
3972
- var normalizeBaseUrl3 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
3973
- var buildWhereClause2 = (options = {}) => {
3974
- const clauses = [];
3975
- const params = [];
3976
- if (typeof options.before === "number") {
3977
- clauses.push("timestamp < ?");
3978
- params.push(options.before);
3979
- }
3980
- if (typeof options.after === "number") {
3981
- clauses.push("timestamp > ?");
3982
- params.push(options.after);
3983
- }
3984
- if (options.modelId) {
3985
- clauses.push("model_id = ?");
3986
- params.push(options.modelId);
3987
- }
3988
- if (options.baseUrl) {
3989
- clauses.push("base_url = ?");
3990
- params.push(normalizeBaseUrl3(options.baseUrl));
3991
- }
3992
- if (options.sessionId) {
3993
- clauses.push("session_id = ?");
3994
- params.push(options.sessionId);
3995
- }
3996
- if (options.client) {
3997
- clauses.push("client = ?");
3998
- params.push(options.client);
3999
- }
4000
- return {
4001
- sql: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
4002
- params
4003
- };
4004
- };
4005
- var createBunSqliteUsageTrackingDriver = (options = {}) => {
4006
- const dbPath = options.dbPath || "routstr.sqlite";
4007
- const tableName = options.tableName || "usage_tracking";
4008
- const legacyStorageDriver = options.legacyStorageDriver;
4009
- const SQLiteDatabase = options.sqlite?.Database;
4010
- let migrationPromise = null;
4011
- if (!SQLiteDatabase) {
4012
- throw new Error(
4013
- "Bun SQLite Database constructor is required. Pass { sqlite: { Database } } when creating the driver."
4014
- );
4015
- }
4016
- const db = new SQLiteDatabase(dbPath);
4017
- db.run(`
4018
- CREATE TABLE IF NOT EXISTS ${tableName} (
4019
- id TEXT PRIMARY KEY,
4020
- timestamp INTEGER NOT NULL,
4021
- model_id TEXT NOT NULL,
4022
- base_url TEXT NOT NULL,
4023
- request_id TEXT NOT NULL,
4024
- cost REAL NOT NULL,
4025
- sats_cost REAL NOT NULL,
4026
- prompt_tokens INTEGER NOT NULL,
4027
- completion_tokens INTEGER NOT NULL,
4028
- total_tokens INTEGER NOT NULL,
4029
- client TEXT,
4030
- session_id TEXT,
4031
- tags TEXT
4032
- )
4033
- `);
4034
- db.run(`CREATE INDEX IF NOT EXISTS idx_${tableName}_timestamp ON ${tableName}(timestamp)`);
4035
- db.run(`CREATE INDEX IF NOT EXISTS idx_${tableName}_model_id ON ${tableName}(model_id)`);
4036
- db.run(`CREATE INDEX IF NOT EXISTS idx_${tableName}_base_url ON ${tableName}(base_url)`);
4037
- const appendOne = (entry) => {
4038
- db.query(`
4039
- INSERT OR REPLACE INTO ${tableName} (
4040
- id, timestamp, model_id, base_url, request_id,
4041
- cost, sats_cost, prompt_tokens, completion_tokens, total_tokens,
4042
- client, session_id, tags
4043
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
4044
- `).run(
4045
- entry.id,
4046
- entry.timestamp,
4047
- entry.modelId,
4048
- normalizeBaseUrl3(entry.baseUrl),
4049
- entry.requestId,
4050
- entry.cost,
4051
- entry.satsCost,
4052
- entry.promptTokens,
4053
- entry.completionTokens,
4054
- entry.totalTokens,
4055
- entry.client ?? null,
4056
- entry.sessionId ?? null,
4057
- JSON.stringify(entry.tags ?? [])
4058
- );
4059
- };
4060
- const mapRow = (row) => ({
4061
- id: row.id,
4062
- timestamp: row.timestamp,
4063
- modelId: row.model_id,
4064
- baseUrl: row.base_url,
4065
- requestId: row.request_id,
4066
- cost: row.cost,
4067
- satsCost: row.sats_cost,
4068
- promptTokens: row.prompt_tokens,
4069
- completionTokens: row.completion_tokens,
4070
- totalTokens: row.total_tokens,
4071
- client: row.client ?? void 0,
4072
- sessionId: row.session_id ?? void 0,
4073
- tags: typeof row.tags === "string" ? JSON.parse(row.tags) : void 0
4074
- });
4075
- const ensureMigrated = async () => {
4076
- if (!legacyStorageDriver) return;
4077
- if (!migrationPromise) {
4078
- migrationPromise = (async () => {
4079
- const migrated = await legacyStorageDriver.getItem(
4080
- MIGRATION_MARKER_KEY3,
4081
- false
4082
- );
4083
- if (migrated) return;
4084
- const legacyEntries = await legacyStorageDriver.getItem(
4085
- SDK_STORAGE_KEYS.USAGE_TRACKING,
4086
- []
4087
- );
4088
- if (legacyEntries.length > 0) {
4089
- for (const entry of legacyEntries) {
4090
- appendOne(entry);
4091
- }
4092
- await legacyStorageDriver.removeItem(SDK_STORAGE_KEYS.USAGE_TRACKING);
4093
- }
4094
- await legacyStorageDriver.setItem(MIGRATION_MARKER_KEY3, true);
4095
- })();
4096
- }
4097
- await migrationPromise;
4098
- };
4099
- return {
4100
- async migrate() {
4101
- await ensureMigrated();
4102
- },
4103
- async append(entry) {
4104
- await ensureMigrated();
4105
- appendOne(entry);
4106
- },
4107
- async appendMany(entries) {
4108
- await ensureMigrated();
4109
- for (const entry of entries) {
4110
- appendOne(entry);
4111
- }
4112
- },
4113
- async list(options2 = {}) {
4114
- await ensureMigrated();
4115
- const { sql, params } = buildWhereClause2(options2);
4116
- const limitSql = typeof options2.limit === "number" ? " LIMIT ?" : "";
4117
- const query = `SELECT * FROM ${tableName} ${sql} ORDER BY timestamp DESC${limitSql}`;
4118
- let rows;
4119
- if (typeof options2.limit === "number") {
4120
- rows = db.query(query).all(...params, options2.limit);
4121
- } else {
4122
- rows = db.query(query).all(...params);
4123
- }
4124
- return rows.map(mapRow);
4125
- },
4126
- async count(options2 = {}) {
4127
- const { sql, params } = buildWhereClause2(options2);
4128
- const query = `SELECT COUNT(*) as count FROM ${tableName} ${sql}`;
4129
- const row = db.query(query).get(...params);
4130
- return Number(row?.count ?? 0);
4131
- },
4132
- async deleteOlderThan(timestamp) {
4133
- await ensureMigrated();
4134
- const before = timestamp;
4135
- const result = db.query(`DELETE FROM ${tableName} WHERE timestamp < ?`).run(before);
4136
- return result.changes ?? 0;
4137
- },
4138
- async clear() {
4139
- await ensureMigrated();
4140
- db.query(`DELETE FROM ${tableName}`).run();
4141
- }
4142
- };
4143
- };
4144
-
4145
3359
  // storage/usageTracking/memory.ts
4146
- var normalizeBaseUrl4 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
3360
+ var normalizeBaseUrl2 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
4147
3361
  var matchesFilters2 = (entry, options = {}) => {
4148
3362
  if (typeof options.before === "number" && entry.timestamp >= options.before) {
4149
3363
  return false;
@@ -4154,7 +3368,7 @@ var matchesFilters2 = (entry, options = {}) => {
4154
3368
  if (options.modelId && entry.modelId !== options.modelId) {
4155
3369
  return false;
4156
3370
  }
4157
- if (options.baseUrl && normalizeBaseUrl4(entry.baseUrl) !== normalizeBaseUrl4(options.baseUrl)) {
3371
+ if (options.baseUrl && normalizeBaseUrl2(entry.baseUrl) !== normalizeBaseUrl2(options.baseUrl)) {
4158
3372
  return false;
4159
3373
  }
4160
3374
  if (options.sessionId && entry.sessionId !== options.sessionId) {
@@ -4163,23 +3377,26 @@ var matchesFilters2 = (entry, options = {}) => {
4163
3377
  if (options.client && entry.client !== options.client) {
4164
3378
  return false;
4165
3379
  }
3380
+ if (options.provider && entry.provider !== options.provider) {
3381
+ return false;
3382
+ }
4166
3383
  return true;
4167
3384
  };
4168
3385
  var createMemoryUsageTrackingDriver = (seed = []) => {
4169
3386
  const store = /* @__PURE__ */ new Map();
4170
3387
  for (const entry of seed) {
4171
- store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl4(entry.baseUrl) });
3388
+ store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl2(entry.baseUrl) });
4172
3389
  }
4173
3390
  return {
4174
3391
  async migrate() {
4175
3392
  return;
4176
3393
  },
4177
3394
  async append(entry) {
4178
- store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl4(entry.baseUrl) });
3395
+ store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl2(entry.baseUrl) });
4179
3396
  },
4180
3397
  async appendMany(entries) {
4181
3398
  for (const entry of entries) {
4182
- store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl4(entry.baseUrl) });
3399
+ store.set(entry.id, { ...entry, baseUrl: normalizeBaseUrl2(entry.baseUrl) });
4183
3400
  }
4184
3401
  },
4185
3402
  async list(options = {}) {
@@ -4207,7 +3424,7 @@ var createMemoryUsageTrackingDriver = (seed = []) => {
4207
3424
  }
4208
3425
  };
4209
3426
  };
4210
- var normalizeBaseUrl5 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
3427
+ var normalizeBaseUrl3 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
4211
3428
  var createEmptyStore = (driver) => createStore((set, get) => ({
4212
3429
  modelsFromAllProviders: {},
4213
3430
  lastUsedModel: null,
@@ -4230,7 +3447,7 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
4230
3447
  setModelsFromAllProviders: (value) => {
4231
3448
  const normalized = {};
4232
3449
  for (const [baseUrl, models] of Object.entries(value)) {
4233
- normalized[normalizeBaseUrl5(baseUrl)] = models;
3450
+ normalized[normalizeBaseUrl3(baseUrl)] = models;
4234
3451
  }
4235
3452
  void driver.setItem(
4236
3453
  SDK_STORAGE_KEYS.MODELS_FROM_ALL_PROVIDERS,
@@ -4243,7 +3460,7 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
4243
3460
  set({ lastUsedModel: value });
4244
3461
  },
4245
3462
  setBaseUrlsList: (value) => {
4246
- const normalized = value.map((url) => normalizeBaseUrl5(url));
3463
+ const normalized = value.map((url) => normalizeBaseUrl3(url));
4247
3464
  void driver.setItem(SDK_STORAGE_KEYS.BASE_URLS_LIST, normalized);
4248
3465
  set({ baseUrlsList: normalized });
4249
3466
  },
@@ -4252,14 +3469,14 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
4252
3469
  set({ lastBaseUrlsUpdate: value });
4253
3470
  },
4254
3471
  setDisabledProviders: (value) => {
4255
- const normalized = value.map((url) => normalizeBaseUrl5(url));
3472
+ const normalized = value.map((url) => normalizeBaseUrl3(url));
4256
3473
  void driver.setItem(SDK_STORAGE_KEYS.DISABLED_PROVIDERS, normalized);
4257
3474
  set({ disabledProviders: normalized });
4258
3475
  },
4259
3476
  setMintsFromAllProviders: (value) => {
4260
3477
  const normalized = {};
4261
3478
  for (const [baseUrl, mints] of Object.entries(value)) {
4262
- normalized[normalizeBaseUrl5(baseUrl)] = mints.map(
3479
+ normalized[normalizeBaseUrl3(baseUrl)] = mints.map(
4263
3480
  (mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint
4264
3481
  );
4265
3482
  }
@@ -4272,7 +3489,7 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
4272
3489
  setInfoFromAllProviders: (value) => {
4273
3490
  const normalized = {};
4274
3491
  for (const [baseUrl, info] of Object.entries(value)) {
4275
- normalized[normalizeBaseUrl5(baseUrl)] = info;
3492
+ normalized[normalizeBaseUrl3(baseUrl)] = info;
4276
3493
  }
4277
3494
  void driver.setItem(SDK_STORAGE_KEYS.INFO_FROM_ALL_PROVIDERS, normalized);
4278
3495
  set({ infoFromAllProviders: normalized });
@@ -4280,7 +3497,7 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
4280
3497
  setLastModelsUpdate: (value) => {
4281
3498
  const normalized = {};
4282
3499
  for (const [baseUrl, timestamp] of Object.entries(value)) {
4283
- normalized[normalizeBaseUrl5(baseUrl)] = timestamp;
3500
+ normalized[normalizeBaseUrl3(baseUrl)] = timestamp;
4284
3501
  }
4285
3502
  void driver.setItem(SDK_STORAGE_KEYS.LAST_MODELS_UPDATE, normalized);
4286
3503
  set({ lastModelsUpdate: normalized });
@@ -4290,7 +3507,7 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
4290
3507
  const updates = typeof value === "function" ? value(state.apiKeys) : value;
4291
3508
  const normalized = updates.map((entry) => ({
4292
3509
  ...entry,
4293
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
3510
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
4294
3511
  balance: entry.balance ?? 0,
4295
3512
  lastUsed: entry.lastUsed ?? null
4296
3513
  }));
@@ -4302,7 +3519,7 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
4302
3519
  set((state) => {
4303
3520
  const updates = typeof value === "function" ? value(state.childKeys) : value;
4304
3521
  const normalized = updates.map((entry) => ({
4305
- parentBaseUrl: normalizeBaseUrl5(entry.parentBaseUrl),
3522
+ parentBaseUrl: normalizeBaseUrl3(entry.parentBaseUrl),
4306
3523
  childKey: entry.childKey,
4307
3524
  balance: entry.balance ?? 0,
4308
3525
  balanceLimit: entry.balanceLimit,
@@ -4316,9 +3533,9 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
4316
3533
  setXcashuTokens: (value) => {
4317
3534
  const normalized = {};
4318
3535
  for (const [baseUrl, tokens] of Object.entries(value)) {
4319
- normalized[normalizeBaseUrl5(baseUrl)] = tokens.map((entry) => ({
3536
+ normalized[normalizeBaseUrl3(baseUrl)] = tokens.map((entry) => ({
4320
3537
  ...entry,
4321
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
3538
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
4322
3539
  createdAt: entry.createdAt ?? Date.now(),
4323
3540
  tryCount: entry.tryCount ?? 0
4324
3541
  }));
@@ -4369,12 +3586,12 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
4369
3586
  },
4370
3587
  // ========== Failure Tracking ==========
4371
3588
  setFailedProviders: (value) => {
4372
- const normalized = value.map((url) => normalizeBaseUrl5(url));
3589
+ const normalized = value.map((url) => normalizeBaseUrl3(url));
4373
3590
  void driver.setItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, normalized);
4374
3591
  set({ failedProviders: normalized });
4375
3592
  },
4376
3593
  addFailedProvider: (baseUrl) => {
4377
- const normalized = normalizeBaseUrl5(baseUrl);
3594
+ const normalized = normalizeBaseUrl3(baseUrl);
4378
3595
  const current = get().failedProviders;
4379
3596
  if (!current.includes(normalized)) {
4380
3597
  const updated = [...current, normalized];
@@ -4383,7 +3600,7 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
4383
3600
  }
4384
3601
  },
4385
3602
  removeFailedProvider: (baseUrl) => {
4386
- const normalized = normalizeBaseUrl5(baseUrl);
3603
+ const normalized = normalizeBaseUrl3(baseUrl);
4387
3604
  const current = get().failedProviders;
4388
3605
  const updated = current.filter((url) => url !== normalized);
4389
3606
  void driver.setItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, updated);
@@ -4392,13 +3609,13 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
4392
3609
  setLastFailed: (value) => {
4393
3610
  const normalized = {};
4394
3611
  for (const [baseUrl, timestamp] of Object.entries(value)) {
4395
- normalized[normalizeBaseUrl5(baseUrl)] = timestamp;
3612
+ normalized[normalizeBaseUrl3(baseUrl)] = timestamp;
4396
3613
  }
4397
3614
  void driver.setItem(SDK_STORAGE_KEYS.LAST_FAILED, normalized);
4398
3615
  set({ lastFailed: normalized });
4399
3616
  },
4400
3617
  setLastFailedTimestamp: (baseUrl, timestamp) => {
4401
- const normalized = normalizeBaseUrl5(baseUrl);
3618
+ const normalized = normalizeBaseUrl3(baseUrl);
4402
3619
  const current = get().lastFailed;
4403
3620
  const updated = { ...current, [normalized]: timestamp };
4404
3621
  void driver.setItem(SDK_STORAGE_KEYS.LAST_FAILED, updated);
@@ -4406,14 +3623,14 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
4406
3623
  },
4407
3624
  setProvidersOnCooldown: (value) => {
4408
3625
  const normalized = value.map((entry) => ({
4409
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
3626
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
4410
3627
  timestamp: entry.timestamp
4411
3628
  }));
4412
3629
  void driver.setItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, normalized);
4413
3630
  set({ providersOnCooldown: normalized });
4414
3631
  },
4415
3632
  addProviderOnCooldown: (baseUrl, timestamp) => {
4416
- const normalized = normalizeBaseUrl5(baseUrl);
3633
+ const normalized = normalizeBaseUrl3(baseUrl);
4417
3634
  const current = get().providersOnCooldown;
4418
3635
  if (!current.some((entry) => entry.baseUrl === normalized)) {
4419
3636
  const updated = [...current, { baseUrl: normalized, timestamp }];
@@ -4422,7 +3639,7 @@ var createEmptyStore = (driver) => createStore((set, get) => ({
4422
3639
  }
4423
3640
  },
4424
3641
  removeProviderFromCooldown: (baseUrl) => {
4425
- const normalized = normalizeBaseUrl5(baseUrl);
3642
+ const normalized = normalizeBaseUrl3(baseUrl);
4426
3643
  const current = get().providersOnCooldown;
4427
3644
  const updated = current.filter((entry) => entry.baseUrl !== normalized);
4428
3645
  void driver.setItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, updated);
@@ -4493,40 +3710,40 @@ var hydrateStoreFromDriver = async (store, driver) => {
4493
3710
  ]);
4494
3711
  const modelsFromAllProviders = Object.fromEntries(
4495
3712
  Object.entries(rawModels).map(([baseUrl, models]) => [
4496
- normalizeBaseUrl5(baseUrl),
3713
+ normalizeBaseUrl3(baseUrl),
4497
3714
  models
4498
3715
  ])
4499
3716
  );
4500
- const baseUrlsList = rawBaseUrls.map((url) => normalizeBaseUrl5(url));
3717
+ const baseUrlsList = rawBaseUrls.map((url) => normalizeBaseUrl3(url));
4501
3718
  const disabledProviders = rawDisabledProviders.map(
4502
- (url) => normalizeBaseUrl5(url)
3719
+ (url) => normalizeBaseUrl3(url)
4503
3720
  );
4504
3721
  const mintsFromAllProviders = Object.fromEntries(
4505
3722
  Object.entries(rawMints).map(([baseUrl, mints]) => [
4506
- normalizeBaseUrl5(baseUrl),
3723
+ normalizeBaseUrl3(baseUrl),
4507
3724
  mints.map((mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint)
4508
3725
  ])
4509
3726
  );
4510
3727
  const infoFromAllProviders = Object.fromEntries(
4511
3728
  Object.entries(rawInfo).map(([baseUrl, info]) => [
4512
- normalizeBaseUrl5(baseUrl),
3729
+ normalizeBaseUrl3(baseUrl),
4513
3730
  info
4514
3731
  ])
4515
3732
  );
4516
3733
  const lastModelsUpdate = Object.fromEntries(
4517
3734
  Object.entries(rawLastModelsUpdate).map(([baseUrl, timestamp]) => [
4518
- normalizeBaseUrl5(baseUrl),
3735
+ normalizeBaseUrl3(baseUrl),
4519
3736
  timestamp
4520
3737
  ])
4521
3738
  );
4522
3739
  const apiKeys = rawApiKeys.map((entry) => ({
4523
3740
  ...entry,
4524
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
3741
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
4525
3742
  balance: entry.balance ?? 0,
4526
3743
  lastUsed: entry.lastUsed ?? null
4527
3744
  }));
4528
3745
  const childKeys = rawChildKeys.map((entry) => ({
4529
- parentBaseUrl: normalizeBaseUrl5(entry.parentBaseUrl),
3746
+ parentBaseUrl: normalizeBaseUrl3(entry.parentBaseUrl),
4530
3747
  childKey: entry.childKey,
4531
3748
  balance: entry.balance ?? 0,
4532
3749
  balanceLimit: entry.balanceLimit,
@@ -4535,9 +3752,9 @@ var hydrateStoreFromDriver = async (store, driver) => {
4535
3752
  }));
4536
3753
  const xcashuTokens = Object.fromEntries(
4537
3754
  Object.entries(rawXcashuTokens).map(([baseUrl, tokens]) => [
4538
- normalizeBaseUrl5(baseUrl),
3755
+ normalizeBaseUrl3(baseUrl),
4539
3756
  tokens.map((entry) => ({
4540
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
3757
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
4541
3758
  token: entry.token,
4542
3759
  createdAt: entry.createdAt ?? Date.now(),
4543
3760
  tryCount: entry.tryCount ?? 0
@@ -4558,16 +3775,16 @@ var hydrateStoreFromDriver = async (store, driver) => {
4558
3775
  lastUsed: entry.lastUsed ?? null
4559
3776
  }));
4560
3777
  const failedProviders = rawFailedProviders.map(
4561
- (url) => normalizeBaseUrl5(url)
3778
+ (url) => normalizeBaseUrl3(url)
4562
3779
  );
4563
3780
  const lastFailed = Object.fromEntries(
4564
3781
  Object.entries(rawLastFailed).map(([baseUrl, timestamp]) => [
4565
- normalizeBaseUrl5(baseUrl),
3782
+ normalizeBaseUrl3(baseUrl),
4566
3783
  timestamp
4567
3784
  ])
4568
3785
  );
4569
3786
  const providersOnCooldown = rawProvidersOnCooldown.map((entry) => ({
4570
- baseUrl: normalizeBaseUrl5(entry.baseUrl),
3787
+ baseUrl: normalizeBaseUrl3(entry.baseUrl),
4571
3788
  timestamp: entry.timestamp
4572
3789
  }));
4573
3790
  store.setState({
@@ -4608,12 +3825,12 @@ var createDiscoveryAdapterFromStore = (store) => ({
4608
3825
  getCachedProviderInfo: () => store.getState().infoFromAllProviders,
4609
3826
  setCachedProviderInfo: (info) => store.getState().setInfoFromAllProviders(info),
4610
3827
  getProviderLastUpdate: (baseUrl) => {
4611
- const normalized = normalizeBaseUrl5(baseUrl);
3828
+ const normalized = normalizeBaseUrl3(baseUrl);
4612
3829
  const timestamps = store.getState().lastModelsUpdate;
4613
3830
  return timestamps[normalized] || null;
4614
3831
  },
4615
3832
  setProviderLastUpdate: (baseUrl, timestamp) => {
4616
- const normalized = normalizeBaseUrl5(baseUrl);
3833
+ const normalized = normalizeBaseUrl3(baseUrl);
4617
3834
  const timestamps = { ...store.getState().lastModelsUpdate };
4618
3835
  timestamps[normalized] = timestamp;
4619
3836
  store.getState().setLastModelsUpdate(timestamps);
@@ -4642,24 +3859,24 @@ var createStorageAdapterFromStore = (store) => ({
4642
3859
  return Object.entries(distributionMap).map(([baseUrl, amt]) => ({ baseUrl, amount: amt })).sort((a, b) => b.amount - a.amount);
4643
3860
  },
4644
3861
  saveProviderInfo: (baseUrl, info) => {
4645
- const normalized = normalizeBaseUrl5(baseUrl);
3862
+ const normalized = normalizeBaseUrl3(baseUrl);
4646
3863
  const next = { ...store.getState().infoFromAllProviders };
4647
3864
  next[normalized] = info;
4648
3865
  store.getState().setInfoFromAllProviders(next);
4649
3866
  },
4650
3867
  getProviderInfo: (baseUrl) => {
4651
- const normalized = normalizeBaseUrl5(baseUrl);
3868
+ const normalized = normalizeBaseUrl3(baseUrl);
4652
3869
  return store.getState().infoFromAllProviders[normalized] || null;
4653
3870
  },
4654
3871
  // ========== API Keys (for apikeys mode) ==========
4655
3872
  getApiKey: (baseUrl) => {
4656
- const normalized = normalizeBaseUrl5(baseUrl);
3873
+ const normalized = normalizeBaseUrl3(baseUrl);
4657
3874
  const entry = store.getState().apiKeys.find((key) => key.baseUrl === normalized);
4658
3875
  if (!entry) return null;
4659
3876
  return entry;
4660
3877
  },
4661
3878
  setApiKey: (baseUrl, key) => {
4662
- const normalized = normalizeBaseUrl5(baseUrl);
3879
+ const normalized = normalizeBaseUrl3(baseUrl);
4663
3880
  const keys = store.getState().apiKeys;
4664
3881
  const existingIndex = keys.findIndex(
4665
3882
  (entry) => entry.baseUrl === normalized
@@ -4677,7 +3894,7 @@ var createStorageAdapterFromStore = (store) => ({
4677
3894
  store.getState().setApiKeys(next);
4678
3895
  },
4679
3896
  updateApiKeyBalance: (baseUrl, balance) => {
4680
- const normalized = normalizeBaseUrl5(baseUrl);
3897
+ const normalized = normalizeBaseUrl3(baseUrl);
4681
3898
  const keys = store.getState().apiKeys;
4682
3899
  const next = keys.map(
4683
3900
  (entry) => entry.baseUrl === normalized ? { ...entry, balance, lastUsed: Date.now() } : entry
@@ -4685,7 +3902,7 @@ var createStorageAdapterFromStore = (store) => ({
4685
3902
  store.getState().setApiKeys(next);
4686
3903
  },
4687
3904
  removeApiKey: (baseUrl) => {
4688
- const normalized = normalizeBaseUrl5(baseUrl);
3905
+ const normalized = normalizeBaseUrl3(baseUrl);
4689
3906
  const next = store.getState().apiKeys.filter((entry) => entry.baseUrl !== normalized);
4690
3907
  store.getState().setApiKeys(next);
4691
3908
  },
@@ -4699,7 +3916,7 @@ var createStorageAdapterFromStore = (store) => ({
4699
3916
  },
4700
3917
  // ========== Child Keys ==========
4701
3918
  getChildKey: (parentBaseUrl) => {
4702
- const normalized = normalizeBaseUrl5(parentBaseUrl);
3919
+ const normalized = normalizeBaseUrl3(parentBaseUrl);
4703
3920
  const entry = store.getState().childKeys.find((key) => key.parentBaseUrl === normalized);
4704
3921
  if (!entry) return null;
4705
3922
  return {
@@ -4712,7 +3929,7 @@ var createStorageAdapterFromStore = (store) => ({
4712
3929
  };
4713
3930
  },
4714
3931
  setChildKey: (parentBaseUrl, childKey, balance, validityDate, balanceLimit) => {
4715
- const normalized = normalizeBaseUrl5(parentBaseUrl);
3932
+ const normalized = normalizeBaseUrl3(parentBaseUrl);
4716
3933
  const keys = store.getState().childKeys;
4717
3934
  const existingIndex = keys.findIndex(
4718
3935
  (entry) => entry.parentBaseUrl === normalized
@@ -4743,7 +3960,7 @@ var createStorageAdapterFromStore = (store) => ({
4743
3960
  }
4744
3961
  },
4745
3962
  updateChildKeyBalance: (parentBaseUrl, balance) => {
4746
- const normalized = normalizeBaseUrl5(parentBaseUrl);
3963
+ const normalized = normalizeBaseUrl3(parentBaseUrl);
4747
3964
  const keys = store.getState().childKeys;
4748
3965
  const next = keys.map(
4749
3966
  (entry) => entry.parentBaseUrl === normalized ? { ...entry, balance } : entry
@@ -4751,7 +3968,7 @@ var createStorageAdapterFromStore = (store) => ({
4751
3968
  store.getState().setChildKeys(next);
4752
3969
  },
4753
3970
  removeChildKey: (parentBaseUrl) => {
4754
- const normalized = normalizeBaseUrl5(parentBaseUrl);
3971
+ const normalized = normalizeBaseUrl3(parentBaseUrl);
4755
3972
  const next = store.getState().childKeys.filter((entry) => entry.parentBaseUrl !== normalized);
4756
3973
  store.getState().setChildKeys(next);
4757
3974
  },
@@ -4776,11 +3993,11 @@ var createStorageAdapterFromStore = (store) => ({
4776
3993
  return store.getState().xcashuTokens;
4777
3994
  },
4778
3995
  getXcashuTokensForBaseUrl: (baseUrl) => {
4779
- const normalized = normalizeBaseUrl5(baseUrl);
3996
+ const normalized = normalizeBaseUrl3(baseUrl);
4780
3997
  return store.getState().xcashuTokens[normalized] || [];
4781
3998
  },
4782
3999
  addXcashuToken: (baseUrl, token) => {
4783
- const normalized = normalizeBaseUrl5(baseUrl);
4000
+ const normalized = normalizeBaseUrl3(baseUrl);
4784
4001
  const tokens = store.getState().xcashuTokens;
4785
4002
  const existing = tokens[normalized] || [];
4786
4003
  const next = { ...tokens };
@@ -4791,7 +4008,7 @@ var createStorageAdapterFromStore = (store) => ({
4791
4008
  store.getState().setXcashuTokens(next);
4792
4009
  },
4793
4010
  removeXcashuToken: (baseUrl, token) => {
4794
- const normalized = normalizeBaseUrl5(baseUrl);
4011
+ const normalized = normalizeBaseUrl3(baseUrl);
4795
4012
  const tokens = store.getState().xcashuTokens;
4796
4013
  const existing = tokens[normalized] || [];
4797
4014
  const next = { ...tokens };
@@ -4802,7 +4019,7 @@ var createStorageAdapterFromStore = (store) => ({
4802
4019
  store.getState().setXcashuTokens(next);
4803
4020
  },
4804
4021
  clearXcashuTokensForBaseUrl: (baseUrl) => {
4805
- const normalized = normalizeBaseUrl5(baseUrl);
4022
+ const normalized = normalizeBaseUrl3(baseUrl);
4806
4023
  const tokens = store.getState().xcashuTokens;
4807
4024
  const next = { ...tokens };
4808
4025
  delete next[normalized];
@@ -4816,16 +4033,16 @@ var createProviderRegistryFromStore = (store, logger) => {
4816
4033
  const log = (logger ?? consoleLogger).child("ProviderRegistry");
4817
4034
  return {
4818
4035
  getModelsForProvider: (baseUrl) => {
4819
- const normalized = normalizeBaseUrl5(baseUrl);
4036
+ const normalized = normalizeBaseUrl3(baseUrl);
4820
4037
  return store.getState().modelsFromAllProviders[normalized] || [];
4821
4038
  },
4822
4039
  getDisabledProviders: () => store.getState().disabledProviders,
4823
4040
  getProviderMints: (baseUrl) => {
4824
- const normalized = normalizeBaseUrl5(baseUrl);
4041
+ const normalized = normalizeBaseUrl3(baseUrl);
4825
4042
  return store.getState().mintsFromAllProviders[normalized] || [];
4826
4043
  },
4827
4044
  getProviderInfo: async (baseUrl) => {
4828
- const normalized = normalizeBaseUrl5(baseUrl);
4045
+ const normalized = normalizeBaseUrl3(baseUrl);
4829
4046
  const cached = store.getState().infoFromAllProviders[normalized];
4830
4047
  if (cached) return cached;
4831
4048
  try {
@@ -4851,11 +4068,11 @@ var createProviderRegistryFromStore = (store, logger) => {
4851
4068
  var MODEL_KEY_PREFIX = "models:provider:";
4852
4069
  var MODEL_TS_KEY_PREFIX = "models:provider_timestamp:";
4853
4070
  var PROVIDER_INDEX_KEY = "models:provider_index";
4854
- var MIGRATION_MARKER_KEY4 = "models_sharded_migration_v1";
4071
+ var MIGRATION_MARKER_KEY2 = "models_sharded_migration_v1";
4855
4072
  var encodeBaseUrl = (baseUrl) => encodeURIComponent(baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
4856
4073
  var modelKey = (baseUrl) => `${MODEL_KEY_PREFIX}${encodeBaseUrl(baseUrl)}`;
4857
4074
  var modelTsKey = (baseUrl) => `${MODEL_TS_KEY_PREFIX}${encodeBaseUrl(baseUrl)}`;
4858
- var normalizeBaseUrl6 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
4075
+ var normalizeBaseUrl4 = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
4859
4076
  var createShardedDiscoveryAdapter = async (options) => {
4860
4077
  const { driver } = options;
4861
4078
  const legacyModels = await driver.getItem(SDK_STORAGE_KEYS.MODELS_FROM_ALL_PROVIDERS, {});
@@ -4863,7 +4080,7 @@ var createShardedDiscoveryAdapter = async (options) => {
4863
4080
  if (Object.keys(legacyModels).length > 0) {
4864
4081
  const migratedProviders = [];
4865
4082
  for (const [baseUrl, models] of Object.entries(legacyModels)) {
4866
- const normalized = normalizeBaseUrl6(baseUrl);
4083
+ const normalized = normalizeBaseUrl4(baseUrl);
4867
4084
  await driver.setItem(modelKey(normalized), models);
4868
4085
  const ts = legacyTimestamps[normalized] ?? Date.now();
4869
4086
  await driver.setItem(modelTsKey(normalized), ts);
@@ -4878,7 +4095,7 @@ var createShardedDiscoveryAdapter = async (options) => {
4878
4095
  await driver.removeItem(SDK_STORAGE_KEYS.MODELS_FROM_ALL_PROVIDERS);
4879
4096
  await driver.removeItem(SDK_STORAGE_KEYS.LAST_MODELS_UPDATE);
4880
4097
  }
4881
- await driver.setItem(MIGRATION_MARKER_KEY4, true);
4098
+ await driver.setItem(MIGRATION_MARKER_KEY2, true);
4882
4099
  const [
4883
4100
  rawMints,
4884
4101
  rawInfo,
@@ -4912,31 +4129,31 @@ var createShardedDiscoveryAdapter = async (options) => {
4912
4129
  const providerIndex = /* @__PURE__ */ new Set();
4913
4130
  const knownProviders = /* @__PURE__ */ new Set();
4914
4131
  for (const baseUrl of Object.keys(rawInfo)) {
4915
- knownProviders.add(normalizeBaseUrl6(baseUrl));
4132
+ knownProviders.add(normalizeBaseUrl4(baseUrl));
4916
4133
  }
4917
4134
  for (const baseUrl of Object.keys(rawMints)) {
4918
- knownProviders.add(normalizeBaseUrl6(baseUrl));
4135
+ knownProviders.add(normalizeBaseUrl4(baseUrl));
4919
4136
  }
4920
4137
  for (const baseUrl of rawBaseUrls) {
4921
- knownProviders.add(normalizeBaseUrl6(baseUrl));
4138
+ knownProviders.add(normalizeBaseUrl4(baseUrl));
4922
4139
  }
4923
4140
  for (const baseUrl of rawDisabled) {
4924
- knownProviders.add(normalizeBaseUrl6(baseUrl));
4141
+ knownProviders.add(normalizeBaseUrl4(baseUrl));
4925
4142
  }
4926
4143
  for (const baseUrl of Object.keys(legacyModels)) {
4927
- knownProviders.add(normalizeBaseUrl6(baseUrl));
4144
+ knownProviders.add(normalizeBaseUrl4(baseUrl));
4928
4145
  }
4929
4146
  const indexProviders = await driver.getItem(
4930
4147
  PROVIDER_INDEX_KEY,
4931
4148
  []
4932
4149
  );
4933
4150
  for (const baseUrl of indexProviders) {
4934
- const normalized = normalizeBaseUrl6(baseUrl);
4151
+ const normalized = normalizeBaseUrl4(baseUrl);
4935
4152
  providerIndex.add(normalized);
4936
4153
  knownProviders.add(normalized);
4937
4154
  }
4938
4155
  for (const baseUrl of knownProviders) {
4939
- const normalized = normalizeBaseUrl6(baseUrl);
4156
+ const normalized = normalizeBaseUrl4(baseUrl);
4940
4157
  const models = await driver.getItem(
4941
4158
  modelKey(normalized),
4942
4159
  null
@@ -4957,19 +4174,19 @@ var createShardedDiscoveryAdapter = async (options) => {
4957
4174
  }
4958
4175
  let mints = Object.fromEntries(
4959
4176
  Object.entries(rawMints).map(([baseUrl, mintList]) => [
4960
- normalizeBaseUrl6(baseUrl),
4177
+ normalizeBaseUrl4(baseUrl),
4961
4178
  mintList.map((mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint)
4962
4179
  ])
4963
4180
  );
4964
4181
  let info = Object.fromEntries(
4965
4182
  Object.entries(rawInfo).map(([baseUrl, entry]) => [
4966
- normalizeBaseUrl6(baseUrl),
4183
+ normalizeBaseUrl4(baseUrl),
4967
4184
  entry
4968
4185
  ])
4969
4186
  );
4970
4187
  let _lastUsedModel = lastUsedModel;
4971
- let _disabledProviders = rawDisabled.map(normalizeBaseUrl6);
4972
- let _baseUrlsList = rawBaseUrls.map(normalizeBaseUrl6);
4188
+ let _disabledProviders = rawDisabled.map(normalizeBaseUrl4);
4189
+ let _baseUrlsList = rawBaseUrls.map(normalizeBaseUrl4);
4973
4190
  let _lastBaseUrlsUpdate = lastBaseUrlsUpdate;
4974
4191
  let _routstr21Models = rawRoutstr21Models;
4975
4192
  let _lastRoutstr21ModelsUpdate = lastRoutstr21ModelsUpdate;
@@ -4987,10 +4204,10 @@ var createShardedDiscoveryAdapter = async (options) => {
4987
4204
  },
4988
4205
  setCachedModels: (models) => {
4989
4206
  const nextKeys = new Set(
4990
- Object.keys(models).map((baseUrl) => normalizeBaseUrl6(baseUrl))
4207
+ Object.keys(models).map((baseUrl) => normalizeBaseUrl4(baseUrl))
4991
4208
  );
4992
4209
  for (const baseUrl of [...modelsByBaseUrl.keys()]) {
4993
- if (!nextKeys.has(normalizeBaseUrl6(baseUrl))) {
4210
+ if (!nextKeys.has(normalizeBaseUrl4(baseUrl))) {
4994
4211
  providerIndex.delete(baseUrl);
4995
4212
  modelsByBaseUrl.delete(baseUrl);
4996
4213
  timestampsByBaseUrl.delete(baseUrl);
@@ -4999,7 +4216,7 @@ var createShardedDiscoveryAdapter = async (options) => {
4999
4216
  }
5000
4217
  }
5001
4218
  for (const [baseUrl, modelList] of Object.entries(models)) {
5002
- const normalized = normalizeBaseUrl6(baseUrl);
4219
+ const normalized = normalizeBaseUrl4(baseUrl);
5003
4220
  providerIndex.add(normalized);
5004
4221
  modelsByBaseUrl.set(normalized, modelList);
5005
4222
  const ts = timestampsByBaseUrl.get(normalized) ?? Date.now();
@@ -5010,10 +4227,10 @@ var createShardedDiscoveryAdapter = async (options) => {
5010
4227
  persistProviderIndex();
5011
4228
  },
5012
4229
  getProviderLastUpdate: (baseUrl) => {
5013
- return timestampsByBaseUrl.get(normalizeBaseUrl6(baseUrl)) ?? null;
4230
+ return timestampsByBaseUrl.get(normalizeBaseUrl4(baseUrl)) ?? null;
5014
4231
  },
5015
4232
  setProviderLastUpdate: (baseUrl, timestamp) => {
5016
- const normalized = normalizeBaseUrl6(baseUrl);
4233
+ const normalized = normalizeBaseUrl4(baseUrl);
5017
4234
  providerIndex.add(normalized);
5018
4235
  timestampsByBaseUrl.set(normalized, timestamp);
5019
4236
  void driver.setItem(modelTsKey(normalized), timestamp);
@@ -5024,7 +4241,7 @@ var createShardedDiscoveryAdapter = async (options) => {
5024
4241
  setCachedMints: (value) => {
5025
4242
  const normalized = {};
5026
4243
  for (const [baseUrl, mintList] of Object.entries(value)) {
5027
- normalized[normalizeBaseUrl6(baseUrl)] = mintList.map(
4244
+ normalized[normalizeBaseUrl4(baseUrl)] = mintList.map(
5028
4245
  (mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint
5029
4246
  );
5030
4247
  }
@@ -5036,7 +4253,7 @@ var createShardedDiscoveryAdapter = async (options) => {
5036
4253
  setCachedProviderInfo: (value) => {
5037
4254
  const normalized = {};
5038
4255
  for (const [baseUrl, entry] of Object.entries(value)) {
5039
- normalized[normalizeBaseUrl6(baseUrl)] = entry;
4256
+ normalized[normalizeBaseUrl4(baseUrl)] = entry;
5040
4257
  }
5041
4258
  info = normalized;
5042
4259
  void driver.setItem(SDK_STORAGE_KEYS.INFO_FROM_ALL_PROVIDERS, normalized);
@@ -5050,7 +4267,7 @@ var createShardedDiscoveryAdapter = async (options) => {
5050
4267
  // -- Disabled providers (kv) --
5051
4268
  getDisabledProviders: () => _disabledProviders,
5052
4269
  setDisabledProviders: (urls) => {
5053
- const normalized = urls.map(normalizeBaseUrl6);
4270
+ const normalized = urls.map(normalizeBaseUrl4);
5054
4271
  _disabledProviders = normalized;
5055
4272
  void driver.setItem(SDK_STORAGE_KEYS.DISABLED_PROVIDERS, normalized);
5056
4273
  },
@@ -5058,7 +4275,7 @@ var createShardedDiscoveryAdapter = async (options) => {
5058
4275
  getBaseUrlsList: () => _baseUrlsList,
5059
4276
  getBaseUrlsLastUpdate: () => _lastBaseUrlsUpdate,
5060
4277
  setBaseUrlsList: (urls) => {
5061
- const normalized = urls.map(normalizeBaseUrl6);
4278
+ const normalized = urls.map(normalizeBaseUrl4);
5062
4279
  _baseUrlsList = normalized;
5063
4280
  void driver.setItem(SDK_STORAGE_KEYS.BASE_URLS_LIST, normalized);
5064
4281
  },
@@ -5086,16 +4303,16 @@ var createProviderRegistryFromDiscoveryAdapter = (adapter, logger) => {
5086
4303
  const log = (logger ?? consoleLogger).child("ProviderRegistry");
5087
4304
  return {
5088
4305
  getModelsForProvider: (baseUrl) => {
5089
- const normalized = normalizeBaseUrl6(baseUrl);
4306
+ const normalized = normalizeBaseUrl4(baseUrl);
5090
4307
  return adapter.getCachedModels()[normalized] || [];
5091
4308
  },
5092
4309
  getDisabledProviders: () => adapter.getDisabledProviders(),
5093
4310
  getProviderMints: (baseUrl) => {
5094
- const normalized = normalizeBaseUrl6(baseUrl);
4311
+ const normalized = normalizeBaseUrl4(baseUrl);
5095
4312
  return adapter.getCachedMints()[normalized] || [];
5096
4313
  },
5097
4314
  getProviderInfo: async (baseUrl) => {
5098
- const normalized = normalizeBaseUrl6(baseUrl);
4315
+ const normalized = normalizeBaseUrl4(baseUrl);
5099
4316
  const cached = adapter.getCachedProviderInfo()[normalized];
5100
4317
  if (cached) return cached;
5101
4318
  try {
@@ -5126,89 +4343,222 @@ var isBrowser3 = () => {
5126
4343
  return false;
5127
4344
  }
5128
4345
  };
5129
- var isNode = () => {
5130
- try {
5131
- return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
5132
- } catch {
5133
- return false;
5134
- }
5135
- };
5136
4346
  var defaultDriver = null;
5137
- var isBun3 = () => {
5138
- return typeof process.versions.bun !== "undefined";
5139
- };
5140
4347
  var getDefaultSdkDriver = () => {
5141
4348
  if (defaultDriver) return defaultDriver;
5142
4349
  if (isBrowser3()) {
5143
4350
  defaultDriver = localStorageDriver;
5144
4351
  return defaultDriver;
5145
4352
  }
5146
- if (isBun3()) {
5147
- defaultDriver = createMemoryDriver();
5148
- return defaultDriver;
4353
+ defaultDriver = createMemoryDriver();
4354
+ return defaultDriver;
4355
+ };
4356
+ var defaultStore = null;
4357
+ var defaultUsageTrackingDriver = null;
4358
+ var getDefaultSdkStore = () => {
4359
+ if (!defaultStore) {
4360
+ defaultStore = createSdkStore({ driver: getDefaultSdkDriver() });
4361
+ }
4362
+ return defaultStore.hydrate.then(() => defaultStore.store);
4363
+ };
4364
+ var getDefaultUsageTrackingDriver = () => {
4365
+ if (defaultUsageTrackingDriver) return defaultUsageTrackingDriver;
4366
+ const storageDriver = getDefaultSdkDriver();
4367
+ if (isBrowser3()) {
4368
+ defaultUsageTrackingDriver = createIndexedDBUsageTrackingDriver({
4369
+ legacyStorageDriver: storageDriver
4370
+ });
4371
+ return defaultUsageTrackingDriver;
4372
+ }
4373
+ defaultUsageTrackingDriver = createMemoryUsageTrackingDriver();
4374
+ return defaultUsageTrackingDriver;
4375
+ };
4376
+ var setDefaultUsageTrackingDriver = (driver) => {
4377
+ defaultUsageTrackingDriver = driver;
4378
+ };
4379
+ var defaultDiscoveryAdapter = null;
4380
+ var getDefaultDiscoveryAdapter = async () => {
4381
+ if (defaultDiscoveryAdapter) return defaultDiscoveryAdapter;
4382
+ const driver = getDefaultSdkDriver();
4383
+ defaultDiscoveryAdapter = await createShardedDiscoveryAdapter({ driver });
4384
+ return defaultDiscoveryAdapter;
4385
+ };
4386
+ var getDefaultStorageAdapter = async () => createStorageAdapterFromStore(await getDefaultSdkStore());
4387
+ var getDefaultProviderRegistry = async () => createProviderRegistryFromDiscoveryAdapter(await getDefaultDiscoveryAdapter());
4388
+
4389
+ // client/usage.ts
4390
+ var numOrUndef = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
4391
+ function extractCostBreakdown(costObj) {
4392
+ if (!costObj || typeof costObj !== "object") return {};
4393
+ return {
4394
+ baseMsats: numOrUndef(costObj.base_msats),
4395
+ inputMsats: numOrUndef(costObj.input_msats),
4396
+ outputMsats: numOrUndef(costObj.output_msats),
4397
+ totalMsats: numOrUndef(costObj.total_msats),
4398
+ totalUsd: numOrUndef(costObj.total_usd),
4399
+ cacheReadInputTokens: numOrUndef(costObj.cache_read_input_tokens),
4400
+ cacheCreationInputTokens: numOrUndef(costObj.cache_creation_input_tokens),
4401
+ cacheReadMsats: numOrUndef(costObj.cache_read_msats),
4402
+ cacheCreationMsats: numOrUndef(costObj.cache_creation_msats),
4403
+ remainingBalanceMsats: numOrUndef(costObj.remaining_balance_msats)
4404
+ };
4405
+ }
4406
+ function extractUsageFromResponseBody(body, fallbackSatsCost = 0) {
4407
+ if (!body || typeof body !== "object") return null;
4408
+ const usage = body.usage;
4409
+ if (!usage || typeof usage !== "object") return null;
4410
+ const promptTokens = Number(usage.prompt_tokens ?? 0);
4411
+ const completionTokens = Number(usage.completion_tokens ?? 0);
4412
+ const totalTokens = Number(usage.total_tokens ?? 0);
4413
+ const costValue = usage.cost;
4414
+ let cost = 0;
4415
+ let satsCost = fallbackSatsCost;
4416
+ let breakdown = {};
4417
+ if (typeof costValue === "number") {
4418
+ cost = costValue;
4419
+ } else if (costValue && typeof costValue === "object") {
4420
+ const costObj = costValue;
4421
+ const totalUsd = costObj.total_usd;
4422
+ const totalMsats = costObj.total_msats;
4423
+ cost = typeof totalUsd === "number" ? totalUsd : 0;
4424
+ if (typeof totalMsats === "number") {
4425
+ satsCost = totalMsats / 1e3;
4426
+ }
4427
+ breakdown = extractCostBreakdown(costObj);
4428
+ }
4429
+ const provider = typeof body.provider === "string" ? body.provider : void 0;
4430
+ if (promptTokens === 0 && completionTokens === 0 && totalTokens === 0 && cost === 0 && satsCost === 0) {
4431
+ return null;
4432
+ }
4433
+ return {
4434
+ promptTokens,
4435
+ completionTokens,
4436
+ totalTokens,
4437
+ cost,
4438
+ satsCost,
4439
+ provider,
4440
+ ...breakdown
4441
+ };
4442
+ }
4443
+ function extractResponseId(body) {
4444
+ if (!body || typeof body !== "object") return void 0;
4445
+ const id = body.id;
4446
+ if (typeof id !== "string") return void 0;
4447
+ const trimmed = id.trim();
4448
+ return trimmed.length > 0 ? trimmed : void 0;
4449
+ }
4450
+ function extractUsageFromSSEJson(parsed, fallbackSatsCost = 0) {
4451
+ if (!parsed || typeof parsed !== "object") {
4452
+ return null;
4453
+ }
4454
+ const provider = typeof parsed.provider === "string" ? parsed.provider : void 0;
4455
+ if (!parsed.usage && parsed.cost && typeof parsed.cost === "object") {
4456
+ const costObj = parsed.cost;
4457
+ const msats2 = costObj.total_msats ?? 0;
4458
+ const cost2 = costObj.total_usd ?? 0;
4459
+ if (msats2 === 0 && cost2 === 0) return null;
4460
+ return {
4461
+ promptTokens: Number(costObj.input_tokens ?? 0),
4462
+ completionTokens: Number(costObj.output_tokens ?? 0),
4463
+ totalTokens: Number((costObj.input_tokens ?? 0) + (costObj.output_tokens ?? 0)),
4464
+ cost: Number(cost2),
4465
+ satsCost: msats2 > 0 ? msats2 / 1e3 : fallbackSatsCost,
4466
+ provider,
4467
+ ...extractCostBreakdown(costObj)
4468
+ };
4469
+ }
4470
+ if (!parsed.usage) {
4471
+ return null;
5149
4472
  }
5150
- if (isNode()) {
5151
- defaultDriver = createSqliteDriver();
5152
- return defaultDriver;
4473
+ const usage = parsed.usage;
4474
+ const usageCost = usage.cost;
4475
+ let cost = 0;
4476
+ let msats = 0;
4477
+ let breakdown = {};
4478
+ if (typeof usageCost === "number") {
4479
+ cost = usageCost;
4480
+ } else if (usageCost && typeof usageCost === "object") {
4481
+ cost = usageCost.total_usd ?? 0;
4482
+ msats = usageCost.total_msats ?? 0;
4483
+ breakdown = extractCostBreakdown(usageCost);
5153
4484
  }
5154
- defaultDriver = createMemoryDriver();
5155
- return defaultDriver;
5156
- };
5157
- var defaultStore = null;
5158
- var defaultUsageTrackingDriver = null;
5159
- var getDefaultSdkStore = () => {
5160
- if (!defaultStore) {
5161
- defaultStore = createSdkStore({ driver: getDefaultSdkDriver() });
4485
+ const routstrCost = parsed.metadata?.routstr?.cost;
4486
+ if (routstrCost && typeof routstrCost === "object") {
4487
+ breakdown = { ...extractCostBreakdown(routstrCost), ...breakdown };
5162
4488
  }
5163
- return defaultStore.hydrate.then(() => defaultStore.store);
5164
- };
5165
- var getDefaultUsageTrackingDriver = () => {
5166
- if (defaultUsageTrackingDriver) return defaultUsageTrackingDriver;
5167
- const storageDriver = getDefaultSdkDriver();
5168
- if (isBrowser3()) {
5169
- defaultUsageTrackingDriver = createIndexedDBUsageTrackingDriver({
5170
- legacyStorageDriver: storageDriver
5171
- });
5172
- return defaultUsageTrackingDriver;
4489
+ if (cost === 0) {
4490
+ cost = parsed.metadata?.routstr?.cost?.total_usd ?? 0;
5173
4491
  }
5174
- if (isBun3()) {
5175
- defaultUsageTrackingDriver = createBunSqliteUsageTrackingDriver();
5176
- return defaultUsageTrackingDriver;
4492
+ if (msats === 0) {
4493
+ msats = parsed.metadata?.routstr?.cost?.total_msats ?? (typeof usage.cost_sats === "number" ? usage.cost_sats * 1e3 : 0);
5177
4494
  }
5178
- if (isNode()) {
5179
- defaultUsageTrackingDriver = createSqliteUsageTrackingDriver({
5180
- legacyStorageDriver: storageDriver
5181
- });
5182
- return defaultUsageTrackingDriver;
4495
+ const promptTokens = Number(usage.prompt_tokens ?? usage.input_tokens ?? 0);
4496
+ const completionTokens = Number(usage.completion_tokens ?? usage.output_tokens ?? 0);
4497
+ const totalTokens = Number(usage.total_tokens ?? promptTokens + completionTokens);
4498
+ const result = {
4499
+ promptTokens,
4500
+ completionTokens,
4501
+ totalTokens,
4502
+ cost: Number(cost ?? 0),
4503
+ satsCost: msats > 0 ? msats / 1e3 : fallbackSatsCost,
4504
+ provider,
4505
+ ...breakdown
4506
+ };
4507
+ if (result.promptTokens === 0 && result.completionTokens === 0 && result.totalTokens === 0 && result.cost === 0 && result.satsCost === 0) {
4508
+ return null;
5183
4509
  }
5184
- defaultUsageTrackingDriver = createMemoryUsageTrackingDriver();
5185
- return defaultUsageTrackingDriver;
5186
- };
5187
- var setDefaultUsageTrackingDriver = (driver) => {
5188
- defaultUsageTrackingDriver = driver;
5189
- };
5190
- var defaultDiscoveryAdapter = null;
5191
- var getDefaultDiscoveryAdapter = async () => {
5192
- if (defaultDiscoveryAdapter) return defaultDiscoveryAdapter;
5193
- const driver = getDefaultSdkDriver();
5194
- defaultDiscoveryAdapter = await createShardedDiscoveryAdapter({ driver });
5195
- return defaultDiscoveryAdapter;
5196
- };
5197
- var getDefaultStorageAdapter = async () => createStorageAdapterFromStore(await getDefaultSdkStore());
5198
- var getDefaultProviderRegistry = async () => createProviderRegistryFromDiscoveryAdapter(await getDefaultDiscoveryAdapter());
4510
+ return result;
4511
+ }
4512
+ function toUsageStats(usage) {
4513
+ if (!usage) return void 0;
4514
+ return {
4515
+ total_tokens: usage.totalTokens,
4516
+ prompt_tokens: usage.promptTokens,
4517
+ completion_tokens: usage.completionTokens,
4518
+ cost: usage.cost,
4519
+ sats_cost: usage.satsCost
4520
+ };
4521
+ }
5199
4522
  function mergeUsage(previous, next) {
5200
4523
  if (!previous) return next;
4524
+ const pickNum = (n, p) => typeof n === "number" && n > 0 ? n : p ?? n;
5201
4525
  return {
5202
4526
  promptTokens: next.promptTokens > 0 ? next.promptTokens : previous.promptTokens,
5203
4527
  completionTokens: next.completionTokens > 0 ? next.completionTokens : previous.completionTokens,
5204
4528
  totalTokens: next.totalTokens > 0 ? next.totalTokens : previous.totalTokens,
5205
4529
  cost: next.cost > 0 ? next.cost : previous.cost,
5206
- satsCost: next.satsCost > 0 ? next.satsCost : previous.satsCost
4530
+ satsCost: next.satsCost > 0 ? next.satsCost : previous.satsCost,
4531
+ provider: next.provider ?? previous.provider,
4532
+ baseMsats: pickNum(next.baseMsats, previous.baseMsats),
4533
+ inputMsats: pickNum(next.inputMsats, previous.inputMsats),
4534
+ outputMsats: pickNum(next.outputMsats, previous.outputMsats),
4535
+ totalMsats: pickNum(next.totalMsats, previous.totalMsats),
4536
+ totalUsd: pickNum(next.totalUsd, previous.totalUsd),
4537
+ cacheReadInputTokens: pickNum(
4538
+ next.cacheReadInputTokens,
4539
+ previous.cacheReadInputTokens
4540
+ ),
4541
+ cacheCreationInputTokens: pickNum(
4542
+ next.cacheCreationInputTokens,
4543
+ previous.cacheCreationInputTokens
4544
+ ),
4545
+ cacheReadMsats: pickNum(next.cacheReadMsats, previous.cacheReadMsats),
4546
+ cacheCreationMsats: pickNum(
4547
+ next.cacheCreationMsats,
4548
+ previous.cacheCreationMsats
4549
+ ),
4550
+ remainingBalanceMsats: pickNum(
4551
+ next.remainingBalanceMsats,
4552
+ previous.remainingBalanceMsats
4553
+ )
5207
4554
  };
5208
4555
  }
5209
4556
  function hasUsageChanged(previous, next) {
5210
4557
  if (!previous) return true;
5211
- return previous.promptTokens !== next.promptTokens || previous.completionTokens !== next.completionTokens || previous.totalTokens !== next.totalTokens || previous.cost !== next.cost || previous.satsCost !== next.satsCost;
4558
+ return previous.promptTokens !== next.promptTokens || previous.completionTokens !== next.completionTokens || previous.totalTokens !== next.totalTokens || previous.cost !== next.cost || previous.satsCost !== next.satsCost || previous.provider !== next.provider || previous.totalMsats !== next.totalMsats || previous.remainingBalanceMsats !== next.remainingBalanceMsats;
4559
+ }
4560
+ function isInspectionComplete(responseIdCaptured, usage) {
4561
+ return responseIdCaptured && !!usage && usage.totalTokens > 0 && typeof usage.totalMsats === "number" && !!usage.provider;
5212
4562
  }
5213
4563
  async function inspectSSEWebStream(stream, onUsage, onResponseId) {
5214
4564
  const reader = stream.getReader();
@@ -5218,14 +4568,22 @@ async function inspectSSEWebStream(stream, onUsage, onResponseId) {
5218
4568
  let capturedResponseId;
5219
4569
  let responseIdCaptured = false;
5220
4570
  const inspectDataPayload = (jsonText) => {
5221
- if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
4571
+ const trimmed = jsonText.trim();
4572
+ if (!trimmed || trimmed === "[DONE]") {
4573
+ if (trimmed === "[DONE]") console.log("[routstr:sse] [DONE]");
4574
+ return;
4575
+ }
4576
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
4577
+ console.log("[routstr:sse] non-JSON payload:", trimmed.slice(0, 200));
5222
4578
  return;
5223
4579
  }
5224
- const trimmed = jsonText.trim();
5225
- if (!trimmed || trimmed === "[DONE]") return;
5226
- if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return;
5227
4580
  try {
5228
4581
  const data = JSON.parse(trimmed);
4582
+ console.log("[routstr:sse] chunk:", JSON.stringify(data));
4583
+ if (isInspectionComplete(responseIdCaptured, capturedUsage)) {
4584
+ console.log("[routstr:sse] (inspection already complete, skipping)");
4585
+ return;
4586
+ }
5229
4587
  if (!responseIdCaptured) {
5230
4588
  const responseId = data?.id;
5231
4589
  if (typeof responseId === "string" && responseId.trim().length > 0) {
@@ -5236,19 +4594,21 @@ async function inspectSSEWebStream(stream, onUsage, onResponseId) {
5236
4594
  }
5237
4595
  const usage = extractUsageFromSSEJson(data);
5238
4596
  if (usage) {
4597
+ console.log("[routstr:sse] \u2192 usage detected:", usage);
5239
4598
  const merged = mergeUsage(capturedUsage, usage);
5240
4599
  if (hasUsageChanged(capturedUsage, merged)) {
5241
4600
  capturedUsage = merged;
4601
+ console.log("[routstr:sse] \u2192 merged (changed):", merged);
5242
4602
  onUsage(merged);
4603
+ } else {
4604
+ console.log("[routstr:sse] \u2192 merged (no change)");
5243
4605
  }
5244
4606
  }
5245
4607
  } catch {
4608
+ console.log("[routstr:sse] failed to parse payload:", trimmed.slice(0, 200));
5246
4609
  }
5247
4610
  };
5248
4611
  const inspectEventBlock = (eventBlock) => {
5249
- if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
5250
- return;
5251
- }
5252
4612
  const lines = eventBlock.split(/\r?\n/);
5253
4613
  const dataParts = [];
5254
4614
  for (const line of lines) {
@@ -5306,14 +4666,22 @@ function createSSEParserTransform(onUsage, onResponseId) {
5306
4666
  let capturedUsage = null;
5307
4667
  let responseIdCaptured = false;
5308
4668
  const inspectDataPayload = (jsonText) => {
5309
- if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
4669
+ const trimmed = jsonText.trim();
4670
+ if (!trimmed || trimmed === "[DONE]") {
4671
+ if (trimmed === "[DONE]") console.log("[routstr:sse] [DONE]");
4672
+ return;
4673
+ }
4674
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
4675
+ console.log("[routstr:sse] non-JSON payload:", trimmed.slice(0, 200));
5310
4676
  return;
5311
4677
  }
5312
- const trimmed = jsonText.trim();
5313
- if (!trimmed || trimmed === "[DONE]") return;
5314
- if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return;
5315
4678
  try {
5316
4679
  const data = JSON.parse(trimmed);
4680
+ console.log("[routstr:sse] chunk:", JSON.stringify(data));
4681
+ if (isInspectionComplete(responseIdCaptured, capturedUsage)) {
4682
+ console.log("[routstr:sse] (inspection already complete, skipping)");
4683
+ return;
4684
+ }
5317
4685
  if (!responseIdCaptured) {
5318
4686
  const responseId = data?.id;
5319
4687
  if (typeof responseId === "string" && responseId.trim().length > 0) {
@@ -5323,19 +4691,21 @@ function createSSEParserTransform(onUsage, onResponseId) {
5323
4691
  }
5324
4692
  const usage = extractUsageFromSSEJson(data);
5325
4693
  if (usage) {
4694
+ console.log("[routstr:sse] \u2192 usage detected:", usage);
5326
4695
  const mergedUsage = mergeUsage(capturedUsage, usage);
5327
4696
  if (hasUsageChanged(capturedUsage, mergedUsage)) {
5328
4697
  capturedUsage = mergedUsage;
4698
+ console.log("[routstr:sse] \u2192 merged (changed):", mergedUsage);
5329
4699
  onUsage(mergedUsage);
4700
+ } else {
4701
+ console.log("[routstr:sse] \u2192 merged (no change)");
5330
4702
  }
5331
4703
  }
5332
4704
  } catch {
4705
+ console.log("[routstr:sse] failed to parse payload:", trimmed.slice(0, 200));
5333
4706
  }
5334
4707
  };
5335
4708
  const inspectEventBlock = (eventBlock) => {
5336
- if (responseIdCaptured && capturedUsage && capturedUsage.totalTokens > 0) {
5337
- return;
5338
- }
5339
4709
  const lines = eventBlock.split(/\r?\n/);
5340
4710
  const dataParts = [];
5341
4711
  for (const line of lines) {
@@ -5408,7 +4778,6 @@ var RoutstrClient = class {
5408
4778
  this.balanceManager,
5409
4779
  this.logger
5410
4780
  );
5411
- this.streamProcessor = new StreamProcessor();
5412
4781
  this.alertLevel = alertLevel;
5413
4782
  this.mode = mode;
5414
4783
  this.usageTrackingDriver = options.usageTrackingDriver;
@@ -5420,7 +4789,6 @@ var RoutstrClient = class {
5420
4789
  providerRegistry;
5421
4790
  cashuSpender;
5422
4791
  balanceManager;
5423
- streamProcessor;
5424
4792
  providerManager;
5425
4793
  alertLevel;
5426
4794
  mode;
@@ -5662,153 +5030,6 @@ var RoutstrClient = class {
5662
5030
  }
5663
5031
  return void 0;
5664
5032
  }
5665
- /**
5666
- * Fetch AI response with streaming
5667
- */
5668
- async fetchAIResponse(options, callbacks) {
5669
- const {
5670
- messageHistory,
5671
- selectedModel,
5672
- baseUrl,
5673
- mintUrl,
5674
- balance,
5675
- transactionHistory,
5676
- maxTokens,
5677
- headers
5678
- } = options;
5679
- const apiMessages = await this._convertMessages(messageHistory);
5680
- const requiredSats = this.providerManager.getRequiredSatsForModel(
5681
- selectedModel,
5682
- apiMessages,
5683
- maxTokens
5684
- );
5685
- try {
5686
- await this._checkBalance();
5687
- callbacks.onPaymentProcessing?.(true);
5688
- const spendResult = await this._spendToken({
5689
- mintUrl,
5690
- amount: requiredSats,
5691
- baseUrl
5692
- });
5693
- let token = spendResult.token;
5694
- let tokenBalance = spendResult.tokenBalance;
5695
- let tokenBalanceUnit = spendResult.tokenBalanceUnit;
5696
- let tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
5697
- let initialTokenBalanceUnknown = spendResult.tokenBalanceUnknown;
5698
- callbacks.onTokenCreated?.(this._getPendingCashuTokenAmount());
5699
- const baseHeaders = this._buildBaseHeaders(headers);
5700
- const requestHeaders = this._withAuthHeader(baseHeaders, token);
5701
- const providerInfo = await this.providerRegistry.getProviderInfo(baseUrl);
5702
- const providerVersion = providerInfo?.version ?? "";
5703
- let modelIdForRequest = selectedModel.id;
5704
- if (/^0\.1\./.test(providerVersion)) {
5705
- const newModel = await this.providerManager.getModelForProvider(
5706
- baseUrl,
5707
- selectedModel.id
5708
- );
5709
- modelIdForRequest = newModel?.id ?? selectedModel.id;
5710
- }
5711
- const body = {
5712
- model: modelIdForRequest,
5713
- messages: apiMessages,
5714
- stream: true
5715
- };
5716
- if (maxTokens !== void 0) {
5717
- body.max_tokens = maxTokens;
5718
- }
5719
- if (selectedModel?.name?.startsWith("OpenAI:")) {
5720
- body.tools = [{ type: "web_search" }];
5721
- }
5722
- const response = await this._makeRequest({
5723
- path: "/v1/chat/completions",
5724
- method: "POST",
5725
- body,
5726
- selectedModel,
5727
- baseUrl,
5728
- mintUrl,
5729
- token,
5730
- requiredSats,
5731
- maxTokens,
5732
- headers: requestHeaders,
5733
- baseHeaders
5734
- });
5735
- if (!response.body) {
5736
- throw new Error("Response body is not available");
5737
- }
5738
- if (response.status === 200) {
5739
- const baseUrlUsed = response.baseUrl || baseUrl;
5740
- const responseToken = response.token || token;
5741
- if (baseUrlUsed !== baseUrl || responseToken !== token) {
5742
- token = responseToken;
5743
- if (typeof response.initialTokenBalanceInSats === "number") {
5744
- tokenBalanceInSats = response.initialTokenBalanceInSats;
5745
- initialTokenBalanceUnknown = Boolean(
5746
- response.initialTokenBalanceUnknown
5747
- );
5748
- } else {
5749
- initialTokenBalanceUnknown = true;
5750
- }
5751
- }
5752
- const streamingResult = await this.streamProcessor.process(
5753
- response,
5754
- {
5755
- onContent: callbacks.onStreamingUpdate,
5756
- onThinking: callbacks.onThinkingUpdate
5757
- },
5758
- selectedModel.id
5759
- );
5760
- if (streamingResult.finish_reason === "content_filter") {
5761
- callbacks.onMessageAppend({
5762
- role: "assistant",
5763
- content: "Your request was denied due to content filtering."
5764
- });
5765
- } else if (streamingResult.content || streamingResult.images && streamingResult.images.length > 0) {
5766
- const message = await this._createAssistantMessage(streamingResult);
5767
- callbacks.onMessageAppend(message);
5768
- } else {
5769
- callbacks.onMessageAppend({
5770
- role: "system",
5771
- content: "The provider did not respond to this request."
5772
- });
5773
- }
5774
- callbacks.onStreamingUpdate("");
5775
- callbacks.onThinkingUpdate("");
5776
- const isApikeysEstimate = this.mode === "apikeys";
5777
- let satsSpent = await this._handlePostResponseBalanceUpdate({
5778
- token,
5779
- baseUrl: baseUrlUsed,
5780
- mintUrl,
5781
- initialTokenBalance: tokenBalanceInSats,
5782
- initialTokenBalanceUnknown,
5783
- fallbackSatsSpent: isApikeysEstimate ? this._getEstimatedCosts(selectedModel, streamingResult) : void 0,
5784
- response,
5785
- modelId: selectedModel.id,
5786
- usage: streamingResult.usage ? {
5787
- promptTokens: Number(streamingResult.usage.prompt_tokens ?? 0),
5788
- completionTokens: Number(
5789
- streamingResult.usage.completion_tokens ?? 0
5790
- ),
5791
- totalTokens: Number(streamingResult.usage.total_tokens ?? 0),
5792
- cost: Number(streamingResult.usage.cost ?? 0),
5793
- satsCost: Number(streamingResult.usage.sats_cost ?? 0)
5794
- } : void 0,
5795
- requestId: streamingResult.responseId
5796
- });
5797
- const estimatedCosts = this._getEstimatedCosts(
5798
- selectedModel,
5799
- streamingResult
5800
- );
5801
- const onLastMessageSatsUpdate = callbacks.onLastMessageSatsUpdate;
5802
- onLastMessageSatsUpdate?.(satsSpent, estimatedCosts);
5803
- } else {
5804
- throw new Error(`${response.status} ${response.statusText}`);
5805
- }
5806
- } catch (error) {
5807
- this._handleError(error, callbacks);
5808
- } finally {
5809
- callbacks.onPaymentProcessing?.(false);
5810
- }
5811
- }
5812
5033
  /**
5813
5034
  * Make the API request with failover support
5814
5035
  */
@@ -6304,292 +5525,555 @@ var RoutstrClient = class {
6304
5525
  requestId = requestId ?? extractResponseId(responseBody) ?? response.headers.get("x-routstr-request-id") ?? void 0;
6305
5526
  }
6306
5527
  }
6307
- if (!usage) {
6308
- return;
5528
+ if (!usage) {
5529
+ return;
5530
+ }
5531
+ const finalRequestId = requestId || "unknown";
5532
+ const store = this.sdkStore ?? await getDefaultSdkStore();
5533
+ const state = store.getState();
5534
+ const matchKey = clientApiKey ?? token;
5535
+ const matchingClient = state.clientIds.find(
5536
+ (client) => client.apiKey === matchKey
5537
+ );
5538
+ const entryId = finalRequestId === "unknown" ? `req-${Date.now()}-${modelId}` : finalRequestId;
5539
+ const usageTracking = this.usageTrackingDriver ?? getDefaultUsageTrackingDriver();
5540
+ const entry = {
5541
+ id: entryId,
5542
+ timestamp: Date.now(),
5543
+ modelId,
5544
+ baseUrl,
5545
+ requestId: finalRequestId,
5546
+ client: matchingClient?.clientId,
5547
+ ...usage
5548
+ };
5549
+ if (this.mode === "xcashu") {
5550
+ entry.satsCost = satsSpent;
5551
+ }
5552
+ await usageTracking.append(entry);
5553
+ } catch (error) {
5554
+ }
5555
+ }
5556
+ /**
5557
+ * Check wallet balance and throw if insufficient
5558
+ */
5559
+ async _checkBalance() {
5560
+ const balances = await this.walletAdapter.getBalances();
5561
+ const totalBalance = Object.values(balances).reduce((sum, v) => sum + v, 0);
5562
+ if (totalBalance <= 0) {
5563
+ throw new InsufficientBalanceError(1, 0);
5564
+ }
5565
+ }
5566
+ /**
5567
+ * Spend a token using CashuSpender with standardized error handling
5568
+ */
5569
+ async _spendToken(params) {
5570
+ const { mintUrl, amount, baseUrl } = params;
5571
+ this._log(
5572
+ "DEBUG",
5573
+ `[RoutstrClient] _spendToken: mode=${this.mode}, amount=${amount}, baseUrl=${baseUrl}, mintUrl=${mintUrl}`
5574
+ );
5575
+ if (this.mode === "apikeys") {
5576
+ let parentApiKey = this.storageAdapter.getApiKey(baseUrl);
5577
+ if (!parentApiKey) {
5578
+ this._log(
5579
+ "DEBUG",
5580
+ `[RoutstrClient] _spendToken: No existing API key for ${baseUrl}, creating new one via Cashu`
5581
+ );
5582
+ const spendResult2 = await this.cashuSpender.spend({
5583
+ mintUrl,
5584
+ amount: amount * TOPUP_MARGIN,
5585
+ baseUrl: "",
5586
+ reuseToken: false
5587
+ });
5588
+ if (!spendResult2.token) {
5589
+ this._log(
5590
+ "ERROR",
5591
+ `[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error:`,
5592
+ spendResult2.error
5593
+ );
5594
+ throw new Error(
5595
+ `[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error: ${spendResult2.error}`
5596
+ );
5597
+ } else {
5598
+ this._log(
5599
+ "DEBUG",
5600
+ `[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult2.token}`
5601
+ );
5602
+ }
5603
+ this._log(
5604
+ "DEBUG",
5605
+ `[RoutstrClient] _spendToken: Created API key for ${baseUrl}, key preview: ${spendResult2.token}, balance: ${spendResult2.balance}`
5606
+ );
5607
+ try {
5608
+ this.storageAdapter.setApiKey(baseUrl, spendResult2.token);
5609
+ } catch (error) {
5610
+ if (error instanceof Error && error.message.includes("ApiKey already exists")) {
5611
+ const receiveResult = await this.cashuSpender.receiveToken(
5612
+ spendResult2.token
5613
+ );
5614
+ if (receiveResult.success) {
5615
+ this._log(
5616
+ "DEBUG",
5617
+ `[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${receiveResult.amount}`
5618
+ );
5619
+ } else {
5620
+ this._log(
5621
+ "DEBUG",
5622
+ `[RoutstrClient] _handleErrorResponse: Token restore failed: ${receiveResult.message}`
5623
+ );
5624
+ }
5625
+ this._log(
5626
+ "DEBUG",
5627
+ `[RoutstrClient] _spendToken: API key already exists for ${baseUrl}, using existing key`
5628
+ );
5629
+ } else {
5630
+ throw error;
5631
+ }
5632
+ }
5633
+ parentApiKey = this.storageAdapter.getApiKey(baseUrl);
5634
+ } else {
5635
+ this._log(
5636
+ "DEBUG",
5637
+ `[RoutstrClient] _spendToken: Using existing API key for ${baseUrl}, key preview: ${parentApiKey.key}`
5638
+ );
5639
+ }
5640
+ let tokenBalance = 0;
5641
+ let tokenBalanceUnit = "sat";
5642
+ let tokenBalanceUnknown = false;
5643
+ const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
5644
+ const distributionForBaseUrl = apiKeyDistribution.find(
5645
+ (d) => d.baseUrl === baseUrl
5646
+ );
5647
+ if (distributionForBaseUrl) {
5648
+ tokenBalance = distributionForBaseUrl.amount;
6309
5649
  }
6310
- const finalRequestId = requestId || "unknown";
6311
- const store = this.sdkStore ?? await getDefaultSdkStore();
6312
- const state = store.getState();
6313
- const matchKey = clientApiKey ?? token;
6314
- const matchingClient = state.clientIds.find(
6315
- (client) => client.apiKey === matchKey
5650
+ if (tokenBalance === 0 && parentApiKey) {
5651
+ try {
5652
+ const balanceInfo = await this.balanceManager.getTokenBalance(
5653
+ parentApiKey.key,
5654
+ baseUrl
5655
+ );
5656
+ tokenBalance = balanceInfo.amount;
5657
+ tokenBalanceUnit = balanceInfo.unit;
5658
+ tokenBalanceUnknown = Boolean(balanceInfo.balanceUnknown);
5659
+ } catch (e) {
5660
+ this._log("WARN", "Could not get initial API key balance:", e);
5661
+ }
5662
+ }
5663
+ this._log(
5664
+ "DEBUG",
5665
+ `[RoutstrClient] _spendToken: Returning token with balance=${tokenBalance} ${tokenBalanceUnit}`
6316
5666
  );
6317
- const entryId = finalRequestId === "unknown" ? `req-${Date.now()}-${modelId}` : finalRequestId;
6318
- const usageTracking = this.usageTrackingDriver ?? getDefaultUsageTrackingDriver();
6319
- const entry = {
6320
- id: entryId,
6321
- timestamp: Date.now(),
6322
- modelId,
6323
- baseUrl,
6324
- requestId: finalRequestId,
6325
- client: matchingClient?.clientId,
6326
- ...usage
5667
+ return {
5668
+ token: parentApiKey?.key ?? "",
5669
+ tokenBalance,
5670
+ tokenBalanceUnit,
5671
+ tokenBalanceUnknown
6327
5672
  };
6328
- if (this.mode === "xcashu") {
6329
- entry.satsCost = satsSpent;
6330
- }
6331
- await usageTracking.append(entry);
6332
- } catch (error) {
6333
5673
  }
5674
+ this._log(
5675
+ "DEBUG",
5676
+ `[RoutstrClient] _spendToken: Calling CashuSpender.spend for amount=${amount}, mintUrl=${mintUrl}, mode=${this.mode}`
5677
+ );
5678
+ const spendResult = await this.cashuSpender.spend({
5679
+ mintUrl,
5680
+ amount,
5681
+ baseUrl: "",
5682
+ reuseToken: false
5683
+ });
5684
+ if (!spendResult.token) {
5685
+ this._log(
5686
+ "ERROR",
5687
+ `[RoutstrClient] _spendToken: CashuSpender.spend failed, error:`,
5688
+ spendResult.error
5689
+ );
5690
+ } else {
5691
+ this._log(
5692
+ "DEBUG",
5693
+ `[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult.token}, balance: ${spendResult.balance} ${spendResult.unit ?? "sat"}`
5694
+ );
5695
+ this.storageAdapter.addXcashuToken(baseUrl, spendResult.token);
5696
+ }
5697
+ return {
5698
+ token: spendResult.token,
5699
+ tokenBalance: spendResult.balance,
5700
+ tokenBalanceUnit: spendResult.unit ?? "sat",
5701
+ tokenBalanceUnknown: false
5702
+ };
6334
5703
  }
6335
5704
  /**
6336
- * Convert messages for API format
5705
+ * Build request headers with common defaults and dev mock controls
6337
5706
  */
6338
- async _convertMessages(messages) {
6339
- return Promise.all(
6340
- messages.filter((m) => m.role !== "system").map(async (m) => ({
6341
- role: m.role,
6342
- content: typeof m.content === "string" ? m.content : m.content
6343
- }))
6344
- );
5707
+ _buildBaseHeaders(additionalHeaders = {}, token) {
5708
+ const headers = {
5709
+ ...additionalHeaders,
5710
+ "Content-Type": "application/json"
5711
+ };
5712
+ return headers;
6345
5713
  }
6346
5714
  /**
6347
- * Create assistant message from streaming result
5715
+ * Attach auth headers using the active client mode
6348
5716
  */
6349
- async _createAssistantMessage(result) {
6350
- if (result.images && result.images.length > 0) {
6351
- const content = [];
6352
- if (result.content) {
6353
- content.push({
6354
- type: "text",
6355
- text: result.content,
6356
- thinking: result.thinking,
6357
- citations: result.citations,
6358
- annotations: result.annotations
6359
- });
6360
- }
6361
- for (const img of result.images) {
6362
- content.push({
6363
- type: "image_url",
6364
- image_url: {
6365
- url: img.image_url.url
5717
+ _withAuthHeader(headers, token) {
5718
+ const nextHeaders = { ...headers };
5719
+ if (this.mode === "xcashu") {
5720
+ nextHeaders["X-Cashu"] = token;
5721
+ } else {
5722
+ nextHeaders["Authorization"] = `Bearer ${token}`;
5723
+ }
5724
+ return nextHeaders;
5725
+ }
5726
+ };
5727
+
5728
+ // client/StreamProcessor.ts
5729
+ var StreamProcessor = class {
5730
+ accumulatedContent = "";
5731
+ accumulatedThinking = "";
5732
+ accumulatedImages = [];
5733
+ isInThinking = false;
5734
+ isInContent = false;
5735
+ /**
5736
+ * Process a streaming response
5737
+ */
5738
+ async process(response, callbacks, modelId) {
5739
+ if (!response.body) {
5740
+ throw new Error("Response body is not available");
5741
+ }
5742
+ const reader = response.body.getReader();
5743
+ const decoder = new TextDecoder("utf-8");
5744
+ let buffer = "";
5745
+ this.accumulatedContent = "";
5746
+ this.accumulatedThinking = "";
5747
+ this.accumulatedImages = [];
5748
+ this.isInThinking = false;
5749
+ this.isInContent = false;
5750
+ let usage;
5751
+ let model;
5752
+ let finish_reason;
5753
+ let citations;
5754
+ let annotations;
5755
+ let responseId;
5756
+ try {
5757
+ while (true) {
5758
+ const { done, value } = await reader.read();
5759
+ if (done) {
5760
+ break;
5761
+ }
5762
+ const chunk = decoder.decode(value, { stream: true });
5763
+ buffer += chunk;
5764
+ const lines = buffer.split("\n");
5765
+ buffer = lines.pop() || "";
5766
+ for (const line of lines) {
5767
+ const parsed = this._parseLine(line);
5768
+ if (!parsed) continue;
5769
+ if (parsed.content) {
5770
+ this._handleContent(parsed.content, callbacks, modelId);
6366
5771
  }
6367
- });
5772
+ if (parsed.reasoning) {
5773
+ this._handleThinking(parsed.reasoning, callbacks);
5774
+ }
5775
+ if (parsed.usage) {
5776
+ usage = parsed.usage;
5777
+ }
5778
+ if (parsed.model) {
5779
+ model = parsed.model;
5780
+ }
5781
+ if (parsed.finish_reason) {
5782
+ finish_reason = parsed.finish_reason;
5783
+ }
5784
+ if (parsed.responseId) {
5785
+ responseId = parsed.responseId;
5786
+ }
5787
+ if (parsed.citations) {
5788
+ citations = parsed.citations;
5789
+ }
5790
+ if (parsed.annotations) {
5791
+ annotations = parsed.annotations;
5792
+ }
5793
+ if (parsed.images) {
5794
+ this._mergeImages(parsed.images);
5795
+ }
5796
+ }
6368
5797
  }
6369
- return {
6370
- role: "assistant",
6371
- content
6372
- };
5798
+ } finally {
5799
+ reader.releaseLock();
6373
5800
  }
6374
5801
  return {
6375
- role: "assistant",
6376
- content: result.content || ""
5802
+ content: this.accumulatedContent,
5803
+ thinking: this.accumulatedThinking || void 0,
5804
+ images: this.accumulatedImages.length > 0 ? this.accumulatedImages : void 0,
5805
+ usage,
5806
+ model,
5807
+ responseId,
5808
+ finish_reason,
5809
+ citations,
5810
+ annotations
6377
5811
  };
6378
5812
  }
6379
5813
  /**
6380
- * Calculate estimated costs from usage
5814
+ * Parse a single SSE line
6381
5815
  */
6382
- _getEstimatedCosts(selectedModel, streamingResult) {
6383
- let estimatedCosts = 0;
6384
- if (streamingResult.usage) {
6385
- const { completion_tokens, prompt_tokens } = streamingResult.usage;
6386
- if (completion_tokens !== void 0 && prompt_tokens !== void 0) {
6387
- estimatedCosts = (selectedModel.sats_pricing?.completion ?? 0) * completion_tokens + (selectedModel.sats_pricing?.prompt ?? 0) * prompt_tokens;
5816
+ _parseLine(line) {
5817
+ if (!line.trim()) return null;
5818
+ if (!line.startsWith("data: ")) {
5819
+ return null;
5820
+ }
5821
+ const jsonData = line.slice(6);
5822
+ if (jsonData === "[DONE]") {
5823
+ return null;
5824
+ }
5825
+ try {
5826
+ const parsed = JSON.parse(jsonData);
5827
+ const result = {};
5828
+ if (parsed.choices?.[0]?.delta?.content) {
5829
+ result.content = parsed.choices[0].delta.content;
5830
+ }
5831
+ if (parsed.choices?.[0]?.delta?.reasoning) {
5832
+ result.reasoning = parsed.choices[0].delta.reasoning;
5833
+ }
5834
+ const extractedUsage = extractUsageFromSSEJson(parsed);
5835
+ if (extractedUsage) {
5836
+ result.usage = toUsageStats(extractedUsage);
5837
+ } else if (parsed.usage) {
5838
+ result.usage = {
5839
+ total_tokens: parsed.usage.total_tokens ?? parsed.usage.input_tokens + parsed.usage.output_tokens,
5840
+ prompt_tokens: parsed.usage.prompt_tokens ?? parsed.usage.input_tokens,
5841
+ completion_tokens: parsed.usage.completion_tokens ?? parsed.usage.output_tokens
5842
+ };
5843
+ }
5844
+ if (parsed.id) {
5845
+ result.responseId = parsed.id;
5846
+ }
5847
+ if (parsed.model) {
5848
+ result.model = parsed.model;
5849
+ }
5850
+ if (parsed.citations) {
5851
+ result.citations = parsed.citations;
5852
+ }
5853
+ if (parsed.annotations) {
5854
+ result.annotations = parsed.annotations;
5855
+ }
5856
+ if (parsed.choices?.[0]?.finish_reason) {
5857
+ result.finish_reason = parsed.choices[0].finish_reason;
5858
+ }
5859
+ const images = parsed.choices?.[0]?.message?.images || parsed.choices?.[0]?.delta?.images;
5860
+ if (images && Array.isArray(images)) {
5861
+ result.images = images;
6388
5862
  }
5863
+ return result;
5864
+ } catch {
5865
+ return null;
5866
+ }
5867
+ }
5868
+ /**
5869
+ * Handle content delta with thinking support
5870
+ */
5871
+ _handleContent(content, callbacks, modelId) {
5872
+ if (this.isInThinking && !this.isInContent) {
5873
+ this.accumulatedThinking += "</thinking>";
5874
+ callbacks.onThinking(this.accumulatedThinking);
5875
+ this.isInThinking = false;
5876
+ this.isInContent = true;
5877
+ }
5878
+ if (modelId) {
5879
+ this._extractThinkingFromContent(content, callbacks);
5880
+ } else {
5881
+ this.accumulatedContent += content;
6389
5882
  }
6390
- return estimatedCosts;
6391
- }
6392
- /**
6393
- * Get pending API key amount
6394
- */
6395
- _getPendingCashuTokenAmount() {
6396
- const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
6397
- return apiKeyDistribution.reduce((total, item) => total + item.amount, 0);
5883
+ callbacks.onContent(this.accumulatedContent);
6398
5884
  }
6399
5885
  /**
6400
- * Handle errors and notify callbacks
5886
+ * Handle thinking/reasoning content
6401
5887
  */
6402
- _handleError(error, callbacks) {
6403
- this._log("ERROR", "[RoutstrClient] _handleError: Error occurred", error);
6404
- if (error instanceof Error) {
6405
- const isStreamError = error.message.includes("Error in input stream") || error.message.includes("Load failed");
6406
- const modifiedErrorMsg = isStreamError ? "AI stream was cut off, turn on Keep Active or please try again" : error.message;
6407
- this._log(
6408
- "ERROR",
6409
- `[RoutstrClient] _handleError: Error type=${error.constructor.name}, message=${modifiedErrorMsg}, isStreamError=${isStreamError}`
6410
- );
6411
- callbacks.onMessageAppend({
6412
- role: "system",
6413
- content: "Uncaught Error: " + modifiedErrorMsg + (this.alertLevel === "max" ? " | " + error.stack : "")
6414
- });
6415
- } else {
6416
- callbacks.onMessageAppend({
6417
- role: "system",
6418
- content: "Unknown Error: Please tag Routstr on Nostr and/or retry."
6419
- });
5888
+ _handleThinking(reasoning, callbacks) {
5889
+ if (!this.isInThinking) {
5890
+ this.accumulatedThinking += "<thinking> ";
5891
+ this.isInThinking = true;
6420
5892
  }
5893
+ this.accumulatedThinking += reasoning;
5894
+ callbacks.onThinking(this.accumulatedThinking);
6421
5895
  }
6422
5896
  /**
6423
- * Check wallet balance and throw if insufficient
5897
+ * Extract thinking blocks from content (for models with inline thinking)
6424
5898
  */
6425
- async _checkBalance() {
6426
- const balances = await this.walletAdapter.getBalances();
6427
- const totalBalance = Object.values(balances).reduce((sum, v) => sum + v, 0);
6428
- if (totalBalance <= 0) {
6429
- throw new InsufficientBalanceError(1, 0);
5899
+ _extractThinkingFromContent(content, callbacks) {
5900
+ const parts = content.split(/(<thinking>|<\/thinking>)/);
5901
+ for (const part of parts) {
5902
+ if (part === "<thinking>") {
5903
+ this.isInThinking = true;
5904
+ if (!this.accumulatedThinking.includes("<thinking>")) {
5905
+ this.accumulatedThinking += "<thinking> ";
5906
+ }
5907
+ } else if (part === "</thinking>") {
5908
+ this.isInThinking = false;
5909
+ this.accumulatedThinking += "</thinking>";
5910
+ } else if (this.isInThinking) {
5911
+ this.accumulatedThinking += part;
5912
+ } else {
5913
+ this.accumulatedContent += part;
5914
+ }
6430
5915
  }
6431
5916
  }
6432
5917
  /**
6433
- * Spend a token using CashuSpender with standardized error handling
5918
+ * Merge images into accumulated array, avoiding duplicates
6434
5919
  */
6435
- async _spendToken(params) {
6436
- const { mintUrl, amount, baseUrl } = params;
6437
- this._log(
6438
- "DEBUG",
6439
- `[RoutstrClient] _spendToken: mode=${this.mode}, amount=${amount}, baseUrl=${baseUrl}, mintUrl=${mintUrl}`
6440
- );
6441
- if (this.mode === "apikeys") {
6442
- let parentApiKey = this.storageAdapter.getApiKey(baseUrl);
6443
- if (!parentApiKey) {
6444
- this._log(
6445
- "DEBUG",
6446
- `[RoutstrClient] _spendToken: No existing API key for ${baseUrl}, creating new one via Cashu`
6447
- );
6448
- const spendResult2 = await this.cashuSpender.spend({
6449
- mintUrl,
6450
- amount: amount * TOPUP_MARGIN,
6451
- baseUrl: "",
6452
- reuseToken: false
6453
- });
6454
- if (!spendResult2.token) {
6455
- this._log(
6456
- "ERROR",
6457
- `[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error:`,
6458
- spendResult2.error
6459
- );
6460
- throw new Error(
6461
- `[RoutstrClient] _spendToken: Failed to create Cashu token for API key creation, error: ${spendResult2.error}`
6462
- );
6463
- } else {
6464
- this._log(
6465
- "DEBUG",
6466
- `[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult2.token}`
6467
- );
5920
+ _mergeImages(newImages) {
5921
+ for (const img of newImages) {
5922
+ const newUrl = img.image_url?.url;
5923
+ const existingIndex = this.accumulatedImages.findIndex((existing) => {
5924
+ const existingUrl = existing.image_url?.url;
5925
+ if (newUrl && existingUrl) {
5926
+ return existingUrl === newUrl;
6468
5927
  }
6469
- this._log(
6470
- "DEBUG",
6471
- `[RoutstrClient] _spendToken: Created API key for ${baseUrl}, key preview: ${spendResult2.token}, balance: ${spendResult2.balance}`
6472
- );
6473
- try {
6474
- this.storageAdapter.setApiKey(baseUrl, spendResult2.token);
6475
- } catch (error) {
6476
- if (error instanceof Error && error.message.includes("ApiKey already exists")) {
6477
- const receiveResult = await this.cashuSpender.receiveToken(
6478
- spendResult2.token
6479
- );
6480
- if (receiveResult.success) {
6481
- this._log(
6482
- "DEBUG",
6483
- `[RoutstrClient] _handleErrorResponse: Token restored successfully, amount=${receiveResult.amount}`
6484
- );
6485
- } else {
6486
- this._log(
6487
- "DEBUG",
6488
- `[RoutstrClient] _handleErrorResponse: Token restore failed: ${receiveResult.message}`
6489
- );
6490
- }
6491
- this._log(
6492
- "DEBUG",
6493
- `[RoutstrClient] _spendToken: API key already exists for ${baseUrl}, using existing key`
6494
- );
6495
- } else {
6496
- throw error;
6497
- }
5928
+ if (img.index !== void 0 && existing.index !== void 0) {
5929
+ return existing.index === img.index;
6498
5930
  }
6499
- parentApiKey = this.storageAdapter.getApiKey(baseUrl);
5931
+ return false;
5932
+ });
5933
+ if (existingIndex === -1) {
5934
+ this.accumulatedImages.push(img);
6500
5935
  } else {
6501
- this._log(
6502
- "DEBUG",
6503
- `[RoutstrClient] _spendToken: Using existing API key for ${baseUrl}, key preview: ${parentApiKey.key}`
6504
- );
6505
- }
6506
- let tokenBalance = 0;
6507
- let tokenBalanceUnit = "sat";
6508
- let tokenBalanceUnknown = false;
6509
- const apiKeyDistribution = this.storageAdapter.getApiKeyDistribution();
6510
- const distributionForBaseUrl = apiKeyDistribution.find(
6511
- (d) => d.baseUrl === baseUrl
6512
- );
6513
- if (distributionForBaseUrl) {
6514
- tokenBalance = distributionForBaseUrl.amount;
6515
- }
6516
- if (tokenBalance === 0 && parentApiKey) {
6517
- try {
6518
- const balanceInfo = await this.balanceManager.getTokenBalance(
6519
- parentApiKey.key,
6520
- baseUrl
6521
- );
6522
- tokenBalance = balanceInfo.amount;
6523
- tokenBalanceUnit = balanceInfo.unit;
6524
- tokenBalanceUnknown = Boolean(balanceInfo.balanceUnknown);
6525
- } catch (e) {
6526
- this._log("WARN", "Could not get initial API key balance:", e);
6527
- }
5936
+ this.accumulatedImages[existingIndex] = img;
6528
5937
  }
6529
- this._log(
6530
- "DEBUG",
6531
- `[RoutstrClient] _spendToken: Returning token with balance=${tokenBalance} ${tokenBalanceUnit}`
6532
- );
6533
- return {
6534
- token: parentApiKey?.key ?? "",
6535
- tokenBalance,
6536
- tokenBalanceUnit,
6537
- tokenBalanceUnknown
6538
- };
6539
5938
  }
6540
- this._log(
6541
- "DEBUG",
6542
- `[RoutstrClient] _spendToken: Calling CashuSpender.spend for amount=${amount}, mintUrl=${mintUrl}, mode=${this.mode}`
6543
- );
6544
- const spendResult = await this.cashuSpender.spend({
5939
+ }
5940
+ };
5941
+
5942
+ // client/fetchAIResponse.ts
5943
+ async function fetchAIResponse(options, callbacks, deps) {
5944
+ const {
5945
+ messageHistory,
5946
+ selectedModel,
5947
+ baseUrl,
5948
+ mintUrl,
5949
+ maxTokens,
5950
+ headers
5951
+ } = options;
5952
+ try {
5953
+ const apiMessages = await convertMessages(messageHistory);
5954
+ callbacks.onPaymentProcessing?.(true);
5955
+ callbacks.onTokenCreated?.(deps.getPendingCashuTokenAmount?.() ?? 0);
5956
+ const providerInfo = await deps.providerRegistry.getProviderInfo(baseUrl);
5957
+ const providerVersion = providerInfo?.version ?? "";
5958
+ let modelIdForRequest = selectedModel.id;
5959
+ if (/^0\.1\./.test(providerVersion)) {
5960
+ const newModel = await deps.client.getProviderManager().getModelForProvider(baseUrl, selectedModel.id);
5961
+ modelIdForRequest = newModel?.id ?? selectedModel.id;
5962
+ }
5963
+ const body = {
5964
+ model: modelIdForRequest,
5965
+ messages: apiMessages,
5966
+ stream: true
5967
+ };
5968
+ if (maxTokens !== void 0) {
5969
+ body.max_tokens = maxTokens;
5970
+ }
5971
+ if (selectedModel?.name?.startsWith("OpenAI:")) {
5972
+ body.tools = [{ type: "web_search" }];
5973
+ }
5974
+ const response = await deps.client.routeRequest({
5975
+ path: "/v1/chat/completions",
5976
+ method: "POST",
5977
+ body,
5978
+ headers,
5979
+ baseUrl,
6545
5980
  mintUrl,
6546
- amount,
6547
- baseUrl: "",
6548
- reuseToken: false
5981
+ modelId: selectedModel.id
6549
5982
  });
6550
- if (!spendResult.token) {
6551
- this._log(
6552
- "ERROR",
6553
- `[RoutstrClient] _spendToken: CashuSpender.spend failed, error:`,
6554
- spendResult.error
6555
- );
5983
+ if (!response.body) {
5984
+ throw new Error("Response body is not available");
5985
+ }
5986
+ if (response.status !== 200) {
5987
+ throw new Error(`${response.status} ${response.statusText}`);
5988
+ }
5989
+ const streamProcessor = new StreamProcessor();
5990
+ const streamingResult = await streamProcessor.process(
5991
+ response,
5992
+ {
5993
+ onContent: callbacks.onStreamingUpdate,
5994
+ onThinking: callbacks.onThinkingUpdate
5995
+ },
5996
+ selectedModel.id
5997
+ );
5998
+ if (streamingResult.finish_reason === "content_filter") {
5999
+ callbacks.onMessageAppend({
6000
+ role: "assistant",
6001
+ content: "Your request was denied due to content filtering."
6002
+ });
6003
+ } else if (streamingResult.content || streamingResult.images && streamingResult.images.length > 0) {
6004
+ const message = await createAssistantMessage(streamingResult);
6005
+ callbacks.onMessageAppend(message);
6556
6006
  } else {
6557
- this._log(
6558
- "DEBUG",
6559
- `[RoutstrClient] _spendToken: Cashu token created, token preview: ${spendResult.token}, balance: ${spendResult.balance} ${spendResult.unit ?? "sat"}`
6560
- );
6561
- this.storageAdapter.addXcashuToken(baseUrl, spendResult.token);
6007
+ callbacks.onMessageAppend({
6008
+ role: "system",
6009
+ content: "The provider did not respond to this request."
6010
+ });
6562
6011
  }
6563
- return {
6564
- token: spendResult.token,
6565
- tokenBalance: spendResult.balance,
6566
- tokenBalanceUnit: spendResult.unit ?? "sat",
6567
- tokenBalanceUnknown: false
6568
- };
6012
+ callbacks.onStreamingUpdate("");
6013
+ callbacks.onThinkingUpdate("");
6014
+ } catch (error) {
6015
+ handleError(error, callbacks, deps.alertLevel, deps.logger);
6016
+ } finally {
6017
+ callbacks.onPaymentProcessing?.(false);
6569
6018
  }
6570
- /**
6571
- * Build request headers with common defaults and dev mock controls
6572
- */
6573
- _buildBaseHeaders(additionalHeaders = {}, token) {
6574
- const headers = {
6575
- ...additionalHeaders,
6576
- "Content-Type": "application/json"
6019
+ }
6020
+ async function convertMessages(messages) {
6021
+ return Promise.all(
6022
+ messages.filter((m) => m.role !== "system").map(async (m) => ({
6023
+ role: m.role,
6024
+ content: typeof m.content === "string" ? m.content : m.content
6025
+ }))
6026
+ );
6027
+ }
6028
+ async function createAssistantMessage(result) {
6029
+ if (result.images && result.images.length > 0) {
6030
+ const content = [];
6031
+ if (result.content) {
6032
+ content.push({
6033
+ type: "text",
6034
+ text: result.content,
6035
+ thinking: result.thinking,
6036
+ citations: result.citations,
6037
+ annotations: result.annotations
6038
+ });
6039
+ }
6040
+ for (const img of result.images) {
6041
+ content.push({
6042
+ type: "image_url",
6043
+ image_url: {
6044
+ url: img.image_url.url
6045
+ }
6046
+ });
6047
+ }
6048
+ return {
6049
+ role: "assistant",
6050
+ content
6577
6051
  };
6578
- return headers;
6579
6052
  }
6580
- /**
6581
- * Attach auth headers using the active client mode
6582
- */
6583
- _withAuthHeader(headers, token) {
6584
- const nextHeaders = { ...headers };
6585
- if (this.mode === "xcashu") {
6586
- nextHeaders["X-Cashu"] = token;
6587
- } else {
6588
- nextHeaders["Authorization"] = `Bearer ${token}`;
6589
- }
6590
- return nextHeaders;
6053
+ return {
6054
+ role: "assistant",
6055
+ content: result.content || ""
6056
+ };
6057
+ }
6058
+ function handleError(error, callbacks, alertLevel, logger) {
6059
+ logger.error("[fetchAIResponse] Error occurred", error);
6060
+ if (error instanceof Error) {
6061
+ const isStreamError = error.message.includes("Error in input stream") || error.message.includes("Load failed");
6062
+ const modifiedErrorMsg = isStreamError ? "AI stream was cut off, turn on Keep Active or please try again" : error.message;
6063
+ logger.error(
6064
+ `[fetchAIResponse] Error type=${error.constructor.name}, message=${modifiedErrorMsg}, isStreamError=${isStreamError}`
6065
+ );
6066
+ callbacks.onMessageAppend({
6067
+ role: "system",
6068
+ content: "Uncaught Error: " + modifiedErrorMsg + (alertLevel === "max" ? " | " + error.stack : "")
6069
+ });
6070
+ } else {
6071
+ callbacks.onMessageAppend({
6072
+ role: "system",
6073
+ content: "Unknown Error: Please tag Routstr on Nostr and/or retry."
6074
+ });
6591
6075
  }
6592
- };
6076
+ }
6593
6077
 
6594
6078
  // routeRequests.ts
6595
6079
  async function resolveRouteRequestContext(options) {
@@ -6741,6 +6225,6 @@ function extractStream(requestBody) {
6741
6225
  return typeof stream === "boolean" ? stream : void 0;
6742
6226
  }
6743
6227
 
6744
- export { BalanceManager, CashuSpender, FailoverError, InsufficientBalanceError, MintDiscovery, MintDiscoveryError, MintUnreachableError, ModelManager, ModelNotFoundError, NoProvidersAvailableError, ProviderBootstrapError, ProviderError, ProviderManager, RoutstrClient, SDK_STORAGE_KEYS, StreamProcessor, StreamingError, TokenOperationError, consoleLogger, createBunSqliteDriver, createBunSqliteUsageTrackingDriver, createDiscoveryAdapterFromStore, createIndexedDBDriver, createIndexedDBUsageTrackingDriver, createMemoryDriver, createMemoryUsageTrackingDriver, createProviderRegistryFromDiscoveryAdapter, createProviderRegistryFromStore, createSSEParserTransform, createSdkStore, createShardedDiscoveryAdapter, createSqliteDriver, createSqliteUsageTrackingDriver, createStorageAdapterFromStore, filterBaseUrlsForTor, getDefaultDiscoveryAdapter, getDefaultProviderRegistry, getDefaultSdkDriver, getDefaultSdkStore, getDefaultStorageAdapter, getDefaultUsageTrackingDriver, getProviderEndpoints, inspectSSEWebStream, isOnionUrl, isTorContext, localStorageDriver, noopLogger, normalizeProviderUrl, routeRequests, setDefaultUsageTrackingDriver };
6228
+ export { BalanceManager, CashuSpender, FailoverError, InsufficientBalanceError, MintDiscovery, MintDiscoveryError, MintUnreachableError, ModelManager, ModelNotFoundError, NoProvidersAvailableError, ProviderBootstrapError, ProviderError, ProviderManager, RoutstrClient, SDK_STORAGE_KEYS, StreamProcessor, StreamingError, TokenOperationError, consoleLogger, createDiscoveryAdapterFromStore, createIndexedDBDriver, createIndexedDBUsageTrackingDriver, createMemoryDriver, createMemoryUsageTrackingDriver, createProviderRegistryFromDiscoveryAdapter, createProviderRegistryFromStore, createSSEParserTransform, createSdkStore, createShardedDiscoveryAdapter, createStorageAdapterFromStore, fetchAIResponse, filterBaseUrlsForTor, getDefaultDiscoveryAdapter, getDefaultProviderRegistry, getDefaultSdkDriver, getDefaultSdkStore, getDefaultStorageAdapter, getDefaultUsageTrackingDriver, getProviderEndpoints, inspectSSEWebStream, isOnionUrl, isTorContext, localStorageDriver, noopLogger, normalizeProviderUrl, routeRequests, setDefaultUsageTrackingDriver };
6745
6229
  //# sourceMappingURL=index.mjs.map
6746
6230
  //# sourceMappingURL=index.mjs.map