@routstr/sdk 0.1.0

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.
package/dist/index.js ADDED
@@ -0,0 +1,3572 @@
1
+ 'use strict';
2
+
3
+ var vanilla = require('zustand/vanilla');
4
+ var cashuTs = require('@cashu/cashu-ts');
5
+
6
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
7
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
8
+ }) : x)(function(x) {
9
+ if (typeof require !== "undefined") return require.apply(this, arguments);
10
+ throw Error('Dynamic require of "' + x + '" is not supported');
11
+ });
12
+
13
+ // core/errors.ts
14
+ var InsufficientBalanceError = class extends Error {
15
+ constructor(required, available, maxMintBalance = 0, maxMintUrl = "") {
16
+ super(
17
+ `Insufficient balance: need ${required} sats, have ${available} sats available. ` + (maxMintBalance > 0 ? `Largest mint balance: ${maxMintBalance} sats from ${maxMintUrl}` : "")
18
+ );
19
+ this.required = required;
20
+ this.available = available;
21
+ this.maxMintBalance = maxMintBalance;
22
+ this.maxMintUrl = maxMintUrl;
23
+ this.name = "InsufficientBalanceError";
24
+ }
25
+ };
26
+ var ProviderError = class extends Error {
27
+ constructor(baseUrl, statusCode, message, requestId) {
28
+ super(
29
+ `Provider ${baseUrl} returned ${statusCode}: ${message}` + (requestId ? ` (Request ID: ${requestId})` : "")
30
+ );
31
+ this.baseUrl = baseUrl;
32
+ this.statusCode = statusCode;
33
+ this.requestId = requestId;
34
+ this.name = "ProviderError";
35
+ }
36
+ };
37
+ var MintUnreachableError = class extends Error {
38
+ constructor(mintUrl) {
39
+ super(
40
+ `Your mint ${mintUrl} is unreachable or is blocking your IP. Please try again later or switch mints.`
41
+ );
42
+ this.mintUrl = mintUrl;
43
+ this.name = "MintUnreachableError";
44
+ }
45
+ };
46
+ var TokenOperationError = class extends Error {
47
+ constructor(message, operation, mintUrl) {
48
+ super(message);
49
+ this.operation = operation;
50
+ this.mintUrl = mintUrl;
51
+ this.name = "TokenOperationError";
52
+ }
53
+ };
54
+ var FailoverError = class extends Error {
55
+ constructor(originalProvider, failedProviders, message) {
56
+ super(
57
+ message || `All providers failed. Original: ${originalProvider}, Failed: ${failedProviders.join(", ")}`
58
+ );
59
+ this.originalProvider = originalProvider;
60
+ this.failedProviders = failedProviders;
61
+ this.name = "FailoverError";
62
+ }
63
+ };
64
+ var StreamingError = class extends Error {
65
+ constructor(message, finishReason, accumulatedContent) {
66
+ super(message);
67
+ this.finishReason = finishReason;
68
+ this.accumulatedContent = accumulatedContent;
69
+ this.name = "StreamingError";
70
+ }
71
+ };
72
+ var ModelNotFoundError = class extends Error {
73
+ constructor(modelId, baseUrl) {
74
+ super(`Model '${modelId}' not found on provider ${baseUrl}`);
75
+ this.modelId = modelId;
76
+ this.baseUrl = baseUrl;
77
+ this.name = "ModelNotFoundError";
78
+ }
79
+ };
80
+ var ProviderBootstrapError = class extends Error {
81
+ constructor(failedProviders, message) {
82
+ super(
83
+ message || `Failed to bootstrap providers. Tried: ${failedProviders.join(", ")}`
84
+ );
85
+ this.failedProviders = failedProviders;
86
+ this.name = "ProviderBootstrapError";
87
+ }
88
+ };
89
+ var NoProvidersAvailableError = class extends Error {
90
+ constructor() {
91
+ super("No providers are available for model discovery");
92
+ this.name = "NoProvidersAvailableError";
93
+ }
94
+ };
95
+ var MintDiscoveryError = class extends Error {
96
+ constructor(baseUrl, message) {
97
+ super(message || `Failed to discover mints from provider ${baseUrl}`);
98
+ this.baseUrl = baseUrl;
99
+ this.name = "MintDiscoveryError";
100
+ }
101
+ };
102
+
103
+ // discovery/ModelManager.ts
104
+ var ModelManager = class _ModelManager {
105
+ constructor(adapter, config = {}) {
106
+ this.adapter = adapter;
107
+ this.providerDirectoryUrl = config.providerDirectoryUrl || "https://api.routstr.com/v1/providers/";
108
+ this.cacheTTL = config.cacheTTL || 21 * 60 * 1e3;
109
+ this.includeProviderUrls = config.includeProviderUrls || [];
110
+ this.excludeProviderUrls = config.excludeProviderUrls || [];
111
+ }
112
+ cacheTTL;
113
+ providerDirectoryUrl;
114
+ includeProviderUrls;
115
+ excludeProviderUrls;
116
+ /**
117
+ * Get the list of bootstrapped provider base URLs
118
+ * @returns Array of provider base URLs
119
+ */
120
+ getBaseUrls() {
121
+ return this.adapter.getBaseUrlsList();
122
+ }
123
+ static async init(adapter, config = {}, options = {}) {
124
+ const manager = new _ModelManager(adapter, config);
125
+ const torMode = options.torMode ?? false;
126
+ const forceRefresh = options.forceRefresh ?? false;
127
+ const providers = await manager.bootstrapProviders(torMode);
128
+ await manager.fetchModels(providers, forceRefresh);
129
+ return manager;
130
+ }
131
+ /**
132
+ * Bootstrap provider list from the provider directory
133
+ * Fetches available providers and caches their base URLs
134
+ * @param torMode Whether running in Tor context
135
+ * @returns Array of provider base URLs
136
+ * @throws ProviderBootstrapError if all providers fail to fetch
137
+ */
138
+ async bootstrapProviders(torMode = false) {
139
+ try {
140
+ const cachedUrls = this.adapter.getBaseUrlsList();
141
+ if (cachedUrls.length > 0) {
142
+ const lastUpdate = this.adapter.getBaseUrlsLastUpdate();
143
+ const cacheValid = lastUpdate && Date.now() - lastUpdate <= this.cacheTTL;
144
+ if (cacheValid) {
145
+ return this.filterBaseUrlsForTor(cachedUrls, torMode);
146
+ }
147
+ }
148
+ const res = await fetch(this.providerDirectoryUrl);
149
+ if (!res.ok) {
150
+ throw new Error(`Failed to fetch providers: ${res.status}`);
151
+ }
152
+ const data = await res.json();
153
+ const providers = Array.isArray(data?.providers) ? data.providers : [];
154
+ const bases = /* @__PURE__ */ new Set();
155
+ for (const p of providers) {
156
+ const endpoints = this.getProviderEndpoints(p, torMode);
157
+ for (const endpoint of endpoints) {
158
+ bases.add(endpoint);
159
+ }
160
+ }
161
+ for (const url of this.includeProviderUrls) {
162
+ const normalized = this.normalizeUrl(url);
163
+ if (!torMode || normalized.includes(".onion")) {
164
+ bases.add(normalized);
165
+ }
166
+ }
167
+ const excluded = new Set(
168
+ this.excludeProviderUrls.map((url) => this.normalizeUrl(url))
169
+ );
170
+ const list = Array.from(bases).filter((base) => {
171
+ if (excluded.has(base)) return false;
172
+ return true;
173
+ });
174
+ if (list.length > 0) {
175
+ this.adapter.setBaseUrlsList(list);
176
+ this.adapter.setBaseUrlsLastUpdate(Date.now());
177
+ }
178
+ return list;
179
+ } catch (e) {
180
+ console.error("Failed to bootstrap providers", e);
181
+ throw new ProviderBootstrapError([], `Provider bootstrap failed: ${e}`);
182
+ }
183
+ }
184
+ /**
185
+ * Fetch models from all providers and select best-priced options
186
+ * Uses cache if available and not expired
187
+ * @param baseUrls List of provider base URLs to fetch from
188
+ * @param forceRefresh Ignore cache and fetch fresh data
189
+ * @returns Array of unique models with best prices selected
190
+ */
191
+ async fetchModels(baseUrls, forceRefresh = false) {
192
+ if (baseUrls.length === 0) {
193
+ throw new NoProvidersAvailableError();
194
+ }
195
+ const bestById = /* @__PURE__ */ new Map();
196
+ const modelsFromAllProviders = {};
197
+ const disabledProviders = this.adapter.getDisabledProviders();
198
+ const estimateMinCost = (m) => {
199
+ return m?.sats_pricing?.completion ?? 0;
200
+ };
201
+ const fetchPromises = baseUrls.map(async (url) => {
202
+ const base = url.endsWith("/") ? url : `${url}/`;
203
+ try {
204
+ let list;
205
+ if (!forceRefresh) {
206
+ const lastUpdate = this.adapter.getProviderLastUpdate(base);
207
+ const cacheValid = lastUpdate && Date.now() - lastUpdate <= this.cacheTTL;
208
+ if (cacheValid) {
209
+ const cachedModels = this.adapter.getCachedModels();
210
+ const cachedList = cachedModels[base] || [];
211
+ list = cachedList;
212
+ } else {
213
+ list = await this.fetchModelsFromProvider(base);
214
+ }
215
+ } else {
216
+ list = await this.fetchModelsFromProvider(base);
217
+ }
218
+ modelsFromAllProviders[base] = list;
219
+ this.adapter.setProviderLastUpdate(base, Date.now());
220
+ if (!disabledProviders.includes(base)) {
221
+ for (const m of list) {
222
+ const existing = bestById.get(m.id);
223
+ if (!m.sats_pricing) continue;
224
+ if (!existing) {
225
+ bestById.set(m.id, { model: m, base });
226
+ continue;
227
+ }
228
+ const currentCost = estimateMinCost(m);
229
+ const existingCost = estimateMinCost(existing.model);
230
+ if (currentCost < existingCost && m.sats_pricing) {
231
+ bestById.set(m.id, { model: m, base });
232
+ }
233
+ }
234
+ }
235
+ return { success: true, base, list };
236
+ } catch (error) {
237
+ if (this.isProviderDownError(error)) {
238
+ console.warn(`Provider ${base} is down right now.`);
239
+ } else {
240
+ console.warn(`Failed to fetch models from ${base}:`, error);
241
+ }
242
+ this.adapter.setProviderLastUpdate(base, Date.now());
243
+ return { success: false, base };
244
+ }
245
+ });
246
+ await Promise.allSettled(fetchPromises);
247
+ const existingCache = this.adapter.getCachedModels();
248
+ this.adapter.setCachedModels({
249
+ ...existingCache,
250
+ ...modelsFromAllProviders
251
+ });
252
+ return Array.from(bestById.values()).map((v) => v.model);
253
+ }
254
+ /**
255
+ * Fetch models from a single provider
256
+ * @param baseUrl Provider base URL
257
+ * @returns Array of models from provider
258
+ */
259
+ async fetchModelsFromProvider(baseUrl) {
260
+ const res = await fetch(`${baseUrl}v1/models`);
261
+ if (!res.ok) {
262
+ throw new Error(`Failed to fetch models: ${res.status}`);
263
+ }
264
+ const json = await res.json();
265
+ const list = Array.isArray(json?.data) ? json.data.map((m) => ({
266
+ ...m,
267
+ id: m.id.split("/").pop() || m.id
268
+ })) : [];
269
+ return list;
270
+ }
271
+ isProviderDownError(error) {
272
+ if (!(error instanceof Error)) return false;
273
+ const msg = error.message.toLowerCase();
274
+ if (msg.includes("fetch failed")) return true;
275
+ if (msg.includes("502")) return true;
276
+ if (msg.includes("503")) return true;
277
+ if (msg.includes("504")) return true;
278
+ const cause = error.cause;
279
+ return cause?.code === "ENOTFOUND";
280
+ }
281
+ /**
282
+ * Get all cached models from all providers
283
+ * @returns Record mapping baseUrl -> models
284
+ */
285
+ getAllCachedModels() {
286
+ return this.adapter.getCachedModels();
287
+ }
288
+ /**
289
+ * Clear cache for a specific provider
290
+ * @param baseUrl Provider base URL
291
+ */
292
+ clearProviderCache(baseUrl) {
293
+ const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
294
+ const cached = this.adapter.getCachedModels();
295
+ delete cached[base];
296
+ this.adapter.setCachedModels(cached);
297
+ this.adapter.setProviderLastUpdate(base, 0);
298
+ }
299
+ /**
300
+ * Clear all model caches
301
+ */
302
+ clearAllCache() {
303
+ this.adapter.setCachedModels({});
304
+ }
305
+ /**
306
+ * Filter base URLs based on Tor context
307
+ * @param baseUrls Provider URLs to filter
308
+ * @param torMode Whether in Tor context
309
+ * @returns Filtered URLs appropriate for Tor mode
310
+ */
311
+ filterBaseUrlsForTor(baseUrls, torMode) {
312
+ if (!torMode) {
313
+ return baseUrls.filter((url) => !url.includes(".onion"));
314
+ }
315
+ return baseUrls.filter((url) => url.includes(".onion"));
316
+ }
317
+ /**
318
+ * Get provider endpoints from provider info
319
+ * @param provider Provider object from directory
320
+ * @param torMode Whether in Tor context
321
+ * @returns Array of endpoint URLs
322
+ */
323
+ getProviderEndpoints(provider, torMode) {
324
+ const endpoints = [];
325
+ if (torMode && provider.onion_url) {
326
+ endpoints.push(this.normalizeUrl(provider.onion_url));
327
+ } else if (provider.endpoint_url) {
328
+ endpoints.push(this.normalizeUrl(provider.endpoint_url));
329
+ }
330
+ return endpoints;
331
+ }
332
+ /**
333
+ * Normalize provider URL with trailing slash
334
+ * @param url URL to normalize
335
+ * @returns Normalized URL
336
+ */
337
+ normalizeUrl(url) {
338
+ if (!url.startsWith("http")) {
339
+ url = `https://${url}`;
340
+ }
341
+ return url.endsWith("/") ? url : `${url}/`;
342
+ }
343
+ };
344
+
345
+ // discovery/MintDiscovery.ts
346
+ var MintDiscovery = class {
347
+ constructor(adapter, config = {}) {
348
+ this.adapter = adapter;
349
+ this.cacheTTL = config.cacheTTL || 21 * 60 * 1e3;
350
+ }
351
+ cacheTTL;
352
+ /**
353
+ * Fetch mints from all providers via their /v1/info endpoints
354
+ * Caches mints and full provider info for later access
355
+ * @param baseUrls List of provider base URLs to fetch from
356
+ * @returns Object with mints and provider info from all providers
357
+ */
358
+ async discoverMints(baseUrls, options = {}) {
359
+ if (baseUrls.length === 0) {
360
+ return { mintsFromProviders: {}, infoFromProviders: {} };
361
+ }
362
+ const mintsFromAllProviders = {};
363
+ const infoFromAllProviders = {};
364
+ const forceRefresh = options.forceRefresh ?? false;
365
+ const fetchPromises = baseUrls.map(async (url) => {
366
+ const base = url.endsWith("/") ? url : `${url}/`;
367
+ try {
368
+ if (!forceRefresh) {
369
+ const lastUpdate = this.adapter.getProviderLastUpdate(base);
370
+ const cacheValid = lastUpdate && Date.now() - lastUpdate <= this.cacheTTL;
371
+ if (cacheValid) {
372
+ const cachedMints = this.adapter.getCachedMints()[base] || [];
373
+ const cachedInfo = this.adapter.getCachedProviderInfo()[base];
374
+ mintsFromAllProviders[base] = cachedMints;
375
+ if (cachedInfo) {
376
+ infoFromAllProviders[base] = cachedInfo;
377
+ }
378
+ return {
379
+ success: true,
380
+ base,
381
+ mints: cachedMints,
382
+ info: cachedInfo
383
+ };
384
+ }
385
+ }
386
+ const res = await fetch(`${base}v1/info`);
387
+ if (!res.ok) {
388
+ throw new Error(`Failed to fetch info: ${res.status}`);
389
+ }
390
+ const json = await res.json();
391
+ const mints = Array.isArray(json?.mints) ? json.mints : [];
392
+ const normalizedMints = mints.map(
393
+ (mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint
394
+ );
395
+ mintsFromAllProviders[base] = normalizedMints;
396
+ infoFromAllProviders[base] = json;
397
+ this.adapter.setProviderLastUpdate(base, Date.now());
398
+ return { success: true, base, mints: normalizedMints, info: json };
399
+ } catch (error) {
400
+ this.adapter.setProviderLastUpdate(base, Date.now());
401
+ if (this.isProviderDownError(error)) {
402
+ console.warn(`Provider ${base} is down right now.`);
403
+ } else {
404
+ console.warn(`Failed to fetch mints from ${base}:`, error);
405
+ }
406
+ return { success: false, base, mints: [], info: null };
407
+ }
408
+ });
409
+ const results = await Promise.allSettled(fetchPromises);
410
+ for (const result of results) {
411
+ if (result.status === "fulfilled") {
412
+ const { base, mints, info } = result.value;
413
+ mintsFromAllProviders[base] = mints;
414
+ if (info) {
415
+ infoFromAllProviders[base] = info;
416
+ }
417
+ } else {
418
+ console.error("Mint discovery error:", result.reason);
419
+ }
420
+ }
421
+ try {
422
+ this.adapter.setCachedMints(mintsFromAllProviders);
423
+ this.adapter.setCachedProviderInfo(infoFromAllProviders);
424
+ } catch (error) {
425
+ console.error("Error caching mint discovery results:", error);
426
+ }
427
+ return {
428
+ mintsFromProviders: mintsFromAllProviders,
429
+ infoFromProviders: infoFromAllProviders
430
+ };
431
+ }
432
+ /**
433
+ * Get cached mints from all providers
434
+ * @returns Record mapping baseUrl -> mint URLs
435
+ */
436
+ getCachedMints() {
437
+ return this.adapter.getCachedMints();
438
+ }
439
+ /**
440
+ * Get cached provider info from all providers
441
+ * @returns Record mapping baseUrl -> provider info
442
+ */
443
+ getCachedProviderInfo() {
444
+ return this.adapter.getCachedProviderInfo();
445
+ }
446
+ /**
447
+ * Get mints for a specific provider
448
+ * @param baseUrl Provider base URL
449
+ * @returns Array of mint URLs for the provider
450
+ */
451
+ getProviderMints(baseUrl) {
452
+ const normalized = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
453
+ const allMints = this.getCachedMints();
454
+ return allMints[normalized] || [];
455
+ }
456
+ /**
457
+ * Get info for a specific provider
458
+ * @param baseUrl Provider base URL
459
+ * @returns Provider info object or null if not found
460
+ */
461
+ getProviderInfo(baseUrl) {
462
+ const normalized = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
463
+ const allInfo = this.getCachedProviderInfo();
464
+ return allInfo[normalized] || null;
465
+ }
466
+ /**
467
+ * Clear mint cache for a specific provider
468
+ * @param baseUrl Provider base URL
469
+ */
470
+ clearProviderMintCache(baseUrl) {
471
+ const normalized = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
472
+ const mints = this.getCachedMints();
473
+ delete mints[normalized];
474
+ this.adapter.setCachedMints(mints);
475
+ const info = this.getCachedProviderInfo();
476
+ delete info[normalized];
477
+ this.adapter.setCachedProviderInfo(info);
478
+ }
479
+ /**
480
+ * Clear all mint caches
481
+ */
482
+ clearAllCache() {
483
+ this.adapter.setCachedMints({});
484
+ this.adapter.setCachedProviderInfo({});
485
+ }
486
+ isProviderDownError(error) {
487
+ if (!(error instanceof Error)) return false;
488
+ const msg = error.message.toLowerCase();
489
+ if (msg.includes("fetch failed")) return true;
490
+ if (msg.includes("502")) return true;
491
+ if (msg.includes("503")) return true;
492
+ if (msg.includes("504")) return true;
493
+ const cause = error.cause;
494
+ if (cause?.code === "ENOTFOUND") return true;
495
+ return false;
496
+ }
497
+ };
498
+
499
+ // wallet/AuditLogger.ts
500
+ var AuditLogger = class _AuditLogger {
501
+ static instance = null;
502
+ static getInstance() {
503
+ if (!_AuditLogger.instance) {
504
+ _AuditLogger.instance = new _AuditLogger();
505
+ }
506
+ return _AuditLogger.instance;
507
+ }
508
+ async log(entry) {
509
+ const fullEntry = {
510
+ ...entry,
511
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
512
+ };
513
+ const logLine = JSON.stringify(fullEntry) + "\n";
514
+ if (typeof window === "undefined") {
515
+ try {
516
+ const fs = await import('fs');
517
+ const path = await import('path');
518
+ const logPath = path.join(process.cwd(), "audit.log");
519
+ fs.appendFileSync(logPath, logLine);
520
+ } catch (error) {
521
+ console.error("[AuditLogger] Failed to write to file:", error);
522
+ }
523
+ } else {
524
+ console.log("[AUDIT]", logLine.trim());
525
+ }
526
+ }
527
+ async logBalanceSnapshot(action, amounts, options) {
528
+ await this.log({
529
+ action,
530
+ totalBalance: amounts.totalBalance,
531
+ providerBalances: amounts.providerBalances,
532
+ mintBalances: amounts.mintBalances,
533
+ amount: options?.amount,
534
+ mintUrl: options?.mintUrl,
535
+ baseUrl: options?.baseUrl,
536
+ status: options?.status ?? "success",
537
+ details: options?.details
538
+ });
539
+ }
540
+ };
541
+ var auditLogger = AuditLogger.getInstance();
542
+
543
+ // wallet/CashuSpender.ts
544
+ var CashuSpender = class {
545
+ constructor(walletAdapter, storageAdapter, providerRegistry, balanceManager) {
546
+ this.walletAdapter = walletAdapter;
547
+ this.storageAdapter = storageAdapter;
548
+ this.providerRegistry = providerRegistry;
549
+ this.balanceManager = balanceManager;
550
+ }
551
+ _isBusy = false;
552
+ async _getBalanceState() {
553
+ const mintBalances = await this.walletAdapter.getBalances();
554
+ const units = this.walletAdapter.getMintUnits();
555
+ let totalMintBalance = 0;
556
+ const normalizedMintBalances = {};
557
+ for (const url in mintBalances) {
558
+ const balance = mintBalances[url];
559
+ const unit = units[url];
560
+ const balanceInSats = unit === "msat" ? balance / 1e3 : balance;
561
+ normalizedMintBalances[url] = balanceInSats;
562
+ totalMintBalance += balanceInSats;
563
+ }
564
+ const pendingDistribution = this.storageAdapter.getPendingTokenDistribution();
565
+ const providerBalances = {};
566
+ let totalProviderBalance = 0;
567
+ for (const pending of pendingDistribution) {
568
+ providerBalances[pending.baseUrl] = pending.amount;
569
+ totalProviderBalance += pending.amount;
570
+ }
571
+ const apiKeys = this.storageAdapter.getAllApiKeys();
572
+ for (const apiKey of apiKeys) {
573
+ if (!providerBalances[apiKey.baseUrl]) {
574
+ providerBalances[apiKey.baseUrl] = apiKey.balance;
575
+ totalProviderBalance += apiKey.balance;
576
+ }
577
+ }
578
+ return {
579
+ totalBalance: totalMintBalance + totalProviderBalance,
580
+ providerBalances,
581
+ mintBalances: normalizedMintBalances
582
+ };
583
+ }
584
+ async _logTransaction(action, options) {
585
+ const balanceState = await this._getBalanceState();
586
+ await auditLogger.logBalanceSnapshot(action, balanceState, options);
587
+ }
588
+ /**
589
+ * Check if the spender is currently in a critical operation
590
+ */
591
+ get isBusy() {
592
+ return this._isBusy;
593
+ }
594
+ /**
595
+ * Spend Cashu tokens with automatic mint selection and retry logic
596
+ */
597
+ async spend(options) {
598
+ const {
599
+ mintUrl,
600
+ amount,
601
+ baseUrl,
602
+ reuseToken = false,
603
+ p2pkPubkey,
604
+ excludeMints = [],
605
+ retryCount = 0
606
+ } = options;
607
+ this._isBusy = true;
608
+ try {
609
+ return await this._spendInternal({
610
+ mintUrl,
611
+ amount,
612
+ baseUrl,
613
+ reuseToken,
614
+ p2pkPubkey,
615
+ excludeMints,
616
+ retryCount
617
+ });
618
+ } finally {
619
+ this._isBusy = false;
620
+ }
621
+ }
622
+ /**
623
+ * Internal spending logic
624
+ */
625
+ async _spendInternal(options) {
626
+ let {
627
+ mintUrl,
628
+ amount,
629
+ baseUrl,
630
+ reuseToken,
631
+ p2pkPubkey,
632
+ excludeMints,
633
+ retryCount
634
+ } = options;
635
+ let adjustedAmount = Math.ceil(amount);
636
+ if (!adjustedAmount || isNaN(adjustedAmount)) {
637
+ return {
638
+ token: null,
639
+ status: "failed",
640
+ balance: 0,
641
+ error: "Please enter a valid amount"
642
+ };
643
+ }
644
+ if (reuseToken && baseUrl) {
645
+ const existingResult = await this._tryReuseToken(
646
+ baseUrl,
647
+ adjustedAmount,
648
+ mintUrl
649
+ );
650
+ if (existingResult) {
651
+ return existingResult;
652
+ }
653
+ }
654
+ const balances = await this.walletAdapter.getBalances();
655
+ const units = this.walletAdapter.getMintUnits();
656
+ let totalBalance = 0;
657
+ for (const url in balances) {
658
+ const balance = balances[url];
659
+ const unit = units[url];
660
+ const balanceInSats = unit === "msat" ? balance / 1e3 : balance;
661
+ totalBalance += balanceInSats;
662
+ }
663
+ const pendingDistribution = this.storageAdapter.getPendingTokenDistribution();
664
+ const totalPending = pendingDistribution.reduce(
665
+ (sum, item) => sum + item.amount,
666
+ 0
667
+ );
668
+ if (totalBalance < adjustedAmount && totalPending + totalBalance > adjustedAmount && (retryCount ?? 0) < 1) {
669
+ return await this._refundAndRetry(options);
670
+ }
671
+ const totalAvailableBalance = totalBalance + totalPending;
672
+ if (totalAvailableBalance < adjustedAmount) {
673
+ return this._createInsufficientBalanceError(
674
+ adjustedAmount,
675
+ balances,
676
+ units,
677
+ totalAvailableBalance
678
+ );
679
+ }
680
+ let { selectedMintUrl, selectedMintBalance } = this._selectMintWithBalance(
681
+ balances,
682
+ units,
683
+ adjustedAmount,
684
+ excludeMints
685
+ );
686
+ if (selectedMintUrl && baseUrl && this.providerRegistry) {
687
+ const providerMints = this.providerRegistry.getProviderMints(baseUrl);
688
+ if (providerMints.length > 0 && !providerMints.includes(selectedMintUrl)) {
689
+ const alternateResult = await this._findAlternateMint(
690
+ options,
691
+ balances,
692
+ units,
693
+ providerMints
694
+ );
695
+ if (alternateResult) {
696
+ return alternateResult;
697
+ }
698
+ adjustedAmount += 2;
699
+ }
700
+ }
701
+ const activeMintBalance = balances[mintUrl] || 0;
702
+ const activeMintUnit = units[mintUrl];
703
+ const activeMintBalanceInSats = activeMintUnit === "msat" ? activeMintBalance / 1e3 : activeMintBalance;
704
+ let token = null;
705
+ if (activeMintBalanceInSats >= adjustedAmount && (baseUrl === "" || !this.providerRegistry)) {
706
+ try {
707
+ token = await this.walletAdapter.sendToken(
708
+ mintUrl,
709
+ adjustedAmount,
710
+ p2pkPubkey
711
+ );
712
+ } catch (error) {
713
+ return this._handleSendError(error, options, balances, units);
714
+ }
715
+ } else if (selectedMintUrl && selectedMintBalance >= adjustedAmount) {
716
+ try {
717
+ token = await this.walletAdapter.sendToken(
718
+ selectedMintUrl,
719
+ adjustedAmount,
720
+ p2pkPubkey
721
+ );
722
+ } catch (error) {
723
+ return this._handleSendError(error, options, balances, units);
724
+ }
725
+ } else {
726
+ return this._createInsufficientBalanceError(
727
+ adjustedAmount,
728
+ balances,
729
+ units
730
+ );
731
+ }
732
+ if (token && baseUrl) {
733
+ this.storageAdapter.setToken(baseUrl, token);
734
+ }
735
+ this._logTransaction("spend", {
736
+ amount: adjustedAmount,
737
+ mintUrl: selectedMintUrl || mintUrl,
738
+ baseUrl,
739
+ status: "success"
740
+ });
741
+ return {
742
+ token,
743
+ status: "success",
744
+ balance: adjustedAmount,
745
+ unit: activeMintUnit
746
+ };
747
+ }
748
+ /**
749
+ * Try to reuse an existing token
750
+ */
751
+ async _tryReuseToken(baseUrl, amount, mintUrl) {
752
+ const storedToken = this.storageAdapter.getToken(baseUrl);
753
+ if (!storedToken) return null;
754
+ const pendingDistribution = this.storageAdapter.getPendingTokenDistribution();
755
+ const balanceForBaseUrl = pendingDistribution.find((b) => b.baseUrl === baseUrl)?.amount || 0;
756
+ console.log("RESUINGDSR GSODGNSD", balanceForBaseUrl, amount);
757
+ if (balanceForBaseUrl > amount) {
758
+ const units = this.walletAdapter.getMintUnits();
759
+ const unit = units[mintUrl] || "sat";
760
+ return {
761
+ token: storedToken,
762
+ status: "success",
763
+ balance: balanceForBaseUrl,
764
+ unit
765
+ };
766
+ }
767
+ if (this.balanceManager) {
768
+ const topUpAmount = Math.ceil(amount * 1.2 - balanceForBaseUrl);
769
+ const topUpResult = await this.balanceManager.topUp({
770
+ mintUrl,
771
+ baseUrl,
772
+ amount: topUpAmount
773
+ });
774
+ console.log("TOPUP ", topUpResult);
775
+ if (topUpResult.success && topUpResult.toppedUpAmount) {
776
+ const newBalance = balanceForBaseUrl + topUpResult.toppedUpAmount;
777
+ const units = this.walletAdapter.getMintUnits();
778
+ const unit = units[mintUrl] || "sat";
779
+ this._logTransaction("topup", {
780
+ amount: topUpResult.toppedUpAmount,
781
+ mintUrl,
782
+ baseUrl,
783
+ status: "success"
784
+ });
785
+ return {
786
+ token: storedToken,
787
+ status: "success",
788
+ balance: newBalance,
789
+ unit
790
+ };
791
+ }
792
+ const providerBalance = await this._getProviderTokenBalance(
793
+ baseUrl,
794
+ storedToken
795
+ );
796
+ console.log(providerBalance);
797
+ if (providerBalance <= 0) {
798
+ this.storageAdapter.removeToken(baseUrl);
799
+ }
800
+ }
801
+ return null;
802
+ }
803
+ /**
804
+ * Refund pending tokens and retry
805
+ */
806
+ async _refundAndRetry(options) {
807
+ const { mintUrl, baseUrl, retryCount } = options;
808
+ const pendingDistribution = this.storageAdapter.getPendingTokenDistribution();
809
+ const refundResults = await Promise.allSettled(
810
+ pendingDistribution.map(async (pending) => {
811
+ const token = this.storageAdapter.getToken(pending.baseUrl);
812
+ if (!token || !this.balanceManager || pending.baseUrl === baseUrl) {
813
+ return { baseUrl: pending.baseUrl, success: false };
814
+ }
815
+ const tokenBalance = await this.balanceManager.getTokenBalance(token, pending.baseUrl);
816
+ if (tokenBalance.reserved > 0) {
817
+ return { baseUrl: pending.baseUrl, success: false };
818
+ }
819
+ const result = await this.balanceManager.refund({
820
+ mintUrl,
821
+ baseUrl: pending.baseUrl,
822
+ token
823
+ });
824
+ return { baseUrl: pending.baseUrl, success: result.success };
825
+ })
826
+ );
827
+ for (const result of refundResults) {
828
+ const refundResult = result.status === "fulfilled" ? result.value : { baseUrl: "", success: false };
829
+ if (refundResult.success) {
830
+ this.storageAdapter.removeToken(refundResult.baseUrl);
831
+ }
832
+ }
833
+ const successfulRefunds = refundResults.filter(
834
+ (r) => r.status === "fulfilled" && r.value.success
835
+ ).length;
836
+ if (successfulRefunds > 0) {
837
+ this._logTransaction("refund", {
838
+ amount: pendingDistribution.length,
839
+ mintUrl,
840
+ status: "success",
841
+ details: `Refunded ${successfulRefunds} of ${pendingDistribution.length} tokens`
842
+ });
843
+ }
844
+ return this._spendInternal({
845
+ ...options,
846
+ retryCount: (retryCount || 0) + 1
847
+ });
848
+ }
849
+ /**
850
+ * Find an alternate mint that the provider accepts
851
+ */
852
+ async _findAlternateMint(options, balances, units, providerMints) {
853
+ const { amount, excludeMints } = options;
854
+ const adjustedAmount = Math.ceil(amount) + 2;
855
+ const extendedExcludes = [...excludeMints || []];
856
+ while (true) {
857
+ const { selectedMintUrl } = this._selectMintWithBalance(
858
+ balances,
859
+ units,
860
+ adjustedAmount,
861
+ extendedExcludes
862
+ );
863
+ if (!selectedMintUrl) break;
864
+ if (providerMints.includes(selectedMintUrl)) {
865
+ try {
866
+ const token = await this.walletAdapter.sendToken(
867
+ selectedMintUrl,
868
+ adjustedAmount
869
+ );
870
+ if (options.baseUrl) {
871
+ this.storageAdapter.setToken(options.baseUrl, token);
872
+ }
873
+ return {
874
+ token,
875
+ status: "success",
876
+ balance: adjustedAmount,
877
+ unit: units[selectedMintUrl] || "sat"
878
+ };
879
+ } catch (error) {
880
+ extendedExcludes.push(selectedMintUrl);
881
+ }
882
+ } else {
883
+ extendedExcludes.push(selectedMintUrl);
884
+ }
885
+ }
886
+ return null;
887
+ }
888
+ /**
889
+ * Handle send errors with retry logic for network errors
890
+ */
891
+ async _handleSendError(error, options, balances, units) {
892
+ const errorMsg = error instanceof Error ? error.message : String(error);
893
+ const isNetworkError = error instanceof Error && (error.message.includes(
894
+ "NetworkError when attempting to fetch resource"
895
+ ) || error.message.includes("Failed to fetch") || error.message.includes("Load failed"));
896
+ if (isNetworkError) {
897
+ const { mintUrl, amount, baseUrl, p2pkPubkey, excludeMints, retryCount } = options;
898
+ const extendedExcludes = [...excludeMints || [], mintUrl];
899
+ const { selectedMintUrl } = this._selectMintWithBalance(
900
+ balances,
901
+ units,
902
+ Math.ceil(amount),
903
+ extendedExcludes
904
+ );
905
+ if (selectedMintUrl && (retryCount || 0) < Object.keys(balances).length) {
906
+ return this._spendInternal({
907
+ ...options,
908
+ mintUrl: selectedMintUrl,
909
+ excludeMints: extendedExcludes,
910
+ retryCount: (retryCount || 0) + 1
911
+ });
912
+ }
913
+ throw new MintUnreachableError(mintUrl);
914
+ }
915
+ return {
916
+ token: null,
917
+ status: "failed",
918
+ balance: 0,
919
+ error: `Error generating token: ${errorMsg}`
920
+ };
921
+ }
922
+ /**
923
+ * Select a mint with sufficient balance
924
+ */
925
+ _selectMintWithBalance(balances, units, amount, excludeMints = []) {
926
+ for (const mintUrl in balances) {
927
+ if (excludeMints.includes(mintUrl)) {
928
+ continue;
929
+ }
930
+ const balance = balances[mintUrl];
931
+ const unit = units[mintUrl];
932
+ const balanceInSats = unit === "msat" ? balance / 1e3 : balance;
933
+ if (balanceInSats >= amount) {
934
+ return { selectedMintUrl: mintUrl, selectedMintBalance: balanceInSats };
935
+ }
936
+ }
937
+ return { selectedMintUrl: null, selectedMintBalance: 0 };
938
+ }
939
+ /**
940
+ * Create an insufficient balance error result
941
+ */
942
+ _createInsufficientBalanceError(required, balances, units, availableBalance) {
943
+ let maxBalance = 0;
944
+ let maxMintUrl = "";
945
+ for (const mintUrl in balances) {
946
+ const balance = balances[mintUrl];
947
+ const unit = units[mintUrl];
948
+ const balanceInSats = unit === "msat" ? balance / 1e3 : balance;
949
+ if (balanceInSats > maxBalance) {
950
+ maxBalance = balanceInSats;
951
+ maxMintUrl = mintUrl;
952
+ }
953
+ }
954
+ const error = new InsufficientBalanceError(
955
+ required,
956
+ availableBalance ?? maxBalance,
957
+ maxBalance,
958
+ maxMintUrl
959
+ );
960
+ return {
961
+ token: null,
962
+ status: "failed",
963
+ balance: 0,
964
+ error: error.message,
965
+ errorDetails: {
966
+ required,
967
+ available: availableBalance ?? maxBalance,
968
+ maxMintBalance: maxBalance,
969
+ maxMintUrl
970
+ }
971
+ };
972
+ }
973
+ async _getProviderTokenBalance(baseUrl, token) {
974
+ try {
975
+ const response = await fetch(`${baseUrl}v1/wallet/info`, {
976
+ headers: {
977
+ Authorization: `Bearer ${token}`
978
+ }
979
+ });
980
+ if (response.ok) {
981
+ const data = await response.json();
982
+ return data.balance / 1e3;
983
+ }
984
+ } catch {
985
+ return 0;
986
+ }
987
+ return 0;
988
+ }
989
+ };
990
+
991
+ // wallet/BalanceManager.ts
992
+ var BalanceManager = class {
993
+ constructor(walletAdapter, storageAdapter) {
994
+ this.walletAdapter = walletAdapter;
995
+ this.storageAdapter = storageAdapter;
996
+ }
997
+ /**
998
+ * Unified refund - handles both NIP-60 and legacy wallet refunds
999
+ */
1000
+ async refund(options) {
1001
+ const { mintUrl, baseUrl, token: providedToken } = options;
1002
+ const storedToken = providedToken || this.storageAdapter.getToken(baseUrl);
1003
+ if (!storedToken) {
1004
+ console.log("[BalanceManager] No token to refund, returning early");
1005
+ return { success: true, message: "No API key to refund" };
1006
+ }
1007
+ let fetchResult;
1008
+ try {
1009
+ fetchResult = await this._fetchRefundToken(baseUrl, storedToken);
1010
+ if (!fetchResult.success) {
1011
+ return {
1012
+ success: false,
1013
+ message: fetchResult.error || "Refund failed",
1014
+ requestId: fetchResult.requestId
1015
+ };
1016
+ }
1017
+ if (!fetchResult.token) {
1018
+ return {
1019
+ success: false,
1020
+ message: "No token received from refund",
1021
+ requestId: fetchResult.requestId
1022
+ };
1023
+ }
1024
+ if (fetchResult.error === "No balance to refund") {
1025
+ console.log(
1026
+ "[BalanceManager] No balance to refund, removing stored token"
1027
+ );
1028
+ this.storageAdapter.removeToken(baseUrl);
1029
+ return { success: true, message: "No balance to refund" };
1030
+ }
1031
+ const receiveResult = await this.walletAdapter.receiveToken(
1032
+ fetchResult.token
1033
+ );
1034
+ const totalAmountMsat = receiveResult.unit === "msat" ? receiveResult.amount : receiveResult.amount * 1e3;
1035
+ if (!providedToken) {
1036
+ this.storageAdapter.removeToken(baseUrl);
1037
+ }
1038
+ return {
1039
+ success: receiveResult.success,
1040
+ refundedAmount: totalAmountMsat,
1041
+ requestId: fetchResult.requestId
1042
+ };
1043
+ } catch (error) {
1044
+ console.error("[BalanceManager] Refund error", error);
1045
+ return this._handleRefundError(error, mintUrl, fetchResult?.requestId);
1046
+ }
1047
+ }
1048
+ /**
1049
+ * Top up API key balance with a cashu token
1050
+ */
1051
+ async topUp(options) {
1052
+ const { mintUrl, baseUrl, amount, token: providedToken } = options;
1053
+ if (!amount || amount <= 0) {
1054
+ return { success: false, message: "Invalid top up amount" };
1055
+ }
1056
+ const storedToken = providedToken || this.storageAdapter.getToken(baseUrl);
1057
+ if (!storedToken) {
1058
+ return { success: false, message: "No API key available for top up" };
1059
+ }
1060
+ let cashuToken = null;
1061
+ let requestId;
1062
+ try {
1063
+ cashuToken = await this.walletAdapter.sendToken(mintUrl, amount);
1064
+ const topUpResult = await this._postTopUp(
1065
+ baseUrl,
1066
+ storedToken,
1067
+ cashuToken
1068
+ );
1069
+ requestId = topUpResult.requestId;
1070
+ if (!topUpResult.success) {
1071
+ await this._recoverFailedTopUp(cashuToken);
1072
+ return {
1073
+ success: false,
1074
+ message: topUpResult.error || "Top up failed",
1075
+ requestId,
1076
+ recoveredToken: true
1077
+ };
1078
+ }
1079
+ return {
1080
+ success: true,
1081
+ toppedUpAmount: amount,
1082
+ requestId
1083
+ };
1084
+ } catch (error) {
1085
+ if (cashuToken) {
1086
+ await this._recoverFailedTopUp(cashuToken);
1087
+ }
1088
+ return this._handleTopUpError(error, mintUrl, requestId);
1089
+ }
1090
+ }
1091
+ /**
1092
+ * Fetch refund token from provider API
1093
+ */
1094
+ async _fetchRefundToken(baseUrl, storedToken) {
1095
+ if (!baseUrl) {
1096
+ return {
1097
+ success: false,
1098
+ error: "No base URL configured"
1099
+ };
1100
+ }
1101
+ const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
1102
+ const url = `${normalizedBaseUrl}v1/wallet/refund`;
1103
+ const controller = new AbortController();
1104
+ const timeoutId = setTimeout(() => {
1105
+ controller.abort();
1106
+ }, 6e4);
1107
+ try {
1108
+ const response = await fetch(url, {
1109
+ method: "POST",
1110
+ headers: {
1111
+ Authorization: `Bearer ${storedToken}`,
1112
+ "Content-Type": "application/json"
1113
+ },
1114
+ signal: controller.signal
1115
+ });
1116
+ clearTimeout(timeoutId);
1117
+ const requestId = response.headers.get("x-routstr-request-id") || void 0;
1118
+ if (!response.ok) {
1119
+ const errorData = await response.json().catch(() => ({}));
1120
+ if (response.status === 400 && errorData?.detail === "No balance to refund") {
1121
+ this.storageAdapter.removeToken(baseUrl);
1122
+ return {
1123
+ success: false,
1124
+ requestId,
1125
+ error: "No balance to refund"
1126
+ };
1127
+ }
1128
+ return {
1129
+ success: false,
1130
+ requestId,
1131
+ error: `Refund request failed with status ${response.status}: ${errorData?.detail || response.statusText}`
1132
+ };
1133
+ }
1134
+ const data = await response.json();
1135
+ console.log("refund rsule", data);
1136
+ return {
1137
+ success: true,
1138
+ token: data.token,
1139
+ requestId
1140
+ };
1141
+ } catch (error) {
1142
+ clearTimeout(timeoutId);
1143
+ console.error("[BalanceManager._fetchRefundToken] Fetch error", error);
1144
+ if (error instanceof Error) {
1145
+ if (error.name === "AbortError") {
1146
+ return {
1147
+ success: false,
1148
+ error: "Request timed out after 1 minute"
1149
+ };
1150
+ }
1151
+ return {
1152
+ success: false,
1153
+ error: error.message
1154
+ };
1155
+ }
1156
+ return {
1157
+ success: false,
1158
+ error: "Unknown error occurred during refund request"
1159
+ };
1160
+ }
1161
+ }
1162
+ /**
1163
+ * Post topup request to provider API
1164
+ */
1165
+ async _postTopUp(baseUrl, storedToken, cashuToken) {
1166
+ if (!baseUrl) {
1167
+ return {
1168
+ success: false,
1169
+ error: "No base URL configured"
1170
+ };
1171
+ }
1172
+ const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
1173
+ const url = `${normalizedBaseUrl}v1/wallet/topup?cashu_token=${encodeURIComponent(
1174
+ cashuToken
1175
+ )}`;
1176
+ const controller = new AbortController();
1177
+ const timeoutId = setTimeout(() => {
1178
+ controller.abort();
1179
+ }, 6e4);
1180
+ try {
1181
+ const response = await fetch(url, {
1182
+ method: "POST",
1183
+ headers: {
1184
+ Authorization: `Bearer ${storedToken}`,
1185
+ "Content-Type": "application/json"
1186
+ },
1187
+ signal: controller.signal
1188
+ });
1189
+ clearTimeout(timeoutId);
1190
+ const requestId = response.headers.get("x-routstr-request-id") || void 0;
1191
+ if (!response.ok) {
1192
+ const errorData = await response.json().catch(() => ({}));
1193
+ return {
1194
+ success: false,
1195
+ requestId,
1196
+ error: errorData?.detail || `Top up failed with status ${response.status}`
1197
+ };
1198
+ }
1199
+ return { success: true, requestId };
1200
+ } catch (error) {
1201
+ clearTimeout(timeoutId);
1202
+ console.error("[BalanceManager._postTopUp] Fetch error", error);
1203
+ if (error instanceof Error) {
1204
+ if (error.name === "AbortError") {
1205
+ return {
1206
+ success: false,
1207
+ error: "Request timed out after 1 minute"
1208
+ };
1209
+ }
1210
+ return {
1211
+ success: false,
1212
+ error: error.message
1213
+ };
1214
+ }
1215
+ return {
1216
+ success: false,
1217
+ error: "Unknown error occurred during top up request"
1218
+ };
1219
+ }
1220
+ }
1221
+ /**
1222
+ * Attempt to receive token back after failed top up
1223
+ */
1224
+ async _recoverFailedTopUp(cashuToken) {
1225
+ try {
1226
+ await this.walletAdapter.receiveToken(cashuToken);
1227
+ } catch (error) {
1228
+ console.error(
1229
+ "[BalanceManager._recoverFailedTopUp] Failed to recover token",
1230
+ error
1231
+ );
1232
+ }
1233
+ }
1234
+ /**
1235
+ * Handle refund errors with specific error types
1236
+ */
1237
+ _handleRefundError(error, mintUrl, requestId) {
1238
+ if (error instanceof Error) {
1239
+ if (error.message.includes(
1240
+ "NetworkError when attempting to fetch resource"
1241
+ ) || error.message.includes("Failed to fetch") || error.message.includes("Load failed")) {
1242
+ return {
1243
+ success: false,
1244
+ message: `Failed to connect to the mint: ${mintUrl}`,
1245
+ requestId
1246
+ };
1247
+ }
1248
+ if (error.message.includes("Wallet not found")) {
1249
+ return {
1250
+ success: false,
1251
+ message: `Wallet couldn't be loaded. Please save this refunded cashu token manually.`,
1252
+ requestId
1253
+ };
1254
+ }
1255
+ return {
1256
+ success: false,
1257
+ message: error.message,
1258
+ requestId
1259
+ };
1260
+ }
1261
+ return {
1262
+ success: false,
1263
+ message: "Refund failed",
1264
+ requestId
1265
+ };
1266
+ }
1267
+ /**
1268
+ * Get token balance from provider
1269
+ */
1270
+ async getTokenBalance(token, baseUrl) {
1271
+ try {
1272
+ const response = await fetch(`${baseUrl}v1/wallet/info`, {
1273
+ headers: {
1274
+ Authorization: `Bearer ${token}`
1275
+ }
1276
+ });
1277
+ if (response.ok) {
1278
+ const data = await response.json();
1279
+ return {
1280
+ amount: data.balance,
1281
+ reserved: data.reserved ?? 0,
1282
+ unit: "msat",
1283
+ apiKey: data.api_key
1284
+ };
1285
+ }
1286
+ } catch {
1287
+ }
1288
+ return { amount: 0, reserved: 0, unit: "sat", apiKey: "" };
1289
+ }
1290
+ /**
1291
+ * Handle topup errors with specific error types
1292
+ */
1293
+ _handleTopUpError(error, mintUrl, requestId) {
1294
+ if (error instanceof Error) {
1295
+ if (error.message.includes(
1296
+ "NetworkError when attempting to fetch resource"
1297
+ ) || error.message.includes("Failed to fetch") || error.message.includes("Load failed")) {
1298
+ return {
1299
+ success: false,
1300
+ message: `Failed to connect to the mint: ${mintUrl}`,
1301
+ requestId
1302
+ };
1303
+ }
1304
+ if (error.message.includes("Wallet not found")) {
1305
+ return {
1306
+ success: false,
1307
+ message: "Wallet couldn't be loaded. The cashu token was recovered locally.",
1308
+ requestId
1309
+ };
1310
+ }
1311
+ return {
1312
+ success: false,
1313
+ message: error.message,
1314
+ requestId
1315
+ };
1316
+ }
1317
+ return {
1318
+ success: false,
1319
+ message: "Top up failed",
1320
+ requestId
1321
+ };
1322
+ }
1323
+ };
1324
+
1325
+ // client/StreamProcessor.ts
1326
+ var StreamProcessor = class {
1327
+ accumulatedContent = "";
1328
+ accumulatedThinking = "";
1329
+ accumulatedImages = [];
1330
+ isInThinking = false;
1331
+ isInContent = false;
1332
+ /**
1333
+ * Process a streaming response
1334
+ */
1335
+ async process(response, callbacks, modelId) {
1336
+ if (!response.body) {
1337
+ throw new Error("Response body is not available");
1338
+ }
1339
+ const reader = response.body.getReader();
1340
+ const decoder = new TextDecoder("utf-8");
1341
+ let buffer = "";
1342
+ this.accumulatedContent = "";
1343
+ this.accumulatedThinking = "";
1344
+ this.accumulatedImages = [];
1345
+ this.isInThinking = false;
1346
+ this.isInContent = false;
1347
+ let usage;
1348
+ let model;
1349
+ let finish_reason;
1350
+ let citations;
1351
+ let annotations;
1352
+ try {
1353
+ while (true) {
1354
+ const { done, value } = await reader.read();
1355
+ if (done) {
1356
+ break;
1357
+ }
1358
+ const chunk = decoder.decode(value, { stream: true });
1359
+ buffer += chunk;
1360
+ const lines = buffer.split("\n");
1361
+ buffer = lines.pop() || "";
1362
+ for (const line of lines) {
1363
+ const parsed = this._parseLine(line);
1364
+ if (!parsed) continue;
1365
+ if (parsed.content) {
1366
+ this._handleContent(parsed.content, callbacks, modelId);
1367
+ }
1368
+ if (parsed.reasoning) {
1369
+ this._handleThinking(parsed.reasoning, callbacks);
1370
+ }
1371
+ if (parsed.usage) {
1372
+ usage = parsed.usage;
1373
+ }
1374
+ if (parsed.model) {
1375
+ model = parsed.model;
1376
+ }
1377
+ if (parsed.finish_reason) {
1378
+ finish_reason = parsed.finish_reason;
1379
+ }
1380
+ if (parsed.citations) {
1381
+ citations = parsed.citations;
1382
+ }
1383
+ if (parsed.annotations) {
1384
+ annotations = parsed.annotations;
1385
+ }
1386
+ if (parsed.images) {
1387
+ this._mergeImages(parsed.images);
1388
+ }
1389
+ }
1390
+ }
1391
+ } finally {
1392
+ reader.releaseLock();
1393
+ }
1394
+ return {
1395
+ content: this.accumulatedContent,
1396
+ thinking: this.accumulatedThinking || void 0,
1397
+ images: this.accumulatedImages.length > 0 ? this.accumulatedImages : void 0,
1398
+ usage,
1399
+ model,
1400
+ finish_reason,
1401
+ citations,
1402
+ annotations
1403
+ };
1404
+ }
1405
+ /**
1406
+ * Parse a single SSE line
1407
+ */
1408
+ _parseLine(line) {
1409
+ if (!line.trim()) return null;
1410
+ if (!line.startsWith("data: ")) {
1411
+ return null;
1412
+ }
1413
+ const jsonData = line.slice(6);
1414
+ if (jsonData === "[DONE]") {
1415
+ return null;
1416
+ }
1417
+ try {
1418
+ const parsed = JSON.parse(jsonData);
1419
+ const result = {};
1420
+ if (parsed.choices?.[0]?.delta?.content) {
1421
+ result.content = parsed.choices[0].delta.content;
1422
+ }
1423
+ if (parsed.choices?.[0]?.delta?.reasoning) {
1424
+ result.reasoning = parsed.choices[0].delta.reasoning;
1425
+ }
1426
+ if (parsed.usage) {
1427
+ result.usage = {
1428
+ total_tokens: parsed.usage.total_tokens,
1429
+ prompt_tokens: parsed.usage.prompt_tokens,
1430
+ completion_tokens: parsed.usage.completion_tokens
1431
+ };
1432
+ }
1433
+ if (parsed.model) {
1434
+ result.model = parsed.model;
1435
+ }
1436
+ if (parsed.citations) {
1437
+ result.citations = parsed.citations;
1438
+ }
1439
+ if (parsed.annotations) {
1440
+ result.annotations = parsed.annotations;
1441
+ }
1442
+ if (parsed.choices?.[0]?.finish_reason) {
1443
+ result.finish_reason = parsed.choices[0].finish_reason;
1444
+ }
1445
+ const images = parsed.choices?.[0]?.message?.images || parsed.choices?.[0]?.delta?.images;
1446
+ if (images && Array.isArray(images)) {
1447
+ result.images = images;
1448
+ }
1449
+ return result;
1450
+ } catch {
1451
+ return null;
1452
+ }
1453
+ }
1454
+ /**
1455
+ * Handle content delta with thinking support
1456
+ */
1457
+ _handleContent(content, callbacks, modelId) {
1458
+ if (this.isInThinking && !this.isInContent) {
1459
+ this.accumulatedThinking += "</thinking>";
1460
+ callbacks.onThinking(this.accumulatedThinking);
1461
+ this.isInThinking = false;
1462
+ this.isInContent = true;
1463
+ }
1464
+ if (modelId) {
1465
+ this._extractThinkingFromContent(content, callbacks);
1466
+ } else {
1467
+ this.accumulatedContent += content;
1468
+ }
1469
+ callbacks.onContent(this.accumulatedContent);
1470
+ }
1471
+ /**
1472
+ * Handle thinking/reasoning content
1473
+ */
1474
+ _handleThinking(reasoning, callbacks) {
1475
+ if (!this.isInThinking) {
1476
+ this.accumulatedThinking += "<thinking> ";
1477
+ this.isInThinking = true;
1478
+ }
1479
+ this.accumulatedThinking += reasoning;
1480
+ callbacks.onThinking(this.accumulatedThinking);
1481
+ }
1482
+ /**
1483
+ * Extract thinking blocks from content (for models with inline thinking)
1484
+ */
1485
+ _extractThinkingFromContent(content, callbacks) {
1486
+ const parts = content.split(/(<thinking>|<\/thinking>)/);
1487
+ for (const part of parts) {
1488
+ if (part === "<thinking>") {
1489
+ this.isInThinking = true;
1490
+ if (!this.accumulatedThinking.includes("<thinking>")) {
1491
+ this.accumulatedThinking += "<thinking> ";
1492
+ }
1493
+ } else if (part === "</thinking>") {
1494
+ this.isInThinking = false;
1495
+ this.accumulatedThinking += "</thinking>";
1496
+ } else if (this.isInThinking) {
1497
+ this.accumulatedThinking += part;
1498
+ } else {
1499
+ this.accumulatedContent += part;
1500
+ }
1501
+ }
1502
+ }
1503
+ /**
1504
+ * Merge images into accumulated array, avoiding duplicates
1505
+ */
1506
+ _mergeImages(newImages) {
1507
+ for (const img of newImages) {
1508
+ const newUrl = img.image_url?.url;
1509
+ const existingIndex = this.accumulatedImages.findIndex((existing) => {
1510
+ const existingUrl = existing.image_url?.url;
1511
+ if (newUrl && existingUrl) {
1512
+ return existingUrl === newUrl;
1513
+ }
1514
+ if (img.index !== void 0 && existing.index !== void 0) {
1515
+ return existing.index === img.index;
1516
+ }
1517
+ return false;
1518
+ });
1519
+ if (existingIndex === -1) {
1520
+ this.accumulatedImages.push(img);
1521
+ } else {
1522
+ this.accumulatedImages[existingIndex] = img;
1523
+ }
1524
+ }
1525
+ }
1526
+ };
1527
+
1528
+ // utils/torUtils.ts
1529
+ var TOR_ONION_SUFFIX = ".onion";
1530
+ var isTorContext = () => {
1531
+ if (typeof window === "undefined") return false;
1532
+ const hostname = window.location.hostname.toLowerCase();
1533
+ return hostname.endsWith(TOR_ONION_SUFFIX);
1534
+ };
1535
+ var isOnionUrl = (url) => {
1536
+ if (!url) return false;
1537
+ const trimmed = url.trim().toLowerCase();
1538
+ if (!trimmed) return false;
1539
+ try {
1540
+ const candidate = trimmed.startsWith("http") ? trimmed : `http://${trimmed}`;
1541
+ return new URL(candidate).hostname.endsWith(TOR_ONION_SUFFIX);
1542
+ } catch {
1543
+ return trimmed.includes(TOR_ONION_SUFFIX);
1544
+ }
1545
+ };
1546
+ var shouldAllowHttp = (url, torMode) => {
1547
+ if (!url.startsWith("http://")) return true;
1548
+ if (url.includes("localhost") || url.includes("127.0.0.1")) return true;
1549
+ return torMode && isOnionUrl(url);
1550
+ };
1551
+ var normalizeProviderUrl = (url, torMode = false) => {
1552
+ if (!url || typeof url !== "string") return null;
1553
+ const trimmed = url.trim();
1554
+ if (!trimmed) return null;
1555
+ if (/^https?:\/\//i.test(trimmed)) {
1556
+ return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
1557
+ }
1558
+ const useHttpForOnion = torMode && isOnionUrl(trimmed);
1559
+ const withProto = `${useHttpForOnion ? "http" : "https"}://${trimmed}`;
1560
+ return withProto.endsWith("/") ? withProto : `${withProto}/`;
1561
+ };
1562
+ var dedupePreserveOrder = (urls) => {
1563
+ const seen = /* @__PURE__ */ new Set();
1564
+ const out = [];
1565
+ for (const url of urls) {
1566
+ if (!seen.has(url)) {
1567
+ seen.add(url);
1568
+ out.push(url);
1569
+ }
1570
+ }
1571
+ return out;
1572
+ };
1573
+ var getProviderEndpoints = (provider, torMode) => {
1574
+ const rawUrls = [
1575
+ provider.endpoint_url,
1576
+ ...Array.isArray(provider.endpoint_urls) ? provider.endpoint_urls : [],
1577
+ provider.onion_url,
1578
+ ...Array.isArray(provider.onion_urls) ? provider.onion_urls : []
1579
+ ];
1580
+ const normalized = rawUrls.map((value) => normalizeProviderUrl(value, torMode)).filter((value) => Boolean(value));
1581
+ const unique = dedupePreserveOrder(normalized).filter(
1582
+ (value) => shouldAllowHttp(value, torMode)
1583
+ );
1584
+ if (unique.length === 0) return [];
1585
+ const onion = unique.filter((value) => isOnionUrl(value));
1586
+ const clearnet = unique.filter((value) => !isOnionUrl(value));
1587
+ if (torMode) {
1588
+ return onion.length > 0 ? onion : clearnet;
1589
+ }
1590
+ return clearnet;
1591
+ };
1592
+ var filterBaseUrlsForTor = (baseUrls, torMode) => {
1593
+ if (!Array.isArray(baseUrls)) return [];
1594
+ const normalized = baseUrls.map((value) => normalizeProviderUrl(value, torMode)).filter((value) => Boolean(value));
1595
+ const filtered = normalized.filter(
1596
+ (value) => torMode ? true : !isOnionUrl(value)
1597
+ );
1598
+ return dedupePreserveOrder(
1599
+ filtered.filter((value) => shouldAllowHttp(value, torMode))
1600
+ );
1601
+ };
1602
+
1603
+ // client/ProviderManager.ts
1604
+ function getImageResolutionFromDataUrl(dataUrl) {
1605
+ try {
1606
+ if (typeof dataUrl !== "string" || !dataUrl.startsWith("data:"))
1607
+ return null;
1608
+ const commaIdx = dataUrl.indexOf(",");
1609
+ if (commaIdx === -1) return null;
1610
+ const meta = dataUrl.slice(5, commaIdx);
1611
+ const base64 = dataUrl.slice(commaIdx + 1);
1612
+ const binary = typeof atob === "function" ? atob(base64) : Buffer.from(base64, "base64").toString("binary");
1613
+ const len = binary.length;
1614
+ const bytes = new Uint8Array(len);
1615
+ for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i);
1616
+ const isPNG = meta.includes("image/png");
1617
+ const isJPEG = meta.includes("image/jpeg") || meta.includes("image/jpg");
1618
+ if (isPNG) {
1619
+ const sig = [137, 80, 78, 71, 13, 10, 26, 10];
1620
+ for (let i = 0; i < sig.length; i++) {
1621
+ if (bytes[i] !== sig[i]) return null;
1622
+ }
1623
+ const view = new DataView(
1624
+ bytes.buffer,
1625
+ bytes.byteOffset,
1626
+ bytes.byteLength
1627
+ );
1628
+ const width = view.getUint32(16, false);
1629
+ const height = view.getUint32(20, false);
1630
+ if (width > 0 && height > 0) return { width, height };
1631
+ return null;
1632
+ }
1633
+ if (isJPEG) {
1634
+ let offset = 0;
1635
+ if (bytes[offset++] !== 255 || bytes[offset++] !== 216) return null;
1636
+ while (offset < bytes.length) {
1637
+ while (offset < bytes.length && bytes[offset] !== 255) offset++;
1638
+ if (offset + 1 >= bytes.length) break;
1639
+ while (bytes[offset] === 255) offset++;
1640
+ const marker = bytes[offset++];
1641
+ if (marker === 216 || marker === 217) continue;
1642
+ if (offset + 1 >= bytes.length) break;
1643
+ const length = bytes[offset] << 8 | bytes[offset + 1];
1644
+ offset += 2;
1645
+ if (marker === 192 || marker === 194) {
1646
+ if (length < 7 || offset + length - 2 > bytes.length) return null;
1647
+ const precision = bytes[offset];
1648
+ const height = bytes[offset + 1] << 8 | bytes[offset + 2];
1649
+ const width = bytes[offset + 3] << 8 | bytes[offset + 4];
1650
+ if (precision > 0 && width > 0 && height > 0)
1651
+ return { width, height };
1652
+ return null;
1653
+ } else {
1654
+ offset += length - 2;
1655
+ }
1656
+ }
1657
+ return null;
1658
+ }
1659
+ return null;
1660
+ } catch {
1661
+ return null;
1662
+ }
1663
+ }
1664
+ function calculateImageTokens(width, height, detail = "auto") {
1665
+ if (detail === "low") return 85;
1666
+ let w = width;
1667
+ let h = height;
1668
+ if (w > 2048 || h > 2048) {
1669
+ const aspectRatio = w / h;
1670
+ if (w > h) {
1671
+ w = 2048;
1672
+ h = Math.floor(w / aspectRatio);
1673
+ } else {
1674
+ h = 2048;
1675
+ w = Math.floor(h * aspectRatio);
1676
+ }
1677
+ }
1678
+ if (w > 768 || h > 768) {
1679
+ const aspectRatio = w / h;
1680
+ if (w > h) {
1681
+ w = 768;
1682
+ h = Math.floor(w / aspectRatio);
1683
+ } else {
1684
+ h = 768;
1685
+ w = Math.floor(h * aspectRatio);
1686
+ }
1687
+ }
1688
+ const tilesWidth = Math.floor((w + 511) / 512);
1689
+ const tilesHeight = Math.floor((h + 511) / 512);
1690
+ const numTiles = tilesWidth * tilesHeight;
1691
+ return 85 + 170 * numTiles;
1692
+ }
1693
+ var ProviderManager = class {
1694
+ constructor(providerRegistry) {
1695
+ this.providerRegistry = providerRegistry;
1696
+ }
1697
+ failedProviders = /* @__PURE__ */ new Set();
1698
+ /**
1699
+ * Reset the failed providers list
1700
+ */
1701
+ resetFailedProviders() {
1702
+ this.failedProviders.clear();
1703
+ }
1704
+ /**
1705
+ * Mark a provider as failed
1706
+ */
1707
+ markFailed(baseUrl) {
1708
+ this.failedProviders.add(baseUrl);
1709
+ }
1710
+ /**
1711
+ * Check if a provider has failed
1712
+ */
1713
+ hasFailed(baseUrl) {
1714
+ return this.failedProviders.has(baseUrl);
1715
+ }
1716
+ /**
1717
+ * Find the next best provider for a model
1718
+ * @param modelId The model ID to find a provider for
1719
+ * @param currentBaseUrl The current provider to exclude
1720
+ * @returns The best provider URL or null if none available
1721
+ */
1722
+ findNextBestProvider(modelId, currentBaseUrl) {
1723
+ try {
1724
+ const torMode = isTorContext();
1725
+ const disabledProviders = new Set(
1726
+ this.providerRegistry.getDisabledProviders()
1727
+ );
1728
+ const allProviders = this.providerRegistry.getAllProvidersModels();
1729
+ const candidates = [];
1730
+ for (const [baseUrl, models] of Object.entries(allProviders)) {
1731
+ if (baseUrl === currentBaseUrl || this.failedProviders.has(baseUrl) || disabledProviders.has(baseUrl)) {
1732
+ continue;
1733
+ }
1734
+ if (!torMode && isOnionUrl(baseUrl)) {
1735
+ continue;
1736
+ }
1737
+ const model = models.find((m) => m.id === modelId);
1738
+ if (!model) continue;
1739
+ const cost = model.sats_pricing?.completion ?? 0;
1740
+ candidates.push({ baseUrl, model, cost });
1741
+ }
1742
+ candidates.sort((a, b) => a.cost - b.cost);
1743
+ return candidates.length > 0 ? candidates[0].baseUrl : null;
1744
+ } catch (error) {
1745
+ console.error("Error finding next best provider:", error);
1746
+ return null;
1747
+ }
1748
+ }
1749
+ /**
1750
+ * Find the best model for a provider
1751
+ * Useful when switching providers and need to find equivalent model
1752
+ */
1753
+ async getModelForProvider(baseUrl, modelId) {
1754
+ const models = this.providerRegistry.getModelsForProvider(baseUrl);
1755
+ const exactMatch = models.find((m) => m.id === modelId);
1756
+ if (exactMatch) return exactMatch;
1757
+ const providerInfo = await this.providerRegistry.getProviderInfo(baseUrl);
1758
+ if (providerInfo?.version && /^0\.1\./.test(providerInfo.version)) {
1759
+ const suffix = modelId.split("/").pop();
1760
+ const suffixMatch = models.find((m) => m.id === suffix);
1761
+ if (suffixMatch) return suffixMatch;
1762
+ }
1763
+ return null;
1764
+ }
1765
+ /**
1766
+ * Get all available providers for a model
1767
+ * Returns sorted list by price
1768
+ */
1769
+ getAllProvidersForModel(modelId) {
1770
+ const candidates = [];
1771
+ const allProviders = this.providerRegistry.getAllProvidersModels();
1772
+ const disabledProviders = new Set(
1773
+ this.providerRegistry.getDisabledProviders()
1774
+ );
1775
+ const torMode = isTorContext();
1776
+ for (const [baseUrl, models] of Object.entries(allProviders)) {
1777
+ if (disabledProviders.has(baseUrl)) continue;
1778
+ if (!torMode && isOnionUrl(baseUrl)) continue;
1779
+ const model = models.find((m) => m.id === modelId);
1780
+ if (!model) continue;
1781
+ const cost = model.sats_pricing?.completion ?? 0;
1782
+ candidates.push({ baseUrl, model, cost });
1783
+ }
1784
+ return candidates.sort((a, b) => a.cost - b.cost);
1785
+ }
1786
+ /**
1787
+ * Get providers for a model sorted by prompt+completion pricing
1788
+ */
1789
+ getProviderPriceRankingForModel(modelId, options = {}) {
1790
+ const normalizedId = this.normalizeModelId(modelId);
1791
+ const includeDisabled = options.includeDisabled ?? false;
1792
+ const torMode = options.torMode ?? false;
1793
+ const disabledProviders = new Set(
1794
+ this.providerRegistry.getDisabledProviders()
1795
+ );
1796
+ const allModels = this.providerRegistry.getAllProvidersModels();
1797
+ const results = [];
1798
+ for (const [baseUrl, models] of Object.entries(allModels)) {
1799
+ if (!includeDisabled && disabledProviders.has(baseUrl)) continue;
1800
+ if (torMode && !baseUrl.includes(".onion")) continue;
1801
+ if (!torMode && baseUrl.includes(".onion")) continue;
1802
+ const match = models.find(
1803
+ (model) => this.normalizeModelId(model.id) === normalizedId
1804
+ );
1805
+ if (!match?.sats_pricing) continue;
1806
+ const prompt = match.sats_pricing.prompt;
1807
+ const completion = match.sats_pricing.completion;
1808
+ if (typeof prompt !== "number" || typeof completion !== "number") {
1809
+ continue;
1810
+ }
1811
+ const promptPerMillion = prompt * 1e6;
1812
+ const completionPerMillion = completion * 1e6;
1813
+ const totalPerMillion = promptPerMillion + completionPerMillion;
1814
+ results.push({
1815
+ baseUrl,
1816
+ model: match,
1817
+ promptPerMillion,
1818
+ completionPerMillion,
1819
+ totalPerMillion
1820
+ });
1821
+ }
1822
+ return results.sort((a, b) => {
1823
+ if (a.totalPerMillion !== b.totalPerMillion) {
1824
+ return a.totalPerMillion - b.totalPerMillion;
1825
+ }
1826
+ return a.baseUrl.localeCompare(b.baseUrl);
1827
+ });
1828
+ }
1829
+ /**
1830
+ * Get best-priced provider for a specific model
1831
+ */
1832
+ getBestProviderForModel(modelId, options = {}) {
1833
+ const ranking = this.getProviderPriceRankingForModel(modelId, options);
1834
+ return ranking[0]?.baseUrl ?? null;
1835
+ }
1836
+ normalizeModelId(modelId) {
1837
+ return modelId.includes("/") ? modelId.split("/").pop() || modelId : modelId;
1838
+ }
1839
+ /**
1840
+ * Check if a provider accepts a specific mint
1841
+ */
1842
+ providerAcceptsMint(baseUrl, mintUrl) {
1843
+ const providerMints = this.providerRegistry.getProviderMints(baseUrl);
1844
+ if (providerMints.length === 0) {
1845
+ return true;
1846
+ }
1847
+ return providerMints.includes(mintUrl);
1848
+ }
1849
+ /**
1850
+ * Get required sats for a model based on message history
1851
+ * Simple estimation based on typical usage
1852
+ */
1853
+ getRequiredSatsForModel(model, apiMessages, maxTokens) {
1854
+ try {
1855
+ let imageTokens = 0;
1856
+ if (apiMessages) {
1857
+ for (const msg of apiMessages) {
1858
+ const content = msg?.content;
1859
+ if (Array.isArray(content)) {
1860
+ for (const part of content) {
1861
+ const isImage = part && typeof part === "object" && part.type === "image_url";
1862
+ const url = isImage ? typeof part.image_url === "string" ? part.image_url : part.image_url?.url : void 0;
1863
+ if (url && typeof url === "string" && url.startsWith("data:")) {
1864
+ const res = getImageResolutionFromDataUrl(url);
1865
+ if (res) {
1866
+ const tokensFromImage = calculateImageTokens(
1867
+ res.width,
1868
+ res.height
1869
+ );
1870
+ imageTokens += tokensFromImage;
1871
+ console.log("IMAGE INPUT RESOLUTION", {
1872
+ width: res.width,
1873
+ height: res.height,
1874
+ tokensFromImage
1875
+ });
1876
+ } else {
1877
+ console.log(
1878
+ "IMAGE INPUT RESOLUTION",
1879
+ "unknown (unsupported format or parse failure)"
1880
+ );
1881
+ }
1882
+ }
1883
+ }
1884
+ }
1885
+ }
1886
+ }
1887
+ const apiMessagesNoImages = apiMessages ? apiMessages.map((m) => {
1888
+ if (Array.isArray(m?.content)) {
1889
+ const filtered = m.content.filter(
1890
+ (p) => !(p && typeof p === "object" && p.type === "image_url")
1891
+ );
1892
+ return { ...m, content: filtered };
1893
+ }
1894
+ return m;
1895
+ }) : void 0;
1896
+ const approximateTokens = apiMessagesNoImages ? Math.ceil(JSON.stringify(apiMessagesNoImages, null, 2).length / 2.84) : 1e4;
1897
+ const totalInputTokens = approximateTokens + imageTokens;
1898
+ const sp = model?.sats_pricing;
1899
+ if (!sp) return 0;
1900
+ if (!sp.max_completion_cost) {
1901
+ return sp.max_cost ?? 50;
1902
+ }
1903
+ const promptCosts = (sp.prompt || 0) * totalInputTokens;
1904
+ let completionCost = sp.max_completion_cost;
1905
+ if (maxTokens !== void 0 && sp.completion) {
1906
+ completionCost = sp.completion * maxTokens;
1907
+ }
1908
+ const totalEstimatedCosts = (promptCosts + completionCost) * 1.05;
1909
+ return totalEstimatedCosts;
1910
+ } catch (e) {
1911
+ console.error(e);
1912
+ return 0;
1913
+ }
1914
+ }
1915
+ };
1916
+
1917
+ // client/RoutstrClient.ts
1918
+ var RoutstrClient = class {
1919
+ constructor(walletAdapter, storageAdapter, providerRegistry, alertLevel, mode = "xcashu") {
1920
+ this.walletAdapter = walletAdapter;
1921
+ this.storageAdapter = storageAdapter;
1922
+ this.providerRegistry = providerRegistry;
1923
+ this.balanceManager = new BalanceManager(walletAdapter, storageAdapter);
1924
+ this.cashuSpender = new CashuSpender(
1925
+ walletAdapter,
1926
+ storageAdapter,
1927
+ providerRegistry,
1928
+ this.balanceManager
1929
+ );
1930
+ this.streamProcessor = new StreamProcessor();
1931
+ this.providerManager = new ProviderManager(providerRegistry);
1932
+ this.alertLevel = alertLevel;
1933
+ this.mode = mode;
1934
+ }
1935
+ cashuSpender;
1936
+ balanceManager;
1937
+ streamProcessor;
1938
+ providerManager;
1939
+ alertLevel;
1940
+ mode;
1941
+ /**
1942
+ * Get the current client mode
1943
+ */
1944
+ getMode() {
1945
+ return this.mode;
1946
+ }
1947
+ /**
1948
+ * Get the CashuSpender instance
1949
+ */
1950
+ getCashuSpender() {
1951
+ return this.cashuSpender;
1952
+ }
1953
+ /**
1954
+ * Get the BalanceManager instance
1955
+ */
1956
+ getBalanceManager() {
1957
+ return this.balanceManager;
1958
+ }
1959
+ /**
1960
+ * Get the ProviderManager instance
1961
+ */
1962
+ getProviderManager() {
1963
+ return this.providerManager;
1964
+ }
1965
+ /**
1966
+ * Check if the client is currently busy (in critical section)
1967
+ */
1968
+ get isBusy() {
1969
+ return this.cashuSpender.isBusy;
1970
+ }
1971
+ /**
1972
+ * Route an API request to the upstream provider
1973
+ *
1974
+ * This is a simpler alternative to fetchAIResponse that just proxies
1975
+ * the request upstream without the streaming callback machinery.
1976
+ * Useful for daemon-style routing where you just need to forward
1977
+ * requests and get responses back.
1978
+ */
1979
+ async routeRequest(params) {
1980
+ const {
1981
+ path,
1982
+ method,
1983
+ body,
1984
+ headers = {},
1985
+ baseUrl,
1986
+ mintUrl,
1987
+ modelId
1988
+ } = params;
1989
+ await this._checkBalance();
1990
+ let requiredSats = 1;
1991
+ let selectedModel;
1992
+ if (modelId) {
1993
+ const providerModel = await this.providerManager.getModelForProvider(
1994
+ baseUrl,
1995
+ modelId
1996
+ );
1997
+ selectedModel = providerModel ?? void 0;
1998
+ if (selectedModel) {
1999
+ requiredSats = this.providerManager.getRequiredSatsForModel(
2000
+ selectedModel,
2001
+ []
2002
+ );
2003
+ }
2004
+ }
2005
+ const { token, tokenBalance, tokenBalanceUnit } = await this._spendToken({
2006
+ mintUrl,
2007
+ amount: requiredSats,
2008
+ baseUrl
2009
+ });
2010
+ let requestBody = body;
2011
+ if (body && typeof body === "object") {
2012
+ const bodyObj = body;
2013
+ if (!bodyObj.stream) {
2014
+ requestBody = { ...bodyObj, stream: false };
2015
+ }
2016
+ }
2017
+ const baseHeaders = this._buildBaseHeaders(headers);
2018
+ const requestHeaders = this._withAuthHeader(baseHeaders, token);
2019
+ const response = await this._makeRequest({
2020
+ path,
2021
+ method,
2022
+ body: method === "GET" ? void 0 : requestBody,
2023
+ baseUrl,
2024
+ mintUrl,
2025
+ token,
2026
+ requiredSats,
2027
+ headers: requestHeaders,
2028
+ baseHeaders,
2029
+ selectedModel
2030
+ });
2031
+ const tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
2032
+ await this._handlePostResponseBalanceUpdate({
2033
+ token,
2034
+ baseUrl,
2035
+ initialTokenBalance: tokenBalanceInSats,
2036
+ response
2037
+ });
2038
+ return response;
2039
+ }
2040
+ /**
2041
+ * Fetch AI response with streaming
2042
+ */
2043
+ async fetchAIResponse(options, callbacks) {
2044
+ const {
2045
+ messageHistory,
2046
+ selectedModel,
2047
+ baseUrl,
2048
+ mintUrl,
2049
+ balance,
2050
+ transactionHistory,
2051
+ maxTokens,
2052
+ headers
2053
+ } = options;
2054
+ const apiMessages = await this._convertMessages(messageHistory);
2055
+ const requiredSats = this.providerManager.getRequiredSatsForModel(
2056
+ selectedModel,
2057
+ apiMessages,
2058
+ maxTokens
2059
+ );
2060
+ try {
2061
+ await this._checkBalance();
2062
+ callbacks.onPaymentProcessing?.(true);
2063
+ const spendResult = await this._spendToken({
2064
+ mintUrl,
2065
+ amount: requiredSats,
2066
+ baseUrl
2067
+ });
2068
+ let token = spendResult.token;
2069
+ let tokenBalance = spendResult.tokenBalance;
2070
+ let tokenBalanceUnit = spendResult.tokenBalanceUnit;
2071
+ const tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
2072
+ callbacks.onTokenCreated?.(this._getPendingCashuTokenAmount());
2073
+ const baseHeaders = this._buildBaseHeaders(headers);
2074
+ const requestHeaders = this._withAuthHeader(baseHeaders, token);
2075
+ this.providerManager.resetFailedProviders();
2076
+ const providerInfo = await this.providerRegistry.getProviderInfo(baseUrl);
2077
+ const providerVersion = providerInfo?.version ?? "";
2078
+ let modelIdForRequest = selectedModel.id;
2079
+ if (/^0\.1\./.test(providerVersion)) {
2080
+ const newModel = await this.providerManager.getModelForProvider(
2081
+ baseUrl,
2082
+ selectedModel.id
2083
+ );
2084
+ modelIdForRequest = newModel?.id ?? selectedModel.id;
2085
+ }
2086
+ const body = {
2087
+ model: modelIdForRequest,
2088
+ messages: apiMessages,
2089
+ stream: true
2090
+ };
2091
+ if (maxTokens !== void 0) {
2092
+ body.max_tokens = maxTokens;
2093
+ }
2094
+ if (selectedModel?.name?.startsWith("OpenAI:")) {
2095
+ body.tools = [{ type: "web_search" }];
2096
+ }
2097
+ const response = await this._makeRequest({
2098
+ path: "/v1/chat/completions",
2099
+ method: "POST",
2100
+ body,
2101
+ selectedModel,
2102
+ baseUrl,
2103
+ mintUrl,
2104
+ token,
2105
+ requiredSats,
2106
+ maxTokens,
2107
+ headers: requestHeaders,
2108
+ baseHeaders
2109
+ });
2110
+ if (!response.body) {
2111
+ throw new Error("Response body is not available");
2112
+ }
2113
+ if (response.status === 200) {
2114
+ const baseUrlUsed = response.baseUrl || baseUrl;
2115
+ const streamingResult = await this.streamProcessor.process(
2116
+ response,
2117
+ {
2118
+ onContent: callbacks.onStreamingUpdate,
2119
+ onThinking: callbacks.onThinkingUpdate
2120
+ },
2121
+ selectedModel.id
2122
+ );
2123
+ if (streamingResult.finish_reason === "content_filter") {
2124
+ callbacks.onMessageAppend({
2125
+ role: "assistant",
2126
+ content: "Your request was denied due to content filtering."
2127
+ });
2128
+ } else if (streamingResult.content || streamingResult.images && streamingResult.images.length > 0) {
2129
+ const message = await this._createAssistantMessage(streamingResult);
2130
+ callbacks.onMessageAppend(message);
2131
+ } else {
2132
+ callbacks.onMessageAppend({
2133
+ role: "system",
2134
+ content: "The provider did not respond to this request."
2135
+ });
2136
+ }
2137
+ callbacks.onStreamingUpdate("");
2138
+ callbacks.onThinkingUpdate("");
2139
+ const isApikeysEstimate = this.mode === "apikeys";
2140
+ let satsSpent = await this._handlePostResponseBalanceUpdate({
2141
+ token,
2142
+ baseUrl: baseUrlUsed,
2143
+ initialTokenBalance: tokenBalanceInSats,
2144
+ fallbackSatsSpent: isApikeysEstimate ? this._getEstimatedCosts(selectedModel, streamingResult) : void 0,
2145
+ response
2146
+ });
2147
+ const estimatedCosts = this._getEstimatedCosts(
2148
+ selectedModel,
2149
+ streamingResult
2150
+ );
2151
+ const onLastMessageSatsUpdate = callbacks.onLastMessageSatsUpdate;
2152
+ onLastMessageSatsUpdate?.(satsSpent, estimatedCosts);
2153
+ } else {
2154
+ throw new Error(`${response.status} ${response.statusText}`);
2155
+ }
2156
+ } catch (error) {
2157
+ this._handleError(error, callbacks);
2158
+ } finally {
2159
+ callbacks.onPaymentProcessing?.(false);
2160
+ }
2161
+ }
2162
+ /**
2163
+ * Make the API request with failover support
2164
+ */
2165
+ async _makeRequest(params) {
2166
+ const { path, method, body, baseUrl, token, headers } = params;
2167
+ try {
2168
+ const url = `${baseUrl.replace(/\/$/, "")}${path}`;
2169
+ const response = await fetch(url, {
2170
+ method,
2171
+ headers,
2172
+ body: body === void 0 || method === "GET" ? void 0 : JSON.stringify(body)
2173
+ });
2174
+ response.baseUrl = baseUrl;
2175
+ if (!response.ok) {
2176
+ return await this._handleErrorResponse(response, params, token);
2177
+ }
2178
+ return response;
2179
+ } catch (error) {
2180
+ if (this._isNetworkError(error?.message || "")) {
2181
+ return await this._handleNetworkError(error, params);
2182
+ }
2183
+ throw error;
2184
+ }
2185
+ }
2186
+ /**
2187
+ * Handle error responses with failover
2188
+ */
2189
+ async _handleErrorResponse(response, params, token) {
2190
+ const { path, method, body, selectedModel, baseUrl, mintUrl } = params;
2191
+ const status = response.status;
2192
+ if (this.mode === "apikeys") {
2193
+ console.log("error ;", status);
2194
+ if (status === 401 || status === 403) {
2195
+ const parentApiKey = this.storageAdapter.getApiKey(baseUrl);
2196
+ if (parentApiKey) {
2197
+ try {
2198
+ const childKeyResult = await this._createChildKey(
2199
+ baseUrl,
2200
+ parentApiKey
2201
+ );
2202
+ this.storageAdapter.setChildKey(
2203
+ baseUrl,
2204
+ childKeyResult.childKey,
2205
+ childKeyResult.balance,
2206
+ childKeyResult.validityDate,
2207
+ childKeyResult.balanceLimit
2208
+ );
2209
+ return this._makeRequest({
2210
+ ...params,
2211
+ token: childKeyResult.childKey,
2212
+ headers: this._withAuthHeader(
2213
+ params.baseHeaders,
2214
+ childKeyResult.childKey
2215
+ )
2216
+ });
2217
+ } catch (e) {
2218
+ }
2219
+ }
2220
+ } else if (status === 402) {
2221
+ const parentApiKey = this.storageAdapter.getApiKey(baseUrl);
2222
+ if (parentApiKey) {
2223
+ const topupResult = await this.balanceManager.topUp({
2224
+ mintUrl,
2225
+ baseUrl,
2226
+ amount: params.requiredSats * 3,
2227
+ token: parentApiKey
2228
+ });
2229
+ console.log(topupResult);
2230
+ return this._makeRequest({
2231
+ ...params,
2232
+ token: params.token,
2233
+ headers: this._withAuthHeader(params.baseHeaders, params.token)
2234
+ });
2235
+ }
2236
+ }
2237
+ throw new ProviderError(baseUrl, status, await response.text());
2238
+ }
2239
+ await this.balanceManager.refund({
2240
+ mintUrl,
2241
+ baseUrl,
2242
+ token
2243
+ });
2244
+ this.providerManager.markFailed(baseUrl);
2245
+ this.storageAdapter.removeToken(baseUrl);
2246
+ if (status === 401 || status === 403 || status === 402 || status === 413 || status === 400 || status === 500 || status === 502) {
2247
+ if (!selectedModel) {
2248
+ throw new ProviderError(baseUrl, status, await response.text());
2249
+ }
2250
+ const nextProvider = this.providerManager.findNextBestProvider(
2251
+ selectedModel.id,
2252
+ baseUrl
2253
+ );
2254
+ if (nextProvider) {
2255
+ const newModel = await this.providerManager.getModelForProvider(
2256
+ nextProvider,
2257
+ selectedModel.id
2258
+ ) ?? selectedModel;
2259
+ const messagesForPricing = Array.isArray(
2260
+ body?.messages
2261
+ ) ? body.messages : [];
2262
+ const newRequiredSats = this.providerManager.getRequiredSatsForModel(
2263
+ newModel,
2264
+ messagesForPricing,
2265
+ params.maxTokens
2266
+ );
2267
+ const spendResult = await this.cashuSpender.spend({
2268
+ mintUrl,
2269
+ amount: newRequiredSats,
2270
+ baseUrl: nextProvider,
2271
+ reuseToken: true
2272
+ });
2273
+ if (spendResult.status === "failed" || !spendResult.token) {
2274
+ if (spendResult.errorDetails) {
2275
+ throw new InsufficientBalanceError(
2276
+ spendResult.errorDetails.required,
2277
+ spendResult.errorDetails.available,
2278
+ spendResult.errorDetails.maxMintBalance,
2279
+ spendResult.errorDetails.maxMintUrl
2280
+ );
2281
+ }
2282
+ throw new Error(
2283
+ spendResult.error || `Insufficient balance for ${nextProvider}`
2284
+ );
2285
+ }
2286
+ return this._makeRequest({
2287
+ ...params,
2288
+ path,
2289
+ method,
2290
+ body,
2291
+ baseUrl: nextProvider,
2292
+ selectedModel: newModel,
2293
+ token: spendResult.token,
2294
+ requiredSats: newRequiredSats,
2295
+ headers: this._withAuthHeader(params.baseHeaders, spendResult.token)
2296
+ });
2297
+ }
2298
+ }
2299
+ throw new ProviderError(baseUrl, status, await response.text());
2300
+ }
2301
+ /**
2302
+ * Handle network errors with failover
2303
+ */
2304
+ async _handleNetworkError(error, params) {
2305
+ const { path, method, body, selectedModel, baseUrl, mintUrl } = params;
2306
+ await this.balanceManager.refund({
2307
+ mintUrl,
2308
+ baseUrl,
2309
+ token: params.token
2310
+ });
2311
+ this.providerManager.markFailed(baseUrl);
2312
+ if (!selectedModel) {
2313
+ throw error;
2314
+ }
2315
+ const nextProvider = this.providerManager.findNextBestProvider(
2316
+ selectedModel.id,
2317
+ baseUrl
2318
+ );
2319
+ if (!nextProvider) {
2320
+ throw new FailoverError(baseUrl, Array.from(this.providerManager));
2321
+ }
2322
+ const newModel = await this.providerManager.getModelForProvider(
2323
+ nextProvider,
2324
+ selectedModel.id
2325
+ ) ?? selectedModel;
2326
+ const messagesForPricing = Array.isArray(
2327
+ body?.messages
2328
+ ) ? body.messages : [];
2329
+ const newRequiredSats = this.providerManager.getRequiredSatsForModel(
2330
+ newModel,
2331
+ messagesForPricing,
2332
+ params.maxTokens
2333
+ );
2334
+ const spendResult = await this.cashuSpender.spend({
2335
+ mintUrl,
2336
+ amount: newRequiredSats,
2337
+ baseUrl: nextProvider,
2338
+ reuseToken: true
2339
+ });
2340
+ if (spendResult.status === "failed" || !spendResult.token) {
2341
+ if (spendResult.errorDetails) {
2342
+ throw new InsufficientBalanceError(
2343
+ spendResult.errorDetails.required,
2344
+ spendResult.errorDetails.available,
2345
+ spendResult.errorDetails.maxMintBalance,
2346
+ spendResult.errorDetails.maxMintUrl
2347
+ );
2348
+ }
2349
+ throw new Error(
2350
+ spendResult.error || `Insufficient balance for ${nextProvider}`
2351
+ );
2352
+ }
2353
+ return this._makeRequest({
2354
+ ...params,
2355
+ path,
2356
+ method,
2357
+ body,
2358
+ baseUrl: nextProvider,
2359
+ selectedModel: newModel,
2360
+ token: spendResult.token,
2361
+ requiredSats: newRequiredSats,
2362
+ headers: this._withAuthHeader(params.baseHeaders, spendResult.token)
2363
+ });
2364
+ }
2365
+ /**
2366
+ * Handle post-response refund and balance updates
2367
+ */
2368
+ async _handlePostResponseRefund(params) {
2369
+ const {
2370
+ mintUrl,
2371
+ baseUrl,
2372
+ tokenBalance,
2373
+ tokenBalanceUnit,
2374
+ initialBalance,
2375
+ selectedModel,
2376
+ streamingResult,
2377
+ callbacks
2378
+ } = params;
2379
+ const tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
2380
+ const estimatedCosts = this._getEstimatedCosts(
2381
+ selectedModel,
2382
+ streamingResult
2383
+ );
2384
+ const refundResult = await this.balanceManager.refund({
2385
+ mintUrl,
2386
+ baseUrl
2387
+ });
2388
+ if (refundResult.success) {
2389
+ refundResult.refundedAmount !== void 0 ? refundResult.refundedAmount / 1e3 : 0;
2390
+ }
2391
+ let satsSpent;
2392
+ if (refundResult.success) {
2393
+ if (refundResult.refundedAmount !== void 0) {
2394
+ satsSpent = tokenBalanceInSats - refundResult.refundedAmount / 1e3;
2395
+ } else if (refundResult.message?.includes("No API key to refund")) {
2396
+ satsSpent = 0;
2397
+ } else {
2398
+ satsSpent = tokenBalanceInSats;
2399
+ }
2400
+ const newBalance = initialBalance - satsSpent;
2401
+ callbacks.onBalanceUpdate(newBalance);
2402
+ } else {
2403
+ if (refundResult.message?.includes("Refund request failed with status 401")) {
2404
+ this.storageAdapter.removeToken(baseUrl);
2405
+ }
2406
+ satsSpent = tokenBalanceInSats;
2407
+ }
2408
+ const netCosts = satsSpent - estimatedCosts;
2409
+ const overchargeThreshold = tokenBalanceUnit === "msat" ? 0.05 : 1;
2410
+ if (netCosts > overchargeThreshold) {
2411
+ if (this.alertLevel === "max") {
2412
+ callbacks.onMessageAppend({
2413
+ role: "system",
2414
+ content: `ATTENTION: Provider may be overcharging. Estimated: ${estimatedCosts.toFixed(
2415
+ tokenBalanceUnit === "msat" ? 3 : 0
2416
+ )}, Actual: ${satsSpent.toFixed(
2417
+ tokenBalanceUnit === "msat" ? 3 : 0
2418
+ )}`
2419
+ });
2420
+ }
2421
+ }
2422
+ const newTransaction = {
2423
+ type: "spent",
2424
+ amount: satsSpent,
2425
+ timestamp: Date.now(),
2426
+ status: "success",
2427
+ model: selectedModel.id,
2428
+ message: "Tokens spent",
2429
+ balance: initialBalance - satsSpent
2430
+ };
2431
+ callbacks.onTransactionUpdate(newTransaction);
2432
+ return satsSpent;
2433
+ }
2434
+ /**
2435
+ * Handle post-response balance update for all modes
2436
+ */
2437
+ async _handlePostResponseBalanceUpdate(params) {
2438
+ const { token, baseUrl, initialTokenBalance, fallbackSatsSpent, response } = params;
2439
+ let satsSpent = initialTokenBalance;
2440
+ if (this.mode === "xcashu" && response) {
2441
+ const refundToken = response.headers.get("x-cashu") ?? void 0;
2442
+ if (refundToken) {
2443
+ try {
2444
+ const receiveResult = await this.walletAdapter.receiveToken(refundToken);
2445
+ satsSpent = initialTokenBalance - receiveResult.amount * (receiveResult.unit == "sat" ? 1 : 1e3);
2446
+ } catch (error) {
2447
+ console.error("[xcashu] Failed to receive refund token:", error);
2448
+ }
2449
+ }
2450
+ } else if (this.mode === "lazyrefund") {
2451
+ const latestBalanceInfo = await this.balanceManager.getTokenBalance(
2452
+ token,
2453
+ baseUrl
2454
+ );
2455
+ const latestTokenBalance = latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
2456
+ this.storageAdapter.updateTokenBalance(baseUrl, latestTokenBalance);
2457
+ satsSpent = initialTokenBalance - latestTokenBalance;
2458
+ } else if (this.mode === "apikeys") {
2459
+ try {
2460
+ const latestBalanceInfo = await this._getApiKeyBalance(baseUrl, token);
2461
+ console.log("LATEST BANAL", latestBalanceInfo);
2462
+ const latestTokenBalance = latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
2463
+ this.storageAdapter.updateChildKeyBalance(baseUrl, latestTokenBalance);
2464
+ satsSpent = initialTokenBalance - latestTokenBalance;
2465
+ } catch (e) {
2466
+ console.warn("Could not get updated API key balance:", e);
2467
+ satsSpent = fallbackSatsSpent ?? initialTokenBalance;
2468
+ }
2469
+ }
2470
+ return satsSpent;
2471
+ }
2472
+ /**
2473
+ * Convert messages for API format
2474
+ */
2475
+ async _convertMessages(messages) {
2476
+ return Promise.all(
2477
+ messages.filter((m) => m.role !== "system").map(async (m) => ({
2478
+ role: m.role,
2479
+ content: typeof m.content === "string" ? m.content : m.content
2480
+ }))
2481
+ );
2482
+ }
2483
+ /**
2484
+ * Create assistant message from streaming result
2485
+ */
2486
+ async _createAssistantMessage(result) {
2487
+ if (result.images && result.images.length > 0) {
2488
+ const content = [];
2489
+ if (result.content) {
2490
+ content.push({
2491
+ type: "text",
2492
+ text: result.content,
2493
+ thinking: result.thinking,
2494
+ citations: result.citations,
2495
+ annotations: result.annotations
2496
+ });
2497
+ }
2498
+ for (const img of result.images) {
2499
+ content.push({
2500
+ type: "image_url",
2501
+ image_url: {
2502
+ url: img.image_url.url
2503
+ }
2504
+ });
2505
+ }
2506
+ return {
2507
+ role: "assistant",
2508
+ content
2509
+ };
2510
+ }
2511
+ return {
2512
+ role: "assistant",
2513
+ content: result.content || ""
2514
+ };
2515
+ }
2516
+ /**
2517
+ * Create a child key for a parent API key via the provider's API
2518
+ * POST /v1/balance/child-key
2519
+ */
2520
+ async _createChildKey(baseUrl, parentApiKey, options) {
2521
+ const response = await fetch(`${baseUrl}v1/balance/child-key`, {
2522
+ method: "POST",
2523
+ headers: {
2524
+ "Content-Type": "application/json",
2525
+ Authorization: `Bearer ${parentApiKey}`
2526
+ },
2527
+ body: JSON.stringify({
2528
+ count: options?.count ?? 1,
2529
+ balance_limit: options?.balanceLimit,
2530
+ balance_limit_reset: options?.balanceLimitReset,
2531
+ validity_date: options?.validityDate
2532
+ })
2533
+ });
2534
+ if (!response.ok) {
2535
+ throw new Error(
2536
+ `Failed to create child key: ${response.status} ${await response.text()}`
2537
+ );
2538
+ }
2539
+ const data = await response.json();
2540
+ return {
2541
+ childKey: data.api_keys?.[0],
2542
+ balance: data.balance ?? 0,
2543
+ balanceLimit: data.balance_limit,
2544
+ validityDate: data.validity_date
2545
+ };
2546
+ }
2547
+ /**
2548
+ * Get balance for an API key from the provider
2549
+ */
2550
+ async _getApiKeyBalance(baseUrl, apiKey) {
2551
+ try {
2552
+ const response = await fetch(`${baseUrl}v1/wallet/info`, {
2553
+ headers: {
2554
+ Authorization: `Bearer ${apiKey}`
2555
+ }
2556
+ });
2557
+ if (response.ok) {
2558
+ const data = await response.json();
2559
+ console.log(data);
2560
+ return {
2561
+ amount: data.balance,
2562
+ unit: "msat"
2563
+ };
2564
+ }
2565
+ } catch {
2566
+ }
2567
+ return { amount: 0, unit: "sat" };
2568
+ }
2569
+ /**
2570
+ * Calculate estimated costs from usage
2571
+ */
2572
+ _getEstimatedCosts(selectedModel, streamingResult) {
2573
+ let estimatedCosts = 0;
2574
+ if (streamingResult.usage) {
2575
+ const { completion_tokens, prompt_tokens } = streamingResult.usage;
2576
+ if (completion_tokens !== void 0 && prompt_tokens !== void 0) {
2577
+ estimatedCosts = (selectedModel.sats_pricing?.completion ?? 0) * completion_tokens + (selectedModel.sats_pricing?.prompt ?? 0) * prompt_tokens;
2578
+ }
2579
+ }
2580
+ return estimatedCosts;
2581
+ }
2582
+ /**
2583
+ * Get pending cashu token amount
2584
+ */
2585
+ _getPendingCashuTokenAmount() {
2586
+ const distribution = this.storageAdapter.getPendingTokenDistribution();
2587
+ return distribution.reduce((total, item) => total + item.amount, 0);
2588
+ }
2589
+ /**
2590
+ * Check if error message indicates a network error
2591
+ */
2592
+ _isNetworkError(message) {
2593
+ return message.includes("NetworkError when attempting to fetch resource") || message.includes("Failed to fetch") || message.includes("Load failed");
2594
+ }
2595
+ /**
2596
+ * Handle errors and notify callbacks
2597
+ */
2598
+ _handleError(error, callbacks) {
2599
+ console.error("RoutstrClient error:", error);
2600
+ if (error instanceof Error) {
2601
+ const modifiedErrorMsg = error.message.includes("Error in input stream") || error.message.includes("Load failed") ? "AI stream was cut off, turn on Keep Active or please try again" : error.message;
2602
+ callbacks.onMessageAppend({
2603
+ role: "system",
2604
+ content: "Uncaught Error: " + modifiedErrorMsg + (this.alertLevel === "max" ? " | " + error.stack : "")
2605
+ });
2606
+ } else {
2607
+ callbacks.onMessageAppend({
2608
+ role: "system",
2609
+ content: "Unknown Error: Please tag Routstr on Nostr and/or retry."
2610
+ });
2611
+ }
2612
+ }
2613
+ /**
2614
+ * Check wallet balance and throw if insufficient
2615
+ */
2616
+ async _checkBalance() {
2617
+ const balances = await this.walletAdapter.getBalances();
2618
+ const totalBalance = Object.values(balances).reduce((sum, v) => sum + v, 0);
2619
+ if (totalBalance <= 0) {
2620
+ throw new InsufficientBalanceError(1, 0);
2621
+ }
2622
+ }
2623
+ /**
2624
+ * Spend a token using CashuSpender with standardized error handling
2625
+ */
2626
+ async _spendToken(params) {
2627
+ const { mintUrl, amount, baseUrl } = params;
2628
+ if (this.mode === "apikeys") {
2629
+ let parentApiKey = this.storageAdapter.getApiKey(baseUrl);
2630
+ if (!parentApiKey) {
2631
+ const spendResult2 = await this.cashuSpender.spend({
2632
+ mintUrl,
2633
+ amount: amount * 3,
2634
+ baseUrl: "",
2635
+ reuseToken: false
2636
+ });
2637
+ if (spendResult2.status === "failed" || !spendResult2.token) {
2638
+ const errorMsg = spendResult2.error || `Insufficient balance. Need ${amount} sats.`;
2639
+ if (this._isNetworkError(errorMsg)) {
2640
+ throw new Error(
2641
+ `Your mint ${mintUrl} is unreachable or is blocking your IP. Please try again later or switch mints.`
2642
+ );
2643
+ }
2644
+ if (spendResult2.errorDetails) {
2645
+ throw new InsufficientBalanceError(
2646
+ spendResult2.errorDetails.required,
2647
+ spendResult2.errorDetails.available,
2648
+ spendResult2.errorDetails.maxMintBalance,
2649
+ spendResult2.errorDetails.maxMintUrl
2650
+ );
2651
+ }
2652
+ throw new Error(errorMsg);
2653
+ }
2654
+ const apiKeyCreated = await this.balanceManager.getTokenBalance(
2655
+ spendResult2.token,
2656
+ baseUrl
2657
+ );
2658
+ parentApiKey = apiKeyCreated.apiKey;
2659
+ this.storageAdapter.setApiKey(baseUrl, parentApiKey);
2660
+ }
2661
+ let childKeyEntry = this.storageAdapter.getChildKey(baseUrl);
2662
+ if (!childKeyEntry) {
2663
+ try {
2664
+ const childKeyResult = await this._createChildKey(
2665
+ baseUrl,
2666
+ parentApiKey
2667
+ );
2668
+ this.storageAdapter.setChildKey(
2669
+ baseUrl,
2670
+ childKeyResult.childKey,
2671
+ childKeyResult.balance,
2672
+ childKeyResult.validityDate,
2673
+ childKeyResult.balanceLimit
2674
+ );
2675
+ childKeyEntry = {
2676
+ parentBaseUrl: baseUrl,
2677
+ childKey: childKeyResult.childKey,
2678
+ balance: childKeyResult.balance,
2679
+ balanceLimit: childKeyResult.balanceLimit,
2680
+ validityDate: childKeyResult.validityDate,
2681
+ createdAt: Date.now()
2682
+ };
2683
+ } catch (e) {
2684
+ console.warn("Could not create child key, using parent key:", e);
2685
+ childKeyEntry = {
2686
+ parentBaseUrl: baseUrl,
2687
+ childKey: parentApiKey,
2688
+ balance: 0,
2689
+ createdAt: Date.now()
2690
+ };
2691
+ }
2692
+ }
2693
+ let tokenBalance = childKeyEntry.balance;
2694
+ let tokenBalanceUnit = "sat";
2695
+ if (tokenBalance === 0) {
2696
+ try {
2697
+ const balanceInfo = await this._getApiKeyBalance(
2698
+ baseUrl,
2699
+ childKeyEntry.childKey
2700
+ );
2701
+ tokenBalance = balanceInfo.amount;
2702
+ tokenBalanceUnit = balanceInfo.unit;
2703
+ } catch (e) {
2704
+ console.warn("Could not get initial API key balance:", e);
2705
+ }
2706
+ }
2707
+ return {
2708
+ token: childKeyEntry.childKey,
2709
+ tokenBalance,
2710
+ tokenBalanceUnit
2711
+ };
2712
+ }
2713
+ const spendResult = await this.cashuSpender.spend({
2714
+ mintUrl,
2715
+ amount,
2716
+ baseUrl,
2717
+ reuseToken: true
2718
+ });
2719
+ if (spendResult.status === "failed" || !spendResult.token) {
2720
+ const errorMsg = spendResult.error || `Insufficient balance. Need ${amount} sats.`;
2721
+ if (this._isNetworkError(errorMsg)) {
2722
+ throw new Error(
2723
+ `Your mint ${mintUrl} is unreachable or is blocking your IP. Please try again later or switch mints.`
2724
+ );
2725
+ }
2726
+ if (spendResult.errorDetails) {
2727
+ throw new InsufficientBalanceError(
2728
+ spendResult.errorDetails.required,
2729
+ spendResult.errorDetails.available,
2730
+ spendResult.errorDetails.maxMintBalance,
2731
+ spendResult.errorDetails.maxMintUrl
2732
+ );
2733
+ }
2734
+ throw new Error(errorMsg);
2735
+ }
2736
+ return {
2737
+ token: spendResult.token,
2738
+ tokenBalance: spendResult.balance,
2739
+ tokenBalanceUnit: spendResult.unit ?? "sat"
2740
+ };
2741
+ }
2742
+ /**
2743
+ * Build request headers with common defaults and dev mock controls
2744
+ */
2745
+ _buildBaseHeaders(additionalHeaders = {}, token) {
2746
+ const headers = {
2747
+ ...additionalHeaders,
2748
+ "Content-Type": "application/json"
2749
+ };
2750
+ return headers;
2751
+ }
2752
+ /**
2753
+ * Attach auth headers using the active client mode
2754
+ */
2755
+ _withAuthHeader(headers, token) {
2756
+ const nextHeaders = { ...headers };
2757
+ if (this.mode === "xcashu") {
2758
+ nextHeaders["X-Cashu"] = token;
2759
+ } else {
2760
+ nextHeaders["Authorization"] = `Bearer ${token}`;
2761
+ }
2762
+ return nextHeaders;
2763
+ }
2764
+ };
2765
+
2766
+ // storage/drivers/localStorage.ts
2767
+ var canUseLocalStorage = () => {
2768
+ return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
2769
+ };
2770
+ var isQuotaExceeded = (error) => {
2771
+ const e = error;
2772
+ return !!e && (e?.name === "QuotaExceededError" || e?.code === 22 || e?.code === 1014);
2773
+ };
2774
+ var NON_CRITICAL_KEYS = /* @__PURE__ */ new Set(["modelsFromAllProviders"]);
2775
+ var localStorageDriver = {
2776
+ getItem(key, defaultValue) {
2777
+ if (!canUseLocalStorage()) return defaultValue;
2778
+ try {
2779
+ const item = window.localStorage.getItem(key);
2780
+ if (item === null) return defaultValue;
2781
+ try {
2782
+ return JSON.parse(item);
2783
+ } catch (parseError) {
2784
+ if (typeof defaultValue === "string") {
2785
+ return item;
2786
+ }
2787
+ throw parseError;
2788
+ }
2789
+ } catch (error) {
2790
+ console.error(`Error retrieving item with key "${key}":`, error);
2791
+ if (canUseLocalStorage()) {
2792
+ try {
2793
+ window.localStorage.removeItem(key);
2794
+ } catch (removeError) {
2795
+ console.error(
2796
+ `Error removing corrupted item with key "${key}":`,
2797
+ removeError
2798
+ );
2799
+ }
2800
+ }
2801
+ return defaultValue;
2802
+ }
2803
+ },
2804
+ setItem(key, value) {
2805
+ if (!canUseLocalStorage()) return;
2806
+ try {
2807
+ window.localStorage.setItem(key, JSON.stringify(value));
2808
+ } catch (error) {
2809
+ if (isQuotaExceeded(error)) {
2810
+ if (NON_CRITICAL_KEYS.has(key)) {
2811
+ console.warn(
2812
+ `Storage quota exceeded; skipping non-critical key "${key}".`
2813
+ );
2814
+ return;
2815
+ }
2816
+ try {
2817
+ window.localStorage.removeItem("modelsFromAllProviders");
2818
+ } catch {
2819
+ }
2820
+ try {
2821
+ window.localStorage.setItem(key, JSON.stringify(value));
2822
+ return;
2823
+ } catch (retryError) {
2824
+ console.warn(
2825
+ `Storage quota exceeded; unable to persist key "${key}" after cleanup attempt.`,
2826
+ retryError
2827
+ );
2828
+ return;
2829
+ }
2830
+ }
2831
+ console.error(`Error storing item with key "${key}":`, error);
2832
+ }
2833
+ },
2834
+ removeItem(key) {
2835
+ if (!canUseLocalStorage()) return;
2836
+ try {
2837
+ window.localStorage.removeItem(key);
2838
+ } catch (error) {
2839
+ console.error(`Error removing item with key "${key}":`, error);
2840
+ }
2841
+ }
2842
+ };
2843
+
2844
+ // storage/drivers/memory.ts
2845
+ var createMemoryDriver = (seed) => {
2846
+ const store = /* @__PURE__ */ new Map();
2847
+ if (seed) {
2848
+ for (const [key, value] of Object.entries(seed)) {
2849
+ store.set(key, value);
2850
+ }
2851
+ }
2852
+ return {
2853
+ getItem(key, defaultValue) {
2854
+ const item = store.get(key);
2855
+ if (item === void 0) return defaultValue;
2856
+ try {
2857
+ return JSON.parse(item);
2858
+ } catch (parseError) {
2859
+ if (typeof defaultValue === "string") {
2860
+ return item;
2861
+ }
2862
+ throw parseError;
2863
+ }
2864
+ },
2865
+ setItem(key, value) {
2866
+ store.set(key, JSON.stringify(value));
2867
+ },
2868
+ removeItem(key) {
2869
+ store.delete(key);
2870
+ }
2871
+ };
2872
+ };
2873
+
2874
+ // storage/drivers/sqlite.ts
2875
+ var createDatabase = (dbPath) => {
2876
+ let Database = null;
2877
+ try {
2878
+ Database = __require("better-sqlite3");
2879
+ } catch (error) {
2880
+ throw new Error(
2881
+ `better-sqlite3 is required for sqlite storage. Install it to use sqlite storage. (${error})`
2882
+ );
2883
+ }
2884
+ return new Database(dbPath);
2885
+ };
2886
+ var createSqliteDriver = (options = {}) => {
2887
+ const dbPath = options.dbPath || "routstr.sqlite";
2888
+ const tableName = options.tableName || "sdk_storage";
2889
+ const db = createDatabase(dbPath);
2890
+ db.exec(
2891
+ `CREATE TABLE IF NOT EXISTS ${tableName} (key TEXT PRIMARY KEY, value TEXT NOT NULL)`
2892
+ );
2893
+ const selectStmt = db.prepare(`SELECT value FROM ${tableName} WHERE key = ?`);
2894
+ const upsertStmt = db.prepare(
2895
+ `INSERT INTO ${tableName} (key, value) VALUES (?, ?)
2896
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`
2897
+ );
2898
+ const deleteStmt = db.prepare(`DELETE FROM ${tableName} WHERE key = ?`);
2899
+ return {
2900
+ getItem(key, defaultValue) {
2901
+ try {
2902
+ const row = selectStmt.get(key);
2903
+ if (!row || typeof row.value !== "string") return defaultValue;
2904
+ try {
2905
+ return JSON.parse(row.value);
2906
+ } catch (parseError) {
2907
+ if (typeof defaultValue === "string") {
2908
+ return row.value;
2909
+ }
2910
+ throw parseError;
2911
+ }
2912
+ } catch (error) {
2913
+ console.error(`SQLite getItem failed for key "${key}":`, error);
2914
+ return defaultValue;
2915
+ }
2916
+ },
2917
+ setItem(key, value) {
2918
+ try {
2919
+ upsertStmt.run(key, JSON.stringify(value));
2920
+ } catch (error) {
2921
+ console.error(`SQLite setItem failed for key "${key}":`, error);
2922
+ }
2923
+ },
2924
+ removeItem(key) {
2925
+ try {
2926
+ deleteStmt.run(key);
2927
+ } catch (error) {
2928
+ console.error(`SQLite removeItem failed for key "${key}":`, error);
2929
+ }
2930
+ }
2931
+ };
2932
+ };
2933
+
2934
+ // storage/keys.ts
2935
+ var SDK_STORAGE_KEYS = {
2936
+ MODELS_FROM_ALL_PROVIDERS: "modelsFromAllProviders",
2937
+ LAST_USED_MODEL: "lastUsedModel",
2938
+ BASE_URLS_LIST: "base_urls_list",
2939
+ DISABLED_PROVIDERS: "disabled_providers",
2940
+ MINTS_FROM_ALL_PROVIDERS: "mints_from_all_providers",
2941
+ INFO_FROM_ALL_PROVIDERS: "info_from_all_providers",
2942
+ LAST_MODELS_UPDATE: "lastModelsUpdate",
2943
+ LAST_BASE_URLS_UPDATE: "lastBaseUrlsUpdate",
2944
+ LOCAL_CASHU_TOKENS: "local_cashu_tokens",
2945
+ API_KEYS: "api_keys",
2946
+ CHILD_KEYS: "child_keys"
2947
+ };
2948
+
2949
+ // storage/store.ts
2950
+ var normalizeBaseUrl = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
2951
+ var getTokenBalance = (token) => {
2952
+ try {
2953
+ const decoded = cashuTs.getDecodedToken(token);
2954
+ const unitDivisor = decoded.unit === "msat" ? 1e3 : 1;
2955
+ let sum = 0;
2956
+ for (const proof of decoded.proofs) {
2957
+ sum += proof.amount / unitDivisor;
2958
+ }
2959
+ return sum;
2960
+ } catch {
2961
+ return 0;
2962
+ }
2963
+ };
2964
+ var createSdkStore = ({ driver }) => {
2965
+ return vanilla.createStore((set, get) => ({
2966
+ modelsFromAllProviders: Object.fromEntries(
2967
+ Object.entries(
2968
+ driver.getItem(
2969
+ SDK_STORAGE_KEYS.MODELS_FROM_ALL_PROVIDERS,
2970
+ {}
2971
+ )
2972
+ ).map(([baseUrl, models]) => [normalizeBaseUrl(baseUrl), models])
2973
+ ),
2974
+ lastUsedModel: driver.getItem(
2975
+ SDK_STORAGE_KEYS.LAST_USED_MODEL,
2976
+ null
2977
+ ),
2978
+ baseUrlsList: driver.getItem(SDK_STORAGE_KEYS.BASE_URLS_LIST, []).map((url) => normalizeBaseUrl(url)),
2979
+ lastBaseUrlsUpdate: driver.getItem(
2980
+ SDK_STORAGE_KEYS.LAST_BASE_URLS_UPDATE,
2981
+ null
2982
+ ),
2983
+ disabledProviders: driver.getItem(SDK_STORAGE_KEYS.DISABLED_PROVIDERS, []).map((url) => normalizeBaseUrl(url)),
2984
+ mintsFromAllProviders: Object.fromEntries(
2985
+ Object.entries(
2986
+ driver.getItem(
2987
+ SDK_STORAGE_KEYS.MINTS_FROM_ALL_PROVIDERS,
2988
+ {}
2989
+ )
2990
+ ).map(([baseUrl, mints]) => [
2991
+ normalizeBaseUrl(baseUrl),
2992
+ mints.map((mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint)
2993
+ ])
2994
+ ),
2995
+ infoFromAllProviders: Object.fromEntries(
2996
+ Object.entries(
2997
+ driver.getItem(
2998
+ SDK_STORAGE_KEYS.INFO_FROM_ALL_PROVIDERS,
2999
+ {}
3000
+ )
3001
+ ).map(([baseUrl, info]) => [normalizeBaseUrl(baseUrl), info])
3002
+ ),
3003
+ lastModelsUpdate: Object.fromEntries(
3004
+ Object.entries(
3005
+ driver.getItem(
3006
+ SDK_STORAGE_KEYS.LAST_MODELS_UPDATE,
3007
+ {}
3008
+ )
3009
+ ).map(([baseUrl, timestamp]) => [normalizeBaseUrl(baseUrl), timestamp])
3010
+ ),
3011
+ cachedTokens: driver.getItem(SDK_STORAGE_KEYS.LOCAL_CASHU_TOKENS, []).map((entry) => ({
3012
+ ...entry,
3013
+ baseUrl: normalizeBaseUrl(entry.baseUrl),
3014
+ balance: typeof entry.balance === "number" ? entry.balance : getTokenBalance(entry.token),
3015
+ lastUsed: entry.lastUsed ?? null
3016
+ })),
3017
+ apiKeys: driver.getItem(SDK_STORAGE_KEYS.API_KEYS, []).map((entry) => ({
3018
+ ...entry,
3019
+ baseUrl: normalizeBaseUrl(entry.baseUrl),
3020
+ balance: entry.balance ?? 0,
3021
+ lastUsed: entry.lastUsed ?? null
3022
+ })),
3023
+ childKeys: driver.getItem(SDK_STORAGE_KEYS.CHILD_KEYS, []).map((entry) => ({
3024
+ parentBaseUrl: normalizeBaseUrl(entry.parentBaseUrl),
3025
+ childKey: entry.childKey,
3026
+ balance: entry.balance ?? 0,
3027
+ balanceLimit: entry.balanceLimit,
3028
+ validityDate: entry.validityDate,
3029
+ createdAt: entry.createdAt ?? Date.now()
3030
+ })),
3031
+ setModelsFromAllProviders: (value) => {
3032
+ const normalized = {};
3033
+ for (const [baseUrl, models] of Object.entries(value)) {
3034
+ normalized[normalizeBaseUrl(baseUrl)] = models;
3035
+ }
3036
+ driver.setItem(SDK_STORAGE_KEYS.MODELS_FROM_ALL_PROVIDERS, normalized);
3037
+ set({ modelsFromAllProviders: normalized });
3038
+ },
3039
+ setLastUsedModel: (value) => {
3040
+ driver.setItem(SDK_STORAGE_KEYS.LAST_USED_MODEL, value);
3041
+ set({ lastUsedModel: value });
3042
+ },
3043
+ setBaseUrlsList: (value) => {
3044
+ const normalized = value.map((url) => normalizeBaseUrl(url));
3045
+ driver.setItem(SDK_STORAGE_KEYS.BASE_URLS_LIST, normalized);
3046
+ set({ baseUrlsList: normalized });
3047
+ },
3048
+ setBaseUrlsLastUpdate: (value) => {
3049
+ driver.setItem(SDK_STORAGE_KEYS.LAST_BASE_URLS_UPDATE, value);
3050
+ set({ lastBaseUrlsUpdate: value });
3051
+ },
3052
+ setDisabledProviders: (value) => {
3053
+ const normalized = value.map((url) => normalizeBaseUrl(url));
3054
+ driver.setItem(SDK_STORAGE_KEYS.DISABLED_PROVIDERS, normalized);
3055
+ set({ disabledProviders: normalized });
3056
+ },
3057
+ setMintsFromAllProviders: (value) => {
3058
+ const normalized = {};
3059
+ for (const [baseUrl, mints] of Object.entries(value)) {
3060
+ normalized[normalizeBaseUrl(baseUrl)] = mints.map(
3061
+ (mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint
3062
+ );
3063
+ }
3064
+ driver.setItem(SDK_STORAGE_KEYS.MINTS_FROM_ALL_PROVIDERS, normalized);
3065
+ set({ mintsFromAllProviders: normalized });
3066
+ },
3067
+ setInfoFromAllProviders: (value) => {
3068
+ const normalized = {};
3069
+ for (const [baseUrl, info] of Object.entries(value)) {
3070
+ normalized[normalizeBaseUrl(baseUrl)] = info;
3071
+ }
3072
+ driver.setItem(SDK_STORAGE_KEYS.INFO_FROM_ALL_PROVIDERS, normalized);
3073
+ set({ infoFromAllProviders: normalized });
3074
+ },
3075
+ setLastModelsUpdate: (value) => {
3076
+ const normalized = {};
3077
+ for (const [baseUrl, timestamp] of Object.entries(value)) {
3078
+ normalized[normalizeBaseUrl(baseUrl)] = timestamp;
3079
+ }
3080
+ driver.setItem(SDK_STORAGE_KEYS.LAST_MODELS_UPDATE, normalized);
3081
+ set({ lastModelsUpdate: normalized });
3082
+ },
3083
+ setCachedTokens: (value) => {
3084
+ set((state) => {
3085
+ const updates = typeof value === "function" ? value(state.cachedTokens) : value;
3086
+ const normalized = updates.map((entry) => ({
3087
+ ...entry,
3088
+ baseUrl: normalizeBaseUrl(entry.baseUrl),
3089
+ balance: typeof entry.balance === "number" ? entry.balance : getTokenBalance(entry.token),
3090
+ lastUsed: entry.lastUsed ?? null
3091
+ }));
3092
+ driver.setItem(SDK_STORAGE_KEYS.LOCAL_CASHU_TOKENS, normalized);
3093
+ return { cachedTokens: normalized };
3094
+ });
3095
+ },
3096
+ setApiKeys: (value) => {
3097
+ set((state) => {
3098
+ const updates = typeof value === "function" ? value(state.apiKeys) : value;
3099
+ const normalized = updates.map((entry) => ({
3100
+ ...entry,
3101
+ baseUrl: normalizeBaseUrl(entry.baseUrl),
3102
+ balance: entry.balance ?? 0,
3103
+ lastUsed: entry.lastUsed ?? null
3104
+ }));
3105
+ driver.setItem(SDK_STORAGE_KEYS.API_KEYS, normalized);
3106
+ return { apiKeys: normalized };
3107
+ });
3108
+ },
3109
+ setChildKeys: (value) => {
3110
+ set((state) => {
3111
+ const updates = typeof value === "function" ? value(state.childKeys) : value;
3112
+ const normalized = updates.map((entry) => ({
3113
+ parentBaseUrl: normalizeBaseUrl(entry.parentBaseUrl),
3114
+ childKey: entry.childKey,
3115
+ balance: entry.balance ?? 0,
3116
+ balanceLimit: entry.balanceLimit,
3117
+ validityDate: entry.validityDate,
3118
+ createdAt: entry.createdAt ?? Date.now()
3119
+ }));
3120
+ driver.setItem(SDK_STORAGE_KEYS.CHILD_KEYS, normalized);
3121
+ return { childKeys: normalized };
3122
+ });
3123
+ }
3124
+ }));
3125
+ };
3126
+ var createDiscoveryAdapterFromStore = (store) => ({
3127
+ getCachedModels: () => store.getState().modelsFromAllProviders,
3128
+ setCachedModels: (models) => store.getState().setModelsFromAllProviders(models),
3129
+ getCachedMints: () => store.getState().mintsFromAllProviders,
3130
+ setCachedMints: (mints) => store.getState().setMintsFromAllProviders(mints),
3131
+ getCachedProviderInfo: () => store.getState().infoFromAllProviders,
3132
+ setCachedProviderInfo: (info) => store.getState().setInfoFromAllProviders(info),
3133
+ getProviderLastUpdate: (baseUrl) => {
3134
+ const normalized = normalizeBaseUrl(baseUrl);
3135
+ const timestamps = store.getState().lastModelsUpdate;
3136
+ return timestamps[normalized] || null;
3137
+ },
3138
+ setProviderLastUpdate: (baseUrl, timestamp) => {
3139
+ const normalized = normalizeBaseUrl(baseUrl);
3140
+ const timestamps = { ...store.getState().lastModelsUpdate };
3141
+ timestamps[normalized] = timestamp;
3142
+ store.getState().setLastModelsUpdate(timestamps);
3143
+ },
3144
+ getLastUsedModel: () => store.getState().lastUsedModel,
3145
+ setLastUsedModel: (modelId) => store.getState().setLastUsedModel(modelId),
3146
+ getDisabledProviders: () => store.getState().disabledProviders,
3147
+ getBaseUrlsList: () => store.getState().baseUrlsList,
3148
+ setBaseUrlsList: (urls) => store.getState().setBaseUrlsList(urls),
3149
+ getBaseUrlsLastUpdate: () => store.getState().lastBaseUrlsUpdate,
3150
+ setBaseUrlsLastUpdate: (timestamp) => store.getState().setBaseUrlsLastUpdate(timestamp)
3151
+ });
3152
+ var createStorageAdapterFromStore = (store) => ({
3153
+ getToken: (baseUrl) => {
3154
+ const normalized = normalizeBaseUrl(baseUrl);
3155
+ const entry = store.getState().cachedTokens.find((token) => token.baseUrl === normalized);
3156
+ if (!entry) return null;
3157
+ const next = store.getState().cachedTokens.map(
3158
+ (token) => token.baseUrl === normalized ? { ...token, lastUsed: Date.now() } : token
3159
+ );
3160
+ store.getState().setCachedTokens(next);
3161
+ return entry.token;
3162
+ },
3163
+ setToken: (baseUrl, token) => {
3164
+ const normalized = normalizeBaseUrl(baseUrl);
3165
+ const tokens = store.getState().cachedTokens;
3166
+ const balance = getTokenBalance(token);
3167
+ const existingIndex = tokens.findIndex(
3168
+ (entry) => entry.baseUrl === normalized
3169
+ );
3170
+ if (existingIndex !== -1) {
3171
+ throw new Error(`Token already exists for baseUrl: ${normalized}`);
3172
+ }
3173
+ const next = [...tokens];
3174
+ next.push({
3175
+ baseUrl: normalized,
3176
+ token,
3177
+ balance,
3178
+ lastUsed: Date.now()
3179
+ });
3180
+ store.getState().setCachedTokens(next);
3181
+ },
3182
+ removeToken: (baseUrl) => {
3183
+ const normalized = normalizeBaseUrl(baseUrl);
3184
+ const next = store.getState().cachedTokens.filter((entry) => entry.baseUrl !== normalized);
3185
+ store.getState().setCachedTokens(next);
3186
+ },
3187
+ updateTokenBalance: (baseUrl, balance) => {
3188
+ const normalized = normalizeBaseUrl(baseUrl);
3189
+ const tokens = store.getState().cachedTokens;
3190
+ const next = tokens.map(
3191
+ (entry) => entry.baseUrl === normalized ? { ...entry, balance } : entry
3192
+ );
3193
+ store.getState().setCachedTokens(next);
3194
+ },
3195
+ getPendingTokenDistribution: () => {
3196
+ const tokens = store.getState().cachedTokens;
3197
+ const distributionMap = {};
3198
+ for (const entry of tokens) {
3199
+ const sum = entry.balance || 0;
3200
+ if (sum > 0) {
3201
+ distributionMap[entry.baseUrl] = (distributionMap[entry.baseUrl] || 0) + sum;
3202
+ }
3203
+ }
3204
+ return Object.entries(distributionMap).map(([baseUrl, amt]) => ({ baseUrl, amount: amt })).sort((a, b) => b.amount - a.amount);
3205
+ },
3206
+ saveProviderInfo: (baseUrl, info) => {
3207
+ const normalized = normalizeBaseUrl(baseUrl);
3208
+ const next = { ...store.getState().infoFromAllProviders };
3209
+ next[normalized] = info;
3210
+ store.getState().setInfoFromAllProviders(next);
3211
+ },
3212
+ getProviderInfo: (baseUrl) => {
3213
+ const normalized = normalizeBaseUrl(baseUrl);
3214
+ return store.getState().infoFromAllProviders[normalized] || null;
3215
+ },
3216
+ // ========== API Keys (for apikeys mode) ==========
3217
+ getApiKey: (baseUrl) => {
3218
+ const normalized = normalizeBaseUrl(baseUrl);
3219
+ const entry = store.getState().apiKeys.find((key) => key.baseUrl === normalized);
3220
+ if (!entry) return null;
3221
+ const next = store.getState().apiKeys.map(
3222
+ (key) => key.baseUrl === normalized ? { ...key, lastUsed: Date.now() } : key
3223
+ );
3224
+ store.getState().setApiKeys(next);
3225
+ return entry.key;
3226
+ },
3227
+ setApiKey: (baseUrl, key) => {
3228
+ const normalized = normalizeBaseUrl(baseUrl);
3229
+ const keys = store.getState().apiKeys;
3230
+ const existingIndex = keys.findIndex(
3231
+ (entry) => entry.baseUrl === normalized
3232
+ );
3233
+ if (existingIndex !== -1) {
3234
+ const next = keys.map(
3235
+ (entry) => entry.baseUrl === normalized ? { ...entry, key, lastUsed: Date.now() } : entry
3236
+ );
3237
+ store.getState().setApiKeys(next);
3238
+ } else {
3239
+ const next = [...keys];
3240
+ next.push({
3241
+ baseUrl: normalized,
3242
+ key,
3243
+ balance: 0,
3244
+ lastUsed: Date.now()
3245
+ });
3246
+ store.getState().setApiKeys(next);
3247
+ }
3248
+ },
3249
+ updateApiKeyBalance: (baseUrl, balance) => {
3250
+ const normalized = normalizeBaseUrl(baseUrl);
3251
+ const keys = store.getState().apiKeys;
3252
+ const next = keys.map(
3253
+ (entry) => entry.baseUrl === normalized ? { ...entry, balance } : entry
3254
+ );
3255
+ store.getState().setApiKeys(next);
3256
+ },
3257
+ getAllApiKeys: () => {
3258
+ return store.getState().apiKeys.map((entry) => ({
3259
+ baseUrl: entry.baseUrl,
3260
+ key: entry.key,
3261
+ balance: entry.balance,
3262
+ lastUsed: entry.lastUsed
3263
+ }));
3264
+ },
3265
+ // ========== Child Keys ==========
3266
+ getChildKey: (parentBaseUrl) => {
3267
+ const normalized = normalizeBaseUrl(parentBaseUrl);
3268
+ const entry = store.getState().childKeys.find((key) => key.parentBaseUrl === normalized);
3269
+ if (!entry) return null;
3270
+ return {
3271
+ parentBaseUrl: entry.parentBaseUrl,
3272
+ childKey: entry.childKey,
3273
+ balance: entry.balance,
3274
+ balanceLimit: entry.balanceLimit,
3275
+ validityDate: entry.validityDate,
3276
+ createdAt: entry.createdAt
3277
+ };
3278
+ },
3279
+ setChildKey: (parentBaseUrl, childKey, balance, validityDate, balanceLimit) => {
3280
+ const normalized = normalizeBaseUrl(parentBaseUrl);
3281
+ const keys = store.getState().childKeys;
3282
+ const existingIndex = keys.findIndex(
3283
+ (entry) => entry.parentBaseUrl === normalized
3284
+ );
3285
+ if (existingIndex !== -1) {
3286
+ const next = keys.map(
3287
+ (entry) => entry.parentBaseUrl === normalized ? {
3288
+ ...entry,
3289
+ childKey,
3290
+ balance: balance ?? 0,
3291
+ validityDate,
3292
+ balanceLimit,
3293
+ createdAt: Date.now()
3294
+ } : entry
3295
+ );
3296
+ store.getState().setChildKeys(next);
3297
+ } else {
3298
+ const next = [...keys];
3299
+ next.push({
3300
+ parentBaseUrl: normalized,
3301
+ childKey,
3302
+ balance: balance ?? 0,
3303
+ validityDate,
3304
+ balanceLimit,
3305
+ createdAt: Date.now()
3306
+ });
3307
+ store.getState().setChildKeys(next);
3308
+ }
3309
+ },
3310
+ updateChildKeyBalance: (parentBaseUrl, balance) => {
3311
+ const normalized = normalizeBaseUrl(parentBaseUrl);
3312
+ const keys = store.getState().childKeys;
3313
+ const next = keys.map(
3314
+ (entry) => entry.parentBaseUrl === normalized ? { ...entry, balance } : entry
3315
+ );
3316
+ store.getState().setChildKeys(next);
3317
+ },
3318
+ removeChildKey: (parentBaseUrl) => {
3319
+ const normalized = normalizeBaseUrl(parentBaseUrl);
3320
+ const next = store.getState().childKeys.filter((entry) => entry.parentBaseUrl !== normalized);
3321
+ store.getState().setChildKeys(next);
3322
+ },
3323
+ getAllChildKeys: () => {
3324
+ return store.getState().childKeys.map((entry) => ({
3325
+ parentBaseUrl: entry.parentBaseUrl,
3326
+ childKey: entry.childKey,
3327
+ balance: entry.balance,
3328
+ balanceLimit: entry.balanceLimit,
3329
+ validityDate: entry.validityDate,
3330
+ createdAt: entry.createdAt
3331
+ }));
3332
+ }
3333
+ });
3334
+ var createProviderRegistryFromStore = (store) => ({
3335
+ getModelsForProvider: (baseUrl) => {
3336
+ const normalized = normalizeBaseUrl(baseUrl);
3337
+ return store.getState().modelsFromAllProviders[normalized] || [];
3338
+ },
3339
+ getDisabledProviders: () => store.getState().disabledProviders,
3340
+ getProviderMints: (baseUrl) => {
3341
+ const normalized = normalizeBaseUrl(baseUrl);
3342
+ return store.getState().mintsFromAllProviders[normalized] || [];
3343
+ },
3344
+ getProviderInfo: async (baseUrl) => {
3345
+ const normalized = normalizeBaseUrl(baseUrl);
3346
+ const cached = store.getState().infoFromAllProviders[normalized];
3347
+ if (cached) return cached;
3348
+ try {
3349
+ const response = await fetch(`${normalized}v1/info`);
3350
+ if (!response.ok) {
3351
+ throw new Error(`Failed ${response.status}`);
3352
+ }
3353
+ const info = await response.json();
3354
+ const next = { ...store.getState().infoFromAllProviders };
3355
+ next[normalized] = info;
3356
+ store.getState().setInfoFromAllProviders(next);
3357
+ return info;
3358
+ } catch (error) {
3359
+ console.warn(`Failed to fetch provider info from ${normalized}:`, error);
3360
+ return null;
3361
+ }
3362
+ },
3363
+ getAllProvidersModels: () => store.getState().modelsFromAllProviders
3364
+ });
3365
+
3366
+ // storage/index.ts
3367
+ var isBrowser = () => {
3368
+ try {
3369
+ return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
3370
+ } catch {
3371
+ return false;
3372
+ }
3373
+ };
3374
+ var isNode = () => {
3375
+ try {
3376
+ return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
3377
+ } catch {
3378
+ return false;
3379
+ }
3380
+ };
3381
+ var defaultDriver = null;
3382
+ var getDefaultSdkDriver = () => {
3383
+ if (defaultDriver) return defaultDriver;
3384
+ if (isBrowser()) {
3385
+ defaultDriver = localStorageDriver;
3386
+ return defaultDriver;
3387
+ }
3388
+ if (isNode()) {
3389
+ defaultDriver = createSqliteDriver();
3390
+ return defaultDriver;
3391
+ }
3392
+ defaultDriver = createMemoryDriver();
3393
+ return defaultDriver;
3394
+ };
3395
+ var defaultStore = null;
3396
+ var getDefaultSdkStore = () => {
3397
+ if (!defaultStore) {
3398
+ defaultStore = createSdkStore({ driver: getDefaultSdkDriver() });
3399
+ }
3400
+ return defaultStore;
3401
+ };
3402
+ var getDefaultDiscoveryAdapter = () => createDiscoveryAdapterFromStore(getDefaultSdkStore());
3403
+ var getDefaultStorageAdapter = () => createStorageAdapterFromStore(getDefaultSdkStore());
3404
+ var getDefaultProviderRegistry = () => createProviderRegistryFromStore(getDefaultSdkStore());
3405
+
3406
+ // routeRequests.ts
3407
+ async function routeRequests(options) {
3408
+ const {
3409
+ modelId,
3410
+ requestBody,
3411
+ path = "/v1/chat/completions",
3412
+ forcedProvider,
3413
+ walletAdapter,
3414
+ storageAdapter,
3415
+ providerRegistry,
3416
+ discoveryAdapter,
3417
+ includeProviderUrls = [],
3418
+ torMode = false,
3419
+ forceRefresh = false,
3420
+ modelManager: providedModelManager
3421
+ } = options;
3422
+ let modelManager;
3423
+ let providers;
3424
+ if (providedModelManager) {
3425
+ modelManager = providedModelManager;
3426
+ providers = modelManager.getBaseUrls();
3427
+ if (providers.length === 0) {
3428
+ throw new Error("No providers available - run bootstrap first");
3429
+ }
3430
+ } else {
3431
+ modelManager = new ModelManager(discoveryAdapter, {
3432
+ includeProviderUrls: forcedProvider ? [forcedProvider, ...includeProviderUrls] : includeProviderUrls
3433
+ });
3434
+ providers = await modelManager.bootstrapProviders(torMode);
3435
+ if (providers.length === 0) {
3436
+ throw new Error("No providers available");
3437
+ }
3438
+ await modelManager.fetchModels(providers, forceRefresh);
3439
+ }
3440
+ const providerManager = new ProviderManager(providerRegistry);
3441
+ let baseUrl;
3442
+ let selectedModel;
3443
+ if (forcedProvider) {
3444
+ const normalizedProvider = forcedProvider.endsWith("/") ? forcedProvider : `${forcedProvider}/`;
3445
+ const cachedModels = modelManager.getAllCachedModels();
3446
+ const models = cachedModels[normalizedProvider] || [];
3447
+ const match = models.find((m) => m.id === modelId);
3448
+ if (!match) {
3449
+ throw new Error(
3450
+ `Provider ${normalizedProvider} does not offer model: ${modelId}`
3451
+ );
3452
+ }
3453
+ baseUrl = normalizedProvider;
3454
+ selectedModel = match;
3455
+ } else {
3456
+ const ranking = providerManager.getProviderPriceRankingForModel(modelId, {
3457
+ torMode,
3458
+ includeDisabled: false
3459
+ });
3460
+ if (ranking.length === 0) {
3461
+ throw new Error(`No providers found for model: ${modelId}`);
3462
+ }
3463
+ const cheapest = ranking[0];
3464
+ baseUrl = cheapest.baseUrl;
3465
+ selectedModel = cheapest.model;
3466
+ }
3467
+ const balances = await walletAdapter.getBalances();
3468
+ const totalBalance = Object.values(balances).reduce((sum, v) => sum + v, 0);
3469
+ if (totalBalance <= 0) {
3470
+ throw new Error(
3471
+ "Wallet balance is empty. Add a mint and fund it before making requests."
3472
+ );
3473
+ }
3474
+ const providerMints = providerRegistry.getProviderMints(baseUrl);
3475
+ const mintUrl = walletAdapter.getActiveMintUrl() || providerMints[0] || Object.keys(balances)[0];
3476
+ if (!mintUrl) {
3477
+ throw new Error("No mint configured in wallet");
3478
+ }
3479
+ const alertLevel = "min";
3480
+ const client = new RoutstrClient(
3481
+ walletAdapter,
3482
+ storageAdapter,
3483
+ providerRegistry,
3484
+ alertLevel,
3485
+ "apikeys"
3486
+ );
3487
+ const maxTokens = extractMaxTokens(requestBody);
3488
+ const stream = extractStream(requestBody);
3489
+ let response = null;
3490
+ try {
3491
+ const proxiedBody = requestBody && typeof requestBody === "object" ? { ...requestBody } : {};
3492
+ proxiedBody.model = selectedModel.id;
3493
+ if (stream !== void 0) {
3494
+ proxiedBody.stream = stream;
3495
+ }
3496
+ if (maxTokens !== void 0) {
3497
+ proxiedBody.max_tokens = maxTokens;
3498
+ }
3499
+ response = await client.routeRequest({
3500
+ path,
3501
+ method: "POST",
3502
+ body: proxiedBody,
3503
+ baseUrl,
3504
+ mintUrl,
3505
+ modelId
3506
+ });
3507
+ if (!response.ok) {
3508
+ throw new Error(`${response.status} ${response.statusText}`);
3509
+ }
3510
+ return response;
3511
+ } catch (error) {
3512
+ if (error instanceof Error && (error.message.includes("401") || error.message.includes("402") || error.message.includes("403"))) {
3513
+ throw new Error(`Authentication failed: ${error.message}`);
3514
+ }
3515
+ throw error;
3516
+ }
3517
+ }
3518
+ function extractMaxTokens(requestBody) {
3519
+ if (!requestBody || typeof requestBody !== "object") {
3520
+ return void 0;
3521
+ }
3522
+ const body = requestBody;
3523
+ const maxTokens = body.max_tokens;
3524
+ if (typeof maxTokens === "number") {
3525
+ return maxTokens;
3526
+ }
3527
+ return void 0;
3528
+ }
3529
+ function extractStream(requestBody) {
3530
+ if (!requestBody || typeof requestBody !== "object") {
3531
+ return false;
3532
+ }
3533
+ const body = requestBody;
3534
+ const stream = body.stream;
3535
+ return stream === true;
3536
+ }
3537
+
3538
+ exports.BalanceManager = BalanceManager;
3539
+ exports.CashuSpender = CashuSpender;
3540
+ exports.FailoverError = FailoverError;
3541
+ exports.InsufficientBalanceError = InsufficientBalanceError;
3542
+ exports.MintDiscovery = MintDiscovery;
3543
+ exports.MintDiscoveryError = MintDiscoveryError;
3544
+ exports.MintUnreachableError = MintUnreachableError;
3545
+ exports.ModelManager = ModelManager;
3546
+ exports.ModelNotFoundError = ModelNotFoundError;
3547
+ exports.NoProvidersAvailableError = NoProvidersAvailableError;
3548
+ exports.ProviderBootstrapError = ProviderBootstrapError;
3549
+ exports.ProviderError = ProviderError;
3550
+ exports.ProviderManager = ProviderManager;
3551
+ exports.RoutstrClient = RoutstrClient;
3552
+ exports.SDK_STORAGE_KEYS = SDK_STORAGE_KEYS;
3553
+ exports.StreamProcessor = StreamProcessor;
3554
+ exports.StreamingError = StreamingError;
3555
+ exports.TokenOperationError = TokenOperationError;
3556
+ exports.createMemoryDriver = createMemoryDriver;
3557
+ exports.createSdkStore = createSdkStore;
3558
+ exports.createSqliteDriver = createSqliteDriver;
3559
+ exports.filterBaseUrlsForTor = filterBaseUrlsForTor;
3560
+ exports.getDefaultDiscoveryAdapter = getDefaultDiscoveryAdapter;
3561
+ exports.getDefaultProviderRegistry = getDefaultProviderRegistry;
3562
+ exports.getDefaultSdkDriver = getDefaultSdkDriver;
3563
+ exports.getDefaultSdkStore = getDefaultSdkStore;
3564
+ exports.getDefaultStorageAdapter = getDefaultStorageAdapter;
3565
+ exports.getProviderEndpoints = getProviderEndpoints;
3566
+ exports.isOnionUrl = isOnionUrl;
3567
+ exports.isTorContext = isTorContext;
3568
+ exports.localStorageDriver = localStorageDriver;
3569
+ exports.normalizeProviderUrl = normalizeProviderUrl;
3570
+ exports.routeRequests = routeRequests;
3571
+ //# sourceMappingURL=index.js.map
3572
+ //# sourceMappingURL=index.js.map