@opennodes/registry 0.1.1 → 0.1.2

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/importers.js CHANGED
@@ -1,346 +1,346 @@
1
- // DS-IMP-01: seed importers so search is useful before any real registration.
2
- // Imported records enter as unverified, unsigned, source='import', and are
3
- // excluded from verified tiers until claimed by their operator.
4
- import { validateCard } from '@opennodes/core';
5
-
6
- const slug = (s) => String(s).toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[^a-z0-9]+/, '').slice(0, 100);
7
- const vendorSlug = (s) => slug(s).replace(/[._]/g, '-');
8
-
9
- /** Map input/output modality arrays (the shape both HF and OpenRouter use) to the ONP enum. */
10
- export function mapModalities(inputs = ['text'], outputs = ['text']) {
11
- const out = new Set(outputs);
12
- if (out.has('image')) return 'image';
13
- if (out.has('video')) return 'video';
14
- if (out.has('audio')) return 'audio';
15
- if (out.has('embedding') || out.has('embeddings')) return 'embedding';
16
- return inputs.some((i) => i !== 'text') ? 'multimodal' : 'text';
17
- }
18
-
19
- const PIPELINE_TAG_MODALITY = {
20
- 'text-generation': 'text', 'text2text-generation': 'text', 'conversational': 'text',
21
- 'image-text-to-text': 'multimodal', 'visual-question-answering': 'multimodal',
22
- 'text-to-image': 'image', 'image-to-image': 'image',
23
- 'text-to-video': 'video', 'image-to-video': 'video', 'image-text-to-video': 'video',
24
- 'text-to-speech': 'audio', 'automatic-speech-recognition': 'audio', 'audio-to-audio': 'audio',
25
- 'feature-extraction': 'embedding', 'sentence-similarity': 'embedding',
26
- };
27
-
28
- /** Per-token price strings/values -> USD per MTok. Values >= 0.01 are assumed per-MTok already. */
29
- const perMtok = (v) => {
30
- const n = Number(v);
31
- if (!Number.isFinite(n) || n <= 0) return 0;
32
- return Math.round((n < 0.01 ? n * 1_000_000 : n) * 1e6) / 1e6;
33
- };
34
-
35
- async function saveImportedCard(store, nodeId, origin, sourceRef, card) {
36
- const errors = validateCard(card);
37
- if (errors.length) return errors[0];
38
- await store.upsertNode(nodeId, origin, sourceRef, 'indexed', 'import');
39
- await store.saveRevision(nodeId, card.revision, card, false);
40
- return null;
41
- }
42
-
43
- /**
44
- * Import a models.dev-shaped catalog: { providerId: { id, name, api?, doc?, models: { key: model } } }.
45
- * Each provider becomes a synthetic node `import.modelsdev.{provider}`; each model an offering.
46
- * Returns { providers, offerings, skipped }.
47
- */
48
- export async function importModelsDev(store, catalog) {
49
- let providers = 0, offerings = 0;
50
- const skipped = [];
51
- for (const [providerId, provider] of Object.entries(catalog ?? {})) {
52
- const models = provider?.models ?? {};
53
- const offeringList = [];
54
- for (const [modelKey, model] of Object.entries(models)) {
55
- const outputs = model?.modalities?.output ?? ['text'];
56
- const modality = outputs.includes('text') ? 'text' : outputs.includes('image') ? 'image' : 'text';
57
- offeringList.push({
58
- offering_id: slug(modelKey) || 'model',
59
- modality,
60
- model: {
61
- name: model?.name ?? modelKey,
62
- family: slug(providerId),
63
- artifact: `urn:proprietary:${slug(providerId).replace(/[._]/g, '-')}:${slug(model?.id ?? modelKey).replace(/\//g, '-')}`,
64
- },
65
- serving: {
66
- ...(model?.limit?.context ? { context_window: Math.floor(model.limit.context) } : {}),
67
- ...(model?.limit?.output ? { max_output_tokens: Math.floor(model.limit.output) } : {}),
68
- ...(model?.tool_call ? { supports: ['tool_calls'] } : {}),
69
- },
70
- binding: { profile: 'onp.openai.chat/v1', model_id: model?.id ?? modelKey },
71
- pricing: {
72
- currency: 'USD',
73
- input_per_mtok: Number(model?.cost?.input ?? 0),
74
- output_per_mtok: Number(model?.cost?.output ?? 0),
75
- schemes: ['prepaid'],
76
- },
77
- });
78
- }
79
- if (!offeringList.length) { skipped.push(providerId); continue; }
80
-
81
- const nodeId = `import.modelsdev.${slug(providerId).replace(/[._]/g, '-')}`;
82
- const base = provider?.api ?? provider?.doc ?? 'https://models.dev';
83
- const card = {
84
- onp: '0.1',
85
- revision: new Date().toISOString(),
86
- node: {
87
- id: nodeId,
88
- name: provider?.name ?? providerId,
89
- description: `Imported from models.dev; metadata only, unclaimed by the operator.`,
90
- operator: { name: provider?.name ?? providerId },
91
- },
92
- endpoints: { openai: base, health: `${base.replace(/\/$/, '')}/health` },
93
- offerings: offeringList,
94
- payment: { schemes: [{ scheme: 'prepaid' }] },
95
- };
96
- const errors = validateCard(card);
97
- if (errors.length) { skipped.push(`${providerId}: ${errors[0]}`); continue; }
98
-
99
- await store.upsertNode(nodeId, new URL(base).origin, `import:modelsdev:${providerId}`, 'indexed', 'import');
100
- await store.saveRevision(nodeId, card.revision, card, false);
101
- providers++;
102
- offerings += offeringList.length;
103
- }
104
- return { providers, offerings, skipped };
105
- }
106
-
107
- export const HF_ROUTER_URL = 'https://router.huggingface.co/v1/models';
108
- export const HF_HUB_URL = 'https://huggingface.co/api/models?inference_provider=all&limit=1000&expand[]=inferenceProviderMapping&expand[]=pipeline_tag&expand[]=safetensors';
109
-
110
- /**
111
- * Import Hugging Face Inference Providers: `router` is the /v1/models response
112
- * (live serving: per-provider status/capabilities/perf), `hub` the Hub API list
113
- * (params_b from safetensors, pipeline_tag). One synthetic node per provider
114
- * (`import.hf.{provider}`); artifact `hf:{repo}` is authoritative.
115
- */
116
- export async function importHuggingFace(store, router, hub = []) {
117
- const hubById = new Map((hub ?? []).map((m) => [m.id, m]));
118
- const byProvider = new Map(); // provider -> offerings[]
119
-
120
- for (const model of router?.data ?? []) {
121
- const hubEntry = hubById.get(model.id);
122
- const paramsB = hubEntry?.safetensors?.total
123
- ? Math.round(hubEntry.safetensors.total / 1e7) / 100 : undefined;
124
- const inputs = model.architecture?.input_modalities ?? ['text'];
125
- const outputs = model.architecture?.output_modalities ?? ['text'];
126
- const modality = hubEntry?.pipeline_tag && PIPELINE_TAG_MODALITY[hubEntry.pipeline_tag]
127
- ? PIPELINE_TAG_MODALITY[hubEntry.pipeline_tag]
128
- : mapModalities(inputs, outputs);
129
-
130
- for (const p of model.providers ?? []) {
131
- if (p.status !== 'live') continue;
132
- const supports = ['streaming'];
133
- if (p.supports_tools) supports.push('tool_calls');
134
- if (p.supports_structured_output) supports.push('json_mode');
135
- if (inputs.includes('image') && outputs.includes('text')) supports.push('vision');
136
- const offering = {
137
- offering_id: slug(model.id),
138
- modality,
139
- modalities: { input: inputs, output: outputs }, // Draft 0.2 field, additive
140
- model: {
141
- name: model.id,
142
- family: vendorSlug(model.owned_by ?? model.id.split('/')[0]),
143
- artifact: `hf:${model.id}`,
144
- ...(paramsB ? { params_b: paramsB } : {}),
145
- },
146
- serving: {
147
- supports,
148
- ...(p.context_length ? { context_window: Math.floor(p.context_length) } : {}),
149
- ...(Number.isFinite(p.first_token_latency_ms)
150
- ? { expected_ttft_ms: { p50: Math.round(p.first_token_latency_ms), basis: 'claimed' } } : {}),
151
- ...(Number.isFinite(p.throughput)
152
- ? { expected_tps: { p50: Math.round(p.throughput), basis: 'claimed' } } : {}),
153
- },
154
- binding: { profile: 'onp.openai.chat/v1', model_id: `${model.id}:${p.provider}` },
155
- pricing: {
156
- currency: 'USD',
157
- input_per_mtok: perMtok(p.pricing?.input),
158
- output_per_mtok: perMtok(p.pricing?.output),
159
- schemes: p.is_free ? ['free'] : ['prepaid'],
160
- },
161
- };
162
- if (!byProvider.has(p.provider)) byProvider.set(p.provider, []);
163
- byProvider.get(p.provider).push(offering);
164
- }
165
- }
166
-
167
- let providers = 0, offerings = 0;
168
- const skipped = [];
169
- for (const [provider, offeringList] of byProvider) {
170
- const nodeId = `import.hf.${vendorSlug(provider)}`;
171
- const card = {
172
- onp: '0.1',
173
- revision: new Date().toISOString(),
174
- node: {
175
- id: nodeId,
176
- name: `${provider} (via Hugging Face Inference Providers)`,
177
- description: 'Imported from the Hugging Face Inference Providers router; metadata only, unclaimed by the operator.',
178
- operator: { name: provider },
179
- },
180
- endpoints: { openai: 'https://router.huggingface.co/v1', health: 'https://router.huggingface.co/health' },
181
- offerings: offeringList,
182
- payment: { schemes: [{ scheme: 'prepaid', signup: 'https://huggingface.co/settings/billing' }] },
183
- };
184
- const err = await saveImportedCard(store, nodeId, 'https://router.huggingface.co', `import:hf:${provider}`, card);
185
- if (err) { skipped.push(`${provider}: ${err}`); continue; }
186
- providers++;
187
- offerings += offeringList.length;
188
- }
189
- return { providers, offerings, skipped };
190
- }
191
-
192
- export const OLLAMA_LIBRARY_URL = 'https://ollama.com/library';
193
-
194
- /** Parse ollama.com/library HTML into entries. Resilient to styling churn:
195
- * anchors on /library/{name}, spans classified by content (sizes look like "27b"/"270m"). */
196
- export function parseOllamaLibrary(html) {
197
- const entries = [];
198
- const blocks = String(html).split('href="/library/').slice(1);
199
- for (const block of blocks) {
200
- const name = block.match(/^([a-z0-9._-]+)"/)?.[1];
201
- if (!name) continue;
202
- const chunk = block.slice(0, 6000);
203
- const description = chunk.match(/<p[^>]*class="[^"]*max-w-lg[^"]*"[^>]*>\s*([^<]{1,400}?)\s*<\/p>/)?.[1]?.trim() ?? null;
204
- const spans = [...chunk.matchAll(/<span[^>]*class="[^"]*inline-flex[^"]*"[^>]*>\s*([^<]{1,40}?)\s*<\/span>/g)].map((m) => m[1].trim());
205
- const sizes = spans.filter((s) => /^\d+(\.\d+)?[mb]$/i.test(s));
206
- const capabilities = spans.filter((s) => !/^\d+(\.\d+)?[mb]$/i.test(s));
207
- const pulls = chunk.match(/<span[^>]*>\s*([\d.]+[KMB]?)\s*<\/span>\s*<span[^>]*>&nbsp;Pulls/)?.[1] ?? null;
208
- entries.push({ name, description, capabilities, sizes, pulls });
209
- }
210
- return entries;
211
- }
212
-
213
- const sizeToParamsB = (s) => {
214
- const m = String(s).match(/^(\d+(?:\.\d+)?)([mb])$/i);
215
- if (!m) return null;
216
- return m[2].toLowerCase() === 'b' ? Number(m[1]) : Math.round(Number(m[1]) / 10) / 100;
217
- };
218
-
219
- /**
220
- * Import the Ollama library as locally-runnable offerings on a synthetic node whose
221
- * endpoint is the user's OWN machine (http://127.0.0.1:11434/v1) — free, local, and
222
- * directly usable from the model-page playground once `ollama run <name>` has pulled it.
223
- */
224
- export async function importOllamaLibrary(store, entries) {
225
- const offeringList = [];
226
- for (const e of entries ?? []) {
227
- if (!e?.name) continue;
228
- const caps = (e.capabilities ?? []).map((c) => String(c).toLowerCase());
229
- const modality = caps.includes('embedding') ? 'embedding' : (caps.includes('vision') ? 'multimodal' : 'text');
230
- const paramsCandidates = (e.sizes ?? []).map(sizeToParamsB).filter((n) => n != null);
231
- const supports = ['streaming'];
232
- if (caps.includes('tools')) supports.push('tool_calls');
233
- if (caps.includes('vision')) supports.push('vision');
234
- offeringList.push({
235
- offering_id: slug(e.name),
236
- modality,
237
- model: {
238
- name: e.name,
239
- family: vendorSlug(e.name.split(/[.:0-9]/)[0] || e.name),
240
- artifact: `urn:proprietary:ollama:${vendorSlug(e.name)}`,
241
- ...(paramsCandidates.length ? { params_b: Math.max(...paramsCandidates) } : {}),
242
- ...(e.description ? { description: String(e.description).slice(0, 500) } : {}),
243
- },
244
- serving: { supports },
245
- binding: {
246
- profile: modality === 'embedding' ? 'onp.openai.embeddings/v1' : 'onp.openai.chat/v1',
247
- model_id: e.name,
248
- },
249
- pricing: { currency: 'USD', input_per_mtok: 0, output_per_mtok: 0, schemes: ['free'] },
250
- local: {
251
- runtime: 'ollama',
252
- run: `ollama run ${e.name}`,
253
- ...(e.pulls ? { pulls: e.pulls } : {}),
254
- ...(e.sizes?.length ? { sizes: e.sizes } : {}),
255
- },
256
- });
257
- }
258
- if (!offeringList.length) return { providers: 0, offerings: 0, skipped: ['no entries'] };
259
- const nodeId = 'import.ollama.library';
260
- const card = {
261
- onp: '0.1',
262
- revision: new Date().toISOString(),
263
- node: {
264
- id: nodeId,
265
- name: 'Ollama library (runs on your machine)',
266
- description: 'Locally-runnable models from the Ollama library. The endpoint is your own machine: install Ollama, `ollama run <model>`, and these offerings serve at localhost — free, private, no network.',
267
- operator: { name: 'Ollama (community library)' },
268
- },
269
- endpoints: { openai: 'http://127.0.0.1:11434/v1', health: 'http://127.0.0.1:11434' },
270
- offerings: offeringList,
271
- payment: { schemes: [{ scheme: 'free' }] },
272
- };
273
- const err = await saveImportedCard(store, nodeId, 'https://ollama.com', 'import:ollama-library', card);
274
- if (err) return { providers: 0, offerings: 0, skipped: [err] };
275
- return { providers: 1, offerings: offeringList.length, skipped: [] };
276
- }
277
-
278
- export const OPENROUTER_URL = 'https://openrouter.ai/api/v1/models';
279
-
280
- /**
281
- * Import the OpenRouter catalog as one synthetic node (`import.openrouter.catalog`):
282
- * per-token price strings -> per-MTok; `hugging_face_id` gives the artifact join
283
- * where present, else a proprietary URN under the model author's namespace.
284
- */
285
- export async function importOpenRouter(store, catalog) {
286
- const offeringList = [];
287
- for (const model of catalog?.data ?? []) {
288
- const inputs = model.architecture?.input_modalities ?? ['text'];
289
- const outputs = model.architecture?.output_modalities ?? ['text'];
290
- const supports = ['streaming'];
291
- const params = model.supported_parameters ?? [];
292
- if (params.includes('tools')) supports.push('tool_calls');
293
- if (params.includes('response_format') || params.includes('structured_outputs')) supports.push('json_mode');
294
- if (inputs.includes('image') && outputs.includes('text')) supports.push('vision');
295
- const author = vendorSlug(model.id.split('/')[0] || 'unknown');
296
- offeringList.push({
297
- offering_id: slug(model.id),
298
- modality: mapModalities(inputs, outputs),
299
- modalities: { input: inputs, output: outputs },
300
- model: {
301
- name: model.name ?? model.id,
302
- family: author,
303
- artifact: model.hugging_face_id
304
- ? `hf:${model.hugging_face_id}`
305
- : `urn:proprietary:${author}:${slug(model.id.split('/').slice(1).join('-') || model.id)}`,
306
- ...(model.description ? { description: String(model.description).slice(0, 500) } : {}),
307
- ...(Number.isFinite(model.created)
308
- ? { released: new Date(model.created * 1000).toISOString().slice(0, 10) } : {}),
309
- },
310
- serving: {
311
- supports,
312
- ...(model.context_length ? { context_window: Math.floor(model.context_length) } : {}),
313
- ...(model.top_provider?.max_completion_tokens
314
- ? { max_output_tokens: Math.floor(model.top_provider.max_completion_tokens) } : {}),
315
- },
316
- binding: { profile: 'onp.openai.chat/v1', model_id: model.id },
317
- pricing: {
318
- currency: 'USD',
319
- input_per_mtok: perMtok(model.pricing?.prompt),
320
- output_per_mtok: perMtok(model.pricing?.completion),
321
- ...(model.pricing?.input_cache_read
322
- ? { cached_input_per_mtok: perMtok(model.pricing.input_cache_read) } : {}),
323
- schemes: Number(model.pricing?.prompt) === 0 ? ['free'] : ['prepaid'],
324
- },
325
- });
326
- }
327
- if (!offeringList.length) return { providers: 0, offerings: 0, skipped: ['empty catalog'] };
328
-
329
- const nodeId = 'import.openrouter.catalog';
330
- const card = {
331
- onp: '0.1',
332
- revision: new Date().toISOString(),
333
- node: {
334
- id: nodeId,
335
- name: 'OpenRouter catalog',
336
- description: 'Imported from the public OpenRouter models API; metadata only, unclaimed by the operator.',
337
- operator: { name: 'OpenRouter' },
338
- },
339
- endpoints: { openai: 'https://openrouter.ai/api/v1', health: 'https://openrouter.ai/api/v1/models' },
340
- offerings: offeringList,
341
- payment: { schemes: [{ scheme: 'prepaid', signup: 'https://openrouter.ai/keys' }] },
342
- };
343
- const err = await saveImportedCard(store, nodeId, 'https://openrouter.ai', 'import:openrouter', card);
344
- if (err) return { providers: 0, offerings: 0, skipped: [err] };
345
- return { providers: 1, offerings: offeringList.length, skipped: [] };
346
- }
1
+ // DS-IMP-01: seed importers so search is useful before any real registration.
2
+ // Imported records enter as unverified, unsigned, source='import', and are
3
+ // excluded from verified tiers until claimed by their operator.
4
+ import { validateCard } from '@opennodes/core';
5
+
6
+ const slug = (s) => String(s).toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[^a-z0-9]+/, '').slice(0, 100);
7
+ const vendorSlug = (s) => slug(s).replace(/[._]/g, '-');
8
+
9
+ /** Map input/output modality arrays (the shape both HF and OpenRouter use) to the ONP enum. */
10
+ export function mapModalities(inputs = ['text'], outputs = ['text']) {
11
+ const out = new Set(outputs);
12
+ if (out.has('image')) return 'image';
13
+ if (out.has('video')) return 'video';
14
+ if (out.has('audio')) return 'audio';
15
+ if (out.has('embedding') || out.has('embeddings')) return 'embedding';
16
+ return inputs.some((i) => i !== 'text') ? 'multimodal' : 'text';
17
+ }
18
+
19
+ const PIPELINE_TAG_MODALITY = {
20
+ 'text-generation': 'text', 'text2text-generation': 'text', 'conversational': 'text',
21
+ 'image-text-to-text': 'multimodal', 'visual-question-answering': 'multimodal',
22
+ 'text-to-image': 'image', 'image-to-image': 'image',
23
+ 'text-to-video': 'video', 'image-to-video': 'video', 'image-text-to-video': 'video',
24
+ 'text-to-speech': 'audio', 'automatic-speech-recognition': 'audio', 'audio-to-audio': 'audio',
25
+ 'feature-extraction': 'embedding', 'sentence-similarity': 'embedding',
26
+ };
27
+
28
+ /** Per-token price strings/values -> USD per MTok. Values >= 0.01 are assumed per-MTok already. */
29
+ const perMtok = (v) => {
30
+ const n = Number(v);
31
+ if (!Number.isFinite(n) || n <= 0) return 0;
32
+ return Math.round((n < 0.01 ? n * 1_000_000 : n) * 1e6) / 1e6;
33
+ };
34
+
35
+ async function saveImportedCard(store, nodeId, origin, sourceRef, card) {
36
+ const errors = validateCard(card);
37
+ if (errors.length) return errors[0];
38
+ await store.upsertNode(nodeId, origin, sourceRef, 'indexed', 'import');
39
+ await store.saveRevision(nodeId, card.revision, card, false);
40
+ return null;
41
+ }
42
+
43
+ /**
44
+ * Import a models.dev-shaped catalog: { providerId: { id, name, api?, doc?, models: { key: model } } }.
45
+ * Each provider becomes a synthetic node `import.modelsdev.{provider}`; each model an offering.
46
+ * Returns { providers, offerings, skipped }.
47
+ */
48
+ export async function importModelsDev(store, catalog) {
49
+ let providers = 0, offerings = 0;
50
+ const skipped = [];
51
+ for (const [providerId, provider] of Object.entries(catalog ?? {})) {
52
+ const models = provider?.models ?? {};
53
+ const offeringList = [];
54
+ for (const [modelKey, model] of Object.entries(models)) {
55
+ const outputs = model?.modalities?.output ?? ['text'];
56
+ const modality = outputs.includes('text') ? 'text' : outputs.includes('image') ? 'image' : 'text';
57
+ offeringList.push({
58
+ offering_id: slug(modelKey) || 'model',
59
+ modality,
60
+ model: {
61
+ name: model?.name ?? modelKey,
62
+ family: slug(providerId),
63
+ artifact: `urn:proprietary:${slug(providerId).replace(/[._]/g, '-')}:${slug(model?.id ?? modelKey).replace(/\//g, '-')}`,
64
+ },
65
+ serving: {
66
+ ...(model?.limit?.context ? { context_window: Math.floor(model.limit.context) } : {}),
67
+ ...(model?.limit?.output ? { max_output_tokens: Math.floor(model.limit.output) } : {}),
68
+ ...(model?.tool_call ? { supports: ['tool_calls'] } : {}),
69
+ },
70
+ binding: { profile: 'onp.openai.chat/v1', model_id: model?.id ?? modelKey },
71
+ pricing: {
72
+ currency: 'USD',
73
+ input_per_mtok: Number(model?.cost?.input ?? 0),
74
+ output_per_mtok: Number(model?.cost?.output ?? 0),
75
+ schemes: ['prepaid'],
76
+ },
77
+ });
78
+ }
79
+ if (!offeringList.length) { skipped.push(providerId); continue; }
80
+
81
+ const nodeId = `import.modelsdev.${slug(providerId).replace(/[._]/g, '-')}`;
82
+ const base = provider?.api ?? provider?.doc ?? 'https://models.dev';
83
+ const card = {
84
+ onp: '0.1',
85
+ revision: new Date().toISOString(),
86
+ node: {
87
+ id: nodeId,
88
+ name: provider?.name ?? providerId,
89
+ description: `Imported from models.dev; metadata only, unclaimed by the operator.`,
90
+ operator: { name: provider?.name ?? providerId },
91
+ },
92
+ endpoints: { openai: base, health: `${base.replace(/\/$/, '')}/health` },
93
+ offerings: offeringList,
94
+ payment: { schemes: [{ scheme: 'prepaid' }] },
95
+ };
96
+ const errors = validateCard(card);
97
+ if (errors.length) { skipped.push(`${providerId}: ${errors[0]}`); continue; }
98
+
99
+ await store.upsertNode(nodeId, new URL(base).origin, `import:modelsdev:${providerId}`, 'indexed', 'import');
100
+ await store.saveRevision(nodeId, card.revision, card, false);
101
+ providers++;
102
+ offerings += offeringList.length;
103
+ }
104
+ return { providers, offerings, skipped };
105
+ }
106
+
107
+ export const HF_ROUTER_URL = 'https://router.huggingface.co/v1/models';
108
+ export const HF_HUB_URL = 'https://huggingface.co/api/models?inference_provider=all&limit=1000&expand[]=inferenceProviderMapping&expand[]=pipeline_tag&expand[]=safetensors';
109
+
110
+ /**
111
+ * Import Hugging Face Inference Providers: `router` is the /v1/models response
112
+ * (live serving: per-provider status/capabilities/perf), `hub` the Hub API list
113
+ * (params_b from safetensors, pipeline_tag). One synthetic node per provider
114
+ * (`import.hf.{provider}`); artifact `hf:{repo}` is authoritative.
115
+ */
116
+ export async function importHuggingFace(store, router, hub = []) {
117
+ const hubById = new Map((hub ?? []).map((m) => [m.id, m]));
118
+ const byProvider = new Map(); // provider -> offerings[]
119
+
120
+ for (const model of router?.data ?? []) {
121
+ const hubEntry = hubById.get(model.id);
122
+ const paramsB = hubEntry?.safetensors?.total
123
+ ? Math.round(hubEntry.safetensors.total / 1e7) / 100 : undefined;
124
+ const inputs = model.architecture?.input_modalities ?? ['text'];
125
+ const outputs = model.architecture?.output_modalities ?? ['text'];
126
+ const modality = hubEntry?.pipeline_tag && PIPELINE_TAG_MODALITY[hubEntry.pipeline_tag]
127
+ ? PIPELINE_TAG_MODALITY[hubEntry.pipeline_tag]
128
+ : mapModalities(inputs, outputs);
129
+
130
+ for (const p of model.providers ?? []) {
131
+ if (p.status !== 'live') continue;
132
+ const supports = ['streaming'];
133
+ if (p.supports_tools) supports.push('tool_calls');
134
+ if (p.supports_structured_output) supports.push('json_mode');
135
+ if (inputs.includes('image') && outputs.includes('text')) supports.push('vision');
136
+ const offering = {
137
+ offering_id: slug(model.id),
138
+ modality,
139
+ modalities: { input: inputs, output: outputs }, // Draft 0.2 field, additive
140
+ model: {
141
+ name: model.id,
142
+ family: vendorSlug(model.owned_by ?? model.id.split('/')[0]),
143
+ artifact: `hf:${model.id}`,
144
+ ...(paramsB ? { params_b: paramsB } : {}),
145
+ },
146
+ serving: {
147
+ supports,
148
+ ...(p.context_length ? { context_window: Math.floor(p.context_length) } : {}),
149
+ ...(Number.isFinite(p.first_token_latency_ms)
150
+ ? { expected_ttft_ms: { p50: Math.round(p.first_token_latency_ms), basis: 'claimed' } } : {}),
151
+ ...(Number.isFinite(p.throughput)
152
+ ? { expected_tps: { p50: Math.round(p.throughput), basis: 'claimed' } } : {}),
153
+ },
154
+ binding: { profile: 'onp.openai.chat/v1', model_id: `${model.id}:${p.provider}` },
155
+ pricing: {
156
+ currency: 'USD',
157
+ input_per_mtok: perMtok(p.pricing?.input),
158
+ output_per_mtok: perMtok(p.pricing?.output),
159
+ schemes: p.is_free ? ['free'] : ['prepaid'],
160
+ },
161
+ };
162
+ if (!byProvider.has(p.provider)) byProvider.set(p.provider, []);
163
+ byProvider.get(p.provider).push(offering);
164
+ }
165
+ }
166
+
167
+ let providers = 0, offerings = 0;
168
+ const skipped = [];
169
+ for (const [provider, offeringList] of byProvider) {
170
+ const nodeId = `import.hf.${vendorSlug(provider)}`;
171
+ const card = {
172
+ onp: '0.1',
173
+ revision: new Date().toISOString(),
174
+ node: {
175
+ id: nodeId,
176
+ name: `${provider} (via Hugging Face Inference Providers)`,
177
+ description: 'Imported from the Hugging Face Inference Providers router; metadata only, unclaimed by the operator.',
178
+ operator: { name: provider },
179
+ },
180
+ endpoints: { openai: 'https://router.huggingface.co/v1', health: 'https://router.huggingface.co/health' },
181
+ offerings: offeringList,
182
+ payment: { schemes: [{ scheme: 'prepaid', signup: 'https://huggingface.co/settings/billing' }] },
183
+ };
184
+ const err = await saveImportedCard(store, nodeId, 'https://router.huggingface.co', `import:hf:${provider}`, card);
185
+ if (err) { skipped.push(`${provider}: ${err}`); continue; }
186
+ providers++;
187
+ offerings += offeringList.length;
188
+ }
189
+ return { providers, offerings, skipped };
190
+ }
191
+
192
+ export const OLLAMA_LIBRARY_URL = 'https://ollama.com/library';
193
+
194
+ /** Parse ollama.com/library HTML into entries. Resilient to styling churn:
195
+ * anchors on /library/{name}, spans classified by content (sizes look like "27b"/"270m"). */
196
+ export function parseOllamaLibrary(html) {
197
+ const entries = [];
198
+ const blocks = String(html).split('href="/library/').slice(1);
199
+ for (const block of blocks) {
200
+ const name = block.match(/^([a-z0-9._-]+)"/)?.[1];
201
+ if (!name) continue;
202
+ const chunk = block.slice(0, 6000);
203
+ const description = chunk.match(/<p[^>]*class="[^"]*max-w-lg[^"]*"[^>]*>\s*([^<]{1,400}?)\s*<\/p>/)?.[1]?.trim() ?? null;
204
+ const spans = [...chunk.matchAll(/<span[^>]*class="[^"]*inline-flex[^"]*"[^>]*>\s*([^<]{1,40}?)\s*<\/span>/g)].map((m) => m[1].trim());
205
+ const sizes = spans.filter((s) => /^\d+(\.\d+)?[mb]$/i.test(s));
206
+ const capabilities = spans.filter((s) => !/^\d+(\.\d+)?[mb]$/i.test(s));
207
+ const pulls = chunk.match(/<span[^>]*>\s*([\d.]+[KMB]?)\s*<\/span>\s*<span[^>]*>&nbsp;Pulls/)?.[1] ?? null;
208
+ entries.push({ name, description, capabilities, sizes, pulls });
209
+ }
210
+ return entries;
211
+ }
212
+
213
+ const sizeToParamsB = (s) => {
214
+ const m = String(s).match(/^(\d+(?:\.\d+)?)([mb])$/i);
215
+ if (!m) return null;
216
+ return m[2].toLowerCase() === 'b' ? Number(m[1]) : Math.round(Number(m[1]) / 10) / 100;
217
+ };
218
+
219
+ /**
220
+ * Import the Ollama library as locally-runnable offerings on a synthetic node whose
221
+ * endpoint is the user's OWN machine (http://127.0.0.1:11434/v1) — free, local, and
222
+ * directly usable from the model-page playground once `ollama run <name>` has pulled it.
223
+ */
224
+ export async function importOllamaLibrary(store, entries) {
225
+ const offeringList = [];
226
+ for (const e of entries ?? []) {
227
+ if (!e?.name) continue;
228
+ const caps = (e.capabilities ?? []).map((c) => String(c).toLowerCase());
229
+ const modality = caps.includes('embedding') ? 'embedding' : (caps.includes('vision') ? 'multimodal' : 'text');
230
+ const paramsCandidates = (e.sizes ?? []).map(sizeToParamsB).filter((n) => n != null);
231
+ const supports = ['streaming'];
232
+ if (caps.includes('tools')) supports.push('tool_calls');
233
+ if (caps.includes('vision')) supports.push('vision');
234
+ offeringList.push({
235
+ offering_id: slug(e.name),
236
+ modality,
237
+ model: {
238
+ name: e.name,
239
+ family: vendorSlug(e.name.split(/[.:0-9]/)[0] || e.name),
240
+ artifact: `urn:proprietary:ollama:${vendorSlug(e.name)}`,
241
+ ...(paramsCandidates.length ? { params_b: Math.max(...paramsCandidates) } : {}),
242
+ ...(e.description ? { description: String(e.description).slice(0, 500) } : {}),
243
+ },
244
+ serving: { supports },
245
+ binding: {
246
+ profile: modality === 'embedding' ? 'onp.openai.embeddings/v1' : 'onp.openai.chat/v1',
247
+ model_id: e.name,
248
+ },
249
+ pricing: { currency: 'USD', input_per_mtok: 0, output_per_mtok: 0, schemes: ['free'] },
250
+ local: {
251
+ runtime: 'ollama',
252
+ run: `ollama run ${e.name}`,
253
+ ...(e.pulls ? { pulls: e.pulls } : {}),
254
+ ...(e.sizes?.length ? { sizes: e.sizes } : {}),
255
+ },
256
+ });
257
+ }
258
+ if (!offeringList.length) return { providers: 0, offerings: 0, skipped: ['no entries'] };
259
+ const nodeId = 'import.ollama.library';
260
+ const card = {
261
+ onp: '0.1',
262
+ revision: new Date().toISOString(),
263
+ node: {
264
+ id: nodeId,
265
+ name: 'Ollama library (runs on your machine)',
266
+ description: 'Locally-runnable models from the Ollama library. The endpoint is your own machine: install Ollama, `ollama run <model>`, and these offerings serve at localhost — free, private, no network.',
267
+ operator: { name: 'Ollama (community library)' },
268
+ },
269
+ endpoints: { openai: 'http://127.0.0.1:11434/v1', health: 'http://127.0.0.1:11434' },
270
+ offerings: offeringList,
271
+ payment: { schemes: [{ scheme: 'free' }] },
272
+ };
273
+ const err = await saveImportedCard(store, nodeId, 'https://ollama.com', 'import:ollama-library', card);
274
+ if (err) return { providers: 0, offerings: 0, skipped: [err] };
275
+ return { providers: 1, offerings: offeringList.length, skipped: [] };
276
+ }
277
+
278
+ export const OPENROUTER_URL = 'https://openrouter.ai/api/v1/models';
279
+
280
+ /**
281
+ * Import the OpenRouter catalog as one synthetic node (`import.openrouter.catalog`):
282
+ * per-token price strings -> per-MTok; `hugging_face_id` gives the artifact join
283
+ * where present, else a proprietary URN under the model author's namespace.
284
+ */
285
+ export async function importOpenRouter(store, catalog) {
286
+ const offeringList = [];
287
+ for (const model of catalog?.data ?? []) {
288
+ const inputs = model.architecture?.input_modalities ?? ['text'];
289
+ const outputs = model.architecture?.output_modalities ?? ['text'];
290
+ const supports = ['streaming'];
291
+ const params = model.supported_parameters ?? [];
292
+ if (params.includes('tools')) supports.push('tool_calls');
293
+ if (params.includes('response_format') || params.includes('structured_outputs')) supports.push('json_mode');
294
+ if (inputs.includes('image') && outputs.includes('text')) supports.push('vision');
295
+ const author = vendorSlug(model.id.split('/')[0] || 'unknown');
296
+ offeringList.push({
297
+ offering_id: slug(model.id),
298
+ modality: mapModalities(inputs, outputs),
299
+ modalities: { input: inputs, output: outputs },
300
+ model: {
301
+ name: model.name ?? model.id,
302
+ family: author,
303
+ artifact: model.hugging_face_id
304
+ ? `hf:${model.hugging_face_id}`
305
+ : `urn:proprietary:${author}:${slug(model.id.split('/').slice(1).join('-') || model.id)}`,
306
+ ...(model.description ? { description: String(model.description).slice(0, 500) } : {}),
307
+ ...(Number.isFinite(model.created)
308
+ ? { released: new Date(model.created * 1000).toISOString().slice(0, 10) } : {}),
309
+ },
310
+ serving: {
311
+ supports,
312
+ ...(model.context_length ? { context_window: Math.floor(model.context_length) } : {}),
313
+ ...(model.top_provider?.max_completion_tokens
314
+ ? { max_output_tokens: Math.floor(model.top_provider.max_completion_tokens) } : {}),
315
+ },
316
+ binding: { profile: 'onp.openai.chat/v1', model_id: model.id },
317
+ pricing: {
318
+ currency: 'USD',
319
+ input_per_mtok: perMtok(model.pricing?.prompt),
320
+ output_per_mtok: perMtok(model.pricing?.completion),
321
+ ...(model.pricing?.input_cache_read
322
+ ? { cached_input_per_mtok: perMtok(model.pricing.input_cache_read) } : {}),
323
+ schemes: Number(model.pricing?.prompt) === 0 ? ['free'] : ['prepaid'],
324
+ },
325
+ });
326
+ }
327
+ if (!offeringList.length) return { providers: 0, offerings: 0, skipped: ['empty catalog'] };
328
+
329
+ const nodeId = 'import.openrouter.catalog';
330
+ const card = {
331
+ onp: '0.1',
332
+ revision: new Date().toISOString(),
333
+ node: {
334
+ id: nodeId,
335
+ name: 'OpenRouter catalog',
336
+ description: 'Imported from the public OpenRouter models API; metadata only, unclaimed by the operator.',
337
+ operator: { name: 'OpenRouter' },
338
+ },
339
+ endpoints: { openai: 'https://openrouter.ai/api/v1', health: 'https://openrouter.ai/api/v1/models' },
340
+ offerings: offeringList,
341
+ payment: { schemes: [{ scheme: 'prepaid', signup: 'https://openrouter.ai/keys' }] },
342
+ };
343
+ const err = await saveImportedCard(store, nodeId, 'https://openrouter.ai', 'import:openrouter', card);
344
+ if (err) return { providers: 0, offerings: 0, skipped: [err] };
345
+ return { providers: 1, offerings: offeringList.length, skipped: [] };
346
+ }