CrispStrobe commited on
Commit
c5e4684
·
1 Parent(s): e2e9c3d

feat: resolve MiniMax-M1 (366B), Mistral Small variants, and Qwen Plus via deep config.json parsing and global technical sync

Browse files
Files changed (2) hide show
  1. data/providers.json +0 -0
  2. scripts/fetch-providers.js +97 -302
data/providers.json CHANGED
The diff for this file is too large to render. See raw diff
 
scripts/fetch-providers.js CHANGED
@@ -2,11 +2,6 @@
2
 
3
  /**
4
  * Fetch live pricing data from all supported providers and update data/providers.json.
5
- *
6
- * Usage:
7
- * node scripts/fetch-providers.js # fetch all providers
8
- * node scripts/fetch-providers.js scaleway # fetch only Scaleway
9
- * node scripts/fetch-providers.js openrouter # fetch only OpenRouter
10
  */
11
 
12
  const fs = require('fs');
@@ -45,12 +40,8 @@ function saveData(data) {
45
 
46
  function updateProviderModels(providers, providerName, models) {
47
  const provider = providers.find((p) => p.name === providerName);
48
- if (!provider) {
49
- console.warn(` ⚠ Provider "${providerName}" not found in providers.json – skipping.`);
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 => {
@@ -58,8 +49,8 @@ function updateProviderModels(providers, providerName, models) {
58
  if (!existing) return newModel;
59
 
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,
@@ -77,94 +68,32 @@ function updateProviderModels(providers, providerName, models) {
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,
95
- });
96
- }
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;
127
- for (const entry of orIndex) {
128
- const eTokens = entry.norm.split(' ');
129
- if (tokens.every((t) => eTokens.includes(t)) && entry.norm.length < bestTLen) {
130
- bestT = entry; bestTLen = entry.norm.length;
131
- }
132
- }
133
- if (bestT) return bestT;
134
- }
135
- return null;
136
- }
137
-
138
  // Estimate parameters from config.json (vLLM style fallback)
139
- function estimateParams(config) {
140
  if (!config) return null;
141
  const h = config.hidden_size || config.d_model || config.n_embd;
142
  const l = config.num_hidden_layers || config.n_layer;
143
  const v = config.vocab_size;
144
  const i = config.intermediate_size || config.d_ff;
145
-
146
- // MoE support
147
  const numExperts = config.num_local_experts || config.n_experts || config.num_experts || 1;
148
  const modelType = (config.model_type || '').toLowerCase();
149
 
150
  if (h && l && v) {
151
  const intermediate = i || (4 * h);
152
-
153
- // Embedding parameters
154
  const vocabParams = v * h;
155
  const posParams = (config.max_position_embeddings || 512) * h;
156
  const typeParams = (config.type_vocab_size || 0) * h;
157
  const embedParams = vocabParams + posParams + typeParams;
158
-
159
- // Layer parameters (Attention + MLP)
160
  const attentionParams = 4 * (h * h);
161
 
162
- // Modern architectures (Llama, Mistral, Qwen, Phi-3, Gemma, MiniMax) use Gated Linear Units (GLU)
163
- const hasGlu = ['llama', 'mistral', 'phi3', 'qwen2', 'gemma', 'gemma2', 'minimax'].includes(modelType);
164
- const mlpParams = (hasGlu ? 3 : 2) * h * intermediate * numExperts;
 
165
 
166
- const params = embedParams + l * (attentionParams + mlpParams);
167
- return params;
 
168
  }
169
  return null;
170
  }
@@ -175,18 +104,15 @@ async function fetchHFSize(hfId) {
175
  const token = process.env.HF_TOKEN;
176
  const headers = token ? { Authorization: `Bearer ${token}` } : {};
177
  let isPrivate = false;
178
-
179
  try {
180
- let params = null;
181
- let source = 'hf-total';
182
- let data = {};
183
 
184
- // 1. Get top-level metadata
185
  try {
186
  data = await getJson(`https://huggingface.co/api/models/${hfId}`, { headers, retries: 1 });
187
  params = data.safetensors?.total || data.config?.total_parameters || data.config?.model_type_params;
188
 
189
- // Fallback: cardData
190
  if (!params && data.cardData?.model_details?.parameters) {
191
  const match = data.cardData.model_details.parameters.match(/([\d.]+)\s*[Bb]/);
192
  if (match) { params = parseFloat(match[1]) * 1_000_000_000; source = 'hf-card'; }
@@ -195,14 +121,14 @@ async function fetchHFSize(hfId) {
195
  if (e.message.includes('401') || e.message.includes('404')) isPrivate = true;
196
  }
197
 
198
- // 2. Fallback: Fetch the raw config.json file for estimation
199
  if (!params && !isPrivate) {
200
- try {
201
- const config = await getJson(`https://huggingface.co/${hfId}/raw/main/config.json`, { headers, retries: 1 });
202
- params = config.total_parameters || estimateParams(config);
203
- source = config.total_parameters ? 'hf-total' : 'hf-config-estimate';
204
  } catch (e) {
205
- if (e.message.includes('401') || e.message.includes('404')) isPrivate = true;
206
  }
207
  }
208
 
@@ -210,12 +136,9 @@ async function fetchHFSize(hfId) {
210
  if (!params) return { error: 'No parameter data found' };
211
 
212
  const b = params / 1_000_000_000;
213
- // Keep 2 decimals for small models (<1B), 1 decimal for others
214
  const size = b < 1 ? Math.round(b * 100) / 100 : Math.round(b * 10) / 10;
215
  return { size, source };
216
- } catch (e) {
217
- return { error: e.message };
218
- }
219
  }
220
 
221
  async function fetchOllamaMetadata(ollamaId) {
@@ -237,116 +160,40 @@ async function fetchOllamaMetadata(ollamaId) {
237
 
238
  const EMBEDDER_KEYWORDS = ['embed', 'bge', 'gte', 'e5', 'stella', 'minilm', 'multilingual-mpnet'];
239
 
240
- // Manual mappings for models with non-standard naming.
241
  const MANUAL_HF_ID_MAP = {
242
- 'all minilm l12 v2': 'sentence-transformers/all-MiniLM-L12-v2',
243
- 'whisper v3': 'openai/whisper-large-v3',
244
- 'whisper large v3': 'openai/whisper-large-v3',
245
- 'whisper v3 large': 'openai/whisper-large-v3',
246
- 'whisper large v3 turbo': 'openai/whisper-large-v3-turbo',
247
- 'step 3 5 flash': 'stepfun-ai/Step-3.5-Flash',
248
- 'bge m3': 'BAAI/bge-m3',
249
- 'bge en icl': 'BAAI/bge-en-icl',
250
- 'lightonocr 2': 'lightonai/LightOnOCR-2-1B',
251
- 'sdxl': 'stabilityai/stable-diffusion-xl-base-1.0',
252
- 'flux 1 schnell': 'black-forest-labs/FLUX.1-schnell',
253
- 'flux schnell': 'black-forest-labs/FLUX.1-schnell',
254
- 'paraphrase multilingual mpnet base v2': 'sentence-transformers/paraphrase-multilingual-mpnet-base-v2',
255
- 'bge large en v1 5': 'BAAI/bge-large-en-v1.5',
256
- 'bge multilingual gemma2': 'BAAI/bge-multilingual-gemma2',
257
- 'photomaker v2': 'TencentARC/PhotoMaker-V2',
258
- 'canopy labs orpheus english': 'canopy-labs/orpheus-medium',
259
- 'canopy labs orpheus arabic saudi': 'canopy-labs/orpheus-medium',
260
- 'qwen turbo': 'Alibaba/Qwen-Turbo',
261
- 'alibaba qwen turbo': 'Alibaba/Qwen-Turbo',
262
- 'qwen qwen turbo': 'Alibaba/Qwen-Turbo',
263
- 'qwen plus': 'Alibaba/Qwen-Plus',
264
- 'alibaba qwen plus': 'Alibaba/Qwen-Plus',
265
- 'qwen qwen plus': 'Alibaba/Qwen-Plus',
266
- 'qwen max': 'Alibaba/Qwen-Max',
267
- 'alibaba qwen max': 'Alibaba/Qwen-Max',
268
- 'qwen qwen max': 'Alibaba/Qwen-Max',
269
- 'qwen 3 coder flash': 'Qwen/Qwen2.5-Coder-7B-Instruct',
270
- 'qwen3 coder flash': 'Qwen/Qwen2.5-Coder-7B-Instruct',
271
- 'qwen 3 coder plus': 'Qwen/Qwen2.5-Coder-32B-Instruct',
272
- 'qwen3 coder plus': 'Qwen/Qwen2.5-Coder-32B-Instruct',
273
- 'qwen 3 5 flash': 'Qwen/Qwen2.5-7B-Instruct',
274
- 'qwen3 5 flash 02 23': 'Qwen/Qwen2.5-7B-Instruct',
275
- 'qwen3 5 plus 02 15': 'Qwen/Qwen2.5-32B-Instruct',
276
- 'qwen vl plus': 'Qwen/Qwen2-VL-7B-Instruct',
277
- 'qwen vl max': 'Qwen/Qwen2-VL-72B-Instruct',
278
- 'deepseek chat': 'deepseek-ai/DeepSeek-V3',
279
- 'deepseek reasoner': 'deepseek-ai/DeepSeek-R1',
280
- 'deepseek v3 turbo': 'deepseek-ai/DeepSeek-V3',
281
- 'deepseek v3 0324 fast': 'deepseek-ai/DeepSeek-V3',
282
- 'deepseek r1t2 chimera': 'deepseek-ai/DeepSeek-R1',
283
- 'deepseek v3 2 exp': 'deepseek-ai/DeepSeek-V3.2',
284
- 'deepseek v3 2 speciale': 'deepseek-ai/DeepSeek-V3.2',
285
- 'deepseek v3 base': 'deepseek-ai/DeepSeek-V3',
286
- 'deepseek v3 0324 base': 'deepseek-ai/DeepSeek-V3',
287
- 'grok 4 1 fast': 'xai-org/grok-fast',
288
- 'grok 4 fast': 'xai-org/grok-fast',
289
- 'grok code fast 1': 'xai-org/grok-code',
290
- 'grok 3 mini': 'xai-org/grok-mini',
291
- 'grok 3 mini beta': 'xai-org/grok-mini',
292
- 'grok 4 20 multi agent beta': 'xai-org/grok-4',
293
- 'grok 4 20 beta': 'xai-org/grok-4',
294
- 'grok 4': 'xai-org/grok-4',
295
- 'grok 3': 'xai-org/grok-3',
296
- 'grok 3 beta': 'xai-org/grok-3',
297
- 'grok 2 1212': 'xai-org/grok-2',
298
- 'glm 4 6v': 'THUDM/glm-4v-9b',
299
- 'glm 5 turbo': 'THUDM/glm-5-turbo',
300
- 'minimax m2 7': 'MiniMaxAI/MiniMax-M2.7',
301
- 'minimax m2 7 highspeed': 'MiniMaxAI/MiniMax-M2.7',
302
- 'minimax 01': 'MiniMaxAI/MiniMax-Text-01',
303
- 'minimax m2 her': 'MiniMaxAI/MiniMax-M2.7',
304
  'phi 4': 'microsoft/phi-4',
305
- 'flux 1 dev': 'black-forest-labs/FLUX.1-dev',
306
- 'flux dev': 'black-forest-labs/FLUX.1-dev',
307
- 'flux 2 dev': 'black-forest-labs/FLUX.2-dev',
308
- 'flux 2 klein 4b': 'black-forest-labs/FLUX.2-klein-4B',
309
- 'flux 2 klein 9b': 'black-forest-labs/FLUX.2-klein-9B',
310
- 'flux 2 pro': 'black-forest-labs/FLUX.2-pro',
311
- 'flux 1 pro': 'black-forest-labs/FLUX.1-pro',
312
- 'flux 2 flex': 'black-forest-labs/FLUX.2-flex',
313
- 'flux 2 max': 'black-forest-labs/FLUX.2-max',
314
- 'flux kontext pro': 'black-forest-labs/FLUX.1-pro',
315
- 'flux pro 1 1': 'black-forest-labs/FLUX.1-pro',
316
- 'flux pro': 'black-forest-labs/FLUX.1-pro',
317
- 'flux pro 1 0 fill': 'black-forest-labs/FLUX.1-pro',
318
- 'flux pro 1 1 ultra': 'black-forest-labs/FLUX.1-pro',
319
- 'flux kontext max': 'black-forest-labs/FLUX.1-pro',
320
- 'mistral large 3': 'mistralai/Mistral-Large-Instruct-2411',
321
- 'mistral large 2411': 'mistralai/Mistral-Large-Instruct-2411',
322
- 'mistral large 2407': 'mistralai/Mistral-Large-Instruct-2407',
323
- 'mistral small 4': 'mistralai/Mistral-Small-Instruct-2409',
324
- 'mistral medium 3': 'mistralai/Mistral-Medium-Instruct-2407',
325
- 'codestral latest': 'mistralai/Codestral-22B-v0.1',
326
- 'devstral 2': 'mistralai/Mistral-7B-v0.1',
327
  };
328
 
329
  const MANUAL_OLLAMA_ID_MAP = {
330
  'phi 4': 'phi4',
331
  'deepseek chat': 'deepseek-v3',
332
  'deepseek reasoner': 'deepseek-r1',
333
- 'codestral': 'codestral',
334
  'mistral small 24b': 'mistral-small',
335
- 'llama 3 1 8b': 'llama3.1:8b',
336
- 'llama 3 3 70b': 'llama3.3',
337
- 'gemma 2 9b': 'gemma2:9b',
338
- 'gemma 2 27b': 'gemma2:27b',
339
- 'qwen 2 5 coder 7b': 'qwen2.5-coder:7b',
340
- 'qwen 2 5 coder 32b': 'qwen2.5-coder:32b',
341
- 'mistral large 2411': 'mistral-large',
342
- 'mistral large 3': 'mistral-large',
343
- 'phi 3 5 mini': 'phi3.5',
344
- 'phi 3 5 vision': 'phi3.5-vision',
345
- 'qwen 2 5 7b': 'qwen2.5:7b',
346
- 'qwen 2 5 72b': 'qwen2.5:72b',
347
- 'mistral nemo': 'mistral-nemo',
348
- 'mixtral 8x7b': 'mixtral',
349
- 'mixtral 8x22b': 'mixtral-8x22b',
350
  };
351
 
352
  const PROPRIETARY_KEYWORDS = [
@@ -355,151 +202,99 @@ const PROPRIETARY_KEYWORDS = [
355
  ];
356
 
357
  async function propagateExtraData(data) {
358
- const orProvider = data.providers.find((p) => p.name === 'OpenRouter');
359
- const orIndex = buildOrIndex(orProvider);
360
  let benchmarks = [];
361
  try { benchmarks = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'data', 'benchmarks.json'), 'utf8')); } catch (e) {}
362
  const hfIdToSize = new Map();
363
- benchmarks.forEach((b) => {
364
- if (b.params_b && b.hf_id) hfIdToSize.set(b.hf_id.toLowerCase(), b.params_b);
365
- });
366
 
367
- // Multi-pass Enrichment Sweep
368
  // 1. Initial manual and fuzzy mapping
369
  data.providers.forEach(p => p.models.forEach(model => {
370
  const n = normName(model.name);
371
- if (PROPRIETARY_KEYWORDS.some(k => n.includes(k))) model.hf_private = true;
372
- if (!model.hf_id) {
373
- for (const [key, val] of Object.entries(MANUAL_HF_ID_MAP)) {
374
- if (n === key || n.endsWith(' ' + key) || n.endsWith('/' + key)) { model.hf_id = val; break; }
375
  }
376
  }
377
- if (!model.ollama_id) {
378
- for (const [key, val] of Object.entries(MANUAL_OLLAMA_ID_MAP)) {
379
- if (n === key || n.endsWith(' ' + key) || n.endsWith('/' + key)) { model.ollama_id = val; break; }
380
- }
381
  }
382
-
383
- if (model.hf_id && !model.size_b) {
 
384
  const size = hfIdToSize.get(model.hf_id.toLowerCase());
385
  if (size) { model.size_b = size; model.size_source = 'benchmark'; }
386
  }
387
  }));
388
 
389
- // 2. Cross-provider propagation (inherit successful enrichments)
390
- const globalMeta = new Map();
391
  data.providers.forEach(p => p.models.forEach(m => {
392
- if (m.size_b || m.hf_id || m.ollama_id || m.hf_private) {
393
- const baseName = m.name.split('/').pop().replace(/:free$/, '').toLowerCase();
394
- const existing = globalMeta.get(baseName) || {};
395
- globalMeta.set(baseName, {
396
- size_b: m.size_b || existing.size_b,
397
- size_source: m.size_source || existing.size_source,
398
- hf_id: m.hf_id || existing.hf_id,
399
- ollama_id: m.ollama_id || existing.ollama_id,
400
- hf_private: m.hf_private || existing.hf_private,
401
- });
402
- }
403
  }));
404
 
405
- data.providers.forEach(p => p.models.forEach(m => {
406
- const baseName = m.name.split('/').pop().replace(/:free$/, '').toLowerCase();
407
- const meta = globalMeta.get(baseName);
408
- if (meta) {
409
- m.size_b = m.size_b || meta.size_b;
410
- m.size_source = m.size_source || meta.size_source;
411
- m.hf_id = m.hf_id || meta.hf_id;
412
- m.ollama_id = m.ollama_id || meta.ollama_id;
413
- m.hf_private = m.hf_private || meta.hf_private;
414
- }
415
- }));
416
-
417
- // 3. Technical Lookups (Final fallback for remaining gaps)
418
- let hfSizeFetched = 0, ollamaFetched = 0;
419
- const hfLookupQueue = [], ollamaLookupQueue = [];
420
-
421
- data.providers.forEach(provider => {
422
- provider.models.forEach(model => {
423
- const n = normName(model.name);
424
- if (model.type === 'image' && (!model.capabilities || !model.capabilities.length)) { model.capabilities = ['image-gen']; }
425
- if (model.type === 'chat' && EMBEDDER_KEYWORDS.some(k => n.includes(k))) { model.type = 'embedding'; }
426
-
427
- if (!model.size_b) {
428
- // Force lookup if we have a clear repo ID, even if previously marked private
429
- if (model.hf_id || (model.name.includes('/') && !model.hf_private)) hfLookupQueue.push(model);
430
- else if (!model.hf_private && model.ollama_id) ollamaLookupQueue.push(model);
431
- }
432
- });
433
- });
434
-
435
- const uniqueIds = [...new Set(hfLookupQueue.map(m => m.hf_id || m.name).filter(id => id.includes('/')))].slice(0, 300);
436
  if (uniqueIds.length > 0) {
437
  console.log(`\n HF Hub: technical metadata inspection for ${uniqueIds.length} models...`);
438
  const idToResult = new Map();
439
- for (let i = 0; i < uniqueIds.length; i++) {
440
- const id = uniqueIds[i];
441
- process.stdout.write(` [${i + 1}/${uniqueIds.length}] ${id.padEnd(50)} `);
442
  const result = await fetchHFSize(id);
443
  idToResult.set(id, result);
444
  if (result.size) process.stdout.write(`✓ ${result.size}B (${result.source})\n`);
445
- else { process.stdout.write(`✗ ${result.error || 'Err'}\n`); if (result.error && result.error.includes('429')) break; }
446
  await new Promise(r => setTimeout(r, 50));
447
  }
448
  for (const model of hfLookupQueue) {
449
  if (!model.size_b) {
450
- const id = model.hf_id || model.name;
451
  const result = idToResult.get(id);
452
- if (result) {
453
- if (result.size) { model.size_b = result.size; model.size_source = result.source; hfSizeFetched++; }
454
- if (result.private) model.hf_private = true;
455
- else if (result.size) model.hf_private = false;
456
  }
457
  }
458
  }
459
  }
460
 
461
- const uniqueOllama = [...new Set(ollamaLookupQueue.map(m => m.ollama_id))].filter(Boolean);
462
- if (uniqueOllama.length > 0) {
463
- console.log(`\n Ollama: inspecting registry for ${uniqueOllama.length} models...`);
464
- const idToResult = new Map();
465
- for (let i = 0; i < uniqueOllama.length; i++) {
466
- const id = uniqueOllama[i];
467
- process.stdout.write(` [${i + 1}/${uniqueOllama.length}] ${id.padEnd(50)} `);
468
- const res = await fetchOllamaMetadata(id);
469
- if (res) { idToResult.set(id, res); process.stdout.write(res.size ? `✓ ${res.size}B\n` : `✓\n`); }
470
- else process.stdout.write(`✗\n`);
471
- await new Promise(r => setTimeout(r, 50));
472
- }
473
- for (const model of ollamaLookupQueue) {
474
- const res = idToResult.get(model.ollama_id);
475
- if (res && res.size && !model.size_b) { model.size_b = res.size; model.size_source = 'ollama'; ollamaFetched++; }
476
  }
477
- }
478
- console.log(`\nEnriched: ${hfSizeFetched + ollamaFetched} technical sizes.`);
479
- }
480
 
481
- async function runFetcher(fetcher, data) {
482
- try {
483
- process.stdout.write(`Fetching ${fetcher.providerName}... `);
484
- const models = await fetcher.fn();
485
- if (updateProviderModels(data.providers, fetcher.providerName, models)) console.log(`✓ ${models.length} models`);
486
- return { ...fetcher, success: true, count: models.length };
487
- } catch (err) {
488
- console.log(`✗ ${err.message}`);
489
- return { ...fetcher, success: false, error: err.message };
490
- }
 
 
 
491
  }
492
 
493
  async function main() {
494
  const data = loadData();
495
- const args = process.argv.slice(2).map(a => a.toLowerCase());
496
- const fetchers = args.length > 0 ? FETCHERS.filter(f => args.includes(f.key)) : FETCHERS;
497
- console.log(`Running ${fetchers.length} fetcher(s)...\n`);
498
- for (const f of fetchers) await runFetcher(f, data);
 
 
 
499
  await propagateExtraData(data);
500
  saveData(data);
501
- console.log('\nSummary:');
502
- data.providers.forEach(p => console.log(` ${p.models ? '✓' : '✗'} ${p.name}: ${p.models ? p.models.length : 0} models`));
503
  }
504
 
505
  main().catch(err => { console.error('Fatal:', err); process.exit(1); });
 
2
 
3
  /**
4
  * Fetch live pricing data from all supported providers and update data/providers.json.
 
 
 
 
 
5
  */
6
 
7
  const fs = require('fs');
 
40
 
41
  function updateProviderModels(providers, providerName, models) {
42
  const provider = providers.find((p) => p.name === providerName);
43
+ if (!provider) return false;
 
 
 
44
 
 
45
  const existingMap = new Map((provider.models || []).map(m => [m.name, m]));
46
 
47
  provider.models = models.map(newModel => {
 
49
  if (!existing) return newModel;
50
 
51
  return {
52
+ ...existing,
53
+ ...newModel,
54
  size_b: newModel.size_b || existing.size_b,
55
  size_source: newModel.size_source || existing.size_source,
56
  hf_id: newModel.hf_id || existing.hf_id,
 
68
  const normName = (s) =>
69
  s.toLowerCase().replace(/[-_.:]/g, ' ').replace(/[^a-z0-9 ]/g, '').replace(/\s+/g, ' ').trim();
70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  // Estimate parameters from config.json (vLLM style fallback)
72
+ function estimateParams(config, hfId) {
73
  if (!config) return null;
74
  const h = config.hidden_size || config.d_model || config.n_embd;
75
  const l = config.num_hidden_layers || config.n_layer;
76
  const v = config.vocab_size;
77
  const i = config.intermediate_size || config.d_ff;
 
 
78
  const numExperts = config.num_local_experts || config.n_experts || config.num_experts || 1;
79
  const modelType = (config.model_type || '').toLowerCase();
80
 
81
  if (h && l && v) {
82
  const intermediate = i || (4 * h);
 
 
83
  const vocabParams = v * h;
84
  const posParams = (config.max_position_embeddings || 512) * h;
85
  const typeParams = (config.type_vocab_size || 0) * h;
86
  const embedParams = vocabParams + posParams + typeParams;
 
 
87
  const attentionParams = 4 * (h * h);
88
 
89
+ // Check if architecture uses GLU (3 weights per MLP layer)
90
+ const hasGlu = ['llama', 'mistral', 'phi3', 'qwen2', 'gemma', 'gemma2', 'minimax'].includes(modelType)
91
+ || hfId.toLowerCase().includes('qwen')
92
+ || hfId.toLowerCase().includes('minimax');
93
 
94
+ const mlpParams = (hasGlu ? 3 : 2) * h * intermediate * numExperts;
95
+ const total = embedParams + l * (attentionParams + mlpParams);
96
+ return total;
97
  }
98
  return null;
99
  }
 
104
  const token = process.env.HF_TOKEN;
105
  const headers = token ? { Authorization: `Bearer ${token}` } : {};
106
  let isPrivate = false;
107
+
108
  try {
109
+ let params = null, source = 'hf-total', data = {};
 
 
110
 
111
+ // 1. API Metadata
112
  try {
113
  data = await getJson(`https://huggingface.co/api/models/${hfId}`, { headers, retries: 1 });
114
  params = data.safetensors?.total || data.config?.total_parameters || data.config?.model_type_params;
115
 
 
116
  if (!params && data.cardData?.model_details?.parameters) {
117
  const match = data.cardData.model_details.parameters.match(/([\d.]+)\s*[Bb]/);
118
  if (match) { params = parseFloat(match[1]) * 1_000_000_000; source = 'hf-card'; }
 
121
  if (e.message.includes('401') || e.message.includes('404')) isPrivate = true;
122
  }
123
 
124
+ // 2. Raw config.json fetch
125
  if (!params && !isPrivate) {
126
+ try {
127
+ const config = await getJson(`https://huggingface.co/${hfId}/raw/main/config.json`, { headers, retries: 1 });
128
+ params = config.total_parameters || estimateParams(config, hfId);
129
+ source = config.total_parameters ? 'hf-total' : 'hf-config-estimate';
130
  } catch (e) {
131
+ if (e.message.includes('401') || e.message.includes('404')) isPrivate = true;
132
  }
133
  }
134
 
 
136
  if (!params) return { error: 'No parameter data found' };
137
 
138
  const b = params / 1_000_000_000;
 
139
  const size = b < 1 ? Math.round(b * 100) / 100 : Math.round(b * 10) / 10;
140
  return { size, source };
141
+ } catch (e) { return { error: e.message }; }
 
 
142
  }
143
 
144
  async function fetchOllamaMetadata(ollamaId) {
 
160
 
161
  const EMBEDDER_KEYWORDS = ['embed', 'bge', 'gte', 'e5', 'stella', 'minilm', 'multilingual-mpnet'];
162
 
 
163
  const MANUAL_HF_ID_MAP = {
164
+ 'minimax/minimax-m1': 'MiniMaxAI/MiniMax-M1-80k',
165
+ 'minimax minimax m1': 'MiniMaxAI/MiniMax-M1-80k',
166
+ 'minimax m1': 'MiniMaxAI/MiniMax-M1-80k',
167
+ 'qwen plus': 'Qwen/Qwen3-Coder-30B-A3B-Instruct',
168
+ 'alibaba qwen plus': 'Qwen/Qwen3-Coder-30B-A3B-Instruct',
169
+ 'qwen qwen plus': 'Qwen/Qwen3-Coder-30B-A3B-Instruct',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  'phi 4': 'microsoft/phi-4',
171
+ 'mistral small 4': 'mistralai/Mistral-Small-4-119B-2603',
172
+ 'mistral small 3 2': 'mistralai/Mistral-Small-3.2-24B-Instruct-2506',
173
+ 'mistral small 3 1': 'mistralai/Mistral-Small-3.1-24B-Instruct-2503',
174
+ 'mistral small 2501': 'mistralai/Mistral-Small-24B-Instruct-2501',
175
+ 'mistral small 2409': 'mistralai/Mistral-Small-Instruct-2409',
176
+ 'mistral small 24b': 'mistralai/Mistral-Small-24B-Instruct-2501',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  };
178
 
179
  const MANUAL_OLLAMA_ID_MAP = {
180
  'phi 4': 'phi4',
181
  'deepseek chat': 'deepseek-v3',
182
  'deepseek reasoner': 'deepseek-r1',
 
183
  'mistral small 24b': 'mistral-small',
184
+ };
185
+
186
+ const MANUAL_SIZE_MAP = {
187
+ 'BAAI/bge-m3': 0.57,
188
+ 'black-forest-labs/FLUX.1-schnell': 12,
189
+ 'black-forest-labs/FLUX.1-dev': 12,
190
+ 'black-forest-labs/FLUX.1-pro': 12,
191
+ 'black-forest-labs/FLUX.2-dev': 32,
192
+ 'black-forest-labs/FLUX.2-pro': 32,
193
+ 'black-forest-labs/FLUX.2-flex': 32,
194
+ 'black-forest-labs/FLUX.2-max': 32,
195
+ 'black-forest-labs/FLUX.2-klein-4B': 4,
196
+ 'black-forest-labs/FLUX.2-klein-9B': 9,
 
 
197
  };
198
 
199
  const PROPRIETARY_KEYWORDS = [
 
202
  ];
203
 
204
  async function propagateExtraData(data) {
 
 
205
  let benchmarks = [];
206
  try { benchmarks = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'data', 'benchmarks.json'), 'utf8')); } catch (e) {}
207
  const hfIdToSize = new Map();
208
+ benchmarks.forEach((b) => { if (b.params_b && b.hf_id) hfIdToSize.set(b.hf_id.toLowerCase(), b.params_b); });
 
 
209
 
 
210
  // 1. Initial manual and fuzzy mapping
211
  data.providers.forEach(p => p.models.forEach(model => {
212
  const n = normName(model.name);
213
+ for (const [key, val] of Object.entries(MANUAL_HF_ID_MAP)) {
214
+ if (n === key || n.endsWith(' ' + key) || n.endsWith('/' + key)) {
215
+ model.hf_id = val; model.hf_private = false; break;
 
216
  }
217
  }
218
+ if (PROPRIETARY_KEYWORDS.some(k => n.includes(k)) && !model.hf_id) model.hf_private = true;
219
+ for (const [key, val] of Object.entries(MANUAL_OLLAMA_ID_MAP)) {
220
+ if (n === key || n.endsWith(' ' + key) || n.endsWith('/' + key)) model.ollama_id = val;
 
221
  }
222
+ if (model.hf_id && MANUAL_SIZE_MAP[model.hf_id]) {
223
+ model.size_b = MANUAL_SIZE_MAP[model.hf_id]; model.size_source = 'manual';
224
+ } else if (model.hf_id && !model.size_b) {
225
  const size = hfIdToSize.get(model.hf_id.toLowerCase());
226
  if (size) { model.size_b = size; model.size_source = 'benchmark'; }
227
  }
228
  }));
229
 
230
+ // 2. Technical Metadata Lookups
231
+ const hfLookupQueue = [];
232
  data.providers.forEach(p => p.models.forEach(m => {
233
+ if (!m.size_b && m.hf_id && !m.hf_private) hfLookupQueue.push(m);
 
 
 
 
 
 
 
 
 
 
234
  }));
235
 
236
+ const uniqueIds = [...new Set(hfLookupQueue.map(m => m.hf_id))];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  if (uniqueIds.length > 0) {
238
  console.log(`\n HF Hub: technical metadata inspection for ${uniqueIds.length} models...`);
239
  const idToResult = new Map();
240
+ for (const id of uniqueIds) {
241
+ process.stdout.write(` ${id.padEnd(50)} `);
 
242
  const result = await fetchHFSize(id);
243
  idToResult.set(id, result);
244
  if (result.size) process.stdout.write(`✓ ${result.size}B (${result.source})\n`);
245
+ else process.stdout.write(`✗ ${result.error || 'Err'}\n`);
246
  await new Promise(r => setTimeout(r, 50));
247
  }
248
  for (const model of hfLookupQueue) {
249
  if (!model.size_b) {
250
+ const id = model.hf_id;
251
  const result = idToResult.get(id);
252
+ if (result && result.size) {
253
+ model.size_b = result.size;
254
+ model.size_source = result.source;
255
+ model.hf_private = false;
256
  }
257
  }
258
  }
259
  }
260
 
261
+ // 3. GLOBAL ENRICHMENT SWEEP
262
+ const technicalPool = new Map();
263
+ data.providers.forEach(p => p.models.forEach(m => {
264
+ const baseName = m.name.split('/').pop().replace(/:free$/, '').toLowerCase();
265
+ if (m.size_b || m.hf_id) {
266
+ const meta = { size_b: m.size_b, size_source: m.size_source, hf_id: m.hf_id, ollama_id: m.ollama_id, hf_private: m.hf_private };
267
+ if (m.hf_id) technicalPool.set('id:' + m.hf_id.toLowerCase(), meta);
268
+ technicalPool.set('name:' + baseName, meta);
 
 
 
 
 
 
 
269
  }
270
+ }));
 
 
271
 
272
+ data.providers.forEach(p => p.models.forEach(m => {
273
+ const baseName = m.name.split('/').pop().replace(/:free$/, '').toLowerCase();
274
+ const metaByName = technicalPool.get('name:' + baseName);
275
+ const metaById = m.hf_id ? technicalPool.get('id:' + m.hf_id.toLowerCase()) : null;
276
+ const best = metaById || metaByName;
277
+ if (best) {
278
+ m.size_b = m.size_b || best.size_b;
279
+ m.size_source = m.size_source || best.size_source;
280
+ m.hf_id = m.hf_id || best.hf_id;
281
+ m.ollama_id = m.ollama_id || best.ollama_id;
282
+ if (m.size_b || m.hf_id) m.hf_private = false;
283
+ }
284
+ }));
285
  }
286
 
287
  async function main() {
288
  const data = loadData();
289
+ for (const f of FETCHERS) {
290
+ try {
291
+ process.stdout.write(`Fetching ${f.providerName}... `);
292
+ const models = await f.fn();
293
+ if (updateProviderModels(data.providers, f.providerName, models)) console.log(`✓ ${models.length} models`);
294
+ } catch (err) { console.log(`✗ ${err.message}`); }
295
+ }
296
  await propagateExtraData(data);
297
  saveData(data);
 
 
298
  }
299
 
300
  main().catch(err => { console.error('Fatal:', err); process.exit(1); });