@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.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
  }
@@ -4140,7 +4259,10 @@ var hydrateStoreFromDriver = async (store, driver) => {
4140
4259
  driver.getItem(SDK_STORAGE_KEYS.CLIENT_IDS, []),
4141
4260
  driver.getItem(SDK_STORAGE_KEYS.FAILED_PROVIDERS, []),
4142
4261
  driver.getItem(SDK_STORAGE_KEYS.LAST_FAILED, {}),
4143
- driver.getItem(SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN, [])
4262
+ driver.getItem(
4263
+ SDK_STORAGE_KEYS.PROVIDERS_ON_COOLDOWN,
4264
+ []
4265
+ )
4144
4266
  ]);
4145
4267
  const modelsFromAllProviders = Object.fromEntries(
4146
4268
  Object.entries(rawModels).map(([baseUrl, models]) => [
@@ -4208,7 +4330,9 @@ var hydrateStoreFromDriver = async (store, driver) => {
4208
4330
  createdAt: entry.createdAt ?? Date.now(),
4209
4331
  lastUsed: entry.lastUsed ?? null
4210
4332
  }));
4211
- const failedProviders = rawFailedProviders.map((url) => normalizeBaseUrl5(url));
4333
+ const failedProviders = rawFailedProviders.map(
4334
+ (url) => normalizeBaseUrl5(url)
4335
+ );
4212
4336
  const lastFailed = Object.fromEntries(
4213
4337
  Object.entries(rawLastFailed).map(([baseUrl, timestamp]) => [
4214
4338
  normalizeBaseUrl5(baseUrl),
@@ -4270,6 +4394,7 @@ var createDiscoveryAdapterFromStore = (store) => ({
4270
4394
  getLastUsedModel: () => store.getState().lastUsedModel,
4271
4395
  setLastUsedModel: (modelId) => store.getState().setLastUsedModel(modelId),
4272
4396
  getDisabledProviders: () => store.getState().disabledProviders,
4397
+ setDisabledProviders: (urls) => store.getState().setDisabledProviders(urls),
4273
4398
  getBaseUrlsList: () => store.getState().baseUrlsList,
4274
4399
  setBaseUrlsList: (urls) => store.getState().setBaseUrlsList(urls),
4275
4400
  getBaseUrlsLastUpdate: () => store.getState().lastBaseUrlsUpdate,
@@ -5942,6 +6067,7 @@ async function resolveRouteRequestContext(options) {
5942
6067
  } else {
5943
6068
  modelManager = new ModelManager(discoveryAdapter, {
5944
6069
  includeProviderUrls: forcedProvider ? [forcedProvider, ...includeProviderUrls] : includeProviderUrls,
6070
+ routstrPubkey: options.routstrPubkey,
5945
6071
  logger
5946
6072
  });
5947
6073
  providers = await modelManager.bootstrapProviders(torMode);