@opennodes/registry 0.1.23 → 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 +1 -1
- package/src/search.js +20 -7
- package/src/server.js +2 -2
- package/src/web/index.html +36 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opennodes/registry",
|
|
3
|
-
"version": "0.1.
|
|
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
|
-
|
|
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
|
@@ -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:
|
|
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
|
-
|
|
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;
|