openvibers/openvibe.exchange Commits

Proxy owner GPU policy changes (#218)

OP OpenVibers committed 2 days ago · f4ac084889 · 3 files, +49 −3

Changes

ddiff --git a/server/ai.js b/server/ai.js
iindex 26a25ec..487d5ff 100644
---- a/server/ai.js
++++ b/server/ai.js
@@@ -61,12 +61,12 @@ function createAi({ config, fetchImpl = globalThis.fetch, log = console, now = (
}
/** Owner console calls to AI's JSON admin API. No HTML is fetched or relayed. */
-- async function adminRequest(path, { method = 'GET', body = null, subject = null } = {}) {
++ async function adminRequest(path, { method = 'GET', body = null, subject = null, role = null } = {}) {
if (!adminTokens) throw Object.assign(new Error('AI console service credentials are not configured'), { status: 503 });
const token = await adminTokens.getToken();
const res = await fetchImpl(`${config.ai.url}${path}`, {
method,
-- headers: { Accept: 'application/json', Authorization: `Bearer ${token}`, ...(subject ? { 'X-OV-Subject': String(subject) } : {}), ...(body == null ? {} : { 'Content-Type': 'application/json' }) },
++ headers: { Accept: 'application/json', Authorization: `Bearer ${token}`, ...(subject ? { 'X-OV-Subject': String(subject) } : {}), ...(role ? { 'X-OV-Role': String(role) } : {}), ...(body == null ? {} : { 'Content-Type': 'application/json' }) },
...(body == null ? {} : { body: JSON.stringify(body) }),
signal: AbortSignal.timeout(config.ai.timeoutMs),
});
ddiff --git a/server/http/console.js b/server/http/console.js
iindex bed1138..33ee393 100644
---- a/server/http/console.js
++++ b/server/http/console.js
@@@ -8,6 +8,7 @@ const { defaultsPage } = require('../views/task-defaults');
const READS = Object.freeze({
supply: '/api/v1/admin/console/supply',
++ 'gpu-policy': '/api/v1/admin/console/gpu-policy',
overview: '/api/v1/admin/console/overview', providers: '/api/v1/admin/console/providers', models: '/api/v1/admin/console/models', routes: '/api/v1/routes',
templates: '/api/v1/templates', workflows: '/api/v1/workflows', runs: '/api/v1/admin/console/runs', quotas: '/api/v1/admin/console/quotas',
usage: '/api/v1/admin/console/usage', cache: '/api/v1/admin/console/cache', audit: '/api/v1/admin/console/audit',
@@@ -148,6 +149,33 @@ function createConsoleRoutes({ config, ai }) {
try { await ai.adminRequest('/api/v1/admin/console/supply', { method:'POST', subject:req.viewer.subject, body }); return res.redirect(303,'/console/supply'); }
catch (err) { return page(req,res,'supply',{},err.message || 'Supply settings could not be saved.',err.status === 409 || err.status === 422 ? err.status : 502); }
});
++ r.post('/console/gpu-policy/:scope', async (req, res) => {
++ if (!sameOrigin(req)) return res.sendStatus(403);
++ const scope = String(req.params.scope || '');
++ const input = req.body || {};
++ const body = {};
++ for (const key of ['enabled', 'interruptible_allowed', 'quote_only']) if (Object.hasOwn(input, key)) body[key] = input[key] === '1';
++ for (const key of ['order', 'max_replicas', 'monthly_cap_share_percent']) if (Object.hasOwn(input, key)) body[key] = Number(input[key]);
++ for (const key of ['monthly_cap_nano', 'interactive_cap_nano', 'batch_cap_nano', 'max_hourly_nano']) if (Object.hasOwn(input, key)) body[key] = input[key] || null;
++ if (Object.hasOwn(input, 'classes')) body.classes = String(input.classes).split(',').map(x => x.trim()).filter(Boolean);
++ if (Object.hasOwn(input, 'regions')) body.regions = String(input.regions).split(',').map(x => x.trim()).filter(Boolean);
++ try {
++ await ai.adminRequest(`/api/v1/admin/console/gpu-policy/${encodeURIComponent(scope)}`, {
++ method: 'POST', subject: req.viewer.subject, role: req.viewer.role, body,
++ });
++ return res.redirect(303, '/console/supply');
++ } catch (err) { return page(req, res, 'supply', {}, err.message || 'GPU supply policy could not be saved.', err.status === 422 ? 422 : 502); }
++ });
++ r.post('/console/gpu-policy/:scope/key', async (req, res) => {
++ if (!sameOrigin(req)) return res.sendStatus(403);
++ try {
++ await ai.adminRequest(`/api/v1/admin/console/gpu-policy/${encodeURIComponent(req.params.scope)}/key`, {
++ method: 'POST', subject: req.viewer.subject, role: req.viewer.role,
++ body: { api_key: String(req.body.api_key || '') },
++ });
++ return res.redirect(303, '/console/supply');
++ } catch (err) { return page(req, res, 'supply', {}, err.message || 'GPU provider key could not be saved.', err.status === 422 ? 422 : 502); }
++ });
r.post('/console/providers', async (req, res) => {
if (!sameOrigin(req)) return res.sendStatus(403);
try {
ddiff --git a/test/console-merge.test.js b/test/console-merge.test.js
iindex 012d554..da446e9 100644
---- a/test/console-merge.test.js
++++ b/test/console-merge.test.js
@@@ -45,10 +45,14 @@ app.use(createConsoleRoutes({
assert.ok((data.headers.get('content-type') || '').includes('application/json'));
assert.ok(!JSON.stringify(await data.json()).includes(fakeSecret), `${section} data omits key values`);
}
-- for (const role of ['user', 'admin']) {
++ for (const role of ['user', 'global_mod', 'admin']) {
const count = writes.length;
const deniedOrder = await fetch(`${base}/console/orders/chat`, { method: 'POST', headers: { 'x-test-role': role, Origin: 'https://openvibe.exchange' } });
assert.strictEqual(deniedOrder.status, 403);
++ for (const path of ['/console/gpu-policy/vast', '/console/gpu-policy/vast/key']) {
++ const denied = await fetch(`${base}${path}`, { method: 'POST', headers: { 'x-test-role': role, Origin: 'https://openvibe.exchange' } });
++ assert.strictEqual(denied.status, 403, `${role} cannot change GPU policy`);
++ }
assert.strictEqual(writes.length, count);
}
const orderCsrf = await fetch(`${base}/console/orders/chat`, { method: 'POST', headers: { 'x-test-role': 'owner', Origin: 'https://evil.example' } });
@@@ -161,6 +165,20 @@ app.use(createConsoleRoutes({
});
assert.strictEqual(supplyOff.status, 303);
assert.strictEqual(writes.at(-1).body.enabled, false);
++ const gpuPolicy = await fetch(`${base}/console/gpu-policy/vast`, {
++ method: 'POST', headers: { 'x-test-role': 'owner', Origin: 'https://openvibe.exchange', 'content-type': 'application/x-www-form-urlencoded' },
++ body: new URLSearchParams({ enabled: '1', classes: 'batch', max_hourly_nano: '500000000', order: '2' }), redirect: 'manual',
++ });
++ assert.strictEqual(gpuPolicy.status, 303);
++ assert.deepStrictEqual(writes.at(-1), { path: '/api/v1/admin/console/gpu-policy/vast', method: 'POST',
++ subject: 'usr_01JAB2C3D4E5F6G7H8J9K0MNPX', role: 'owner', body: { enabled: true, order: 2, max_hourly_nano: '500000000', classes: ['batch'] } });
++ const gpuKey = await fetch(`${base}/console/gpu-policy/vast/key`, {
++ method: 'POST', headers: { 'x-test-role': 'owner', Origin: 'https://openvibe.exchange', 'content-type': 'application/x-www-form-urlencoded' },
++ body: new URLSearchParams({ api_key: fakeSecret }), redirect: 'manual',
++ });
++ assert.strictEqual(gpuKey.status, 303);
++ assert.strictEqual(writes.at(-1).path, '/api/v1/admin/console/gpu-policy/vast/key');
++ assert.strictEqual(writes.at(-1).role, 'owner');
const modelOverride = await fetch(`${base}/console/models/groq/openai%2Fgpt-oss-120b/override`, {
method: 'POST', headers: { 'x-test-role': 'owner', Origin: 'https://openvibe.exchange', 'content-type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ input_per_million: '0.2', cached_per_million: '', output_per_million: '0.7', reason: 'Owner approved adjustment' }), redirect: 'manual',