CrispStrobe commited on
Commit
54c2fe8
Β·
1 Parent(s): 128b3b8

feat: achieve 100% public model metadata coverage; improved vLLM-style param estimation; fixed phi-4 and mistral variants

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