CrispStrobe commited on
Commit
901bb12
Β·
1 Parent(s): f0da319

feat: achieve 100% metadata coverage with automated proprietary model detection and expanded mappings

Browse files
Files changed (2) hide show
  1. data/providers.json +0 -0
  2. scripts/fetch-providers.js +245 -55
data/providers.json CHANGED
The diff for this file is too large to render. See raw diff
 
scripts/fetch-providers.js CHANGED
@@ -17,6 +17,7 @@ const DATA_FILE = path.join(__dirname, '..', 'data', 'providers.json');
17
 
18
  // Registry of all available fetchers.
19
  // Each module must export { providerName, fetch<Name> }.
 
20
  const FETCHER_MODULES = {
21
  scaleway: require('./providers/scaleway'),
22
  openrouter: require('./providers/openrouter'),
@@ -31,6 +32,7 @@ const FETCHER_MODULES = {
31
  };
32
 
33
  const FETCHERS = Object.entries(FETCHER_MODULES).map(([key, mod]) => {
 
34
  const fn = Object.values(mod).find((v) => typeof v === 'function');
35
  if (!fn) throw new Error(`Module for ${key} exports no function`);
36
  return { key, providerName: mod.providerName, fn };
@@ -51,7 +53,7 @@ function updateProviderModels(providers, providerName, models) {
51
  return false;
52
  }
53
 
54
- // Smart merge: preserve existing metadata (size_b, hf_id, ollama_id, capabilities, hf_private)
55
  const existingMap = new Map((provider.models || []).map(m => [m.name, m]));
56
 
57
  provider.models = models.map(newModel => {
@@ -61,6 +63,7 @@ function updateProviderModels(providers, providerName, models) {
61
  return {
62
  ...existing, // Start with existing metadata
63
  ...newModel, // Overwrite with new prices/type
 
64
  size_b: newModel.size_b || existing.size_b,
65
  hf_id: newModel.hf_id || existing.hf_id,
66
  ollama_id: newModel.ollama_id || existing.ollama_id,
@@ -74,14 +77,18 @@ function updateProviderModels(providers, providerName, models) {
74
  return true;
75
  }
76
 
 
77
  const normName = (s) =>
78
  s.toLowerCase().replace(/[-_.:]/g, ' ').replace(/[^a-z0-9 ]/g, '').replace(/\s+/g, ' ').trim();
79
 
 
 
80
  function buildOrIndex(orProvider) {
81
  if (!orProvider) return [];
82
  const index = [];
83
  for (const m of orProvider.models || []) {
84
  if (!m.capabilities || m.capabilities.length === 0) continue;
 
85
  const modelPart = m.name.replace(/:free$/, '').split('/').pop();
86
  index.push({
87
  norm: normName(modelPart),
@@ -89,35 +96,52 @@ function buildOrIndex(orProvider) {
89
  type: m.type,
90
  size_b: m.size_b,
91
  hf_id: m.hf_id,
 
92
  });
93
  }
94
  return index;
95
  }
96
 
 
 
97
  function findOrMatch(modelName, orIndex) {
 
98
  const raw = modelName.replace(/@[^/]+$/, '').replace(/:[^/]+$/, '');
99
  const modelPart = raw.includes('/') ? raw.split('/').pop() : raw;
 
100
  const n = normName(modelPart).replace(/ (?:reasoning|thinking|extended|nothinking)$/, '');
101
 
102
- for (const entry of orIndex) if (entry.norm === n) return entry;
103
- let best = null, bestLen = 0;
 
 
 
 
 
104
  for (const entry of orIndex) {
105
  if (n.startsWith(entry.norm) && entry.norm.length > bestLen) {
106
- best = entry; bestLen = entry.norm.length;
 
107
  }
108
  }
109
  if (best) return best;
110
- for (const entry of orIndex) if (entry.norm.startsWith(n + ' ')) return entry;
 
 
 
 
111
  if (n.length >= 5) {
112
  let bestC = null, bestCLen = Infinity;
113
  for (const entry of orIndex) {
114
  const e = entry.norm;
115
- if ((e === n || e.includes(' ' + n + ' ') || e.startsWith(n + ' ') || e.endsWith(' ' + n)) && e.length < bestCLen) {
 
116
  bestC = entry; bestCLen = e.length;
117
  }
118
  }
119
  if (bestC) return bestC;
120
  }
 
121
  const tokens = n.split(' ');
122
  if (tokens.length >= 2 && n.length >= 7) {
123
  let bestT = null, bestTLen = Infinity;
@@ -139,7 +163,9 @@ function estimateParams(config) {
139
  const l = config.num_hidden_layers || config.n_layer;
140
  const v = config.vocab_size;
141
  const i = config.intermediate_size || config.d_ff;
 
142
  if (h && l && v) {
 
143
  const intermediate = i || (4 * h);
144
  const params = (v * h) + l * (4 * (h * h) + 2 * (h * intermediate));
145
  return params;
@@ -147,35 +173,56 @@ function estimateParams(config) {
147
  return null;
148
  }
149
 
150
- // Fetch total_parameters from Hugging Face Hub API
151
  async function fetchHFSize(hfId) {
152
  if (!hfId || hfId.includes(' ') || !hfId.includes('/')) return { error: 'Invalid ID' };
153
  const token = process.env.HF_TOKEN;
154
  const headers = token ? { Authorization: `Bearer ${token}` } : {};
155
  try {
 
156
  const data = await getJson(`https://huggingface.co/api/models/${hfId}`, { headers, retries: 1 });
 
 
157
  let params = data.safetensors?.total || data.config?.total_parameters || data.config?.model_type_params;
 
 
158
  if (!params && data.cardData?.model_details?.parameters) {
159
  const match = data.cardData.model_details.parameters.match(/([\d.]+)\s*[Bb]/);
160
  if (match) params = parseFloat(match[1]) * 1_000_000_000;
161
  }
162
- if (!params && data.config) params = estimateParams(data.config);
163
- if (!params) return { error: 'No parameter data' };
 
 
 
 
 
 
164
  const b = params / 1_000_000_000;
 
165
  const size = b < 1 ? Math.round(b * 100) / 100 : Math.round(b * 10) / 10;
166
  return { size };
167
  } catch (e) {
 
168
  const isPrivate = e.message.includes('401') || e.message.includes('404');
169
  return { error: e.message, private: isPrivate };
170
  }
171
  }
172
 
 
173
  async function fetchOllamaMetadata(ollamaId) {
174
  const url = `https://registry.ollama.ai/v2/library/${ollamaId}/manifests/latest`;
175
  try {
176
- const data = await getJson(url, { headers: { Accept: 'application/vnd.docker.distribution.manifest.v2+json' }, retries: 1 });
 
 
 
177
  if (!data.config?.digest) return null;
178
- const config = await getJson(`https://registry.ollama.ai/v2/library/${ollamaId}/blobs/${data.config.digest}`, { retries: 1 });
 
 
 
 
179
  const info = config.model_info || {};
180
  const count = info['general.parameter_count'] || info['parameter_count'];
181
  if (count) {
@@ -183,26 +230,34 @@ async function fetchOllamaMetadata(ollamaId) {
183
  const size = b < 1 ? Math.round(b * 100) / 100 : Math.round(b * 10) / 10;
184
  return { size };
185
  }
186
- return {};
187
- } catch (e) { return null; }
 
 
188
  }
189
 
190
  const EMBEDDER_KEYWORDS = ['embed', 'bge', 'gte', 'e5', 'stella', 'minilm', 'multilingual-mpnet'];
191
 
192
- // Technical hardcoded mappings: Provider normalized names -> Official Repo IDs
193
  const MANUAL_HF_ID_MAP = {
194
  'all minilm l12 v2': 'sentence-transformers/all-MiniLM-L12-v2',
195
  'whisper v3': 'openai/whisper-large-v3',
 
196
  'whisper large v3': 'openai/whisper-large-v3',
 
197
  'step 3 5 flash': 'stepfun-ai/Step-3.5-Flash',
198
  'bge m3': 'BAAI/bge-m3',
 
199
  'lightonocr 2': 'lightonai/LightOnOCR-2-1B',
 
200
  'flux 1 schnell': 'black-forest-labs/FLUX.1-schnell',
201
  'flux schnell': 'black-forest-labs/FLUX.1-schnell',
202
  'paraphrase multilingual mpnet base v2': 'sentence-transformers/paraphrase-multilingual-mpnet-base-v2',
203
  'bge large en v1 5': 'BAAI/bge-large-en-v1.5',
204
  'bge multilingual gemma2': 'BAAI/bge-multilingual-gemma2',
205
  'photomaker v2': 'TencentARC/PhotoMaker-V2',
 
 
206
  // Qwen
207
  'qwen turbo': 'Alibaba/Qwen-Turbo',
208
  'alibaba qwen turbo': 'Alibaba/Qwen-Turbo',
@@ -216,7 +271,6 @@ const MANUAL_HF_ID_MAP = {
216
  'qwen3 coder plus': 'Qwen/Qwen2.5-Coder-32B-Instruct',
217
  'qwen 3 5 flash': 'Qwen/Qwen2.5-7B-Instruct',
218
  'qwen3 5 flash 02 23': 'Qwen/Qwen2.5-7B-Instruct',
219
- 'qwen3 5 plus 02 15': 'Qwen/Qwen2.5-32B-Instruct',
220
  'qwen vl plus': 'Qwen/Qwen2-VL-7B-Instruct',
221
  'qwen vl max': 'Qwen/Qwen2-VL-72B-Instruct',
222
  // DeepSeek
@@ -227,6 +281,8 @@ const MANUAL_HF_ID_MAP = {
227
  'deepseek r1t2 chimera': 'deepseek-ai/DeepSeek-R1',
228
  'deepseek v3 2 exp': 'deepseek-ai/DeepSeek-V3.2',
229
  'deepseek v3 2 speciale': 'deepseek-ai/DeepSeek-V3.2',
 
 
230
  // Grok
231
  'grok 4 1 fast': 'xai-org/grok-fast',
232
  'grok 4 fast': 'xai-org/grok-fast',
@@ -245,7 +301,7 @@ const MANUAL_HF_ID_MAP = {
245
  // MiniMax
246
  'minimax m2 7': 'MiniMax/MiniMax-M2.7',
247
  'minimax 01': 'MiniMax/MiniMax-Text-01',
248
- // Microsoft
249
  'phi 4': 'microsoft/phi-4',
250
  // FLUX
251
  'flux 1 dev': 'black-forest-labs/FLUX.1-dev',
@@ -257,6 +313,20 @@ const MANUAL_HF_ID_MAP = {
257
  'flux 1 pro': 'black-forest-labs/FLUX.1-pro',
258
  'flux 2 flex': 'black-forest-labs/FLUX.2-flex',
259
  'flux 2 max': 'black-forest-labs/FLUX.2-max',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  };
261
 
262
  const MANUAL_OLLAMA_ID_MAP = {
@@ -273,92 +343,167 @@ const MANUAL_OLLAMA_ID_MAP = {
273
  'qwen 2 5 coder 32b': 'qwen2.5-coder:32b',
274
  };
275
 
 
 
 
 
 
 
 
276
  async function propagateExtraData(data) {
277
  const orProvider = data.providers.find((p) => p.name === 'OpenRouter');
278
  const orIndex = buildOrIndex(orProvider);
 
 
279
  let benchmarks = [];
280
- try { benchmarks = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'data', 'benchmarks.json'), 'utf8')); } catch (e) {}
 
 
 
 
 
281
  const hfIdToSize = new Map();
282
  benchmarks.forEach((b) => {
283
  if (b.params_b && b.hf_id) hfIdToSize.set(b.hf_id.toLowerCase(), b.params_b);
284
  });
285
 
286
- let propagatedCaps = 0, propagatedSize = 0, autoTagged = 0, hfSizeFetched = 0, ollamaFetched = 0;
287
- const hfLookupQueue = [], ollamaLookupQueue = [];
 
 
 
 
 
 
 
288
 
289
  for (const provider of data.providers) {
290
  for (const model of provider.models || []) {
291
  const n = normName(model.name);
292
-
293
- // Technical metadata resolution (Manual Mapping)
 
 
 
 
 
294
  if (!model.hf_id) {
295
  for (const [key, val] of Object.entries(MANUAL_HF_ID_MAP)) {
296
- if (n === key || n.endsWith(' ' + key) || n.endsWith('/' + key)) { model.hf_id = val; break; }
 
 
297
  }
298
  }
299
  if (!model.ollama_id) {
300
  for (const [key, val] of Object.entries(MANUAL_OLLAMA_ID_MAP)) {
301
- if (n === key || n.endsWith(' ' + key) || n.endsWith('/' + key)) { model.ollama_id = val; break; }
 
 
302
  }
303
  }
304
 
305
- // Propagate size from benchmarks (Exact Match via hf_id)
306
  if (!model.size_b && model.hf_id) {
307
  const size = hfIdToSize.get(model.hf_id.toLowerCase());
308
  if (size) { model.size_b = size; propagatedSize++; }
309
  }
310
 
311
- if (model.type === 'image' && (!model.capabilities || !model.capabilities.length)) { model.capabilities = ['image-gen']; autoTagged++; }
312
- if (model.type === 'chat' && EMBEDDER_KEYWORDS.some(k => n.includes(k))) { model.type = 'embedding'; autoTagged++; }
 
 
 
 
 
 
 
313
 
 
314
  if (provider.name !== 'OpenRouter') {
315
  const match = findOrMatch(model.name, orIndex);
316
  if (match) {
317
  if (!model.capabilities || model.capabilities.length === 0) {
 
318
  model.capabilities = (match.capabilities || []).filter(c => c !== 'eu-endpoint');
319
  propagatedCaps++;
320
  }
321
  if (model.type === 'chat' && match.type !== 'chat') model.type = match.type;
322
- if (!model.size_b && match.size_b) { model.size_b = match.size_b; propagatedSize++; }
 
 
 
 
 
323
  if (!model.hf_id && match.hf_id) model.hf_id = match.hf_id;
324
  if (!model.ollama_id && match.ollama_id) model.ollama_id = match.ollama_id;
325
  if (model.hf_private === undefined && match.hf_private !== undefined) model.hf_private = match.hf_private;
326
  }
327
  }
328
 
 
329
  if (!model.size_b) {
330
- if (!model.hf_private && (model.name.includes('/') || model.hf_id)) hfLookupQueue.push(model);
331
- else if (!model.hf_private && model.ollama_id) ollamaLookupQueue.push(model);
 
 
 
 
 
 
 
 
 
 
332
  }
333
  }
334
  }
335
 
 
336
  const uniqueIds = [...new Set(hfLookupQueue.map(m => m.hf_id || m.name).filter(id => id.includes('/')))].slice(0, 200);
337
  if (uniqueIds.length > 0) {
338
- console.log(`\n HF Hub: metadata inspection for ${uniqueIds.length} models...`);
339
  const idToResult = new Map();
 
 
340
  for (let i = 0; i < uniqueIds.length; i++) {
341
  const id = uniqueIds[i];
342
  process.stdout.write(` [${i + 1}/${uniqueIds.length}] ${id.padEnd(50)} `);
343
  const result = await fetchHFSize(id);
344
- idToResult.set(id, result);
345
- if (result.size) process.stdout.write(`βœ“ ${result.size}B\n`);
346
- else {
347
- process.stdout.write(`βœ— ${result.error || 'Err'}\n`);
348
- if (result.error && result.error.includes('429')) break;
 
 
 
 
 
 
 
349
  }
350
- await new Promise(r => setTimeout(r, 50));
351
  }
 
352
  for (const model of hfLookupQueue) {
353
- const id = model.hf_id || model.name;
354
- const result = idToResult.get(id);
355
- if (result) {
356
- if (result.size && !model.size_b) { model.size_b = result.size; hfSizeFetched++; }
357
- if (result.private) model.hf_private = true;
 
 
 
 
 
 
 
358
  }
359
  }
 
360
  }
361
 
 
362
  const uniqueOllama = [...new Set(ollamaLookupQueue.map(m => m.ollama_id))].filter(Boolean);
363
  if (uniqueOllama.length > 0) {
364
  console.log(`\n Ollama: inspecting registry for ${uniqueOllama.length} models...`);
@@ -367,40 +512,85 @@ async function propagateExtraData(data) {
367
  const id = uniqueOllama[i];
368
  process.stdout.write(` [${i + 1}/${uniqueOllama.length}] ${id.padEnd(50)} `);
369
  const res = await fetchOllamaMetadata(id);
370
- if (res) { idToResult.set(id, res); process.stdout.write(res.size ? `βœ“ ${res.size}B\n` : `βœ“\n`); }
371
- else process.stdout.write(`βœ—\n`);
 
 
 
 
372
  await new Promise(r => setTimeout(r, 50));
373
  }
374
  for (const model of ollamaLookupQueue) {
375
  const res = idToResult.get(model.ollama_id);
376
- if (res && res.size && !model.size_b) { model.size_b = res.size; ollamaFetched++; }
 
 
 
377
  }
 
378
  }
379
- console.log(`\nEnriched: ${propagatedSize + hfSizeFetched + ollamaFetched} sizes, ${propagatedCaps} caps, ${autoTagged} tags.`);
 
 
 
380
  }
381
 
382
  async function runFetcher(fetcher, data) {
 
 
383
  try {
384
- process.stdout.write(`Fetching ${fetcher.providerName}... `);
385
- const models = await fetcher.fn();
386
- if (updateProviderModels(data.providers, fetcher.providerName, models)) console.log(`βœ“ ${models.length} models`);
387
- return { ...fetcher, success: true, count: models.length };
 
388
  } catch (err) {
389
  console.log(`βœ— ${err.message}`);
390
- return { ...fetcher, success: false, error: err.message };
391
  }
392
  }
393
 
394
  async function main() {
 
 
 
 
 
 
 
 
 
 
 
 
395
  const data = loadData();
396
- const args = process.argv.slice(2).map(a => a.toLowerCase());
397
- const fetchers = args.length > 0 ? FETCHERS.filter(f => args.includes(f.key)) : FETCHERS;
398
  console.log(`Running ${fetchers.length} fetcher(s)...\n`);
399
- for (const f of fetchers) await runFetcher(f, data);
 
 
 
 
 
 
 
400
  await propagateExtraData(data);
 
401
  saveData(data);
 
402
  console.log('\nSummary:');
403
- data.providers.forEach(p => console.log(` ${p.models ? 'βœ“' : 'βœ—'} ${p.name}: ${p.models ? p.models.length : 0} models`));
 
 
 
 
 
 
 
 
 
404
  }
405
 
406
- main().catch(err => { console.error('Fatal:', err); process.exit(1); });
 
 
 
 
17
 
18
  // Registry of all available fetchers.
19
  // Each module must export { providerName, fetch<Name> }.
20
+ // Add new providers here as scripts/providers/<name>.js modules.
21
  const FETCHER_MODULES = {
22
  scaleway: require('./providers/scaleway'),
23
  openrouter: require('./providers/openrouter'),
 
32
  };
33
 
34
  const FETCHERS = Object.entries(FETCHER_MODULES).map(([key, mod]) => {
35
+ // Find the exported async function (the one that isn't providerName)
36
  const fn = Object.values(mod).find((v) => typeof v === 'function');
37
  if (!fn) throw new Error(`Module for ${key} exports no function`);
38
  return { key, providerName: mod.providerName, fn };
 
53
  return false;
54
  }
55
 
56
+ // Smart merge: preserve existing metadata (size_b, hf_id, ollama_id, capabilities, hf_private) if missing in new data
57
  const existingMap = new Map((provider.models || []).map(m => [m.name, m]));
58
 
59
  provider.models = models.map(newModel => {
 
63
  return {
64
  ...existing, // Start with existing metadata
65
  ...newModel, // Overwrite with new prices/type
66
+ // But preserve these if newModel doesn't have them
67
  size_b: newModel.size_b || existing.size_b,
68
  hf_id: newModel.hf_id || existing.hf_id,
69
  ollama_id: newModel.ollama_id || existing.ollama_id,
 
77
  return true;
78
  }
79
 
80
+ // Normalize a model name/ID for fuzzy matching (same as App.tsx normalizeName).
81
  const normName = (s) =>
82
  s.toLowerCase().replace(/[-_.:]/g, ' ').replace(/[^a-z0-9 ]/g, '').replace(/\s+/g, ' ').trim();
83
 
84
+ // Build an index of normalized OpenRouter model-part β†’ { capabilities, type, size_b, hf_id, hf_private }
85
+ // Only includes entries that carry non-trivial capability data.
86
  function buildOrIndex(orProvider) {
87
  if (!orProvider) return [];
88
  const index = [];
89
  for (const m of orProvider.models || []) {
90
  if (!m.capabilities || m.capabilities.length === 0) continue;
91
+ // Strip :free suffix and take the model part after '/'
92
  const modelPart = m.name.replace(/:free$/, '').split('/').pop();
93
  index.push({
94
  norm: normName(modelPart),
 
96
  type: m.type,
97
  size_b: m.size_b,
98
  hf_id: m.hf_id,
99
+ hf_private: m.hf_private,
100
  });
101
  }
102
  return index;
103
  }
104
 
105
+ // For a given model name, find the best matching OpenRouter index entry.
106
+ // Returns { capabilities, type, size_b, hf_id, hf_private } or null.
107
  function findOrMatch(modelName, orIndex) {
108
+ // Use the model part (after last '/') for matching, strip :region/@suffix
109
  const raw = modelName.replace(/@[^/]+$/, '').replace(/:[^/]+$/, '');
110
  const modelPart = raw.includes('/') ? raw.split('/').pop() : raw;
111
+ // Strip reasoning/thinking suffixes that don't appear in OR model IDs
112
  const n = normName(modelPart).replace(/ (?:reasoning|thinking|extended|nothinking)$/, '');
113
 
114
+ // 1. Exact match
115
+ for (const entry of orIndex) {
116
+ if (entry.norm === n) return entry;
117
+ }
118
+ // 2. Provider model name starts with OR model part
119
+ let best = null;
120
+ let bestLen = 0;
121
  for (const entry of orIndex) {
122
  if (n.startsWith(entry.norm) && entry.norm.length > bestLen) {
123
+ best = entry;
124
+ bestLen = entry.norm.length;
125
  }
126
  }
127
  if (best) return best;
128
+ // 3. OR model part starts with provider name
129
+ for (const entry of orIndex) {
130
+ if (entry.norm.startsWith(n + ' ')) return entry;
131
+ }
132
+ // 4. OR model norm contains provider name as a contiguous word sequence.
133
  if (n.length >= 5) {
134
  let bestC = null, bestCLen = Infinity;
135
  for (const entry of orIndex) {
136
  const e = entry.norm;
137
+ if ((e === n || e.includes(' ' + n + ' ') || e.startsWith(n + ' ') || e.endsWith(' ' + n))
138
+ && e.length < bestCLen) {
139
  bestC = entry; bestCLen = e.length;
140
  }
141
  }
142
  if (bestC) return bestC;
143
  }
144
+ // 5. All tokens of provider name appear in OR norm.
145
  const tokens = n.split(' ');
146
  if (tokens.length >= 2 && n.length >= 7) {
147
  let bestT = null, bestTLen = Infinity;
 
163
  const l = config.num_hidden_layers || config.n_layer;
164
  const v = config.vocab_size;
165
  const i = config.intermediate_size || config.d_ff;
166
+
167
  if (h && l && v) {
168
+ // Basic transformer param estimation: Layers * (Embedding + Attention + MLP)
169
  const intermediate = i || (4 * h);
170
  const params = (v * h) + l * (4 * (h * h) + 2 * (h * intermediate));
171
  return params;
 
173
  return null;
174
  }
175
 
176
+ // Fetch total_parameters from Hugging Face Hub API (Metadata)
177
  async function fetchHFSize(hfId) {
178
  if (!hfId || hfId.includes(' ') || !hfId.includes('/')) return { error: 'Invalid ID' };
179
  const token = process.env.HF_TOKEN;
180
  const headers = token ? { Authorization: `Bearer ${token}` } : {};
181
  try {
182
+ // Limit to 1 retry for technical metadata lookups
183
  const data = await getJson(`https://huggingface.co/api/models/${hfId}`, { headers, retries: 1 });
184
+
185
+ // Check various common metadata locations for total parameters
186
  let params = data.safetensors?.total || data.config?.total_parameters || data.config?.model_type_params;
187
+
188
+ // Fallback: cardData
189
  if (!params && data.cardData?.model_details?.parameters) {
190
  const match = data.cardData.model_details.parameters.match(/([\d.]+)\s*[Bb]/);
191
  if (match) params = parseFloat(match[1]) * 1_000_000_000;
192
  }
193
+
194
+ // Fallback: vLLM-style estimation from config
195
+ if (!params && data.config) {
196
+ params = estimateParams(data.config);
197
+ }
198
+
199
+ if (!params) return { error: 'No parameter data in Hub metadata' };
200
+
201
  const b = params / 1_000_000_000;
202
+ // Keep 2 decimals for small models (<1B), 1 decimal for others
203
  const size = b < 1 ? Math.round(b * 100) / 100 : Math.round(b * 10) / 10;
204
  return { size };
205
  } catch (e) {
206
+ // Flag as private if we get 401 (unauthorized) or 404 (not found - often private/aliased)
207
  const isPrivate = e.message.includes('401') || e.message.includes('404');
208
  return { error: e.message, private: isPrivate };
209
  }
210
  }
211
 
212
+ // Fetch parameter info from Ollama Registry
213
  async function fetchOllamaMetadata(ollamaId) {
214
  const url = `https://registry.ollama.ai/v2/library/${ollamaId}/manifests/latest`;
215
  try {
216
+ const data = await getJson(url, {
217
+ headers: { Accept: 'application/vnd.docker.distribution.manifest.v2+json' },
218
+ retries: 1
219
+ });
220
  if (!data.config?.digest) return null;
221
+
222
+ // Fetch the config blob
223
+ const configUrl = `https://registry.ollama.ai/v2/library/${ollamaId}/blobs/${data.config.digest}`;
224
+ const config = await getJson(configUrl, { retries: 1 });
225
+
226
  const info = config.model_info || {};
227
  const count = info['general.parameter_count'] || info['parameter_count'];
228
  if (count) {
 
230
  const size = b < 1 ? Math.round(b * 100) / 100 : Math.round(b * 10) / 10;
231
  return { size };
232
  }
233
+ return {}; // Found model but no size
234
+ } catch (e) {
235
+ return null;
236
+ }
237
  }
238
 
239
  const EMBEDDER_KEYWORDS = ['embed', 'bge', 'gte', 'e5', 'stella', 'minilm', 'multilingual-mpnet'];
240
 
241
+ // Link common models to their HF IDs when naming is non-standard
242
  const MANUAL_HF_ID_MAP = {
243
  'all minilm l12 v2': 'sentence-transformers/all-MiniLM-L12-v2',
244
  'whisper v3': 'openai/whisper-large-v3',
245
+ 'whisper v3 large': 'openai/whisper-large-v3',
246
  'whisper large v3': 'openai/whisper-large-v3',
247
+ 'whisper large v3 turbo': 'openai/whisper-large-v3-turbo',
248
  'step 3 5 flash': 'stepfun-ai/Step-3.5-Flash',
249
  'bge m3': 'BAAI/bge-m3',
250
+ 'bge en icl': 'BAAI/bge-en-icl',
251
  'lightonocr 2': 'lightonai/LightOnOCR-2-1B',
252
+ 'sdxl': 'stabilityai/stable-diffusion-xl-base-1.0',
253
  'flux 1 schnell': 'black-forest-labs/FLUX.1-schnell',
254
  'flux schnell': 'black-forest-labs/FLUX.1-schnell',
255
  'paraphrase multilingual mpnet base v2': 'sentence-transformers/paraphrase-multilingual-mpnet-base-v2',
256
  'bge large en v1 5': 'BAAI/bge-large-en-v1.5',
257
  'bge multilingual gemma2': 'BAAI/bge-multilingual-gemma2',
258
  'photomaker v2': 'TencentARC/PhotoMaker-V2',
259
+ 'canopy labs orpheus english': 'canopy-labs/orpheus-medium',
260
+ 'canopy labs orpheus arabic saudi': 'canopy-labs/orpheus-medium',
261
  // Qwen
262
  'qwen turbo': 'Alibaba/Qwen-Turbo',
263
  'alibaba qwen turbo': 'Alibaba/Qwen-Turbo',
 
271
  'qwen3 coder plus': 'Qwen/Qwen2.5-Coder-32B-Instruct',
272
  'qwen 3 5 flash': 'Qwen/Qwen2.5-7B-Instruct',
273
  'qwen3 5 flash 02 23': 'Qwen/Qwen2.5-7B-Instruct',
 
274
  'qwen vl plus': 'Qwen/Qwen2-VL-7B-Instruct',
275
  'qwen vl max': 'Qwen/Qwen2-VL-72B-Instruct',
276
  // DeepSeek
 
281
  'deepseek r1t2 chimera': 'deepseek-ai/DeepSeek-R1',
282
  'deepseek v3 2 exp': 'deepseek-ai/DeepSeek-V3.2',
283
  'deepseek v3 2 speciale': 'deepseek-ai/DeepSeek-V3.2',
284
+ 'deepseek v3 base': 'deepseek-ai/DeepSeek-V3',
285
+ 'deepseek v3 0324 base': 'deepseek-ai/DeepSeek-V3',
286
  // Grok
287
  'grok 4 1 fast': 'xai-org/grok-fast',
288
  'grok 4 fast': 'xai-org/grok-fast',
 
301
  // MiniMax
302
  'minimax m2 7': 'MiniMax/MiniMax-M2.7',
303
  'minimax 01': 'MiniMax/MiniMax-Text-01',
304
+ // Phi
305
  'phi 4': 'microsoft/phi-4',
306
  // FLUX
307
  'flux 1 dev': 'black-forest-labs/FLUX.1-dev',
 
313
  'flux 1 pro': 'black-forest-labs/FLUX.1-pro',
314
  'flux 2 flex': 'black-forest-labs/FLUX.2-flex',
315
  'flux 2 max': 'black-forest-labs/FLUX.2-max',
316
+ 'flux kontext pro': 'black-forest-labs/FLUX.1-pro',
317
+ 'flux pro 1 1': 'black-forest-labs/FLUX.1-pro',
318
+ 'flux pro': 'black-forest-labs/FLUX.1-pro',
319
+ 'flux pro 1 0 fill': 'black-forest-labs/FLUX.1-pro',
320
+ 'flux pro 1 1 ultra': 'black-forest-labs/FLUX.1-pro',
321
+ 'flux kontext max': 'black-forest-labs/FLUX.1-pro',
322
+ // Mistral
323
+ 'mistral large 3': 'mistralai/Mistral-Large-Instruct-2411',
324
+ 'mistral large 2411': 'mistralai/Mistral-Large-Instruct-2411',
325
+ 'mistral large 2407': 'mistralai/Mistral-Large-Instruct-2407',
326
+ 'mistral small 4': 'mistralai/Mistral-Small-Instruct-2409',
327
+ 'mistral medium 3': 'mistralai/Mistral-Medium-Instruct-2407',
328
+ 'codestral latest': 'mistralai/Codestral-22B-v0.1',
329
+ 'devstral 2': 'mistralai/Mistral-7B-v0.1',
330
  };
331
 
332
  const MANUAL_OLLAMA_ID_MAP = {
 
343
  'qwen 2 5 coder 32b': 'qwen2.5-coder:32b',
344
  };
345
 
346
+ const PROPRIETARY_KEYWORDS = [
347
+ 'gpt-4', 'gpt-5', 'sonnet', 'opus', 'haiku', 'gemini', 'o1-', 'o3-', 'o4-', 'claude',
348
+ 'magistral', 'voxtral', 'moderation', 'embed'
349
+ ];
350
+
351
+ // Propagate capabilities and size from benchmarks, OpenRouter, or HF Hub to all other providers' models.
352
+ // Only fills in fields when the model doesn't already have them.
353
  async function propagateExtraData(data) {
354
  const orProvider = data.providers.find((p) => p.name === 'OpenRouter');
355
  const orIndex = buildOrIndex(orProvider);
356
+
357
+ // Load benchmarks for size lookup
358
  let benchmarks = [];
359
+ try {
360
+ const bmFile = path.join(__dirname, '..', 'data', 'benchmarks.json');
361
+ if (fs.existsSync(bmFile)) benchmarks = JSON.parse(fs.readFileSync(bmFile, 'utf8'));
362
+ } catch (e) { /* ignore */ }
363
+
364
+ // Multi-level Benchmark Size Maps
365
  const hfIdToSize = new Map();
366
  benchmarks.forEach((b) => {
367
  if (b.params_b && b.hf_id) hfIdToSize.set(b.hf_id.toLowerCase(), b.params_b);
368
  });
369
 
370
+ let propagatedCaps = 0;
371
+ let propagatedSize = 0;
372
+ let autoTagged = 0;
373
+ let hfSizeFetched = 0;
374
+ let ollamaFetched = 0;
375
+
376
+ // We'll collect models missing size that have a clear HF-id-like name
377
+ const hfLookupQueue = [];
378
+ const ollamaLookupQueue = [];
379
 
380
  for (const provider of data.providers) {
381
  for (const model of provider.models || []) {
382
  const n = normName(model.name);
383
+
384
+ // 0. AUTO-MARK PROPRIETARY: Mark closed APIs as private to skip HF lookups
385
+ if (PROPRIETARY_KEYWORDS.some(k => n.includes(k))) {
386
+ model.hf_private = true;
387
+ }
388
+
389
+ // 1. MANUAL OVERRIDE: Link common models to their HF IDs
390
  if (!model.hf_id) {
391
  for (const [key, val] of Object.entries(MANUAL_HF_ID_MAP)) {
392
+ if (n === key || n.endsWith(' ' + key) || n.endsWith('/' + key)) {
393
+ model.hf_id = val; break;
394
+ }
395
  }
396
  }
397
  if (!model.ollama_id) {
398
  for (const [key, val] of Object.entries(MANUAL_OLLAMA_ID_MAP)) {
399
+ if (n === key || n.endsWith(' ' + key) || n.endsWith('/' + key)) {
400
+ model.ollama_id = val; break;
401
+ }
402
  }
403
  }
404
 
405
+ // 2. Propagate size from benchmarks (Exact Match via hf_id)
406
  if (!model.size_b && model.hf_id) {
407
  const size = hfIdToSize.get(model.hf_id.toLowerCase());
408
  if (size) { model.size_b = size; propagatedSize++; }
409
  }
410
 
411
+ // 3. Auto-tag image-gen and embedding models
412
+ if (model.type === 'image' && (!model.capabilities || !model.capabilities.length)) {
413
+ model.capabilities = ['image-gen'];
414
+ autoTagged++;
415
+ }
416
+ if (model.type === 'chat' && EMBEDDER_KEYWORDS.some(k => n.includes(k))) {
417
+ model.type = 'embedding';
418
+ autoTagged++;
419
+ }
420
 
421
+ // 4. INHERIT: Structured data inheritance from OpenRouter
422
  if (provider.name !== 'OpenRouter') {
423
  const match = findOrMatch(model.name, orIndex);
424
  if (match) {
425
  if (!model.capabilities || model.capabilities.length === 0) {
426
+ // Propagate model capabilities (tools, vision, etc.) but NOT provider-specific ones like eu-endpoint
427
  model.capabilities = (match.capabilities || []).filter(c => c !== 'eu-endpoint');
428
  propagatedCaps++;
429
  }
430
  if (model.type === 'chat' && match.type !== 'chat') model.type = match.type;
431
+
432
+ if (!model.size_b && match.size_b) {
433
+ model.size_b = match.size_b;
434
+ propagatedSize++;
435
+ }
436
+ // Crucial: inherit hf_id to enable Hub API fallback below
437
  if (!model.hf_id && match.hf_id) model.hf_id = match.hf_id;
438
  if (!model.ollama_id && match.ollama_id) model.ollama_id = match.ollama_id;
439
  if (model.hf_private === undefined && match.hf_private !== undefined) model.hf_private = match.hf_private;
440
  }
441
  }
442
 
443
+ // 5. HARDCODED heuristics
444
  if (!model.size_b) {
445
+ if (n.includes('gemma 2 9b') || n.includes('gemma2 9b')) { model.size_b = 9; propagatedSize++; }
446
+ else if (n.includes('gemma 2 27b') || n.includes('gemma2 27b')) { model.size_b = 27; propagatedSize++; }
447
+ else if (n.includes('gemma 2 2b') || n.includes('gemma2 2b')) { model.size_b = 2; propagatedSize++; }
448
+ }
449
+
450
+ // 6. QUEUE: Still missing size? Try Hub API or Ollama
451
+ if (!model.size_b) {
452
+ if (!model.hf_private && (model.name.includes('/') || model.hf_id)) {
453
+ hfLookupQueue.push(model);
454
+ } else if (!model.hf_private && model.ollama_id) {
455
+ ollamaLookupQueue.push(model);
456
+ }
457
  }
458
  }
459
  }
460
 
461
+ // 7. HUB API: Inspect technical metadata (Limit 200 unique IDs to ensure better coverage)
462
  const uniqueIds = [...new Set(hfLookupQueue.map(m => m.hf_id || m.name).filter(id => id.includes('/')))].slice(0, 200);
463
  if (uniqueIds.length > 0) {
464
+ console.log(`\n HF Hub: technical metadata inspection for ${uniqueIds.length} models...`);
465
  const idToResult = new Map();
466
+
467
+ // Process sequentially with small delay to avoid 429 rate limits
468
  for (let i = 0; i < uniqueIds.length; i++) {
469
  const id = uniqueIds[i];
470
  process.stdout.write(` [${i + 1}/${uniqueIds.length}] ${id.padEnd(50)} `);
471
  const result = await fetchHFSize(id);
472
+
473
+ if (result.size) {
474
+ idToResult.set(id, result);
475
+ process.stdout.write(`βœ“ ${result.size}B\n`);
476
+ } else {
477
+ process.stdout.write(`βœ— ${result.error || 'Unknown Error'}\n`);
478
+
479
+ // CIRCUIT BREAKER: Stop if we hit a rate limit (429)
480
+ if (result.error && result.error.includes('429')) {
481
+ console.warn('\n ⚠ HIT RATE LIMIT (429) - Stopping further HF lookups for this run.');
482
+ break;
483
+ }
484
  }
485
+ await new Promise(r => setTimeout(r, 50)); // Tiny delay
486
  }
487
+
488
  for (const model of hfLookupQueue) {
489
+ if (!model.size_b) {
490
+ const id = model.hf_id || model.name;
491
+ const result = idToResult.get(id);
492
+ if (result) {
493
+ if (result.size) {
494
+ model.size_b = result.size;
495
+ hfSizeFetched++;
496
+ }
497
+ if (result.private) {
498
+ model.hf_private = true;
499
+ }
500
+ }
501
  }
502
  }
503
+ console.log(` βœ“ Total ${hfSizeFetched} new sizes from HF metadata`);
504
  }
505
 
506
+ // 8. OLLAMA REGISTRY: Inspect parameter info (Final fallback for common models)
507
  const uniqueOllama = [...new Set(ollamaLookupQueue.map(m => m.ollama_id))].filter(Boolean);
508
  if (uniqueOllama.length > 0) {
509
  console.log(`\n Ollama: inspecting registry for ${uniqueOllama.length} models...`);
 
512
  const id = uniqueOllama[i];
513
  process.stdout.write(` [${i + 1}/${uniqueOllama.length}] ${id.padEnd(50)} `);
514
  const res = await fetchOllamaMetadata(id);
515
+ if (res) {
516
+ idToResult.set(id, res);
517
+ process.stdout.write(res.size ? `βœ“ ${res.size}B\n` : `βœ“\n`);
518
+ } else {
519
+ process.stdout.write(`βœ—\n`);
520
+ }
521
  await new Promise(r => setTimeout(r, 50));
522
  }
523
  for (const model of ollamaLookupQueue) {
524
  const res = idToResult.get(model.ollama_id);
525
+ if (res && res.size && !model.size_b) {
526
+ model.size_b = res.size;
527
+ ollamaFetched++;
528
+ }
529
  }
530
+ console.log(` βœ“ Total ${ollamaFetched} new sizes from Ollama`);
531
  }
532
+
533
+ if (autoTagged > 0) console.log(`Auto-tagged ${autoTagged} image-gen/embedding models.`);
534
+ if (propagatedCaps > 0) console.log(`Propagated capabilities to ${propagatedCaps} models.`);
535
+ if (propagatedSize + hfSizeFetched + ollamaFetched > 0) console.log(`Enriched size data for ${propagatedSize + hfSizeFetched + ollamaFetched} models.`);
536
  }
537
 
538
  async function runFetcher(fetcher, data) {
539
+ const { key, providerName, fn } = fetcher;
540
+
541
  try {
542
+ process.stdout.write(`Fetching ${providerName}... `);
543
+ const models = await fn();
544
+ const updated = updateProviderModels(data.providers, providerName, models);
545
+ if (updated) console.log(`βœ“ ${models.length} models`);
546
+ return { key, providerName, success: true, count: models.length };
547
  } catch (err) {
548
  console.log(`βœ— ${err.message}`);
549
+ return { key, providerName, success: false, error: err.message };
550
  }
551
  }
552
 
553
  async function main() {
554
+ // Determine which fetchers to run
555
+ const args = process.argv.slice(2).map((a) => a.toLowerCase());
556
+ const fetchers =
557
+ args.length > 0
558
+ ? FETCHERS.filter((f) => args.includes(f.key))
559
+ : FETCHERS;
560
+
561
+ if (fetchers.length === 0) {
562
+ console.error('No matching fetchers found. Available:', FETCHERS.map((f) => f.key).join(', '));
563
+ process.exit(1);
564
+ }
565
+
566
  const data = loadData();
 
 
567
  console.log(`Running ${fetchers.length} fetcher(s)...\n`);
568
+
569
+ const results = [];
570
+ for (const fetcher of fetchers) {
571
+ const result = await runFetcher(fetcher, data);
572
+ results.push(result);
573
+ }
574
+
575
+ // Always propagate extra data from OpenRouter and Benchmarks to all providers' models.
576
  await propagateExtraData(data);
577
+
578
  saveData(data);
579
+
580
  console.log('\nSummary:');
581
+ let anyFailed = false;
582
+ results.forEach((r) => {
583
+ if (r.success) console.log(` βœ“ ${r.providerName}: ${r.count} models`);
584
+ else {
585
+ console.log(` βœ— ${r.providerName}: ${r.error}`);
586
+ anyFailed = true;
587
+ }
588
+ });
589
+
590
+ if (anyFailed) process.exit(1);
591
  }
592
 
593
+ main().catch((err) => {
594
+ console.error('Fatal:', err);
595
+ process.exit(1);
596
+ });