openvibers/openvibe.ai Commits
Read Verda, Thunder and Shadeform public GPU prices (#491)
Changes
@@@ -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(/ /g, ' ').replace(/&/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,};@@@ -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>nnew file mode 100644@@@ -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>nnew file mode 100644@@@ -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>nnew file mode 100644@@@ -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>@@@ -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', asyconst 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 () => {
@@ -186,6 +186,67 @@ function parseTensordock(html) {
@@ -186,6 +186,67 @@ function parseTensordock(html) {
return out;
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(/ /g, ' ').replace(/&/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". */
/** CloudRift's public table: only the cards whose on-demand rate is a number; its H100/H200 say "Upon request". */
function parseCloudrift(html) {
function parseCloudrift(html) {
const text = String(html).replace(/<script[\s\S]*?<\/script>/gi, ' ').replace(/<style[\s\S]*?<\/style>/gi, ' ')
const text = String(html).replace(/<script[\s\S]*?<\/script>/gi, ' ').replace(/<style[\s\S]*?<\/style>/gi, ' ')
@@ -261,6 +322,30 @@ const SOURCES = Object.freeze([
@@ -261,6 +322,30 @@ const SOURCES = Object.freeze([
return parseCloudrift(await read(fetchImpl, 'https://www.cloudrift.ai/pricing', { timeoutMs }));
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]));
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
@@ -435,6 +520,6 @@ function createGpuMarket({ db, config = {}, clock = { now: () => Date.now() }, f
}
}
module.exports = {
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,
TARGETS, SOURCES, SOURCE_BY_ID, HOUR_MS,
};
};
@@ -2,10 +2,10 @@
@@ -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">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 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 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 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 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 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>
</body></html>
new file mode 100644
new file mode 100644
@@ -0,0 +1,5 @@
@@ -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>
new file mode 100644
new file mode 100644
@@ -0,0 +1,5 @@
@@ -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>
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -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>
@@ -25,6 +25,9 @@ function fixtureFetch({ fail = null } = {}) {
@@ -25,6 +25,9 @@ function fixtureFetch({ fail = null } = {}) {
else if (String(url).includes('salad.com')) name = 'salad-pricing.html';
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('tensordock.com')) name = 'tensordock-pricing.html';
else if (String(url).includes('cloudrift.ai')) name = 'cloudrift-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}`);
else throw new Error(`no saved fixture for ${url}`);
const text = fixture(name);
const text = fixture(name);
return { ok: true, status: 200, text: async () => text };
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 () =>
@@ -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-4090').hourly_usd, 0.45);
assert.equal(cloudrift.find((r) => r.gpu_key === 'rtx-5090').hourly_usd, 0.65);
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');
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 () => {
t.test('the cache keeps the lowest reading per provider, card and kind', async () => {
db = await testDb();
db = await testDb();
market = marketLib.createGpuMarket({ db: db.db, config: { gpuMarket: { enabled: false } }, clock: { now: () => now } });
market = marketLib.createGpuMarket({ db: db.db, config: { gpuMarket: { enabled: false } }, clock: { now: () => now } });
const out = await market.refresh({ fetchImpl: fixtureFetch() });
const out = await market.refresh({ fetchImpl: fixtureFetch() });
- assert.equal(out.refreshed, 5);
+ assert.equal(out.refreshed, 9);
assert.equal(out.failed, 0);
assert.equal(out.failed, 0);
goodReport = await market.report();
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));
assert.ok(goodReport.sources.every((s) => s.status === 'ok' && s.stale === false));
const g = (key) => goodReport.gpus.find((x) => x.key === key);
const g = (key) => goodReport.gpus.find((x) => x.key === key);
assert.equal(g('rtx-4090').on_demand.provider, 'vast');
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 () => {
@@ -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;
const okAt = goodReport.sources.find((s) => s.provider === 'runpod').ok_at;
now += 10 * 60 * 1000;
now += 10 * 60 * 1000;
const out = await market.refresh({ fetchImpl: fixtureFetch({ fail: 'runpod.io' }) });
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 report = await market.report();
const runpod = report.sources.find((s) => s.provider === 'runpod');
const runpod = report.sources.find((s) => s.provider === 'runpod');
assert.equal(runpod.status, 'failed');
assert.equal(runpod.status, 'failed');
@@ -134,7 +154,7 @@ t.test('the implied per-token cost uses the catalog throughput and the cheapest
@@ -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.
// RunPod is stale from the previous test; the 3090 and 4090 rows are Vast and fresh.
const rows = tokenCosts({ market: goodReport, models, catalog });
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 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');
const p = row('deepseek-r1-32b', 'RTX 4090', 'on_demand', 'vast');
assert.equal(p.is_cheapest, true);
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
@@ -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 read = token('console', ['ai.usage.read']);
const gpus = await request(h.base, 'GET', '/api/v1/admin/console/gpus', { tok: 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.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));
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 });
const supply = await request(h.base, 'GET', '/api/v1/admin/console/supply', { tok: read });
assert.equal(supply.status, 200, supply.text);
assert.equal(supply.status, 200, supply.text);
assert.equal(supply.body.dry_run, true);
assert.equal(supply.body.dry_run, true);
assert.equal(supply.body.market.read_at, gpus.body.market.read_at);
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 () => {
t.test('close', async () => {