@opennodes/registry 0.1.22 → 0.1.24

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opennodes/registry",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "description": "OpenNodes (ONP) reference registry: node registration with proof-of-resource qualification (Stage A/B/C), transparent ranking, enforceable estimates, catalog importers (OpenRouter, Hugging Face, models.dev, Ollama), MCP surface, and the web app (catalog, chat, advisor, Node Builder)",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
package/src/search.js CHANGED
@@ -51,8 +51,11 @@ async function buildOfferings(store) {
51
51
  const card = await store.latestCard(node.id);
52
52
  if (!card) continue;
53
53
  const probes = await store.probesFor(node.id);
54
- const stageA = probes.filter((p) => p.stage === 'A');
55
- const probeSuccess = stageA.length ? stageA.filter((p) => p.ok).length / stageA.length : 0;
54
+ // Pass rate over the recent window of conformance (A) and continuous (C) checks. Stage A
55
+ // alone runs every 6 h and is pushed out of the window by liveness checks within hours,
56
+ // which left healthy verified nodes showing 0 probes and a 0 pass rate.
57
+ const checks = probes.filter((p) => p.stage === 'A' || p.stage === 'C');
58
+ const probeSuccess = checks.length ? checks.filter((p) => p.ok).length / checks.length : 0;
56
59
  const tier = node.state === 'indexed' ? 'unverified' : node.state;
57
60
  const nodeObs = await store.getObservations(node.id, '_node');
58
61
  const availability = nodeObs.availability ?? null;
@@ -102,7 +105,7 @@ async function buildOfferings(store) {
102
105
  benchmarks: offering.benchmarks ?? null,
103
106
  observed: {
104
107
  probe_success: round2(probeSuccess),
105
- probes: stageA.length,
108
+ probes: checks.length,
106
109
  ttft_ms: obs.ttft_ms ?? null,
107
110
  tps: obs.tps ?? null,
108
111
  availability,
@@ -152,10 +155,14 @@ export async function searchOfferingsPaged(store, query) {
152
155
 
153
156
  // Rank depends on the filtered set: annotate copies, never the shared cached offerings.
154
157
  items = items.map((o) => ({ ...o }));
155
- const maxPrice = Math.max(...items.map((o) => o.pricing.input_per_mtok ?? 0), 0.000001);
158
+ // Price scores by position among the results (cheapest 1, dearest 0), not against the single
159
+ // most expensive offering: one outlier made almost everything "cheap" and every import tie.
160
+ const prices = items.map((o) => o.pricing.input_per_mtok).filter((p) => p != null).sort((a, b) => a - b);
161
+ const cheaperThan = (p) => { let lo = 0, hi = prices.length; while (lo < hi) { const m = (lo + hi) >> 1; if (prices[m] < p) lo = m + 1; else hi = m; } return lo; };
156
162
  for (const o of items) {
163
+ const p = o.pricing.input_per_mtok;
157
164
  const components = {
158
- price: 1 - (o.pricing.input_per_mtok ?? maxPrice) / maxPrice,
165
+ price: p == null ? 0 : prices.length > 1 ? 1 - cheaperThan(p) / (prices.length - 1) : 1,
159
166
  tier: TIER_SCORE[o.tier] ?? 0,
160
167
  probe_success: o.observed.probe_success,
161
168
  };
@@ -172,11 +179,17 @@ export async function searchOfferingsPaged(store, query) {
172
179
  if (sort === 'price') return (a.pricing.input_per_mtok ?? Infinity) - (b.pricing.input_per_mtok ?? Infinity);
173
180
  if (sort === 'ttft') return ttftOf(a) - ttftOf(b);
174
181
  if (sort === 'tps') return tpsOf(b) - tpsOf(a);
175
- return b.rank - a.rank;
182
+ // Equal rank: callable before listing-only, larger context first, then by name — a stable,
183
+ // meaningful order instead of whatever order the catalogs were imported in.
184
+ return b.rank - a.rank || Number(b.invocable) - Number(a.invocable)
185
+ || (b.serving?.context_window ?? 0) - (a.serving?.context_window ?? 0)
186
+ || String(a.model?.name ?? '').localeCompare(String(b.model?.name ?? ''))
187
+ || `${a.node_id}/${a.offering_id}`.localeCompare(`${b.node_id}/${b.offering_id}`);
176
188
  });
177
189
 
178
190
  const limit = Math.min(Number(q('limit') ?? 50), 200);
179
- return { offerings: items.slice(0, limit), total: items.length };
191
+ const offset = Math.max(0, Math.floor(Number(q('offset') ?? 0)) || 0);
192
+ return { offerings: items.slice(offset, offset + limit), total: items.length, offset };
180
193
  }
181
194
 
182
195
  export async function searchOfferings(store, query) {
package/src/server.js CHANGED
@@ -350,8 +350,8 @@ export async function startRegistry({
350
350
  }],
351
351
 
352
352
  ['GET', '/v0/offerings', async (req, res, { query }) => {
353
- const { offerings, total } = await searchOfferingsPaged(store, query);
354
- sendJson(res, 200, { offerings, total }); // total = matches before `limit`
353
+ const { offerings, total, offset } = await searchOfferingsPaged(store, query);
354
+ sendJson(res, 200, { offerings, total, offset }); // total = matches before `offset`/`limit`
355
355
  }],
356
356
 
357
357
  ['GET', '/v0/offerings/:nodeId/:offeringId', async (req, res, { params }) => {
@@ -606,8 +606,39 @@ function provLine(o) {
606
606
  return bits.join(' · ');
607
607
  }
608
608
 
609
+ const PAGE = 60;
610
+ let listQuery = '', listTotal = 0;
611
+
612
+ // Count line and the "Show more" button at the end of the list.
613
+ function updateListFooter() {
614
+ $('#m-more')?.remove();
615
+ const shown = lastResults.length;
616
+ $('#m-counts').textContent = fmtInt(listTotal) + ' offerings' + (shown < listTotal ? ` · showing 1–${fmtInt(shown)}` : '');
617
+ if (shown < listTotal) {
618
+ $('#m-list').insertAdjacentHTML('beforeend', `<button class="btn" id="m-more" style="background:var(--panel); color:var(--accent); align-self:stretch">Show ${fmtInt(Math.min(PAGE, listTotal - shown))} more</button>`);
619
+ $('#m-more').addEventListener('click', loadMore);
620
+ }
621
+ }
622
+
623
+ async function loadMore() {
624
+ const btn = $('#m-more');
625
+ btn.disabled = true;
626
+ const query = listQuery;
627
+ const p = new URLSearchParams(query);
628
+ p.set('offset', lastResults.length);
629
+ try {
630
+ const { offerings } = await api('/v0/offerings?' + p);
631
+ if (query !== listQuery) return; // the filters changed meanwhile
632
+ const start = lastResults.length;
633
+ lastResults = lastResults.concat(offerings);
634
+ btn.insertAdjacentHTML('beforebegin', offerings.map(renderItem).join(''));
635
+ [...document.querySelectorAll('#m-list .mitem')].slice(start).forEach((el) => el.addEventListener('click', () => selectModel(el)));
636
+ updateListFooter();
637
+ } catch (e) { btn.disabled = false; btn.textContent = 'Retry — ' + e.message; }
638
+ }
639
+
609
640
  async function search() {
610
- const p = new URLSearchParams({ limit: 60 });
641
+ const p = new URLSearchParams({ limit: PAGE });
611
642
  if ($('#f-q').value) p.set('q', $('#f-q').value);
612
643
  if ($('#f-modality').value) p.set('modality', $('#f-modality').value);
613
644
  if ($('#f-verified').classList.contains('on')) p.set('tier', 'verified');
@@ -615,13 +646,15 @@ async function search() {
615
646
  if ($('#f-price').value) p.set('max_input_price', $('#f-price').value);
616
647
  if (sup.size) p.set('supports', [...sup].join(','));
617
648
  p.set('sort', $('#f-sort').value);
649
+ listQuery = p.toString();
618
650
  try {
619
651
  const { offerings, total } = await api('/v0/offerings?' + p);
652
+ if (listQuery !== p.toString()) return; // a newer search is under way
620
653
  lastResults = offerings;
621
- const n = total ?? offerings.length;
622
- $('#m-counts').textContent = fmtInt(n) + ' offerings' + (offerings.length < n ? ` · showing top ${offerings.length}` : '');
654
+ listTotal = total ?? offerings.length;
623
655
  $('#m-list').innerHTML = offerings.map(renderItem).join('')
624
656
  || '<p class="sub">No offerings match. A fresh registry fills up via catalog imports and node registrations.</p>';
657
+ updateListFooter();
625
658
  document.querySelectorAll('.mitem').forEach((el) => el.addEventListener('click', () => selectModel(el)));
626
659
  // keep the current selection when still listed, else select the first result
627
660
  const current = $('#model-card')?.dataset.artifact;
@@ -1780,13 +1813,14 @@ function claimBanner(n, card) {
1780
1813
  }
1781
1814
 
1782
1815
  /** Server-location check (registry location.js) in plain words. */
1783
- function locationRow(loc) {
1816
+ function locationRow(loc, imported) {
1784
1817
  if (!loc) return '';
1785
1818
  const srv = loc.server || {};
1819
+ const claim = imported ? 'inferred for this unclaimed listing' : 'declared by the operator';
1786
1820
  const words = {
1787
- match: `✓ server in ${esc(srv.country)}, as declared`,
1788
- same_region: `✓ server in ${esc(srv.country)} (EEA); operator declared ${esc(loc.declared)}`,
1789
- mismatch: `<b style="color:var(--bad, #b42318)">⚠ server in ${esc(srv.country)}, operator declared ${esc(loc.declared)}</b>`,
1821
+ match: `✓ server in ${esc(srv.country)}, matching the country ${imported ? 'inferred for this unclaimed listing' : 'the operator declared'}`,
1822
+ same_region: `✓ server in ${esc(srv.country)} (EEA); country ${claim}: ${esc(loc.declared)}`,
1823
+ mismatch: `<b style="color:var(--bad, #b42318)">⚠ server in ${esc(srv.country)}; country ${claim}: ${esc(loc.declared)}</b>`,
1790
1824
  cdn: `behind a CDN (${esc(srv.network || 'anycast')}) — location not determinable from the network`,
1791
1825
  inconsistent: `<b style="color:var(--bad, #b42318)">⚠ geolocation (${esc(srv.country || '?')}) contradicted by measured latency</b>`,
1792
1826
  unknown: `not determined${loc.reason ? ' — ' + esc(loc.reason) : ''}`,
@@ -1830,7 +1864,7 @@ async function loadNode(id) {
1830
1864
  <div class="panel pad" style="max-width:56rem; margin-top:0.6rem"><table class="grid">
1831
1865
  <tr><td style="color:var(--muted); width:8rem" class="mono">endpoint</td><td class="mono" style="font-size:0.74rem">${card.endpoints?.openai ? esc(card.endpoints.openai) : '— <span style="font-family:inherit; color:var(--muted)">listing only: no known API endpoint, not callable from OpenNodes</span>'}</td></tr>
1832
1866
  ${docsUrl(card.endpoints) ? `<tr><td style="color:var(--muted)" class="mono">docs</td><td class="mono" style="font-size:0.74rem"><a href="${esc(docsUrl(card.endpoints))}" target="_blank" rel="noopener">${esc(docsUrl(card.endpoints))}</a></td></tr>` : ''}
1833
- ${locationRow(n.location)}
1867
+ ${locationRow(n.location, n.source === 'import')}
1834
1868
  ${card.hardware ? `<tr><td style="color:var(--muted)" class="mono">hardware</td><td style="font-size:0.78rem">${hwStr({ hardware: card.hardware })}</td></tr>` : ''}
1835
1869
  <tr><td style="color:var(--muted)" class="mono">revision</td><td class="mono" style="font-size:0.74rem">${esc(card.revision ?? '—')}</td></tr>
1836
1870
  </table></div>