Spaces:
Running
Running
CrispStrobe commited on
Commit Β·
d135f12
1
Parent(s): f3c5e54
fix: implement robust fetching with retries and exponential backoff for benchmarks and providers
Browse files- scripts/fetch-benchmarks.js +5 -22
- scripts/fetch-utils.js +69 -0
- scripts/providers/black-forest-labs.js +2 -4
- scripts/providers/groq.js +2 -4
- scripts/providers/infomaniak.js +2 -4
- scripts/providers/ionos.js +2 -4
- scripts/providers/langdock.js +2 -4
- scripts/providers/mistral.js +2 -4
- scripts/providers/nebius.js +3 -3
- scripts/providers/openrouter.js +2 -7
- scripts/providers/requesty.js +2 -6
- scripts/providers/scaleway.js +2 -7
scripts/fetch-benchmarks.js
CHANGED
|
@@ -34,19 +34,12 @@
|
|
| 34 |
const fs = require('fs');
|
| 35 |
const path = require('path');
|
| 36 |
const yaml = require('js-yaml');
|
|
|
|
| 37 |
|
| 38 |
const OUT_FILE = path.join(__dirname, '..', 'data', 'benchmarks.json');
|
| 39 |
|
| 40 |
// βββ helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 41 |
|
| 42 |
-
async function getJson(url) {
|
| 43 |
-
const res = await fetch(url, {
|
| 44 |
-
headers: { 'User-Agent': 'providers-benchmark-fetcher', Accept: 'application/json' },
|
| 45 |
-
});
|
| 46 |
-
if (!res.ok) throw new Error(`HTTP ${res.status} from ${url}`);
|
| 47 |
-
return res.json();
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
const normName = (s) =>
|
| 51 |
(s || '').toLowerCase().replace(/[-_.]/g, ' ').replace(/[^a-z0-9 ]/g, '').replace(/\s+/g, ' ').trim();
|
| 52 |
|
|
@@ -241,9 +234,7 @@ async function fetchLiveBench() {
|
|
| 241 |
console.log(`${dates.length} releases (${dates[0]} β ${dates[dates.length - 1]})`);
|
| 242 |
|
| 243 |
// Use taskβgroup mapping from the latest categories JSON (stable across releases)
|
| 244 |
-
const cats = await
|
| 245 |
-
headers: { 'User-Agent': 'providers-benchmark-fetcher' },
|
| 246 |
-
}).then((r) => r.json());
|
| 247 |
|
| 248 |
const taskToGroup = {};
|
| 249 |
for (const [cat, tasks] of Object.entries(cats)) {
|
|
@@ -263,9 +254,7 @@ async function fetchLiveBench() {
|
|
| 263 |
for (const date of dates) {
|
| 264 |
let csv;
|
| 265 |
try {
|
| 266 |
-
csv = await
|
| 267 |
-
headers: { 'User-Agent': 'providers-benchmark-fetcher' },
|
| 268 |
-
}).then((r) => { if (!r.ok) throw new Error(`${r.status}`); return r.text(); });
|
| 269 |
} catch (e) {
|
| 270 |
console.warn(`\n β LiveBench ${date}: ${e.message}`);
|
| 271 |
continue;
|
|
@@ -393,15 +382,12 @@ async function fetchChatbotArena() {
|
|
| 393 |
// Requesting with "RSC: 1" returns a streaming text/x-component payload that
|
| 394 |
// embeds the full leaderboard entries (rank, ELO rating, votes) in the server
|
| 395 |
// response β no authentication required.
|
| 396 |
-
const text = await
|
| 397 |
headers: {
|
| 398 |
'User-Agent': 'Mozilla/5.0',
|
| 399 |
'RSC': '1',
|
| 400 |
'Accept': 'text/x-component',
|
| 401 |
},
|
| 402 |
-
}).then((r) => {
|
| 403 |
-
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
| 404 |
-
return r.text();
|
| 405 |
});
|
| 406 |
|
| 407 |
// Each RSC line has the format: <hex_id>:<json_value>
|
|
@@ -468,10 +454,7 @@ const AIDER_RAW = 'https://raw.githubusercontent.com/Aider-AI/aider/main/aider/w
|
|
| 468 |
|
| 469 |
async function fetchAider() {
|
| 470 |
process.stdout.write('Aider: fetching edit leaderboard... ');
|
| 471 |
-
const text = await
|
| 472 |
-
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
| 473 |
-
return r.text();
|
| 474 |
-
});
|
| 475 |
|
| 476 |
const rows = yaml.load(text);
|
| 477 |
|
|
|
|
| 34 |
const fs = require('fs');
|
| 35 |
const path = require('path');
|
| 36 |
const yaml = require('js-yaml');
|
| 37 |
+
const { getJson, getText } = require('./fetch-utils');
|
| 38 |
|
| 39 |
const OUT_FILE = path.join(__dirname, '..', 'data', 'benchmarks.json');
|
| 40 |
|
| 41 |
// βββ helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
const normName = (s) =>
|
| 44 |
(s || '').toLowerCase().replace(/[-_.]/g, ' ').replace(/[^a-z0-9 ]/g, '').replace(/\s+/g, ' ').trim();
|
| 45 |
|
|
|
|
| 234 |
console.log(`${dates.length} releases (${dates[0]} β ${dates[dates.length - 1]})`);
|
| 235 |
|
| 236 |
// Use taskβgroup mapping from the latest categories JSON (stable across releases)
|
| 237 |
+
const cats = await getJson(`${LB_BASE_URL}/categories_${dates[dates.length - 1]}.json`);
|
|
|
|
|
|
|
| 238 |
|
| 239 |
const taskToGroup = {};
|
| 240 |
for (const [cat, tasks] of Object.entries(cats)) {
|
|
|
|
| 254 |
for (const date of dates) {
|
| 255 |
let csv;
|
| 256 |
try {
|
| 257 |
+
csv = await getText(`${LB_BASE_URL}/table_${date}.csv`);
|
|
|
|
|
|
|
| 258 |
} catch (e) {
|
| 259 |
console.warn(`\n β LiveBench ${date}: ${e.message}`);
|
| 260 |
continue;
|
|
|
|
| 382 |
// Requesting with "RSC: 1" returns a streaming text/x-component payload that
|
| 383 |
// embeds the full leaderboard entries (rank, ELO rating, votes) in the server
|
| 384 |
// response β no authentication required.
|
| 385 |
+
const text = await getText('https://lmarena.ai/en/leaderboard/text', {
|
| 386 |
headers: {
|
| 387 |
'User-Agent': 'Mozilla/5.0',
|
| 388 |
'RSC': '1',
|
| 389 |
'Accept': 'text/x-component',
|
| 390 |
},
|
|
|
|
|
|
|
|
|
|
| 391 |
});
|
| 392 |
|
| 393 |
// Each RSC line has the format: <hex_id>:<json_value>
|
|
|
|
| 454 |
|
| 455 |
async function fetchAider() {
|
| 456 |
process.stdout.write('Aider: fetching edit leaderboard... ');
|
| 457 |
+
const text = await getText(AIDER_RAW);
|
|
|
|
|
|
|
|
|
|
| 458 |
|
| 459 |
const rows = yaml.load(text);
|
| 460 |
|
scripts/fetch-utils.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use strict';
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Robust fetch helper with retries, exponential backoff, and timeout.
|
| 5 |
+
*/
|
| 6 |
+
async function fetchRobust(url, options = {}) {
|
| 7 |
+
const { retries = 5, backoff = 1000, timeout = 60000, ...fetchOptions } = options;
|
| 8 |
+
let lastError;
|
| 9 |
+
|
| 10 |
+
for (let i = 0; i < retries; i++) {
|
| 11 |
+
const controller = new AbortController();
|
| 12 |
+
const timer = setTimeout(() => controller.abort(), timeout);
|
| 13 |
+
try {
|
| 14 |
+
const res = await fetch(url, {
|
| 15 |
+
...fetchOptions,
|
| 16 |
+
signal: controller.signal,
|
| 17 |
+
headers: {
|
| 18 |
+
'User-Agent': 'providers-benchmark-fetcher',
|
| 19 |
+
...fetchOptions.headers,
|
| 20 |
+
},
|
| 21 |
+
});
|
| 22 |
+
clearTimeout(timer);
|
| 23 |
+
|
| 24 |
+
if (res.ok) return res;
|
| 25 |
+
|
| 26 |
+
// Retry on transient status codes: 429 (Rate Limit) and 5xx (Server Errors)
|
| 27 |
+
if (res.status === 429 || (res.status >= 500 && res.status < 600)) {
|
| 28 |
+
lastError = new Error(`HTTP ${res.status} from ${url}`);
|
| 29 |
+
if (i < retries - 1) {
|
| 30 |
+
const delay = backoff * Math.pow(2, i) + Math.random() * 1000;
|
| 31 |
+
process.stdout.write(`\n β ${lastError.message}. Retrying in ${Math.round(delay)}ms... (${i + 1}/${retries})\n`);
|
| 32 |
+
await new Promise((r) => setTimeout(r, delay));
|
| 33 |
+
continue;
|
| 34 |
+
}
|
| 35 |
+
} else {
|
| 36 |
+
// Don't retry on other 4xx errors (e.g. 404, 401, 403)
|
| 37 |
+
throw new Error(`HTTP ${res.status} from ${url}`);
|
| 38 |
+
}
|
| 39 |
+
} catch (err) {
|
| 40 |
+
clearTimeout(timer);
|
| 41 |
+
lastError = err;
|
| 42 |
+
const isTimeout = err.name === 'AbortError';
|
| 43 |
+
if (i < retries - 1) {
|
| 44 |
+
const delay = backoff * Math.pow(2, i) + Math.random() * 1000;
|
| 45 |
+
const msg = isTimeout ? `Timeout after ${timeout}ms` : err.message;
|
| 46 |
+
process.stdout.write(`\n β Fetch error from ${url}: ${msg}. Retrying in ${Math.round(delay)}ms... (${i + 1}/${retries})\n`);
|
| 47 |
+
await new Promise((r) => setTimeout(r, delay));
|
| 48 |
+
continue;
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
throw lastError;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
async function getJson(url, options = {}) {
|
| 56 |
+
const res = await fetchRobust(url, { ...options, headers: { Accept: 'application/json', ...options.headers } });
|
| 57 |
+
return res.json();
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
async function getText(url, options = {}) {
|
| 61 |
+
const res = await fetchRobust(url, options);
|
| 62 |
+
return res.text();
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
module.exports = {
|
| 66 |
+
fetchRobust,
|
| 67 |
+
getJson,
|
| 68 |
+
getText,
|
| 69 |
+
};
|
scripts/providers/black-forest-labs.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
| 17 |
*/
|
| 18 |
|
| 19 |
const URL = 'https://bfl.ai/pricing';
|
|
|
|
| 20 |
|
| 21 |
// Extract plain text from Sanity portable text blocks.
|
| 22 |
function portableTextToString(blocks) {
|
|
@@ -52,7 +53,7 @@ function extractJsonArray(str, marker) {
|
|
| 52 |
}
|
| 53 |
|
| 54 |
async function fetchBfl() {
|
| 55 |
-
const
|
| 56 |
headers: {
|
| 57 |
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 58 |
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
@@ -60,9 +61,6 @@ async function fetchBfl() {
|
|
| 60 |
},
|
| 61 |
});
|
| 62 |
|
| 63 |
-
if (!response.ok) throw new Error(`HTTP ${response.status} from ${URL}`);
|
| 64 |
-
const html = await response.text();
|
| 65 |
-
|
| 66 |
// Decode all RSC payload chunks
|
| 67 |
let combined = '';
|
| 68 |
const pushRe = /self\.__next_f\.push\(\[1,"([\s\S]*?)"\]\)/g;
|
|
|
|
| 17 |
*/
|
| 18 |
|
| 19 |
const URL = 'https://bfl.ai/pricing';
|
| 20 |
+
const { getText } = require('../fetch-utils');
|
| 21 |
|
| 22 |
// Extract plain text from Sanity portable text blocks.
|
| 23 |
function portableTextToString(blocks) {
|
|
|
|
| 53 |
}
|
| 54 |
|
| 55 |
async function fetchBfl() {
|
| 56 |
+
const html = await getText(URL, {
|
| 57 |
headers: {
|
| 58 |
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 59 |
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
|
|
| 61 |
},
|
| 62 |
});
|
| 63 |
|
|
|
|
|
|
|
|
|
|
| 64 |
// Decode all RSC payload chunks
|
| 65 |
let combined = '';
|
| 66 |
const pushRe = /self\.__next_f\.push\(\[1,"([\s\S]*?)"\]\)/g;
|
scripts/providers/groq.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
| 14 |
*/
|
| 15 |
|
| 16 |
const cheerio = require('cheerio');
|
|
|
|
| 17 |
|
| 18 |
const URL = 'https://groq.com/pricing';
|
| 19 |
|
|
@@ -40,16 +41,13 @@ const cellText = ($, cell) => {
|
|
| 40 |
};
|
| 41 |
|
| 42 |
async function fetchGroq() {
|
| 43 |
-
const
|
| 44 |
headers: {
|
| 45 |
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 46 |
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
| 47 |
'Accept-Language': 'en-US,en;q=0.9',
|
| 48 |
},
|
| 49 |
});
|
| 50 |
-
|
| 51 |
-
if (!response.ok) throw new Error(`HTTP ${response.status} from ${URL}`);
|
| 52 |
-
const html = await response.text();
|
| 53 |
const $ = cheerio.load(html);
|
| 54 |
|
| 55 |
const models = [];
|
|
|
|
| 14 |
*/
|
| 15 |
|
| 16 |
const cheerio = require('cheerio');
|
| 17 |
+
const { getText } = require('../fetch-utils');
|
| 18 |
|
| 19 |
const URL = 'https://groq.com/pricing';
|
| 20 |
|
|
|
|
| 41 |
};
|
| 42 |
|
| 43 |
async function fetchGroq() {
|
| 44 |
+
const html = await getText(URL, {
|
| 45 |
headers: {
|
| 46 |
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 47 |
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
| 48 |
'Accept-Language': 'en-US,en;q=0.9',
|
| 49 |
},
|
| 50 |
});
|
|
|
|
|
|
|
|
|
|
| 51 |
const $ = cheerio.load(html);
|
| 52 |
|
| 53 |
const models = [];
|
scripts/providers/infomaniak.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
| 20 |
*/
|
| 21 |
|
| 22 |
const cheerio = require('cheerio');
|
|
|
|
| 23 |
|
| 24 |
const URL = 'https://www.infomaniak.com/en/hosting/ai-services/prices';
|
| 25 |
|
|
@@ -44,16 +45,13 @@ const inferType = (name) => {
|
|
| 44 |
};
|
| 45 |
|
| 46 |
async function fetchInfomaniak() {
|
| 47 |
-
const
|
| 48 |
headers: {
|
| 49 |
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 50 |
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
| 51 |
'Accept-Language': 'en-US,en;q=0.9',
|
| 52 |
},
|
| 53 |
});
|
| 54 |
-
|
| 55 |
-
if (!response.ok) throw new Error(`HTTP ${response.status} from ${URL}`);
|
| 56 |
-
const html = await response.text();
|
| 57 |
const $ = cheerio.load(html);
|
| 58 |
|
| 59 |
const models = [];
|
|
|
|
| 20 |
*/
|
| 21 |
|
| 22 |
const cheerio = require('cheerio');
|
| 23 |
+
const { getText } = require('../fetch-utils');
|
| 24 |
|
| 25 |
const URL = 'https://www.infomaniak.com/en/hosting/ai-services/prices';
|
| 26 |
|
|
|
|
| 45 |
};
|
| 46 |
|
| 47 |
async function fetchInfomaniak() {
|
| 48 |
+
const html = await getText(URL, {
|
| 49 |
headers: {
|
| 50 |
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 51 |
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
| 52 |
'Accept-Language': 'en-US,en;q=0.9',
|
| 53 |
},
|
| 54 |
});
|
|
|
|
|
|
|
|
|
|
| 55 |
const $ = cheerio.load(html);
|
| 56 |
|
| 57 |
const models = [];
|
scripts/providers/ionos.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
| 18 |
*/
|
| 19 |
|
| 20 |
const cheerio = require('cheerio');
|
|
|
|
| 21 |
|
| 22 |
const URL = 'https://cloud.ionos.com/managed/ai-model-hub';
|
| 23 |
|
|
@@ -40,16 +41,13 @@ const splitModels = (text) =>
|
|
| 40 |
.filter(Boolean);
|
| 41 |
|
| 42 |
async function fetchIonos() {
|
| 43 |
-
const
|
| 44 |
headers: {
|
| 45 |
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 46 |
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
| 47 |
'Accept-Language': 'en-US,en;q=0.9',
|
| 48 |
},
|
| 49 |
});
|
| 50 |
-
|
| 51 |
-
if (!response.ok) throw new Error(`HTTP ${response.status} from ${URL}`);
|
| 52 |
-
const html = await response.text();
|
| 53 |
const $ = cheerio.load(html);
|
| 54 |
|
| 55 |
const models = [];
|
|
|
|
| 18 |
*/
|
| 19 |
|
| 20 |
const cheerio = require('cheerio');
|
| 21 |
+
const { getText } = require('../fetch-utils');
|
| 22 |
|
| 23 |
const URL = 'https://cloud.ionos.com/managed/ai-model-hub';
|
| 24 |
|
|
|
|
| 41 |
.filter(Boolean);
|
| 42 |
|
| 43 |
async function fetchIonos() {
|
| 44 |
+
const html = await getText(URL, {
|
| 45 |
headers: {
|
| 46 |
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 47 |
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
| 48 |
'Accept-Language': 'en-US,en;q=0.9',
|
| 49 |
},
|
| 50 |
});
|
|
|
|
|
|
|
|
|
|
| 51 |
const $ = cheerio.load(html);
|
| 52 |
|
| 53 |
const models = [];
|
scripts/providers/langdock.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
| 21 |
|
| 22 |
const cheerio = require('cheerio');
|
| 23 |
const { loadEnv } = require('../load-env');
|
|
|
|
| 24 |
loadEnv();
|
| 25 |
|
| 26 |
const MODELS_URL = 'https://langdock.com/models';
|
|
@@ -39,16 +40,13 @@ const getSizeB = (name) => {
|
|
| 39 |
};
|
| 40 |
|
| 41 |
async function fetchLangdock() {
|
| 42 |
-
const
|
| 43 |
headers: {
|
| 44 |
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 45 |
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
| 46 |
'Accept-Language': 'en-US,en;q=0.9',
|
| 47 |
},
|
| 48 |
});
|
| 49 |
-
|
| 50 |
-
if (!response.ok) throw new Error(`HTTP ${response.status} from ${MODELS_URL}`);
|
| 51 |
-
const html = await response.text();
|
| 52 |
const $ = cheerio.load(html);
|
| 53 |
|
| 54 |
const models = [];
|
|
|
|
| 21 |
|
| 22 |
const cheerio = require('cheerio');
|
| 23 |
const { loadEnv } = require('../load-env');
|
| 24 |
+
const { getText } = require('../fetch-utils');
|
| 25 |
loadEnv();
|
| 26 |
|
| 27 |
const MODELS_URL = 'https://langdock.com/models';
|
|
|
|
| 40 |
};
|
| 41 |
|
| 42 |
async function fetchLangdock() {
|
| 43 |
+
const html = await getText(MODELS_URL, {
|
| 44 |
headers: {
|
| 45 |
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 46 |
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
| 47 |
'Accept-Language': 'en-US,en;q=0.9',
|
| 48 |
},
|
| 49 |
});
|
|
|
|
|
|
|
|
|
|
| 50 |
const $ = cheerio.load(html);
|
| 51 |
|
| 52 |
const models = [];
|
scripts/providers/mistral.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
| 10 |
*/
|
| 11 |
|
| 12 |
const URL = 'https://mistral.ai/pricing';
|
|
|
|
| 13 |
|
| 14 |
const stripHtml = (html) => (html || '').replace(/<[^>]+>/g, '').trim();
|
| 15 |
|
|
@@ -63,7 +64,7 @@ function extractApisArray(payload) {
|
|
| 63 |
}
|
| 64 |
|
| 65 |
async function fetchMistral() {
|
| 66 |
-
const
|
| 67 |
headers: {
|
| 68 |
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 69 |
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
@@ -71,9 +72,6 @@ async function fetchMistral() {
|
|
| 71 |
},
|
| 72 |
});
|
| 73 |
|
| 74 |
-
if (!response.ok) throw new Error(`HTTP ${response.status} from ${URL}`);
|
| 75 |
-
const html = await response.text();
|
| 76 |
-
|
| 77 |
// The page uses Next.js App Router RSC streaming. Pricing data is in a
|
| 78 |
// self.__next_f.push([1, "ENCODED_STRING"]) script tag. Inside the raw HTML,
|
| 79 |
// inner quotes are escaped as \" so we search for the literal \\"apis\\":[{
|
|
|
|
| 10 |
*/
|
| 11 |
|
| 12 |
const URL = 'https://mistral.ai/pricing';
|
| 13 |
+
const { getText } = require('../fetch-utils');
|
| 14 |
|
| 15 |
const stripHtml = (html) => (html || '').replace(/<[^>]+>/g, '').trim();
|
| 16 |
|
|
|
|
| 64 |
}
|
| 65 |
|
| 66 |
async function fetchMistral() {
|
| 67 |
+
const html = await getText(URL, {
|
| 68 |
headers: {
|
| 69 |
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 70 |
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
|
|
| 72 |
},
|
| 73 |
});
|
| 74 |
|
|
|
|
|
|
|
|
|
|
| 75 |
// The page uses Next.js App Router RSC streaming. Pricing data is in a
|
| 76 |
// self.__next_f.push([1, "ENCODED_STRING"]) script tag. Inside the raw HTML,
|
| 77 |
// inner quotes are escaped as \" so we search for the literal \\"apis\\":[{
|
scripts/providers/nebius.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
| 13 |
* ['Model','Input'] β image gen / embeddings; single rows
|
| 14 |
*/
|
| 15 |
|
|
|
|
|
|
|
| 16 |
const URL = 'https://nebius.com/token-factory/prices';
|
| 17 |
|
| 18 |
const parseUsd = (text) => {
|
|
@@ -122,7 +124,7 @@ function modelsFromTable({ rows }) {
|
|
| 122 |
}
|
| 123 |
|
| 124 |
async function fetchNebius() {
|
| 125 |
-
const
|
| 126 |
headers: {
|
| 127 |
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 128 |
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
@@ -130,8 +132,6 @@ async function fetchNebius() {
|
|
| 130 |
},
|
| 131 |
});
|
| 132 |
|
| 133 |
-
if (!response.ok) throw new Error(`HTTP ${response.status} from ${URL}`);
|
| 134 |
-
const html = await response.text();
|
| 135 |
if (html.includes('cf-browser-verification') || html.includes('Just a moment')) {
|
| 136 |
throw new Error('Blocked by Cloudflare');
|
| 137 |
}
|
|
|
|
| 13 |
* ['Model','Input'] β image gen / embeddings; single rows
|
| 14 |
*/
|
| 15 |
|
| 16 |
+
const { getText } = require('../fetch-utils');
|
| 17 |
+
|
| 18 |
const URL = 'https://nebius.com/token-factory/prices';
|
| 19 |
|
| 20 |
const parseUsd = (text) => {
|
|
|
|
| 124 |
}
|
| 125 |
|
| 126 |
async function fetchNebius() {
|
| 127 |
+
const html = await getText(URL, {
|
| 128 |
headers: {
|
| 129 |
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 130 |
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
|
|
| 132 |
},
|
| 133 |
});
|
| 134 |
|
|
|
|
|
|
|
| 135 |
if (html.includes('cf-browser-verification') || html.includes('Just a moment')) {
|
| 136 |
throw new Error('Blocked by Cloudflare');
|
| 137 |
}
|
scripts/providers/openrouter.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
| 9 |
|
| 10 |
const { loadEnv } = require('../load-env');
|
| 11 |
loadEnv();
|
|
|
|
| 12 |
|
| 13 |
const API_URL = 'https://openrouter.ai/api/v1/models';
|
| 14 |
|
|
@@ -61,13 +62,7 @@ async function fetchOpenRouter() {
|
|
| 61 |
};
|
| 62 |
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
|
| 63 |
|
| 64 |
-
const
|
| 65 |
-
|
| 66 |
-
if (!response.ok) {
|
| 67 |
-
throw new Error(`HTTP ${response.status} from OpenRouter API`);
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
const data = await response.json();
|
| 71 |
const models = [];
|
| 72 |
|
| 73 |
for (const model of data.data || []) {
|
|
|
|
| 9 |
|
| 10 |
const { loadEnv } = require('../load-env');
|
| 11 |
loadEnv();
|
| 12 |
+
const { getJson } = require('../fetch-utils');
|
| 13 |
|
| 14 |
const API_URL = 'https://openrouter.ai/api/v1/models';
|
| 15 |
|
|
|
|
| 62 |
};
|
| 63 |
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
|
| 64 |
|
| 65 |
+
const data = await getJson(API_URL, { headers });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
const models = [];
|
| 67 |
|
| 68 |
for (const model of data.data || []) {
|
scripts/providers/requesty.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
| 2 |
|
| 3 |
const { loadEnv } = require('../load-env');
|
| 4 |
loadEnv();
|
|
|
|
| 5 |
|
| 6 |
const API_URL = 'https://router.requesty.ai/v1/models';
|
| 7 |
|
|
@@ -23,18 +24,13 @@ async function fetchRequesty() {
|
|
| 23 |
return [];
|
| 24 |
}
|
| 25 |
|
| 26 |
-
const
|
| 27 |
headers: {
|
| 28 |
Authorization: `Bearer ${apiKey}`,
|
| 29 |
Accept: 'application/json',
|
| 30 |
},
|
| 31 |
});
|
| 32 |
|
| 33 |
-
if (!response.ok) {
|
| 34 |
-
throw new Error(`HTTP ${response.status} from Requesty API`);
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
const data = await response.json();
|
| 38 |
const models = [];
|
| 39 |
|
| 40 |
for (const model of data.data || []) {
|
|
|
|
| 2 |
|
| 3 |
const { loadEnv } = require('../load-env');
|
| 4 |
loadEnv();
|
| 5 |
+
const { getJson } = require('../fetch-utils');
|
| 6 |
|
| 7 |
const API_URL = 'https://router.requesty.ai/v1/models';
|
| 8 |
|
|
|
|
| 24 |
return [];
|
| 25 |
}
|
| 26 |
|
| 27 |
+
const data = await getJson(API_URL, {
|
| 28 |
headers: {
|
| 29 |
Authorization: `Bearer ${apiKey}`,
|
| 30 |
Accept: 'application/json',
|
| 31 |
},
|
| 32 |
});
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
const models = [];
|
| 35 |
|
| 36 |
for (const model of data.data || []) {
|
scripts/providers/scaleway.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
'use strict';
|
| 2 |
|
| 3 |
const cheerio = require('cheerio');
|
|
|
|
| 4 |
|
| 5 |
const URL = 'https://www.scaleway.com/en/pricing/model-as-a-service/';
|
| 6 |
|
|
@@ -28,7 +29,7 @@ const getSizeB = (name) => {
|
|
| 28 |
};
|
| 29 |
|
| 30 |
async function fetchScaleway() {
|
| 31 |
-
const
|
| 32 |
headers: {
|
| 33 |
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 34 |
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
@@ -37,12 +38,6 @@ async function fetchScaleway() {
|
|
| 37 |
},
|
| 38 |
});
|
| 39 |
|
| 40 |
-
if (!response.ok) {
|
| 41 |
-
throw new Error(`HTTP ${response.status} fetching ${URL}`);
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
const html = await response.text();
|
| 45 |
-
|
| 46 |
// Quick sanity check for Cloudflare block
|
| 47 |
if (html.includes('cf-browser-verification') || html.includes('Just a moment')) {
|
| 48 |
throw new Error('Blocked by Cloudflare β try adding a delay or using a browser-based fetcher');
|
|
|
|
| 1 |
'use strict';
|
| 2 |
|
| 3 |
const cheerio = require('cheerio');
|
| 4 |
+
const { getText } = require('../fetch-utils');
|
| 5 |
|
| 6 |
const URL = 'https://www.scaleway.com/en/pricing/model-as-a-service/';
|
| 7 |
|
|
|
|
| 29 |
};
|
| 30 |
|
| 31 |
async function fetchScaleway() {
|
| 32 |
+
const html = await getText(URL, {
|
| 33 |
headers: {
|
| 34 |
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 35 |
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
|
|
| 38 |
},
|
| 39 |
});
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
// Quick sanity check for Cloudflare block
|
| 42 |
if (html.includes('cf-browser-verification') || html.includes('Just a moment')) {
|
| 43 |
throw new Error('Blocked by Cloudflare β try adding a delay or using a browser-based fetcher');
|