@opennodes/registry 0.1.3 → 0.1.4

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/web/index.html +29 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opennodes/registry",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
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",
@@ -983,11 +983,31 @@ async function loadChatTargets() {
983
983
  $('#ch-modenote').textContent = MODE_NOTES[$('#ch-mode').value] ?? '';
984
984
  if (!chat.offerings.length) {
985
985
  try {
986
- const { offerings } = await api('/v0/offerings?limit=60&modality=text');
987
- chat.offerings = offerings;
986
+ // Registered nodes answer straight from the browser with no key; imported catalog listings
987
+ // (OpenRouter, HF providers, models.dev) need the visitor's own key for that provider.
988
+ const [reg, all] = await Promise.all([
989
+ api('/v0/offerings?limit=60&modality=text&tier=community'),
990
+ api('/v0/offerings?limit=60&modality=text'),
991
+ ]);
992
+ const key = (o) => o.node_id + '/' + o.offering_id;
993
+ const ready = reg.offerings.filter((o) => o.source !== 'import');
994
+ const seen = new Set(ready.map(key));
995
+ const rest = all.offerings.filter((o) => !seen.has(key(o)));
996
+ const local = rest.filter((o) => o.local); // Ollama library: runs on the visitor's own machine
997
+ const imports = rest.filter((o) => !o.local);
998
+ chat.offerings = [...ready, ...local, ...imports];
999
+ const opt = (o, i) => `<option value="${i}">${esc(o.model.name)} — ${esc(o.node_id)} — ${priceStr(o.pricing)}</option>`;
988
1000
  $('#ch-add').innerHTML = '<option value="">+ Add a model…</option>'
989
- + offerings.map((o, i) => `<option value="${i}">${esc(o.model.name)} — ${esc(o.node_id)} — ${priceStr(o.pricing)}</option>`).join('')
1001
+ + (ready.length ? `<optgroup label="Ready to use — no key needed (${ready.length})">${ready.map((o, i) => opt(o, i)).join('')}</optgroup>` : '')
1002
+ + (local.length ? `<optgroup label="On your machine — needs a local Ollama with this model pulled">${local.map((o, i) => opt(o, ready.length + i)).join('')}</optgroup>` : '')
1003
+ + (imports.length ? `<optgroup label="Imported catalogs — your own API key required">${imports.map((o, i) => opt(o, ready.length + local.length + i)).join('')}</optgroup>` : '')
990
1004
  + '<option value="custom">custom endpoint (local Ollama / any OpenAI server)</option>';
1005
+ if (!chat.roster.length && ready.length && !sessionStorage.getItem('onp-advisor-handoff')) {
1006
+ chat.roster.push({ offering: ready[0] }); // a newcomer can just type
1007
+ renderRoster();
1008
+ const log = $('#ch-log');
1009
+ if (log && !chat.history.length) log.innerHTML = `<p class="sub" style="margin:0">Ready: <b>${esc(ready[0].model.name)}</b> on ${esc(ready[0].node_id)} (${esc(ready[0].tier)}, no key needed) is preselected — say something, or add more models on the right to compare.</p>`;
1010
+ }
991
1011
  } catch { $('#ch-add').innerHTML = '<option value="custom">custom endpoint</option>'; }
992
1012
  }
993
1013
  await applyAdvisorHandoff();
@@ -1099,6 +1119,7 @@ $('#ch-judge').addEventListener('change', refreshEstimate);
1099
1119
  function importKeyHint(offering, base, key) {
1100
1120
  if (!offering || offering.source !== 'import' || key) return null;
1101
1121
  const host = new URL(base).host;
1122
+ if (offering.local || /^(localhost|127\.0\.0\.1|\[::1\])(:|$)/.test(host)) return null; // your own machine: no key
1102
1123
  const where = host.includes('huggingface') ? 'a Hugging Face token (huggingface.co/settings/tokens)'
1103
1124
  : host.includes('openrouter') ? 'an OpenRouter API key (openrouter.ai/keys)' : `an API key for ${host}`;
1104
1125
  return `This is an imported catalog listing served by ${host}: your browser calls it directly, which needs ${where} — paste it in the API key field (stored only in this browser). Or run it locally with BYOK: npx @opennodes/cli gateway --key ${host}=<key>. Registered OpenNodes nodes need no key.`;
@@ -1189,7 +1210,8 @@ async function callTargetStream(t, messages, onDelta, maxTokens = 1024) {
1189
1210
  }
1190
1211
 
1191
1212
  const metaLine = (r) => [r.ttft != null ? `ttft ${r.ttft}ms · ` + r.ms + 'ms total' : r.ms + 'ms', r.usage ? `${r.usage.prompt_tokens}+${r.usage.completion_tokens} tok` : null, r.receipt ? 'receipt ✓' : null].filter(Boolean).join(' · ');
1192
- const corsHint = ' — if this is a CORS/network error, the node must allow browser origins (local Ollama: set OLLAMA_ORIGINS)';
1213
+ const corsHint = (err) => /failed to fetch|networkerror|load failed/i.test(String(err?.message))
1214
+ ? ' — network/CORS error: the node must allow browser origins (local Ollama: set OLLAMA_ORIGINS)' : '';
1193
1215
 
1194
1216
  function logEl(html) {
1195
1217
  if ($('#ch-log').querySelector('p.sub')) $('#ch-log').innerHTML = '';
@@ -1239,7 +1261,7 @@ async function runSingle(text) {
1239
1261
  } catch (err) {
1240
1262
  chat.history.pop();
1241
1263
  el.firstChild.textContent = '';
1242
- el.querySelector('.mm').innerHTML = '<span class="err">' + esc(err.message) + '</span>' + corsHint;
1264
+ el.querySelector('.mm').innerHTML = '<span class="err">' + esc(err.message) + '</span>' + corsHint(err);
1243
1265
  }
1244
1266
  }
1245
1267
 
@@ -1259,7 +1281,7 @@ async function fanoutRound(labelPrefix, buildMessages, { remember = false } = {}
1259
1281
  }
1260
1282
  return { label: String.fromCharCode(65 + i), text: r.text, ok: true };
1261
1283
  } catch (err) {
1262
- cards[i].querySelector('.ctext').innerHTML = '<span class="err">' + esc(err.message) + '</span><span class="prov">' + corsHint + '</span>';
1284
+ cards[i].querySelector('.ctext').innerHTML = '<span class="err">' + esc(err.message) + '</span><span class="prov">' + corsHint(err) + '</span>';
1263
1285
  return { label: String.fromCharCode(65 + i), ok: false };
1264
1286
  }
1265
1287
  }));
@@ -1346,7 +1368,7 @@ async function runPipeline(text) {
1346
1368
  el.querySelector('.ctext').textContent = r.text || '(empty)';
1347
1369
  el.querySelector('.mm').textContent = metaLine(r);
1348
1370
  } catch (err) {
1349
- el.querySelector('.ctext').innerHTML = '<span class="err">' + esc(err.message) + '</span><span class="prov">' + corsHint + ' — pipeline stopped</span>';
1371
+ el.querySelector('.ctext').innerHTML = '<span class="err">' + esc(err.message) + '</span><span class="prov">' + corsHint(err) + ' — pipeline stopped</span>';
1350
1372
  return;
1351
1373
  }
1352
1374
  }