Spaces:
Running
Running
CrispStrobe
fix: implement robust fetching with retries and exponential backoff for benchmarks and providers
d135f12 | ; | |
| /** | |
| * Langdock model fetcher. | |
| * | |
| * Langdock's /v1/models API returns model IDs with no pricing. | |
| * Pricing lives at https://langdock.com/models β a Webflow SSR page. | |
| * | |
| * HTML structure (cheerio selectors): | |
| * div.w-dyn-item β each model card | |
| * div.models_row[fs-provider] β row with provider attribute | |
| * div.models_cell.is-model β model name cell | |
| * .text-size-small.text-weight-medium β model name text | |
| * div.models_cell (2nd) β input price cell | |
| * p.text-size-small span:eq(1) β price number | |
| * div.models_cell (3rd) β output price cell | |
| * p.text-size-small span:eq(1) β price number | |
| * | |
| * Pricing is in EUR with a stated 10% Langdock surcharge on provider rates. | |
| */ | |
| const cheerio = require('cheerio'); | |
| const { loadEnv } = require('../load-env'); | |
| const { getText } = require('../fetch-utils'); | |
| loadEnv(); | |
| const MODELS_URL = 'https://langdock.com/models'; | |
| const parseEur = (text) => { | |
| if (!text) return null; | |
| const clean = text.trim(); | |
| if (!clean || clean === '-' || clean === 'β') return null; | |
| const m = clean.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; | |
| }; | |
| async function fetchLangdock() { | |
| const html = await getText(MODELS_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 seen = new Set(); | |
| $('div.w-dyn-item').each((_, item) => { | |
| const row = $(item).find('div.models_row').first(); | |
| if (!row.length) return; | |
| const provider = row.attr('fs-provider') || ''; | |
| const nameEl = row.find('div.models_cell.is-model .text-size-small').filter((_, el) => { | |
| // Pick the element with font-weight medium (model name), not the provider label | |
| return $(el).hasClass('text-weight-medium'); | |
| }).first(); | |
| if (!nameEl.length) return; | |
| const name = nameEl.text().trim(); | |
| if (!name) return; | |
| const cells = row.find('div.models_cell').not('.is-model'); | |
| // Input and output are the first two non-model cells that contain "/ 1M tokens" | |
| let inputPrice = null; | |
| let outputPrice = null; | |
| let priceCount = 0; | |
| cells.each((_, cell) => { | |
| const text = $(cell).text(); | |
| if (!text.includes('1M tokens') && !text.includes('1M token')) return; | |
| const spans = $(cell).find('p.text-size-small span'); | |
| // Spans: [currency_symbol, price_number, unit_string] | |
| const priceSpan = spans.eq(1); | |
| const val = parseEur(priceSpan.text()); | |
| if (val === null) return; | |
| if (priceCount === 0) inputPrice = val; | |
| else if (priceCount === 1) outputPrice = val; | |
| priceCount++; | |
| }); | |
| if (inputPrice === null) return; | |
| const key = `${provider}|${name}|${inputPrice}|${outputPrice}`; | |
| if (seen.has(key)) return; | |
| seen.add(key); | |
| const size_b = getSizeB(name); | |
| const model = { | |
| name, | |
| type: 'chat', | |
| input_price_per_1m: inputPrice, | |
| output_price_per_1m: outputPrice ?? 0, | |
| currency: 'EUR', | |
| }; | |
| if (size_b) model.size_b = size_b; | |
| if (provider) model.provider_upstream = provider; | |
| models.push(model); | |
| }); | |
| return models; | |
| } | |
| module.exports = { fetchLangdock, providerName: 'Langdock' }; | |
| if (require.main === module) { | |
| fetchLangdock() | |
| .then((models) => { | |
| console.log(`Fetched ${models.length} models from Langdock:\n`); | |
| const byProvider = {}; | |
| models.forEach((m) => { | |
| const p = m.provider_upstream || 'Unknown'; | |
| (byProvider[p] = byProvider[p] || []).push(m); | |
| }); | |
| for (const [prov, ms] of Object.entries(byProvider)) { | |
| console.log(` [${prov}]`); | |
| ms.forEach((m) => | |
| console.log(` ${m.name.padEnd(40)} β¬${m.input_price_per_1m} / β¬${m.output_price_per_1m}`) | |
| ); | |
| } | |
| }) | |
| .catch((err) => { | |
| console.error('Error:', err.message); | |
| process.exit(1); | |
| }); | |
| } | |