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 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 fetch(`${LB_BASE_URL}/categories_${dates[dates.length - 1]}.json`, {
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 fetch(`${LB_BASE_URL}/table_${date}.csv`, {
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 fetch('https://lmarena.ai/en/leaderboard/text', {
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 fetch(AIDER_RAW, { headers: { 'User-Agent': 'providers-benchmark-fetcher' } }).then((r) => {
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 response = await fetch(URL, {
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 response = await fetch(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
-
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 response = await fetch(URL, {
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 response = await fetch(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
-
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 response = await fetch(MODELS_URL, {
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 response = await fetch(URL, {
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 response = await fetch(URL, {
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 response = await fetch(API_URL, { headers });
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 response = await fetch(API_URL, {
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 response = await fetch(URL, {
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');