openvibers/openvibe.exchange Commits
Preview GPU rental quotes and expose placement strategies (#124)
Changes
@@@ -85,3 +85,9 @@ AGPL-3.0-only. See [LICENSE](LICENSE).- openvibe-shared: v4.153.0- openvibe-publishing: v1.5.0<!-- versions:end -->++++`/own` also previews GPU quotes without sign-in or a rental. Choose Cheapest, Fastest or++Automatic for a rental; Most private and On my machine explain when a Link/OpenVibe machine++is required. The preview uses AI's cached public GPU quote API and never takes a Balance hold.++Provider keys, monthly caps, provisioning and the journal remain in AI; desktop/browser pools++and their operator view remain in Run. The Exchange keeps no provider credentials.@@@ -649,3 +649,9 @@ body.ex-companion-shell > main.ex { flex: 1; min-height: 0; display: flex; }.ex-why-gate { margin: 8px 0; }.ex-faq { display: grid; gap: 10px; }.ex-faq .ovk-fold { margin: 0; }++++/* GPU quote and rental forms share the card's text inset, including the returned quote. */++#quote .ovk-form, #quote [role="status"], #model .ovk-form, #weights .ovk-form {++ margin: 14px; display: grid; gap: 10px; min-width: 0;++}++#quote .ovk-form select, #model .ovk-form select { width: 100%; min-width: 0; min-height: 44px; }@@@ -268,12 +268,13 @@ function createAi({ config, fetchImpl = globalThis.fetch, log = console, now = (}return out || {};}++ async function gpuQuote(model, strategy = 'cheapest') { return ownCall('GET', `/api/v1/exchange/gpu-quotes/${encodeURIComponent(model)}?strategy=${encodeURIComponent(strategy)}`); }async function ownStatus() { return ownCall('GET', '/api/v1/exchange/endpoints'); }async function ownList(token) { return ownCall('GET', '/api/v1/own-endpoints', { token }); }async function ownCreate(token, body) { return ownCall('POST', '/api/v1/own-endpoints', { token, body }); }async function ownStop(token, id) { return ownCall('POST', `/api/v1/own-endpoints/${encodeURIComponent(id)}/stop`, { token }); }-- return { catalog, usage, communityOffers, chat, presetsFor, setKeyPrivacy, ownedProjects, ownStatus, ownList, ownCreate, ownStop, configured: Boolean(tokens) };++ return { catalog, usage, communityOffers, chat, presetsFor, setKeyPrivacy, ownedProjects, gpuQuote, ownStatus, ownList, ownCreate, ownStop, configured: Boolean(tokens) };}/**@@@ -750,7 +750,7 @@ ${m.meta && m.meta.preset ? `<dt>Preset</dt><dd>${esc(m.meta.preset)}</dd>` : ''// ── Your own endpoint ────────────────────────────────────// The private GPU page. The status is public (so the reason it is off is visible before signing in);// a person's own endpoints are read from AI with their own Network token, never with the service token.-- async function renderOwn(req, res, { error = null } = {}) {++ async function renderOwn(req, res, { error = null, quote = null } = {}) {let status = null;let problem = error;try { status = await ai.ownStatus(); } catch (err) {@@@ -772,16 +772,24 @@ ${m.meta && m.meta.preset ? `<dt>Preset</dt><dd>${esc(m.meta.preset)}</dd>` : ''index: true, cache: signedIn(req) ? null : PUBLIC_CACHE, agentLayer: true,title: 'Your own endpoint',description: 'A private GPU for one model or your own weights: only you reach it, it scales to zero when idle, and your Balance pays for the seconds it runs.',-- body: raw(views.ownPage({ status, endpoints, viewer: req.viewer, error: problem, signedIn: signedIn(req) })),++ body: raw(views.ownPage({ status, endpoints, quote, viewer: req.viewer, error: problem, signedIn: signedIn(req) })),});}r.get('/own', async (req, res) => renderOwn(req, res));++ r.get('/own/quote', async (req, res) => {++ const strategy = String(req.query.strategy || 'cheapest');++ const model = String(req.query.model || '').slice(0, 80);++ if (!['automatic','cheapest','fastest','private','own'].includes(strategy)) return renderOwn(req, res, { error: 'Choose one of the listed strategies.' });++ try { return renderOwn(req, res, { quote: await ai.gpuQuote(model, strategy) }); }++ catch { return renderOwn(req, res, { error: 'The live quote is unavailable. Nothing was rented.' }); }++ });r.post('/own', async (req, res) => {if (!signedIn(req)) return res.redirect(303, '/auth/login?next=%2Fown');if (!sameOrigin(req, config.baseUrl)) return notFromHere(req, res);const body = {};++ if (req.body.strategy) body.strategy = String(req.body.strategy).slice(0, 20);if (req.body.model) body.model = String(req.body.model).slice(0, 80);if (req.body.weights_repo || req.body.weights_file) {body.weights = {@@@ -910,7 +910,7 @@ ${panel({ title: 'Where those things live now', body: `<ul class="ex-list">* then the form. The live prices and models come from AI's public status; a person's endpoints come from* AI with their own Network token.*/--function ownPage({ status = null, endpoints = [], viewer = null, error = null, signedIn = false }) {++function ownPage({ status = null, endpoints = [], quote = null, viewer = null, error = null, signedIn = false }) {const models = (status && status.models) || [];const off = !status || status.enabled !== true;const why = (status && (status.reason || (status.reasons || []).join(' '))) || 'The endpoint service is not reachable right now.';@@@ -941,12 +941,19 @@ function ownPage({ status = null, endpoints = [], viewer = null, error = null, sreturn `<div class="ex-wrap ex-page">${kit.head({ title: 'Your own endpoint', sub: 'A private GPU for one model, or your own weights. It scales to zero when you stop using it.' })}${error ? kit.notice({ kind: 'warn', text: esc(error) }) : ''}++${panel({ title: 'Compare before renting', id: 'quote', body: `<p class="ex-prose">A quote does not start a GPU or charge your Balance. Prices and availability can change before launch.</p>++<form method="get" action="/own/quote" class="ovk-form"><label for="quote-model">Model</label><select id="quote-model" name="model">${modelOptions}</select>++<label for="quote-strategy">Placement</label><select id="quote-strategy" name="strategy"><option value="cheapest">Cheapest</option><option value="fastest">Fastest</option><option value="automatic">Automatic across marketplaces</option><option value="private">Most private</option><option value="own">On my machine</option></select>${kit.button({ label: 'Get a quote', type: 'submit' })}</form>++${quote ? `<div role="status"><p>${quote.selected ? `${esc(quote.selected.marketplace)} · ${esc(quote.selected.gpu_name)} · ${esc(quote.selected.region)} · $${esc(quote.selected.price_per_hour)} per hour` : 'No eligible marketplace GPU.'}</p><p>${esc((quote.reasons || []).join(' · '))}</p><p>Base GPU price; storage and network charges may add to it. Quote only. Nothing was rented.</p></div>` : ''}++<p class="ex-small">Most private and On my machine require your Link machine or OpenVibe capacity; a private endpoint on a third-party GPU does not meet those placement rules. <a href="https://openvibe.link">Connect your machine</a>.</p>` })}++${off ? `<div class="ex-card"><div class="ex-sec-body">${kit.notice({ kind: 'warn', text: esc(why) })}<p class="ex-prose">It turns on when the owner adds a GPU provider key and a monthly cap in the admin panel, and turns Balance billing on. Nothing is rented before then.</p></div></div>` : ''}${!off && !signedIn ? `${kit.notice({ kind: 'info', text: 'Sign in to start a private endpoint.' })}<p>${kit.button({ href: '/auth/login?next=%2Fown', label: 'Sign in' })}</p>` : ''}${!off ? `${panel({ title: 'Pick a model', id: 'model', body: models.length ? `<p class="ex-prose">The GPU is yours alone: only your own sign-in can call it, and it is never listed as an offer. The price is the cheapest GPU that fits, per hour; you pay your Balance for the seconds it runs.</p><form method="post" action="/own" class="ovk-form"><label class="ex-label" for="own-model">Model</label><select id="own-model" name="model">${modelOptions}</select>++<label for="own-strategy">Placement</label><select id="own-strategy" name="strategy"><option value="cheapest">Cheapest</option><option value="automatic">Automatic across marketplaces</option><option value="fastest">Fastest</option></select><label class="ex-label" for="own-idle">Stop after this many idle minutes</label><input id="own-idle" name="idle_minutes" type="number" min="1" max="${Number(status.max_idle_minutes || 120)}" value="${Number(status.idle_minutes || 10)}"><p class="ex-small ex-muted">How long it stays up after your last request. Lower costs less; higher avoids a cold start.</p>@@@ -14,6 +14,7 @@ const { createFakeAi } = require('./helpers/ai');status: { enabled: false, reason: 'The owner has not added a GPU provider key (Vast.ai or RunPod) yet, so no endpoint can start.', models: [], idle_minutes: 10, max_idle_minutes: 120, public_base: 'https://ai.openvibe.services/api/v1/own-endpoints' },endpoints: [], calls: [],};++ ai.gpuQuote = async (model, strategy) => { state.calls.push({ kind: 'quote', model, strategy }); return { dry_run: true, selected: { marketplace: 'vast', gpu_name: 'RTX 3090', region: 'US', price_per_hour: '0.20' }, reasons: ['objective cheapest'] }; };ai.ownStatus = async () => state.status;ai.ownList = async (token) => { state.calls.push({ kind: 'list', token }); return { endpoints: state.endpoints, status: state.status }; };ai.ownCreate = async (token, body) => { state.calls.push({ kind: 'create', token, body }); return { endpoint: { id: 'oep_TEST' } }; };@@@ -38,6 +39,14 @@ const { createFakeAi } = require('./helpers/ai');assert.ok(!r.text.includes('Start my endpoint'), 'the submit is only for a signed-in person');});++ await check('quote works signed out, shows live price, and creates no rental', async () => {++ const r = await t.get('/own/quote?model=deepseek-r1-32b&strategy=cheapest');++ assert.strictEqual(r.status, 200, r.text);++ assert.ok(r.text.includes('0.20 per hour'));++ assert.ok(r.text.includes('Nothing was rented'));++ assert.ok(!state.calls.some(c => c.kind === 'create'));++ });++const u = t.network.addUser('gpu-owner');await check('signed in: the model and own-weights forms, with the private promise', async () => {const r = await t.get('/own', { as: u });@@@ -50,13 +59,13 @@ const { createFakeAi } = require('./helpers/ai');});await check('create posts to AI with the person\'s own token and the chosen model', async () => {-- const r = await t.get('/own', { as: u, form: { model: 'deepseek-r1-32b', idle_minutes: '5' }, method: 'POST' });++ const r = await t.get('/own', { as: u, form: { model: 'deepseek-r1-32b', idle_minutes: '5', strategy: 'automatic' }, method: 'POST' });assert.strictEqual(r.status, 303, r.text);assert.strictEqual(r.headers.get('location'), '/own');const call = state.calls.find((c) => c.kind === 'create');assert.ok(call, 'AI was asked to create it');assert.ok(call.token, 'with the person\'s token, never the service token');-- assert.deepStrictEqual(call.body, { model: 'deepseek-r1-32b', idle_minutes: 5 });++ assert.deepStrictEqual(call.body, { model: 'deepseek-r1-32b', idle_minutes: 5, strategy: 'automatic' });});await check('own weights are passed as a weights object', async () => {
@@ -85,3 +85,9 @@ AGPL-3.0-only. See [LICENSE](LICENSE).
@@ -85,3 +85,9 @@ AGPL-3.0-only. See [LICENSE](LICENSE).
- openvibe-shared: v4.153.0
- openvibe-shared: v4.153.0
- openvibe-publishing: v1.5.0
- openvibe-publishing: v1.5.0
<!-- versions:end -->
<!-- versions:end -->
+
+`/own` also previews GPU quotes without sign-in or a rental. Choose Cheapest, Fastest or
+Automatic for a rental; Most private and On my machine explain when a Link/OpenVibe machine
+is required. The preview uses AI's cached public GPU quote API and never takes a Balance hold.
+Provider keys, monthly caps, provisioning and the journal remain in AI; desktop/browser pools
+and their operator view remain in Run. The Exchange keeps no provider credentials.
@@ -649,3 +649,9 @@ body.ex-companion-shell > main.ex { flex: 1; min-height: 0; display: flex; }
@@ -649,3 +649,9 @@ body.ex-companion-shell > main.ex { flex: 1; min-height: 0; display: flex; }
.ex-why-gate { margin: 8px 0; }
.ex-why-gate { margin: 8px 0; }
.ex-faq { display: grid; gap: 10px; }
.ex-faq { display: grid; gap: 10px; }
.ex-faq .ovk-fold { margin: 0; }
.ex-faq .ovk-fold { margin: 0; }
+
+/* GPU quote and rental forms share the card's text inset, including the returned quote. */
+#quote .ovk-form, #quote [role="status"], #model .ovk-form, #weights .ovk-form {
+ margin: 14px; display: grid; gap: 10px; min-width: 0;
+}
+#quote .ovk-form select, #model .ovk-form select { width: 100%; min-width: 0; min-height: 44px; }
@@ -268,12 +268,13 @@ function createAi({ config, fetchImpl = globalThis.fetch, log = console, now = (
@@ -268,12 +268,13 @@ function createAi({ config, fetchImpl = globalThis.fetch, log = console, now = (
}
}
return out || {};
return out || {};
}
}
+ async function gpuQuote(model, strategy = 'cheapest') { return ownCall('GET', `/api/v1/exchange/gpu-quotes/${encodeURIComponent(model)}?strategy=${encodeURIComponent(strategy)}`); }
async function ownStatus() { return ownCall('GET', '/api/v1/exchange/endpoints'); }
async function ownStatus() { return ownCall('GET', '/api/v1/exchange/endpoints'); }
async function ownList(token) { return ownCall('GET', '/api/v1/own-endpoints', { token }); }
async function ownList(token) { return ownCall('GET', '/api/v1/own-endpoints', { token }); }
async function ownCreate(token, body) { return ownCall('POST', '/api/v1/own-endpoints', { token, body }); }
async function ownCreate(token, body) { return ownCall('POST', '/api/v1/own-endpoints', { token, body }); }
async function ownStop(token, id) { return ownCall('POST', `/api/v1/own-endpoints/${encodeURIComponent(id)}/stop`, { token }); }
async function ownStop(token, id) { return ownCall('POST', `/api/v1/own-endpoints/${encodeURIComponent(id)}/stop`, { token }); }
- return { catalog, usage, communityOffers, chat, presetsFor, setKeyPrivacy, ownedProjects, ownStatus, ownList, ownCreate, ownStop, configured: Boolean(tokens) };
+ return { catalog, usage, communityOffers, chat, presetsFor, setKeyPrivacy, ownedProjects, gpuQuote, ownStatus, ownList, ownCreate, ownStop, configured: Boolean(tokens) };
}
}
/**
/**
@@ -750,7 +750,7 @@ ${m.meta && m.meta.preset ? `<dt>Preset</dt><dd>${esc(m.meta.preset)}</dd>` : ''
@@ -750,7 +750,7 @@ ${m.meta && m.meta.preset ? `<dt>Preset</dt><dd>${esc(m.meta.preset)}</dd>` : ''
// ── Your own endpoint ────────────────────────────────────
// ── Your own endpoint ────────────────────────────────────
// The private GPU page. The status is public (so the reason it is off is visible before signing in);
// The private GPU page. The status is public (so the reason it is off is visible before signing in);
// a person's own endpoints are read from AI with their own Network token, never with the service token.
// a person's own endpoints are read from AI with their own Network token, never with the service token.
- async function renderOwn(req, res, { error = null } = {}) {
+ async function renderOwn(req, res, { error = null, quote = null } = {}) {
let status = null;
let status = null;
let problem = error;
let problem = error;
try { status = await ai.ownStatus(); } catch (err) {
try { status = await ai.ownStatus(); } catch (err) {
@@ -772,16 +772,24 @@ ${m.meta && m.meta.preset ? `<dt>Preset</dt><dd>${esc(m.meta.preset)}</dd>` : ''
@@ -772,16 +772,24 @@ ${m.meta && m.meta.preset ? `<dt>Preset</dt><dd>${esc(m.meta.preset)}</dd>` : ''
index: true, cache: signedIn(req) ? null : PUBLIC_CACHE, agentLayer: true,
index: true, cache: signedIn(req) ? null : PUBLIC_CACHE, agentLayer: true,
title: 'Your own endpoint',
title: 'Your own endpoint',
description: 'A private GPU for one model or your own weights: only you reach it, it scales to zero when idle, and your Balance pays for the seconds it runs.',
description: 'A private GPU for one model or your own weights: only you reach it, it scales to zero when idle, and your Balance pays for the seconds it runs.',
- body: raw(views.ownPage({ status, endpoints, viewer: req.viewer, error: problem, signedIn: signedIn(req) })),
+ body: raw(views.ownPage({ status, endpoints, quote, viewer: req.viewer, error: problem, signedIn: signedIn(req) })),
});
});
}
}
r.get('/own', async (req, res) => renderOwn(req, res));
r.get('/own', async (req, res) => renderOwn(req, res));
+ r.get('/own/quote', async (req, res) => {
+ const strategy = String(req.query.strategy || 'cheapest');
+ const model = String(req.query.model || '').slice(0, 80);
+ if (!['automatic','cheapest','fastest','private','own'].includes(strategy)) return renderOwn(req, res, { error: 'Choose one of the listed strategies.' });
+ try { return renderOwn(req, res, { quote: await ai.gpuQuote(model, strategy) }); }
+ catch { return renderOwn(req, res, { error: 'The live quote is unavailable. Nothing was rented.' }); }
+ });
r.post('/own', async (req, res) => {
r.post('/own', async (req, res) => {
if (!signedIn(req)) return res.redirect(303, '/auth/login?next=%2Fown');
if (!signedIn(req)) return res.redirect(303, '/auth/login?next=%2Fown');
if (!sameOrigin(req, config.baseUrl)) return notFromHere(req, res);
if (!sameOrigin(req, config.baseUrl)) return notFromHere(req, res);
const body = {};
const body = {};
+ if (req.body.strategy) body.strategy = String(req.body.strategy).slice(0, 20);
if (req.body.model) body.model = String(req.body.model).slice(0, 80);
if (req.body.model) body.model = String(req.body.model).slice(0, 80);
if (req.body.weights_repo || req.body.weights_file) {
if (req.body.weights_repo || req.body.weights_file) {
body.weights = {
body.weights = {
@@ -910,7 +910,7 @@ ${panel({ title: 'Where those things live now', body: `<ul class="ex-list">
@@ -910,7 +910,7 @@ ${panel({ title: 'Where those things live now', body: `<ul class="ex-list">
* then the form. The live prices and models come from AI's public status; a person's endpoints come from
* then the form. The live prices and models come from AI's public status; a person's endpoints come from
* AI with their own Network token.
* AI with their own Network token.
*/
*/
-function ownPage({ status = null, endpoints = [], viewer = null, error = null, signedIn = false }) {
+function ownPage({ status = null, endpoints = [], quote = null, viewer = null, error = null, signedIn = false }) {
const models = (status && status.models) || [];
const models = (status && status.models) || [];
const off = !status || status.enabled !== true;
const off = !status || status.enabled !== true;
const why = (status && (status.reason || (status.reasons || []).join(' '))) || 'The endpoint service is not reachable right now.';
const why = (status && (status.reason || (status.reasons || []).join(' '))) || 'The endpoint service is not reachable right now.';
@@ -941,12 +941,19 @@ function ownPage({ status = null, endpoints = [], viewer = null, error = null, s
@@ -941,12 +941,19 @@ function ownPage({ status = null, endpoints = [], viewer = null, error = null, s
return `<div class="ex-wrap ex-page">
return `<div class="ex-wrap ex-page">
${kit.head({ title: 'Your own endpoint', sub: 'A private GPU for one model, or your own weights. It scales to zero when you stop using it.' })}
${kit.head({ title: 'Your own endpoint', sub: 'A private GPU for one model, or your own weights. It scales to zero when you stop using it.' })}
${error ? kit.notice({ kind: 'warn', text: esc(error) }) : ''}
${error ? kit.notice({ kind: 'warn', text: esc(error) }) : ''}
+${panel({ title: 'Compare before renting', id: 'quote', body: `<p class="ex-prose">A quote does not start a GPU or charge your Balance. Prices and availability can change before launch.</p>
+<form method="get" action="/own/quote" class="ovk-form"><label for="quote-model">Model</label><select id="quote-model" name="model">${modelOptions}</select>
+<label for="quote-strategy">Placement</label><select id="quote-strategy" name="strategy"><option value="cheapest">Cheapest</option><option value="fastest">Fastest</option><option value="automatic">Automatic across marketplaces</option><option value="private">Most private</option><option value="own">On my machine</option></select>${kit.button({ label: 'Get a quote', type: 'submit' })}</form>
+${quote ? `<div role="status"><p>${quote.selected ? `${esc(quote.selected.marketplace)} · ${esc(quote.selected.gpu_name)} · ${esc(quote.selected.region)} · $${esc(quote.selected.price_per_hour)} per hour` : 'No eligible marketplace GPU.'}</p><p>${esc((quote.reasons || []).join(' · '))}</p><p>Base GPU price; storage and network charges may add to it. Quote only. Nothing was rented.</p></div>` : ''}
+<p class="ex-small">Most private and On my machine require your Link machine or OpenVibe capacity; a private endpoint on a third-party GPU does not meet those placement rules. <a href="https://openvibe.link">Connect your machine</a>.</p>` })}
+
${off ? `<div class="ex-card"><div class="ex-sec-body">${kit.notice({ kind: 'warn', text: esc(why) })}<p class="ex-prose">It turns on when the owner adds a GPU provider key and a monthly cap in the admin panel, and turns Balance billing on. Nothing is rented before then.</p></div></div>` : ''}
${off ? `<div class="ex-card"><div class="ex-sec-body">${kit.notice({ kind: 'warn', text: esc(why) })}<p class="ex-prose">It turns on when the owner adds a GPU provider key and a monthly cap in the admin panel, and turns Balance billing on. Nothing is rented before then.</p></div></div>` : ''}
${!off && !signedIn ? `${kit.notice({ kind: 'info', text: 'Sign in to start a private endpoint.' })}<p>${kit.button({ href: '/auth/login?next=%2Fown', label: 'Sign in' })}</p>` : ''}
${!off && !signedIn ? `${kit.notice({ kind: 'info', text: 'Sign in to start a private endpoint.' })}<p>${kit.button({ href: '/auth/login?next=%2Fown', label: 'Sign in' })}</p>` : ''}
${!off ? `${panel({ title: 'Pick a model', id: 'model', body: models.length ? `<p class="ex-prose">The GPU is yours alone: only your own sign-in can call it, and it is never listed as an offer. The price is the cheapest GPU that fits, per hour; you pay your Balance for the seconds it runs.</p>
${!off ? `${panel({ title: 'Pick a model', id: 'model', body: models.length ? `<p class="ex-prose">The GPU is yours alone: only your own sign-in can call it, and it is never listed as an offer. The price is the cheapest GPU that fits, per hour; you pay your Balance for the seconds it runs.</p>
<form method="post" action="/own" class="ovk-form">
<form method="post" action="/own" class="ovk-form">
<label class="ex-label" for="own-model">Model</label>
<label class="ex-label" for="own-model">Model</label>
<select id="own-model" name="model">${modelOptions}</select>
<select id="own-model" name="model">${modelOptions}</select>
+<label for="own-strategy">Placement</label><select id="own-strategy" name="strategy"><option value="cheapest">Cheapest</option><option value="automatic">Automatic across marketplaces</option><option value="fastest">Fastest</option></select>
<label class="ex-label" for="own-idle">Stop after this many idle minutes</label>
<label class="ex-label" for="own-idle">Stop after this many idle minutes</label>
<input id="own-idle" name="idle_minutes" type="number" min="1" max="${Number(status.max_idle_minutes || 120)}" value="${Number(status.idle_minutes || 10)}">
<input id="own-idle" name="idle_minutes" type="number" min="1" max="${Number(status.max_idle_minutes || 120)}" value="${Number(status.idle_minutes || 10)}">
<p class="ex-small ex-muted">How long it stays up after your last request. Lower costs less; higher avoids a cold start.</p>
<p class="ex-small ex-muted">How long it stays up after your last request. Lower costs less; higher avoids a cold start.</p>
@@ -14,6 +14,7 @@ const { createFakeAi } = require('./helpers/ai');
@@ -14,6 +14,7 @@ const { createFakeAi } = require('./helpers/ai');
status: { enabled: false, reason: 'The owner has not added a GPU provider key (Vast.ai or RunPod) yet, so no endpoint can start.', models: [], idle_minutes: 10, max_idle_minutes: 120, public_base: 'https://ai.openvibe.services/api/v1/own-endpoints' },
status: { enabled: false, reason: 'The owner has not added a GPU provider key (Vast.ai or RunPod) yet, so no endpoint can start.', models: [], idle_minutes: 10, max_idle_minutes: 120, public_base: 'https://ai.openvibe.services/api/v1/own-endpoints' },
endpoints: [], calls: [],
endpoints: [], calls: [],
};
};
+ ai.gpuQuote = async (model, strategy) => { state.calls.push({ kind: 'quote', model, strategy }); return { dry_run: true, selected: { marketplace: 'vast', gpu_name: 'RTX 3090', region: 'US', price_per_hour: '0.20' }, reasons: ['objective cheapest'] }; };
ai.ownStatus = async () => state.status;
ai.ownStatus = async () => state.status;
ai.ownList = async (token) => { state.calls.push({ kind: 'list', token }); return { endpoints: state.endpoints, status: state.status }; };
ai.ownList = async (token) => { state.calls.push({ kind: 'list', token }); return { endpoints: state.endpoints, status: state.status }; };
ai.ownCreate = async (token, body) => { state.calls.push({ kind: 'create', token, body }); return { endpoint: { id: 'oep_TEST' } }; };
ai.ownCreate = async (token, body) => { state.calls.push({ kind: 'create', token, body }); return { endpoint: { id: 'oep_TEST' } }; };
@@ -38,6 +39,14 @@ const { createFakeAi } = require('./helpers/ai');
@@ -38,6 +39,14 @@ const { createFakeAi } = require('./helpers/ai');
assert.ok(!r.text.includes('Start my endpoint'), 'the submit is only for a signed-in person');
assert.ok(!r.text.includes('Start my endpoint'), 'the submit is only for a signed-in person');
});
});
+ await check('quote works signed out, shows live price, and creates no rental', async () => {
+ const r = await t.get('/own/quote?model=deepseek-r1-32b&strategy=cheapest');
+ assert.strictEqual(r.status, 200, r.text);
+ assert.ok(r.text.includes('0.20 per hour'));
+ assert.ok(r.text.includes('Nothing was rented'));
+ assert.ok(!state.calls.some(c => c.kind === 'create'));
+ });
+
const u = t.network.addUser('gpu-owner');
const u = t.network.addUser('gpu-owner');
await check('signed in: the model and own-weights forms, with the private promise', async () => {
await check('signed in: the model and own-weights forms, with the private promise', async () => {
const r = await t.get('/own', { as: u });
const r = await t.get('/own', { as: u });
@@ -50,13 +59,13 @@ const { createFakeAi } = require('./helpers/ai');
@@ -50,13 +59,13 @@ const { createFakeAi } = require('./helpers/ai');
});
});
await check('create posts to AI with the person\'s own token and the chosen model', async () => {
await check('create posts to AI with the person\'s own token and the chosen model', async () => {
- const r = await t.get('/own', { as: u, form: { model: 'deepseek-r1-32b', idle_minutes: '5' }, method: 'POST' });
+ const r = await t.get('/own', { as: u, form: { model: 'deepseek-r1-32b', idle_minutes: '5', strategy: 'automatic' }, method: 'POST' });
assert.strictEqual(r.status, 303, r.text);
assert.strictEqual(r.status, 303, r.text);
assert.strictEqual(r.headers.get('location'), '/own');
assert.strictEqual(r.headers.get('location'), '/own');
const call = state.calls.find((c) => c.kind === 'create');
const call = state.calls.find((c) => c.kind === 'create');
assert.ok(call, 'AI was asked to create it');
assert.ok(call, 'AI was asked to create it');
assert.ok(call.token, 'with the person\'s token, never the service token');
assert.ok(call.token, 'with the person\'s token, never the service token');
- assert.deepStrictEqual(call.body, { model: 'deepseek-r1-32b', idle_minutes: 5 });
+ assert.deepStrictEqual(call.body, { model: 'deepseek-r1-32b', idle_minutes: 5, strategy: 'automatic' });
});
});
await check('own weights are passed as a weights object', async () => {
await check('own weights are passed as a weights object', async () => {