CrispStrobe
fix: implement robust fetching with retries and exponential backoff for benchmarks and providers
d135f12
'use strict';
/**
* IONOS AI Model Hub pricing fetcher.
*
* Source: https://cloud.ionos.com/managed/ai-model-hub
* The page is SSR'd (Next.js pages router with Tailwind CSS classes).
* Pricing is embedded in real <table> elements β€” cheerio works fine.
*
* Tables on the page (desktop versions are even-indexed):
* 0. LLM / chat β€” cols: tier | model(s) | input $/M tok | output $/M tok
* The model cell can list several models separated by \n
* 2. OCR / vision β€” cols: model | input $/M tok | output $/M tok
* 4. Image β€” cols: model | price per image
* 6. Embedding β€” cols: model | price per 1M tokens
* 8. Storage β€” skip
* Odd-indexed tables (1,3,5,7,9) are mobile card duplicates of the above.
*/
const cheerio = require('cheerio');
const { getText } = require('../fetch-utils');
const URL = 'https://cloud.ionos.com/managed/ai-model-hub';
const parseUsd = (text) => {
if (!text) return null;
const m = text.trim().match(/\$?([\d]+\.[\d]*|[\d]+)/);
return m ? parseFloat(m[1]) : null;
};
const getSizeB = (name) => {
const m = (name || '').match(/[^.\d](\d+)[Bb]/) || (name || '').match(/^(\d+)[Bb]/);
return m ? parseInt(m[1]) : undefined;
};
// Split a cell value that may contain multiple model names separated by newlines
const splitModels = (text) =>
text
.split('\n')
.map((s) => s.trim())
.filter(Boolean);
async function fetchIonos() {
const html = await getText(URL, {
headers: {
'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',
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.9',
},
});
const $ = cheerio.load(html);
const models = [];
const tables = $('table').toArray();
// ── Table 0: LLM / chat ─────────────────────────────────────────────────────
// cols: tier (may be empty for continuation rows) | model(s) | input | output
const llmTable = $(tables[0]);
llmTable.find('tbody tr').each((_, row) => {
const cells = $(row).find('td');
if (cells.length < 4) return;
const rawNames = cells.eq(1).text();
const inputPrice = parseUsd(cells.eq(2).text());
const outputPrice = parseUsd(cells.eq(3).text());
if (inputPrice === null) return;
splitModels(rawNames).forEach((name) => {
if (!name) return;
const model = {
name,
type: 'chat',
input_price_per_1m: inputPrice,
output_price_per_1m: outputPrice ?? 0,
currency: 'USD',
};
const size_b = getSizeB(name);
if (size_b) model.size_b = size_b;
models.push(model);
});
});
// ── Table 2: OCR / vision ───────────────────────────────────────────────────
// cols: model | input | output
const ocrTable = $(tables[2]);
ocrTable.find('tbody tr').each((_, row) => {
const cells = $(row).find('td');
if (cells.length < 3) return;
const name = cells.eq(0).text().trim();
const inputPrice = parseUsd(cells.eq(1).text());
const outputPrice = parseUsd(cells.eq(2).text());
if (!name || inputPrice === null) return;
models.push({
name,
type: 'vision',
capabilities: ['vision', 'files'],
input_price_per_1m: inputPrice,
output_price_per_1m: outputPrice ?? 0,
currency: 'USD',
});
});
// ── Table 4: Image generation ───────────────────────────────────────────────
// cols: model | price per image
const imgTable = $(tables[4]);
imgTable.find('tbody tr').each((_, row) => {
const cells = $(row).find('td');
if (cells.length < 2) return;
// Strip badge text like " New" appended after the model name
const name = cells.eq(0).text().trim().replace(/\s+New$/, '');
const pricePerImage = parseUsd(cells.eq(1).text());
if (!name || pricePerImage === null) return;
models.push({
name,
type: 'image',
input_price_per_1m: pricePerImage,
output_price_per_1m: 0,
currency: 'USD',
});
});
// ── Table 6: Embedding ───────────────────────────────────────────────────────
// cols: model | price per 1M tokens
const embTable = $(tables[6]);
embTable.find('tbody tr').each((_, row) => {
const cells = $(row).find('td');
if (cells.length < 2) return;
const name = cells.eq(0).text().trim();
const inputPrice = parseUsd(cells.eq(1).text());
if (!name || inputPrice === null) return;
models.push({
name,
type: 'embedding',
input_price_per_1m: inputPrice,
output_price_per_1m: 0,
currency: 'USD',
});
});
return models;
}
module.exports = { fetchIonos, providerName: 'IONOS' };
if (require.main === module) {
fetchIonos()
.then((models) => {
console.log(`Fetched ${models.length} models from IONOS:\n`);
const byType = {};
models.forEach((m) => { (byType[m.type] = byType[m.type] || []).push(m); });
for (const [type, ms] of Object.entries(byType)) {
console.log(` [${type}]`);
ms.forEach((m) =>
console.log(` ${m.name.padEnd(45)} $${m.input_price_per_1m} / $${m.output_price_per_1m}`)
);
}
})
.catch((err) => {
console.error('Error:', err.message);
process.exit(1);
});
}