@opennodes/registry 0.1.17 → 0.1.19

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.17",
3
+ "version": "0.1.19",
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.2",
46
+ "@opennodes/core": "0.1.4",
47
47
  "pg": "^8.13.1"
48
48
  }
49
49
  }
package/src/importers.js CHANGED
@@ -60,6 +60,17 @@ export const MODELS_DEV_API_BASES = {
60
60
  openai: 'https://api.openai.com/v1',
61
61
  };
62
62
 
63
+ /**
64
+ * Country of providers whose own API we import directly and whose company and inference hosting
65
+ * are known to be in that country — conservative on purpose (routers, gateways and providers with
66
+ * mixed or unclear hosting are left out). Imports carry it as an *inferred* jurisdiction; only the
67
+ * operator's own signed card can declare one.
68
+ */
69
+ export const KNOWN_PROVIDER_COUNTRY = Object.freeze({
70
+ 'cloudferro-sherlock': 'PL', berget: 'SE', evroc: 'SE', 'regolo-ai': 'IT', scaleway: 'FR', ovhcloud: 'FR',
71
+ mistral: 'FR', stackit: 'DE', hetzner: 'DE', infomaniak: 'CH',
72
+ });
73
+
63
74
  /**
64
75
  * Import a models.dev-shaped catalog: { providerId: { id, name, api?, doc?, models: { key: model } } }.
65
76
  * Each provider becomes a synthetic node `import.modelsdev.{provider}`; each model an offering.
@@ -111,7 +122,7 @@ export async function importModelsDev(store, catalog) {
111
122
  id: nodeId,
112
123
  name: provider?.name ?? providerId,
113
124
  description: `Imported from models.dev; metadata only, unclaimed by the operator.`,
114
- operator: { name: provider?.name ?? providerId },
125
+ operator: { name: provider?.name ?? providerId, ...(KNOWN_PROVIDER_COUNTRY[providerId] ? { country: KNOWN_PROVIDER_COUNTRY[providerId] } : {}) },
115
126
  },
116
127
  endpoints: {
117
128
  ...(base ? { openai: base, health: `${base}/health` } : {}),
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,14 @@ 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 is (ONP-2 node.operator.country, node.regions).
44
+ // Registered nodes declare it in their signed card; imports get it from the importer's list of
45
+ // known providers, labeled inferred.
46
+ jurisdiction: {
47
+ country: card.node?.operator?.country ?? null,
48
+ regions: card.node?.regions ?? [],
49
+ basis: card.node?.operator?.country ? (node.source === 'import' ? 'inferred' : 'declared') : null,
50
+ },
42
51
  // Allocated hardware for THIS offering (offering-level override wins over the
43
52
  // node-level description) — same artifact on different vendors runs on different
44
53
  // silicon, and clients deserve to see which. Always basis-labeled, never measured.
@@ -76,6 +85,7 @@ export async function searchOfferingsPaged(store, query) {
76
85
  }
77
86
  if (q('max_input_price')) items = items.filter((o) => (o.pricing.input_per_mtok ?? Infinity) <= Number(q('max_input_price')));
78
87
  if (q('scheme')) items = items.filter((o) => o.pricing.schemes.includes(q('scheme')));
88
+ if (q('jurisdiction')) items = items.filter((o) => inJurisdiction(o, q('jurisdiction')));
79
89
  if (q('tier')) {
80
90
  const order = ['unverified', 'community', 'verified', 'attested'];
81
91
  const min = order.indexOf(q('tier'));
@@ -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 in the EU, Iceland, Liechtenstein or Norway — declared by registered nodes, inferred for imported listings of known European providers"><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 }) });
@@ -1799,7 +1805,7 @@ async function loadNode(id) {
1799
1805
  <div class="statstrip">
1800
1806
  ${stat(tierChip(tier), 'TRUST TIER')}
1801
1807
  ${stat(String(offers.length), 'OFFERINGS')}
1802
- ${card.node?.operator?.name ? stat(esc(card.node.operator.name) + (card.node.operator.country ? ' (' + esc(card.node.operator.country) + ')' : ''), 'OPERATOR') : ''}
1808
+ ${card.node?.operator?.name ? stat(esc(card.node.operator.name) + (card.node.operator.country ? ' (' + esc(card.node.operator.country) + (n.source === 'import' ? ' · inferred' : '') + ')' : ''), 'OPERATOR') : ''}
1803
1809
  ${(card.node?.regions || []).length ? stat(esc(card.node.regions.join(', ')), 'REGIONS') : ''}
1804
1810
  ${n.source === 'import' ? stat('<span class="chip imp">IMPORTED</span>', 'SOURCE — unclaimed by operator') : ''}
1805
1811
  </div>