openvibers/openvibe.exchange Commits
Proxy owner GPU policy changes (#218)
Changes
@@@ -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),});@@@ -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 {@@@ -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',
@@ -61,12 +61,12 @@ function createAi({ config, fetchImpl = globalThis.fetch, log = console, now = (
@@ -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. */
/** 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 });
if (!adminTokens) throw Object.assign(new Error('AI console service credentials are not configured'), { status: 503 });
const token = await adminTokens.getToken();
const token = await adminTokens.getToken();
const res = await fetchImpl(`${config.ai.url}${path}`, {
const res = await fetchImpl(`${config.ai.url}${path}`, {
method,
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) }),
...(body == null ? {} : { body: JSON.stringify(body) }),
signal: AbortSignal.timeout(config.ai.timeoutMs),
signal: AbortSignal.timeout(config.ai.timeoutMs),
});
});
@@ -8,6 +8,7 @@ const { defaultsPage } = require('../views/task-defaults');
@@ -8,6 +8,7 @@ const { defaultsPage } = require('../views/task-defaults');
const READS = Object.freeze({
const READS = Object.freeze({
supply: '/api/v1/admin/console/supply',
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',
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',
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',
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 }) {
@@ -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'); }
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); }
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) => {
r.post('/console/providers', async (req, res) => {
if (!sameOrigin(req)) return res.sendStatus(403);
if (!sameOrigin(req)) return res.sendStatus(403);
try {
try {
@@ -45,10 +45,14 @@ app.use(createConsoleRoutes({
@@ -45,10 +45,14 @@ app.use(createConsoleRoutes({
assert.ok((data.headers.get('content-type') || '').includes('application/json'));
assert.ok((data.headers.get('content-type') || '').includes('application/json'));
assert.ok(!JSON.stringify(await data.json()).includes(fakeSecret), `${section} data omits key values`);
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 count = writes.length;
const deniedOrder = await fetch(`${base}/console/orders/chat`, { method: 'POST', headers: { 'x-test-role': role, Origin: 'https://openvibe.exchange' } });
const deniedOrder = await fetch(`${base}/console/orders/chat`, { method: 'POST', headers: { 'x-test-role': role, Origin: 'https://openvibe.exchange' } });
assert.strictEqual(deniedOrder.status, 403);
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);
assert.strictEqual(writes.length, count);
}
}
const orderCsrf = await fetch(`${base}/console/orders/chat`, { method: 'POST', headers: { 'x-test-role': 'owner', Origin: 'https://evil.example' } });
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({
@@ -161,6 +165,20 @@ app.use(createConsoleRoutes({
});
});
assert.strictEqual(supplyOff.status, 303);
assert.strictEqual(supplyOff.status, 303);
assert.strictEqual(writes.at(-1).body.enabled, false);
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`, {
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' },
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',
body: new URLSearchParams({ input_per_million: '0.2', cached_per_million: '', output_per_million: '0.7', reason: 'Owner approved adjustment' }), redirect: 'manual',