@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 +1 -1
- package/src/importers.js +1 -1
- package/src/web/index.html +65 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opennodes/registry",
|
|
3
|
-
"version": "0.1.
|
|
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 =
|
|
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;
|
package/src/web/index.html
CHANGED
|
@@ -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 =
|
|
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:
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
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; }
|