@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/README.md +95 -0
- package/dist/index.d.mts +4931 -0
- package/dist/index.d.ts +4931 -0
- package/dist/index.js +3572 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3538 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +77 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,4931 @@
|
|
|
1
|
+
import * as zustand_vanilla from 'zustand/vanilla';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Core types for the Routstr SDK
|
|
5
|
+
* These types are shared across wallet and client modules
|
|
6
|
+
*/
|
|
7
|
+
interface MessageContentType {
|
|
8
|
+
type: "text" | "image_url" | "file";
|
|
9
|
+
text?: string;
|
|
10
|
+
image_url?: {
|
|
11
|
+
url: string;
|
|
12
|
+
storageId?: string;
|
|
13
|
+
};
|
|
14
|
+
file?: {
|
|
15
|
+
url: string;
|
|
16
|
+
name?: string;
|
|
17
|
+
mimeType?: string;
|
|
18
|
+
size?: number;
|
|
19
|
+
};
|
|
20
|
+
hidden?: boolean;
|
|
21
|
+
thinking?: string;
|
|
22
|
+
citations?: string[];
|
|
23
|
+
}
|
|
24
|
+
interface Message {
|
|
25
|
+
role: string;
|
|
26
|
+
content: string | MessageContentType[];
|
|
27
|
+
_eventId?: string;
|
|
28
|
+
_prevId?: string;
|
|
29
|
+
_createdAt?: number;
|
|
30
|
+
_modelId?: string;
|
|
31
|
+
satsSpent?: number;
|
|
32
|
+
}
|
|
33
|
+
interface TransactionHistory {
|
|
34
|
+
type: "spent" | "mint" | "send" | "import" | "refund";
|
|
35
|
+
amount: number;
|
|
36
|
+
timestamp: number;
|
|
37
|
+
status: "success" | "failed";
|
|
38
|
+
model?: string;
|
|
39
|
+
message?: string;
|
|
40
|
+
balance?: number;
|
|
41
|
+
}
|
|
42
|
+
interface ModelPricing {
|
|
43
|
+
prompt: number;
|
|
44
|
+
completion: number;
|
|
45
|
+
request: number;
|
|
46
|
+
image: number;
|
|
47
|
+
web_search: number;
|
|
48
|
+
internal_reasoning: number;
|
|
49
|
+
}
|
|
50
|
+
interface ModelSatsPricing extends ModelPricing {
|
|
51
|
+
max_completion_cost: number;
|
|
52
|
+
max_prompt_cost: number;
|
|
53
|
+
max_cost: number;
|
|
54
|
+
}
|
|
55
|
+
interface ModelArchitecture {
|
|
56
|
+
modality: string;
|
|
57
|
+
input_modalities: readonly string[];
|
|
58
|
+
output_modalities: readonly string[];
|
|
59
|
+
tokenizer: string;
|
|
60
|
+
instruct_type: string | null;
|
|
61
|
+
}
|
|
62
|
+
interface PerRequestLimits {
|
|
63
|
+
readonly prompt_tokens?: number;
|
|
64
|
+
readonly completion_tokens?: number;
|
|
65
|
+
readonly requests_per_minute?: number;
|
|
66
|
+
readonly images_per_minute?: number;
|
|
67
|
+
readonly web_searches_per_minute?: number;
|
|
68
|
+
readonly [key: string]: number | undefined;
|
|
69
|
+
}
|
|
70
|
+
interface Model {
|
|
71
|
+
id: string;
|
|
72
|
+
name: string;
|
|
73
|
+
created?: number;
|
|
74
|
+
description?: string;
|
|
75
|
+
context_length?: number;
|
|
76
|
+
architecture?: ModelArchitecture;
|
|
77
|
+
pricing?: ModelPricing;
|
|
78
|
+
sats_pricing: ModelSatsPricing;
|
|
79
|
+
per_request_limits?: PerRequestLimits;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Result from spending cashu tokens
|
|
83
|
+
*/
|
|
84
|
+
interface SpendResult {
|
|
85
|
+
token: string | null;
|
|
86
|
+
status: "success" | "failed";
|
|
87
|
+
balance: number;
|
|
88
|
+
unit?: "sat" | "msat";
|
|
89
|
+
error?: string;
|
|
90
|
+
errorDetails?: {
|
|
91
|
+
required: number;
|
|
92
|
+
available: number;
|
|
93
|
+
maxMintBalance: number;
|
|
94
|
+
maxMintUrl: string;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Result from refund operations
|
|
99
|
+
*/
|
|
100
|
+
interface RefundResult {
|
|
101
|
+
success: boolean;
|
|
102
|
+
refundedAmount?: number;
|
|
103
|
+
message?: string;
|
|
104
|
+
requestId?: string;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Result from top up operations
|
|
108
|
+
*/
|
|
109
|
+
interface TopUpResult {
|
|
110
|
+
success: boolean;
|
|
111
|
+
toppedUpAmount?: number;
|
|
112
|
+
message?: string;
|
|
113
|
+
requestId?: string;
|
|
114
|
+
recoveredToken?: boolean;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Token balance information
|
|
118
|
+
*/
|
|
119
|
+
interface TokenBalance {
|
|
120
|
+
amount: number;
|
|
121
|
+
unit: "sat" | "msat";
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* API error verdict for retry logic
|
|
125
|
+
*/
|
|
126
|
+
interface APIErrorVerdict {
|
|
127
|
+
retry: boolean;
|
|
128
|
+
reason: string;
|
|
129
|
+
newBaseUrl?: string;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Image data from API response
|
|
133
|
+
*/
|
|
134
|
+
interface ImageData {
|
|
135
|
+
type: "image_url";
|
|
136
|
+
image_url: {
|
|
137
|
+
url: string;
|
|
138
|
+
};
|
|
139
|
+
index?: number;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Annotation data from API response
|
|
143
|
+
*/
|
|
144
|
+
interface AnnotationData {
|
|
145
|
+
type: "url_citation";
|
|
146
|
+
start_index: number;
|
|
147
|
+
end_index: number;
|
|
148
|
+
url: string;
|
|
149
|
+
title: string;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Usage statistics from API response
|
|
153
|
+
*/
|
|
154
|
+
interface UsageStats {
|
|
155
|
+
total_tokens?: number;
|
|
156
|
+
prompt_tokens?: number;
|
|
157
|
+
completion_tokens?: number;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Result from streaming response processing
|
|
161
|
+
*/
|
|
162
|
+
interface StreamingResult {
|
|
163
|
+
content: string;
|
|
164
|
+
thinking?: string;
|
|
165
|
+
images?: ImageData[];
|
|
166
|
+
usage?: UsageStats;
|
|
167
|
+
model?: string;
|
|
168
|
+
finish_reason?: string;
|
|
169
|
+
citations?: string[];
|
|
170
|
+
annotations?: AnnotationData[];
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Parameters for fetching AI response
|
|
174
|
+
*/
|
|
175
|
+
interface FetchAIResponseParams {
|
|
176
|
+
messageHistory: Message[];
|
|
177
|
+
selectedModel: Model;
|
|
178
|
+
baseUrl: string;
|
|
179
|
+
mintUrl: string;
|
|
180
|
+
balance: number;
|
|
181
|
+
transactionHistory: TransactionHistory[];
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Candidate provider for failover
|
|
185
|
+
*/
|
|
186
|
+
interface CandidateProvider {
|
|
187
|
+
baseUrl: string;
|
|
188
|
+
model: Model;
|
|
189
|
+
cost: number;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Mint selection result
|
|
193
|
+
*/
|
|
194
|
+
interface MintSelection {
|
|
195
|
+
selectedMintUrl: string | null;
|
|
196
|
+
selectedMintBalance: number;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Pending token entry
|
|
200
|
+
*/
|
|
201
|
+
interface PendingTokenEntry {
|
|
202
|
+
baseUrl: string;
|
|
203
|
+
amount: number;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Provider information from /v1/info endpoint
|
|
207
|
+
*/
|
|
208
|
+
interface ProviderInfo {
|
|
209
|
+
mints?: string[];
|
|
210
|
+
[key: string]: any;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Model discovery result
|
|
214
|
+
*/
|
|
215
|
+
interface ModelDiscoveryResult {
|
|
216
|
+
models: Model[];
|
|
217
|
+
bestById: Map<string, {
|
|
218
|
+
model: Model;
|
|
219
|
+
base: string;
|
|
220
|
+
}>;
|
|
221
|
+
totalProcessed: number;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Mint discovery result
|
|
225
|
+
*/
|
|
226
|
+
interface MintDiscoveryResult {
|
|
227
|
+
mintsFromProviders: Record<string, string[]>;
|
|
228
|
+
infoFromProviders: Record<string, ProviderInfo>;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Custom error classes for the Routstr SDK
|
|
233
|
+
* Provides specific error types for different failure modes
|
|
234
|
+
*/
|
|
235
|
+
/**
|
|
236
|
+
* Error thrown when balance is insufficient for an operation
|
|
237
|
+
*/
|
|
238
|
+
declare class InsufficientBalanceError$1 extends Error {
|
|
239
|
+
required: number;
|
|
240
|
+
available: number;
|
|
241
|
+
maxMintBalance: number;
|
|
242
|
+
maxMintUrl: string;
|
|
243
|
+
constructor(required: number, available: number, maxMintBalance?: number, maxMintUrl?: string);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Error thrown when a provider returns an error response
|
|
247
|
+
*/
|
|
248
|
+
declare class ProviderError$1 extends Error {
|
|
249
|
+
baseUrl: string;
|
|
250
|
+
statusCode: number;
|
|
251
|
+
requestId?: string | undefined;
|
|
252
|
+
constructor(baseUrl: string, statusCode: number, message: string, requestId?: string | undefined);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Error thrown when a mint is unreachable
|
|
256
|
+
*/
|
|
257
|
+
declare class MintUnreachableError$1 extends Error {
|
|
258
|
+
mintUrl: string;
|
|
259
|
+
constructor(mintUrl: string);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Error thrown when a token operation fails
|
|
263
|
+
*/
|
|
264
|
+
declare class TokenOperationError$1 extends Error {
|
|
265
|
+
operation: "send" | "receive" | "refund";
|
|
266
|
+
mintUrl?: string | undefined;
|
|
267
|
+
constructor(message: string, operation: "send" | "receive" | "refund", mintUrl?: string | undefined);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Error thrown when provider failover fails
|
|
271
|
+
*/
|
|
272
|
+
declare class FailoverError$1 extends Error {
|
|
273
|
+
originalProvider: string;
|
|
274
|
+
failedProviders: string[];
|
|
275
|
+
constructor(originalProvider: string, failedProviders: string[], message?: string);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Error thrown when streaming response processing fails
|
|
279
|
+
*/
|
|
280
|
+
declare class StreamingError$1 extends Error {
|
|
281
|
+
finishReason?: string | undefined;
|
|
282
|
+
accumulatedContent?: string | undefined;
|
|
283
|
+
constructor(message: string, finishReason?: string | undefined, accumulatedContent?: string | undefined);
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Error thrown when model is not found on a provider
|
|
287
|
+
*/
|
|
288
|
+
declare class ModelNotFoundError$1 extends Error {
|
|
289
|
+
modelId: string;
|
|
290
|
+
baseUrl: string;
|
|
291
|
+
constructor(modelId: string, baseUrl: string);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Error thrown when provider bootstrap fails
|
|
295
|
+
*/
|
|
296
|
+
declare class ProviderBootstrapError$1 extends Error {
|
|
297
|
+
failedProviders: string[];
|
|
298
|
+
constructor(failedProviders: string[], message?: string);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Error thrown when no providers are available
|
|
302
|
+
*/
|
|
303
|
+
declare class NoProvidersAvailableError$1 extends Error {
|
|
304
|
+
constructor();
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Error thrown when mint discovery fails
|
|
308
|
+
*/
|
|
309
|
+
declare class MintDiscoveryError$1 extends Error {
|
|
310
|
+
baseUrl: string;
|
|
311
|
+
constructor(baseUrl: string, message?: string);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Interfaces for the model and provider discovery system
|
|
316
|
+
* These abstractions allow the discovery logic to be independent of storage implementation
|
|
317
|
+
*/
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Discovery adapter for managing cached provider and model data
|
|
321
|
+
* Abstracts localStorage operations so discovery logic is testable and reusable
|
|
322
|
+
*/
|
|
323
|
+
interface DiscoveryAdapter {
|
|
324
|
+
/**
|
|
325
|
+
* Get cached models from all providers
|
|
326
|
+
* @returns Record mapping baseUrl -> array of models
|
|
327
|
+
*/
|
|
328
|
+
getCachedModels(): Record<string, Model[]>;
|
|
329
|
+
/**
|
|
330
|
+
* Save models cache
|
|
331
|
+
* @param models Record mapping baseUrl -> array of models
|
|
332
|
+
*/
|
|
333
|
+
setCachedModels(models: Record<string, Model[]>): void;
|
|
334
|
+
/**
|
|
335
|
+
* Get cached mints from all providers
|
|
336
|
+
* @returns Record mapping baseUrl -> array of mint URLs
|
|
337
|
+
*/
|
|
338
|
+
getCachedMints(): Record<string, string[]>;
|
|
339
|
+
/**
|
|
340
|
+
* Save mints cache
|
|
341
|
+
* @param mints Record mapping baseUrl -> array of mint URLs
|
|
342
|
+
*/
|
|
343
|
+
setCachedMints(mints: Record<string, string[]>): void;
|
|
344
|
+
/**
|
|
345
|
+
* Get cached provider info from all providers
|
|
346
|
+
* @returns Record mapping baseUrl -> provider info object
|
|
347
|
+
*/
|
|
348
|
+
getCachedProviderInfo(): Record<string, ProviderInfo>;
|
|
349
|
+
/**
|
|
350
|
+
* Save provider info cache
|
|
351
|
+
* @param info Record mapping baseUrl -> provider info object
|
|
352
|
+
*/
|
|
353
|
+
setCachedProviderInfo(info: Record<string, ProviderInfo>): void;
|
|
354
|
+
/**
|
|
355
|
+
* Get provider last update timestamp
|
|
356
|
+
* @param baseUrl Provider base URL
|
|
357
|
+
* @returns Timestamp in milliseconds or null if never updated
|
|
358
|
+
*/
|
|
359
|
+
getProviderLastUpdate(baseUrl: string): number | null;
|
|
360
|
+
/**
|
|
361
|
+
* Set provider last update timestamp
|
|
362
|
+
* @param baseUrl Provider base URL
|
|
363
|
+
* @param timestamp Timestamp in milliseconds
|
|
364
|
+
*/
|
|
365
|
+
setProviderLastUpdate(baseUrl: string, timestamp: number): void;
|
|
366
|
+
/**
|
|
367
|
+
* Get last used model ID
|
|
368
|
+
* @returns Model ID or null if none
|
|
369
|
+
*/
|
|
370
|
+
getLastUsedModel(): string | null;
|
|
371
|
+
/**
|
|
372
|
+
* Save last used model ID
|
|
373
|
+
* @param modelId Model ID to save
|
|
374
|
+
*/
|
|
375
|
+
setLastUsedModel(modelId: string): void;
|
|
376
|
+
/**
|
|
377
|
+
* Get list of disabled provider base URLs
|
|
378
|
+
* @returns Array of disabled provider URLs
|
|
379
|
+
*/
|
|
380
|
+
getDisabledProviders(): string[];
|
|
381
|
+
/**
|
|
382
|
+
* Get list of configured provider base URLs
|
|
383
|
+
* @returns Array of provider base URLs
|
|
384
|
+
*/
|
|
385
|
+
getBaseUrlsList(): string[];
|
|
386
|
+
/**
|
|
387
|
+
* Get base URLs list last update timestamp
|
|
388
|
+
* @returns Timestamp in milliseconds or null if never updated
|
|
389
|
+
*/
|
|
390
|
+
getBaseUrlsLastUpdate(): number | null;
|
|
391
|
+
/**
|
|
392
|
+
* Save list of provider base URLs
|
|
393
|
+
* @param urls Array of provider base URLs
|
|
394
|
+
*/
|
|
395
|
+
setBaseUrlsList(urls: string[]): void;
|
|
396
|
+
/**
|
|
397
|
+
* Set base URLs list last update timestamp
|
|
398
|
+
* @param timestamp Timestamp in milliseconds
|
|
399
|
+
*/
|
|
400
|
+
setBaseUrlsLastUpdate(timestamp: number): void;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* ModelManager class for discovering, fetching, and managing models from providers
|
|
405
|
+
* Core responsibility: fetching models from providers, caching them, and selecting the best option
|
|
406
|
+
* (lowest cost) across multiple providers
|
|
407
|
+
*/
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Configuration for ModelManager
|
|
411
|
+
*/
|
|
412
|
+
interface ModelManagerConfig {
|
|
413
|
+
/** URL to fetch provider directory from */
|
|
414
|
+
providerDirectoryUrl?: string;
|
|
415
|
+
/** Additional provider base URLs to include */
|
|
416
|
+
includeProviderUrls?: string[];
|
|
417
|
+
/** Provider base URLs to exclude */
|
|
418
|
+
excludeProviderUrls?: string[];
|
|
419
|
+
/** Cache TTL in milliseconds (default: 21 minutes) */
|
|
420
|
+
cacheTTL?: number;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* ModelManager handles all model discovery and caching logic
|
|
424
|
+
* Abstracts away storage details via DiscoveryAdapter
|
|
425
|
+
*/
|
|
426
|
+
declare class ModelManager$1 {
|
|
427
|
+
private adapter;
|
|
428
|
+
private readonly cacheTTL;
|
|
429
|
+
private readonly providerDirectoryUrl;
|
|
430
|
+
private readonly includeProviderUrls;
|
|
431
|
+
private readonly excludeProviderUrls;
|
|
432
|
+
constructor(adapter: DiscoveryAdapter, config?: ModelManagerConfig);
|
|
433
|
+
/**
|
|
434
|
+
* Get the list of bootstrapped provider base URLs
|
|
435
|
+
* @returns Array of provider base URLs
|
|
436
|
+
*/
|
|
437
|
+
getBaseUrls(): string[];
|
|
438
|
+
static init(adapter: DiscoveryAdapter, config?: ModelManagerConfig, options?: {
|
|
439
|
+
torMode?: boolean;
|
|
440
|
+
forceRefresh?: boolean;
|
|
441
|
+
}): Promise<ModelManager$1>;
|
|
442
|
+
/**
|
|
443
|
+
* Bootstrap provider list from the provider directory
|
|
444
|
+
* Fetches available providers and caches their base URLs
|
|
445
|
+
* @param torMode Whether running in Tor context
|
|
446
|
+
* @returns Array of provider base URLs
|
|
447
|
+
* @throws ProviderBootstrapError if all providers fail to fetch
|
|
448
|
+
*/
|
|
449
|
+
bootstrapProviders(torMode?: boolean): Promise<string[]>;
|
|
450
|
+
/**
|
|
451
|
+
* Fetch models from all providers and select best-priced options
|
|
452
|
+
* Uses cache if available and not expired
|
|
453
|
+
* @param baseUrls List of provider base URLs to fetch from
|
|
454
|
+
* @param forceRefresh Ignore cache and fetch fresh data
|
|
455
|
+
* @returns Array of unique models with best prices selected
|
|
456
|
+
*/
|
|
457
|
+
fetchModels(baseUrls: string[], forceRefresh?: boolean): Promise<Model[]>;
|
|
458
|
+
/**
|
|
459
|
+
* Fetch models from a single provider
|
|
460
|
+
* @param baseUrl Provider base URL
|
|
461
|
+
* @returns Array of models from provider
|
|
462
|
+
*/
|
|
463
|
+
private fetchModelsFromProvider;
|
|
464
|
+
private isProviderDownError;
|
|
465
|
+
/**
|
|
466
|
+
* Get all cached models from all providers
|
|
467
|
+
* @returns Record mapping baseUrl -> models
|
|
468
|
+
*/
|
|
469
|
+
getAllCachedModels(): Record<string, Model[]>;
|
|
470
|
+
/**
|
|
471
|
+
* Clear cache for a specific provider
|
|
472
|
+
* @param baseUrl Provider base URL
|
|
473
|
+
*/
|
|
474
|
+
clearProviderCache(baseUrl: string): void;
|
|
475
|
+
/**
|
|
476
|
+
* Clear all model caches
|
|
477
|
+
*/
|
|
478
|
+
clearAllCache(): void;
|
|
479
|
+
/**
|
|
480
|
+
* Filter base URLs based on Tor context
|
|
481
|
+
* @param baseUrls Provider URLs to filter
|
|
482
|
+
* @param torMode Whether in Tor context
|
|
483
|
+
* @returns Filtered URLs appropriate for Tor mode
|
|
484
|
+
*/
|
|
485
|
+
filterBaseUrlsForTor(baseUrls: string[], torMode: boolean): string[];
|
|
486
|
+
/**
|
|
487
|
+
* Get provider endpoints from provider info
|
|
488
|
+
* @param provider Provider object from directory
|
|
489
|
+
* @param torMode Whether in Tor context
|
|
490
|
+
* @returns Array of endpoint URLs
|
|
491
|
+
*/
|
|
492
|
+
private getProviderEndpoints;
|
|
493
|
+
/**
|
|
494
|
+
* Normalize provider URL with trailing slash
|
|
495
|
+
* @param url URL to normalize
|
|
496
|
+
* @returns Normalized URL
|
|
497
|
+
*/
|
|
498
|
+
private normalizeUrl;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* MintDiscovery class for discovering mints and provider info
|
|
503
|
+
* Core responsibility: fetching mint information from providers and caching it
|
|
504
|
+
*/
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Configuration for MintDiscovery
|
|
508
|
+
*/
|
|
509
|
+
interface MintDiscoveryConfig {
|
|
510
|
+
/** Cache TTL in milliseconds (default: 21 minutes) */
|
|
511
|
+
cacheTTL?: number;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* MintDiscovery handles mint and provider info discovery
|
|
515
|
+
* Abstracts away storage details via DiscoveryAdapter
|
|
516
|
+
*/
|
|
517
|
+
declare class MintDiscovery$1 {
|
|
518
|
+
private adapter;
|
|
519
|
+
private readonly cacheTTL;
|
|
520
|
+
constructor(adapter: DiscoveryAdapter, config?: MintDiscoveryConfig);
|
|
521
|
+
/**
|
|
522
|
+
* Fetch mints from all providers via their /v1/info endpoints
|
|
523
|
+
* Caches mints and full provider info for later access
|
|
524
|
+
* @param baseUrls List of provider base URLs to fetch from
|
|
525
|
+
* @returns Object with mints and provider info from all providers
|
|
526
|
+
*/
|
|
527
|
+
discoverMints(baseUrls: string[], options?: {
|
|
528
|
+
forceRefresh?: boolean;
|
|
529
|
+
}): Promise<{
|
|
530
|
+
mintsFromProviders: Record<string, string[]>;
|
|
531
|
+
infoFromProviders: Record<string, ProviderInfo>;
|
|
532
|
+
}>;
|
|
533
|
+
/**
|
|
534
|
+
* Get cached mints from all providers
|
|
535
|
+
* @returns Record mapping baseUrl -> mint URLs
|
|
536
|
+
*/
|
|
537
|
+
getCachedMints(): Record<string, string[]>;
|
|
538
|
+
/**
|
|
539
|
+
* Get cached provider info from all providers
|
|
540
|
+
* @returns Record mapping baseUrl -> provider info
|
|
541
|
+
*/
|
|
542
|
+
getCachedProviderInfo(): Record<string, ProviderInfo>;
|
|
543
|
+
/**
|
|
544
|
+
* Get mints for a specific provider
|
|
545
|
+
* @param baseUrl Provider base URL
|
|
546
|
+
* @returns Array of mint URLs for the provider
|
|
547
|
+
*/
|
|
548
|
+
getProviderMints(baseUrl: string): string[];
|
|
549
|
+
/**
|
|
550
|
+
* Get info for a specific provider
|
|
551
|
+
* @param baseUrl Provider base URL
|
|
552
|
+
* @returns Provider info object or null if not found
|
|
553
|
+
*/
|
|
554
|
+
getProviderInfo(baseUrl: string): ProviderInfo | null;
|
|
555
|
+
/**
|
|
556
|
+
* Clear mint cache for a specific provider
|
|
557
|
+
* @param baseUrl Provider base URL
|
|
558
|
+
*/
|
|
559
|
+
clearProviderMintCache(baseUrl: string): void;
|
|
560
|
+
/**
|
|
561
|
+
* Clear all mint caches
|
|
562
|
+
*/
|
|
563
|
+
clearAllCache(): void;
|
|
564
|
+
private isProviderDownError;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Interfaces for the Routstr SDK wallet abstraction layer
|
|
569
|
+
* These interfaces allow the SDK to be framework-agnostic
|
|
570
|
+
*/
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* WalletAdapter - Abstracts wallet operations (NIP-60 or legacy)
|
|
574
|
+
* The React app implements this using its hooks
|
|
575
|
+
*/
|
|
576
|
+
interface WalletAdapter {
|
|
577
|
+
/** Get balances for all mints (mintUrl -> balance in sats) */
|
|
578
|
+
getBalances(): Promise<Record<string, number>>;
|
|
579
|
+
/** Get unit type for each mint (mintUrl -> 'sat' | 'msat') */
|
|
580
|
+
getMintUnits(): Record<string, "sat" | "msat">;
|
|
581
|
+
/** Get the currently active mint URL */
|
|
582
|
+
getActiveMintUrl(): string | null;
|
|
583
|
+
/**
|
|
584
|
+
* Create and send a cashu token from a mint
|
|
585
|
+
* @param mintUrl The mint URL to send from
|
|
586
|
+
* @param amount Amount in sats
|
|
587
|
+
* @param p2pkPubkey Optional P2PK public key
|
|
588
|
+
* @returns Encoded cashu token string
|
|
589
|
+
*/
|
|
590
|
+
sendToken(mintUrl: string, amount: number, p2pkPubkey?: string): Promise<string>;
|
|
591
|
+
/**
|
|
592
|
+
* Receive/store a cashu token
|
|
593
|
+
* Handles both NIP-60 and legacy storage internally
|
|
594
|
+
* @param token Encoded cashu token string
|
|
595
|
+
* @returns Result with success flag and amount received
|
|
596
|
+
*/
|
|
597
|
+
receiveToken(token: string): Promise<{
|
|
598
|
+
success: boolean;
|
|
599
|
+
amount: number;
|
|
600
|
+
unit: "sat" | "msat";
|
|
601
|
+
}>;
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* StorageAdapter - Abstracts local storage operations
|
|
605
|
+
* Separates token storage from wallet operations
|
|
606
|
+
*/
|
|
607
|
+
interface ApiKeyEntry {
|
|
608
|
+
baseUrl: string;
|
|
609
|
+
key: string;
|
|
610
|
+
balance: number;
|
|
611
|
+
lastUsed: number | null;
|
|
612
|
+
}
|
|
613
|
+
interface ChildKeyEntry {
|
|
614
|
+
parentBaseUrl: string;
|
|
615
|
+
childKey: string;
|
|
616
|
+
balance: number;
|
|
617
|
+
balanceLimit?: number;
|
|
618
|
+
validityDate?: number;
|
|
619
|
+
createdAt: number;
|
|
620
|
+
}
|
|
621
|
+
interface StorageAdapter {
|
|
622
|
+
/** Get stored API token for a provider */
|
|
623
|
+
getToken(baseUrl: string): string | null;
|
|
624
|
+
/** Store API token for a provider */
|
|
625
|
+
setToken(baseUrl: string, token: string): void;
|
|
626
|
+
/** Remove API token for a provider */
|
|
627
|
+
removeToken(baseUrl: string): void;
|
|
628
|
+
/** Update balance for an existing stored token */
|
|
629
|
+
updateTokenBalance(baseUrl: string, balance: number): void;
|
|
630
|
+
/** Get all stored tokens as distribution (baseUrl -> amount in sats) */
|
|
631
|
+
getPendingTokenDistribution(): Array<{
|
|
632
|
+
baseUrl: string;
|
|
633
|
+
amount: number;
|
|
634
|
+
}>;
|
|
635
|
+
/** Save provider info to cache */
|
|
636
|
+
saveProviderInfo(baseUrl: string, info: ProviderInfo): void;
|
|
637
|
+
/** Get cached provider info */
|
|
638
|
+
getProviderInfo(baseUrl: string): ProviderInfo | null;
|
|
639
|
+
/** Get stored API key for a provider */
|
|
640
|
+
getApiKey(baseUrl: string): string | null;
|
|
641
|
+
/** Store API key for a provider */
|
|
642
|
+
setApiKey(baseUrl: string, key: string): void;
|
|
643
|
+
/** Update balance for an existing stored API key (based on provider response) */
|
|
644
|
+
updateApiKeyBalance(baseUrl: string, balance: number): void;
|
|
645
|
+
/** Get all stored API keys */
|
|
646
|
+
getAllApiKeys(): ApiKeyEntry[];
|
|
647
|
+
/** Get stored child key for a parent provider */
|
|
648
|
+
getChildKey(parentBaseUrl: string): ChildKeyEntry | null;
|
|
649
|
+
/** Store a child key for a parent provider */
|
|
650
|
+
setChildKey(parentBaseUrl: string, childKey: string, balance?: number, validityDate?: number, balanceLimit?: number): void;
|
|
651
|
+
/** Update balance for an existing child key */
|
|
652
|
+
updateChildKeyBalance(parentBaseUrl: string, balance: number): void;
|
|
653
|
+
/** Remove child key for a parent provider */
|
|
654
|
+
removeChildKey(parentBaseUrl: string): void;
|
|
655
|
+
/** Get all stored child keys */
|
|
656
|
+
getAllChildKeys(): ChildKeyEntry[];
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* ProviderRegistry - Provides access to provider/model data
|
|
660
|
+
* Used by ProviderManager for failover logic
|
|
661
|
+
*/
|
|
662
|
+
interface ProviderRegistry {
|
|
663
|
+
/** Get all models available from a provider */
|
|
664
|
+
getModelsForProvider(baseUrl: string): Model[];
|
|
665
|
+
/** Get list of disabled provider URLs */
|
|
666
|
+
getDisabledProviders(): string[];
|
|
667
|
+
/** Get mints accepted by a provider */
|
|
668
|
+
getProviderMints(baseUrl: string): string[];
|
|
669
|
+
/**
|
|
670
|
+
* Get provider info (version, etc.)
|
|
671
|
+
* Should fetch from network if not cached, or return cached version
|
|
672
|
+
*/
|
|
673
|
+
getProviderInfo(baseUrl: string): Promise<ProviderInfo | null>;
|
|
674
|
+
/** Get all providers with their models */
|
|
675
|
+
getAllProvidersModels(): Record<string, Model[]>;
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* StreamingCallbacks - Callbacks for real-time updates during API calls
|
|
679
|
+
* Used by RoutstrClient to communicate with the UI
|
|
680
|
+
*/
|
|
681
|
+
interface StreamingCallbacks {
|
|
682
|
+
/** Called when new content arrives from the stream */
|
|
683
|
+
onStreamingUpdate: (content: string) => void;
|
|
684
|
+
/** Called when thinking/reasoning content arrives */
|
|
685
|
+
onThinkingUpdate: (content: string) => void;
|
|
686
|
+
/** Called when a complete message should be appended */
|
|
687
|
+
onMessageAppend: (message: Message) => void;
|
|
688
|
+
/** Called when balance changes */
|
|
689
|
+
onBalanceUpdate: (balance: number) => void;
|
|
690
|
+
/** Called when a transaction is recorded */
|
|
691
|
+
onTransactionUpdate: (transaction: TransactionHistory) => void;
|
|
692
|
+
/** Called when a new token is created (amount in sats) */
|
|
693
|
+
onTokenCreated?: (amount: number) => void;
|
|
694
|
+
/** Called when payment processing starts/stops */
|
|
695
|
+
onPaymentProcessing?: (isProcessing: boolean) => void;
|
|
696
|
+
/** Called when sats spent on the last message is known */
|
|
697
|
+
onLastMessageSatsUpdate?: (satsSpent: number, estimatedCosts: number) => void;
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Options for creating a RoutstrClient
|
|
701
|
+
*/
|
|
702
|
+
interface RoutstrClientOptions {
|
|
703
|
+
/** Wallet adapter for cashu operations */
|
|
704
|
+
walletAdapter: WalletAdapter;
|
|
705
|
+
/** Storage adapter for token management */
|
|
706
|
+
storageAdapter: StorageAdapter;
|
|
707
|
+
/** Provider registry for failover logic */
|
|
708
|
+
providerRegistry: ProviderRegistry;
|
|
709
|
+
/** Nostr relay URLs (for future nostr-based features) */
|
|
710
|
+
relayUrls?: string[];
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* BalanceManager - Handles refunding and topping up tokens from providers
|
|
715
|
+
*
|
|
716
|
+
* Handles:
|
|
717
|
+
* - Fetching refund tokens from provider API
|
|
718
|
+
* - Receiving/storing refunded tokens
|
|
719
|
+
* - Topping up API key balances with cashu tokens
|
|
720
|
+
* - Error handling for various refund/topup failure modes
|
|
721
|
+
*
|
|
722
|
+
* Extracted from utils/cashuUtils.ts
|
|
723
|
+
*/
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Options for refunding tokens
|
|
727
|
+
*/
|
|
728
|
+
interface RefundOptions {
|
|
729
|
+
/** The mint URL (for NIP-60 wallet operations) */
|
|
730
|
+
mintUrl: string;
|
|
731
|
+
/** The provider base URL */
|
|
732
|
+
baseUrl: string;
|
|
733
|
+
/** Optional specific token to refund (if not provided, uses stored token) */
|
|
734
|
+
token?: string;
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Options for topping up API key balance
|
|
738
|
+
*/
|
|
739
|
+
interface TopUpOptions {
|
|
740
|
+
/** The mint URL to spend from */
|
|
741
|
+
mintUrl: string;
|
|
742
|
+
/** The provider base URL */
|
|
743
|
+
baseUrl: string;
|
|
744
|
+
/** Amount to top up in sats */
|
|
745
|
+
amount: number;
|
|
746
|
+
/** Optional specific API key to top up (if not provided, uses stored token) */
|
|
747
|
+
token?: string;
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* BalanceManager handles token refunds and topups from providers
|
|
751
|
+
*/
|
|
752
|
+
declare class BalanceManager$1 {
|
|
753
|
+
private walletAdapter;
|
|
754
|
+
private storageAdapter;
|
|
755
|
+
constructor(walletAdapter: WalletAdapter, storageAdapter: StorageAdapter);
|
|
756
|
+
/**
|
|
757
|
+
* Unified refund - handles both NIP-60 and legacy wallet refunds
|
|
758
|
+
*/
|
|
759
|
+
refund(options: RefundOptions): Promise<RefundResult>;
|
|
760
|
+
/**
|
|
761
|
+
* Top up API key balance with a cashu token
|
|
762
|
+
*/
|
|
763
|
+
topUp(options: TopUpOptions): Promise<TopUpResult>;
|
|
764
|
+
/**
|
|
765
|
+
* Fetch refund token from provider API
|
|
766
|
+
*/
|
|
767
|
+
private _fetchRefundToken;
|
|
768
|
+
/**
|
|
769
|
+
* Post topup request to provider API
|
|
770
|
+
*/
|
|
771
|
+
private _postTopUp;
|
|
772
|
+
/**
|
|
773
|
+
* Attempt to receive token back after failed top up
|
|
774
|
+
*/
|
|
775
|
+
private _recoverFailedTopUp;
|
|
776
|
+
/**
|
|
777
|
+
* Handle refund errors with specific error types
|
|
778
|
+
*/
|
|
779
|
+
private _handleRefundError;
|
|
780
|
+
/**
|
|
781
|
+
* Get token balance from provider
|
|
782
|
+
*/
|
|
783
|
+
getTokenBalance(token: string, baseUrl: string): Promise<{
|
|
784
|
+
amount: number;
|
|
785
|
+
reserved: number;
|
|
786
|
+
unit: "sat" | "msat";
|
|
787
|
+
apiKey: string;
|
|
788
|
+
}>;
|
|
789
|
+
/**
|
|
790
|
+
* Handle topup errors with specific error types
|
|
791
|
+
*/
|
|
792
|
+
private _handleTopUpError;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* CashuSpender - Core spending logic for Cashu tokens
|
|
797
|
+
*
|
|
798
|
+
* Handles:
|
|
799
|
+
* - Mint selection with sufficient balance
|
|
800
|
+
* - Provider mint compatibility checks
|
|
801
|
+
* - Retry logic with alternate mints
|
|
802
|
+
* - Critical section management (busy state)
|
|
803
|
+
*
|
|
804
|
+
* Extracted from hooks/useCashuWithXYZ.ts
|
|
805
|
+
*/
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Options for spending cashu tokens
|
|
809
|
+
*/
|
|
810
|
+
interface SpendOptions {
|
|
811
|
+
/** The mint URL to send from (can be overridden if insufficient balance) */
|
|
812
|
+
mintUrl: string;
|
|
813
|
+
/** The amount to spend in sats */
|
|
814
|
+
amount: number;
|
|
815
|
+
/** The provider base URL (for token storage and provider mint checks) */
|
|
816
|
+
baseUrl: string;
|
|
817
|
+
/** Whether to reuse an existing token if available */
|
|
818
|
+
reuseToken?: boolean;
|
|
819
|
+
/** Optional P2PK public key */
|
|
820
|
+
p2pkPubkey?: string;
|
|
821
|
+
/** Array of mint URLs to exclude (for retry logic) */
|
|
822
|
+
excludeMints?: string[];
|
|
823
|
+
/** Current retry count (for internal recursion) */
|
|
824
|
+
retryCount?: number;
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* CashuSpender manages the spending of Cashu tokens
|
|
828
|
+
*/
|
|
829
|
+
declare class CashuSpender$1 {
|
|
830
|
+
private walletAdapter;
|
|
831
|
+
private storageAdapter;
|
|
832
|
+
private providerRegistry?;
|
|
833
|
+
private balanceManager?;
|
|
834
|
+
private _isBusy;
|
|
835
|
+
constructor(walletAdapter: WalletAdapter, storageAdapter: StorageAdapter, providerRegistry?: ProviderRegistry | undefined, balanceManager?: BalanceManager$1 | undefined);
|
|
836
|
+
private _getBalanceState;
|
|
837
|
+
private _logTransaction;
|
|
838
|
+
/**
|
|
839
|
+
* Check if the spender is currently in a critical operation
|
|
840
|
+
*/
|
|
841
|
+
get isBusy(): boolean;
|
|
842
|
+
/**
|
|
843
|
+
* Spend Cashu tokens with automatic mint selection and retry logic
|
|
844
|
+
*/
|
|
845
|
+
spend(options: SpendOptions): Promise<SpendResult>;
|
|
846
|
+
/**
|
|
847
|
+
* Internal spending logic
|
|
848
|
+
*/
|
|
849
|
+
private _spendInternal;
|
|
850
|
+
/**
|
|
851
|
+
* Try to reuse an existing token
|
|
852
|
+
*/
|
|
853
|
+
private _tryReuseToken;
|
|
854
|
+
/**
|
|
855
|
+
* Refund pending tokens and retry
|
|
856
|
+
*/
|
|
857
|
+
private _refundAndRetry;
|
|
858
|
+
/**
|
|
859
|
+
* Find an alternate mint that the provider accepts
|
|
860
|
+
*/
|
|
861
|
+
private _findAlternateMint;
|
|
862
|
+
/**
|
|
863
|
+
* Handle send errors with retry logic for network errors
|
|
864
|
+
*/
|
|
865
|
+
private _handleSendError;
|
|
866
|
+
/**
|
|
867
|
+
* Select a mint with sufficient balance
|
|
868
|
+
*/
|
|
869
|
+
private _selectMintWithBalance;
|
|
870
|
+
/**
|
|
871
|
+
* Create an insufficient balance error result
|
|
872
|
+
*/
|
|
873
|
+
private _createInsufficientBalanceError;
|
|
874
|
+
private _getProviderTokenBalance;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* ProviderManager - Handles provider selection and failover logic
|
|
879
|
+
*
|
|
880
|
+
* Handles:
|
|
881
|
+
* - Finding the best provider for a model based on price
|
|
882
|
+
* - Provider failover when errors occur
|
|
883
|
+
* - Tracking failed providers to avoid retry loops
|
|
884
|
+
* - Provider version compatibility
|
|
885
|
+
*
|
|
886
|
+
* Extracted from utils/apiUtils.ts findNextBestProvider and related logic
|
|
887
|
+
*/
|
|
888
|
+
|
|
889
|
+
interface ModelProviderPrice {
|
|
890
|
+
baseUrl: string;
|
|
891
|
+
model: Model;
|
|
892
|
+
promptPerMillion: number;
|
|
893
|
+
completionPerMillion: number;
|
|
894
|
+
totalPerMillion: number;
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* ProviderManager handles provider selection and failover
|
|
898
|
+
*/
|
|
899
|
+
declare class ProviderManager$1 {
|
|
900
|
+
private providerRegistry;
|
|
901
|
+
private failedProviders;
|
|
902
|
+
constructor(providerRegistry: ProviderRegistry);
|
|
903
|
+
/**
|
|
904
|
+
* Reset the failed providers list
|
|
905
|
+
*/
|
|
906
|
+
resetFailedProviders(): void;
|
|
907
|
+
/**
|
|
908
|
+
* Mark a provider as failed
|
|
909
|
+
*/
|
|
910
|
+
markFailed(baseUrl: string): void;
|
|
911
|
+
/**
|
|
912
|
+
* Check if a provider has failed
|
|
913
|
+
*/
|
|
914
|
+
hasFailed(baseUrl: string): boolean;
|
|
915
|
+
/**
|
|
916
|
+
* Find the next best provider for a model
|
|
917
|
+
* @param modelId The model ID to find a provider for
|
|
918
|
+
* @param currentBaseUrl The current provider to exclude
|
|
919
|
+
* @returns The best provider URL or null if none available
|
|
920
|
+
*/
|
|
921
|
+
findNextBestProvider(modelId: string, currentBaseUrl: string): string | null;
|
|
922
|
+
/**
|
|
923
|
+
* Find the best model for a provider
|
|
924
|
+
* Useful when switching providers and need to find equivalent model
|
|
925
|
+
*/
|
|
926
|
+
getModelForProvider(baseUrl: string, modelId: string): Promise<Model | null>;
|
|
927
|
+
/**
|
|
928
|
+
* Get all available providers for a model
|
|
929
|
+
* Returns sorted list by price
|
|
930
|
+
*/
|
|
931
|
+
getAllProvidersForModel(modelId: string): Array<{
|
|
932
|
+
baseUrl: string;
|
|
933
|
+
model: Model;
|
|
934
|
+
cost: number;
|
|
935
|
+
}>;
|
|
936
|
+
/**
|
|
937
|
+
* Get providers for a model sorted by prompt+completion pricing
|
|
938
|
+
*/
|
|
939
|
+
getProviderPriceRankingForModel(modelId: string, options?: {
|
|
940
|
+
torMode?: boolean;
|
|
941
|
+
includeDisabled?: boolean;
|
|
942
|
+
}): ModelProviderPrice[];
|
|
943
|
+
/**
|
|
944
|
+
* Get best-priced provider for a specific model
|
|
945
|
+
*/
|
|
946
|
+
getBestProviderForModel(modelId: string, options?: {
|
|
947
|
+
torMode?: boolean;
|
|
948
|
+
includeDisabled?: boolean;
|
|
949
|
+
}): string | null;
|
|
950
|
+
private normalizeModelId;
|
|
951
|
+
/**
|
|
952
|
+
* Check if a provider accepts a specific mint
|
|
953
|
+
*/
|
|
954
|
+
providerAcceptsMint(baseUrl: string, mintUrl: string): boolean;
|
|
955
|
+
/**
|
|
956
|
+
* Get required sats for a model based on message history
|
|
957
|
+
* Simple estimation based on typical usage
|
|
958
|
+
*/
|
|
959
|
+
getRequiredSatsForModel(model: Model, apiMessages: any[], maxTokens?: number): number;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* RoutstrClient - Main API client for Routstr
|
|
964
|
+
*
|
|
965
|
+
* Orchestrates:
|
|
966
|
+
* - Token spending via CashuSpender
|
|
967
|
+
* - API requests with authentication
|
|
968
|
+
* - Streaming response processing
|
|
969
|
+
* - Provider failover via ProviderManager
|
|
970
|
+
* - Error handling and refunds
|
|
971
|
+
*
|
|
972
|
+
* Extracted from utils/apiUtils.ts
|
|
973
|
+
*/
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Options for fetching AI response
|
|
977
|
+
*/
|
|
978
|
+
interface FetchOptions {
|
|
979
|
+
messageHistory: Message[];
|
|
980
|
+
selectedModel: Model;
|
|
981
|
+
baseUrl: string;
|
|
982
|
+
mintUrl: string;
|
|
983
|
+
balance: number;
|
|
984
|
+
transactionHistory: TransactionHistory[];
|
|
985
|
+
maxTokens?: number;
|
|
986
|
+
headers?: Record<string, string>;
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* RoutstrClient is the main SDK entry point
|
|
990
|
+
*/
|
|
991
|
+
type AlertLevel = "max" | "min";
|
|
992
|
+
type RoutstrClientMode = "xcashu" | "lazyrefund" | "apikeys";
|
|
993
|
+
interface RouteRequestParams {
|
|
994
|
+
path: string;
|
|
995
|
+
method: string;
|
|
996
|
+
body?: unknown;
|
|
997
|
+
headers?: Record<string, string>;
|
|
998
|
+
baseUrl: string;
|
|
999
|
+
mintUrl: string;
|
|
1000
|
+
modelId?: string;
|
|
1001
|
+
}
|
|
1002
|
+
declare class RoutstrClient$1 {
|
|
1003
|
+
private walletAdapter;
|
|
1004
|
+
private storageAdapter;
|
|
1005
|
+
private providerRegistry;
|
|
1006
|
+
private cashuSpender;
|
|
1007
|
+
private balanceManager;
|
|
1008
|
+
private streamProcessor;
|
|
1009
|
+
private providerManager;
|
|
1010
|
+
private alertLevel;
|
|
1011
|
+
private mode;
|
|
1012
|
+
constructor(walletAdapter: WalletAdapter, storageAdapter: StorageAdapter, providerRegistry: ProviderRegistry, alertLevel: AlertLevel, mode?: RoutstrClientMode);
|
|
1013
|
+
/**
|
|
1014
|
+
* Get the current client mode
|
|
1015
|
+
*/
|
|
1016
|
+
getMode(): RoutstrClientMode;
|
|
1017
|
+
/**
|
|
1018
|
+
* Get the CashuSpender instance
|
|
1019
|
+
*/
|
|
1020
|
+
getCashuSpender(): CashuSpender$1;
|
|
1021
|
+
/**
|
|
1022
|
+
* Get the BalanceManager instance
|
|
1023
|
+
*/
|
|
1024
|
+
getBalanceManager(): BalanceManager$1;
|
|
1025
|
+
/**
|
|
1026
|
+
* Get the ProviderManager instance
|
|
1027
|
+
*/
|
|
1028
|
+
getProviderManager(): ProviderManager$1;
|
|
1029
|
+
/**
|
|
1030
|
+
* Check if the client is currently busy (in critical section)
|
|
1031
|
+
*/
|
|
1032
|
+
get isBusy(): boolean;
|
|
1033
|
+
/**
|
|
1034
|
+
* Route an API request to the upstream provider
|
|
1035
|
+
*
|
|
1036
|
+
* This is a simpler alternative to fetchAIResponse that just proxies
|
|
1037
|
+
* the request upstream without the streaming callback machinery.
|
|
1038
|
+
* Useful for daemon-style routing where you just need to forward
|
|
1039
|
+
* requests and get responses back.
|
|
1040
|
+
*/
|
|
1041
|
+
routeRequest(params: RouteRequestParams): Promise<Response>;
|
|
1042
|
+
/**
|
|
1043
|
+
* Fetch AI response with streaming
|
|
1044
|
+
*/
|
|
1045
|
+
fetchAIResponse(options: FetchOptions, callbacks: StreamingCallbacks): Promise<void>;
|
|
1046
|
+
/**
|
|
1047
|
+
* Make the API request with failover support
|
|
1048
|
+
*/
|
|
1049
|
+
private _makeRequest;
|
|
1050
|
+
/**
|
|
1051
|
+
* Handle error responses with failover
|
|
1052
|
+
*/
|
|
1053
|
+
private _handleErrorResponse;
|
|
1054
|
+
/**
|
|
1055
|
+
* Handle network errors with failover
|
|
1056
|
+
*/
|
|
1057
|
+
private _handleNetworkError;
|
|
1058
|
+
/**
|
|
1059
|
+
* Handle post-response refund and balance updates
|
|
1060
|
+
*/
|
|
1061
|
+
private _handlePostResponseRefund;
|
|
1062
|
+
/**
|
|
1063
|
+
* Handle post-response balance update for all modes
|
|
1064
|
+
*/
|
|
1065
|
+
private _handlePostResponseBalanceUpdate;
|
|
1066
|
+
/**
|
|
1067
|
+
* Convert messages for API format
|
|
1068
|
+
*/
|
|
1069
|
+
private _convertMessages;
|
|
1070
|
+
/**
|
|
1071
|
+
* Create assistant message from streaming result
|
|
1072
|
+
*/
|
|
1073
|
+
private _createAssistantMessage;
|
|
1074
|
+
/**
|
|
1075
|
+
* Create a child key for a parent API key via the provider's API
|
|
1076
|
+
* POST /v1/balance/child-key
|
|
1077
|
+
*/
|
|
1078
|
+
private _createChildKey;
|
|
1079
|
+
/**
|
|
1080
|
+
* Get balance for an API key from the provider
|
|
1081
|
+
*/
|
|
1082
|
+
private _getApiKeyBalance;
|
|
1083
|
+
/**
|
|
1084
|
+
* Calculate estimated costs from usage
|
|
1085
|
+
*/
|
|
1086
|
+
private _getEstimatedCosts;
|
|
1087
|
+
/**
|
|
1088
|
+
* Get pending cashu token amount
|
|
1089
|
+
*/
|
|
1090
|
+
private _getPendingCashuTokenAmount;
|
|
1091
|
+
/**
|
|
1092
|
+
* Check if error message indicates a network error
|
|
1093
|
+
*/
|
|
1094
|
+
private _isNetworkError;
|
|
1095
|
+
/**
|
|
1096
|
+
* Handle errors and notify callbacks
|
|
1097
|
+
*/
|
|
1098
|
+
private _handleError;
|
|
1099
|
+
/**
|
|
1100
|
+
* Check wallet balance and throw if insufficient
|
|
1101
|
+
*/
|
|
1102
|
+
private _checkBalance;
|
|
1103
|
+
/**
|
|
1104
|
+
* Spend a token using CashuSpender with standardized error handling
|
|
1105
|
+
*/
|
|
1106
|
+
private _spendToken;
|
|
1107
|
+
/**
|
|
1108
|
+
* Build request headers with common defaults and dev mock controls
|
|
1109
|
+
*/
|
|
1110
|
+
private _buildBaseHeaders;
|
|
1111
|
+
/**
|
|
1112
|
+
* Attach auth headers using the active client mode
|
|
1113
|
+
*/
|
|
1114
|
+
private _withAuthHeader;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* StreamProcessor - Handles SSE streaming response parsing
|
|
1119
|
+
*
|
|
1120
|
+
* Handles:
|
|
1121
|
+
* - Line buffering for large payloads
|
|
1122
|
+
* - Content extraction from delta chunks
|
|
1123
|
+
* - Thinking/reasoning block extraction
|
|
1124
|
+
* - Image data merging and deduplication
|
|
1125
|
+
* - Usage statistics extraction
|
|
1126
|
+
* - Citations and annotations
|
|
1127
|
+
*
|
|
1128
|
+
* Extracted from utils/apiUtils.ts processStreamingResponse
|
|
1129
|
+
*/
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* Callbacks for streaming updates
|
|
1133
|
+
*/
|
|
1134
|
+
interface StreamCallbacks {
|
|
1135
|
+
/** Called when new content arrives */
|
|
1136
|
+
onContent: (content: string) => void;
|
|
1137
|
+
/** Called when thinking content arrives */
|
|
1138
|
+
onThinking: (thinking: string) => void;
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* StreamProcessor parses SSE streaming responses
|
|
1142
|
+
*/
|
|
1143
|
+
declare class StreamProcessor$1 {
|
|
1144
|
+
private accumulatedContent;
|
|
1145
|
+
private accumulatedThinking;
|
|
1146
|
+
private accumulatedImages;
|
|
1147
|
+
private isInThinking;
|
|
1148
|
+
private isInContent;
|
|
1149
|
+
/**
|
|
1150
|
+
* Process a streaming response
|
|
1151
|
+
*/
|
|
1152
|
+
process(response: Response, callbacks: StreamCallbacks, modelId?: string): Promise<StreamingResult>;
|
|
1153
|
+
/**
|
|
1154
|
+
* Parse a single SSE line
|
|
1155
|
+
*/
|
|
1156
|
+
private _parseLine;
|
|
1157
|
+
/**
|
|
1158
|
+
* Handle content delta with thinking support
|
|
1159
|
+
*/
|
|
1160
|
+
private _handleContent;
|
|
1161
|
+
/**
|
|
1162
|
+
* Handle thinking/reasoning content
|
|
1163
|
+
*/
|
|
1164
|
+
private _handleThinking;
|
|
1165
|
+
/**
|
|
1166
|
+
* Extract thinking blocks from content (for models with inline thinking)
|
|
1167
|
+
*/
|
|
1168
|
+
private _extractThinkingFromContent;
|
|
1169
|
+
/**
|
|
1170
|
+
* Merge images into accumulated array, avoiding duplicates
|
|
1171
|
+
*/
|
|
1172
|
+
private _mergeImages;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
type ProviderDirectoryEntry = {
|
|
1176
|
+
endpoint_url?: string | null;
|
|
1177
|
+
endpoint_urls?: string[] | null;
|
|
1178
|
+
onion_url?: string | null;
|
|
1179
|
+
onion_urls?: string[] | null;
|
|
1180
|
+
name?: string | null;
|
|
1181
|
+
};
|
|
1182
|
+
declare const isTorContext$1: () => boolean;
|
|
1183
|
+
declare const isOnionUrl$1: (url: string) => boolean;
|
|
1184
|
+
declare const normalizeProviderUrl$1: (url?: string | null, torMode?: boolean) => string | null;
|
|
1185
|
+
declare const getProviderEndpoints$1: (provider: ProviderDirectoryEntry, torMode: boolean) => string[];
|
|
1186
|
+
declare const filterBaseUrlsForTor$1: (baseUrls: string[], torMode: boolean) => string[];
|
|
1187
|
+
|
|
1188
|
+
var vanilla = require('zustand/vanilla');
|
|
1189
|
+
var cashuTs = require('@cashu/cashu-ts');
|
|
1190
|
+
|
|
1191
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
1192
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
1193
|
+
}) : x)(function(x) {
|
|
1194
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
1195
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
// core/errors.ts
|
|
1199
|
+
var InsufficientBalanceError = class extends Error {
|
|
1200
|
+
constructor(required, available, maxMintBalance = 0, maxMintUrl = "") {
|
|
1201
|
+
super(
|
|
1202
|
+
`Insufficient balance: need ${required} sats, have ${available} sats available. ` + (maxMintBalance > 0 ? `Largest mint balance: ${maxMintBalance} sats from ${maxMintUrl}` : "")
|
|
1203
|
+
);
|
|
1204
|
+
this.required = required;
|
|
1205
|
+
this.available = available;
|
|
1206
|
+
this.maxMintBalance = maxMintBalance;
|
|
1207
|
+
this.maxMintUrl = maxMintUrl;
|
|
1208
|
+
this.name = "InsufficientBalanceError";
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
var ProviderError = class extends Error {
|
|
1212
|
+
constructor(baseUrl, statusCode, message, requestId) {
|
|
1213
|
+
super(
|
|
1214
|
+
`Provider ${baseUrl} returned ${statusCode}: ${message}` + (requestId ? ` (Request ID: ${requestId})` : "")
|
|
1215
|
+
);
|
|
1216
|
+
this.baseUrl = baseUrl;
|
|
1217
|
+
this.statusCode = statusCode;
|
|
1218
|
+
this.requestId = requestId;
|
|
1219
|
+
this.name = "ProviderError";
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1222
|
+
var MintUnreachableError = class extends Error {
|
|
1223
|
+
constructor(mintUrl) {
|
|
1224
|
+
super(
|
|
1225
|
+
`Your mint ${mintUrl} is unreachable or is blocking your IP. Please try again later or switch mints.`
|
|
1226
|
+
);
|
|
1227
|
+
this.mintUrl = mintUrl;
|
|
1228
|
+
this.name = "MintUnreachableError";
|
|
1229
|
+
}
|
|
1230
|
+
};
|
|
1231
|
+
var TokenOperationError = class extends Error {
|
|
1232
|
+
constructor(message, operation, mintUrl) {
|
|
1233
|
+
super(message);
|
|
1234
|
+
this.operation = operation;
|
|
1235
|
+
this.mintUrl = mintUrl;
|
|
1236
|
+
this.name = "TokenOperationError";
|
|
1237
|
+
}
|
|
1238
|
+
};
|
|
1239
|
+
var FailoverError = class extends Error {
|
|
1240
|
+
constructor(originalProvider, failedProviders, message) {
|
|
1241
|
+
super(
|
|
1242
|
+
message || `All providers failed. Original: ${originalProvider}, Failed: ${failedProviders.join(", ")}`
|
|
1243
|
+
);
|
|
1244
|
+
this.originalProvider = originalProvider;
|
|
1245
|
+
this.failedProviders = failedProviders;
|
|
1246
|
+
this.name = "FailoverError";
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1249
|
+
var StreamingError = class extends Error {
|
|
1250
|
+
constructor(message, finishReason, accumulatedContent) {
|
|
1251
|
+
super(message);
|
|
1252
|
+
this.finishReason = finishReason;
|
|
1253
|
+
this.accumulatedContent = accumulatedContent;
|
|
1254
|
+
this.name = "StreamingError";
|
|
1255
|
+
}
|
|
1256
|
+
};
|
|
1257
|
+
var ModelNotFoundError = class extends Error {
|
|
1258
|
+
constructor(modelId, baseUrl) {
|
|
1259
|
+
super(`Model '${modelId}' not found on provider ${baseUrl}`);
|
|
1260
|
+
this.modelId = modelId;
|
|
1261
|
+
this.baseUrl = baseUrl;
|
|
1262
|
+
this.name = "ModelNotFoundError";
|
|
1263
|
+
}
|
|
1264
|
+
};
|
|
1265
|
+
var ProviderBootstrapError = class extends Error {
|
|
1266
|
+
constructor(failedProviders, message) {
|
|
1267
|
+
super(
|
|
1268
|
+
message || `Failed to bootstrap providers. Tried: ${failedProviders.join(", ")}`
|
|
1269
|
+
);
|
|
1270
|
+
this.failedProviders = failedProviders;
|
|
1271
|
+
this.name = "ProviderBootstrapError";
|
|
1272
|
+
}
|
|
1273
|
+
};
|
|
1274
|
+
var NoProvidersAvailableError = class extends Error {
|
|
1275
|
+
constructor() {
|
|
1276
|
+
super("No providers are available for model discovery");
|
|
1277
|
+
this.name = "NoProvidersAvailableError";
|
|
1278
|
+
}
|
|
1279
|
+
};
|
|
1280
|
+
var MintDiscoveryError = class extends Error {
|
|
1281
|
+
constructor(baseUrl, message) {
|
|
1282
|
+
super(message || `Failed to discover mints from provider ${baseUrl}`);
|
|
1283
|
+
this.baseUrl = baseUrl;
|
|
1284
|
+
this.name = "MintDiscoveryError";
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
|
|
1288
|
+
// discovery/ModelManager.ts
|
|
1289
|
+
var ModelManager = class _ModelManager {
|
|
1290
|
+
constructor(adapter, config = {}) {
|
|
1291
|
+
this.adapter = adapter;
|
|
1292
|
+
this.providerDirectoryUrl = config.providerDirectoryUrl || "https://api.routstr.com/v1/providers/";
|
|
1293
|
+
this.cacheTTL = config.cacheTTL || 21 * 60 * 1e3;
|
|
1294
|
+
this.includeProviderUrls = config.includeProviderUrls || [];
|
|
1295
|
+
this.excludeProviderUrls = config.excludeProviderUrls || [];
|
|
1296
|
+
}
|
|
1297
|
+
cacheTTL;
|
|
1298
|
+
providerDirectoryUrl;
|
|
1299
|
+
includeProviderUrls;
|
|
1300
|
+
excludeProviderUrls;
|
|
1301
|
+
/**
|
|
1302
|
+
* Get the list of bootstrapped provider base URLs
|
|
1303
|
+
* @returns Array of provider base URLs
|
|
1304
|
+
*/
|
|
1305
|
+
getBaseUrls() {
|
|
1306
|
+
return this.adapter.getBaseUrlsList();
|
|
1307
|
+
}
|
|
1308
|
+
static async init(adapter, config = {}, options = {}) {
|
|
1309
|
+
const manager = new _ModelManager(adapter, config);
|
|
1310
|
+
const torMode = options.torMode ?? false;
|
|
1311
|
+
const forceRefresh = options.forceRefresh ?? false;
|
|
1312
|
+
const providers = await manager.bootstrapProviders(torMode);
|
|
1313
|
+
await manager.fetchModels(providers, forceRefresh);
|
|
1314
|
+
return manager;
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Bootstrap provider list from the provider directory
|
|
1318
|
+
* Fetches available providers and caches their base URLs
|
|
1319
|
+
* @param torMode Whether running in Tor context
|
|
1320
|
+
* @returns Array of provider base URLs
|
|
1321
|
+
* @throws ProviderBootstrapError if all providers fail to fetch
|
|
1322
|
+
*/
|
|
1323
|
+
async bootstrapProviders(torMode = false) {
|
|
1324
|
+
try {
|
|
1325
|
+
const cachedUrls = this.adapter.getBaseUrlsList();
|
|
1326
|
+
if (cachedUrls.length > 0) {
|
|
1327
|
+
const lastUpdate = this.adapter.getBaseUrlsLastUpdate();
|
|
1328
|
+
const cacheValid = lastUpdate && Date.now() - lastUpdate <= this.cacheTTL;
|
|
1329
|
+
if (cacheValid) {
|
|
1330
|
+
return this.filterBaseUrlsForTor(cachedUrls, torMode);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
const res = await fetch(this.providerDirectoryUrl);
|
|
1334
|
+
if (!res.ok) {
|
|
1335
|
+
throw new Error(`Failed to fetch providers: ${res.status}`);
|
|
1336
|
+
}
|
|
1337
|
+
const data = await res.json();
|
|
1338
|
+
const providers = Array.isArray(data?.providers) ? data.providers : [];
|
|
1339
|
+
const bases = /* @__PURE__ */ new Set();
|
|
1340
|
+
for (const p of providers) {
|
|
1341
|
+
const endpoints = this.getProviderEndpoints(p, torMode);
|
|
1342
|
+
for (const endpoint of endpoints) {
|
|
1343
|
+
bases.add(endpoint);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
for (const url of this.includeProviderUrls) {
|
|
1347
|
+
const normalized = this.normalizeUrl(url);
|
|
1348
|
+
if (!torMode || normalized.includes(".onion")) {
|
|
1349
|
+
bases.add(normalized);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
const excluded = new Set(
|
|
1353
|
+
this.excludeProviderUrls.map((url) => this.normalizeUrl(url))
|
|
1354
|
+
);
|
|
1355
|
+
const list = Array.from(bases).filter((base) => {
|
|
1356
|
+
if (excluded.has(base)) return false;
|
|
1357
|
+
return true;
|
|
1358
|
+
});
|
|
1359
|
+
if (list.length > 0) {
|
|
1360
|
+
this.adapter.setBaseUrlsList(list);
|
|
1361
|
+
this.adapter.setBaseUrlsLastUpdate(Date.now());
|
|
1362
|
+
}
|
|
1363
|
+
return list;
|
|
1364
|
+
} catch (e) {
|
|
1365
|
+
console.error("Failed to bootstrap providers", e);
|
|
1366
|
+
throw new ProviderBootstrapError([], `Provider bootstrap failed: ${e}`);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* Fetch models from all providers and select best-priced options
|
|
1371
|
+
* Uses cache if available and not expired
|
|
1372
|
+
* @param baseUrls List of provider base URLs to fetch from
|
|
1373
|
+
* @param forceRefresh Ignore cache and fetch fresh data
|
|
1374
|
+
* @returns Array of unique models with best prices selected
|
|
1375
|
+
*/
|
|
1376
|
+
async fetchModels(baseUrls, forceRefresh = false) {
|
|
1377
|
+
if (baseUrls.length === 0) {
|
|
1378
|
+
throw new NoProvidersAvailableError();
|
|
1379
|
+
}
|
|
1380
|
+
const bestById = /* @__PURE__ */ new Map();
|
|
1381
|
+
const modelsFromAllProviders = {};
|
|
1382
|
+
const disabledProviders = this.adapter.getDisabledProviders();
|
|
1383
|
+
const estimateMinCost = (m) => {
|
|
1384
|
+
return m?.sats_pricing?.completion ?? 0;
|
|
1385
|
+
};
|
|
1386
|
+
const fetchPromises = baseUrls.map(async (url) => {
|
|
1387
|
+
const base = url.endsWith("/") ? url : `${url}/`;
|
|
1388
|
+
try {
|
|
1389
|
+
let list;
|
|
1390
|
+
if (!forceRefresh) {
|
|
1391
|
+
const lastUpdate = this.adapter.getProviderLastUpdate(base);
|
|
1392
|
+
const cacheValid = lastUpdate && Date.now() - lastUpdate <= this.cacheTTL;
|
|
1393
|
+
if (cacheValid) {
|
|
1394
|
+
const cachedModels = this.adapter.getCachedModels();
|
|
1395
|
+
const cachedList = cachedModels[base] || [];
|
|
1396
|
+
list = cachedList;
|
|
1397
|
+
} else {
|
|
1398
|
+
list = await this.fetchModelsFromProvider(base);
|
|
1399
|
+
}
|
|
1400
|
+
} else {
|
|
1401
|
+
list = await this.fetchModelsFromProvider(base);
|
|
1402
|
+
}
|
|
1403
|
+
modelsFromAllProviders[base] = list;
|
|
1404
|
+
this.adapter.setProviderLastUpdate(base, Date.now());
|
|
1405
|
+
if (!disabledProviders.includes(base)) {
|
|
1406
|
+
for (const m of list) {
|
|
1407
|
+
const existing = bestById.get(m.id);
|
|
1408
|
+
if (!m.sats_pricing) continue;
|
|
1409
|
+
if (!existing) {
|
|
1410
|
+
bestById.set(m.id, { model: m, base });
|
|
1411
|
+
continue;
|
|
1412
|
+
}
|
|
1413
|
+
const currentCost = estimateMinCost(m);
|
|
1414
|
+
const existingCost = estimateMinCost(existing.model);
|
|
1415
|
+
if (currentCost < existingCost && m.sats_pricing) {
|
|
1416
|
+
bestById.set(m.id, { model: m, base });
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
return { success: true, base, list };
|
|
1421
|
+
} catch (error) {
|
|
1422
|
+
if (this.isProviderDownError(error)) {
|
|
1423
|
+
console.warn(`Provider ${base} is down right now.`);
|
|
1424
|
+
} else {
|
|
1425
|
+
console.warn(`Failed to fetch models from ${base}:`, error);
|
|
1426
|
+
}
|
|
1427
|
+
this.adapter.setProviderLastUpdate(base, Date.now());
|
|
1428
|
+
return { success: false, base };
|
|
1429
|
+
}
|
|
1430
|
+
});
|
|
1431
|
+
await Promise.allSettled(fetchPromises);
|
|
1432
|
+
const existingCache = this.adapter.getCachedModels();
|
|
1433
|
+
this.adapter.setCachedModels({
|
|
1434
|
+
...existingCache,
|
|
1435
|
+
...modelsFromAllProviders
|
|
1436
|
+
});
|
|
1437
|
+
return Array.from(bestById.values()).map((v) => v.model);
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Fetch models from a single provider
|
|
1441
|
+
* @param baseUrl Provider base URL
|
|
1442
|
+
* @returns Array of models from provider
|
|
1443
|
+
*/
|
|
1444
|
+
async fetchModelsFromProvider(baseUrl) {
|
|
1445
|
+
const res = await fetch(`${baseUrl}v1/models`);
|
|
1446
|
+
if (!res.ok) {
|
|
1447
|
+
throw new Error(`Failed to fetch models: ${res.status}`);
|
|
1448
|
+
}
|
|
1449
|
+
const json = await res.json();
|
|
1450
|
+
const list = Array.isArray(json?.data) ? json.data.map((m) => ({
|
|
1451
|
+
...m,
|
|
1452
|
+
id: m.id.split("/").pop() || m.id
|
|
1453
|
+
})) : [];
|
|
1454
|
+
return list;
|
|
1455
|
+
}
|
|
1456
|
+
isProviderDownError(error) {
|
|
1457
|
+
if (!(error instanceof Error)) return false;
|
|
1458
|
+
const msg = error.message.toLowerCase();
|
|
1459
|
+
if (msg.includes("fetch failed")) return true;
|
|
1460
|
+
if (msg.includes("502")) return true;
|
|
1461
|
+
if (msg.includes("503")) return true;
|
|
1462
|
+
if (msg.includes("504")) return true;
|
|
1463
|
+
const cause = error.cause;
|
|
1464
|
+
return cause?.code === "ENOTFOUND";
|
|
1465
|
+
}
|
|
1466
|
+
/**
|
|
1467
|
+
* Get all cached models from all providers
|
|
1468
|
+
* @returns Record mapping baseUrl -> models
|
|
1469
|
+
*/
|
|
1470
|
+
getAllCachedModels() {
|
|
1471
|
+
return this.adapter.getCachedModels();
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Clear cache for a specific provider
|
|
1475
|
+
* @param baseUrl Provider base URL
|
|
1476
|
+
*/
|
|
1477
|
+
clearProviderCache(baseUrl) {
|
|
1478
|
+
const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
1479
|
+
const cached = this.adapter.getCachedModels();
|
|
1480
|
+
delete cached[base];
|
|
1481
|
+
this.adapter.setCachedModels(cached);
|
|
1482
|
+
this.adapter.setProviderLastUpdate(base, 0);
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1485
|
+
* Clear all model caches
|
|
1486
|
+
*/
|
|
1487
|
+
clearAllCache() {
|
|
1488
|
+
this.adapter.setCachedModels({});
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* Filter base URLs based on Tor context
|
|
1492
|
+
* @param baseUrls Provider URLs to filter
|
|
1493
|
+
* @param torMode Whether in Tor context
|
|
1494
|
+
* @returns Filtered URLs appropriate for Tor mode
|
|
1495
|
+
*/
|
|
1496
|
+
filterBaseUrlsForTor(baseUrls, torMode) {
|
|
1497
|
+
if (!torMode) {
|
|
1498
|
+
return baseUrls.filter((url) => !url.includes(".onion"));
|
|
1499
|
+
}
|
|
1500
|
+
return baseUrls.filter((url) => url.includes(".onion"));
|
|
1501
|
+
}
|
|
1502
|
+
/**
|
|
1503
|
+
* Get provider endpoints from provider info
|
|
1504
|
+
* @param provider Provider object from directory
|
|
1505
|
+
* @param torMode Whether in Tor context
|
|
1506
|
+
* @returns Array of endpoint URLs
|
|
1507
|
+
*/
|
|
1508
|
+
getProviderEndpoints(provider, torMode) {
|
|
1509
|
+
const endpoints = [];
|
|
1510
|
+
if (torMode && provider.onion_url) {
|
|
1511
|
+
endpoints.push(this.normalizeUrl(provider.onion_url));
|
|
1512
|
+
} else if (provider.endpoint_url) {
|
|
1513
|
+
endpoints.push(this.normalizeUrl(provider.endpoint_url));
|
|
1514
|
+
}
|
|
1515
|
+
return endpoints;
|
|
1516
|
+
}
|
|
1517
|
+
/**
|
|
1518
|
+
* Normalize provider URL with trailing slash
|
|
1519
|
+
* @param url URL to normalize
|
|
1520
|
+
* @returns Normalized URL
|
|
1521
|
+
*/
|
|
1522
|
+
normalizeUrl(url) {
|
|
1523
|
+
if (!url.startsWith("http")) {
|
|
1524
|
+
url = `https://${url}`;
|
|
1525
|
+
}
|
|
1526
|
+
return url.endsWith("/") ? url : `${url}/`;
|
|
1527
|
+
}
|
|
1528
|
+
};
|
|
1529
|
+
|
|
1530
|
+
// discovery/MintDiscovery.ts
|
|
1531
|
+
var MintDiscovery = class {
|
|
1532
|
+
constructor(adapter, config = {}) {
|
|
1533
|
+
this.adapter = adapter;
|
|
1534
|
+
this.cacheTTL = config.cacheTTL || 21 * 60 * 1e3;
|
|
1535
|
+
}
|
|
1536
|
+
cacheTTL;
|
|
1537
|
+
/**
|
|
1538
|
+
* Fetch mints from all providers via their /v1/info endpoints
|
|
1539
|
+
* Caches mints and full provider info for later access
|
|
1540
|
+
* @param baseUrls List of provider base URLs to fetch from
|
|
1541
|
+
* @returns Object with mints and provider info from all providers
|
|
1542
|
+
*/
|
|
1543
|
+
async discoverMints(baseUrls, options = {}) {
|
|
1544
|
+
if (baseUrls.length === 0) {
|
|
1545
|
+
return { mintsFromProviders: {}, infoFromProviders: {} };
|
|
1546
|
+
}
|
|
1547
|
+
const mintsFromAllProviders = {};
|
|
1548
|
+
const infoFromAllProviders = {};
|
|
1549
|
+
const forceRefresh = options.forceRefresh ?? false;
|
|
1550
|
+
const fetchPromises = baseUrls.map(async (url) => {
|
|
1551
|
+
const base = url.endsWith("/") ? url : `${url}/`;
|
|
1552
|
+
try {
|
|
1553
|
+
if (!forceRefresh) {
|
|
1554
|
+
const lastUpdate = this.adapter.getProviderLastUpdate(base);
|
|
1555
|
+
const cacheValid = lastUpdate && Date.now() - lastUpdate <= this.cacheTTL;
|
|
1556
|
+
if (cacheValid) {
|
|
1557
|
+
const cachedMints = this.adapter.getCachedMints()[base] || [];
|
|
1558
|
+
const cachedInfo = this.adapter.getCachedProviderInfo()[base];
|
|
1559
|
+
mintsFromAllProviders[base] = cachedMints;
|
|
1560
|
+
if (cachedInfo) {
|
|
1561
|
+
infoFromAllProviders[base] = cachedInfo;
|
|
1562
|
+
}
|
|
1563
|
+
return {
|
|
1564
|
+
success: true,
|
|
1565
|
+
base,
|
|
1566
|
+
mints: cachedMints,
|
|
1567
|
+
info: cachedInfo
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
const res = await fetch(`${base}v1/info`);
|
|
1572
|
+
if (!res.ok) {
|
|
1573
|
+
throw new Error(`Failed to fetch info: ${res.status}`);
|
|
1574
|
+
}
|
|
1575
|
+
const json = await res.json();
|
|
1576
|
+
const mints = Array.isArray(json?.mints) ? json.mints : [];
|
|
1577
|
+
const normalizedMints = mints.map(
|
|
1578
|
+
(mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint
|
|
1579
|
+
);
|
|
1580
|
+
mintsFromAllProviders[base] = normalizedMints;
|
|
1581
|
+
infoFromAllProviders[base] = json;
|
|
1582
|
+
this.adapter.setProviderLastUpdate(base, Date.now());
|
|
1583
|
+
return { success: true, base, mints: normalizedMints, info: json };
|
|
1584
|
+
} catch (error) {
|
|
1585
|
+
this.adapter.setProviderLastUpdate(base, Date.now());
|
|
1586
|
+
if (this.isProviderDownError(error)) {
|
|
1587
|
+
console.warn(`Provider ${base} is down right now.`);
|
|
1588
|
+
} else {
|
|
1589
|
+
console.warn(`Failed to fetch mints from ${base}:`, error);
|
|
1590
|
+
}
|
|
1591
|
+
return { success: false, base, mints: [], info: null };
|
|
1592
|
+
}
|
|
1593
|
+
});
|
|
1594
|
+
const results = await Promise.allSettled(fetchPromises);
|
|
1595
|
+
for (const result of results) {
|
|
1596
|
+
if (result.status === "fulfilled") {
|
|
1597
|
+
const { base, mints, info } = result.value;
|
|
1598
|
+
mintsFromAllProviders[base] = mints;
|
|
1599
|
+
if (info) {
|
|
1600
|
+
infoFromAllProviders[base] = info;
|
|
1601
|
+
}
|
|
1602
|
+
} else {
|
|
1603
|
+
console.error("Mint discovery error:", result.reason);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
try {
|
|
1607
|
+
this.adapter.setCachedMints(mintsFromAllProviders);
|
|
1608
|
+
this.adapter.setCachedProviderInfo(infoFromAllProviders);
|
|
1609
|
+
} catch (error) {
|
|
1610
|
+
console.error("Error caching mint discovery results:", error);
|
|
1611
|
+
}
|
|
1612
|
+
return {
|
|
1613
|
+
mintsFromProviders: mintsFromAllProviders,
|
|
1614
|
+
infoFromProviders: infoFromAllProviders
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1618
|
+
* Get cached mints from all providers
|
|
1619
|
+
* @returns Record mapping baseUrl -> mint URLs
|
|
1620
|
+
*/
|
|
1621
|
+
getCachedMints() {
|
|
1622
|
+
return this.adapter.getCachedMints();
|
|
1623
|
+
}
|
|
1624
|
+
/**
|
|
1625
|
+
* Get cached provider info from all providers
|
|
1626
|
+
* @returns Record mapping baseUrl -> provider info
|
|
1627
|
+
*/
|
|
1628
|
+
getCachedProviderInfo() {
|
|
1629
|
+
return this.adapter.getCachedProviderInfo();
|
|
1630
|
+
}
|
|
1631
|
+
/**
|
|
1632
|
+
* Get mints for a specific provider
|
|
1633
|
+
* @param baseUrl Provider base URL
|
|
1634
|
+
* @returns Array of mint URLs for the provider
|
|
1635
|
+
*/
|
|
1636
|
+
getProviderMints(baseUrl) {
|
|
1637
|
+
const normalized = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
1638
|
+
const allMints = this.getCachedMints();
|
|
1639
|
+
return allMints[normalized] || [];
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* Get info for a specific provider
|
|
1643
|
+
* @param baseUrl Provider base URL
|
|
1644
|
+
* @returns Provider info object or null if not found
|
|
1645
|
+
*/
|
|
1646
|
+
getProviderInfo(baseUrl) {
|
|
1647
|
+
const normalized = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
1648
|
+
const allInfo = this.getCachedProviderInfo();
|
|
1649
|
+
return allInfo[normalized] || null;
|
|
1650
|
+
}
|
|
1651
|
+
/**
|
|
1652
|
+
* Clear mint cache for a specific provider
|
|
1653
|
+
* @param baseUrl Provider base URL
|
|
1654
|
+
*/
|
|
1655
|
+
clearProviderMintCache(baseUrl) {
|
|
1656
|
+
const normalized = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
1657
|
+
const mints = this.getCachedMints();
|
|
1658
|
+
delete mints[normalized];
|
|
1659
|
+
this.adapter.setCachedMints(mints);
|
|
1660
|
+
const info = this.getCachedProviderInfo();
|
|
1661
|
+
delete info[normalized];
|
|
1662
|
+
this.adapter.setCachedProviderInfo(info);
|
|
1663
|
+
}
|
|
1664
|
+
/**
|
|
1665
|
+
* Clear all mint caches
|
|
1666
|
+
*/
|
|
1667
|
+
clearAllCache() {
|
|
1668
|
+
this.adapter.setCachedMints({});
|
|
1669
|
+
this.adapter.setCachedProviderInfo({});
|
|
1670
|
+
}
|
|
1671
|
+
isProviderDownError(error) {
|
|
1672
|
+
if (!(error instanceof Error)) return false;
|
|
1673
|
+
const msg = error.message.toLowerCase();
|
|
1674
|
+
if (msg.includes("fetch failed")) return true;
|
|
1675
|
+
if (msg.includes("502")) return true;
|
|
1676
|
+
if (msg.includes("503")) return true;
|
|
1677
|
+
if (msg.includes("504")) return true;
|
|
1678
|
+
const cause = error.cause;
|
|
1679
|
+
if (cause?.code === "ENOTFOUND") return true;
|
|
1680
|
+
return false;
|
|
1681
|
+
}
|
|
1682
|
+
};
|
|
1683
|
+
|
|
1684
|
+
// wallet/AuditLogger.ts
|
|
1685
|
+
var AuditLogger = class _AuditLogger {
|
|
1686
|
+
static instance = null;
|
|
1687
|
+
static getInstance() {
|
|
1688
|
+
if (!_AuditLogger.instance) {
|
|
1689
|
+
_AuditLogger.instance = new _AuditLogger();
|
|
1690
|
+
}
|
|
1691
|
+
return _AuditLogger.instance;
|
|
1692
|
+
}
|
|
1693
|
+
async log(entry) {
|
|
1694
|
+
const fullEntry = {
|
|
1695
|
+
...entry,
|
|
1696
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1697
|
+
};
|
|
1698
|
+
const logLine = JSON.stringify(fullEntry) + "\n";
|
|
1699
|
+
if (typeof window === "undefined") {
|
|
1700
|
+
try {
|
|
1701
|
+
const fs = await import('fs');
|
|
1702
|
+
const path = await import('path');
|
|
1703
|
+
const logPath = path.join(process.cwd(), "audit.log");
|
|
1704
|
+
fs.appendFileSync(logPath, logLine);
|
|
1705
|
+
} catch (error) {
|
|
1706
|
+
console.error("[AuditLogger] Failed to write to file:", error);
|
|
1707
|
+
}
|
|
1708
|
+
} else {
|
|
1709
|
+
console.log("[AUDIT]", logLine.trim());
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
async logBalanceSnapshot(action, amounts, options) {
|
|
1713
|
+
await this.log({
|
|
1714
|
+
action,
|
|
1715
|
+
totalBalance: amounts.totalBalance,
|
|
1716
|
+
providerBalances: amounts.providerBalances,
|
|
1717
|
+
mintBalances: amounts.mintBalances,
|
|
1718
|
+
amount: options?.amount,
|
|
1719
|
+
mintUrl: options?.mintUrl,
|
|
1720
|
+
baseUrl: options?.baseUrl,
|
|
1721
|
+
status: options?.status ?? "success",
|
|
1722
|
+
details: options?.details
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
};
|
|
1726
|
+
var auditLogger = AuditLogger.getInstance();
|
|
1727
|
+
|
|
1728
|
+
// wallet/CashuSpender.ts
|
|
1729
|
+
var CashuSpender = class {
|
|
1730
|
+
constructor(walletAdapter, storageAdapter, providerRegistry, balanceManager) {
|
|
1731
|
+
this.walletAdapter = walletAdapter;
|
|
1732
|
+
this.storageAdapter = storageAdapter;
|
|
1733
|
+
this.providerRegistry = providerRegistry;
|
|
1734
|
+
this.balanceManager = balanceManager;
|
|
1735
|
+
}
|
|
1736
|
+
_isBusy = false;
|
|
1737
|
+
async _getBalanceState() {
|
|
1738
|
+
const mintBalances = await this.walletAdapter.getBalances();
|
|
1739
|
+
const units = this.walletAdapter.getMintUnits();
|
|
1740
|
+
let totalMintBalance = 0;
|
|
1741
|
+
const normalizedMintBalances = {};
|
|
1742
|
+
for (const url in mintBalances) {
|
|
1743
|
+
const balance = mintBalances[url];
|
|
1744
|
+
const unit = units[url];
|
|
1745
|
+
const balanceInSats = unit === "msat" ? balance / 1e3 : balance;
|
|
1746
|
+
normalizedMintBalances[url] = balanceInSats;
|
|
1747
|
+
totalMintBalance += balanceInSats;
|
|
1748
|
+
}
|
|
1749
|
+
const pendingDistribution = this.storageAdapter.getPendingTokenDistribution();
|
|
1750
|
+
const providerBalances = {};
|
|
1751
|
+
let totalProviderBalance = 0;
|
|
1752
|
+
for (const pending of pendingDistribution) {
|
|
1753
|
+
providerBalances[pending.baseUrl] = pending.amount;
|
|
1754
|
+
totalProviderBalance += pending.amount;
|
|
1755
|
+
}
|
|
1756
|
+
const apiKeys = this.storageAdapter.getAllApiKeys();
|
|
1757
|
+
for (const apiKey of apiKeys) {
|
|
1758
|
+
if (!providerBalances[apiKey.baseUrl]) {
|
|
1759
|
+
providerBalances[apiKey.baseUrl] = apiKey.balance;
|
|
1760
|
+
totalProviderBalance += apiKey.balance;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
return {
|
|
1764
|
+
totalBalance: totalMintBalance + totalProviderBalance,
|
|
1765
|
+
providerBalances,
|
|
1766
|
+
mintBalances: normalizedMintBalances
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
async _logTransaction(action, options) {
|
|
1770
|
+
const balanceState = await this._getBalanceState();
|
|
1771
|
+
await auditLogger.logBalanceSnapshot(action, balanceState, options);
|
|
1772
|
+
}
|
|
1773
|
+
/**
|
|
1774
|
+
* Check if the spender is currently in a critical operation
|
|
1775
|
+
*/
|
|
1776
|
+
get isBusy() {
|
|
1777
|
+
return this._isBusy;
|
|
1778
|
+
}
|
|
1779
|
+
/**
|
|
1780
|
+
* Spend Cashu tokens with automatic mint selection and retry logic
|
|
1781
|
+
*/
|
|
1782
|
+
async spend(options) {
|
|
1783
|
+
const {
|
|
1784
|
+
mintUrl,
|
|
1785
|
+
amount,
|
|
1786
|
+
baseUrl,
|
|
1787
|
+
reuseToken = false,
|
|
1788
|
+
p2pkPubkey,
|
|
1789
|
+
excludeMints = [],
|
|
1790
|
+
retryCount = 0
|
|
1791
|
+
} = options;
|
|
1792
|
+
this._isBusy = true;
|
|
1793
|
+
try {
|
|
1794
|
+
return await this._spendInternal({
|
|
1795
|
+
mintUrl,
|
|
1796
|
+
amount,
|
|
1797
|
+
baseUrl,
|
|
1798
|
+
reuseToken,
|
|
1799
|
+
p2pkPubkey,
|
|
1800
|
+
excludeMints,
|
|
1801
|
+
retryCount
|
|
1802
|
+
});
|
|
1803
|
+
} finally {
|
|
1804
|
+
this._isBusy = false;
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
/**
|
|
1808
|
+
* Internal spending logic
|
|
1809
|
+
*/
|
|
1810
|
+
async _spendInternal(options) {
|
|
1811
|
+
let {
|
|
1812
|
+
mintUrl,
|
|
1813
|
+
amount,
|
|
1814
|
+
baseUrl,
|
|
1815
|
+
reuseToken,
|
|
1816
|
+
p2pkPubkey,
|
|
1817
|
+
excludeMints,
|
|
1818
|
+
retryCount
|
|
1819
|
+
} = options;
|
|
1820
|
+
let adjustedAmount = Math.ceil(amount);
|
|
1821
|
+
if (!adjustedAmount || isNaN(adjustedAmount)) {
|
|
1822
|
+
return {
|
|
1823
|
+
token: null,
|
|
1824
|
+
status: "failed",
|
|
1825
|
+
balance: 0,
|
|
1826
|
+
error: "Please enter a valid amount"
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
if (reuseToken && baseUrl) {
|
|
1830
|
+
const existingResult = await this._tryReuseToken(
|
|
1831
|
+
baseUrl,
|
|
1832
|
+
adjustedAmount,
|
|
1833
|
+
mintUrl
|
|
1834
|
+
);
|
|
1835
|
+
if (existingResult) {
|
|
1836
|
+
return existingResult;
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
const balances = await this.walletAdapter.getBalances();
|
|
1840
|
+
const units = this.walletAdapter.getMintUnits();
|
|
1841
|
+
let totalBalance = 0;
|
|
1842
|
+
for (const url in balances) {
|
|
1843
|
+
const balance = balances[url];
|
|
1844
|
+
const unit = units[url];
|
|
1845
|
+
const balanceInSats = unit === "msat" ? balance / 1e3 : balance;
|
|
1846
|
+
totalBalance += balanceInSats;
|
|
1847
|
+
}
|
|
1848
|
+
const pendingDistribution = this.storageAdapter.getPendingTokenDistribution();
|
|
1849
|
+
const totalPending = pendingDistribution.reduce(
|
|
1850
|
+
(sum, item) => sum + item.amount,
|
|
1851
|
+
0
|
|
1852
|
+
);
|
|
1853
|
+
if (totalBalance < adjustedAmount && totalPending + totalBalance > adjustedAmount && (retryCount ?? 0) < 1) {
|
|
1854
|
+
return await this._refundAndRetry(options);
|
|
1855
|
+
}
|
|
1856
|
+
const totalAvailableBalance = totalBalance + totalPending;
|
|
1857
|
+
if (totalAvailableBalance < adjustedAmount) {
|
|
1858
|
+
return this._createInsufficientBalanceError(
|
|
1859
|
+
adjustedAmount,
|
|
1860
|
+
balances,
|
|
1861
|
+
units,
|
|
1862
|
+
totalAvailableBalance
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1865
|
+
let { selectedMintUrl, selectedMintBalance } = this._selectMintWithBalance(
|
|
1866
|
+
balances,
|
|
1867
|
+
units,
|
|
1868
|
+
adjustedAmount,
|
|
1869
|
+
excludeMints
|
|
1870
|
+
);
|
|
1871
|
+
if (selectedMintUrl && baseUrl && this.providerRegistry) {
|
|
1872
|
+
const providerMints = this.providerRegistry.getProviderMints(baseUrl);
|
|
1873
|
+
if (providerMints.length > 0 && !providerMints.includes(selectedMintUrl)) {
|
|
1874
|
+
const alternateResult = await this._findAlternateMint(
|
|
1875
|
+
options,
|
|
1876
|
+
balances,
|
|
1877
|
+
units,
|
|
1878
|
+
providerMints
|
|
1879
|
+
);
|
|
1880
|
+
if (alternateResult) {
|
|
1881
|
+
return alternateResult;
|
|
1882
|
+
}
|
|
1883
|
+
adjustedAmount += 2;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
const activeMintBalance = balances[mintUrl] || 0;
|
|
1887
|
+
const activeMintUnit = units[mintUrl];
|
|
1888
|
+
const activeMintBalanceInSats = activeMintUnit === "msat" ? activeMintBalance / 1e3 : activeMintBalance;
|
|
1889
|
+
let token = null;
|
|
1890
|
+
if (activeMintBalanceInSats >= adjustedAmount && (baseUrl === "" || !this.providerRegistry)) {
|
|
1891
|
+
try {
|
|
1892
|
+
token = await this.walletAdapter.sendToken(
|
|
1893
|
+
mintUrl,
|
|
1894
|
+
adjustedAmount,
|
|
1895
|
+
p2pkPubkey
|
|
1896
|
+
);
|
|
1897
|
+
} catch (error) {
|
|
1898
|
+
return this._handleSendError(error, options, balances, units);
|
|
1899
|
+
}
|
|
1900
|
+
} else if (selectedMintUrl && selectedMintBalance >= adjustedAmount) {
|
|
1901
|
+
try {
|
|
1902
|
+
token = await this.walletAdapter.sendToken(
|
|
1903
|
+
selectedMintUrl,
|
|
1904
|
+
adjustedAmount,
|
|
1905
|
+
p2pkPubkey
|
|
1906
|
+
);
|
|
1907
|
+
} catch (error) {
|
|
1908
|
+
return this._handleSendError(error, options, balances, units);
|
|
1909
|
+
}
|
|
1910
|
+
} else {
|
|
1911
|
+
return this._createInsufficientBalanceError(
|
|
1912
|
+
adjustedAmount,
|
|
1913
|
+
balances,
|
|
1914
|
+
units
|
|
1915
|
+
);
|
|
1916
|
+
}
|
|
1917
|
+
if (token && baseUrl) {
|
|
1918
|
+
this.storageAdapter.setToken(baseUrl, token);
|
|
1919
|
+
}
|
|
1920
|
+
this._logTransaction("spend", {
|
|
1921
|
+
amount: adjustedAmount,
|
|
1922
|
+
mintUrl: selectedMintUrl || mintUrl,
|
|
1923
|
+
baseUrl,
|
|
1924
|
+
status: "success"
|
|
1925
|
+
});
|
|
1926
|
+
return {
|
|
1927
|
+
token,
|
|
1928
|
+
status: "success",
|
|
1929
|
+
balance: adjustedAmount,
|
|
1930
|
+
unit: activeMintUnit
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
/**
|
|
1934
|
+
* Try to reuse an existing token
|
|
1935
|
+
*/
|
|
1936
|
+
async _tryReuseToken(baseUrl, amount, mintUrl) {
|
|
1937
|
+
const storedToken = this.storageAdapter.getToken(baseUrl);
|
|
1938
|
+
if (!storedToken) return null;
|
|
1939
|
+
const pendingDistribution = this.storageAdapter.getPendingTokenDistribution();
|
|
1940
|
+
const balanceForBaseUrl = pendingDistribution.find((b) => b.baseUrl === baseUrl)?.amount || 0;
|
|
1941
|
+
console.log("RESUINGDSR GSODGNSD", balanceForBaseUrl, amount);
|
|
1942
|
+
if (balanceForBaseUrl > amount) {
|
|
1943
|
+
const units = this.walletAdapter.getMintUnits();
|
|
1944
|
+
const unit = units[mintUrl] || "sat";
|
|
1945
|
+
return {
|
|
1946
|
+
token: storedToken,
|
|
1947
|
+
status: "success",
|
|
1948
|
+
balance: balanceForBaseUrl,
|
|
1949
|
+
unit
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
if (this.balanceManager) {
|
|
1953
|
+
const topUpAmount = Math.ceil(amount * 1.2 - balanceForBaseUrl);
|
|
1954
|
+
const topUpResult = await this.balanceManager.topUp({
|
|
1955
|
+
mintUrl,
|
|
1956
|
+
baseUrl,
|
|
1957
|
+
amount: topUpAmount
|
|
1958
|
+
});
|
|
1959
|
+
console.log("TOPUP ", topUpResult);
|
|
1960
|
+
if (topUpResult.success && topUpResult.toppedUpAmount) {
|
|
1961
|
+
const newBalance = balanceForBaseUrl + topUpResult.toppedUpAmount;
|
|
1962
|
+
const units = this.walletAdapter.getMintUnits();
|
|
1963
|
+
const unit = units[mintUrl] || "sat";
|
|
1964
|
+
this._logTransaction("topup", {
|
|
1965
|
+
amount: topUpResult.toppedUpAmount,
|
|
1966
|
+
mintUrl,
|
|
1967
|
+
baseUrl,
|
|
1968
|
+
status: "success"
|
|
1969
|
+
});
|
|
1970
|
+
return {
|
|
1971
|
+
token: storedToken,
|
|
1972
|
+
status: "success",
|
|
1973
|
+
balance: newBalance,
|
|
1974
|
+
unit
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
const providerBalance = await this._getProviderTokenBalance(
|
|
1978
|
+
baseUrl,
|
|
1979
|
+
storedToken
|
|
1980
|
+
);
|
|
1981
|
+
console.log(providerBalance);
|
|
1982
|
+
if (providerBalance <= 0) {
|
|
1983
|
+
this.storageAdapter.removeToken(baseUrl);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
return null;
|
|
1987
|
+
}
|
|
1988
|
+
/**
|
|
1989
|
+
* Refund pending tokens and retry
|
|
1990
|
+
*/
|
|
1991
|
+
async _refundAndRetry(options) {
|
|
1992
|
+
const { mintUrl, baseUrl, retryCount } = options;
|
|
1993
|
+
const pendingDistribution = this.storageAdapter.getPendingTokenDistribution();
|
|
1994
|
+
const refundResults = await Promise.allSettled(
|
|
1995
|
+
pendingDistribution.map(async (pending) => {
|
|
1996
|
+
const token = this.storageAdapter.getToken(pending.baseUrl);
|
|
1997
|
+
if (!token || !this.balanceManager || pending.baseUrl === baseUrl) {
|
|
1998
|
+
return { baseUrl: pending.baseUrl, success: false };
|
|
1999
|
+
}
|
|
2000
|
+
const tokenBalance = await this.balanceManager.getTokenBalance(token, pending.baseUrl);
|
|
2001
|
+
if (tokenBalance.reserved > 0) {
|
|
2002
|
+
return { baseUrl: pending.baseUrl, success: false };
|
|
2003
|
+
}
|
|
2004
|
+
const result = await this.balanceManager.refund({
|
|
2005
|
+
mintUrl,
|
|
2006
|
+
baseUrl: pending.baseUrl,
|
|
2007
|
+
token
|
|
2008
|
+
});
|
|
2009
|
+
return { baseUrl: pending.baseUrl, success: result.success };
|
|
2010
|
+
})
|
|
2011
|
+
);
|
|
2012
|
+
for (const result of refundResults) {
|
|
2013
|
+
const refundResult = result.status === "fulfilled" ? result.value : { baseUrl: "", success: false };
|
|
2014
|
+
if (refundResult.success) {
|
|
2015
|
+
this.storageAdapter.removeToken(refundResult.baseUrl);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
const successfulRefunds = refundResults.filter(
|
|
2019
|
+
(r) => r.status === "fulfilled" && r.value.success
|
|
2020
|
+
).length;
|
|
2021
|
+
if (successfulRefunds > 0) {
|
|
2022
|
+
this._logTransaction("refund", {
|
|
2023
|
+
amount: pendingDistribution.length,
|
|
2024
|
+
mintUrl,
|
|
2025
|
+
status: "success",
|
|
2026
|
+
details: `Refunded ${successfulRefunds} of ${pendingDistribution.length} tokens`
|
|
2027
|
+
});
|
|
2028
|
+
}
|
|
2029
|
+
return this._spendInternal({
|
|
2030
|
+
...options,
|
|
2031
|
+
retryCount: (retryCount || 0) + 1
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
/**
|
|
2035
|
+
* Find an alternate mint that the provider accepts
|
|
2036
|
+
*/
|
|
2037
|
+
async _findAlternateMint(options, balances, units, providerMints) {
|
|
2038
|
+
const { amount, excludeMints } = options;
|
|
2039
|
+
const adjustedAmount = Math.ceil(amount) + 2;
|
|
2040
|
+
const extendedExcludes = [...excludeMints || []];
|
|
2041
|
+
while (true) {
|
|
2042
|
+
const { selectedMintUrl } = this._selectMintWithBalance(
|
|
2043
|
+
balances,
|
|
2044
|
+
units,
|
|
2045
|
+
adjustedAmount,
|
|
2046
|
+
extendedExcludes
|
|
2047
|
+
);
|
|
2048
|
+
if (!selectedMintUrl) break;
|
|
2049
|
+
if (providerMints.includes(selectedMintUrl)) {
|
|
2050
|
+
try {
|
|
2051
|
+
const token = await this.walletAdapter.sendToken(
|
|
2052
|
+
selectedMintUrl,
|
|
2053
|
+
adjustedAmount
|
|
2054
|
+
);
|
|
2055
|
+
if (options.baseUrl) {
|
|
2056
|
+
this.storageAdapter.setToken(options.baseUrl, token);
|
|
2057
|
+
}
|
|
2058
|
+
return {
|
|
2059
|
+
token,
|
|
2060
|
+
status: "success",
|
|
2061
|
+
balance: adjustedAmount,
|
|
2062
|
+
unit: units[selectedMintUrl] || "sat"
|
|
2063
|
+
};
|
|
2064
|
+
} catch (error) {
|
|
2065
|
+
extendedExcludes.push(selectedMintUrl);
|
|
2066
|
+
}
|
|
2067
|
+
} else {
|
|
2068
|
+
extendedExcludes.push(selectedMintUrl);
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
return null;
|
|
2072
|
+
}
|
|
2073
|
+
/**
|
|
2074
|
+
* Handle send errors with retry logic for network errors
|
|
2075
|
+
*/
|
|
2076
|
+
async _handleSendError(error, options, balances, units) {
|
|
2077
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2078
|
+
const isNetworkError = error instanceof Error && (error.message.includes(
|
|
2079
|
+
"NetworkError when attempting to fetch resource"
|
|
2080
|
+
) || error.message.includes("Failed to fetch") || error.message.includes("Load failed"));
|
|
2081
|
+
if (isNetworkError) {
|
|
2082
|
+
const { mintUrl, amount, baseUrl, p2pkPubkey, excludeMints, retryCount } = options;
|
|
2083
|
+
const extendedExcludes = [...excludeMints || [], mintUrl];
|
|
2084
|
+
const { selectedMintUrl } = this._selectMintWithBalance(
|
|
2085
|
+
balances,
|
|
2086
|
+
units,
|
|
2087
|
+
Math.ceil(amount),
|
|
2088
|
+
extendedExcludes
|
|
2089
|
+
);
|
|
2090
|
+
if (selectedMintUrl && (retryCount || 0) < Object.keys(balances).length) {
|
|
2091
|
+
return this._spendInternal({
|
|
2092
|
+
...options,
|
|
2093
|
+
mintUrl: selectedMintUrl,
|
|
2094
|
+
excludeMints: extendedExcludes,
|
|
2095
|
+
retryCount: (retryCount || 0) + 1
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
throw new MintUnreachableError(mintUrl);
|
|
2099
|
+
}
|
|
2100
|
+
return {
|
|
2101
|
+
token: null,
|
|
2102
|
+
status: "failed",
|
|
2103
|
+
balance: 0,
|
|
2104
|
+
error: `Error generating token: ${errorMsg}`
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
/**
|
|
2108
|
+
* Select a mint with sufficient balance
|
|
2109
|
+
*/
|
|
2110
|
+
_selectMintWithBalance(balances, units, amount, excludeMints = []) {
|
|
2111
|
+
for (const mintUrl in balances) {
|
|
2112
|
+
if (excludeMints.includes(mintUrl)) {
|
|
2113
|
+
continue;
|
|
2114
|
+
}
|
|
2115
|
+
const balance = balances[mintUrl];
|
|
2116
|
+
const unit = units[mintUrl];
|
|
2117
|
+
const balanceInSats = unit === "msat" ? balance / 1e3 : balance;
|
|
2118
|
+
if (balanceInSats >= amount) {
|
|
2119
|
+
return { selectedMintUrl: mintUrl, selectedMintBalance: balanceInSats };
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
return { selectedMintUrl: null, selectedMintBalance: 0 };
|
|
2123
|
+
}
|
|
2124
|
+
/**
|
|
2125
|
+
* Create an insufficient balance error result
|
|
2126
|
+
*/
|
|
2127
|
+
_createInsufficientBalanceError(required, balances, units, availableBalance) {
|
|
2128
|
+
let maxBalance = 0;
|
|
2129
|
+
let maxMintUrl = "";
|
|
2130
|
+
for (const mintUrl in balances) {
|
|
2131
|
+
const balance = balances[mintUrl];
|
|
2132
|
+
const unit = units[mintUrl];
|
|
2133
|
+
const balanceInSats = unit === "msat" ? balance / 1e3 : balance;
|
|
2134
|
+
if (balanceInSats > maxBalance) {
|
|
2135
|
+
maxBalance = balanceInSats;
|
|
2136
|
+
maxMintUrl = mintUrl;
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
const error = new InsufficientBalanceError(
|
|
2140
|
+
required,
|
|
2141
|
+
availableBalance ?? maxBalance,
|
|
2142
|
+
maxBalance,
|
|
2143
|
+
maxMintUrl
|
|
2144
|
+
);
|
|
2145
|
+
return {
|
|
2146
|
+
token: null,
|
|
2147
|
+
status: "failed",
|
|
2148
|
+
balance: 0,
|
|
2149
|
+
error: error.message,
|
|
2150
|
+
errorDetails: {
|
|
2151
|
+
required,
|
|
2152
|
+
available: availableBalance ?? maxBalance,
|
|
2153
|
+
maxMintBalance: maxBalance,
|
|
2154
|
+
maxMintUrl
|
|
2155
|
+
}
|
|
2156
|
+
};
|
|
2157
|
+
}
|
|
2158
|
+
async _getProviderTokenBalance(baseUrl, token) {
|
|
2159
|
+
try {
|
|
2160
|
+
const response = await fetch(`${baseUrl}v1/wallet/info`, {
|
|
2161
|
+
headers: {
|
|
2162
|
+
Authorization: `Bearer ${token}`
|
|
2163
|
+
}
|
|
2164
|
+
});
|
|
2165
|
+
if (response.ok) {
|
|
2166
|
+
const data = await response.json();
|
|
2167
|
+
return data.balance / 1e3;
|
|
2168
|
+
}
|
|
2169
|
+
} catch {
|
|
2170
|
+
return 0;
|
|
2171
|
+
}
|
|
2172
|
+
return 0;
|
|
2173
|
+
}
|
|
2174
|
+
};
|
|
2175
|
+
|
|
2176
|
+
// wallet/BalanceManager.ts
|
|
2177
|
+
var BalanceManager = class {
|
|
2178
|
+
constructor(walletAdapter, storageAdapter) {
|
|
2179
|
+
this.walletAdapter = walletAdapter;
|
|
2180
|
+
this.storageAdapter = storageAdapter;
|
|
2181
|
+
}
|
|
2182
|
+
/**
|
|
2183
|
+
* Unified refund - handles both NIP-60 and legacy wallet refunds
|
|
2184
|
+
*/
|
|
2185
|
+
async refund(options) {
|
|
2186
|
+
const { mintUrl, baseUrl, token: providedToken } = options;
|
|
2187
|
+
const storedToken = providedToken || this.storageAdapter.getToken(baseUrl);
|
|
2188
|
+
if (!storedToken) {
|
|
2189
|
+
console.log("[BalanceManager] No token to refund, returning early");
|
|
2190
|
+
return { success: true, message: "No API key to refund" };
|
|
2191
|
+
}
|
|
2192
|
+
let fetchResult;
|
|
2193
|
+
try {
|
|
2194
|
+
fetchResult = await this._fetchRefundToken(baseUrl, storedToken);
|
|
2195
|
+
if (!fetchResult.success) {
|
|
2196
|
+
return {
|
|
2197
|
+
success: false,
|
|
2198
|
+
message: fetchResult.error || "Refund failed",
|
|
2199
|
+
requestId: fetchResult.requestId
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2202
|
+
if (!fetchResult.token) {
|
|
2203
|
+
return {
|
|
2204
|
+
success: false,
|
|
2205
|
+
message: "No token received from refund",
|
|
2206
|
+
requestId: fetchResult.requestId
|
|
2207
|
+
};
|
|
2208
|
+
}
|
|
2209
|
+
if (fetchResult.error === "No balance to refund") {
|
|
2210
|
+
console.log(
|
|
2211
|
+
"[BalanceManager] No balance to refund, removing stored token"
|
|
2212
|
+
);
|
|
2213
|
+
this.storageAdapter.removeToken(baseUrl);
|
|
2214
|
+
return { success: true, message: "No balance to refund" };
|
|
2215
|
+
}
|
|
2216
|
+
const receiveResult = await this.walletAdapter.receiveToken(
|
|
2217
|
+
fetchResult.token
|
|
2218
|
+
);
|
|
2219
|
+
const totalAmountMsat = receiveResult.unit === "msat" ? receiveResult.amount : receiveResult.amount * 1e3;
|
|
2220
|
+
if (!providedToken) {
|
|
2221
|
+
this.storageAdapter.removeToken(baseUrl);
|
|
2222
|
+
}
|
|
2223
|
+
return {
|
|
2224
|
+
success: receiveResult.success,
|
|
2225
|
+
refundedAmount: totalAmountMsat,
|
|
2226
|
+
requestId: fetchResult.requestId
|
|
2227
|
+
};
|
|
2228
|
+
} catch (error) {
|
|
2229
|
+
console.error("[BalanceManager] Refund error", error);
|
|
2230
|
+
return this._handleRefundError(error, mintUrl, fetchResult?.requestId);
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
/**
|
|
2234
|
+
* Top up API key balance with a cashu token
|
|
2235
|
+
*/
|
|
2236
|
+
async topUp(options) {
|
|
2237
|
+
const { mintUrl, baseUrl, amount, token: providedToken } = options;
|
|
2238
|
+
if (!amount || amount <= 0) {
|
|
2239
|
+
return { success: false, message: "Invalid top up amount" };
|
|
2240
|
+
}
|
|
2241
|
+
const storedToken = providedToken || this.storageAdapter.getToken(baseUrl);
|
|
2242
|
+
if (!storedToken) {
|
|
2243
|
+
return { success: false, message: "No API key available for top up" };
|
|
2244
|
+
}
|
|
2245
|
+
let cashuToken = null;
|
|
2246
|
+
let requestId;
|
|
2247
|
+
try {
|
|
2248
|
+
cashuToken = await this.walletAdapter.sendToken(mintUrl, amount);
|
|
2249
|
+
const topUpResult = await this._postTopUp(
|
|
2250
|
+
baseUrl,
|
|
2251
|
+
storedToken,
|
|
2252
|
+
cashuToken
|
|
2253
|
+
);
|
|
2254
|
+
requestId = topUpResult.requestId;
|
|
2255
|
+
if (!topUpResult.success) {
|
|
2256
|
+
await this._recoverFailedTopUp(cashuToken);
|
|
2257
|
+
return {
|
|
2258
|
+
success: false,
|
|
2259
|
+
message: topUpResult.error || "Top up failed",
|
|
2260
|
+
requestId,
|
|
2261
|
+
recoveredToken: true
|
|
2262
|
+
};
|
|
2263
|
+
}
|
|
2264
|
+
return {
|
|
2265
|
+
success: true,
|
|
2266
|
+
toppedUpAmount: amount,
|
|
2267
|
+
requestId
|
|
2268
|
+
};
|
|
2269
|
+
} catch (error) {
|
|
2270
|
+
if (cashuToken) {
|
|
2271
|
+
await this._recoverFailedTopUp(cashuToken);
|
|
2272
|
+
}
|
|
2273
|
+
return this._handleTopUpError(error, mintUrl, requestId);
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
/**
|
|
2277
|
+
* Fetch refund token from provider API
|
|
2278
|
+
*/
|
|
2279
|
+
async _fetchRefundToken(baseUrl, storedToken) {
|
|
2280
|
+
if (!baseUrl) {
|
|
2281
|
+
return {
|
|
2282
|
+
success: false,
|
|
2283
|
+
error: "No base URL configured"
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
2287
|
+
const url = `${normalizedBaseUrl}v1/wallet/refund`;
|
|
2288
|
+
const controller = new AbortController();
|
|
2289
|
+
const timeoutId = setTimeout(() => {
|
|
2290
|
+
controller.abort();
|
|
2291
|
+
}, 6e4);
|
|
2292
|
+
try {
|
|
2293
|
+
const response = await fetch(url, {
|
|
2294
|
+
method: "POST",
|
|
2295
|
+
headers: {
|
|
2296
|
+
Authorization: `Bearer ${storedToken}`,
|
|
2297
|
+
"Content-Type": "application/json"
|
|
2298
|
+
},
|
|
2299
|
+
signal: controller.signal
|
|
2300
|
+
});
|
|
2301
|
+
clearTimeout(timeoutId);
|
|
2302
|
+
const requestId = response.headers.get("x-routstr-request-id") || void 0;
|
|
2303
|
+
if (!response.ok) {
|
|
2304
|
+
const errorData = await response.json().catch(() => ({}));
|
|
2305
|
+
if (response.status === 400 && errorData?.detail === "No balance to refund") {
|
|
2306
|
+
this.storageAdapter.removeToken(baseUrl);
|
|
2307
|
+
return {
|
|
2308
|
+
success: false,
|
|
2309
|
+
requestId,
|
|
2310
|
+
error: "No balance to refund"
|
|
2311
|
+
};
|
|
2312
|
+
}
|
|
2313
|
+
return {
|
|
2314
|
+
success: false,
|
|
2315
|
+
requestId,
|
|
2316
|
+
error: `Refund request failed with status ${response.status}: ${errorData?.detail || response.statusText}`
|
|
2317
|
+
};
|
|
2318
|
+
}
|
|
2319
|
+
const data = await response.json();
|
|
2320
|
+
console.log("refund rsule", data);
|
|
2321
|
+
return {
|
|
2322
|
+
success: true,
|
|
2323
|
+
token: data.token,
|
|
2324
|
+
requestId
|
|
2325
|
+
};
|
|
2326
|
+
} catch (error) {
|
|
2327
|
+
clearTimeout(timeoutId);
|
|
2328
|
+
console.error("[BalanceManager._fetchRefundToken] Fetch error", error);
|
|
2329
|
+
if (error instanceof Error) {
|
|
2330
|
+
if (error.name === "AbortError") {
|
|
2331
|
+
return {
|
|
2332
|
+
success: false,
|
|
2333
|
+
error: "Request timed out after 1 minute"
|
|
2334
|
+
};
|
|
2335
|
+
}
|
|
2336
|
+
return {
|
|
2337
|
+
success: false,
|
|
2338
|
+
error: error.message
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
return {
|
|
2342
|
+
success: false,
|
|
2343
|
+
error: "Unknown error occurred during refund request"
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
/**
|
|
2348
|
+
* Post topup request to provider API
|
|
2349
|
+
*/
|
|
2350
|
+
async _postTopUp(baseUrl, storedToken, cashuToken) {
|
|
2351
|
+
if (!baseUrl) {
|
|
2352
|
+
return {
|
|
2353
|
+
success: false,
|
|
2354
|
+
error: "No base URL configured"
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
2358
|
+
const url = `${normalizedBaseUrl}v1/wallet/topup?cashu_token=${encodeURIComponent(
|
|
2359
|
+
cashuToken
|
|
2360
|
+
)}`;
|
|
2361
|
+
const controller = new AbortController();
|
|
2362
|
+
const timeoutId = setTimeout(() => {
|
|
2363
|
+
controller.abort();
|
|
2364
|
+
}, 6e4);
|
|
2365
|
+
try {
|
|
2366
|
+
const response = await fetch(url, {
|
|
2367
|
+
method: "POST",
|
|
2368
|
+
headers: {
|
|
2369
|
+
Authorization: `Bearer ${storedToken}`,
|
|
2370
|
+
"Content-Type": "application/json"
|
|
2371
|
+
},
|
|
2372
|
+
signal: controller.signal
|
|
2373
|
+
});
|
|
2374
|
+
clearTimeout(timeoutId);
|
|
2375
|
+
const requestId = response.headers.get("x-routstr-request-id") || void 0;
|
|
2376
|
+
if (!response.ok) {
|
|
2377
|
+
const errorData = await response.json().catch(() => ({}));
|
|
2378
|
+
return {
|
|
2379
|
+
success: false,
|
|
2380
|
+
requestId,
|
|
2381
|
+
error: errorData?.detail || `Top up failed with status ${response.status}`
|
|
2382
|
+
};
|
|
2383
|
+
}
|
|
2384
|
+
return { success: true, requestId };
|
|
2385
|
+
} catch (error) {
|
|
2386
|
+
clearTimeout(timeoutId);
|
|
2387
|
+
console.error("[BalanceManager._postTopUp] Fetch error", error);
|
|
2388
|
+
if (error instanceof Error) {
|
|
2389
|
+
if (error.name === "AbortError") {
|
|
2390
|
+
return {
|
|
2391
|
+
success: false,
|
|
2392
|
+
error: "Request timed out after 1 minute"
|
|
2393
|
+
};
|
|
2394
|
+
}
|
|
2395
|
+
return {
|
|
2396
|
+
success: false,
|
|
2397
|
+
error: error.message
|
|
2398
|
+
};
|
|
2399
|
+
}
|
|
2400
|
+
return {
|
|
2401
|
+
success: false,
|
|
2402
|
+
error: "Unknown error occurred during top up request"
|
|
2403
|
+
};
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
/**
|
|
2407
|
+
* Attempt to receive token back after failed top up
|
|
2408
|
+
*/
|
|
2409
|
+
async _recoverFailedTopUp(cashuToken) {
|
|
2410
|
+
try {
|
|
2411
|
+
await this.walletAdapter.receiveToken(cashuToken);
|
|
2412
|
+
} catch (error) {
|
|
2413
|
+
console.error(
|
|
2414
|
+
"[BalanceManager._recoverFailedTopUp] Failed to recover token",
|
|
2415
|
+
error
|
|
2416
|
+
);
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
/**
|
|
2420
|
+
* Handle refund errors with specific error types
|
|
2421
|
+
*/
|
|
2422
|
+
_handleRefundError(error, mintUrl, requestId) {
|
|
2423
|
+
if (error instanceof Error) {
|
|
2424
|
+
if (error.message.includes(
|
|
2425
|
+
"NetworkError when attempting to fetch resource"
|
|
2426
|
+
) || error.message.includes("Failed to fetch") || error.message.includes("Load failed")) {
|
|
2427
|
+
return {
|
|
2428
|
+
success: false,
|
|
2429
|
+
message: `Failed to connect to the mint: ${mintUrl}`,
|
|
2430
|
+
requestId
|
|
2431
|
+
};
|
|
2432
|
+
}
|
|
2433
|
+
if (error.message.includes("Wallet not found")) {
|
|
2434
|
+
return {
|
|
2435
|
+
success: false,
|
|
2436
|
+
message: `Wallet couldn't be loaded. Please save this refunded cashu token manually.`,
|
|
2437
|
+
requestId
|
|
2438
|
+
};
|
|
2439
|
+
}
|
|
2440
|
+
return {
|
|
2441
|
+
success: false,
|
|
2442
|
+
message: error.message,
|
|
2443
|
+
requestId
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
2446
|
+
return {
|
|
2447
|
+
success: false,
|
|
2448
|
+
message: "Refund failed",
|
|
2449
|
+
requestId
|
|
2450
|
+
};
|
|
2451
|
+
}
|
|
2452
|
+
/**
|
|
2453
|
+
* Get token balance from provider
|
|
2454
|
+
*/
|
|
2455
|
+
async getTokenBalance(token, baseUrl) {
|
|
2456
|
+
try {
|
|
2457
|
+
const response = await fetch(`${baseUrl}v1/wallet/info`, {
|
|
2458
|
+
headers: {
|
|
2459
|
+
Authorization: `Bearer ${token}`
|
|
2460
|
+
}
|
|
2461
|
+
});
|
|
2462
|
+
if (response.ok) {
|
|
2463
|
+
const data = await response.json();
|
|
2464
|
+
return {
|
|
2465
|
+
amount: data.balance,
|
|
2466
|
+
reserved: data.reserved ?? 0,
|
|
2467
|
+
unit: "msat",
|
|
2468
|
+
apiKey: data.api_key
|
|
2469
|
+
};
|
|
2470
|
+
}
|
|
2471
|
+
} catch {
|
|
2472
|
+
}
|
|
2473
|
+
return { amount: 0, reserved: 0, unit: "sat", apiKey: "" };
|
|
2474
|
+
}
|
|
2475
|
+
/**
|
|
2476
|
+
* Handle topup errors with specific error types
|
|
2477
|
+
*/
|
|
2478
|
+
_handleTopUpError(error, mintUrl, requestId) {
|
|
2479
|
+
if (error instanceof Error) {
|
|
2480
|
+
if (error.message.includes(
|
|
2481
|
+
"NetworkError when attempting to fetch resource"
|
|
2482
|
+
) || error.message.includes("Failed to fetch") || error.message.includes("Load failed")) {
|
|
2483
|
+
return {
|
|
2484
|
+
success: false,
|
|
2485
|
+
message: `Failed to connect to the mint: ${mintUrl}`,
|
|
2486
|
+
requestId
|
|
2487
|
+
};
|
|
2488
|
+
}
|
|
2489
|
+
if (error.message.includes("Wallet not found")) {
|
|
2490
|
+
return {
|
|
2491
|
+
success: false,
|
|
2492
|
+
message: "Wallet couldn't be loaded. The cashu token was recovered locally.",
|
|
2493
|
+
requestId
|
|
2494
|
+
};
|
|
2495
|
+
}
|
|
2496
|
+
return {
|
|
2497
|
+
success: false,
|
|
2498
|
+
message: error.message,
|
|
2499
|
+
requestId
|
|
2500
|
+
};
|
|
2501
|
+
}
|
|
2502
|
+
return {
|
|
2503
|
+
success: false,
|
|
2504
|
+
message: "Top up failed",
|
|
2505
|
+
requestId
|
|
2506
|
+
};
|
|
2507
|
+
}
|
|
2508
|
+
};
|
|
2509
|
+
|
|
2510
|
+
// client/StreamProcessor.ts
|
|
2511
|
+
var StreamProcessor = class {
|
|
2512
|
+
accumulatedContent = "";
|
|
2513
|
+
accumulatedThinking = "";
|
|
2514
|
+
accumulatedImages = [];
|
|
2515
|
+
isInThinking = false;
|
|
2516
|
+
isInContent = false;
|
|
2517
|
+
/**
|
|
2518
|
+
* Process a streaming response
|
|
2519
|
+
*/
|
|
2520
|
+
async process(response, callbacks, modelId) {
|
|
2521
|
+
if (!response.body) {
|
|
2522
|
+
throw new Error("Response body is not available");
|
|
2523
|
+
}
|
|
2524
|
+
const reader = response.body.getReader();
|
|
2525
|
+
const decoder = new TextDecoder("utf-8");
|
|
2526
|
+
let buffer = "";
|
|
2527
|
+
this.accumulatedContent = "";
|
|
2528
|
+
this.accumulatedThinking = "";
|
|
2529
|
+
this.accumulatedImages = [];
|
|
2530
|
+
this.isInThinking = false;
|
|
2531
|
+
this.isInContent = false;
|
|
2532
|
+
let usage;
|
|
2533
|
+
let model;
|
|
2534
|
+
let finish_reason;
|
|
2535
|
+
let citations;
|
|
2536
|
+
let annotations;
|
|
2537
|
+
try {
|
|
2538
|
+
while (true) {
|
|
2539
|
+
const { done, value } = await reader.read();
|
|
2540
|
+
if (done) {
|
|
2541
|
+
break;
|
|
2542
|
+
}
|
|
2543
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
2544
|
+
buffer += chunk;
|
|
2545
|
+
const lines = buffer.split("\n");
|
|
2546
|
+
buffer = lines.pop() || "";
|
|
2547
|
+
for (const line of lines) {
|
|
2548
|
+
const parsed = this._parseLine(line);
|
|
2549
|
+
if (!parsed) continue;
|
|
2550
|
+
if (parsed.content) {
|
|
2551
|
+
this._handleContent(parsed.content, callbacks, modelId);
|
|
2552
|
+
}
|
|
2553
|
+
if (parsed.reasoning) {
|
|
2554
|
+
this._handleThinking(parsed.reasoning, callbacks);
|
|
2555
|
+
}
|
|
2556
|
+
if (parsed.usage) {
|
|
2557
|
+
usage = parsed.usage;
|
|
2558
|
+
}
|
|
2559
|
+
if (parsed.model) {
|
|
2560
|
+
model = parsed.model;
|
|
2561
|
+
}
|
|
2562
|
+
if (parsed.finish_reason) {
|
|
2563
|
+
finish_reason = parsed.finish_reason;
|
|
2564
|
+
}
|
|
2565
|
+
if (parsed.citations) {
|
|
2566
|
+
citations = parsed.citations;
|
|
2567
|
+
}
|
|
2568
|
+
if (parsed.annotations) {
|
|
2569
|
+
annotations = parsed.annotations;
|
|
2570
|
+
}
|
|
2571
|
+
if (parsed.images) {
|
|
2572
|
+
this._mergeImages(parsed.images);
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
} finally {
|
|
2577
|
+
reader.releaseLock();
|
|
2578
|
+
}
|
|
2579
|
+
return {
|
|
2580
|
+
content: this.accumulatedContent,
|
|
2581
|
+
thinking: this.accumulatedThinking || void 0,
|
|
2582
|
+
images: this.accumulatedImages.length > 0 ? this.accumulatedImages : void 0,
|
|
2583
|
+
usage,
|
|
2584
|
+
model,
|
|
2585
|
+
finish_reason,
|
|
2586
|
+
citations,
|
|
2587
|
+
annotations
|
|
2588
|
+
};
|
|
2589
|
+
}
|
|
2590
|
+
/**
|
|
2591
|
+
* Parse a single SSE line
|
|
2592
|
+
*/
|
|
2593
|
+
_parseLine(line) {
|
|
2594
|
+
if (!line.trim()) return null;
|
|
2595
|
+
if (!line.startsWith("data: ")) {
|
|
2596
|
+
return null;
|
|
2597
|
+
}
|
|
2598
|
+
const jsonData = line.slice(6);
|
|
2599
|
+
if (jsonData === "[DONE]") {
|
|
2600
|
+
return null;
|
|
2601
|
+
}
|
|
2602
|
+
try {
|
|
2603
|
+
const parsed = JSON.parse(jsonData);
|
|
2604
|
+
const result = {};
|
|
2605
|
+
if (parsed.choices?.[0]?.delta?.content) {
|
|
2606
|
+
result.content = parsed.choices[0].delta.content;
|
|
2607
|
+
}
|
|
2608
|
+
if (parsed.choices?.[0]?.delta?.reasoning) {
|
|
2609
|
+
result.reasoning = parsed.choices[0].delta.reasoning;
|
|
2610
|
+
}
|
|
2611
|
+
if (parsed.usage) {
|
|
2612
|
+
result.usage = {
|
|
2613
|
+
total_tokens: parsed.usage.total_tokens,
|
|
2614
|
+
prompt_tokens: parsed.usage.prompt_tokens,
|
|
2615
|
+
completion_tokens: parsed.usage.completion_tokens
|
|
2616
|
+
};
|
|
2617
|
+
}
|
|
2618
|
+
if (parsed.model) {
|
|
2619
|
+
result.model = parsed.model;
|
|
2620
|
+
}
|
|
2621
|
+
if (parsed.citations) {
|
|
2622
|
+
result.citations = parsed.citations;
|
|
2623
|
+
}
|
|
2624
|
+
if (parsed.annotations) {
|
|
2625
|
+
result.annotations = parsed.annotations;
|
|
2626
|
+
}
|
|
2627
|
+
if (parsed.choices?.[0]?.finish_reason) {
|
|
2628
|
+
result.finish_reason = parsed.choices[0].finish_reason;
|
|
2629
|
+
}
|
|
2630
|
+
const images = parsed.choices?.[0]?.message?.images || parsed.choices?.[0]?.delta?.images;
|
|
2631
|
+
if (images && Array.isArray(images)) {
|
|
2632
|
+
result.images = images;
|
|
2633
|
+
}
|
|
2634
|
+
return result;
|
|
2635
|
+
} catch {
|
|
2636
|
+
return null;
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
/**
|
|
2640
|
+
* Handle content delta with thinking support
|
|
2641
|
+
*/
|
|
2642
|
+
_handleContent(content, callbacks, modelId) {
|
|
2643
|
+
if (this.isInThinking && !this.isInContent) {
|
|
2644
|
+
this.accumulatedThinking += "</thinking>";
|
|
2645
|
+
callbacks.onThinking(this.accumulatedThinking);
|
|
2646
|
+
this.isInThinking = false;
|
|
2647
|
+
this.isInContent = true;
|
|
2648
|
+
}
|
|
2649
|
+
if (modelId) {
|
|
2650
|
+
this._extractThinkingFromContent(content, callbacks);
|
|
2651
|
+
} else {
|
|
2652
|
+
this.accumulatedContent += content;
|
|
2653
|
+
}
|
|
2654
|
+
callbacks.onContent(this.accumulatedContent);
|
|
2655
|
+
}
|
|
2656
|
+
/**
|
|
2657
|
+
* Handle thinking/reasoning content
|
|
2658
|
+
*/
|
|
2659
|
+
_handleThinking(reasoning, callbacks) {
|
|
2660
|
+
if (!this.isInThinking) {
|
|
2661
|
+
this.accumulatedThinking += "<thinking> ";
|
|
2662
|
+
this.isInThinking = true;
|
|
2663
|
+
}
|
|
2664
|
+
this.accumulatedThinking += reasoning;
|
|
2665
|
+
callbacks.onThinking(this.accumulatedThinking);
|
|
2666
|
+
}
|
|
2667
|
+
/**
|
|
2668
|
+
* Extract thinking blocks from content (for models with inline thinking)
|
|
2669
|
+
*/
|
|
2670
|
+
_extractThinkingFromContent(content, callbacks) {
|
|
2671
|
+
const parts = content.split(/(<thinking>|<\/thinking>)/);
|
|
2672
|
+
for (const part of parts) {
|
|
2673
|
+
if (part === "<thinking>") {
|
|
2674
|
+
this.isInThinking = true;
|
|
2675
|
+
if (!this.accumulatedThinking.includes("<thinking>")) {
|
|
2676
|
+
this.accumulatedThinking += "<thinking> ";
|
|
2677
|
+
}
|
|
2678
|
+
} else if (part === "</thinking>") {
|
|
2679
|
+
this.isInThinking = false;
|
|
2680
|
+
this.accumulatedThinking += "</thinking>";
|
|
2681
|
+
} else if (this.isInThinking) {
|
|
2682
|
+
this.accumulatedThinking += part;
|
|
2683
|
+
} else {
|
|
2684
|
+
this.accumulatedContent += part;
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
/**
|
|
2689
|
+
* Merge images into accumulated array, avoiding duplicates
|
|
2690
|
+
*/
|
|
2691
|
+
_mergeImages(newImages) {
|
|
2692
|
+
for (const img of newImages) {
|
|
2693
|
+
const newUrl = img.image_url?.url;
|
|
2694
|
+
const existingIndex = this.accumulatedImages.findIndex((existing) => {
|
|
2695
|
+
const existingUrl = existing.image_url?.url;
|
|
2696
|
+
if (newUrl && existingUrl) {
|
|
2697
|
+
return existingUrl === newUrl;
|
|
2698
|
+
}
|
|
2699
|
+
if (img.index !== void 0 && existing.index !== void 0) {
|
|
2700
|
+
return existing.index === img.index;
|
|
2701
|
+
}
|
|
2702
|
+
return false;
|
|
2703
|
+
});
|
|
2704
|
+
if (existingIndex === -1) {
|
|
2705
|
+
this.accumulatedImages.push(img);
|
|
2706
|
+
} else {
|
|
2707
|
+
this.accumulatedImages[existingIndex] = img;
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
};
|
|
2712
|
+
|
|
2713
|
+
// utils/torUtils.ts
|
|
2714
|
+
var TOR_ONION_SUFFIX = ".onion";
|
|
2715
|
+
var isTorContext = () => {
|
|
2716
|
+
if (typeof window === "undefined") return false;
|
|
2717
|
+
const hostname = window.location.hostname.toLowerCase();
|
|
2718
|
+
return hostname.endsWith(TOR_ONION_SUFFIX);
|
|
2719
|
+
};
|
|
2720
|
+
var isOnionUrl = (url) => {
|
|
2721
|
+
if (!url) return false;
|
|
2722
|
+
const trimmed = url.trim().toLowerCase();
|
|
2723
|
+
if (!trimmed) return false;
|
|
2724
|
+
try {
|
|
2725
|
+
const candidate = trimmed.startsWith("http") ? trimmed : `http://${trimmed}`;
|
|
2726
|
+
return new URL(candidate).hostname.endsWith(TOR_ONION_SUFFIX);
|
|
2727
|
+
} catch {
|
|
2728
|
+
return trimmed.includes(TOR_ONION_SUFFIX);
|
|
2729
|
+
}
|
|
2730
|
+
};
|
|
2731
|
+
var shouldAllowHttp = (url, torMode) => {
|
|
2732
|
+
if (!url.startsWith("http://")) return true;
|
|
2733
|
+
if (url.includes("localhost") || url.includes("127.0.0.1")) return true;
|
|
2734
|
+
return torMode && isOnionUrl(url);
|
|
2735
|
+
};
|
|
2736
|
+
var normalizeProviderUrl = (url, torMode = false) => {
|
|
2737
|
+
if (!url || typeof url !== "string") return null;
|
|
2738
|
+
const trimmed = url.trim();
|
|
2739
|
+
if (!trimmed) return null;
|
|
2740
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
2741
|
+
return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
|
|
2742
|
+
}
|
|
2743
|
+
const useHttpForOnion = torMode && isOnionUrl(trimmed);
|
|
2744
|
+
const withProto = `${useHttpForOnion ? "http" : "https"}://${trimmed}`;
|
|
2745
|
+
return withProto.endsWith("/") ? withProto : `${withProto}/`;
|
|
2746
|
+
};
|
|
2747
|
+
var dedupePreserveOrder = (urls) => {
|
|
2748
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2749
|
+
const out = [];
|
|
2750
|
+
for (const url of urls) {
|
|
2751
|
+
if (!seen.has(url)) {
|
|
2752
|
+
seen.add(url);
|
|
2753
|
+
out.push(url);
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
return out;
|
|
2757
|
+
};
|
|
2758
|
+
var getProviderEndpoints = (provider, torMode) => {
|
|
2759
|
+
const rawUrls = [
|
|
2760
|
+
provider.endpoint_url,
|
|
2761
|
+
...Array.isArray(provider.endpoint_urls) ? provider.endpoint_urls : [],
|
|
2762
|
+
provider.onion_url,
|
|
2763
|
+
...Array.isArray(provider.onion_urls) ? provider.onion_urls : []
|
|
2764
|
+
];
|
|
2765
|
+
const normalized = rawUrls.map((value) => normalizeProviderUrl(value, torMode)).filter((value) => Boolean(value));
|
|
2766
|
+
const unique = dedupePreserveOrder(normalized).filter(
|
|
2767
|
+
(value) => shouldAllowHttp(value, torMode)
|
|
2768
|
+
);
|
|
2769
|
+
if (unique.length === 0) return [];
|
|
2770
|
+
const onion = unique.filter((value) => isOnionUrl(value));
|
|
2771
|
+
const clearnet = unique.filter((value) => !isOnionUrl(value));
|
|
2772
|
+
if (torMode) {
|
|
2773
|
+
return onion.length > 0 ? onion : clearnet;
|
|
2774
|
+
}
|
|
2775
|
+
return clearnet;
|
|
2776
|
+
};
|
|
2777
|
+
var filterBaseUrlsForTor = (baseUrls, torMode) => {
|
|
2778
|
+
if (!Array.isArray(baseUrls)) return [];
|
|
2779
|
+
const normalized = baseUrls.map((value) => normalizeProviderUrl(value, torMode)).filter((value) => Boolean(value));
|
|
2780
|
+
const filtered = normalized.filter(
|
|
2781
|
+
(value) => torMode ? true : !isOnionUrl(value)
|
|
2782
|
+
);
|
|
2783
|
+
return dedupePreserveOrder(
|
|
2784
|
+
filtered.filter((value) => shouldAllowHttp(value, torMode))
|
|
2785
|
+
);
|
|
2786
|
+
};
|
|
2787
|
+
|
|
2788
|
+
// client/ProviderManager.ts
|
|
2789
|
+
function getImageResolutionFromDataUrl(dataUrl) {
|
|
2790
|
+
try {
|
|
2791
|
+
if (typeof dataUrl !== "string" || !dataUrl.startsWith("data:"))
|
|
2792
|
+
return null;
|
|
2793
|
+
const commaIdx = dataUrl.indexOf(",");
|
|
2794
|
+
if (commaIdx === -1) return null;
|
|
2795
|
+
const meta = dataUrl.slice(5, commaIdx);
|
|
2796
|
+
const base64 = dataUrl.slice(commaIdx + 1);
|
|
2797
|
+
const binary = typeof atob === "function" ? atob(base64) : Buffer.from(base64, "base64").toString("binary");
|
|
2798
|
+
const len = binary.length;
|
|
2799
|
+
const bytes = new Uint8Array(len);
|
|
2800
|
+
for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i);
|
|
2801
|
+
const isPNG = meta.includes("image/png");
|
|
2802
|
+
const isJPEG = meta.includes("image/jpeg") || meta.includes("image/jpg");
|
|
2803
|
+
if (isPNG) {
|
|
2804
|
+
const sig = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
2805
|
+
for (let i = 0; i < sig.length; i++) {
|
|
2806
|
+
if (bytes[i] !== sig[i]) return null;
|
|
2807
|
+
}
|
|
2808
|
+
const view = new DataView(
|
|
2809
|
+
bytes.buffer,
|
|
2810
|
+
bytes.byteOffset,
|
|
2811
|
+
bytes.byteLength
|
|
2812
|
+
);
|
|
2813
|
+
const width = view.getUint32(16, false);
|
|
2814
|
+
const height = view.getUint32(20, false);
|
|
2815
|
+
if (width > 0 && height > 0) return { width, height };
|
|
2816
|
+
return null;
|
|
2817
|
+
}
|
|
2818
|
+
if (isJPEG) {
|
|
2819
|
+
let offset = 0;
|
|
2820
|
+
if (bytes[offset++] !== 255 || bytes[offset++] !== 216) return null;
|
|
2821
|
+
while (offset < bytes.length) {
|
|
2822
|
+
while (offset < bytes.length && bytes[offset] !== 255) offset++;
|
|
2823
|
+
if (offset + 1 >= bytes.length) break;
|
|
2824
|
+
while (bytes[offset] === 255) offset++;
|
|
2825
|
+
const marker = bytes[offset++];
|
|
2826
|
+
if (marker === 216 || marker === 217) continue;
|
|
2827
|
+
if (offset + 1 >= bytes.length) break;
|
|
2828
|
+
const length = bytes[offset] << 8 | bytes[offset + 1];
|
|
2829
|
+
offset += 2;
|
|
2830
|
+
if (marker === 192 || marker === 194) {
|
|
2831
|
+
if (length < 7 || offset + length - 2 > bytes.length) return null;
|
|
2832
|
+
const precision = bytes[offset];
|
|
2833
|
+
const height = bytes[offset + 1] << 8 | bytes[offset + 2];
|
|
2834
|
+
const width = bytes[offset + 3] << 8 | bytes[offset + 4];
|
|
2835
|
+
if (precision > 0 && width > 0 && height > 0)
|
|
2836
|
+
return { width, height };
|
|
2837
|
+
return null;
|
|
2838
|
+
} else {
|
|
2839
|
+
offset += length - 2;
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
return null;
|
|
2843
|
+
}
|
|
2844
|
+
return null;
|
|
2845
|
+
} catch {
|
|
2846
|
+
return null;
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
function calculateImageTokens(width, height, detail = "auto") {
|
|
2850
|
+
if (detail === "low") return 85;
|
|
2851
|
+
let w = width;
|
|
2852
|
+
let h = height;
|
|
2853
|
+
if (w > 2048 || h > 2048) {
|
|
2854
|
+
const aspectRatio = w / h;
|
|
2855
|
+
if (w > h) {
|
|
2856
|
+
w = 2048;
|
|
2857
|
+
h = Math.floor(w / aspectRatio);
|
|
2858
|
+
} else {
|
|
2859
|
+
h = 2048;
|
|
2860
|
+
w = Math.floor(h * aspectRatio);
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
if (w > 768 || h > 768) {
|
|
2864
|
+
const aspectRatio = w / h;
|
|
2865
|
+
if (w > h) {
|
|
2866
|
+
w = 768;
|
|
2867
|
+
h = Math.floor(w / aspectRatio);
|
|
2868
|
+
} else {
|
|
2869
|
+
h = 768;
|
|
2870
|
+
w = Math.floor(h * aspectRatio);
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
const tilesWidth = Math.floor((w + 511) / 512);
|
|
2874
|
+
const tilesHeight = Math.floor((h + 511) / 512);
|
|
2875
|
+
const numTiles = tilesWidth * tilesHeight;
|
|
2876
|
+
return 85 + 170 * numTiles;
|
|
2877
|
+
}
|
|
2878
|
+
var ProviderManager = class {
|
|
2879
|
+
constructor(providerRegistry) {
|
|
2880
|
+
this.providerRegistry = providerRegistry;
|
|
2881
|
+
}
|
|
2882
|
+
failedProviders = /* @__PURE__ */ new Set();
|
|
2883
|
+
/**
|
|
2884
|
+
* Reset the failed providers list
|
|
2885
|
+
*/
|
|
2886
|
+
resetFailedProviders() {
|
|
2887
|
+
this.failedProviders.clear();
|
|
2888
|
+
}
|
|
2889
|
+
/**
|
|
2890
|
+
* Mark a provider as failed
|
|
2891
|
+
*/
|
|
2892
|
+
markFailed(baseUrl) {
|
|
2893
|
+
this.failedProviders.add(baseUrl);
|
|
2894
|
+
}
|
|
2895
|
+
/**
|
|
2896
|
+
* Check if a provider has failed
|
|
2897
|
+
*/
|
|
2898
|
+
hasFailed(baseUrl) {
|
|
2899
|
+
return this.failedProviders.has(baseUrl);
|
|
2900
|
+
}
|
|
2901
|
+
/**
|
|
2902
|
+
* Find the next best provider for a model
|
|
2903
|
+
* @param modelId The model ID to find a provider for
|
|
2904
|
+
* @param currentBaseUrl The current provider to exclude
|
|
2905
|
+
* @returns The best provider URL or null if none available
|
|
2906
|
+
*/
|
|
2907
|
+
findNextBestProvider(modelId, currentBaseUrl) {
|
|
2908
|
+
try {
|
|
2909
|
+
const torMode = isTorContext();
|
|
2910
|
+
const disabledProviders = new Set(
|
|
2911
|
+
this.providerRegistry.getDisabledProviders()
|
|
2912
|
+
);
|
|
2913
|
+
const allProviders = this.providerRegistry.getAllProvidersModels();
|
|
2914
|
+
const candidates = [];
|
|
2915
|
+
for (const [baseUrl, models] of Object.entries(allProviders)) {
|
|
2916
|
+
if (baseUrl === currentBaseUrl || this.failedProviders.has(baseUrl) || disabledProviders.has(baseUrl)) {
|
|
2917
|
+
continue;
|
|
2918
|
+
}
|
|
2919
|
+
if (!torMode && isOnionUrl(baseUrl)) {
|
|
2920
|
+
continue;
|
|
2921
|
+
}
|
|
2922
|
+
const model = models.find((m) => m.id === modelId);
|
|
2923
|
+
if (!model) continue;
|
|
2924
|
+
const cost = model.sats_pricing?.completion ?? 0;
|
|
2925
|
+
candidates.push({ baseUrl, model, cost });
|
|
2926
|
+
}
|
|
2927
|
+
candidates.sort((a, b) => a.cost - b.cost);
|
|
2928
|
+
return candidates.length > 0 ? candidates[0].baseUrl : null;
|
|
2929
|
+
} catch (error) {
|
|
2930
|
+
console.error("Error finding next best provider:", error);
|
|
2931
|
+
return null;
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
/**
|
|
2935
|
+
* Find the best model for a provider
|
|
2936
|
+
* Useful when switching providers and need to find equivalent model
|
|
2937
|
+
*/
|
|
2938
|
+
async getModelForProvider(baseUrl, modelId) {
|
|
2939
|
+
const models = this.providerRegistry.getModelsForProvider(baseUrl);
|
|
2940
|
+
const exactMatch = models.find((m) => m.id === modelId);
|
|
2941
|
+
if (exactMatch) return exactMatch;
|
|
2942
|
+
const providerInfo = await this.providerRegistry.getProviderInfo(baseUrl);
|
|
2943
|
+
if (providerInfo?.version && /^0\.1\./.test(providerInfo.version)) {
|
|
2944
|
+
const suffix = modelId.split("/").pop();
|
|
2945
|
+
const suffixMatch = models.find((m) => m.id === suffix);
|
|
2946
|
+
if (suffixMatch) return suffixMatch;
|
|
2947
|
+
}
|
|
2948
|
+
return null;
|
|
2949
|
+
}
|
|
2950
|
+
/**
|
|
2951
|
+
* Get all available providers for a model
|
|
2952
|
+
* Returns sorted list by price
|
|
2953
|
+
*/
|
|
2954
|
+
getAllProvidersForModel(modelId) {
|
|
2955
|
+
const candidates = [];
|
|
2956
|
+
const allProviders = this.providerRegistry.getAllProvidersModels();
|
|
2957
|
+
const disabledProviders = new Set(
|
|
2958
|
+
this.providerRegistry.getDisabledProviders()
|
|
2959
|
+
);
|
|
2960
|
+
const torMode = isTorContext();
|
|
2961
|
+
for (const [baseUrl, models] of Object.entries(allProviders)) {
|
|
2962
|
+
if (disabledProviders.has(baseUrl)) continue;
|
|
2963
|
+
if (!torMode && isOnionUrl(baseUrl)) continue;
|
|
2964
|
+
const model = models.find((m) => m.id === modelId);
|
|
2965
|
+
if (!model) continue;
|
|
2966
|
+
const cost = model.sats_pricing?.completion ?? 0;
|
|
2967
|
+
candidates.push({ baseUrl, model, cost });
|
|
2968
|
+
}
|
|
2969
|
+
return candidates.sort((a, b) => a.cost - b.cost);
|
|
2970
|
+
}
|
|
2971
|
+
/**
|
|
2972
|
+
* Get providers for a model sorted by prompt+completion pricing
|
|
2973
|
+
*/
|
|
2974
|
+
getProviderPriceRankingForModel(modelId, options = {}) {
|
|
2975
|
+
const normalizedId = this.normalizeModelId(modelId);
|
|
2976
|
+
const includeDisabled = options.includeDisabled ?? false;
|
|
2977
|
+
const torMode = options.torMode ?? false;
|
|
2978
|
+
const disabledProviders = new Set(
|
|
2979
|
+
this.providerRegistry.getDisabledProviders()
|
|
2980
|
+
);
|
|
2981
|
+
const allModels = this.providerRegistry.getAllProvidersModels();
|
|
2982
|
+
const results = [];
|
|
2983
|
+
for (const [baseUrl, models] of Object.entries(allModels)) {
|
|
2984
|
+
if (!includeDisabled && disabledProviders.has(baseUrl)) continue;
|
|
2985
|
+
if (torMode && !baseUrl.includes(".onion")) continue;
|
|
2986
|
+
if (!torMode && baseUrl.includes(".onion")) continue;
|
|
2987
|
+
const match = models.find(
|
|
2988
|
+
(model) => this.normalizeModelId(model.id) === normalizedId
|
|
2989
|
+
);
|
|
2990
|
+
if (!match?.sats_pricing) continue;
|
|
2991
|
+
const prompt = match.sats_pricing.prompt;
|
|
2992
|
+
const completion = match.sats_pricing.completion;
|
|
2993
|
+
if (typeof prompt !== "number" || typeof completion !== "number") {
|
|
2994
|
+
continue;
|
|
2995
|
+
}
|
|
2996
|
+
const promptPerMillion = prompt * 1e6;
|
|
2997
|
+
const completionPerMillion = completion * 1e6;
|
|
2998
|
+
const totalPerMillion = promptPerMillion + completionPerMillion;
|
|
2999
|
+
results.push({
|
|
3000
|
+
baseUrl,
|
|
3001
|
+
model: match,
|
|
3002
|
+
promptPerMillion,
|
|
3003
|
+
completionPerMillion,
|
|
3004
|
+
totalPerMillion
|
|
3005
|
+
});
|
|
3006
|
+
}
|
|
3007
|
+
return results.sort((a, b) => {
|
|
3008
|
+
if (a.totalPerMillion !== b.totalPerMillion) {
|
|
3009
|
+
return a.totalPerMillion - b.totalPerMillion;
|
|
3010
|
+
}
|
|
3011
|
+
return a.baseUrl.localeCompare(b.baseUrl);
|
|
3012
|
+
});
|
|
3013
|
+
}
|
|
3014
|
+
/**
|
|
3015
|
+
* Get best-priced provider for a specific model
|
|
3016
|
+
*/
|
|
3017
|
+
getBestProviderForModel(modelId, options = {}) {
|
|
3018
|
+
const ranking = this.getProviderPriceRankingForModel(modelId, options);
|
|
3019
|
+
return ranking[0]?.baseUrl ?? null;
|
|
3020
|
+
}
|
|
3021
|
+
normalizeModelId(modelId) {
|
|
3022
|
+
return modelId.includes("/") ? modelId.split("/").pop() || modelId : modelId;
|
|
3023
|
+
}
|
|
3024
|
+
/**
|
|
3025
|
+
* Check if a provider accepts a specific mint
|
|
3026
|
+
*/
|
|
3027
|
+
providerAcceptsMint(baseUrl, mintUrl) {
|
|
3028
|
+
const providerMints = this.providerRegistry.getProviderMints(baseUrl);
|
|
3029
|
+
if (providerMints.length === 0) {
|
|
3030
|
+
return true;
|
|
3031
|
+
}
|
|
3032
|
+
return providerMints.includes(mintUrl);
|
|
3033
|
+
}
|
|
3034
|
+
/**
|
|
3035
|
+
* Get required sats for a model based on message history
|
|
3036
|
+
* Simple estimation based on typical usage
|
|
3037
|
+
*/
|
|
3038
|
+
getRequiredSatsForModel(model, apiMessages, maxTokens) {
|
|
3039
|
+
try {
|
|
3040
|
+
let imageTokens = 0;
|
|
3041
|
+
if (apiMessages) {
|
|
3042
|
+
for (const msg of apiMessages) {
|
|
3043
|
+
const content = msg?.content;
|
|
3044
|
+
if (Array.isArray(content)) {
|
|
3045
|
+
for (const part of content) {
|
|
3046
|
+
const isImage = part && typeof part === "object" && part.type === "image_url";
|
|
3047
|
+
const url = isImage ? typeof part.image_url === "string" ? part.image_url : part.image_url?.url : void 0;
|
|
3048
|
+
if (url && typeof url === "string" && url.startsWith("data:")) {
|
|
3049
|
+
const res = getImageResolutionFromDataUrl(url);
|
|
3050
|
+
if (res) {
|
|
3051
|
+
const tokensFromImage = calculateImageTokens(
|
|
3052
|
+
res.width,
|
|
3053
|
+
res.height
|
|
3054
|
+
);
|
|
3055
|
+
imageTokens += tokensFromImage;
|
|
3056
|
+
console.log("IMAGE INPUT RESOLUTION", {
|
|
3057
|
+
width: res.width,
|
|
3058
|
+
height: res.height,
|
|
3059
|
+
tokensFromImage
|
|
3060
|
+
});
|
|
3061
|
+
} else {
|
|
3062
|
+
console.log(
|
|
3063
|
+
"IMAGE INPUT RESOLUTION",
|
|
3064
|
+
"unknown (unsupported format or parse failure)"
|
|
3065
|
+
);
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
const apiMessagesNoImages = apiMessages ? apiMessages.map((m) => {
|
|
3073
|
+
if (Array.isArray(m?.content)) {
|
|
3074
|
+
const filtered = m.content.filter(
|
|
3075
|
+
(p) => !(p && typeof p === "object" && p.type === "image_url")
|
|
3076
|
+
);
|
|
3077
|
+
return { ...m, content: filtered };
|
|
3078
|
+
}
|
|
3079
|
+
return m;
|
|
3080
|
+
}) : void 0;
|
|
3081
|
+
const approximateTokens = apiMessagesNoImages ? Math.ceil(JSON.stringify(apiMessagesNoImages, null, 2).length / 2.84) : 1e4;
|
|
3082
|
+
const totalInputTokens = approximateTokens + imageTokens;
|
|
3083
|
+
const sp = model?.sats_pricing;
|
|
3084
|
+
if (!sp) return 0;
|
|
3085
|
+
if (!sp.max_completion_cost) {
|
|
3086
|
+
return sp.max_cost ?? 50;
|
|
3087
|
+
}
|
|
3088
|
+
const promptCosts = (sp.prompt || 0) * totalInputTokens;
|
|
3089
|
+
let completionCost = sp.max_completion_cost;
|
|
3090
|
+
if (maxTokens !== void 0 && sp.completion) {
|
|
3091
|
+
completionCost = sp.completion * maxTokens;
|
|
3092
|
+
}
|
|
3093
|
+
const totalEstimatedCosts = (promptCosts + completionCost) * 1.05;
|
|
3094
|
+
return totalEstimatedCosts;
|
|
3095
|
+
} catch (e) {
|
|
3096
|
+
console.error(e);
|
|
3097
|
+
return 0;
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
};
|
|
3101
|
+
|
|
3102
|
+
// client/RoutstrClient.ts
|
|
3103
|
+
var RoutstrClient = class {
|
|
3104
|
+
constructor(walletAdapter, storageAdapter, providerRegistry, alertLevel, mode = "xcashu") {
|
|
3105
|
+
this.walletAdapter = walletAdapter;
|
|
3106
|
+
this.storageAdapter = storageAdapter;
|
|
3107
|
+
this.providerRegistry = providerRegistry;
|
|
3108
|
+
this.balanceManager = new BalanceManager(walletAdapter, storageAdapter);
|
|
3109
|
+
this.cashuSpender = new CashuSpender(
|
|
3110
|
+
walletAdapter,
|
|
3111
|
+
storageAdapter,
|
|
3112
|
+
providerRegistry,
|
|
3113
|
+
this.balanceManager
|
|
3114
|
+
);
|
|
3115
|
+
this.streamProcessor = new StreamProcessor();
|
|
3116
|
+
this.providerManager = new ProviderManager(providerRegistry);
|
|
3117
|
+
this.alertLevel = alertLevel;
|
|
3118
|
+
this.mode = mode;
|
|
3119
|
+
}
|
|
3120
|
+
cashuSpender;
|
|
3121
|
+
balanceManager;
|
|
3122
|
+
streamProcessor;
|
|
3123
|
+
providerManager;
|
|
3124
|
+
alertLevel;
|
|
3125
|
+
mode;
|
|
3126
|
+
/**
|
|
3127
|
+
* Get the current client mode
|
|
3128
|
+
*/
|
|
3129
|
+
getMode() {
|
|
3130
|
+
return this.mode;
|
|
3131
|
+
}
|
|
3132
|
+
/**
|
|
3133
|
+
* Get the CashuSpender instance
|
|
3134
|
+
*/
|
|
3135
|
+
getCashuSpender() {
|
|
3136
|
+
return this.cashuSpender;
|
|
3137
|
+
}
|
|
3138
|
+
/**
|
|
3139
|
+
* Get the BalanceManager instance
|
|
3140
|
+
*/
|
|
3141
|
+
getBalanceManager() {
|
|
3142
|
+
return this.balanceManager;
|
|
3143
|
+
}
|
|
3144
|
+
/**
|
|
3145
|
+
* Get the ProviderManager instance
|
|
3146
|
+
*/
|
|
3147
|
+
getProviderManager() {
|
|
3148
|
+
return this.providerManager;
|
|
3149
|
+
}
|
|
3150
|
+
/**
|
|
3151
|
+
* Check if the client is currently busy (in critical section)
|
|
3152
|
+
*/
|
|
3153
|
+
get isBusy() {
|
|
3154
|
+
return this.cashuSpender.isBusy;
|
|
3155
|
+
}
|
|
3156
|
+
/**
|
|
3157
|
+
* Route an API request to the upstream provider
|
|
3158
|
+
*
|
|
3159
|
+
* This is a simpler alternative to fetchAIResponse that just proxies
|
|
3160
|
+
* the request upstream without the streaming callback machinery.
|
|
3161
|
+
* Useful for daemon-style routing where you just need to forward
|
|
3162
|
+
* requests and get responses back.
|
|
3163
|
+
*/
|
|
3164
|
+
async routeRequest(params) {
|
|
3165
|
+
const {
|
|
3166
|
+
path,
|
|
3167
|
+
method,
|
|
3168
|
+
body,
|
|
3169
|
+
headers = {},
|
|
3170
|
+
baseUrl,
|
|
3171
|
+
mintUrl,
|
|
3172
|
+
modelId
|
|
3173
|
+
} = params;
|
|
3174
|
+
await this._checkBalance();
|
|
3175
|
+
let requiredSats = 1;
|
|
3176
|
+
let selectedModel;
|
|
3177
|
+
if (modelId) {
|
|
3178
|
+
const providerModel = await this.providerManager.getModelForProvider(
|
|
3179
|
+
baseUrl,
|
|
3180
|
+
modelId
|
|
3181
|
+
);
|
|
3182
|
+
selectedModel = providerModel ?? void 0;
|
|
3183
|
+
if (selectedModel) {
|
|
3184
|
+
requiredSats = this.providerManager.getRequiredSatsForModel(
|
|
3185
|
+
selectedModel,
|
|
3186
|
+
[]
|
|
3187
|
+
);
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
const { token, tokenBalance, tokenBalanceUnit } = await this._spendToken({
|
|
3191
|
+
mintUrl,
|
|
3192
|
+
amount: requiredSats,
|
|
3193
|
+
baseUrl
|
|
3194
|
+
});
|
|
3195
|
+
let requestBody = body;
|
|
3196
|
+
if (body && typeof body === "object") {
|
|
3197
|
+
const bodyObj = body;
|
|
3198
|
+
if (!bodyObj.stream) {
|
|
3199
|
+
requestBody = { ...bodyObj, stream: false };
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
const baseHeaders = this._buildBaseHeaders(headers);
|
|
3203
|
+
const requestHeaders = this._withAuthHeader(baseHeaders, token);
|
|
3204
|
+
const response = await this._makeRequest({
|
|
3205
|
+
path,
|
|
3206
|
+
method,
|
|
3207
|
+
body: method === "GET" ? void 0 : requestBody,
|
|
3208
|
+
baseUrl,
|
|
3209
|
+
mintUrl,
|
|
3210
|
+
token,
|
|
3211
|
+
requiredSats,
|
|
3212
|
+
headers: requestHeaders,
|
|
3213
|
+
baseHeaders,
|
|
3214
|
+
selectedModel
|
|
3215
|
+
});
|
|
3216
|
+
const tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
|
|
3217
|
+
await this._handlePostResponseBalanceUpdate({
|
|
3218
|
+
token,
|
|
3219
|
+
baseUrl,
|
|
3220
|
+
initialTokenBalance: tokenBalanceInSats,
|
|
3221
|
+
response
|
|
3222
|
+
});
|
|
3223
|
+
return response;
|
|
3224
|
+
}
|
|
3225
|
+
/**
|
|
3226
|
+
* Fetch AI response with streaming
|
|
3227
|
+
*/
|
|
3228
|
+
async fetchAIResponse(options, callbacks) {
|
|
3229
|
+
const {
|
|
3230
|
+
messageHistory,
|
|
3231
|
+
selectedModel,
|
|
3232
|
+
baseUrl,
|
|
3233
|
+
mintUrl,
|
|
3234
|
+
balance,
|
|
3235
|
+
transactionHistory,
|
|
3236
|
+
maxTokens,
|
|
3237
|
+
headers
|
|
3238
|
+
} = options;
|
|
3239
|
+
const apiMessages = await this._convertMessages(messageHistory);
|
|
3240
|
+
const requiredSats = this.providerManager.getRequiredSatsForModel(
|
|
3241
|
+
selectedModel,
|
|
3242
|
+
apiMessages,
|
|
3243
|
+
maxTokens
|
|
3244
|
+
);
|
|
3245
|
+
try {
|
|
3246
|
+
await this._checkBalance();
|
|
3247
|
+
callbacks.onPaymentProcessing?.(true);
|
|
3248
|
+
const spendResult = await this._spendToken({
|
|
3249
|
+
mintUrl,
|
|
3250
|
+
amount: requiredSats,
|
|
3251
|
+
baseUrl
|
|
3252
|
+
});
|
|
3253
|
+
let token = spendResult.token;
|
|
3254
|
+
let tokenBalance = spendResult.tokenBalance;
|
|
3255
|
+
let tokenBalanceUnit = spendResult.tokenBalanceUnit;
|
|
3256
|
+
const tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
|
|
3257
|
+
callbacks.onTokenCreated?.(this._getPendingCashuTokenAmount());
|
|
3258
|
+
const baseHeaders = this._buildBaseHeaders(headers);
|
|
3259
|
+
const requestHeaders = this._withAuthHeader(baseHeaders, token);
|
|
3260
|
+
this.providerManager.resetFailedProviders();
|
|
3261
|
+
const providerInfo = await this.providerRegistry.getProviderInfo(baseUrl);
|
|
3262
|
+
const providerVersion = providerInfo?.version ?? "";
|
|
3263
|
+
let modelIdForRequest = selectedModel.id;
|
|
3264
|
+
if (/^0\.1\./.test(providerVersion)) {
|
|
3265
|
+
const newModel = await this.providerManager.getModelForProvider(
|
|
3266
|
+
baseUrl,
|
|
3267
|
+
selectedModel.id
|
|
3268
|
+
);
|
|
3269
|
+
modelIdForRequest = newModel?.id ?? selectedModel.id;
|
|
3270
|
+
}
|
|
3271
|
+
const body = {
|
|
3272
|
+
model: modelIdForRequest,
|
|
3273
|
+
messages: apiMessages,
|
|
3274
|
+
stream: true
|
|
3275
|
+
};
|
|
3276
|
+
if (maxTokens !== void 0) {
|
|
3277
|
+
body.max_tokens = maxTokens;
|
|
3278
|
+
}
|
|
3279
|
+
if (selectedModel?.name?.startsWith("OpenAI:")) {
|
|
3280
|
+
body.tools = [{ type: "web_search" }];
|
|
3281
|
+
}
|
|
3282
|
+
const response = await this._makeRequest({
|
|
3283
|
+
path: "/v1/chat/completions",
|
|
3284
|
+
method: "POST",
|
|
3285
|
+
body,
|
|
3286
|
+
selectedModel,
|
|
3287
|
+
baseUrl,
|
|
3288
|
+
mintUrl,
|
|
3289
|
+
token,
|
|
3290
|
+
requiredSats,
|
|
3291
|
+
maxTokens,
|
|
3292
|
+
headers: requestHeaders,
|
|
3293
|
+
baseHeaders
|
|
3294
|
+
});
|
|
3295
|
+
if (!response.body) {
|
|
3296
|
+
throw new Error("Response body is not available");
|
|
3297
|
+
}
|
|
3298
|
+
if (response.status === 200) {
|
|
3299
|
+
const baseUrlUsed = response.baseUrl || baseUrl;
|
|
3300
|
+
const streamingResult = await this.streamProcessor.process(
|
|
3301
|
+
response,
|
|
3302
|
+
{
|
|
3303
|
+
onContent: callbacks.onStreamingUpdate,
|
|
3304
|
+
onThinking: callbacks.onThinkingUpdate
|
|
3305
|
+
},
|
|
3306
|
+
selectedModel.id
|
|
3307
|
+
);
|
|
3308
|
+
if (streamingResult.finish_reason === "content_filter") {
|
|
3309
|
+
callbacks.onMessageAppend({
|
|
3310
|
+
role: "assistant",
|
|
3311
|
+
content: "Your request was denied due to content filtering."
|
|
3312
|
+
});
|
|
3313
|
+
} else if (streamingResult.content || streamingResult.images && streamingResult.images.length > 0) {
|
|
3314
|
+
const message = await this._createAssistantMessage(streamingResult);
|
|
3315
|
+
callbacks.onMessageAppend(message);
|
|
3316
|
+
} else {
|
|
3317
|
+
callbacks.onMessageAppend({
|
|
3318
|
+
role: "system",
|
|
3319
|
+
content: "The provider did not respond to this request."
|
|
3320
|
+
});
|
|
3321
|
+
}
|
|
3322
|
+
callbacks.onStreamingUpdate("");
|
|
3323
|
+
callbacks.onThinkingUpdate("");
|
|
3324
|
+
const isApikeysEstimate = this.mode === "apikeys";
|
|
3325
|
+
let satsSpent = await this._handlePostResponseBalanceUpdate({
|
|
3326
|
+
token,
|
|
3327
|
+
baseUrl: baseUrlUsed,
|
|
3328
|
+
initialTokenBalance: tokenBalanceInSats,
|
|
3329
|
+
fallbackSatsSpent: isApikeysEstimate ? this._getEstimatedCosts(selectedModel, streamingResult) : void 0,
|
|
3330
|
+
response
|
|
3331
|
+
});
|
|
3332
|
+
const estimatedCosts = this._getEstimatedCosts(
|
|
3333
|
+
selectedModel,
|
|
3334
|
+
streamingResult
|
|
3335
|
+
);
|
|
3336
|
+
const onLastMessageSatsUpdate = callbacks.onLastMessageSatsUpdate;
|
|
3337
|
+
onLastMessageSatsUpdate?.(satsSpent, estimatedCosts);
|
|
3338
|
+
} else {
|
|
3339
|
+
throw new Error(`${response.status} ${response.statusText}`);
|
|
3340
|
+
}
|
|
3341
|
+
} catch (error) {
|
|
3342
|
+
this._handleError(error, callbacks);
|
|
3343
|
+
} finally {
|
|
3344
|
+
callbacks.onPaymentProcessing?.(false);
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
/**
|
|
3348
|
+
* Make the API request with failover support
|
|
3349
|
+
*/
|
|
3350
|
+
async _makeRequest(params) {
|
|
3351
|
+
const { path, method, body, baseUrl, token, headers } = params;
|
|
3352
|
+
try {
|
|
3353
|
+
const url = `${baseUrl.replace(/\/$/, "")}${path}`;
|
|
3354
|
+
const response = await fetch(url, {
|
|
3355
|
+
method,
|
|
3356
|
+
headers,
|
|
3357
|
+
body: body === void 0 || method === "GET" ? void 0 : JSON.stringify(body)
|
|
3358
|
+
});
|
|
3359
|
+
response.baseUrl = baseUrl;
|
|
3360
|
+
if (!response.ok) {
|
|
3361
|
+
return await this._handleErrorResponse(response, params, token);
|
|
3362
|
+
}
|
|
3363
|
+
return response;
|
|
3364
|
+
} catch (error) {
|
|
3365
|
+
if (this._isNetworkError(error?.message || "")) {
|
|
3366
|
+
return await this._handleNetworkError(error, params);
|
|
3367
|
+
}
|
|
3368
|
+
throw error;
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
/**
|
|
3372
|
+
* Handle error responses with failover
|
|
3373
|
+
*/
|
|
3374
|
+
async _handleErrorResponse(response, params, token) {
|
|
3375
|
+
const { path, method, body, selectedModel, baseUrl, mintUrl } = params;
|
|
3376
|
+
const status = response.status;
|
|
3377
|
+
if (this.mode === "apikeys") {
|
|
3378
|
+
console.log("error ;", status);
|
|
3379
|
+
if (status === 401 || status === 403) {
|
|
3380
|
+
const parentApiKey = this.storageAdapter.getApiKey(baseUrl);
|
|
3381
|
+
if (parentApiKey) {
|
|
3382
|
+
try {
|
|
3383
|
+
const childKeyResult = await this._createChildKey(
|
|
3384
|
+
baseUrl,
|
|
3385
|
+
parentApiKey
|
|
3386
|
+
);
|
|
3387
|
+
this.storageAdapter.setChildKey(
|
|
3388
|
+
baseUrl,
|
|
3389
|
+
childKeyResult.childKey,
|
|
3390
|
+
childKeyResult.balance,
|
|
3391
|
+
childKeyResult.validityDate,
|
|
3392
|
+
childKeyResult.balanceLimit
|
|
3393
|
+
);
|
|
3394
|
+
return this._makeRequest({
|
|
3395
|
+
...params,
|
|
3396
|
+
token: childKeyResult.childKey,
|
|
3397
|
+
headers: this._withAuthHeader(
|
|
3398
|
+
params.baseHeaders,
|
|
3399
|
+
childKeyResult.childKey
|
|
3400
|
+
)
|
|
3401
|
+
});
|
|
3402
|
+
} catch (e) {
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
} else if (status === 402) {
|
|
3406
|
+
const parentApiKey = this.storageAdapter.getApiKey(baseUrl);
|
|
3407
|
+
if (parentApiKey) {
|
|
3408
|
+
const topupResult = await this.balanceManager.topUp({
|
|
3409
|
+
mintUrl,
|
|
3410
|
+
baseUrl,
|
|
3411
|
+
amount: params.requiredSats * 3,
|
|
3412
|
+
token: parentApiKey
|
|
3413
|
+
});
|
|
3414
|
+
console.log(topupResult);
|
|
3415
|
+
return this._makeRequest({
|
|
3416
|
+
...params,
|
|
3417
|
+
token: params.token,
|
|
3418
|
+
headers: this._withAuthHeader(params.baseHeaders, params.token)
|
|
3419
|
+
});
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
throw new ProviderError(baseUrl, status, await response.text());
|
|
3423
|
+
}
|
|
3424
|
+
await this.balanceManager.refund({
|
|
3425
|
+
mintUrl,
|
|
3426
|
+
baseUrl,
|
|
3427
|
+
token
|
|
3428
|
+
});
|
|
3429
|
+
this.providerManager.markFailed(baseUrl);
|
|
3430
|
+
this.storageAdapter.removeToken(baseUrl);
|
|
3431
|
+
if (status === 401 || status === 403 || status === 402 || status === 413 || status === 400 || status === 500 || status === 502) {
|
|
3432
|
+
if (!selectedModel) {
|
|
3433
|
+
throw new ProviderError(baseUrl, status, await response.text());
|
|
3434
|
+
}
|
|
3435
|
+
const nextProvider = this.providerManager.findNextBestProvider(
|
|
3436
|
+
selectedModel.id,
|
|
3437
|
+
baseUrl
|
|
3438
|
+
);
|
|
3439
|
+
if (nextProvider) {
|
|
3440
|
+
const newModel = await this.providerManager.getModelForProvider(
|
|
3441
|
+
nextProvider,
|
|
3442
|
+
selectedModel.id
|
|
3443
|
+
) ?? selectedModel;
|
|
3444
|
+
const messagesForPricing = Array.isArray(
|
|
3445
|
+
body?.messages
|
|
3446
|
+
) ? body.messages : [];
|
|
3447
|
+
const newRequiredSats = this.providerManager.getRequiredSatsForModel(
|
|
3448
|
+
newModel,
|
|
3449
|
+
messagesForPricing,
|
|
3450
|
+
params.maxTokens
|
|
3451
|
+
);
|
|
3452
|
+
const spendResult = await this.cashuSpender.spend({
|
|
3453
|
+
mintUrl,
|
|
3454
|
+
amount: newRequiredSats,
|
|
3455
|
+
baseUrl: nextProvider,
|
|
3456
|
+
reuseToken: true
|
|
3457
|
+
});
|
|
3458
|
+
if (spendResult.status === "failed" || !spendResult.token) {
|
|
3459
|
+
if (spendResult.errorDetails) {
|
|
3460
|
+
throw new InsufficientBalanceError(
|
|
3461
|
+
spendResult.errorDetails.required,
|
|
3462
|
+
spendResult.errorDetails.available,
|
|
3463
|
+
spendResult.errorDetails.maxMintBalance,
|
|
3464
|
+
spendResult.errorDetails.maxMintUrl
|
|
3465
|
+
);
|
|
3466
|
+
}
|
|
3467
|
+
throw new Error(
|
|
3468
|
+
spendResult.error || `Insufficient balance for ${nextProvider}`
|
|
3469
|
+
);
|
|
3470
|
+
}
|
|
3471
|
+
return this._makeRequest({
|
|
3472
|
+
...params,
|
|
3473
|
+
path,
|
|
3474
|
+
method,
|
|
3475
|
+
body,
|
|
3476
|
+
baseUrl: nextProvider,
|
|
3477
|
+
selectedModel: newModel,
|
|
3478
|
+
token: spendResult.token,
|
|
3479
|
+
requiredSats: newRequiredSats,
|
|
3480
|
+
headers: this._withAuthHeader(params.baseHeaders, spendResult.token)
|
|
3481
|
+
});
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
throw new ProviderError(baseUrl, status, await response.text());
|
|
3485
|
+
}
|
|
3486
|
+
/**
|
|
3487
|
+
* Handle network errors with failover
|
|
3488
|
+
*/
|
|
3489
|
+
async _handleNetworkError(error, params) {
|
|
3490
|
+
const { path, method, body, selectedModel, baseUrl, mintUrl } = params;
|
|
3491
|
+
await this.balanceManager.refund({
|
|
3492
|
+
mintUrl,
|
|
3493
|
+
baseUrl,
|
|
3494
|
+
token: params.token
|
|
3495
|
+
});
|
|
3496
|
+
this.providerManager.markFailed(baseUrl);
|
|
3497
|
+
if (!selectedModel) {
|
|
3498
|
+
throw error;
|
|
3499
|
+
}
|
|
3500
|
+
const nextProvider = this.providerManager.findNextBestProvider(
|
|
3501
|
+
selectedModel.id,
|
|
3502
|
+
baseUrl
|
|
3503
|
+
);
|
|
3504
|
+
if (!nextProvider) {
|
|
3505
|
+
throw new FailoverError(baseUrl, Array.from(this.providerManager));
|
|
3506
|
+
}
|
|
3507
|
+
const newModel = await this.providerManager.getModelForProvider(
|
|
3508
|
+
nextProvider,
|
|
3509
|
+
selectedModel.id
|
|
3510
|
+
) ?? selectedModel;
|
|
3511
|
+
const messagesForPricing = Array.isArray(
|
|
3512
|
+
body?.messages
|
|
3513
|
+
) ? body.messages : [];
|
|
3514
|
+
const newRequiredSats = this.providerManager.getRequiredSatsForModel(
|
|
3515
|
+
newModel,
|
|
3516
|
+
messagesForPricing,
|
|
3517
|
+
params.maxTokens
|
|
3518
|
+
);
|
|
3519
|
+
const spendResult = await this.cashuSpender.spend({
|
|
3520
|
+
mintUrl,
|
|
3521
|
+
amount: newRequiredSats,
|
|
3522
|
+
baseUrl: nextProvider,
|
|
3523
|
+
reuseToken: true
|
|
3524
|
+
});
|
|
3525
|
+
if (spendResult.status === "failed" || !spendResult.token) {
|
|
3526
|
+
if (spendResult.errorDetails) {
|
|
3527
|
+
throw new InsufficientBalanceError(
|
|
3528
|
+
spendResult.errorDetails.required,
|
|
3529
|
+
spendResult.errorDetails.available,
|
|
3530
|
+
spendResult.errorDetails.maxMintBalance,
|
|
3531
|
+
spendResult.errorDetails.maxMintUrl
|
|
3532
|
+
);
|
|
3533
|
+
}
|
|
3534
|
+
throw new Error(
|
|
3535
|
+
spendResult.error || `Insufficient balance for ${nextProvider}`
|
|
3536
|
+
);
|
|
3537
|
+
}
|
|
3538
|
+
return this._makeRequest({
|
|
3539
|
+
...params,
|
|
3540
|
+
path,
|
|
3541
|
+
method,
|
|
3542
|
+
body,
|
|
3543
|
+
baseUrl: nextProvider,
|
|
3544
|
+
selectedModel: newModel,
|
|
3545
|
+
token: spendResult.token,
|
|
3546
|
+
requiredSats: newRequiredSats,
|
|
3547
|
+
headers: this._withAuthHeader(params.baseHeaders, spendResult.token)
|
|
3548
|
+
});
|
|
3549
|
+
}
|
|
3550
|
+
/**
|
|
3551
|
+
* Handle post-response refund and balance updates
|
|
3552
|
+
*/
|
|
3553
|
+
async _handlePostResponseRefund(params) {
|
|
3554
|
+
const {
|
|
3555
|
+
mintUrl,
|
|
3556
|
+
baseUrl,
|
|
3557
|
+
tokenBalance,
|
|
3558
|
+
tokenBalanceUnit,
|
|
3559
|
+
initialBalance,
|
|
3560
|
+
selectedModel,
|
|
3561
|
+
streamingResult,
|
|
3562
|
+
callbacks
|
|
3563
|
+
} = params;
|
|
3564
|
+
const tokenBalanceInSats = tokenBalanceUnit === "msat" ? tokenBalance / 1e3 : tokenBalance;
|
|
3565
|
+
const estimatedCosts = this._getEstimatedCosts(
|
|
3566
|
+
selectedModel,
|
|
3567
|
+
streamingResult
|
|
3568
|
+
);
|
|
3569
|
+
const refundResult = await this.balanceManager.refund({
|
|
3570
|
+
mintUrl,
|
|
3571
|
+
baseUrl
|
|
3572
|
+
});
|
|
3573
|
+
if (refundResult.success) {
|
|
3574
|
+
refundResult.refundedAmount !== void 0 ? refundResult.refundedAmount / 1e3 : 0;
|
|
3575
|
+
}
|
|
3576
|
+
let satsSpent;
|
|
3577
|
+
if (refundResult.success) {
|
|
3578
|
+
if (refundResult.refundedAmount !== void 0) {
|
|
3579
|
+
satsSpent = tokenBalanceInSats - refundResult.refundedAmount / 1e3;
|
|
3580
|
+
} else if (refundResult.message?.includes("No API key to refund")) {
|
|
3581
|
+
satsSpent = 0;
|
|
3582
|
+
} else {
|
|
3583
|
+
satsSpent = tokenBalanceInSats;
|
|
3584
|
+
}
|
|
3585
|
+
const newBalance = initialBalance - satsSpent;
|
|
3586
|
+
callbacks.onBalanceUpdate(newBalance);
|
|
3587
|
+
} else {
|
|
3588
|
+
if (refundResult.message?.includes("Refund request failed with status 401")) {
|
|
3589
|
+
this.storageAdapter.removeToken(baseUrl);
|
|
3590
|
+
}
|
|
3591
|
+
satsSpent = tokenBalanceInSats;
|
|
3592
|
+
}
|
|
3593
|
+
const netCosts = satsSpent - estimatedCosts;
|
|
3594
|
+
const overchargeThreshold = tokenBalanceUnit === "msat" ? 0.05 : 1;
|
|
3595
|
+
if (netCosts > overchargeThreshold) {
|
|
3596
|
+
if (this.alertLevel === "max") {
|
|
3597
|
+
callbacks.onMessageAppend({
|
|
3598
|
+
role: "system",
|
|
3599
|
+
content: `ATTENTION: Provider may be overcharging. Estimated: ${estimatedCosts.toFixed(
|
|
3600
|
+
tokenBalanceUnit === "msat" ? 3 : 0
|
|
3601
|
+
)}, Actual: ${satsSpent.toFixed(
|
|
3602
|
+
tokenBalanceUnit === "msat" ? 3 : 0
|
|
3603
|
+
)}`
|
|
3604
|
+
});
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
const newTransaction = {
|
|
3608
|
+
type: "spent",
|
|
3609
|
+
amount: satsSpent,
|
|
3610
|
+
timestamp: Date.now(),
|
|
3611
|
+
status: "success",
|
|
3612
|
+
model: selectedModel.id,
|
|
3613
|
+
message: "Tokens spent",
|
|
3614
|
+
balance: initialBalance - satsSpent
|
|
3615
|
+
};
|
|
3616
|
+
callbacks.onTransactionUpdate(newTransaction);
|
|
3617
|
+
return satsSpent;
|
|
3618
|
+
}
|
|
3619
|
+
/**
|
|
3620
|
+
* Handle post-response balance update for all modes
|
|
3621
|
+
*/
|
|
3622
|
+
async _handlePostResponseBalanceUpdate(params) {
|
|
3623
|
+
const { token, baseUrl, initialTokenBalance, fallbackSatsSpent, response } = params;
|
|
3624
|
+
let satsSpent = initialTokenBalance;
|
|
3625
|
+
if (this.mode === "xcashu" && response) {
|
|
3626
|
+
const refundToken = response.headers.get("x-cashu") ?? void 0;
|
|
3627
|
+
if (refundToken) {
|
|
3628
|
+
try {
|
|
3629
|
+
const receiveResult = await this.walletAdapter.receiveToken(refundToken);
|
|
3630
|
+
satsSpent = initialTokenBalance - receiveResult.amount * (receiveResult.unit == "sat" ? 1 : 1e3);
|
|
3631
|
+
} catch (error) {
|
|
3632
|
+
console.error("[xcashu] Failed to receive refund token:", error);
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
} else if (this.mode === "lazyrefund") {
|
|
3636
|
+
const latestBalanceInfo = await this.balanceManager.getTokenBalance(
|
|
3637
|
+
token,
|
|
3638
|
+
baseUrl
|
|
3639
|
+
);
|
|
3640
|
+
const latestTokenBalance = latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
|
|
3641
|
+
this.storageAdapter.updateTokenBalance(baseUrl, latestTokenBalance);
|
|
3642
|
+
satsSpent = initialTokenBalance - latestTokenBalance;
|
|
3643
|
+
} else if (this.mode === "apikeys") {
|
|
3644
|
+
try {
|
|
3645
|
+
const latestBalanceInfo = await this._getApiKeyBalance(baseUrl, token);
|
|
3646
|
+
console.log("LATEST BANAL", latestBalanceInfo);
|
|
3647
|
+
const latestTokenBalance = latestBalanceInfo.unit === "msat" ? latestBalanceInfo.amount / 1e3 : latestBalanceInfo.amount;
|
|
3648
|
+
this.storageAdapter.updateChildKeyBalance(baseUrl, latestTokenBalance);
|
|
3649
|
+
satsSpent = initialTokenBalance - latestTokenBalance;
|
|
3650
|
+
} catch (e) {
|
|
3651
|
+
console.warn("Could not get updated API key balance:", e);
|
|
3652
|
+
satsSpent = fallbackSatsSpent ?? initialTokenBalance;
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
return satsSpent;
|
|
3656
|
+
}
|
|
3657
|
+
/**
|
|
3658
|
+
* Convert messages for API format
|
|
3659
|
+
*/
|
|
3660
|
+
async _convertMessages(messages) {
|
|
3661
|
+
return Promise.all(
|
|
3662
|
+
messages.filter((m) => m.role !== "system").map(async (m) => ({
|
|
3663
|
+
role: m.role,
|
|
3664
|
+
content: typeof m.content === "string" ? m.content : m.content
|
|
3665
|
+
}))
|
|
3666
|
+
);
|
|
3667
|
+
}
|
|
3668
|
+
/**
|
|
3669
|
+
* Create assistant message from streaming result
|
|
3670
|
+
*/
|
|
3671
|
+
async _createAssistantMessage(result) {
|
|
3672
|
+
if (result.images && result.images.length > 0) {
|
|
3673
|
+
const content = [];
|
|
3674
|
+
if (result.content) {
|
|
3675
|
+
content.push({
|
|
3676
|
+
type: "text",
|
|
3677
|
+
text: result.content,
|
|
3678
|
+
thinking: result.thinking,
|
|
3679
|
+
citations: result.citations,
|
|
3680
|
+
annotations: result.annotations
|
|
3681
|
+
});
|
|
3682
|
+
}
|
|
3683
|
+
for (const img of result.images) {
|
|
3684
|
+
content.push({
|
|
3685
|
+
type: "image_url",
|
|
3686
|
+
image_url: {
|
|
3687
|
+
url: img.image_url.url
|
|
3688
|
+
}
|
|
3689
|
+
});
|
|
3690
|
+
}
|
|
3691
|
+
return {
|
|
3692
|
+
role: "assistant",
|
|
3693
|
+
content
|
|
3694
|
+
};
|
|
3695
|
+
}
|
|
3696
|
+
return {
|
|
3697
|
+
role: "assistant",
|
|
3698
|
+
content: result.content || ""
|
|
3699
|
+
};
|
|
3700
|
+
}
|
|
3701
|
+
/**
|
|
3702
|
+
* Create a child key for a parent API key via the provider's API
|
|
3703
|
+
* POST /v1/balance/child-key
|
|
3704
|
+
*/
|
|
3705
|
+
async _createChildKey(baseUrl, parentApiKey, options) {
|
|
3706
|
+
const response = await fetch(`${baseUrl}v1/balance/child-key`, {
|
|
3707
|
+
method: "POST",
|
|
3708
|
+
headers: {
|
|
3709
|
+
"Content-Type": "application/json",
|
|
3710
|
+
Authorization: `Bearer ${parentApiKey}`
|
|
3711
|
+
},
|
|
3712
|
+
body: JSON.stringify({
|
|
3713
|
+
count: options?.count ?? 1,
|
|
3714
|
+
balance_limit: options?.balanceLimit,
|
|
3715
|
+
balance_limit_reset: options?.balanceLimitReset,
|
|
3716
|
+
validity_date: options?.validityDate
|
|
3717
|
+
})
|
|
3718
|
+
});
|
|
3719
|
+
if (!response.ok) {
|
|
3720
|
+
throw new Error(
|
|
3721
|
+
`Failed to create child key: ${response.status} ${await response.text()}`
|
|
3722
|
+
);
|
|
3723
|
+
}
|
|
3724
|
+
const data = await response.json();
|
|
3725
|
+
return {
|
|
3726
|
+
childKey: data.api_keys?.[0],
|
|
3727
|
+
balance: data.balance ?? 0,
|
|
3728
|
+
balanceLimit: data.balance_limit,
|
|
3729
|
+
validityDate: data.validity_date
|
|
3730
|
+
};
|
|
3731
|
+
}
|
|
3732
|
+
/**
|
|
3733
|
+
* Get balance for an API key from the provider
|
|
3734
|
+
*/
|
|
3735
|
+
async _getApiKeyBalance(baseUrl, apiKey) {
|
|
3736
|
+
try {
|
|
3737
|
+
const response = await fetch(`${baseUrl}v1/wallet/info`, {
|
|
3738
|
+
headers: {
|
|
3739
|
+
Authorization: `Bearer ${apiKey}`
|
|
3740
|
+
}
|
|
3741
|
+
});
|
|
3742
|
+
if (response.ok) {
|
|
3743
|
+
const data = await response.json();
|
|
3744
|
+
console.log(data);
|
|
3745
|
+
return {
|
|
3746
|
+
amount: data.balance,
|
|
3747
|
+
unit: "msat"
|
|
3748
|
+
};
|
|
3749
|
+
}
|
|
3750
|
+
} catch {
|
|
3751
|
+
}
|
|
3752
|
+
return { amount: 0, unit: "sat" };
|
|
3753
|
+
}
|
|
3754
|
+
/**
|
|
3755
|
+
* Calculate estimated costs from usage
|
|
3756
|
+
*/
|
|
3757
|
+
_getEstimatedCosts(selectedModel, streamingResult) {
|
|
3758
|
+
let estimatedCosts = 0;
|
|
3759
|
+
if (streamingResult.usage) {
|
|
3760
|
+
const { completion_tokens, prompt_tokens } = streamingResult.usage;
|
|
3761
|
+
if (completion_tokens !== void 0 && prompt_tokens !== void 0) {
|
|
3762
|
+
estimatedCosts = (selectedModel.sats_pricing?.completion ?? 0) * completion_tokens + (selectedModel.sats_pricing?.prompt ?? 0) * prompt_tokens;
|
|
3763
|
+
}
|
|
3764
|
+
}
|
|
3765
|
+
return estimatedCosts;
|
|
3766
|
+
}
|
|
3767
|
+
/**
|
|
3768
|
+
* Get pending cashu token amount
|
|
3769
|
+
*/
|
|
3770
|
+
_getPendingCashuTokenAmount() {
|
|
3771
|
+
const distribution = this.storageAdapter.getPendingTokenDistribution();
|
|
3772
|
+
return distribution.reduce((total, item) => total + item.amount, 0);
|
|
3773
|
+
}
|
|
3774
|
+
/**
|
|
3775
|
+
* Check if error message indicates a network error
|
|
3776
|
+
*/
|
|
3777
|
+
_isNetworkError(message) {
|
|
3778
|
+
return message.includes("NetworkError when attempting to fetch resource") || message.includes("Failed to fetch") || message.includes("Load failed");
|
|
3779
|
+
}
|
|
3780
|
+
/**
|
|
3781
|
+
* Handle errors and notify callbacks
|
|
3782
|
+
*/
|
|
3783
|
+
_handleError(error, callbacks) {
|
|
3784
|
+
console.error("RoutstrClient error:", error);
|
|
3785
|
+
if (error instanceof Error) {
|
|
3786
|
+
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;
|
|
3787
|
+
callbacks.onMessageAppend({
|
|
3788
|
+
role: "system",
|
|
3789
|
+
content: "Uncaught Error: " + modifiedErrorMsg + (this.alertLevel === "max" ? " | " + error.stack : "")
|
|
3790
|
+
});
|
|
3791
|
+
} else {
|
|
3792
|
+
callbacks.onMessageAppend({
|
|
3793
|
+
role: "system",
|
|
3794
|
+
content: "Unknown Error: Please tag Routstr on Nostr and/or retry."
|
|
3795
|
+
});
|
|
3796
|
+
}
|
|
3797
|
+
}
|
|
3798
|
+
/**
|
|
3799
|
+
* Check wallet balance and throw if insufficient
|
|
3800
|
+
*/
|
|
3801
|
+
async _checkBalance() {
|
|
3802
|
+
const balances = await this.walletAdapter.getBalances();
|
|
3803
|
+
const totalBalance = Object.values(balances).reduce((sum, v) => sum + v, 0);
|
|
3804
|
+
if (totalBalance <= 0) {
|
|
3805
|
+
throw new InsufficientBalanceError(1, 0);
|
|
3806
|
+
}
|
|
3807
|
+
}
|
|
3808
|
+
/**
|
|
3809
|
+
* Spend a token using CashuSpender with standardized error handling
|
|
3810
|
+
*/
|
|
3811
|
+
async _spendToken(params) {
|
|
3812
|
+
const { mintUrl, amount, baseUrl } = params;
|
|
3813
|
+
if (this.mode === "apikeys") {
|
|
3814
|
+
let parentApiKey = this.storageAdapter.getApiKey(baseUrl);
|
|
3815
|
+
if (!parentApiKey) {
|
|
3816
|
+
const spendResult2 = await this.cashuSpender.spend({
|
|
3817
|
+
mintUrl,
|
|
3818
|
+
amount: amount * 3,
|
|
3819
|
+
baseUrl: "",
|
|
3820
|
+
reuseToken: false
|
|
3821
|
+
});
|
|
3822
|
+
if (spendResult2.status === "failed" || !spendResult2.token) {
|
|
3823
|
+
const errorMsg = spendResult2.error || `Insufficient balance. Need ${amount} sats.`;
|
|
3824
|
+
if (this._isNetworkError(errorMsg)) {
|
|
3825
|
+
throw new Error(
|
|
3826
|
+
`Your mint ${mintUrl} is unreachable or is blocking your IP. Please try again later or switch mints.`
|
|
3827
|
+
);
|
|
3828
|
+
}
|
|
3829
|
+
if (spendResult2.errorDetails) {
|
|
3830
|
+
throw new InsufficientBalanceError(
|
|
3831
|
+
spendResult2.errorDetails.required,
|
|
3832
|
+
spendResult2.errorDetails.available,
|
|
3833
|
+
spendResult2.errorDetails.maxMintBalance,
|
|
3834
|
+
spendResult2.errorDetails.maxMintUrl
|
|
3835
|
+
);
|
|
3836
|
+
}
|
|
3837
|
+
throw new Error(errorMsg);
|
|
3838
|
+
}
|
|
3839
|
+
const apiKeyCreated = await this.balanceManager.getTokenBalance(
|
|
3840
|
+
spendResult2.token,
|
|
3841
|
+
baseUrl
|
|
3842
|
+
);
|
|
3843
|
+
parentApiKey = apiKeyCreated.apiKey;
|
|
3844
|
+
this.storageAdapter.setApiKey(baseUrl, parentApiKey);
|
|
3845
|
+
}
|
|
3846
|
+
let childKeyEntry = this.storageAdapter.getChildKey(baseUrl);
|
|
3847
|
+
if (!childKeyEntry) {
|
|
3848
|
+
try {
|
|
3849
|
+
const childKeyResult = await this._createChildKey(
|
|
3850
|
+
baseUrl,
|
|
3851
|
+
parentApiKey
|
|
3852
|
+
);
|
|
3853
|
+
this.storageAdapter.setChildKey(
|
|
3854
|
+
baseUrl,
|
|
3855
|
+
childKeyResult.childKey,
|
|
3856
|
+
childKeyResult.balance,
|
|
3857
|
+
childKeyResult.validityDate,
|
|
3858
|
+
childKeyResult.balanceLimit
|
|
3859
|
+
);
|
|
3860
|
+
childKeyEntry = {
|
|
3861
|
+
parentBaseUrl: baseUrl,
|
|
3862
|
+
childKey: childKeyResult.childKey,
|
|
3863
|
+
balance: childKeyResult.balance,
|
|
3864
|
+
balanceLimit: childKeyResult.balanceLimit,
|
|
3865
|
+
validityDate: childKeyResult.validityDate,
|
|
3866
|
+
createdAt: Date.now()
|
|
3867
|
+
};
|
|
3868
|
+
} catch (e) {
|
|
3869
|
+
console.warn("Could not create child key, using parent key:", e);
|
|
3870
|
+
childKeyEntry = {
|
|
3871
|
+
parentBaseUrl: baseUrl,
|
|
3872
|
+
childKey: parentApiKey,
|
|
3873
|
+
balance: 0,
|
|
3874
|
+
createdAt: Date.now()
|
|
3875
|
+
};
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
let tokenBalance = childKeyEntry.balance;
|
|
3879
|
+
let tokenBalanceUnit = "sat";
|
|
3880
|
+
if (tokenBalance === 0) {
|
|
3881
|
+
try {
|
|
3882
|
+
const balanceInfo = await this._getApiKeyBalance(
|
|
3883
|
+
baseUrl,
|
|
3884
|
+
childKeyEntry.childKey
|
|
3885
|
+
);
|
|
3886
|
+
tokenBalance = balanceInfo.amount;
|
|
3887
|
+
tokenBalanceUnit = balanceInfo.unit;
|
|
3888
|
+
} catch (e) {
|
|
3889
|
+
console.warn("Could not get initial API key balance:", e);
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
return {
|
|
3893
|
+
token: childKeyEntry.childKey,
|
|
3894
|
+
tokenBalance,
|
|
3895
|
+
tokenBalanceUnit
|
|
3896
|
+
};
|
|
3897
|
+
}
|
|
3898
|
+
const spendResult = await this.cashuSpender.spend({
|
|
3899
|
+
mintUrl,
|
|
3900
|
+
amount,
|
|
3901
|
+
baseUrl,
|
|
3902
|
+
reuseToken: true
|
|
3903
|
+
});
|
|
3904
|
+
if (spendResult.status === "failed" || !spendResult.token) {
|
|
3905
|
+
const errorMsg = spendResult.error || `Insufficient balance. Need ${amount} sats.`;
|
|
3906
|
+
if (this._isNetworkError(errorMsg)) {
|
|
3907
|
+
throw new Error(
|
|
3908
|
+
`Your mint ${mintUrl} is unreachable or is blocking your IP. Please try again later or switch mints.`
|
|
3909
|
+
);
|
|
3910
|
+
}
|
|
3911
|
+
if (spendResult.errorDetails) {
|
|
3912
|
+
throw new InsufficientBalanceError(
|
|
3913
|
+
spendResult.errorDetails.required,
|
|
3914
|
+
spendResult.errorDetails.available,
|
|
3915
|
+
spendResult.errorDetails.maxMintBalance,
|
|
3916
|
+
spendResult.errorDetails.maxMintUrl
|
|
3917
|
+
);
|
|
3918
|
+
}
|
|
3919
|
+
throw new Error(errorMsg);
|
|
3920
|
+
}
|
|
3921
|
+
return {
|
|
3922
|
+
token: spendResult.token,
|
|
3923
|
+
tokenBalance: spendResult.balance,
|
|
3924
|
+
tokenBalanceUnit: spendResult.unit ?? "sat"
|
|
3925
|
+
};
|
|
3926
|
+
}
|
|
3927
|
+
/**
|
|
3928
|
+
* Build request headers with common defaults and dev mock controls
|
|
3929
|
+
*/
|
|
3930
|
+
_buildBaseHeaders(additionalHeaders = {}, token) {
|
|
3931
|
+
const headers = {
|
|
3932
|
+
...additionalHeaders,
|
|
3933
|
+
"Content-Type": "application/json"
|
|
3934
|
+
};
|
|
3935
|
+
return headers;
|
|
3936
|
+
}
|
|
3937
|
+
/**
|
|
3938
|
+
* Attach auth headers using the active client mode
|
|
3939
|
+
*/
|
|
3940
|
+
_withAuthHeader(headers, token) {
|
|
3941
|
+
const nextHeaders = { ...headers };
|
|
3942
|
+
if (this.mode === "xcashu") {
|
|
3943
|
+
nextHeaders["X-Cashu"] = token;
|
|
3944
|
+
} else {
|
|
3945
|
+
nextHeaders["Authorization"] = `Bearer ${token}`;
|
|
3946
|
+
}
|
|
3947
|
+
return nextHeaders;
|
|
3948
|
+
}
|
|
3949
|
+
};
|
|
3950
|
+
|
|
3951
|
+
// storage/drivers/localStorage.ts
|
|
3952
|
+
var canUseLocalStorage = () => {
|
|
3953
|
+
return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
|
|
3954
|
+
};
|
|
3955
|
+
var isQuotaExceeded = (error) => {
|
|
3956
|
+
const e = error;
|
|
3957
|
+
return !!e && (e?.name === "QuotaExceededError" || e?.code === 22 || e?.code === 1014);
|
|
3958
|
+
};
|
|
3959
|
+
var NON_CRITICAL_KEYS = /* @__PURE__ */ new Set(["modelsFromAllProviders"]);
|
|
3960
|
+
var localStorageDriver$1 = {
|
|
3961
|
+
getItem(key, defaultValue) {
|
|
3962
|
+
if (!canUseLocalStorage()) return defaultValue;
|
|
3963
|
+
try {
|
|
3964
|
+
const item = window.localStorage.getItem(key);
|
|
3965
|
+
if (item === null) return defaultValue;
|
|
3966
|
+
try {
|
|
3967
|
+
return JSON.parse(item);
|
|
3968
|
+
} catch (parseError) {
|
|
3969
|
+
if (typeof defaultValue === "string") {
|
|
3970
|
+
return item;
|
|
3971
|
+
}
|
|
3972
|
+
throw parseError;
|
|
3973
|
+
}
|
|
3974
|
+
} catch (error) {
|
|
3975
|
+
console.error(`Error retrieving item with key "${key}":`, error);
|
|
3976
|
+
if (canUseLocalStorage()) {
|
|
3977
|
+
try {
|
|
3978
|
+
window.localStorage.removeItem(key);
|
|
3979
|
+
} catch (removeError) {
|
|
3980
|
+
console.error(
|
|
3981
|
+
`Error removing corrupted item with key "${key}":`,
|
|
3982
|
+
removeError
|
|
3983
|
+
);
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
3986
|
+
return defaultValue;
|
|
3987
|
+
}
|
|
3988
|
+
},
|
|
3989
|
+
setItem(key, value) {
|
|
3990
|
+
if (!canUseLocalStorage()) return;
|
|
3991
|
+
try {
|
|
3992
|
+
window.localStorage.setItem(key, JSON.stringify(value));
|
|
3993
|
+
} catch (error) {
|
|
3994
|
+
if (isQuotaExceeded(error)) {
|
|
3995
|
+
if (NON_CRITICAL_KEYS.has(key)) {
|
|
3996
|
+
console.warn(
|
|
3997
|
+
`Storage quota exceeded; skipping non-critical key "${key}".`
|
|
3998
|
+
);
|
|
3999
|
+
return;
|
|
4000
|
+
}
|
|
4001
|
+
try {
|
|
4002
|
+
window.localStorage.removeItem("modelsFromAllProviders");
|
|
4003
|
+
} catch {
|
|
4004
|
+
}
|
|
4005
|
+
try {
|
|
4006
|
+
window.localStorage.setItem(key, JSON.stringify(value));
|
|
4007
|
+
return;
|
|
4008
|
+
} catch (retryError) {
|
|
4009
|
+
console.warn(
|
|
4010
|
+
`Storage quota exceeded; unable to persist key "${key}" after cleanup attempt.`,
|
|
4011
|
+
retryError
|
|
4012
|
+
);
|
|
4013
|
+
return;
|
|
4014
|
+
}
|
|
4015
|
+
}
|
|
4016
|
+
console.error(`Error storing item with key "${key}":`, error);
|
|
4017
|
+
}
|
|
4018
|
+
},
|
|
4019
|
+
removeItem(key) {
|
|
4020
|
+
if (!canUseLocalStorage()) return;
|
|
4021
|
+
try {
|
|
4022
|
+
window.localStorage.removeItem(key);
|
|
4023
|
+
} catch (error) {
|
|
4024
|
+
console.error(`Error removing item with key "${key}":`, error);
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
4027
|
+
};
|
|
4028
|
+
|
|
4029
|
+
// storage/drivers/memory.ts
|
|
4030
|
+
var createMemoryDriver$1 = (seed) => {
|
|
4031
|
+
const store = /* @__PURE__ */ new Map();
|
|
4032
|
+
if (seed) {
|
|
4033
|
+
for (const [key, value] of Object.entries(seed)) {
|
|
4034
|
+
store.set(key, value);
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
return {
|
|
4038
|
+
getItem(key, defaultValue) {
|
|
4039
|
+
const item = store.get(key);
|
|
4040
|
+
if (item === void 0) return defaultValue;
|
|
4041
|
+
try {
|
|
4042
|
+
return JSON.parse(item);
|
|
4043
|
+
} catch (parseError) {
|
|
4044
|
+
if (typeof defaultValue === "string") {
|
|
4045
|
+
return item;
|
|
4046
|
+
}
|
|
4047
|
+
throw parseError;
|
|
4048
|
+
}
|
|
4049
|
+
},
|
|
4050
|
+
setItem(key, value) {
|
|
4051
|
+
store.set(key, JSON.stringify(value));
|
|
4052
|
+
},
|
|
4053
|
+
removeItem(key) {
|
|
4054
|
+
store.delete(key);
|
|
4055
|
+
}
|
|
4056
|
+
};
|
|
4057
|
+
};
|
|
4058
|
+
|
|
4059
|
+
// storage/drivers/sqlite.ts
|
|
4060
|
+
var createDatabase = (dbPath) => {
|
|
4061
|
+
let Database = null;
|
|
4062
|
+
try {
|
|
4063
|
+
Database = __require("better-sqlite3");
|
|
4064
|
+
} catch (error) {
|
|
4065
|
+
throw new Error(
|
|
4066
|
+
`better-sqlite3 is required for sqlite storage. Install it to use sqlite storage. (${error})`
|
|
4067
|
+
);
|
|
4068
|
+
}
|
|
4069
|
+
return new Database(dbPath);
|
|
4070
|
+
};
|
|
4071
|
+
var createSqliteDriver$1 = (options = {}) => {
|
|
4072
|
+
const dbPath = options.dbPath || "routstr.sqlite";
|
|
4073
|
+
const tableName = options.tableName || "sdk_storage";
|
|
4074
|
+
const db = createDatabase(dbPath);
|
|
4075
|
+
db.exec(
|
|
4076
|
+
`CREATE TABLE IF NOT EXISTS ${tableName} (key TEXT PRIMARY KEY, value TEXT NOT NULL)`
|
|
4077
|
+
);
|
|
4078
|
+
const selectStmt = db.prepare(`SELECT value FROM ${tableName} WHERE key = ?`);
|
|
4079
|
+
const upsertStmt = db.prepare(
|
|
4080
|
+
`INSERT INTO ${tableName} (key, value) VALUES (?, ?)
|
|
4081
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
4082
|
+
);
|
|
4083
|
+
const deleteStmt = db.prepare(`DELETE FROM ${tableName} WHERE key = ?`);
|
|
4084
|
+
return {
|
|
4085
|
+
getItem(key, defaultValue) {
|
|
4086
|
+
try {
|
|
4087
|
+
const row = selectStmt.get(key);
|
|
4088
|
+
if (!row || typeof row.value !== "string") return defaultValue;
|
|
4089
|
+
try {
|
|
4090
|
+
return JSON.parse(row.value);
|
|
4091
|
+
} catch (parseError) {
|
|
4092
|
+
if (typeof defaultValue === "string") {
|
|
4093
|
+
return row.value;
|
|
4094
|
+
}
|
|
4095
|
+
throw parseError;
|
|
4096
|
+
}
|
|
4097
|
+
} catch (error) {
|
|
4098
|
+
console.error(`SQLite getItem failed for key "${key}":`, error);
|
|
4099
|
+
return defaultValue;
|
|
4100
|
+
}
|
|
4101
|
+
},
|
|
4102
|
+
setItem(key, value) {
|
|
4103
|
+
try {
|
|
4104
|
+
upsertStmt.run(key, JSON.stringify(value));
|
|
4105
|
+
} catch (error) {
|
|
4106
|
+
console.error(`SQLite setItem failed for key "${key}":`, error);
|
|
4107
|
+
}
|
|
4108
|
+
},
|
|
4109
|
+
removeItem(key) {
|
|
4110
|
+
try {
|
|
4111
|
+
deleteStmt.run(key);
|
|
4112
|
+
} catch (error) {
|
|
4113
|
+
console.error(`SQLite removeItem failed for key "${key}":`, error);
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
4116
|
+
};
|
|
4117
|
+
};
|
|
4118
|
+
|
|
4119
|
+
// storage/keys.ts
|
|
4120
|
+
var SDK_STORAGE_KEYS$1 = {
|
|
4121
|
+
MODELS_FROM_ALL_PROVIDERS: "modelsFromAllProviders",
|
|
4122
|
+
LAST_USED_MODEL: "lastUsedModel",
|
|
4123
|
+
BASE_URLS_LIST: "base_urls_list",
|
|
4124
|
+
DISABLED_PROVIDERS: "disabled_providers",
|
|
4125
|
+
MINTS_FROM_ALL_PROVIDERS: "mints_from_all_providers",
|
|
4126
|
+
INFO_FROM_ALL_PROVIDERS: "info_from_all_providers",
|
|
4127
|
+
LAST_MODELS_UPDATE: "lastModelsUpdate",
|
|
4128
|
+
LAST_BASE_URLS_UPDATE: "lastBaseUrlsUpdate",
|
|
4129
|
+
LOCAL_CASHU_TOKENS: "local_cashu_tokens",
|
|
4130
|
+
API_KEYS: "api_keys",
|
|
4131
|
+
CHILD_KEYS: "child_keys"
|
|
4132
|
+
};
|
|
4133
|
+
|
|
4134
|
+
// storage/store.ts
|
|
4135
|
+
var normalizeBaseUrl = (baseUrl) => baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
4136
|
+
var getTokenBalance = (token) => {
|
|
4137
|
+
try {
|
|
4138
|
+
const decoded = cashuTs.getDecodedToken(token);
|
|
4139
|
+
const unitDivisor = decoded.unit === "msat" ? 1e3 : 1;
|
|
4140
|
+
let sum = 0;
|
|
4141
|
+
for (const proof of decoded.proofs) {
|
|
4142
|
+
sum += proof.amount / unitDivisor;
|
|
4143
|
+
}
|
|
4144
|
+
return sum;
|
|
4145
|
+
} catch {
|
|
4146
|
+
return 0;
|
|
4147
|
+
}
|
|
4148
|
+
};
|
|
4149
|
+
var createSdkStore$1 = ({ driver }) => {
|
|
4150
|
+
return vanilla.createStore((set, get) => ({
|
|
4151
|
+
modelsFromAllProviders: Object.fromEntries(
|
|
4152
|
+
Object.entries(
|
|
4153
|
+
driver.getItem(
|
|
4154
|
+
SDK_STORAGE_KEYS$1.MODELS_FROM_ALL_PROVIDERS,
|
|
4155
|
+
{}
|
|
4156
|
+
)
|
|
4157
|
+
).map(([baseUrl, models]) => [normalizeBaseUrl(baseUrl), models])
|
|
4158
|
+
),
|
|
4159
|
+
lastUsedModel: driver.getItem(
|
|
4160
|
+
SDK_STORAGE_KEYS$1.LAST_USED_MODEL,
|
|
4161
|
+
null
|
|
4162
|
+
),
|
|
4163
|
+
baseUrlsList: driver.getItem(SDK_STORAGE_KEYS$1.BASE_URLS_LIST, []).map((url) => normalizeBaseUrl(url)),
|
|
4164
|
+
lastBaseUrlsUpdate: driver.getItem(
|
|
4165
|
+
SDK_STORAGE_KEYS$1.LAST_BASE_URLS_UPDATE,
|
|
4166
|
+
null
|
|
4167
|
+
),
|
|
4168
|
+
disabledProviders: driver.getItem(SDK_STORAGE_KEYS$1.DISABLED_PROVIDERS, []).map((url) => normalizeBaseUrl(url)),
|
|
4169
|
+
mintsFromAllProviders: Object.fromEntries(
|
|
4170
|
+
Object.entries(
|
|
4171
|
+
driver.getItem(
|
|
4172
|
+
SDK_STORAGE_KEYS$1.MINTS_FROM_ALL_PROVIDERS,
|
|
4173
|
+
{}
|
|
4174
|
+
)
|
|
4175
|
+
).map(([baseUrl, mints]) => [
|
|
4176
|
+
normalizeBaseUrl(baseUrl),
|
|
4177
|
+
mints.map((mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint)
|
|
4178
|
+
])
|
|
4179
|
+
),
|
|
4180
|
+
infoFromAllProviders: Object.fromEntries(
|
|
4181
|
+
Object.entries(
|
|
4182
|
+
driver.getItem(
|
|
4183
|
+
SDK_STORAGE_KEYS$1.INFO_FROM_ALL_PROVIDERS,
|
|
4184
|
+
{}
|
|
4185
|
+
)
|
|
4186
|
+
).map(([baseUrl, info]) => [normalizeBaseUrl(baseUrl), info])
|
|
4187
|
+
),
|
|
4188
|
+
lastModelsUpdate: Object.fromEntries(
|
|
4189
|
+
Object.entries(
|
|
4190
|
+
driver.getItem(
|
|
4191
|
+
SDK_STORAGE_KEYS$1.LAST_MODELS_UPDATE,
|
|
4192
|
+
{}
|
|
4193
|
+
)
|
|
4194
|
+
).map(([baseUrl, timestamp]) => [normalizeBaseUrl(baseUrl), timestamp])
|
|
4195
|
+
),
|
|
4196
|
+
cachedTokens: driver.getItem(SDK_STORAGE_KEYS$1.LOCAL_CASHU_TOKENS, []).map((entry) => ({
|
|
4197
|
+
...entry,
|
|
4198
|
+
baseUrl: normalizeBaseUrl(entry.baseUrl),
|
|
4199
|
+
balance: typeof entry.balance === "number" ? entry.balance : getTokenBalance(entry.token),
|
|
4200
|
+
lastUsed: entry.lastUsed ?? null
|
|
4201
|
+
})),
|
|
4202
|
+
apiKeys: driver.getItem(SDK_STORAGE_KEYS$1.API_KEYS, []).map((entry) => ({
|
|
4203
|
+
...entry,
|
|
4204
|
+
baseUrl: normalizeBaseUrl(entry.baseUrl),
|
|
4205
|
+
balance: entry.balance ?? 0,
|
|
4206
|
+
lastUsed: entry.lastUsed ?? null
|
|
4207
|
+
})),
|
|
4208
|
+
childKeys: driver.getItem(SDK_STORAGE_KEYS$1.CHILD_KEYS, []).map((entry) => ({
|
|
4209
|
+
parentBaseUrl: normalizeBaseUrl(entry.parentBaseUrl),
|
|
4210
|
+
childKey: entry.childKey,
|
|
4211
|
+
balance: entry.balance ?? 0,
|
|
4212
|
+
balanceLimit: entry.balanceLimit,
|
|
4213
|
+
validityDate: entry.validityDate,
|
|
4214
|
+
createdAt: entry.createdAt ?? Date.now()
|
|
4215
|
+
})),
|
|
4216
|
+
setModelsFromAllProviders: (value) => {
|
|
4217
|
+
const normalized = {};
|
|
4218
|
+
for (const [baseUrl, models] of Object.entries(value)) {
|
|
4219
|
+
normalized[normalizeBaseUrl(baseUrl)] = models;
|
|
4220
|
+
}
|
|
4221
|
+
driver.setItem(SDK_STORAGE_KEYS$1.MODELS_FROM_ALL_PROVIDERS, normalized);
|
|
4222
|
+
set({ modelsFromAllProviders: normalized });
|
|
4223
|
+
},
|
|
4224
|
+
setLastUsedModel: (value) => {
|
|
4225
|
+
driver.setItem(SDK_STORAGE_KEYS$1.LAST_USED_MODEL, value);
|
|
4226
|
+
set({ lastUsedModel: value });
|
|
4227
|
+
},
|
|
4228
|
+
setBaseUrlsList: (value) => {
|
|
4229
|
+
const normalized = value.map((url) => normalizeBaseUrl(url));
|
|
4230
|
+
driver.setItem(SDK_STORAGE_KEYS$1.BASE_URLS_LIST, normalized);
|
|
4231
|
+
set({ baseUrlsList: normalized });
|
|
4232
|
+
},
|
|
4233
|
+
setBaseUrlsLastUpdate: (value) => {
|
|
4234
|
+
driver.setItem(SDK_STORAGE_KEYS$1.LAST_BASE_URLS_UPDATE, value);
|
|
4235
|
+
set({ lastBaseUrlsUpdate: value });
|
|
4236
|
+
},
|
|
4237
|
+
setDisabledProviders: (value) => {
|
|
4238
|
+
const normalized = value.map((url) => normalizeBaseUrl(url));
|
|
4239
|
+
driver.setItem(SDK_STORAGE_KEYS$1.DISABLED_PROVIDERS, normalized);
|
|
4240
|
+
set({ disabledProviders: normalized });
|
|
4241
|
+
},
|
|
4242
|
+
setMintsFromAllProviders: (value) => {
|
|
4243
|
+
const normalized = {};
|
|
4244
|
+
for (const [baseUrl, mints] of Object.entries(value)) {
|
|
4245
|
+
normalized[normalizeBaseUrl(baseUrl)] = mints.map(
|
|
4246
|
+
(mint) => mint.endsWith("/") ? mint.slice(0, -1) : mint
|
|
4247
|
+
);
|
|
4248
|
+
}
|
|
4249
|
+
driver.setItem(SDK_STORAGE_KEYS$1.MINTS_FROM_ALL_PROVIDERS, normalized);
|
|
4250
|
+
set({ mintsFromAllProviders: normalized });
|
|
4251
|
+
},
|
|
4252
|
+
setInfoFromAllProviders: (value) => {
|
|
4253
|
+
const normalized = {};
|
|
4254
|
+
for (const [baseUrl, info] of Object.entries(value)) {
|
|
4255
|
+
normalized[normalizeBaseUrl(baseUrl)] = info;
|
|
4256
|
+
}
|
|
4257
|
+
driver.setItem(SDK_STORAGE_KEYS$1.INFO_FROM_ALL_PROVIDERS, normalized);
|
|
4258
|
+
set({ infoFromAllProviders: normalized });
|
|
4259
|
+
},
|
|
4260
|
+
setLastModelsUpdate: (value) => {
|
|
4261
|
+
const normalized = {};
|
|
4262
|
+
for (const [baseUrl, timestamp] of Object.entries(value)) {
|
|
4263
|
+
normalized[normalizeBaseUrl(baseUrl)] = timestamp;
|
|
4264
|
+
}
|
|
4265
|
+
driver.setItem(SDK_STORAGE_KEYS$1.LAST_MODELS_UPDATE, normalized);
|
|
4266
|
+
set({ lastModelsUpdate: normalized });
|
|
4267
|
+
},
|
|
4268
|
+
setCachedTokens: (value) => {
|
|
4269
|
+
set((state) => {
|
|
4270
|
+
const updates = typeof value === "function" ? value(state.cachedTokens) : value;
|
|
4271
|
+
const normalized = updates.map((entry) => ({
|
|
4272
|
+
...entry,
|
|
4273
|
+
baseUrl: normalizeBaseUrl(entry.baseUrl),
|
|
4274
|
+
balance: typeof entry.balance === "number" ? entry.balance : getTokenBalance(entry.token),
|
|
4275
|
+
lastUsed: entry.lastUsed ?? null
|
|
4276
|
+
}));
|
|
4277
|
+
driver.setItem(SDK_STORAGE_KEYS$1.LOCAL_CASHU_TOKENS, normalized);
|
|
4278
|
+
return { cachedTokens: normalized };
|
|
4279
|
+
});
|
|
4280
|
+
},
|
|
4281
|
+
setApiKeys: (value) => {
|
|
4282
|
+
set((state) => {
|
|
4283
|
+
const updates = typeof value === "function" ? value(state.apiKeys) : value;
|
|
4284
|
+
const normalized = updates.map((entry) => ({
|
|
4285
|
+
...entry,
|
|
4286
|
+
baseUrl: normalizeBaseUrl(entry.baseUrl),
|
|
4287
|
+
balance: entry.balance ?? 0,
|
|
4288
|
+
lastUsed: entry.lastUsed ?? null
|
|
4289
|
+
}));
|
|
4290
|
+
driver.setItem(SDK_STORAGE_KEYS$1.API_KEYS, normalized);
|
|
4291
|
+
return { apiKeys: normalized };
|
|
4292
|
+
});
|
|
4293
|
+
},
|
|
4294
|
+
setChildKeys: (value) => {
|
|
4295
|
+
set((state) => {
|
|
4296
|
+
const updates = typeof value === "function" ? value(state.childKeys) : value;
|
|
4297
|
+
const normalized = updates.map((entry) => ({
|
|
4298
|
+
parentBaseUrl: normalizeBaseUrl(entry.parentBaseUrl),
|
|
4299
|
+
childKey: entry.childKey,
|
|
4300
|
+
balance: entry.balance ?? 0,
|
|
4301
|
+
balanceLimit: entry.balanceLimit,
|
|
4302
|
+
validityDate: entry.validityDate,
|
|
4303
|
+
createdAt: entry.createdAt ?? Date.now()
|
|
4304
|
+
}));
|
|
4305
|
+
driver.setItem(SDK_STORAGE_KEYS$1.CHILD_KEYS, normalized);
|
|
4306
|
+
return { childKeys: normalized };
|
|
4307
|
+
});
|
|
4308
|
+
}
|
|
4309
|
+
}));
|
|
4310
|
+
};
|
|
4311
|
+
var createDiscoveryAdapterFromStore = (store) => ({
|
|
4312
|
+
getCachedModels: () => store.getState().modelsFromAllProviders,
|
|
4313
|
+
setCachedModels: (models) => store.getState().setModelsFromAllProviders(models),
|
|
4314
|
+
getCachedMints: () => store.getState().mintsFromAllProviders,
|
|
4315
|
+
setCachedMints: (mints) => store.getState().setMintsFromAllProviders(mints),
|
|
4316
|
+
getCachedProviderInfo: () => store.getState().infoFromAllProviders,
|
|
4317
|
+
setCachedProviderInfo: (info) => store.getState().setInfoFromAllProviders(info),
|
|
4318
|
+
getProviderLastUpdate: (baseUrl) => {
|
|
4319
|
+
const normalized = normalizeBaseUrl(baseUrl);
|
|
4320
|
+
const timestamps = store.getState().lastModelsUpdate;
|
|
4321
|
+
return timestamps[normalized] || null;
|
|
4322
|
+
},
|
|
4323
|
+
setProviderLastUpdate: (baseUrl, timestamp) => {
|
|
4324
|
+
const normalized = normalizeBaseUrl(baseUrl);
|
|
4325
|
+
const timestamps = { ...store.getState().lastModelsUpdate };
|
|
4326
|
+
timestamps[normalized] = timestamp;
|
|
4327
|
+
store.getState().setLastModelsUpdate(timestamps);
|
|
4328
|
+
},
|
|
4329
|
+
getLastUsedModel: () => store.getState().lastUsedModel,
|
|
4330
|
+
setLastUsedModel: (modelId) => store.getState().setLastUsedModel(modelId),
|
|
4331
|
+
getDisabledProviders: () => store.getState().disabledProviders,
|
|
4332
|
+
getBaseUrlsList: () => store.getState().baseUrlsList,
|
|
4333
|
+
setBaseUrlsList: (urls) => store.getState().setBaseUrlsList(urls),
|
|
4334
|
+
getBaseUrlsLastUpdate: () => store.getState().lastBaseUrlsUpdate,
|
|
4335
|
+
setBaseUrlsLastUpdate: (timestamp) => store.getState().setBaseUrlsLastUpdate(timestamp)
|
|
4336
|
+
});
|
|
4337
|
+
var createStorageAdapterFromStore = (store) => ({
|
|
4338
|
+
getToken: (baseUrl) => {
|
|
4339
|
+
const normalized = normalizeBaseUrl(baseUrl);
|
|
4340
|
+
const entry = store.getState().cachedTokens.find((token) => token.baseUrl === normalized);
|
|
4341
|
+
if (!entry) return null;
|
|
4342
|
+
const next = store.getState().cachedTokens.map(
|
|
4343
|
+
(token) => token.baseUrl === normalized ? { ...token, lastUsed: Date.now() } : token
|
|
4344
|
+
);
|
|
4345
|
+
store.getState().setCachedTokens(next);
|
|
4346
|
+
return entry.token;
|
|
4347
|
+
},
|
|
4348
|
+
setToken: (baseUrl, token) => {
|
|
4349
|
+
const normalized = normalizeBaseUrl(baseUrl);
|
|
4350
|
+
const tokens = store.getState().cachedTokens;
|
|
4351
|
+
const balance = getTokenBalance(token);
|
|
4352
|
+
const existingIndex = tokens.findIndex(
|
|
4353
|
+
(entry) => entry.baseUrl === normalized
|
|
4354
|
+
);
|
|
4355
|
+
if (existingIndex !== -1) {
|
|
4356
|
+
throw new Error(`Token already exists for baseUrl: ${normalized}`);
|
|
4357
|
+
}
|
|
4358
|
+
const next = [...tokens];
|
|
4359
|
+
next.push({
|
|
4360
|
+
baseUrl: normalized,
|
|
4361
|
+
token,
|
|
4362
|
+
balance,
|
|
4363
|
+
lastUsed: Date.now()
|
|
4364
|
+
});
|
|
4365
|
+
store.getState().setCachedTokens(next);
|
|
4366
|
+
},
|
|
4367
|
+
removeToken: (baseUrl) => {
|
|
4368
|
+
const normalized = normalizeBaseUrl(baseUrl);
|
|
4369
|
+
const next = store.getState().cachedTokens.filter((entry) => entry.baseUrl !== normalized);
|
|
4370
|
+
store.getState().setCachedTokens(next);
|
|
4371
|
+
},
|
|
4372
|
+
updateTokenBalance: (baseUrl, balance) => {
|
|
4373
|
+
const normalized = normalizeBaseUrl(baseUrl);
|
|
4374
|
+
const tokens = store.getState().cachedTokens;
|
|
4375
|
+
const next = tokens.map(
|
|
4376
|
+
(entry) => entry.baseUrl === normalized ? { ...entry, balance } : entry
|
|
4377
|
+
);
|
|
4378
|
+
store.getState().setCachedTokens(next);
|
|
4379
|
+
},
|
|
4380
|
+
getPendingTokenDistribution: () => {
|
|
4381
|
+
const tokens = store.getState().cachedTokens;
|
|
4382
|
+
const distributionMap = {};
|
|
4383
|
+
for (const entry of tokens) {
|
|
4384
|
+
const sum = entry.balance || 0;
|
|
4385
|
+
if (sum > 0) {
|
|
4386
|
+
distributionMap[entry.baseUrl] = (distributionMap[entry.baseUrl] || 0) + sum;
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
4389
|
+
return Object.entries(distributionMap).map(([baseUrl, amt]) => ({ baseUrl, amount: amt })).sort((a, b) => b.amount - a.amount);
|
|
4390
|
+
},
|
|
4391
|
+
saveProviderInfo: (baseUrl, info) => {
|
|
4392
|
+
const normalized = normalizeBaseUrl(baseUrl);
|
|
4393
|
+
const next = { ...store.getState().infoFromAllProviders };
|
|
4394
|
+
next[normalized] = info;
|
|
4395
|
+
store.getState().setInfoFromAllProviders(next);
|
|
4396
|
+
},
|
|
4397
|
+
getProviderInfo: (baseUrl) => {
|
|
4398
|
+
const normalized = normalizeBaseUrl(baseUrl);
|
|
4399
|
+
return store.getState().infoFromAllProviders[normalized] || null;
|
|
4400
|
+
},
|
|
4401
|
+
// ========== API Keys (for apikeys mode) ==========
|
|
4402
|
+
getApiKey: (baseUrl) => {
|
|
4403
|
+
const normalized = normalizeBaseUrl(baseUrl);
|
|
4404
|
+
const entry = store.getState().apiKeys.find((key) => key.baseUrl === normalized);
|
|
4405
|
+
if (!entry) return null;
|
|
4406
|
+
const next = store.getState().apiKeys.map(
|
|
4407
|
+
(key) => key.baseUrl === normalized ? { ...key, lastUsed: Date.now() } : key
|
|
4408
|
+
);
|
|
4409
|
+
store.getState().setApiKeys(next);
|
|
4410
|
+
return entry.key;
|
|
4411
|
+
},
|
|
4412
|
+
setApiKey: (baseUrl, key) => {
|
|
4413
|
+
const normalized = normalizeBaseUrl(baseUrl);
|
|
4414
|
+
const keys = store.getState().apiKeys;
|
|
4415
|
+
const existingIndex = keys.findIndex(
|
|
4416
|
+
(entry) => entry.baseUrl === normalized
|
|
4417
|
+
);
|
|
4418
|
+
if (existingIndex !== -1) {
|
|
4419
|
+
const next = keys.map(
|
|
4420
|
+
(entry) => entry.baseUrl === normalized ? { ...entry, key, lastUsed: Date.now() } : entry
|
|
4421
|
+
);
|
|
4422
|
+
store.getState().setApiKeys(next);
|
|
4423
|
+
} else {
|
|
4424
|
+
const next = [...keys];
|
|
4425
|
+
next.push({
|
|
4426
|
+
baseUrl: normalized,
|
|
4427
|
+
key,
|
|
4428
|
+
balance: 0,
|
|
4429
|
+
lastUsed: Date.now()
|
|
4430
|
+
});
|
|
4431
|
+
store.getState().setApiKeys(next);
|
|
4432
|
+
}
|
|
4433
|
+
},
|
|
4434
|
+
updateApiKeyBalance: (baseUrl, balance) => {
|
|
4435
|
+
const normalized = normalizeBaseUrl(baseUrl);
|
|
4436
|
+
const keys = store.getState().apiKeys;
|
|
4437
|
+
const next = keys.map(
|
|
4438
|
+
(entry) => entry.baseUrl === normalized ? { ...entry, balance } : entry
|
|
4439
|
+
);
|
|
4440
|
+
store.getState().setApiKeys(next);
|
|
4441
|
+
},
|
|
4442
|
+
getAllApiKeys: () => {
|
|
4443
|
+
return store.getState().apiKeys.map((entry) => ({
|
|
4444
|
+
baseUrl: entry.baseUrl,
|
|
4445
|
+
key: entry.key,
|
|
4446
|
+
balance: entry.balance,
|
|
4447
|
+
lastUsed: entry.lastUsed
|
|
4448
|
+
}));
|
|
4449
|
+
},
|
|
4450
|
+
// ========== Child Keys ==========
|
|
4451
|
+
getChildKey: (parentBaseUrl) => {
|
|
4452
|
+
const normalized = normalizeBaseUrl(parentBaseUrl);
|
|
4453
|
+
const entry = store.getState().childKeys.find((key) => key.parentBaseUrl === normalized);
|
|
4454
|
+
if (!entry) return null;
|
|
4455
|
+
return {
|
|
4456
|
+
parentBaseUrl: entry.parentBaseUrl,
|
|
4457
|
+
childKey: entry.childKey,
|
|
4458
|
+
balance: entry.balance,
|
|
4459
|
+
balanceLimit: entry.balanceLimit,
|
|
4460
|
+
validityDate: entry.validityDate,
|
|
4461
|
+
createdAt: entry.createdAt
|
|
4462
|
+
};
|
|
4463
|
+
},
|
|
4464
|
+
setChildKey: (parentBaseUrl, childKey, balance, validityDate, balanceLimit) => {
|
|
4465
|
+
const normalized = normalizeBaseUrl(parentBaseUrl);
|
|
4466
|
+
const keys = store.getState().childKeys;
|
|
4467
|
+
const existingIndex = keys.findIndex(
|
|
4468
|
+
(entry) => entry.parentBaseUrl === normalized
|
|
4469
|
+
);
|
|
4470
|
+
if (existingIndex !== -1) {
|
|
4471
|
+
const next = keys.map(
|
|
4472
|
+
(entry) => entry.parentBaseUrl === normalized ? {
|
|
4473
|
+
...entry,
|
|
4474
|
+
childKey,
|
|
4475
|
+
balance: balance ?? 0,
|
|
4476
|
+
validityDate,
|
|
4477
|
+
balanceLimit,
|
|
4478
|
+
createdAt: Date.now()
|
|
4479
|
+
} : entry
|
|
4480
|
+
);
|
|
4481
|
+
store.getState().setChildKeys(next);
|
|
4482
|
+
} else {
|
|
4483
|
+
const next = [...keys];
|
|
4484
|
+
next.push({
|
|
4485
|
+
parentBaseUrl: normalized,
|
|
4486
|
+
childKey,
|
|
4487
|
+
balance: balance ?? 0,
|
|
4488
|
+
validityDate,
|
|
4489
|
+
balanceLimit,
|
|
4490
|
+
createdAt: Date.now()
|
|
4491
|
+
});
|
|
4492
|
+
store.getState().setChildKeys(next);
|
|
4493
|
+
}
|
|
4494
|
+
},
|
|
4495
|
+
updateChildKeyBalance: (parentBaseUrl, balance) => {
|
|
4496
|
+
const normalized = normalizeBaseUrl(parentBaseUrl);
|
|
4497
|
+
const keys = store.getState().childKeys;
|
|
4498
|
+
const next = keys.map(
|
|
4499
|
+
(entry) => entry.parentBaseUrl === normalized ? { ...entry, balance } : entry
|
|
4500
|
+
);
|
|
4501
|
+
store.getState().setChildKeys(next);
|
|
4502
|
+
},
|
|
4503
|
+
removeChildKey: (parentBaseUrl) => {
|
|
4504
|
+
const normalized = normalizeBaseUrl(parentBaseUrl);
|
|
4505
|
+
const next = store.getState().childKeys.filter((entry) => entry.parentBaseUrl !== normalized);
|
|
4506
|
+
store.getState().setChildKeys(next);
|
|
4507
|
+
},
|
|
4508
|
+
getAllChildKeys: () => {
|
|
4509
|
+
return store.getState().childKeys.map((entry) => ({
|
|
4510
|
+
parentBaseUrl: entry.parentBaseUrl,
|
|
4511
|
+
childKey: entry.childKey,
|
|
4512
|
+
balance: entry.balance,
|
|
4513
|
+
balanceLimit: entry.balanceLimit,
|
|
4514
|
+
validityDate: entry.validityDate,
|
|
4515
|
+
createdAt: entry.createdAt
|
|
4516
|
+
}));
|
|
4517
|
+
}
|
|
4518
|
+
});
|
|
4519
|
+
var createProviderRegistryFromStore = (store) => ({
|
|
4520
|
+
getModelsForProvider: (baseUrl) => {
|
|
4521
|
+
const normalized = normalizeBaseUrl(baseUrl);
|
|
4522
|
+
return store.getState().modelsFromAllProviders[normalized] || [];
|
|
4523
|
+
},
|
|
4524
|
+
getDisabledProviders: () => store.getState().disabledProviders,
|
|
4525
|
+
getProviderMints: (baseUrl) => {
|
|
4526
|
+
const normalized = normalizeBaseUrl(baseUrl);
|
|
4527
|
+
return store.getState().mintsFromAllProviders[normalized] || [];
|
|
4528
|
+
},
|
|
4529
|
+
getProviderInfo: async (baseUrl) => {
|
|
4530
|
+
const normalized = normalizeBaseUrl(baseUrl);
|
|
4531
|
+
const cached = store.getState().infoFromAllProviders[normalized];
|
|
4532
|
+
if (cached) return cached;
|
|
4533
|
+
try {
|
|
4534
|
+
const response = await fetch(`${normalized}v1/info`);
|
|
4535
|
+
if (!response.ok) {
|
|
4536
|
+
throw new Error(`Failed ${response.status}`);
|
|
4537
|
+
}
|
|
4538
|
+
const info = await response.json();
|
|
4539
|
+
const next = { ...store.getState().infoFromAllProviders };
|
|
4540
|
+
next[normalized] = info;
|
|
4541
|
+
store.getState().setInfoFromAllProviders(next);
|
|
4542
|
+
return info;
|
|
4543
|
+
} catch (error) {
|
|
4544
|
+
console.warn(`Failed to fetch provider info from ${normalized}:`, error);
|
|
4545
|
+
return null;
|
|
4546
|
+
}
|
|
4547
|
+
},
|
|
4548
|
+
getAllProvidersModels: () => store.getState().modelsFromAllProviders
|
|
4549
|
+
});
|
|
4550
|
+
|
|
4551
|
+
// storage/index.ts
|
|
4552
|
+
var isBrowser = () => {
|
|
4553
|
+
try {
|
|
4554
|
+
return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
|
|
4555
|
+
} catch {
|
|
4556
|
+
return false;
|
|
4557
|
+
}
|
|
4558
|
+
};
|
|
4559
|
+
var isNode = () => {
|
|
4560
|
+
try {
|
|
4561
|
+
return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
|
|
4562
|
+
} catch {
|
|
4563
|
+
return false;
|
|
4564
|
+
}
|
|
4565
|
+
};
|
|
4566
|
+
var defaultDriver = null;
|
|
4567
|
+
var getDefaultSdkDriver$1 = () => {
|
|
4568
|
+
if (defaultDriver) return defaultDriver;
|
|
4569
|
+
if (isBrowser()) {
|
|
4570
|
+
defaultDriver = localStorageDriver$1;
|
|
4571
|
+
return defaultDriver;
|
|
4572
|
+
}
|
|
4573
|
+
if (isNode()) {
|
|
4574
|
+
defaultDriver = createSqliteDriver$1();
|
|
4575
|
+
return defaultDriver;
|
|
4576
|
+
}
|
|
4577
|
+
defaultDriver = createMemoryDriver$1();
|
|
4578
|
+
return defaultDriver;
|
|
4579
|
+
};
|
|
4580
|
+
var defaultStore = null;
|
|
4581
|
+
var getDefaultSdkStore$1 = () => {
|
|
4582
|
+
if (!defaultStore) {
|
|
4583
|
+
defaultStore = createSdkStore$1({ driver: getDefaultSdkDriver$1() });
|
|
4584
|
+
}
|
|
4585
|
+
return defaultStore;
|
|
4586
|
+
};
|
|
4587
|
+
var getDefaultDiscoveryAdapter$1 = () => createDiscoveryAdapterFromStore(getDefaultSdkStore$1());
|
|
4588
|
+
var getDefaultStorageAdapter$1 = () => createStorageAdapterFromStore(getDefaultSdkStore$1());
|
|
4589
|
+
var getDefaultProviderRegistry$1 = () => createProviderRegistryFromStore(getDefaultSdkStore$1());
|
|
4590
|
+
|
|
4591
|
+
// routeRequests.ts
|
|
4592
|
+
async function routeRequests$1(options) {
|
|
4593
|
+
const {
|
|
4594
|
+
modelId,
|
|
4595
|
+
requestBody,
|
|
4596
|
+
path = "/v1/chat/completions",
|
|
4597
|
+
forcedProvider,
|
|
4598
|
+
walletAdapter,
|
|
4599
|
+
storageAdapter,
|
|
4600
|
+
providerRegistry,
|
|
4601
|
+
discoveryAdapter,
|
|
4602
|
+
includeProviderUrls = [],
|
|
4603
|
+
torMode = false,
|
|
4604
|
+
forceRefresh = false,
|
|
4605
|
+
modelManager: providedModelManager
|
|
4606
|
+
} = options;
|
|
4607
|
+
let modelManager;
|
|
4608
|
+
let providers;
|
|
4609
|
+
if (providedModelManager) {
|
|
4610
|
+
modelManager = providedModelManager;
|
|
4611
|
+
providers = modelManager.getBaseUrls();
|
|
4612
|
+
if (providers.length === 0) {
|
|
4613
|
+
throw new Error("No providers available - run bootstrap first");
|
|
4614
|
+
}
|
|
4615
|
+
} else {
|
|
4616
|
+
modelManager = new ModelManager(discoveryAdapter, {
|
|
4617
|
+
includeProviderUrls: forcedProvider ? [forcedProvider, ...includeProviderUrls] : includeProviderUrls
|
|
4618
|
+
});
|
|
4619
|
+
providers = await modelManager.bootstrapProviders(torMode);
|
|
4620
|
+
if (providers.length === 0) {
|
|
4621
|
+
throw new Error("No providers available");
|
|
4622
|
+
}
|
|
4623
|
+
await modelManager.fetchModels(providers, forceRefresh);
|
|
4624
|
+
}
|
|
4625
|
+
const providerManager = new ProviderManager(providerRegistry);
|
|
4626
|
+
let baseUrl;
|
|
4627
|
+
let selectedModel;
|
|
4628
|
+
if (forcedProvider) {
|
|
4629
|
+
const normalizedProvider = forcedProvider.endsWith("/") ? forcedProvider : `${forcedProvider}/`;
|
|
4630
|
+
const cachedModels = modelManager.getAllCachedModels();
|
|
4631
|
+
const models = cachedModels[normalizedProvider] || [];
|
|
4632
|
+
const match = models.find((m) => m.id === modelId);
|
|
4633
|
+
if (!match) {
|
|
4634
|
+
throw new Error(
|
|
4635
|
+
`Provider ${normalizedProvider} does not offer model: ${modelId}`
|
|
4636
|
+
);
|
|
4637
|
+
}
|
|
4638
|
+
baseUrl = normalizedProvider;
|
|
4639
|
+
selectedModel = match;
|
|
4640
|
+
} else {
|
|
4641
|
+
const ranking = providerManager.getProviderPriceRankingForModel(modelId, {
|
|
4642
|
+
torMode,
|
|
4643
|
+
includeDisabled: false
|
|
4644
|
+
});
|
|
4645
|
+
if (ranking.length === 0) {
|
|
4646
|
+
throw new Error(`No providers found for model: ${modelId}`);
|
|
4647
|
+
}
|
|
4648
|
+
const cheapest = ranking[0];
|
|
4649
|
+
baseUrl = cheapest.baseUrl;
|
|
4650
|
+
selectedModel = cheapest.model;
|
|
4651
|
+
}
|
|
4652
|
+
const balances = await walletAdapter.getBalances();
|
|
4653
|
+
const totalBalance = Object.values(balances).reduce((sum, v) => sum + v, 0);
|
|
4654
|
+
if (totalBalance <= 0) {
|
|
4655
|
+
throw new Error(
|
|
4656
|
+
"Wallet balance is empty. Add a mint and fund it before making requests."
|
|
4657
|
+
);
|
|
4658
|
+
}
|
|
4659
|
+
const providerMints = providerRegistry.getProviderMints(baseUrl);
|
|
4660
|
+
const mintUrl = walletAdapter.getActiveMintUrl() || providerMints[0] || Object.keys(balances)[0];
|
|
4661
|
+
if (!mintUrl) {
|
|
4662
|
+
throw new Error("No mint configured in wallet");
|
|
4663
|
+
}
|
|
4664
|
+
const alertLevel = "min";
|
|
4665
|
+
const client = new RoutstrClient(
|
|
4666
|
+
walletAdapter,
|
|
4667
|
+
storageAdapter,
|
|
4668
|
+
providerRegistry,
|
|
4669
|
+
alertLevel,
|
|
4670
|
+
"apikeys"
|
|
4671
|
+
);
|
|
4672
|
+
const maxTokens = extractMaxTokens(requestBody);
|
|
4673
|
+
const stream = extractStream(requestBody);
|
|
4674
|
+
let response = null;
|
|
4675
|
+
try {
|
|
4676
|
+
const proxiedBody = requestBody && typeof requestBody === "object" ? { ...requestBody } : {};
|
|
4677
|
+
proxiedBody.model = selectedModel.id;
|
|
4678
|
+
if (stream !== void 0) {
|
|
4679
|
+
proxiedBody.stream = stream;
|
|
4680
|
+
}
|
|
4681
|
+
if (maxTokens !== void 0) {
|
|
4682
|
+
proxiedBody.max_tokens = maxTokens;
|
|
4683
|
+
}
|
|
4684
|
+
response = await client.routeRequest({
|
|
4685
|
+
path,
|
|
4686
|
+
method: "POST",
|
|
4687
|
+
body: proxiedBody,
|
|
4688
|
+
baseUrl,
|
|
4689
|
+
mintUrl,
|
|
4690
|
+
modelId
|
|
4691
|
+
});
|
|
4692
|
+
if (!response.ok) {
|
|
4693
|
+
throw new Error(`${response.status} ${response.statusText}`);
|
|
4694
|
+
}
|
|
4695
|
+
return response;
|
|
4696
|
+
} catch (error) {
|
|
4697
|
+
if (error instanceof Error && (error.message.includes("401") || error.message.includes("402") || error.message.includes("403"))) {
|
|
4698
|
+
throw new Error(`Authentication failed: ${error.message}`);
|
|
4699
|
+
}
|
|
4700
|
+
throw error;
|
|
4701
|
+
}
|
|
4702
|
+
}
|
|
4703
|
+
function extractMaxTokens(requestBody) {
|
|
4704
|
+
if (!requestBody || typeof requestBody !== "object") {
|
|
4705
|
+
return void 0;
|
|
4706
|
+
}
|
|
4707
|
+
const body = requestBody;
|
|
4708
|
+
const maxTokens = body.max_tokens;
|
|
4709
|
+
if (typeof maxTokens === "number") {
|
|
4710
|
+
return maxTokens;
|
|
4711
|
+
}
|
|
4712
|
+
return void 0;
|
|
4713
|
+
}
|
|
4714
|
+
function extractStream(requestBody) {
|
|
4715
|
+
if (!requestBody || typeof requestBody !== "object") {
|
|
4716
|
+
return false;
|
|
4717
|
+
}
|
|
4718
|
+
const body = requestBody;
|
|
4719
|
+
const stream = body.stream;
|
|
4720
|
+
return stream === true;
|
|
4721
|
+
}
|
|
4722
|
+
|
|
4723
|
+
exports.BalanceManager = BalanceManager;
|
|
4724
|
+
exports.CashuSpender = CashuSpender;
|
|
4725
|
+
exports.FailoverError = FailoverError;
|
|
4726
|
+
exports.InsufficientBalanceError = InsufficientBalanceError;
|
|
4727
|
+
exports.MintDiscovery = MintDiscovery;
|
|
4728
|
+
exports.MintDiscoveryError = MintDiscoveryError;
|
|
4729
|
+
exports.MintUnreachableError = MintUnreachableError;
|
|
4730
|
+
exports.ModelManager = ModelManager;
|
|
4731
|
+
exports.ModelNotFoundError = ModelNotFoundError;
|
|
4732
|
+
exports.NoProvidersAvailableError = NoProvidersAvailableError;
|
|
4733
|
+
exports.ProviderBootstrapError = ProviderBootstrapError;
|
|
4734
|
+
exports.ProviderError = ProviderError;
|
|
4735
|
+
exports.ProviderManager = ProviderManager;
|
|
4736
|
+
exports.RoutstrClient = RoutstrClient;
|
|
4737
|
+
exports.SDK_STORAGE_KEYS = SDK_STORAGE_KEYS$1;
|
|
4738
|
+
exports.StreamProcessor = StreamProcessor;
|
|
4739
|
+
exports.StreamingError = StreamingError;
|
|
4740
|
+
exports.TokenOperationError = TokenOperationError;
|
|
4741
|
+
exports.createMemoryDriver = createMemoryDriver$1;
|
|
4742
|
+
exports.createSdkStore = createSdkStore$1;
|
|
4743
|
+
exports.createSqliteDriver = createSqliteDriver$1;
|
|
4744
|
+
exports.filterBaseUrlsForTor = filterBaseUrlsForTor;
|
|
4745
|
+
exports.getDefaultDiscoveryAdapter = getDefaultDiscoveryAdapter$1;
|
|
4746
|
+
exports.getDefaultProviderRegistry = getDefaultProviderRegistry$1;
|
|
4747
|
+
exports.getDefaultSdkDriver = getDefaultSdkDriver$1;
|
|
4748
|
+
exports.getDefaultSdkStore = getDefaultSdkStore$1;
|
|
4749
|
+
exports.getDefaultStorageAdapter = getDefaultStorageAdapter$1;
|
|
4750
|
+
exports.getProviderEndpoints = getProviderEndpoints;
|
|
4751
|
+
exports.isOnionUrl = isOnionUrl;
|
|
4752
|
+
exports.isTorContext = isTorContext;
|
|
4753
|
+
exports.localStorageDriver = localStorageDriver$1;
|
|
4754
|
+
exports.normalizeProviderUrl = normalizeProviderUrl;
|
|
4755
|
+
exports.routeRequests = routeRequests$1;
|
|
4756
|
+
|
|
4757
|
+
interface StorageDriver {
|
|
4758
|
+
getItem<T>(key: string, defaultValue: T): T;
|
|
4759
|
+
setItem<T>(key: string, value: T): void;
|
|
4760
|
+
removeItem(key: string): void;
|
|
4761
|
+
}
|
|
4762
|
+
interface SdkStorageState {
|
|
4763
|
+
modelsFromAllProviders: Record<string, Model[]>;
|
|
4764
|
+
lastUsedModel: string | null;
|
|
4765
|
+
baseUrlsList: string[];
|
|
4766
|
+
disabledProviders: string[];
|
|
4767
|
+
mintsFromAllProviders: Record<string, string[]>;
|
|
4768
|
+
infoFromAllProviders: Record<string, ProviderInfo>;
|
|
4769
|
+
lastModelsUpdate: Record<string, number>;
|
|
4770
|
+
lastBaseUrlsUpdate: number | null;
|
|
4771
|
+
cachedTokens: Array<{
|
|
4772
|
+
baseUrl: string;
|
|
4773
|
+
token: string;
|
|
4774
|
+
balance: number;
|
|
4775
|
+
lastUsed: number | null;
|
|
4776
|
+
}>;
|
|
4777
|
+
apiKeys: Array<{
|
|
4778
|
+
baseUrl: string;
|
|
4779
|
+
key: string;
|
|
4780
|
+
balance: number;
|
|
4781
|
+
lastUsed: number | null;
|
|
4782
|
+
}>;
|
|
4783
|
+
childKeys: Array<{
|
|
4784
|
+
parentBaseUrl: string;
|
|
4785
|
+
childKey: string;
|
|
4786
|
+
balance: number;
|
|
4787
|
+
balanceLimit?: number;
|
|
4788
|
+
validityDate?: number;
|
|
4789
|
+
createdAt: number;
|
|
4790
|
+
}>;
|
|
4791
|
+
}
|
|
4792
|
+
|
|
4793
|
+
interface SdkStoreOptions {
|
|
4794
|
+
driver: StorageDriver;
|
|
4795
|
+
}
|
|
4796
|
+
interface SdkStorageStore extends SdkStorageState {
|
|
4797
|
+
setModelsFromAllProviders: (value: Record<string, Model[]>) => void;
|
|
4798
|
+
setLastUsedModel: (value: string | null) => void;
|
|
4799
|
+
setBaseUrlsList: (value: string[]) => void;
|
|
4800
|
+
setBaseUrlsLastUpdate: (value: number | null) => void;
|
|
4801
|
+
setDisabledProviders: (value: string[]) => void;
|
|
4802
|
+
setMintsFromAllProviders: (value: Record<string, string[]>) => void;
|
|
4803
|
+
setInfoFromAllProviders: (value: Record<string, ProviderInfo>) => void;
|
|
4804
|
+
setLastModelsUpdate: (value: Record<string, number>) => void;
|
|
4805
|
+
setCachedTokens: (value: Array<{
|
|
4806
|
+
baseUrl: string;
|
|
4807
|
+
token: string;
|
|
4808
|
+
balance?: number;
|
|
4809
|
+
lastUsed?: number | null;
|
|
4810
|
+
}> | ((current: SdkStorageStore["cachedTokens"]) => SdkStorageStore["cachedTokens"])) => void;
|
|
4811
|
+
setApiKeys: (value: Array<{
|
|
4812
|
+
baseUrl: string;
|
|
4813
|
+
key: string;
|
|
4814
|
+
balance?: number;
|
|
4815
|
+
lastUsed?: number | null;
|
|
4816
|
+
}> | ((current: SdkStorageStore["apiKeys"]) => SdkStorageStore["apiKeys"])) => void;
|
|
4817
|
+
setChildKeys: (value: Array<{
|
|
4818
|
+
parentBaseUrl: string;
|
|
4819
|
+
childKey: string;
|
|
4820
|
+
balance: number;
|
|
4821
|
+
balanceLimit?: number;
|
|
4822
|
+
validityDate?: number;
|
|
4823
|
+
createdAt: number;
|
|
4824
|
+
}>) => void;
|
|
4825
|
+
}
|
|
4826
|
+
declare const createSdkStore: ({ driver }: SdkStoreOptions) => zustand_vanilla.StoreApi<SdkStorageStore>;
|
|
4827
|
+
|
|
4828
|
+
declare const localStorageDriver: StorageDriver;
|
|
4829
|
+
|
|
4830
|
+
declare const createMemoryDriver: (seed?: Record<string, string>) => StorageDriver;
|
|
4831
|
+
|
|
4832
|
+
interface SqliteDriverOptions {
|
|
4833
|
+
dbPath?: string;
|
|
4834
|
+
tableName?: string;
|
|
4835
|
+
}
|
|
4836
|
+
declare const createSqliteDriver: (options?: SqliteDriverOptions) => StorageDriver;
|
|
4837
|
+
|
|
4838
|
+
declare const SDK_STORAGE_KEYS: {
|
|
4839
|
+
readonly MODELS_FROM_ALL_PROVIDERS: "modelsFromAllProviders";
|
|
4840
|
+
readonly LAST_USED_MODEL: "lastUsedModel";
|
|
4841
|
+
readonly BASE_URLS_LIST: "base_urls_list";
|
|
4842
|
+
readonly DISABLED_PROVIDERS: "disabled_providers";
|
|
4843
|
+
readonly MINTS_FROM_ALL_PROVIDERS: "mints_from_all_providers";
|
|
4844
|
+
readonly INFO_FROM_ALL_PROVIDERS: "info_from_all_providers";
|
|
4845
|
+
readonly LAST_MODELS_UPDATE: "lastModelsUpdate";
|
|
4846
|
+
readonly LAST_BASE_URLS_UPDATE: "lastBaseUrlsUpdate";
|
|
4847
|
+
readonly LOCAL_CASHU_TOKENS: "local_cashu_tokens";
|
|
4848
|
+
readonly API_KEYS: "api_keys";
|
|
4849
|
+
readonly CHILD_KEYS: "child_keys";
|
|
4850
|
+
};
|
|
4851
|
+
|
|
4852
|
+
declare const getDefaultSdkDriver: () => StorageDriver;
|
|
4853
|
+
declare const getDefaultSdkStore: () => zustand_vanilla.StoreApi<SdkStorageStore>;
|
|
4854
|
+
declare const getDefaultDiscoveryAdapter: () => undefined;
|
|
4855
|
+
declare const getDefaultStorageAdapter: () => undefined;
|
|
4856
|
+
declare const getDefaultProviderRegistry: () => undefined;
|
|
4857
|
+
|
|
4858
|
+
/**
|
|
4859
|
+
* routeRequests - SDK helper for routing OpenAI-compatible requests to the cheapest provider
|
|
4860
|
+
*
|
|
4861
|
+
* This module provides a reusable function for routing requests to the cheapest
|
|
4862
|
+
* provider based on model pricing, with automatic Cashu token handling.
|
|
4863
|
+
*/
|
|
4864
|
+
|
|
4865
|
+
/**
|
|
4866
|
+
* Options for routeRequests function
|
|
4867
|
+
*/
|
|
4868
|
+
interface RouteRequestOptions {
|
|
4869
|
+
/** The model ID to route (e.g., "gpt-4o") */
|
|
4870
|
+
modelId: string;
|
|
4871
|
+
/** The request body to proxy to the provider */
|
|
4872
|
+
requestBody: unknown;
|
|
4873
|
+
/** Optional: API path (defaults to /v1/chat/completions) */
|
|
4874
|
+
path?: string;
|
|
4875
|
+
/** Optional: force a specific provider base URL */
|
|
4876
|
+
forcedProvider?: string;
|
|
4877
|
+
/** Wallet adapter for Cashu operations */
|
|
4878
|
+
walletAdapter: WalletAdapter;
|
|
4879
|
+
/** Storage adapter for caching */
|
|
4880
|
+
storageAdapter: StorageAdapter;
|
|
4881
|
+
/** Provider registry for tracking available providers */
|
|
4882
|
+
providerRegistry: ProviderRegistry;
|
|
4883
|
+
/** Discovery adapter for model/mint discovery */
|
|
4884
|
+
discoveryAdapter: DiscoveryAdapter;
|
|
4885
|
+
/** Optional: additional provider URLs to include */
|
|
4886
|
+
includeProviderUrls?: string[];
|
|
4887
|
+
/** Optional: Tor mode for onion routing */
|
|
4888
|
+
torMode?: boolean;
|
|
4889
|
+
/** Optional: force refresh of cached data */
|
|
4890
|
+
forceRefresh?: boolean;
|
|
4891
|
+
/** Optional: pre-initialized ModelManager (skips bootstrap if provided) */
|
|
4892
|
+
modelManager?: ModelManager$1;
|
|
4893
|
+
}
|
|
4894
|
+
/**
|
|
4895
|
+
* Result from routeRequests function
|
|
4896
|
+
*/
|
|
4897
|
+
interface RouteRequestResult {
|
|
4898
|
+
/** The selected provider base URL */
|
|
4899
|
+
baseUrl: string;
|
|
4900
|
+
/** The selected model with pricing info */
|
|
4901
|
+
selectedModel: Model;
|
|
4902
|
+
/** Pricing info for the selected provider */
|
|
4903
|
+
pricing: {
|
|
4904
|
+
promptPerMillion: number;
|
|
4905
|
+
completionPerMillion: number;
|
|
4906
|
+
totalPerMillion: number;
|
|
4907
|
+
};
|
|
4908
|
+
/** The response from the provider */
|
|
4909
|
+
response: {
|
|
4910
|
+
status: number;
|
|
4911
|
+
statusText: string;
|
|
4912
|
+
headers: Record<string, string>;
|
|
4913
|
+
body: unknown;
|
|
4914
|
+
};
|
|
4915
|
+
}
|
|
4916
|
+
/**
|
|
4917
|
+
* Route an OpenAI-compatible request to the cheapest provider
|
|
4918
|
+
*
|
|
4919
|
+
* This function:
|
|
4920
|
+
* 1. Bootstraps providers and fetches models
|
|
4921
|
+
* 2. Discovers available mints
|
|
4922
|
+
* 3. Selects the cheapest provider for the requested model
|
|
4923
|
+
* 4. Handles Cashu send/receive via RoutstrClient
|
|
4924
|
+
* 5. Proxies the request and returns the response
|
|
4925
|
+
*
|
|
4926
|
+
* @param options - Routing options
|
|
4927
|
+
* @returns The provider response
|
|
4928
|
+
*/
|
|
4929
|
+
declare function routeRequests(options: RouteRequestOptions): Promise<Response>;
|
|
4930
|
+
|
|
4931
|
+
export { type APIErrorVerdict, type AlertLevel, type AnnotationData, type ApiKeyEntry, BalanceManager$1 as BalanceManager, type CandidateProvider, CashuSpender$1 as CashuSpender, type ChildKeyEntry, type DiscoveryAdapter, FailoverError$1 as FailoverError, type FetchAIResponseParams, type FetchOptions, type ImageData, InsufficientBalanceError$1 as InsufficientBalanceError, type Message, type MessageContentType, MintDiscovery$1 as MintDiscovery, MintDiscoveryError$1 as MintDiscoveryError, type MintDiscoveryResult, type MintSelection, MintUnreachableError$1 as MintUnreachableError, type Model, type ModelArchitecture, type ModelDiscoveryResult, ModelManager$1 as ModelManager, type ModelManagerConfig, ModelNotFoundError$1 as ModelNotFoundError, type ModelPricing, type ModelProviderPrice, type ModelSatsPricing, NoProvidersAvailableError$1 as NoProvidersAvailableError, type PendingTokenEntry, type PerRequestLimits, ProviderBootstrapError$1 as ProviderBootstrapError, type ProviderDirectoryEntry, ProviderError$1 as ProviderError, type ProviderInfo, ProviderManager$1 as ProviderManager, type ProviderRegistry, type RefundOptions, type RefundResult, type RouteRequestOptions, type RouteRequestParams, type RouteRequestResult, RoutstrClient$1 as RoutstrClient, type RoutstrClientMode, type RoutstrClientOptions, SDK_STORAGE_KEYS, type SpendOptions, type SpendResult, type StorageAdapter, type StorageDriver, type StreamCallbacks, StreamProcessor$1 as StreamProcessor, type StreamingCallbacks, StreamingError$1 as StreamingError, type StreamingResult, type TokenBalance, TokenOperationError$1 as TokenOperationError, type TopUpOptions, type TopUpResult, type TransactionHistory, type UsageStats, type WalletAdapter, createMemoryDriver, createSdkStore, createSqliteDriver, filterBaseUrlsForTor$1 as filterBaseUrlsForTor, getDefaultDiscoveryAdapter, getDefaultProviderRegistry, getDefaultSdkDriver, getDefaultSdkStore, getDefaultStorageAdapter, getProviderEndpoints$1 as getProviderEndpoints, isOnionUrl$1 as isOnionUrl, isTorContext$1 as isTorContext, localStorageDriver, normalizeProviderUrl$1 as normalizeProviderUrl, routeRequests };
|