@opennodes/registry 0.1.1 → 0.1.3

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/src/search.js CHANGED
@@ -1,118 +1,123 @@
1
- // Offering search + transparent ranking (ONP-3 §5–6).
2
-
3
- export const RANK_COMPONENTS = { price: 0.35, tier: 0.35, probe_success: 0.30 };
4
- const TIER_SCORE = { unverified: 0, community: 0.4, verified: 0.8, attested: 1.0 };
5
-
6
- /** Flatten indexed nodes into searchable offering summaries. */
7
- export async function collectOfferings(store) {
8
- const out = [];
9
- for (const node of await store.allNodes()) {
10
- if (['delisted', 'suspended', 'submitted', 'challenged', 'disputed'].includes(node.state)) continue;
11
- const card = await store.latestCard(node.id);
12
- if (!card) continue;
13
- const probes = await store.probesFor(node.id);
14
- const stageA = probes.filter((p) => p.stage === 'A');
15
- const probeSuccess = stageA.length ? stageA.filter((p) => p.ok).length / stageA.length : 0;
16
- const tier = node.state === 'indexed' ? 'unverified' : node.state;
17
- const availability = (await store.getObservations(node.id, '_node')).availability ?? null;
18
- for (const offering of card.offerings) {
19
- const obs = await store.getObservations(node.id, offering.offering_id);
20
- // Measured values override claims (DS-ADM-03/04): cap the advertised context to
21
- // the verified size, and surface registry-measured latency/throughput.
22
- const serving = { ...offering.serving };
23
- if (obs.context_cap && obs.context_cap < (serving.context_window ?? Infinity)) {
24
- serving.context_window = obs.context_cap;
25
- serving.context_capped_from = offering.serving.context_window;
26
- }
27
- out.push({
28
- node_id: node.id,
29
- offering_id: offering.offering_id,
30
- tier,
31
- source: node.source ?? 'registration',
32
- institutional: Boolean(node.institutional),
33
- card_revision: card.revision,
34
- modality: offering.modality,
35
- modalities: offering.modalities ?? null,
36
- local: offering.local ?? null,
37
- model: offering.model,
38
- serving,
39
- binding: offering.binding,
40
- pricing: offering.pricing,
41
- data_policy: offering.data_policy ?? null,
42
- // Allocated hardware for THIS offering (offering-level override wins over the
43
- // node-level description) — same artifact on different vendors runs on different
44
- // silicon, and clients deserve to see which. Always basis-labeled, never measured.
45
- hardware: offering.hardware ?? card.hardware ?? null,
46
- benchmarks: offering.benchmarks ?? null,
47
- observed: {
48
- probe_success: round2(probeSuccess),
49
- probes: stageA.length,
50
- ttft_ms: obs.ttft_ms ?? null,
51
- tps: obs.tps ?? null,
52
- availability,
53
- },
54
- endpoints: card.endpoints,
55
- });
56
- }
57
- }
58
- return out;
59
- }
60
-
61
- export async function searchOfferings(store, query) {
62
- let items = await collectOfferings(store);
63
-
64
- const q = (k) => query.get(k);
65
- if (q('modality')) items = items.filter((o) => o.modality === q('modality'));
66
- if (q('family')) items = items.filter((o) => o.model.family === q('family') || o.model.artifact === q('family'));
67
- if (q('min_context')) items = items.filter((o) => (o.serving?.context_window ?? 0) >= Number(q('min_context')));
68
- if (q('supports')) {
69
- const wanted = q('supports').split(',');
70
- items = items.filter((o) => wanted.every((s) => o.serving?.supports?.includes(s)));
71
- }
72
- if (q('max_input_price')) items = items.filter((o) => (o.pricing.input_per_mtok ?? Infinity) <= Number(q('max_input_price')));
73
- if (q('scheme')) items = items.filter((o) => o.pricing.schemes.includes(q('scheme')));
74
- if (q('tier')) {
75
- const order = ['unverified', 'community', 'verified', 'attested'];
76
- const min = order.indexOf(q('tier'));
77
- items = items.filter((o) => order.indexOf(o.tier) >= min);
78
- }
79
- if (q('lang')) {
80
- const [lang, minGrade = 'basic'] = q('lang').split(':');
81
- const grades = ['basic', 'strong', 'native'];
82
- items = items.filter((o) => (o.serving?.languages ?? []).some(
83
- (l) => l.lang === lang && grades.indexOf(l.grade) >= grades.indexOf(minGrade)));
84
- }
85
- if (q('q')) {
86
- const needle = q('q').toLowerCase();
87
- items = items.filter((o) => JSON.stringify(o.model).toLowerCase().includes(needle)
88
- || o.offering_id.includes(needle) || o.node_id.includes(needle));
89
- }
90
-
91
- const maxPrice = Math.max(...items.map((o) => o.pricing.input_per_mtok ?? 0), 0.000001);
92
- for (const o of items) {
93
- const components = {
94
- price: 1 - (o.pricing.input_per_mtok ?? maxPrice) / maxPrice,
95
- tier: TIER_SCORE[o.tier] ?? 0,
96
- probe_success: o.observed.probe_success,
97
- };
98
- o.rank = round2(Object.entries(RANK_COMPONENTS).reduce((sum, [k, w]) => sum + w * components[k], 0));
99
- o.rank_explanation = { weights: RANK_COMPONENTS, components };
100
- }
101
-
102
- // Performance sorts prefer registry-measured values; claimed expected_* is the
103
- // fallback, and offerings with neither sink to the bottom (ONP-3 §4).
104
- const ttftOf = (o) => o.observed.ttft_ms?.p50 ?? o.serving?.expected_ttft_ms?.p50 ?? Infinity;
105
- const tpsOf = (o) => (o.observed.tps > 0 ? o.observed.tps : null) ?? o.serving?.expected_tps?.p50 ?? -Infinity;
106
- const sort = q('sort') ?? 'rank';
107
- items.sort((a, b) => {
108
- if (sort === 'price') return (a.pricing.input_per_mtok ?? Infinity) - (b.pricing.input_per_mtok ?? Infinity);
109
- if (sort === 'ttft') return ttftOf(a) - ttftOf(b);
110
- if (sort === 'tps') return tpsOf(b) - tpsOf(a);
111
- return b.rank - a.rank;
112
- });
113
-
114
- const limit = Math.min(Number(q('limit') ?? 50), 200);
115
- return items.slice(0, limit);
116
- }
117
-
118
- const round2 = (n) => Math.round(n * 100) / 100;
1
+ // Offering search + transparent ranking (ONP-3 §5–6).
2
+
3
+ export const RANK_COMPONENTS = { price: 0.35, tier: 0.35, probe_success: 0.30 };
4
+ const TIER_SCORE = { unverified: 0, community: 0.4, verified: 0.8, attested: 1.0 };
5
+
6
+ /** Flatten indexed nodes into searchable offering summaries. */
7
+ export async function collectOfferings(store) {
8
+ const out = [];
9
+ for (const node of await store.allNodes()) {
10
+ if (['delisted', 'suspended', 'submitted', 'challenged', 'disputed'].includes(node.state)) continue;
11
+ const card = await store.latestCard(node.id);
12
+ if (!card) continue;
13
+ const probes = await store.probesFor(node.id);
14
+ const stageA = probes.filter((p) => p.stage === 'A');
15
+ const probeSuccess = stageA.length ? stageA.filter((p) => p.ok).length / stageA.length : 0;
16
+ const tier = node.state === 'indexed' ? 'unverified' : node.state;
17
+ const availability = (await store.getObservations(node.id, '_node')).availability ?? null;
18
+ for (const offering of card.offerings) {
19
+ const obs = await store.getObservations(node.id, offering.offering_id);
20
+ // Measured values override claims (DS-ADM-03/04): cap the advertised context to
21
+ // the verified size, and surface registry-measured latency/throughput.
22
+ const serving = { ...offering.serving };
23
+ if (obs.context_cap != null && obs.context_cap < (serving.context_window ?? Infinity)) {
24
+ serving.context_window = obs.context_cap;
25
+ serving.context_capped_from = offering.serving.context_window;
26
+ }
27
+ out.push({
28
+ node_id: node.id,
29
+ offering_id: offering.offering_id,
30
+ tier,
31
+ source: node.source ?? 'registration',
32
+ institutional: Boolean(node.institutional),
33
+ card_revision: card.revision,
34
+ modality: offering.modality,
35
+ modalities: offering.modalities ?? null,
36
+ local: offering.local ?? null,
37
+ model: offering.model,
38
+ serving,
39
+ binding: offering.binding,
40
+ pricing: offering.pricing,
41
+ data_policy: offering.data_policy ?? null,
42
+ // Allocated hardware for THIS offering (offering-level override wins over the
43
+ // node-level description) — same artifact on different vendors runs on different
44
+ // silicon, and clients deserve to see which. Always basis-labeled, never measured.
45
+ hardware: offering.hardware ?? card.hardware ?? null,
46
+ benchmarks: offering.benchmarks ?? null,
47
+ observed: {
48
+ probe_success: round2(probeSuccess),
49
+ probes: stageA.length,
50
+ ttft_ms: obs.ttft_ms ?? null,
51
+ tps: obs.tps ?? null,
52
+ availability,
53
+ },
54
+ endpoints: card.endpoints,
55
+ });
56
+ }
57
+ }
58
+ return out;
59
+ }
60
+
61
+ /** Search returning the page plus the total number of matches (before `limit`). */
62
+ export async function searchOfferingsPaged(store, query) {
63
+ let items = await collectOfferings(store);
64
+
65
+ const q = (k) => query.get(k);
66
+ if (q('modality')) items = items.filter((o) => o.modality === q('modality'));
67
+ if (q('family')) items = items.filter((o) => o.model.family === q('family') || o.model.artifact === q('family'));
68
+ if (q('min_context')) items = items.filter((o) => (o.serving?.context_window ?? 0) >= Number(q('min_context')));
69
+ if (q('supports')) {
70
+ const wanted = q('supports').split(',');
71
+ items = items.filter((o) => wanted.every((s) => o.serving?.supports?.includes(s)));
72
+ }
73
+ if (q('max_input_price')) items = items.filter((o) => (o.pricing.input_per_mtok ?? Infinity) <= Number(q('max_input_price')));
74
+ if (q('scheme')) items = items.filter((o) => o.pricing.schemes.includes(q('scheme')));
75
+ if (q('tier')) {
76
+ const order = ['unverified', 'community', 'verified', 'attested'];
77
+ const min = order.indexOf(q('tier'));
78
+ items = items.filter((o) => order.indexOf(o.tier) >= min);
79
+ }
80
+ if (q('lang')) {
81
+ const [lang, minGrade = 'basic'] = q('lang').split(':');
82
+ const grades = ['basic', 'strong', 'native'];
83
+ items = items.filter((o) => (o.serving?.languages ?? []).some(
84
+ (l) => l.lang === lang && grades.indexOf(l.grade) >= grades.indexOf(minGrade)));
85
+ }
86
+ if (q('q')) {
87
+ const needle = q('q').toLowerCase();
88
+ items = items.filter((o) => JSON.stringify(o.model).toLowerCase().includes(needle)
89
+ || o.offering_id.includes(needle) || o.node_id.includes(needle));
90
+ }
91
+
92
+ const maxPrice = Math.max(...items.map((o) => o.pricing.input_per_mtok ?? 0), 0.000001);
93
+ for (const o of items) {
94
+ const components = {
95
+ price: 1 - (o.pricing.input_per_mtok ?? maxPrice) / maxPrice,
96
+ tier: TIER_SCORE[o.tier] ?? 0,
97
+ probe_success: o.observed.probe_success,
98
+ };
99
+ o.rank = round2(Object.entries(RANK_COMPONENTS).reduce((sum, [k, w]) => sum + w * components[k], 0));
100
+ o.rank_explanation = { weights: RANK_COMPONENTS, components };
101
+ }
102
+
103
+ // Performance sorts prefer registry-measured values; claimed expected_* is the
104
+ // fallback, and offerings with neither sink to the bottom (ONP-3 §4).
105
+ const ttftOf = (o) => o.observed.ttft_ms?.p50 ?? o.serving?.expected_ttft_ms?.p50 ?? Infinity;
106
+ const tpsOf = (o) => (o.observed.tps > 0 ? o.observed.tps : null) ?? o.serving?.expected_tps?.p50 ?? -Infinity;
107
+ const sort = q('sort') ?? 'rank';
108
+ items.sort((a, b) => {
109
+ if (sort === 'price') return (a.pricing.input_per_mtok ?? Infinity) - (b.pricing.input_per_mtok ?? Infinity);
110
+ if (sort === 'ttft') return ttftOf(a) - ttftOf(b);
111
+ if (sort === 'tps') return tpsOf(b) - tpsOf(a);
112
+ return b.rank - a.rank;
113
+ });
114
+
115
+ const limit = Math.min(Number(q('limit') ?? 50), 200);
116
+ return { offerings: items.slice(0, limit), total: items.length };
117
+ }
118
+
119
+ export async function searchOfferings(store, query) {
120
+ return (await searchOfferingsPaged(store, query)).offerings;
121
+ }
122
+
123
+ const round2 = (n) => Math.round(n * 100) / 100;