openvibers/openvibe.exchange Commits

Publish compute prices beside model prices (#198)

OP OpenVibers committed 2 days ago · ad42df36d3 · 5 files, +44 −1

* Publish dated compute prices beside model prices

* Keep unverified E2B region out of residency claims

* Cover EU sandbox promotion and keep price table scrollable

Changes

ddiff --git a/server/app.js b/server/app.js
iindex dea91e7..e2613bb 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -164,6 +164,7 @@ async function createApp(opts = {}) {
app.use(cookieParser());
// ── Machine endpoints ───────────────────────────────────
++ app.get('/api/compute/prices', (_req, res) => res.set('Cache-Control', 'public, max-age=60').json(require('./compute-prices')));
app.get('/api/health', (_req, res) => res.json({ status: 'ok', service: 'openvibe-exchange', name: 'OpenVibe.Exchange', version: VERSION }));
// IndexNow's key file (/<key>.txt), when a key is configured.
if (indexnow.enabled) app.use(indexnow.keyFile);
ddiff --git a/server/compute-prices.js b/server/compute-prices.js
nnew file mode 100644
iindex 0000000..e54f521
---- /dev/null
++++ b/server/compute-prices.js
@@@ -0,0 +1,20 @@
++'use strict';
++// Reviewed public provider tariffs, 2026-11-11. Refresh before expiry; no automatic stale-price purchases.
++const checked_at = '2026-11-11T00:00:00Z';
++const expires_at = Date.parse('2026-11-18T00:00:00Z');
++const cpu = (provider, region, hourly, source, extra = {}) => ({ id: `${provider}-${region}-2x4`, provider, region,
++ kind: 'cpu', cpu: 2, ram_mb: 4096, gpu_mb: 0, currency: 'USD', hourly, per_second: hourly / 3600,
++ billing_seconds: 1, checked_at, expires_at, source, ...extra });
++const prices = [
++ cpu('baseten', 'us-pdx-1', 2 * .0655 + 4 * .0211, 'https://www.baseten.co/pricing', { promotion: 'Free until November 16; owner must set expiry', promotion_expires: '2026-11-16T00:00:00Z' }),
++ cpu('baseten', 'eu-lon-1', 2 * .0655 + 4 * .0211, 'https://www.baseten.co/pricing', { promotion_expires: '2026-11-16T00:00:00Z' }),
++ cpu('daytona', 'us', 2 * .0504 + 4 * .0162, 'https://www.daytona.io/pricing', { disk_gb: 5, promotion: '$200 one-time credit; expiry must be confirmed in the account' }),
++ cpu('daytona', 'eu', 2 * .0504 + 4 * .0162, 'https://www.daytona.io/pricing', { disk_gb: 5 }),
++ cpu('e2b', 'provider-default', 3600 * (2 * .000014 + 4 * .0000045), 'https://e2b.dev/pricing', { promotion: '$100 one-time Hobby credit; 10 concurrent; 1-hour sessions' }),
++ // Fly publishes 2 shared vCPU / 2 GiB at $13.39 for 720 hours in iad/ewr.
++ ...['iad','ewr'].map(region => cpu('fly', region, 13.39 / 720, 'https://docs.fly.io/about/pricing', { id: `fly-${region}-2x2`, ram_mb: 2048 })),
++ { id: 'daytona-4090-preemptible', provider: 'daytona', kind: 'gpu', gpu: 'RTX 4090', gpu_mb: 24576, currency: 'USD', hourly: .57, per_second: .57/3600, interruptible: true, checked_at, expires_at, source: 'https://www.daytona.io/pricing' },
++ { id: 'daytona-4090-ondemand', provider: 'daytona', kind: 'gpu', gpu: 'RTX 4090', gpu_mb: 24576, currency: 'USD', hourly: .99, per_second: .99/3600, interruptible: false, checked_at, expires_at, source: 'https://www.daytona.io/pricing' },
++ { id: 'runpod-4090-pod', provider: 'runpod', kind: 'gpu', gpu: 'RTX 4090', gpu_mb: 24576, currency: 'USD', hourly: .89, per_second: .89/3600, checked_at, expires_at, source: 'https://www.runpod.io/pricing' },
++];
++module.exports = { checked_at, prices };
ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex 280d071..4102003 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -72,6 +72,12 @@ function createPageRoutes(ctx) {
return catalog;
};
++ r.get('/compute', (req, res) => {
++ const { prices, checked_at } = require('../compute-prices');
++ const rows = prices.map(p => `<tr><td>${esc(p.provider)}</td><td>${esc(p.id)}</td><td>${esc(p.gpu || `${p.cpu} vCPU / ${p.ram_mb} MiB`)}</td><td>$${p.hourly.toFixed(6)}/hour</td><td>$${p.per_second.toFixed(9)}/second</td><td><a href="${esc(p.source)}">Provider price</a></td></tr>`).join('');
++ return page(req,res,{title:'Compute prices',body:raw(`<main><h1>Compute prices</h1><p>Checked ${esc(checked_at)}. Base compute only; storage, network and plan fees may apply. Promotional balances are account-specific.</p><p><a href="/">Model prices</a> · <a href="https://openvibe.network/admin#ai">Owner provider settings</a></p><div style="overflow-x:auto"><table><thead><tr><th>Provider</th><th>Price ID</th><th>Resources</th><th>Hourly</th><th>Per second</th><th>Source</th></tr></thead><tbody>${rows}</tbody></table></div></main>`)});
++ });
++
// ── Exchange v1, retired ──────────────────────────────────
function retiredHandler(req, res) {
res.set('Cache-Control', 'public, max-age=86400');
ddiff --git a/server/views/platform.js b/server/views/platform.js
iindex 2c73f70..b163c82 100644
---- a/server/views/platform.js
++++ b/server/views/platform.js
@@@ -78,7 +78,7 @@ function pricingPage() {
}
function home({ catalog, usage, viewer }) {
const models = listed(catalog);
-- return `<div class="ex-wrap xp">${unavailable(catalog)}<section class="xp-hero"><h1>One API for every model.<br>Pay what the model costs.</h1>${kit.find({ action: '/models', name: 'q', placeholder: 'Find a model', icon: 'search' })}<p>${kit.button({ href: viewer?.kind === 'user' ? '/dashboard/keys#new' : '/auth/login?next=%2Fdashboard%2Fkeys%23new', label: 'Get a key' })} ${kit.button({ href: '/docs', label: 'Read the docs', kind: 'quiet' })}</p>${code()}</section>${kit.stats(
++ return `<div class="ex-wrap xp">${unavailable(catalog)}<section class="xp-hero"><h1>One API for every model.<br>Pay what the model costs.</h1>${kit.find({ action: '/models', name: 'q', placeholder: 'Find a model', icon: 'search' })}<p>${kit.button({ href: viewer?.kind === 'user' ? '/dashboard/keys#new' : '/auth/login?next=%2Fdashboard%2Fkeys%23new', label: 'Get a key' })} ${kit.button({ href: '/docs', label: 'Read the docs', kind: 'quiet' })} ${kit.button({ href: '/compute', label: 'Compute prices', kind: 'quiet' })}</p>${code()}</section>${kit.stats(
[
[models.length, 'models'],
[
ddiff --git a/test/compute-prices.test.js b/test/compute-prices.test.js
nnew file mode 100644
iindex 0000000..4e4381c
---- /dev/null
++++ b/test/compute-prices.test.js
@@@ -0,0 +1,16 @@
++'use strict';
++const assert=require('node:assert/strict');
++const {boot}=require('./helpers/boot');
++const {createFakeAi}=require('./helpers/ai');
++(async()=>{
++ const t=await boot({ai:createFakeAi()});
++ try {
++ const r=await t.get('/api/compute/prices');assert.equal(r.status,200,r.text);
++ const data=JSON.parse(r.text);assert.equal(data.prices.length,10);
++ assert.equal(data.prices.find(p=>p.id==='daytona-4090-preemptible').interruptible,true);
++ for(const p of data.prices){assert.ok(p.hourly>0);assert.ok(p.expires_at>Date.parse(p.checked_at));assert.ok(p.source.startsWith('https://'));assert.equal(p.per_second,p.hourly/3600);}
++ const page=await t.get('/compute');assert.equal(page.status,200);assert.match(page.text,/Compute prices/);assert.match(page.text,/Model prices/);
++ assert.match((await t.get('/')).text,/href="\/compute"/);
++ process.stdout.write('compute catalogue: dated source prices, spot distinction, units and model/compute links passed\n');
++ }finally{await t.close();}
++})().catch(e=>{process.stderr.write(String(e.stack)+'\n');process.exitCode=1;});