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