@opennodes/registry 0.1.27 → 0.1.29

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.27",
3
+ "version": "0.1.29",
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/importers.js CHANGED
@@ -72,7 +72,7 @@ export const KNOWN_PROVIDER_COUNTRY = Object.freeze({
72
72
  });
73
73
 
74
74
  /** Providers whose models are included in a subscription plan rather than priced per token. */
75
- export const SUBSCRIPTION_PROVIDERS = /token-plan|^gitlab$/;
75
+ export const SUBSCRIPTION_PROVIDERS = /-plan(-|$)|^gitlab$/; // token, coding, code and step plans
76
76
 
77
77
  function perTokenPriced(providerId, model) {
78
78
  if (SUBSCRIPTION_PROVIDERS.test(providerId)) return false;
@@ -24,6 +24,7 @@
24
24
  }
25
25
  }
26
26
  * { box-sizing: border-box; }
27
+ [hidden] { display: none !important; } /* an inline display (e.g. grid) must not override the hidden attribute */
27
28
  body { margin: 0; background: var(--ground); color: var(--ink);
28
29
  font-family: "Archivo", "Helvetica Neue", Arial, sans-serif; font-size: 0.9rem; line-height: 1.5; }
29
30
  a { color: var(--accent); text-decoration: none; } a:hover { text-decoration: underline; }
@@ -290,6 +291,7 @@
290
291
  <input class="wide mono" id="pg-key" type="password" placeholder="optional" style="font-size:0.78rem">
291
292
  </div>
292
293
  </div>
294
+ <p class="sub" id="pg-note" hidden style="margin:0.6rem 0 0; font-size:0.8rem"></p>
293
295
  <div id="pg-customrow" hidden style="display:grid; grid-template-columns: 2fr 1fr; gap:0.8rem">
294
296
  <div><label class="f">Custom OpenAI-compatible endpoint <span style="font-weight:400">(e.g. your local Ollama: http://127.0.0.1:11434/v1)</span></label>
295
297
  <input class="wide mono" id="pg-endpoint" placeholder="http://127.0.0.1:11434/v1" style="font-size:0.78rem"></div>
@@ -303,6 +305,7 @@
303
305
  <span class="prov" id="pg-meta"></span>
304
306
  </div>
305
307
  <div id="pg-out" style="margin-top:0.8rem; font-size:0.88rem; white-space:pre-wrap; line-height:1.55"></div>
308
+ <div id="pg-hosted"></div>
306
309
  </div>
307
310
  <div id="md-perfsec"></div>
308
311
  <div id="md-benchsec"></div>
@@ -534,7 +537,7 @@ const ago = (iso) => {
534
537
  return Math.round(s / 86400) + 'd ago';
535
538
  };
536
539
  const tierChip = (t) => `<span class="chip t-${esc(t)}">${esc(String(t).toUpperCase())}</span>`;
537
- const SUBSCRIPTION_NODE = /token-plan|\.gitlab$/;
540
+ const SUBSCRIPTION_NODE = /-plan(-|$)|\.gitlab$/;
538
541
  const pricePublished = (p) => p && (p.input_per_mtok != null || p.output_per_mtok != null);
539
542
  const priceStr = (p, nodeId = '') => {
540
543
  if (!pricePublished(p)) return SUBSCRIPTION_NODE.test(nodeId) ? 'in a subscription plan' : 'price not published';
@@ -611,6 +614,8 @@ function ctxBit(o) {
611
614
  }
612
615
  /** Listing-only imports carry no endpoints.openai: they are shown but never called. */
613
616
  const invocable = (o) => o.invocable !== false && Boolean(o.endpoints?.openai);
617
+ /** Ollama library entries and local-app catalogs have no server: their endpoint is the visitor's own computer. */
618
+ const runsLocally = (o) => Boolean(o.local) || /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:|\/|$)/i.test(o.endpoints?.openai ?? '');
614
619
  /** endpoints.docs only when it is a plain web link (cards are third-party data). */
615
620
  const docsUrl = (endpoints) => /^https?:\/\//i.test(endpoints?.docs ?? '') ? endpoints.docs : null;
616
621
  function provLine(o) {
@@ -887,6 +892,10 @@ async function loadModel(artifact) {
887
892
  $('#md-title').textContent = '…';
888
893
  $('#md-offerings').innerHTML = '';
889
894
  $('#md-benchsec').innerHTML = '';
895
+ $('#pg-hosted').innerHTML = '';
896
+ $('#pg-note').hidden = true;
897
+ $('#pg-target').innerHTML = '';
898
+ pgOfferings = [];
890
899
  try {
891
900
  const { offerings } = await api('/v0/offerings?limit=100&q=' + encodeURIComponent(artifact));
892
901
  const list = offerings.filter((o) => o.model.artifact === artifact);
@@ -939,14 +948,17 @@ async function loadModel(artifact) {
939
948
  // section nav (anchors appear only for sections that render)
940
949
  const navBits = [['#sec-offerings', 'OFFERINGS'], ['#sec-play', 'PLAYGROUND']];
941
950
 
942
- // playground targets: this page's offerings + a custom endpoint (e.g. local Ollama)
943
- pgOfferings = list.filter(invocable);
944
- $('#pg-target').innerHTML = pgOfferings.map((o, i) =>
945
- `<option value="${i}">${esc(o.node_id)} — ${priceStr(o.pricing, o.node_id)} [${esc(o.tier)}]</option>`).join('')
951
+ // playground targets: hosted offerings first, then the ones that run on the visitor's own computer,
952
+ // then a custom endpoint
953
+ pgOfferings = [...list.filter((o) => invocable(o) && !runsLocally(o)), ...list.filter((o) => invocable(o) && runsLocally(o))];
954
+ $('#pg-target').innerHTML = pgOfferings.map((o, i) => `<option value="${i}">${pgLabel(o)}</option>`).join('')
946
955
  + '<option value="custom">custom endpoint (your local Ollama / any OpenAI-compatible server)</option>';
947
956
  $('#pg-customrow').hidden = pgOfferings.length > 0; // listing-only offerings leave just the custom endpoint
948
957
  $('#pg-out').textContent = '';
949
958
  $('#pg-meta').textContent = '';
959
+ pgUpdateNote();
960
+ $('#pg-hosted').innerHTML = '';
961
+ if (pgOfferings.length && pgOfferings.every(runsLocally)) showHostedAlternatives(first, artifact);
950
962
  $('#md-offerings').innerHTML = `<table class="grid num"><thead><tr>
951
963
  <th>NODE</th><th>PRICE IN/OUT</th><th>HARDWARE</th><th>CONTEXT</th><th>TTFT</th><th>TOK/S</th><th>UP</th><th>CAPABILITIES</th><th>REVISION</th>
952
964
  </tr></thead><tbody>` + list.map((o) => {
@@ -1037,8 +1049,55 @@ async function loadModel(artifact) {
1037
1049
 
1038
1050
  /* ---------- playground ---------- */
1039
1051
  let pgOfferings = [];
1052
+ const localHost = (o) => { try { return new URL(o.endpoints.openai).host; } catch { return 'localhost'; } };
1053
+ function pgLabel(o) {
1054
+ if (!runsLocally(o)) return `${esc(o.node_id)} — ${priceStr(o.pricing, o.node_id)} [${esc(o.tier)}]`;
1055
+ return `your computer — ${esc(o.local?.runtime === 'ollama' ? 'Ollama' : 'local app')} at ${esc(localHost(o))} (no hosted server for this listing)`;
1056
+ }
1057
+ /** Says plainly where Send goes when the target is the visitor's own machine. */
1058
+ function pgUpdateNote() {
1059
+ const note = $('#pg-note');
1060
+ const v = $('#pg-target').value;
1061
+ const o = v === 'custom' ? null : pgOfferings[Number(v)];
1062
+ note.hidden = !o || !runsLocally(o);
1063
+ if (note.hidden) return;
1064
+ const app = o.local?.runtime === 'ollama' ? 'Ollama' : 'the local app';
1065
+ note.innerHTML = `This listing has no server behind it: Send goes to ${app} on <b>your own computer</b> (${esc(localHost(o))}), `
1066
+ + `and answers only if ${app} is running with this model installed${o.local?.run ? ` (<code class="mono">${esc(o.local.run)}</code>)` : ''}. `
1067
+ + 'It tests your machine, not a node.';
1068
+ }
1069
+ /** On a local-only model page: hosted listings of the same model, so it can be tried without installing anything. */
1070
+ async function showHostedAlternatives(first, artifact) {
1071
+ const base = (first.binding?.model_id ?? first.model.name).split(':')[0].toLowerCase();
1072
+ const norm = (s) => String(s ?? '').toLowerCase().replace(/[^a-z0-9]/g, '');
1073
+ const needle = norm(base);
1074
+ if (needle.length < 3) return;
1075
+ const variants = [...new Set([base, base.replace(/([a-z])(\d)/, '$1-$2')])];
1076
+ try {
1077
+ const found = new Map();
1078
+ for (const v of variants) {
1079
+ const r = await api('/v0/offerings?limit=200&invocable=true&q=' + encodeURIComponent(v));
1080
+ for (const o of r.offerings) {
1081
+ if (runsLocally(o) || !invocable(o)) continue;
1082
+ if (!norm(`${o.model.name} ${o.model.artifact} ${o.binding?.model_id}`).includes(needle)) continue;
1083
+ found.set(o.node_id + '/' + o.offering_id, o);
1084
+ }
1085
+ }
1086
+ if ($('#model-card').dataset.artifact !== artifact) return; // the visitor moved on
1087
+ const tierOrder = { attested: 4, verified: 3, community: 2, unverified: 1 };
1088
+ const picks = [...found.values()].sort((a, b) =>
1089
+ (tierOrder[b.tier] ?? 0) - (tierOrder[a.tier] ?? 0)
1090
+ || (a.pricing.input_per_mtok ?? Infinity) - (b.pricing.input_per_mtok ?? Infinity)).slice(0, 6);
1091
+ if (!picks.length) return;
1092
+ $('#pg-hosted').innerHTML = `<p class="sub" style="margin:1rem 0 0.4rem; font-size:0.8rem">To try it without installing anything, open a hosted listing with a matching name:</p>
1093
+ <div class="relcards">` + picks.map((o) => `<a class="relcard" href="#/model/${encodeURIComponent(o.model.artifact)}">
1094
+ <span class="rn">${esc(o.model.name)}</span>
1095
+ <span class="rm">${esc(o.node_id)} · ${priceStr(o.pricing, o.node_id)} · ${o.source === 'import' ? 'your key for that provider' : 'no key needed'}</span></a>`).join('') + '</div>';
1096
+ } catch { /* alternatives are optional */ }
1097
+ }
1040
1098
  $('#pg-target').addEventListener('change', () => {
1041
1099
  $('#pg-customrow').hidden = $('#pg-target').value !== 'custom';
1100
+ pgUpdateNote();
1042
1101
  });
1043
1102
  $('#pg-send').addEventListener('click', async () => {
1044
1103
  const out = $('#pg-out'), meta = $('#pg-meta');
@@ -1046,6 +1105,7 @@ $('#pg-send').addEventListener('click', async () => {
1046
1105
  if (!prompt) return;
1047
1106
  const isCustom = $('#pg-target').value === 'custom';
1048
1107
  const offering = isCustom ? null : pgOfferings[Number($('#pg-target').value)];
1108
+ if (!isCustom && !offering) return;
1049
1109
  const base = isCustom ? $('#pg-endpoint').value.trim().replace(/\/$/, '') : offering.endpoints.openai.replace(/\/$/, '');
1050
1110
  const model = isCustom ? $('#pg-model').value.trim() : offering.binding.model_id;
1051
1111
  if (!base || !model) { out.innerHTML = '<span class="err">endpoint and model id are required</span>'; return; }