@opennodes/registry 0.1.27 → 0.1.28

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