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