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);
    });
}