openvibers/openvibe.ai Commits

Read Verda, Thunder and Shadeform public GPU prices (#491)

OP OpenVibers committed 1 days ago · 946f67ccb0 · 6 files, +134 −12

* Read Verda, Thunder and Shadeform public GPU prices

* Read RunPod Serverless flex rates too

Changes

ddiff --git a/server/gpu/market.js b/server/gpu/market.js
iindex aa74a8d..34f2484 100644
---- a/server/gpu/market.js
++++ b/server/gpu/market.js
@@@ -186,6 +186,67 @@ function parseTensordock(html) {
return out;
}
++/** RunPod's Serverless flex rates: the same pricing page, the per-second flex column. */
++function parseRunpodServerless(html) {
++ const out = [];
++ for (const chunk of String(html).split(/class="gpu_table-row"/).slice(1)) {
++ const name = (chunk.match(/class="gpu_name[^"]*">([^<]+)</) || [])[1];
++ if (!name) continue;
++ const target = targetFor(name);
++ if (!target) continue;
++ const flex = (chunk.match(/data-serverless="([\d.]+)"/) || [])[1];
++ const row = reading(target, 'on_demand', flex, {
++ note: 'Serverless flex, billed per second from worker start to stop and scaling to zero; the OpenAI-compatible vLLM endpoint is the same card hour',
++ variant: name.trim(), source: 'https://www.runpod.io/pricing',
++ });
++ if (row) out.push(row);
++ }
++ return out;
++}
++
++/** A plain public price page: the card name followed by its published hourly rate. */
++function parsePlainPrices(html, source, rows) {
++ const text = String(html).replace(/<script[\s\S]*?<\/script>/gi, ' ').replace(/<style[\s\S]*?<\/style>/gi, ' ')
++ .replace(/<[^>]+>/g, ' ').replace(/&nbsp;/g, ' ').replace(/&amp;/g, '&').replace(/\s+/g, ' ');
++ const out = [];
++ for (const row of rows) {
++ const normalized = row.name.replace(/ /g, '\\s+');
++ const m = text.match(new RegExp(normalized + row.between + '\\$([\\d.]+)', 'i'));
++ if (!m) continue;
++ const parsed = reading(BY_KEY.get(row.key), row.kind, m[1], {
++ note: row.note, variant: row.name, source,
++ });
++ if (parsed) out.push(parsed);
++ }
++ return out;
++}
++
++/** Verda's public price page: on-demand and, where it lists them, spot rates. */
++function parseVerda(html) {
++ return parsePlainPrices(html, 'https://verda.ai/pricing', [
++ { key: 'h100', name: 'H100', between: '[\\s\\S]{0,120}?on.?demand[\\s\\S]{0,80}?', kind: 'on_demand', note: 'published on-demand rate; spot is listed on the same page and may be evicted without warning' },
++ { key: 'h100', name: 'H100', between: '[\\s\\S]{0,160}?spot[\\s\\S]{0,80}?', kind: 'spot', note: 'published spot rate; Verda may evict it at any time without warning' },
++ { key: 'a100-80gb', name: 'A100 80GB', between: '[\\s\\S]{0,120}?on.?demand[\\s\\S]{0,80}?', kind: 'on_demand', note: 'published on-demand rate' },
++ { key: 'a100-80gb', name: 'A100 80GB', between: '[\\s\\S]{0,160}?spot[\\s\\S]{0,80}?', kind: 'spot', note: 'published spot rate; Verda may evict it at any time without warning' },
++ ]);
++}
++
++/** Thunder Compute's public price page: on-demand, billed per minute. */
++function parseThunder(html) {
++ return parsePlainPrices(html, 'https://www.thundercompute.com/pricing', [
++ { key: 'h100', name: 'H100', between: '[\\s\\S]{0,80}?', kind: 'on_demand', note: 'on-demand, billed per minute' },
++ { key: 'a100-80gb', name: 'A100 80GB', between: '[\\s\\S]{0,80}?', kind: 'on_demand', note: 'on-demand, billed per minute' },
++ ]);
++}
++
++/** Shadeform's public marketplace: the lowest on-demand rate it advertises across its clouds. */
++function parseShadeform(html) {
++ return parsePlainPrices(html, 'https://shadeform.ai/pricing', [
++ { key: 'h100', name: 'H100', between: '[\\s\\S]{0,100}?(?:from|starting at)?[\\s\\S]{0,20}?', kind: 'on_demand', note: 'the lowest on-demand rate advertised across Shadeform\'s clouds; the marketplace adds no fee' },
++ { key: 'a100-80gb', name: 'A100 80GB', between: '[\\s\\S]{0,100}?(?:from|starting at)?[\\s\\S]{0,20}?', kind: 'on_demand', note: 'the lowest on-demand rate advertised across Shadeform\'s clouds; the marketplace adds no fee' },
++ ]);
++}
++
/** CloudRift's public table: only the cards whose on-demand rate is a number; its H100/H200 say "Upon request". */
function parseCloudrift(html) {
const text = String(html).replace(/<script[\s\S]*?<\/script>/gi, ' ').replace(/<style[\s\S]*?<\/style>/gi, ' ')
@@@ -261,6 +322,30 @@ const SOURCES = Object.freeze([
return parseCloudrift(await read(fetchImpl, 'https://www.cloudrift.ai/pricing', { timeoutMs }));
},
},
++ {
++ id: 'verda', label: 'Verda', source: 'https://verda.ai/pricing',
++ async read({ fetchImpl, timeoutMs }) {
++ return parseVerda(await read(fetchImpl, 'https://verda.ai/pricing', { timeoutMs }));
++ },
++ },
++ {
++ id: 'thunder', label: 'Thunder Compute', source: 'https://www.thundercompute.com/pricing',
++ async read({ fetchImpl, timeoutMs }) {
++ return parseThunder(await read(fetchImpl, 'https://www.thundercompute.com/pricing', { timeoutMs }));
++ },
++ },
++ {
++ id: 'runpod_serverless', label: 'RunPod Serverless', source: 'https://www.runpod.io/pricing',
++ async read({ fetchImpl, timeoutMs }) {
++ return parseRunpodServerless(await read(fetchImpl, 'https://www.runpod.io/pricing', { timeoutMs }));
++ },
++ },
++ {
++ id: 'shadeform', label: 'Shadeform', source: 'https://shadeform.ai/pricing',
++ async read({ fetchImpl, timeoutMs }) {
++ return parseShadeform(await read(fetchImpl, 'https://shadeform.ai/pricing', { timeoutMs }));
++ },
++ },
]);
const SOURCE_BY_ID = new Map(SOURCES.map((s) => [s.id, s]));
@@@ -435,6 +520,6 @@ function createGpuMarket({ db, config = {}, clock = { now: () => Date.now() }, f
}
module.exports = {
-- createGpuMarket, parseVast, parseRunpod, parseSalad, parseTensordock, parseCloudrift, targetFor, lowest,
++ createGpuMarket, parseVast, parseRunpod, parseSalad, parseTensordock, parseCloudrift, parseVerda, parseThunder, parseShadeform, parseRunpodServerless, targetFor, lowest,
TARGETS, SOURCES, SOURCE_BY_ID, HOUR_MS,
};
ddiff --git a/test/fixtures/gpu-market/runpod-pricing.html b/test/fixtures/gpu-market/runpod-pricing.html
iindex 11fe0a8..9a68fb6 100644
---- a/test/fixtures/gpu-market/runpod-pricing.html
++++ b/test/fixtures/gpu-market/runpod-pricing.html
@@@ -2,10 +2,10 @@
<div role="listitem" class="gpu_collection-item w-dyn-item"><div class="gpu_table-row"><p class="gpu_name is-pricing-page">H200</p><div class="gpu_table-pricing"><div data-secure="5.29" data-community="3.59" class="gpu_table-price"><span class="active-price">5.29</span></div></div></div></div>
<div role="listitem" class="gpu_collection-item w-dyn-item"><div class="gpu_table-row"><p class="gpu_name is-pricing-page">H100 NVL</p><div class="gpu_table-pricing"><div data-secure="3.19" data-community="2.59" class="gpu_table-price"><span class="active-price">3.19</span></div></div></div></div>
<div role="listitem" class="gpu_collection-item w-dyn-item"><div class="gpu_table-row"><p class="gpu_name is-pricing-page">H100 PCIe</p><div class="gpu_table-pricing"><div data-secure="2.89" data-community="1.99" class="gpu_table-price"><span class="active-price">2.89</span></div></div></div></div>
--<div role="listitem" class="gpu_collection-item w-dyn-item"><div class="gpu_table-row"><p class="gpu_name is-pricing-page">H100 SXM</p><div class="gpu_table-pricing"><div data-secure="3.99" data-community="2.69" class="gpu_table-price"><span class="active-price">3.99</span></div></div></div></div>
++<div role="listitem" class="gpu_collection-item w-dyn-item"><div class="gpu_table-row"><p class="gpu_name is-pricing-page">H100 SXM</p><div class="gpu_table-pricing"><div data-secure="3.99" data-community="2.69" data-serverless="4.79" class="gpu_table-price"><span class="active-price">3.99</span></div></div></div></div>
<div role="listitem" class="gpu_collection-item w-dyn-item"><div class="gpu_table-row"><p class="gpu_name is-pricing-page">A100 PCIe</p><div class="gpu_table-pricing"><div data-secure="1.79" data-community="1.19" class="gpu_table-price"><span class="active-price">1.79</span></div></div></div></div>
--<div role="listitem" class="gpu_collection-item w-dyn-item"><div class="gpu_table-row"><p class="gpu_name is-pricing-page">A100 SXM</p><div class="gpu_table-pricing"><div data-secure="1.79" data-community="1.39" class="gpu_table-price"><span class="active-price">1.79</span></div></div></div></div>
++<div role="listitem" class="gpu_collection-item w-dyn-item"><div class="gpu_table-row"><p class="gpu_name is-pricing-page">A100 SXM</p><div class="gpu_table-pricing"><div data-secure="1.79" data-community="1.39" data-serverless="2.72" class="gpu_table-price"><span class="active-price">1.79</span></div></div></div></div>
<div role="listitem" class="gpu_collection-item w-dyn-item"><div class="gpu_table-row"><p class="gpu_name is-pricing-page">RTX 5090</p><div class="gpu_table-pricing"><div data-secure="1.19" data-community="0.69" class="gpu_table-price"><span class="active-price">1.19</span></div></div></div></div>
<div role="listitem" class="gpu_collection-item w-dyn-item"><div class="gpu_table-row"><p class="gpu_name is-pricing-page">RTX 3090</p><div class="gpu_table-pricing"><div data-secure="0.5" data-community="0.22" class="gpu_table-price"><span class="active-price">0.5</span></div></div></div></div>
--<div role="listitem" class="gpu_collection-item w-dyn-item"><div class="gpu_table-row"><p class="gpu_name is-pricing-page">RTX 4090</p><div class="gpu_table-pricing"><div data-secure="0.89" data-community="0.34" class="gpu_table-price"><span class="active-price">0.89</span></div></div></div></div>
++<div role="listitem" class="gpu_collection-item w-dyn-item"><div class="gpu_table-row"><p class="gpu_name is-pricing-page">RTX 4090</p><div class="gpu_table-pricing"><div data-secure="0.89" data-community="0.34" data-serverless="1.10" class="gpu_table-price"><span class="active-price">0.89</span></div></div></div></div>
</body></html>
ddiff --git a/test/fixtures/gpu-market/shadeform-pricing.html b/test/fixtures/gpu-market/shadeform-pricing.html
nnew file mode 100644
iindex 0000000..237045e
---- /dev/null
++++ b/test/fixtures/gpu-market/shadeform-pricing.html
@@@ -0,0 +1,5 @@
++<!doctype html><html><body>
++<h1>Marketplace pricing</h1>
++<p>H100 from $1.99 / hour across 30+ clouds, no marketplace fee.</p>
++<p>A100 80GB from $1.10 / hour across 30+ clouds, no marketplace fee.</p>
++</body></html>
ddiff --git a/test/fixtures/gpu-market/thunder-pricing.html b/test/fixtures/gpu-market/thunder-pricing.html
nnew file mode 100644
iindex 0000000..76c56ea
---- /dev/null
++++ b/test/fixtures/gpu-market/thunder-pricing.html
@@@ -0,0 +1,5 @@
++<!doctype html><html><body>
++<h1>Pricing</h1>
++<div class="card"><h3>H100</h3><p>$3.20 / hour, billed per minute</p></div>
++<div class="card"><h3>A100 80GB</h3><p>$1.09 / hour, billed per minute</p></div>
++</body></html>
ddiff --git a/test/fixtures/gpu-market/verda-pricing.html b/test/fixtures/gpu-market/verda-pricing.html
nnew file mode 100644
iindex 0000000..0c7a9ee
---- /dev/null
++++ b/test/fixtures/gpu-market/verda-pricing.html
@@@ -0,0 +1,7 @@
++<!doctype html><html><body>
++<h1>GPU pricing</h1>
++<table>
++<tr><td>H100 SXM5 80GB</td><td>On-demand</td><td>$3.17 / GPU-hr</td><td>Spot</td><td>$1.29 / GPU-hr</td></tr>
++<tr><td>A100 80GB</td><td>On-demand</td><td>$2.20 / GPU-hr</td><td>Spot</td><td>$0.89 / GPU-hr</td></tr>
++</table>
++</body></html>
ddiff --git a/test/gpu-market.test.js b/test/gpu-market.test.js
iindex 8ce1514..5f3447d 100644
---- a/test/gpu-market.test.js
++++ b/test/gpu-market.test.js
@@@ -25,6 +25,9 @@ function fixtureFetch({ fail = null } = {}) {
else if (String(url).includes('salad.com')) name = 'salad-pricing.html';
else if (String(url).includes('tensordock.com')) name = 'tensordock-pricing.html';
else if (String(url).includes('cloudrift.ai')) name = 'cloudrift-pricing.html';
++ else if (String(url).includes('verda.ai')) name = 'verda-pricing.html';
++ else if (String(url).includes('thundercompute.com')) name = 'thunder-pricing.html';
++ else if (String(url).includes('shadeform.ai')) name = 'shadeform-pricing.html';
else throw new Error(`no saved fixture for ${url}`);
const text = fixture(name);
return { ok: true, status: 200, text: async () => text };
@@@ -77,16 +80,33 @@ t.test('the saved provider pages parse into the same reading shape', async () =>
assert.equal(cloudrift.find((r) => r.gpu_key === 'rtx-4090').hourly_usd, 0.45);
assert.equal(cloudrift.find((r) => r.gpu_key === 'rtx-5090').hourly_usd, 0.65);
assert.equal(cloudrift.some((r) => r.gpu_key === 'h100'), false, 'CloudRift quotes H100 on demand on request');
++
++ const verda = marketLib.parseVerda(fixture('verda-pricing.html'));
++ assert.equal(verda.find((r) => r.gpu_key === 'h100' && r.kind === 'on_demand').hourly_usd, 3.17);
++ assert.equal(verda.find((r) => r.gpu_key === 'h100' && r.kind === 'spot').hourly_usd, 1.29);
++ assert.equal(verda.find((r) => r.gpu_key === 'a100-80gb' && r.kind === 'spot').hourly_usd, 0.89);
++ const thunder = marketLib.parseThunder(fixture('thunder-pricing.html'));
++ assert.equal(thunder.find((r) => r.gpu_key === 'h100').hourly_usd, 3.2);
++ assert.equal(thunder.find((r) => r.gpu_key === 'a100-80gb').hourly_usd, 1.09);
++ assert.match(thunder[0].note, /per minute/);
++ const serverless = marketLib.parseRunpodServerless(fixture('runpod-pricing.html'));
++ assert.equal(serverless.find((r) => r.gpu_key === 'rtx-4090').hourly_usd, 1.1);
++ assert.equal(serverless.find((r) => r.gpu_key === 'h100').hourly_usd, 4.79);
++ assert.match(serverless[0].note, /scale|zero|per second/i);
++ const shadeform = marketLib.parseShadeform(fixture('shadeform-pricing.html'));
++ assert.equal(shadeform.find((r) => r.gpu_key === 'h100').hourly_usd, 1.99);
++ assert.equal(shadeform.find((r) => r.gpu_key === 'a100-80gb').hourly_usd, 1.1);
++ assert.match(shadeform[0].note, /no fee/);
});
t.test('the cache keeps the lowest reading per provider, card and kind', async () => {
db = await testDb();
market = marketLib.createGpuMarket({ db: db.db, config: { gpuMarket: { enabled: false } }, clock: { now: () => now } });
const out = await market.refresh({ fetchImpl: fixtureFetch() });
-- assert.equal(out.refreshed, 5);
++ assert.equal(out.refreshed, 9);
assert.equal(out.failed, 0);
goodReport = await market.report();
-- assert.equal(goodReport.sources.length, 5);
++ assert.equal(goodReport.sources.length, 9);
assert.ok(goodReport.sources.every((s) => s.status === 'ok' && s.stale === false));
const g = (key) => goodReport.gpus.find((x) => x.key === key);
assert.equal(g('rtx-4090').on_demand.provider, 'vast');
@@@ -108,8 +128,8 @@ t.test('a source that fails keeps its last reading, marked stale', async () => {
const okAt = goodReport.sources.find((s) => s.provider === 'runpod').ok_at;
now += 10 * 60 * 1000;
const out = await market.refresh({ fetchImpl: fixtureFetch({ fail: 'runpod.io' }) });
-- assert.equal(out.failed, 1);
-- assert.equal(out.refreshed, 4);
++ assert.equal(out.failed, 2);
++ assert.equal(out.refreshed, 7);
const report = await market.report();
const runpod = report.sources.find((s) => s.provider === 'runpod');
assert.equal(runpod.status, 'failed');
@@@ -134,7 +154,7 @@ t.test('the implied per-token cost uses the catalog throughput and the cheapest
];
// RunPod is stale from the previous test; the 3090 and 4090 rows are Vast and fresh.
const rows = tokenCosts({ market: goodReport, models, catalog });
-- assert.equal(rows.length, 22, 'both models, both fitting cards, both kinds and every provider reading');
++ assert.equal(rows.length, 24, 'both models, both fitting cards, both kinds and every provider reading');
const row = (key, gpu, kind, provider) => rows.find((r) => r.model_key === key && r.gpu === gpu && r.kind === kind && r.provider === provider);
const p = row('deepseek-r1-32b', 'RTX 4090', 'on_demand', 'vast');
assert.equal(p.is_cheapest, true);
@@@ -159,14 +179,14 @@ t.test('the admin API exposes both to the GPUs and supply console sections', asy
const read = token('console', ['ai.usage.read']);
const gpus = await request(h.base, 'GET', '/api/v1/admin/console/gpus', { tok: read });
assert.equal(gpus.status, 200, gpus.text);
-- assert.equal(gpus.body.market.sources.length, 5);
-- assert.equal(gpus.body.token_costs.length, 22);
++ assert.equal(gpus.body.market.sources.length, 9);
++ assert.equal(gpus.body.token_costs.length, 24);
assert.ok(gpus.body.state && Array.isArray(gpus.body.state.models));
const supply = await request(h.base, 'GET', '/api/v1/admin/console/supply', { tok: read });
assert.equal(supply.status, 200, supply.text);
assert.equal(supply.body.dry_run, true);
assert.equal(supply.body.market.read_at, gpus.body.market.read_at);
-- assert.equal(supply.body.token_costs.length, 22);
++ assert.equal(supply.body.token_costs.length, 24);
});
t.test('close', async () => {