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