@opennodes/registry 0.1.23 → 0.1.25
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 +1 -1
- package/src/search.js +20 -7
- package/src/server.js +2 -2
- package/src/web/index.html +59 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opennodes/registry",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.25",
|
|
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
|
-
|
|
55
|
-
|
|
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:
|
|
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
|
-
|
|
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 - (
|
|
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
|
-
|
|
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
|
-
|
|
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 }) => {
|
package/src/web/index.html
CHANGED
|
@@ -47,6 +47,12 @@
|
|
|
47
47
|
.mitem .t .nm { font-weight: 600; font-size: 0.86rem; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
48
48
|
.mitem .b { display: flex; gap: 0.8rem; margin-top: 0.25rem; font-size: 0.7rem; color: var(--muted); font-variant-numeric: tabular-nums; flex-wrap: wrap; }
|
|
49
49
|
.mdetail { min-height: 30rem; }
|
|
50
|
+
.pager { display: flex; align-items: center; justify-content: center; gap: 0.3rem; margin-top: 0.5rem; font-size: 0.78rem; color: var(--muted); flex-wrap: wrap; }
|
|
51
|
+
.pager:empty { display: none; }
|
|
52
|
+
.pgbtn { font: inherit; min-width: 2rem; padding: 0.25rem 0.5rem; border-radius: 6px; border: 1px solid var(--line); background: var(--panel); color: var(--accent); cursor: pointer; }
|
|
53
|
+
.pgbtn:disabled { color: var(--faint); cursor: default; }
|
|
54
|
+
.pgnum { padding: 0 0.3rem; }
|
|
55
|
+
.pgnum input { width: 4rem; font: inherit; padding: 0.2rem 0.3rem; border: 1px solid var(--line); border-radius: 6px; background: var(--panel); color: var(--ink); text-align: center; }
|
|
50
56
|
|
|
51
57
|
/* chat */
|
|
52
58
|
.chatgrid { display: grid; grid-template-columns: minmax(0, 1fr) 19rem; gap: 1.2rem; align-items: start; }
|
|
@@ -244,7 +250,7 @@
|
|
|
244
250
|
<div id="adv-out"></div>
|
|
245
251
|
</details>
|
|
246
252
|
<div class="mgrid">
|
|
247
|
-
<div class="mlist" id="m-list"></div>
|
|
253
|
+
<div class="mcol"><div class="mlist" id="m-list"></div><div class="pager" id="m-pager"></div></div>
|
|
248
254
|
<div class="mdetail" id="m-detail"><p class="sub">Select a model from the list.</p></div>
|
|
249
255
|
</div>
|
|
250
256
|
</div>
|
|
@@ -606,8 +612,52 @@ function provLine(o) {
|
|
|
606
612
|
return bits.join(' · ');
|
|
607
613
|
}
|
|
608
614
|
|
|
615
|
+
const PAGE = 60;
|
|
616
|
+
let listQuery = '', listTotal = 0, listPage = 0;
|
|
617
|
+
|
|
618
|
+
// Count line and page controls under the list: first / previous / page N of M / next / last.
|
|
619
|
+
function updatePager() {
|
|
620
|
+
const pages = Math.max(1, Math.ceil(listTotal / PAGE));
|
|
621
|
+
const from = listTotal ? listPage * PAGE + 1 : 0, to = Math.min(listTotal, (listPage + 1) * PAGE);
|
|
622
|
+
$('#m-counts').textContent = fmtInt(listTotal) + ' offerings' + (pages > 1 ? ` · ${fmtInt(from)}–${fmtInt(to)}` : '');
|
|
623
|
+
const pager = $('#m-pager');
|
|
624
|
+
if (pages <= 1) { pager.innerHTML = ''; return; }
|
|
625
|
+
const btn = (label, page, title) => `<button class="pgbtn" data-page="${page}" title="${title}"${page < 0 || page >= pages || page === listPage ? ' disabled' : ''}>${label}</button>`;
|
|
626
|
+
pager.innerHTML = btn('«', 0, 'first page') + btn('‹', listPage - 1, 'previous page')
|
|
627
|
+
+ `<span class="pgnum">Page <input id="m-page" type="number" min="1" max="${pages}" value="${listPage + 1}" aria-label="page number"> of ${fmtInt(pages)}</span>`
|
|
628
|
+
+ btn('›', listPage + 1, 'next page') + btn('»', pages - 1, 'last page');
|
|
629
|
+
pager.querySelectorAll('.pgbtn').forEach((b) => b.addEventListener('click', () => goToPage(Number(b.dataset.page))));
|
|
630
|
+
$('#m-page').addEventListener('change', (e) => goToPage(Math.min(pages, Math.max(1, Math.round(Number(e.target.value) || 1))) - 1));
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
async function goToPage(page) {
|
|
634
|
+
const query = listQuery;
|
|
635
|
+
const p = new URLSearchParams(query);
|
|
636
|
+
p.set('offset', page * PAGE);
|
|
637
|
+
$('#m-pager').querySelectorAll('button, input').forEach((el) => { el.disabled = true; });
|
|
638
|
+
try {
|
|
639
|
+
const { offerings, total } = await api('/v0/offerings?' + p);
|
|
640
|
+
if (query !== listQuery) return; // the filters changed meanwhile
|
|
641
|
+
listPage = page;
|
|
642
|
+
listTotal = total ?? listTotal;
|
|
643
|
+
lastResults = offerings;
|
|
644
|
+
renderList(offerings);
|
|
645
|
+
$('#m-list').scrollTop = 0;
|
|
646
|
+
updatePager();
|
|
647
|
+
} catch (e) { updatePager(); $('#m-pager').insertAdjacentHTML('beforeend', '<span class="err">' + esc(e.message) + '</span>'); }
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Render one page of the list, keeping the highlighted model when it is on this page.
|
|
651
|
+
function renderList(offerings) {
|
|
652
|
+
$('#m-list').innerHTML = offerings.map(renderItem).join('')
|
|
653
|
+
|| '<p class="sub">No offerings match. A fresh registry fills up via catalog imports and node registrations.</p>';
|
|
654
|
+
document.querySelectorAll('.mitem').forEach((el) => el.addEventListener('click', () => selectModel(el)));
|
|
655
|
+
const current = $('#model-card')?.dataset.artifact;
|
|
656
|
+
document.querySelectorAll('.mitem').forEach((el) => el.classList.toggle('sel', el.dataset.artifact === current));
|
|
657
|
+
}
|
|
658
|
+
|
|
609
659
|
async function search() {
|
|
610
|
-
const p = new URLSearchParams({ limit:
|
|
660
|
+
const p = new URLSearchParams({ limit: PAGE });
|
|
611
661
|
if ($('#f-q').value) p.set('q', $('#f-q').value);
|
|
612
662
|
if ($('#f-modality').value) p.set('modality', $('#f-modality').value);
|
|
613
663
|
if ($('#f-verified').classList.contains('on')) p.set('tier', 'verified');
|
|
@@ -615,21 +665,22 @@ async function search() {
|
|
|
615
665
|
if ($('#f-price').value) p.set('max_input_price', $('#f-price').value);
|
|
616
666
|
if (sup.size) p.set('supports', [...sup].join(','));
|
|
617
667
|
p.set('sort', $('#f-sort').value);
|
|
668
|
+
listQuery = p.toString();
|
|
618
669
|
try {
|
|
619
670
|
const { offerings, total } = await api('/v0/offerings?' + p);
|
|
671
|
+
if (listQuery !== p.toString()) return; // a newer search is under way
|
|
620
672
|
lastResults = offerings;
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
document.querySelectorAll('.mitem').forEach((el) => el.addEventListener('click', () => selectModel(el)));
|
|
673
|
+
listTotal = total ?? offerings.length;
|
|
674
|
+
listPage = 0;
|
|
675
|
+
renderList(offerings);
|
|
676
|
+
updatePager();
|
|
626
677
|
// keep the current selection when still listed, else select the first result
|
|
627
678
|
const current = $('#model-card')?.dataset.artifact;
|
|
628
679
|
const keep = current && [...document.querySelectorAll('.mitem')].find((el) => el.dataset.artifact === current);
|
|
629
680
|
const target = keep ?? document.querySelector('.mitem');
|
|
630
681
|
if (target) selectModel(target);
|
|
631
682
|
else $('#m-detail').innerHTML = '<p class="sub">No offerings match.</p>';
|
|
632
|
-
} catch (e) { $('#m-list').innerHTML = '<p class="err">' + esc(e.message) + '</p>'; }
|
|
683
|
+
} catch (e) { $('#m-list').innerHTML = '<p class="err">' + esc(e.message) + '</p>'; $('#m-pager').innerHTML = ''; }
|
|
633
684
|
}
|
|
634
685
|
|
|
635
686
|
/* ---------- advisor (ONP-3 §5b) ---------- */
|