@routstr/sdk 0.3.5 → 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
  }
@@ -4142,7 +4261,10 @@ var hydrateStoreFromDriver = async (store, driver) => {
4142
4261
  driver.getItem(SDK_STORAGE_KEYS.CLIENT_IDS, []),
4143
4262
  driver.getItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, []),
4144
4263
  driver.getItem(SDK_STORAGE_KEYS.LAST_FAILED, {}),
4145
- driver.getItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, [])
4264
+ driver.getItem(
4265
+ SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN,
4266
+ []
4267
+ )
4146
4268
  ]);
4147
4269
  const modelsFromAllProviders = Object.fromEntries(
4148
4270
  Object.entries(rawModels).map(([baseUrl, models]) => [
@@ -4210,7 +4332,9 @@ var hydrateStoreFromDriver = async (store, driver) => {
4210
4332
  createdAt: entry.createdAt ?? Date.now(),
4211
4333
  lastUsed: entry.lastUsed ?? null
4212
4334
  }));
4213
- const failedProviders = rawFailedProviders.map((url) => normalizeBaseUrl5(url));
4335
+ const failedProviders = rawFailedProviders.map(
4336
+ (url) => normalizeBaseUrl5(url)
4337
+ );
4214
4338
  const lastFailed = Object.fromEntries(
4215
4339
  Object.entries(rawLastFailed).map(([baseUrl, timestamp]) => [
4216
4340
  normalizeBaseUrl5(baseUrl),
@@ -4272,6 +4396,7 @@ var createDiscoveryAdapterFromStore = (store) => ({
4272
4396
  getLastUsedModel: () => store.getState().lastUsedModel,
4273
4397
  setLastUsedModel: (modelId) => store.getState().setLastUsedModel(modelId),
4274
4398
  getDisabledProviders: () => store.getState().disabledProviders,
4399
+ setDisabledProviders: (urls) => store.getState().setDisabledProviders(urls),
4275
4400
  getBaseUrlsList: () => store.getState().baseUrlsList,
4276
4401
  setBaseUrlsList: (urls) => store.getState().setBaseUrlsList(urls),
4277
4402
  getBaseUrlsLastUpdate: () => store.getState().lastBaseUrlsUpdate,
@@ -5944,6 +6069,7 @@ async function resolveRouteRequestContext(options) {
5944
6069
  } else {
5945
6070
  modelManager = new ModelManager(discoveryAdapter, {
5946
6071
  includeProviderUrls: forcedProvider ? [forcedProvider, ...includeProviderUrls] : includeProviderUrls,
6072
+ routstrPubkey: options.routstrPubkey,
5947
6073
  logger
5948
6074
  });
5949
6075
  providers = await modelManager.bootstrapProviders(torMode);