@routstr/sdk 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -128,13 +128,16 @@ var ModelManager = class _ModelManager {
128
128
  this.cacheTTL = config.cacheTTL || 210 * 60 * 1e3;
129
129
  this.includeProviderUrls = config.includeProviderUrls || [];
130
130
  this.excludeProviderUrls = config.excludeProviderUrls || [];
131
+ this.routstrPubkey = config.routstrPubkey || "4ad6fa2d16e2a9b576c863b4cf7404a70d4dc320c0c447d10ad6ff58993eacc8";
131
132
  this.logger = (config.logger ?? consoleLogger).child("ModelManager");
132
133
  }
133
134
  cacheTTL;
134
135
  providerDirectoryUrl;
135
136
  includeProviderUrls;
136
137
  excludeProviderUrls;
138
+ routstrPubkey;
137
139
  logger;
140
+ providerNodePubkeysByUrl = /* @__PURE__ */ new Map();
138
141
  /**
139
142
  * Get the list of bootstrapped provider base URLs
140
143
  * @returns Array of provider base URLs
@@ -165,8 +168,13 @@ var ModelManager = class _ModelManager {
165
168
  const lastUpdate = this.adapter.getBaseUrlsLastUpdate();
166
169
  const cacheValid = lastUpdate && Date.now() - lastUpdate <= this.cacheTTL;
167
170
  if (cacheValid) {
171
+ const filteredCachedUrls = this.filterBaseUrlsForTor(
172
+ cachedUrls,
173
+ torMode
174
+ );
168
175
  await this.fetchRoutstr21Models(forceRefresh);
169
- return this.filterBaseUrlsForTor(cachedUrls, torMode);
176
+ await this.syncReviewedProvidersFromNostr(filteredCachedUrls);
177
+ return filteredCachedUrls;
170
178
  }
171
179
  }
172
180
  }
@@ -177,6 +185,7 @@ var ModelManager = class _ModelManager {
177
185
  this.adapter.setBaseUrlsList(filtered);
178
186
  this.adapter.setBaseUrlsLastUpdate(Date.now());
179
187
  await this.fetchRoutstr21Models(forceRefresh);
188
+ await this.syncReviewedProvidersFromNostr(filtered);
180
189
  return filtered;
181
190
  }
182
191
  } catch (e) {
@@ -219,6 +228,7 @@ var ModelManager = class _ModelManager {
219
228
  });
220
229
  const timeline = localEventStore.getTimeline({ kinds: [kind] });
221
230
  const bases = /* @__PURE__ */ new Set();
231
+ this.providerNodePubkeysByUrl = /* @__PURE__ */ new Map();
222
232
  for (const event of timeline) {
223
233
  const eventUrls = [];
224
234
  for (const tag of event.tags) {
@@ -231,6 +241,11 @@ var ModelManager = class _ModelManager {
231
241
  const normalized = this.normalizeUrl(url);
232
242
  if (!torMode || normalized.includes(".onion")) {
233
243
  bases.add(normalized);
244
+ this.addProviderNode(
245
+ this.providerNodePubkeysByUrl,
246
+ normalized,
247
+ event.pubkey
248
+ );
234
249
  }
235
250
  }
236
251
  continue;
@@ -242,6 +257,11 @@ var ModelManager = class _ModelManager {
242
257
  const endpoints = this.getProviderEndpoints(p, torMode);
243
258
  for (const endpoint of endpoints) {
244
259
  bases.add(endpoint);
260
+ this.addProviderNode(
261
+ this.providerNodePubkeysByUrl,
262
+ endpoint,
263
+ p?.pubkey || event.pubkey
264
+ );
245
265
  }
246
266
  }
247
267
  } catch {
@@ -252,11 +272,19 @@ var ModelManager = class _ModelManager {
252
272
  const endpoints = this.getProviderEndpoints(p, torMode);
253
273
  for (const endpoint of endpoints) {
254
274
  bases.add(endpoint);
275
+ this.addProviderNode(
276
+ this.providerNodePubkeysByUrl,
277
+ endpoint,
278
+ p?.pubkey || event.pubkey
279
+ );
255
280
  }
256
281
  }
257
282
  }
258
283
  } catch {
259
- this.logger.warn("NostrBootstrap: failed to parse event content:", event.id);
284
+ this.logger.warn(
285
+ "NostrBootstrap: failed to parse event content:",
286
+ event.id
287
+ );
260
288
  }
261
289
  }
262
290
  }
@@ -287,10 +315,12 @@ var ModelManager = class _ModelManager {
287
315
  const data = await res.json();
288
316
  const providers = Array.isArray(data?.providers) ? data.providers : [];
289
317
  const bases = /* @__PURE__ */ new Set();
318
+ this.providerNodePubkeysByUrl = /* @__PURE__ */ new Map();
290
319
  for (const p of providers) {
291
320
  const endpoints = this.getProviderEndpoints(p, torMode);
292
321
  for (const endpoint of endpoints) {
293
322
  bases.add(endpoint);
323
+ this.addProviderNode(this.providerNodePubkeysByUrl, endpoint, p?.pubkey);
294
324
  }
295
325
  }
296
326
  for (const url of this.includeProviderUrls) {
@@ -307,6 +337,7 @@ var ModelManager = class _ModelManager {
307
337
  this.adapter.setBaseUrlsList(list);
308
338
  this.adapter.setBaseUrlsLastUpdate(Date.now());
309
339
  await this.fetchRoutstr21Models(forceRefresh);
340
+ await this.syncReviewedProvidersFromNostr(list);
310
341
  }
311
342
  return list;
312
343
  } catch (e) {
@@ -314,6 +345,93 @@ var ModelManager = class _ModelManager {
314
345
  throw new ProviderBootstrapError([], `Provider bootstrap failed: ${e}`);
315
346
  }
316
347
  }
348
+ /**
349
+ * Fetch Routstr review events from Nostr (kind 38425) and disable providers
350
+ * whose 38421 node pubkey does not have at least one review tagged `t=lgtm`.
351
+ *
352
+ * Review events are expected to have:
353
+ * - `node`: the reviewed 38421 provider event pubkey
354
+ * - `t`: review label, where `lgtm` means the node looks good
355
+ *
356
+ * @param baseUrls Current provider base URLs to evaluate
357
+ * @returns Array of provider base URLs disabled by the review set
358
+ */
359
+ async syncReviewedProvidersFromNostr(baseUrls = this.adapter.getBaseUrlsList(), providerNodes = this.providerNodePubkeysByUrl) {
360
+ if (baseUrls.length === 0) return [];
361
+ if (!this.adapter.setDisabledProviders) {
362
+ this.logger.warn(
363
+ "NostrReviews: adapter does not support setDisabledProviders; skipping provider disable sync"
364
+ );
365
+ return [];
366
+ }
367
+ const LGTM_RELAYS = [
368
+ "wss://relay.primal.net",
369
+ "wss://nos.lol",
370
+ "wss://relay.damus.io",
371
+ "wss://relay.routstr.com"
372
+ ];
373
+ const reviewedNodePubkeys = /* @__PURE__ */ new Set();
374
+ {
375
+ const pool = new applesauceRelay.RelayPool();
376
+ const store = new applesauceCore.EventStore();
377
+ const timeoutMs = 5e3;
378
+ await new Promise((resolve) => {
379
+ pool.req(LGTM_RELAYS, {
380
+ kinds: [38425],
381
+ "#t": ["lgtm"],
382
+ limit: 500,
383
+ authors: [this.routstrPubkey]
384
+ }).pipe(
385
+ applesauceRelay.onlyEvents(),
386
+ rxjs.tap((event) => store.add(event))
387
+ ).subscribe({ complete: () => resolve() });
388
+ setTimeout(() => resolve(), timeoutMs);
389
+ });
390
+ for (const event of store.getTimeline({ kinds: [38425] })) {
391
+ const hasLgtmTag = event.tags.some(
392
+ (tag) => tag[0] === "t" && tag[1]?.toLowerCase() === "lgtm"
393
+ );
394
+ if (!hasLgtmTag) continue;
395
+ for (const tag of event.tags) {
396
+ if (tag[0] === "node" && typeof tag[1] === "string" && tag[1]) {
397
+ reviewedNodePubkeys.add(tag[1]);
398
+ }
399
+ }
400
+ }
401
+ }
402
+ if (reviewedNodePubkeys.size === 0) {
403
+ this.logger.warn(
404
+ "NostrReviews: no kind 38425 lgtm reviews found; keeping disabled providers unchanged"
405
+ );
406
+ return [];
407
+ }
408
+ if (providerNodes.size === 0) {
409
+ this.logger.warn(
410
+ "NostrReviews: no kind 38421 provider node metadata found; keeping disabled providers unchanged"
411
+ );
412
+ return [];
413
+ }
414
+ const disabledByReview = [];
415
+ for (const url of baseUrls) {
416
+ const normalized = this.normalizeUrl(url);
417
+ const nodePubkeys = providerNodes.get(normalized) || /* @__PURE__ */ new Set();
418
+ const hasLgtmReview = Array.from(nodePubkeys).some(
419
+ (pubkey) => reviewedNodePubkeys.has(pubkey)
420
+ );
421
+ if (!hasLgtmReview) {
422
+ disabledByReview.push(normalized);
423
+ }
424
+ }
425
+ this.adapter.setDisabledProviders(Array.from(new Set(disabledByReview)));
426
+ return disabledByReview;
427
+ }
428
+ addProviderNode(map, url, pubkey) {
429
+ if (!pubkey) return;
430
+ const normalized = this.normalizeUrl(url);
431
+ const existing = map.get(normalized) || /* @__PURE__ */ new Set();
432
+ existing.add(pubkey);
433
+ map.set(normalized, existing);
434
+ }
317
435
  /**
318
436
  * Fetch models from all providers and select best-priced options
319
437
  * Uses cache if available and not expired
@@ -506,9 +624,7 @@ var ModelManager = class _ModelManager {
506
624
  kinds: [38423],
507
625
  "#d": ["routstr-21-models"],
508
626
  limit: 1,
509
- authors: [
510
- "4ad6fa2d16e2a9b576c863b4cf7404a70d4dc320c0c447d10ad6ff58993eacc8"
511
- ]
627
+ authors: [this.routstrPubkey]
512
628
  }).pipe(
513
629
  applesauceRelay.onlyEvents(),
514
630
  rxjs.tap((event2) => {
@@ -535,7 +651,10 @@ var ModelManager = class _ModelManager {
535
651
  this.adapter.setRoutstr21ModelsLastUpdate(Date.now());
536
652
  return models;
537
653
  } catch {
538
- this.logger.warn("Routstr21Models: failed to parse Nostr event content:", event.id);
654
+ this.logger.warn(
655
+ "Routstr21Models: failed to parse Nostr event content:",
656
+ event.id
657
+ );
539
658
  return cachedModels.length > 0 ? cachedModels : [];
540
659
  }
541
660
  }
@@ -1221,19 +1340,50 @@ var CashuSpender = class {
1221
1340
  apiKeyEntry.baseUrl
1222
1341
  );
1223
1342
  if (apiKeyEntryFull && this.balanceManager) {
1343
+ try {
1344
+ const balanceResult = await this.balanceManager.getTokenBalance(
1345
+ apiKeyEntryFull.key,
1346
+ apiKeyEntry.baseUrl
1347
+ );
1348
+ if (balanceResult.isInvalidApiKey) {
1349
+ this.storageAdapter.removeApiKey(apiKeyEntry.baseUrl);
1350
+ results.push({
1351
+ baseUrl: apiKeyEntry.baseUrl,
1352
+ success: true
1353
+ });
1354
+ continue;
1355
+ }
1356
+ if (balanceResult.amount >= 0) {
1357
+ const balanceSat = balanceResult.unit === "msat" ? Math.floor(balanceResult.amount / 1e3) : balanceResult.amount;
1358
+ this.storageAdapter.updateApiKeyBalance(
1359
+ apiKeyEntry.baseUrl,
1360
+ balanceSat
1361
+ );
1362
+ }
1363
+ } catch {
1364
+ }
1365
+ const refreshedEntry = this.storageAdapter.getApiKey(
1366
+ apiKeyEntry.baseUrl
1367
+ );
1368
+ if (!refreshedEntry) continue;
1224
1369
  const refundResult = await this.balanceManager.refundApiKey({
1225
1370
  mintUrl,
1226
1371
  baseUrl: apiKeyEntry.baseUrl,
1227
- apiKey: apiKeyEntryFull.key,
1372
+ apiKey: refreshedEntry.key,
1228
1373
  forceRefund
1229
1374
  });
1230
1375
  if (refundResult.success) {
1231
1376
  this.storageAdapter.removeApiKey(apiKeyEntry.baseUrl);
1232
1377
  } else {
1233
- this.storageAdapter.updateApiKeyBalance(
1234
- apiKeyEntry.baseUrl,
1235
- apiKeyEntry.amount
1378
+ const currentEntry = this.storageAdapter.getApiKey(
1379
+ apiKeyEntry.baseUrl
1236
1380
  );
1381
+ if (currentEntry) {
1382
+ this.storageAdapter.updateApiKeyBalance(
1383
+ apiKeyEntry.baseUrl,
1384
+ currentEntry.balance
1385
+ );
1386
+ }
1237
1387
  }
1238
1388
  results.push({
1239
1389
  baseUrl: apiKeyEntry.baseUrl,
@@ -1442,7 +1592,8 @@ var BalanceManager = class _BalanceManager {
1442
1592
  };
1443
1593
  }
1444
1594
  if (fetchResult.error === "No balance to refund") {
1445
- return { success: false, message: "No balance to refund" };
1595
+ this.storageAdapter.removeApiKey(baseUrl);
1596
+ return { success: true, message: "No balance to refund, key cleaned up" };
1446
1597
  }
1447
1598
  const receiveResult = await this.cashuSpender.receiveToken(
1448
1599
  fetchResult.token
@@ -4110,7 +4261,10 @@ var hydrateStoreFromDriver = async (store, driver) => {
4110
4261
  driver.getItem(SDK_STORAGE_KEYS.CLIENT_IDS, []),
4111
4262
  driver.getItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, []),
4112
4263
  driver.getItem(SDK_STORAGE_KEYS.LAST_FAILED, {}),
4113
- driver.getItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, [])
4264
+ driver.getItem(
4265
+ SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN,
4266
+ []
4267
+ )
4114
4268
  ]);
4115
4269
  const modelsFromAllProviders = Object.fromEntries(
4116
4270
  Object.entries(rawModels).map(([baseUrl, models]) => [
@@ -4178,7 +4332,9 @@ var hydrateStoreFromDriver = async (store, driver) => {
4178
4332
  createdAt: entry.createdAt ?? Date.now(),
4179
4333
  lastUsed: entry.lastUsed ?? null
4180
4334
  }));
4181
- const failedProviders = rawFailedProviders.map((url) => normalizeBaseUrl5(url));
4335
+ const failedProviders = rawFailedProviders.map(
4336
+ (url) => normalizeBaseUrl5(url)
4337
+ );
4182
4338
  const lastFailed = Object.fromEntries(
4183
4339
  Object.entries(rawLastFailed).map(([baseUrl, timestamp]) => [
4184
4340
  normalizeBaseUrl5(baseUrl),
@@ -4240,6 +4396,7 @@ var createDiscoveryAdapterFromStore = (store) => ({
4240
4396
  getLastUsedModel: () => store.getState().lastUsedModel,
4241
4397
  setLastUsedModel: (modelId) => store.getState().setLastUsedModel(modelId),
4242
4398
  getDisabledProviders: () => store.getState().disabledProviders,
4399
+ setDisabledProviders: (urls) => store.getState().setDisabledProviders(urls),
4243
4400
  getBaseUrlsList: () => store.getState().baseUrlsList,
4244
4401
  setBaseUrlsList: (urls) => store.getState().setBaseUrlsList(urls),
4245
4402
  getBaseUrlsLastUpdate: () => store.getState().lastBaseUrlsUpdate,
@@ -4255,9 +4412,7 @@ var createStorageAdapterFromStore = (store) => ({
4255
4412
  const distributionMap = {};
4256
4413
  for (const entry of apiKeys) {
4257
4414
  const sum = entry.balance || 0;
4258
- if (sum > 0) {
4259
- distributionMap[entry.baseUrl] = (distributionMap[entry.baseUrl] || 0) + sum;
4260
- }
4415
+ distributionMap[entry.baseUrl] = (distributionMap[entry.baseUrl] || 0) + sum;
4261
4416
  }
4262
4417
  return Object.entries(distributionMap).map(([baseUrl, amt]) => ({ baseUrl, amount: amt })).sort((a, b) => b.amount - a.amount);
4263
4418
  },
@@ -5914,6 +6069,7 @@ async function resolveRouteRequestContext(options) {
5914
6069
  } else {
5915
6070
  modelManager = new ModelManager(discoveryAdapter, {
5916
6071
  includeProviderUrls: forcedProvider ? [forcedProvider, ...includeProviderUrls] : includeProviderUrls,
6072
+ routstrPubkey: options.routstrPubkey,
5917
6073
  logger
5918
6074
  });
5919
6075
  providers = await modelManager.bootstrapProviders(torMode);