@opennodes/registry 0.1.16 → 0.1.18
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 +2 -2
- package/src/search.js +4 -0
- package/src/web/index.html +37 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opennodes/registry",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
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",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"access": "public"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@opennodes/core": "0.1.
|
|
46
|
+
"@opennodes/core": "0.1.3",
|
|
47
47
|
"pg": "^8.13.1"
|
|
48
48
|
}
|
|
49
49
|
}
|
package/src/search.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Offering search + transparent ranking (ONP-3 §5–6).
|
|
2
|
+
import { inJurisdiction } from '@opennodes/core';
|
|
2
3
|
|
|
3
4
|
export const RANK_COMPONENTS = { price: 0.35, tier: 0.35, probe_success: 0.30 };
|
|
4
5
|
const TIER_SCORE = { unverified: 0, community: 0.4, verified: 0.8, attested: 1.0 };
|
|
@@ -39,6 +40,8 @@ export async function collectOfferings(store) {
|
|
|
39
40
|
binding: offering.binding,
|
|
40
41
|
pricing: offering.pricing,
|
|
41
42
|
data_policy: offering.data_policy ?? null,
|
|
43
|
+
// Where the operator says it is (ONP-2 node.operator.country, node.regions); imports carry none.
|
|
44
|
+
jurisdiction: { country: card.node?.operator?.country ?? null, regions: card.node?.regions ?? [] },
|
|
42
45
|
// Allocated hardware for THIS offering (offering-level override wins over the
|
|
43
46
|
// node-level description) — same artifact on different vendors runs on different
|
|
44
47
|
// silicon, and clients deserve to see which. Always basis-labeled, never measured.
|
|
@@ -76,6 +79,7 @@ export async function searchOfferingsPaged(store, query) {
|
|
|
76
79
|
}
|
|
77
80
|
if (q('max_input_price')) items = items.filter((o) => (o.pricing.input_per_mtok ?? Infinity) <= Number(q('max_input_price')));
|
|
78
81
|
if (q('scheme')) items = items.filter((o) => o.pricing.schemes.includes(q('scheme')));
|
|
82
|
+
if (q('jurisdiction')) items = items.filter((o) => inJurisdiction(o, q('jurisdiction')));
|
|
79
83
|
if (q('tier')) {
|
|
80
84
|
const order = ['unverified', 'community', 'verified', 'attested'];
|
|
81
85
|
const min = order.indexOf(q('tier'));
|
package/src/web/index.html
CHANGED
|
@@ -219,6 +219,7 @@
|
|
|
219
219
|
<div class="toolbar">
|
|
220
220
|
<input type="text" id="f-q" placeholder="Search models, families, nodes…">
|
|
221
221
|
<span class="switchrow" id="f-verified" title="identity-fingerprinted · context-verified · continuously probed"><span class="switch"></span><b style="font-size:0.78rem">Verified only</b></span>
|
|
222
|
+
<span class="switchrow" id="f-eea" title="operators that declare a country in the EU, Iceland, Liechtenstein or Norway; imported listings declare none"><span class="switch"></span><b style="font-size:0.78rem">EU/EEA</b></span>
|
|
222
223
|
<select id="f-modality">
|
|
223
224
|
<option value="">any modality</option>
|
|
224
225
|
<option>text</option><option>multimodal</option><option>image</option>
|
|
@@ -236,6 +237,7 @@
|
|
|
236
237
|
<div class="toolbar" style="margin-top:0.4rem">
|
|
237
238
|
<select id="adv-preset"><option value="">balanced</option><option value="cheap">cheapest</option><option value="fast">fastest</option><option value="quality">best quality</option><option value="private">privacy-first</option></select>
|
|
238
239
|
<span class="switchrow" id="adv-verified"><span class="switch"></span><b style="font-size:0.78rem">Verified only</b></span>
|
|
240
|
+
<span class="switchrow" id="adv-eea" title="only operators that declare an EU/EEA country"><span class="switch"></span><b style="font-size:0.78rem">EU/EEA</b></span>
|
|
239
241
|
<button class="btn" id="adv-go">Recommend</button>
|
|
240
242
|
<span class="prov">Deterministic: same catalog + task + policy → same answer, with the score components shown. The <code>onp mcp</code> / gateway advisors extract features locally and never send your prompt; this box sends the text to this registry.</span>
|
|
241
243
|
</div>
|
|
@@ -556,6 +558,7 @@ document.querySelectorAll('.toggle').forEach((b) => b.addEventListener('click',
|
|
|
556
558
|
b.classList.toggle('on'); b.classList.contains('on') ? sup.add(b.dataset.sup) : sup.delete(b.dataset.sup); search();
|
|
557
559
|
}));
|
|
558
560
|
$('#f-verified').addEventListener('click', () => { $('#f-verified').classList.toggle('on'); search(); });
|
|
561
|
+
$('#f-eea').addEventListener('click', () => { $('#f-eea').classList.toggle('on'); search(); });
|
|
559
562
|
let debounce;
|
|
560
563
|
for (const id of ['f-q', 'f-price']) $('#' + id).addEventListener('input', () => { clearTimeout(debounce); debounce = setTimeout(search, 250); });
|
|
561
564
|
for (const id of ['f-modality', 'f-sort']) $('#' + id).addEventListener('change', search);
|
|
@@ -608,6 +611,7 @@ async function search() {
|
|
|
608
611
|
if ($('#f-q').value) p.set('q', $('#f-q').value);
|
|
609
612
|
if ($('#f-modality').value) p.set('modality', $('#f-modality').value);
|
|
610
613
|
if ($('#f-verified').classList.contains('on')) p.set('tier', 'verified');
|
|
614
|
+
if ($('#f-eea').classList.contains('on')) p.set('jurisdiction', 'eea');
|
|
611
615
|
if ($('#f-price').value) p.set('max_input_price', $('#f-price').value);
|
|
612
616
|
if (sup.size) p.set('supports', [...sup].join(','));
|
|
613
617
|
p.set('sort', $('#f-sort').value);
|
|
@@ -630,6 +634,7 @@ async function search() {
|
|
|
630
634
|
|
|
631
635
|
/* ---------- advisor (ONP-3 §5b) ---------- */
|
|
632
636
|
$('#adv-verified').addEventListener('click', () => $('#adv-verified').classList.toggle('on'));
|
|
637
|
+
$('#adv-eea').addEventListener('click', () => $('#adv-eea').classList.toggle('on'));
|
|
633
638
|
$('#adv-go').addEventListener('click', runAdvisor);
|
|
634
639
|
$('#adv-task').addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) runAdvisor(); });
|
|
635
640
|
async function runAdvisor() {
|
|
@@ -638,6 +643,7 @@ async function runAdvisor() {
|
|
|
638
643
|
const policy = { limit: 3 };
|
|
639
644
|
if ($('#adv-preset').value) policy.preset = $('#adv-preset').value;
|
|
640
645
|
if ($('#adv-verified').classList.contains('on')) policy.min_tier = 'verified';
|
|
646
|
+
if ($('#adv-eea').classList.contains('on')) policy.jurisdiction = 'eea';
|
|
641
647
|
$('#adv-out').innerHTML = '<p class="sub">ranking…</p>';
|
|
642
648
|
try {
|
|
643
649
|
const r = await api('/v0/recommend', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ task, policy }) });
|
|
@@ -1743,6 +1749,36 @@ async function loadFeed() {
|
|
|
1743
1749
|
}
|
|
1744
1750
|
|
|
1745
1751
|
/* ---------- node detail ---------- */
|
|
1752
|
+
/**
|
|
1753
|
+
* Imported listings invite their operator to claim them (ONP-3 §4: a registered node whose
|
|
1754
|
+
* namespace covers the import's API host supersedes it). Providers reached through the HF router
|
|
1755
|
+
* can't claim via huggingface.co, so they are invited to list their own API instead.
|
|
1756
|
+
*/
|
|
1757
|
+
const CLAIM_GUIDE = 'https://github.com/opennodes-io/opennodes/tree/main/node-kit#card-only-listing-you-already-run-an-openai-compatible-api';
|
|
1758
|
+
function claimBanner(n, card) {
|
|
1759
|
+
if (n.source !== 'import' || ['delisted', 'claimed'].includes(n.state)) return '';
|
|
1760
|
+
const name = card.node?.operator?.name || card.node?.name || n.id;
|
|
1761
|
+
let host = null;
|
|
1762
|
+
try { host = new URL(card.endpoints?.openai).hostname; } catch { /* listing without an API */ }
|
|
1763
|
+
const viaHf = host === 'router.huggingface.co';
|
|
1764
|
+
const suggestedId = host && !viaHf ? host.split('.').reverse().join('.') : null;
|
|
1765
|
+
const body = viaHf
|
|
1766
|
+
? `This listing reaches <b>${esc(name)}</b> through the Hugging Face router, so it is shown as an unprobed catalog import. List your own API directly to have it measured and verified by this registry.`
|
|
1767
|
+
: host
|
|
1768
|
+
? `Claim it: publish a signed Node Card at <span class="mono">https://${esc(host)}/.well-known/open-node.json</span> with node id <span class="mono">${esc(suggestedId)}</span> (your parent domain works too) and register. Your verified listing replaces this import, the registry measures what you really serve, and nothing in your stack changes.`
|
|
1769
|
+
: 'This listing has no known API endpoint. Publish a Node Card for your OpenAI-compatible API to be listed, measured and verified.';
|
|
1770
|
+
const mail = `mailto:operators@opennodes.io?subject=${encodeURIComponent('Claim ' + n.id)}`;
|
|
1771
|
+
return `<div class="panel pad" style="max-width:56rem; margin:0.4rem 0 0.8rem; border-left:3px solid var(--accent)">
|
|
1772
|
+
<p style="margin:0 0 0.35rem; font-weight:600">Do you run ${esc(name)}?</p>
|
|
1773
|
+
<p style="margin:0 0 0.6rem; font-size:0.86rem; color:var(--muted); overflow-wrap:anywhere">${body} Free, about 15 minutes with the Node Kit.</p>
|
|
1774
|
+
<span style="display:inline-flex; gap:0.6rem; flex-wrap:wrap; font-size:0.84rem">
|
|
1775
|
+
<a href="${CLAIM_GUIDE}" target="_blank" rel="noopener">How to claim →</a>
|
|
1776
|
+
<a href="#/register">Register a node</a>
|
|
1777
|
+
<a href="${mail}">operators@opennodes.io</a>
|
|
1778
|
+
</span>
|
|
1779
|
+
</div>`;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1746
1782
|
async function loadNode(id) {
|
|
1747
1783
|
$('#node-title').textContent = id;
|
|
1748
1784
|
try {
|
|
@@ -1764,6 +1800,7 @@ async function loadNode(id) {
|
|
|
1764
1800
|
<td>${(o.serving?.supports || []).filter((s) => s !== 'streaming').map((s) => `<span class="chip">${esc(s)}</span>`).join(' ') || '—'}</td>
|
|
1765
1801
|
</tr>`).join('');
|
|
1766
1802
|
$('#node-detail').innerHTML = `
|
|
1803
|
+
${claimBanner(n, card)}
|
|
1767
1804
|
${card.node?.description ? `<p class="desc">${esc(card.node.description)}</p>` : ''}
|
|
1768
1805
|
<div class="statstrip">
|
|
1769
1806
|
${stat(tierChip(tier), 'TRUST TIER')}
|