Spaces:
Paused
Paused
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- data/providers.json +0 -0
- scripts/fetch-providers.js +145 -253
- 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
|
| 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 |
-
|
| 116 |
-
|
| 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 |
-
|
| 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
|
| 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 |
-
//
|
| 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 {};
|
| 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 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 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 |
-
|
| 449 |
-
|
| 450 |
-
|
|
|
|
| 451 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 452 |
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 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 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
}
|
| 481 |
|
| 482 |
-
// 6. QUEUE: Still missing size? Try Hub API or Ollama
|
| 483 |
if (!model.size_b) {
|
| 484 |
-
if
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
ollamaLookupQueue.push(model);
|
| 488 |
-
}
|
| 489 |
}
|
| 490 |
-
}
|
| 491 |
-
}
|
| 492 |
|
| 493 |
-
|
| 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 |
-
|
| 507 |
-
|
| 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 |
-
|
| 527 |
-
|
| 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 |
-
|
| 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 |
-
|
| 577 |
-
|
| 578 |
-
return { key, providerName, success: true, count: models.length };
|
| 579 |
} catch (err) {
|
| 580 |
console.log(`β ${err.message}`);
|
| 581 |
-
return {
|
| 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 |
-
|
| 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(
|
| 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 {
|