Spaces:
Paused
Paused
| ; | |
| /** | |
| * OVHcloud AI Endpoints — EU-sovereign (France), pay-per-token, OpenAI-compatible. | |
| * | |
| * The public catalog page is a Next.js app that embeds the full model list | |
| * (categories, capabilities, HF source links and per-unit pricing) in its | |
| * streamed `self.__next_f.push([...])` payload. We reconstruct that payload, | |
| * pull out the `"models":[ ... ]` array and map it to our schema. This is far | |
| * more robust than scraping the rendered price DOM. | |
| */ | |
| const cheerio = require('cheerio'); | |
| const { getText } = require('../fetch-utils'); | |
| const URL = 'https://www.ovhcloud.com/en/public-cloud/ai-endpoints/catalog/'; | |
| // Reconstruct the decoded Next.js flight payload from all push() chunks. | |
| function decodeNextPayload(html) { | |
| const $ = cheerio.load(html); | |
| let decoded = ''; | |
| const re = /self\.__next_f\.push\(\[\d+,("(?:[^"\\]|\\.)*")\]\)/g; | |
| $('script').each((_, el) => { | |
| const c = $(el).html() || ''; | |
| if (!c.includes('__next_f.push')) return; | |
| let m; | |
| while ((m = re.exec(c))) { | |
| // The second arg is a JSON string literal — JSON.parse decodes the escapes. | |
| try { decoded += JSON.parse(m[1]); } catch (e) { /* skip malformed chunk */ } | |
| } | |
| }); | |
| return decoded; | |
| } | |
| // Bracket-match the JSON array that follows the first `"models":[` occurrence. | |
| function extractModelsArray(decoded) { | |
| const key = '"models":['; | |
| const at = decoded.indexOf(key); | |
| if (at === -1) return null; | |
| const start = at + key.length - 1; // position of the opening '[' | |
| let depth = 0, inStr = false, esc = false; | |
| for (let j = start; j < decoded.length; j++) { | |
| const ch = decoded[j]; | |
| if (esc) { esc = false; continue; } | |
| if (ch === '\\') { esc = true; continue; } | |
| if (ch === '"') { inStr = !inStr; continue; } | |
| if (inStr) continue; | |
| if (ch === '[') depth++; | |
| else if (ch === ']') { depth--; if (depth === 0) { return decoded.slice(start, j + 1); } } | |
| } | |
| return null; | |
| } | |
| const getType = (category, caps) => { | |
| const c = (category || '').toLowerCase(); | |
| if (c.includes('embedding')) return 'embedding'; | |
| if (c.includes('speech') || c.includes('audio')) return 'audio'; | |
| if (c.includes('image generation')) return 'image'; | |
| if ((caps.input_modality || []).includes('image')) return 'vision'; | |
| return 'chat'; | |
| }; | |
| const getSizeB = (name) => { | |
| const match = (name || '').match(/(?:\b|-)([\d.]+)[Bb](?:\b|-|:|$)/); | |
| if (!match) return undefined; | |
| const num = parseFloat(match[1]); | |
| return (num > 0 && num < 2000) ? num : undefined; | |
| }; | |
| const hfIdFromModel = (m) => { | |
| const link = m.metadata?.publishing_information?.source_link || ''; | |
| if (link.includes('huggingface.co/')) { | |
| return link.split('huggingface.co/')[1].replace(/\/+$/, '') || undefined; | |
| } | |
| // Fall back to an alias that looks like an HF id (publisher/model). | |
| const alias = (m.metadata?.aliases || []).find((a) => a.includes('/')); | |
| return alias || undefined; | |
| }; | |
| function mapModel(m) { | |
| const caps = m.metadata?.model_specs?.capabilities || {}; | |
| const type = getType(m.category, caps); | |
| const pricing = m.metadata?.usage_information?.pricing || []; | |
| const model = { name: m.name || m.id, type, currency: 'EUR' }; | |
| // Capabilities | |
| const capList = []; | |
| if ((caps.input_modality || []).includes('image')) capList.push('vision'); | |
| if (caps.function_calling) capList.push('tools'); | |
| if (caps.reasoning) capList.push('reasoning'); | |
| if (type === 'image') capList.push('image-gen'); | |
| if (type === 'audio') capList.push('audio'); | |
| if (capList.length) model.capabilities = capList; | |
| // Pricing — map OVH's per-unit entries onto our fields. | |
| const priceBy = {}; | |
| pricing.forEach((p) => { priceBy[p.price_unit] = p.price; }); | |
| if (priceBy.per_image !== undefined) { | |
| model.price_per_image = priceBy.per_image; | |
| } else if (priceBy.audio_duration_seconds !== undefined) { | |
| // Store per-minute to match the app's audio convention. | |
| model.price_per_minute = Math.round(priceBy.audio_duration_seconds * 60 * 1e6) / 1e6; | |
| } else { | |
| // Token/char based (chat, vision, embedding, code, moderation, TTS). | |
| const input = priceBy.million_input_tokens ?? priceBy.million_input_chars ?? 0; | |
| const output = priceBy.million_output_tokens ?? 0; | |
| model.input_price_per_1m = input; | |
| model.output_price_per_1m = output; | |
| } | |
| const hf_id = hfIdFromModel(m); | |
| if (hf_id) model.hf_id = hf_id; | |
| const size_b = getSizeB(m.name || m.id); | |
| if (size_b) model.size_b = size_b; | |
| return model; | |
| } | |
| async function fetchOvhcloud() { | |
| 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 decoded = decodeNextPayload(html); | |
| const raw = extractModelsArray(decoded); | |
| if (!raw) throw new Error('Could not locate embedded models payload (page structure changed?)'); | |
| let models; | |
| try { | |
| models = JSON.parse(raw); | |
| } catch (e) { | |
| throw new Error(`Failed to parse embedded models JSON: ${e.message}`); | |
| } | |
| return models | |
| .filter((m) => m && m.name && m.available !== false) | |
| .map(mapModel); | |
| } | |
| module.exports = { fetchOvhcloud, providerName: 'OVHcloud' }; | |
| // Run standalone: node scripts/providers/ovhcloud.js | |
| if (require.main === module) { | |
| fetchOvhcloud() | |
| .then((models) => { | |
| console.log(`Fetched ${models.length} models from OVHcloud:\n`); | |
| models.forEach((m) => { | |
| const price = m.price_per_image !== undefined | |
| ? `€${m.price_per_image}/img` | |
| : m.price_per_minute !== undefined | |
| ? `€${m.price_per_minute}/min` | |
| : `€${m.input_price_per_1m} / €${m.output_price_per_1m}`; | |
| console.log(` ${m.name.padEnd(42)} ${m.type.padEnd(10)} ${price}`); | |
| }); | |
| }) | |
| .catch((err) => { | |
| console.error('Error:', err.message); | |
| process.exit(1); | |
| }); | |
| } | |