Spaces:
Paused
Paused
File size: 5,760 Bytes
ffba252 d135f12 ffba252 d135f12 ffba252 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | '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);
});
}
|