openvibers/openvibe.exchange Commits

Exchange: the compute price JSON is open to any origin and documented on /api (#230)

OP OpenVibers committed 1 days ago · f9c08b7cf8 · 4 files, +22 −1

Compare reads GET /api/compute/prices from another origin, so the endpoint now
sends Access-Control-Allow-Origin: * (and answers OPTIONS), still cached for 60
seconds. The API page documents the endpoint, its shape, the open CORS and the
cache, and links /compute, OpenVibe.Compare and its JSON. /compute joins the
sitemap and llms.txt. The compute test pins the headers, the docs and that
/compute links to the comparison.

Changes

ddiff --git a/server/app.js b/server/app.js
iindex f1c64c5..47f9398 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -164,7 +164,16 @@ 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/compute/prices', (_req, res) => res.set({
++ 'Access-Control-Allow-Origin': '*',
++ 'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
++ 'Cache-Control': 'public, max-age=60',
++ }).json(require('./compute-prices')));
++ app.options('/api/compute/prices', (_req, res) => res.set({
++ 'Access-Control-Allow-Origin': '*',
++ 'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
++ 'Access-Control-Max-Age': '86400',
++ }).sendStatus(204));
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/http/discovery.js b/server/http/discovery.js
iindex e57f1d5..337471b 100644
---- a/server/http/discovery.js
++++ b/server/http/discovery.js
@@@ -19,6 +19,7 @@ const PUBLIC_PAGES = [
{ path: '/', changefreq: 'daily', priority: 1.0, title: 'OpenVibe.Exchange home', text: 'Every AI model, one account: live prices, speed and uptime, chat, and one endpoint.' },
{ path: '/models', changefreq: 'daily', priority: 0.9, title: 'Models', text: 'Every model with every offer that serves it: price in, out and cached per million tokens, measured p50 and p95 speed, uptime and data policy.' },
{ path: '/api', changefreq: 'weekly', priority: 0.7, title: 'The API', text: 'One OpenAI- and Anthropic-compatible endpoint, model ids, keys from Network and aggregate usage.' },
++ { path: '/compute', changefreq: 'weekly', priority: 0.7, title: 'Compute prices', text: 'What an hour of a GPU or a computer costs at each provider: the reviewed tariff with its source, checked and review dates, as JSON at /api/compute/prices.' },
{ path: '/start', changefreq: 'weekly', priority: 0.8, title: 'Start building', text: 'One key and one endpoint: the OpenAI and Anthropic SDK samples, four steps to a first answer, OpenVibe.AI’s six operations, and the sandbox and production limits.' },
{ path: '/why', changefreq: 'weekly', priority: 0.7, title: 'Why this model', text: 'Every answer carries its receipt: what the router optimised, the offer that answered, every other candidate’s cost and latency, and why each was passed over.' },
{ path: '/faq', changefreq: 'weekly', priority: 0.6, title: 'Questions', text: 'The endpoint, the SDKs, keys, prices, privacy, the uncensored tier, selling compute, and the move of OpenVibe.AI’s public pages here.' },
@@@ -114,6 +115,7 @@ function createDiscoveryRoutes(ctx) {
{ title: 'Models', url: abs('/models'), note: 'prices and measured latency' },
{ title: 'Rankings', url: abs('/rankings'), note: 'blind Arena votes by task, beside price and speed' },
{ title: 'The API', url: abs('/api') },
++ { title: 'Compute prices', url: abs('/compute'), note: 'GPUs and computers per hour; JSON at /api/compute/prices' },
{ title: 'Start building', url: abs('/start') },
{ title: 'Why this model', url: abs('/why') },
{ title: 'Questions', url: abs('/faq') },
ddiff --git a/server/views/exchange.js b/server/views/exchange.js
iindex a02f388..0d72044 100644
---- a/server/views/exchange.js
++++ b/server/views/exchange.js
@@@ -522,6 +522,10 @@ ${keyPrivacy && keyPrivacy.note ? kit.notice({ kind: 'info', text: keyPrivacy.no
return `<div class="ex-wrap ex-page">
${kit.head({ title: 'The API', sub: 'One endpoint. OpenAI and Anthropic compatible.', actions: [kit.button({ href: CHAT, label: 'Chat', icon: 'chat', kind: 'quiet' })] })}
${panel({ title: 'Batches, at half price', body: `<p class="ex-prose">Half price. Answered within your deadline when spare low-cost capacity is free; if none comes free in time, the prompt expires and you pay nothing.</p>` })}
++${panel({ title: 'Compute prices', id: 'compute', body: `
++<p class="ex-prose"><code>GET /api/compute/prices</code> is the reviewed compute tariff behind <a href="/compute">Compute prices</a>, as JSON. It needs no key and no session: the answer is public and carries <code>Access-Control-Allow-Origin: *</code>, so another origin can read it in a browser. It is cached for 60 seconds (<code>Cache-Control: public, max-age=60</code>). The prices are provider tariffs, not quotes: each row keeps the date it was checked (<code>checked_at</code>) and the date it must be reviewed again (<code>expires_at</code>).</p>
++<p class="ex-prose">Shape: <code>{ checked_at, prices: [{ id, provider, region?, kind: "cpu" | "gpu", cpu?, ram_mb?, gpu?, gpu_mb?, currency, hourly, per_second, billing_seconds, promotion?, promotion_expires?, interruptible?, expires_at, source }] }</code>. <code>hourly</code> and <code>per_second</code> are USD; <code>expires_at</code> is epoch milliseconds; <code>source</code> is the provider's own price page. Storage, network and plan fees are not included.</p>
++<p class="ex-prose">The same list is filtered, sorted and compared on <a href="https://openvibe.compare/compute">OpenVibe.Compare</a>, which serves the comparison at <code>https://openvibe.compare/api/v1/compute</code>.</p>` })}
${panel({ title: 'One endpoint', body: `
<p class="ex-prose"><code>${ENDPOINT}</code> speaks OpenAI's Chat Completions; <code>https://api.openvibe.exchange/v1/messages</code> speaks Anthropic's Messages API, so Claude Code works by pointing <code>ANTHROPIC_BASE_URL</code> at it.</p>
<p class="ex-prose">Coming from OpenRouter or a provider's own API? <a href="/docs/migrate">Move your application to Exchange</a>: the first request, the gateway settings, routing checks and limited keys, step by step.</p>
ddiff --git a/test/compute-prices.test.js b/test/compute-prices.test.js
iindex 4e4381c..742cc14 100644
---- a/test/compute-prices.test.js
++++ b/test/compute-prices.test.js
@@@ -6,11 +6,17 @@ const {createFakeAi}=require('./helpers/ai');
const t=await boot({ai:createFakeAi()});
try {
const r=await t.get('/api/compute/prices');assert.equal(r.status,200,r.text);
++ assert.equal(r.headers.get('access-control-allow-origin'),'*');assert.match(r.headers.get('cache-control'),/max-age=60/);
++ const options=await t.get('/api/compute/prices',{method:'OPTIONS'});assert.equal(options.status,204);assert.equal(options.headers.get('access-control-allow-origin'),'*');
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"/);
++ assert.match(page.text,/openvibe\.compare\/compute/);
++ const docs=await t.get('/api');assert.match(docs.text,/GET \/api\/compute\/prices/);assert.match(docs.text,/Access-Control-Allow-Origin: \*/);assert.match(docs.text,/max-age=60/);
++ const map=await t.get('/sitemap.xml');assert.match(map.text,/https:\/\/openvibe\.exchange\/compute<\/loc>/);
++ assert.match((await t.get('/llms.txt')).text,/https:\/\/openvibe\.exchange\/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;});