openvibers/openvibe.bench Commits
Bench: hand benchmark runs to Codes (#26)
Changes
nnew file mode 100644@@@ -0,0 +1,6 @@++-- phase: expand++-- B3: verified human-judged runs publish their outputs before a score exists.++ALTER TABLE bench_runs ALTER COLUMN score DROP NOT NULL;++ALTER TABLE bench_runs ALTER COLUMN score DROP DEFAULT;++ALTER TABLE bench_run_tasks ALTER COLUMN score DROP NOT NULL;++ALTER TABLE bench_run_tasks ALTER COLUMN score DROP DEFAULT;@@@ -79,7 +79,7 @@ async function createApp(opts = {}) {: async () => { throw new Error('OV_OAUTH_CLIENT_SECRET is not set: Bench cannot answer account events'); });const ctx = {-- config, s, keys, sso, principal, bench, registry, ai, jobs, publisher, accountData, accountSend, log,++ config, s, keys, sso, principal, bench, registry, ai, jobs, publisher, accountData, accountSend, log, fetchImpl,random: opts.random || Math.random,};@@@ -15,7 +15,7 @@*/const crypto = require('crypto');const { ids } = require('openvibe-contracts');--const { contractRun, checkContract } = require('./validate');++ const { contractRun, checkContract } = require('./validate');const num = (v, d = 0) => (v == null || v === '' || Number.isNaN(Number(v)) ? d : Number(v));const iso = (v) => (v == null ? null : (v instanceof Date ? v.toISOString() : String(v)));@@@ -97,7 +97,7 @@ function wireRun(row, tasks = null) {settings: json(row.settings, {}),executor: json(row.executor, {}),hardware: json(row.hardware, null),-- score: num(row.score),++ score: row.score == null ? null : num(row.score),cost_usd: num(row.cost_usd),ms: num(row.ms),tokens: num(row.tokens),@@@ -132,11 +132,11 @@ function wireTask(row) {if (!row) return null;return {task_id: row.task_id,-- score: num(row.score),++ score: row.score == null ? null : num(row.score),cost_usd: num(row.cost_usd),ms: num(row.ms),tokens: num(row.tokens),-- artifacts: Array.isArray(json(row.artifacts, [])) ? json(row.artifacts, []) : [],++ artifacts: (Array.isArray(json(row.artifacts, [])) ? json(row.artifacts, []) : []).map((a) => ({ kind: a.kind, url: a.url })).filter((a) => a.url),transcript_url: row.transcript_url || null,};}@@@ -348,6 +348,15 @@ function createRegistry(s, { log = console } = {}) {* run, which is badged and kept out of the default rankings.*/async function publishRun(doc, { provenance, subject }) {++ if (doc.id) {++ const prior = await getRun(String(doc.id));++ if (prior) {++ if (prior.benchmark === String(doc.benchmark) && prior.model === String(doc.model) && prior.session === (doc.session || null)) {++ return { ok: true, run: prior };++ }++ return refusal(409, 'bench.run_id_conflict', 'That run id already names a different result.');++ }++ }const benchmarkId = String(doc.benchmark_id || (doc.benchmark || '').split('@')[0] || '');const version = num(doc.benchmark_version || (doc.benchmark || '').split('@')[1]);const versionRow = await getVersion(benchmarkId, version);@@@ -369,7 +378,7 @@ function createRegistry(s, { log = console } = {}) {// A submitted run has no executor: whatever the body says, the provenance decides.const executorKind = provenance === 'submitted' ? 'submitted' : String((doc.executor && doc.executor.kind) || 'codes');const executor = { kind: executorKind, id: String((doc.executor && doc.executor.id) || subject || 'unknown') };-- const id = s.newId('brn');++ const id = String(doc.id || s.newId('brn'));// The published contract has the last word: a run that is not a bench.run@1 document is refused.const contract = contractRun({...doc,@@@ -377,17 +386,17 @@ function createRegistry(s, { log = console } = {}) {benchmark: `${benchmarkId}@${version}`,executor,tasks,-- score: num(doc.score, 0),++ score: doc.score == null ? null : num(doc.score, 0),cost_usd: num(doc.cost_usd, 0),ms: num(doc.ms, 0),tokens: num(doc.tokens, 0),provenance,}, s.iso());-- const contractCheck = checkContract('bench.run@1', contract);++ const contractCheck = checkContract('bench.run@2', contract);if (!contractCheck.valid) {return refusal(400, 'bench.invalid', contractCheck.errors.map((e) => e.message).join('; '));}-- const score = num(doc.score, 0);++ const score = doc.score == null ? null : num(doc.score, 0);const cost = num(doc.cost_usd, 0);const ms = num(doc.ms, 0);const tokens = num(doc.tokens, 0);@@@ -405,7 +414,7 @@ function createRegistry(s, { log = console } = {}) {await db.exec(`INSERT INTO bench_run_tasks (id, run_id, task_id, score, cost_usd, ms, tokens, artifacts, transcript_url, created_at)VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10::timestamptz)`,-- [s.newId('brt'), id, String(t.task_id), num(t.score), num(t.cost_usd), num(t.ms), num(t.tokens),++ [s.newId('brt'), id, String(t.task_id), t.score == null ? null : num(t.score), num(t.cost_usd), num(t.ms), num(t.tokens),JSON.stringify(Array.isArray(t.artifacts) ? t.artifacts : []), t.transcript_url == null ? null : String(t.transcript_url), s.iso()],);}@@@ -483,6 +492,7 @@ function createRegistry(s, { log = console } = {}) {const medMs = median(times);const sessions = new Map();for (const r of list) {++ if (r.score == null) continue;const key = r.session || 'default';if (!sessions.has(key)) sessions.set(key, []);sessions.get(key).push(r.score);@@@ -524,7 +534,7 @@ function createRegistry(s, { log = console } = {}) {params.push(String(model));if (ref.version != null) { params.push(ref.version); sql += ' AND benchmark_version = $3'; }const rows = await db.many(sql, params);-- const scores = rows.map((r) => num(r.score));++ const scores = rows.filter((r) => r.score != null).map((r) => num(r.score));if (!scores.length) return { benchmark: ref.id, model: String(model), runs: 0, percentile: null, score: num(score) };const value = num(score);const below = scores.filter((v) => v < value).length;@@@ -90,14 +90,14 @@ function validateRun(doc) {doc.tasks.forEach((t, i) => {if (!isObj(t)) { e.add(`/tasks/${i}`, 'must be an object'); return; }if (!isStr(t.task_id) || !t.task_id.trim()) e.add(`/tasks/${i}/task_id`, 'is required');-- if (!isNum(t.score) || t.score < 0 || t.score > 100) e.add(`/tasks/${i}/score`, 'must be 0-100');++ if (t.score != null && (!isNum(t.score) || t.score < 0 || t.score > 100)) e.add(`/tasks/${i}/score`, 'must be null or 0-100');if (!isNum(t.cost_usd) || t.cost_usd < 0) e.add(`/tasks/${i}/cost_usd`, 'must be 0 or more');if (!isNum(t.ms) || t.ms < 0) e.add(`/tasks/${i}/ms`, 'must be 0 or more');if (!isNum(t.tokens) || t.tokens < 0) e.add(`/tasks/${i}/tokens`, 'must be 0 or more');if (!Array.isArray(t.artifacts)) e.add(`/tasks/${i}/artifacts`, 'must be an array');});}-- if (!isNum(doc.score) || doc.score < 0 || doc.score > 100) e.add('/score', 'must be 0-100');++ if (doc.score != null && (!isNum(doc.score) || doc.score < 0 || doc.score > 100)) e.add('/score', 'must be null or 0-100');if (!isNum(doc.cost_usd) || doc.cost_usd < 0) e.add('/cost_usd', 'must be 0 or more');if (!isNum(doc.ms) || doc.ms < 0) e.add('/ms', 'must be 0 or more');if (!isNum(doc.tokens) || doc.tokens < 0) e.add('/tokens', 'must be 0 or more');@@@ -118,7 +118,7 @@ function contractRun(doc, createdAt) {const tasks = (Array.isArray(doc.tasks) ? doc.tasks : []).map((t) => {const out = {task_id: String(t.task_id || ''),-- score: num(t.score),++ score: t.score == null ? null : num(t.score),cost_usd: num(t.cost_usd),ms: Math.round(num(t.ms)),tokens: Math.round(num(t.tokens)),@@@ -142,7 +142,7 @@ function contractRun(doc, createdAt) {},executor: { kind: String(executor.kind || ''), id: String(executor.id || '') },tasks,-- score: num(doc.score),++ score: doc.score == null ? null : num(doc.score),cost_usd: num(doc.cost_usd),ms: Math.round(num(doc.ms)),tokens: Math.round(num(doc.tokens)),@@@ -23,12 +23,18 @@ const { http } = require('openvibe-contracts');const { asyncRouter } = require('./router');const { sameOrigin } = require('./same-origin');const { validateBenchmark, validateRun } = require('../bench/validate');++const { createServiceTokenClient } = require('openvibe-sdk/auth');function createRegistryApi(ctx) {const { config, principal, registry } = ctx;++ const fetchImpl = ctx.fetchImpl || globalThis.fetch;++ const codesTokens = config.oauth.clientSecret ? createServiceTokenClient({++ tokenUrl: `${config.networkInternalUrl}/oauth/token`, clientId: config.oauth.clientId,++ clientSecret: config.oauth.clientSecret, audience: 'openvibe.codes', scope: 'codes.task.start', fetch: fetchImpl,++ }) : null;const r = asyncRouter();-- r.use(express.json({ limit: '256kb' }));++ r.use(express.json({ limit: '16mb' }));r.use((req, res, next) => { res.set('Cache-Control', 'no-store'); next(); });r.use(principal.middleware);@@@ -165,6 +171,37 @@ function createRegistryApi(ctx) {} catch (err) { return failed(res, err); }});++ // Hand execution to Codes, which owns the person's keys and the benchmark task lifecycle.++ r.post('/benchmarks/:id/run', actor('bench.run.publish'), async (req, res) => {++ const p = req.principal;++ if (p.kind !== 'user') return problem(res, 403, 'bench.person_required', 'Sign in as the person who will run this benchmark.');++ if (!config.codesRunUrl) return problem(res, 503, 'bench.runner_unavailable', 'Codes benchmark runs are not configured.');++ const b = req.body && typeof req.body === 'object' ? req.body : {};++ const models = Array.isArray(b.models) ? b.models : [];++ const harnesses = Array.isArray(b.harnesses) ? b.harnesses : [];++ const repeat = Number(b.repeat);++ if (!models.length || !harnesses.length || models.length > 20 || harnesses.length > 20 || !Number.isInteger(repeat) || repeat < 1 || repeat > 20) {++ return invalid(res, 'models and harnesses must be non-empty arrays (at most 20 each), and repeat must be 1–20');++ }++ const definition = await registry.getDefinition(String(req.params.id), { reveal: false });++ if (!definition) return problem(res, 404, 'bench.benchmark_not_found', 'No such benchmark.');++ const costEstimate = (definition.definition.tasks || []).length * models.length * harnesses.length * repeat++ * Number(definition.definition.limits && definition.definition.limits.usd || 0);++ if (costEstimate > 1 && b.confirm !== true) return res.status(409).json({ code: 'bench.confirm_cost', cost_estimate_usd: costEstimate, confirm_required: true });++ try {++ if (!codesTokens) return problem(res, 503, 'bench.runner_unavailable', 'Bench has no Network client credentials for Codes.');++ const codesRunApi = new URL(`/api/v1/benchmarks/${encodeURIComponent(String(req.params.id))}/run`, config.codesRunUrl).toString();++ const response = await fetchImpl(codesRunApi, {++ method: 'POST', headers: { ...(await codesTokens.authHeaders()), 'content-type': 'application/json', 'x-ov-subject': p.subject, 'x-ov-user-token': p.token },++ body: JSON.stringify({ benchmark: String(req.params.id), models, harnesses, repeat }),++ signal: AbortSignal.timeout(15000),++ });++ const body = await response.json().catch(() => ({}));++ if (!response.ok) return problem(res, response.status < 500 ? response.status : 502, body.code || 'bench.runner_failed', body.detail || body.message || 'Codes could not start this benchmark.');++ return res.status(202).json(body);++ } catch (err) { return failed(res, err); }++ });++// ── Runs ─────────────────────────────────────────────────r.post('/runs', actor('bench.run.publish'), async (req, res) => {const b = req.body && typeof req.body === 'object' ? req.body : {};@@@ -18,7 +18,24 @@ function createRenderRoutes(ctx) {const row = await ctx.s.db.maybe('SELECT e.id, e.payload, e.error, r.kind_id FROM bench_entries e JOIN bench_rounds r ON r.id = e.round_id WHERE e.id = $1',[String(id || '')]);-- if (!row) return null;++ if (!row) {++ const match = /^((?:brn_)[0-9A-HJKMNP-TV-Z]{26})~([A-Za-z0-9_-]{1,120})$/.exec(String(id || ''));++ if (!match) return null;++ const taskId = Buffer.from(match[2], 'base64url').toString('utf8');++ const runTask = await ctx.s.db.maybe(++ `SELECT t.artifacts, v.definition FROM bench_run_tasks t++ JOIN bench_runs r ON r.id = t.run_id++ JOIN bench_benchmark_versions v ON v.benchmark_id = r.benchmark_id AND v.version = r.benchmark_version++ WHERE t.run_id = $1 AND t.task_id = $2`, [match[1], taskId]);++ if (!runTask) return null;++ let artifacts = runTask.artifacts;++ let definition = runTask.definition;++ if (typeof artifacts === 'string') { try { artifacts = JSON.parse(artifacts); } catch { artifacts = []; } }++ if (typeof definition === 'string') { try { definition = JSON.parse(definition); } catch { definition = {}; } }++ const artifact = (Array.isArray(artifacts) ? artifacts : []).find((a) => a && a.payload);++ if (!artifact) return null;++ return { id: String(id), payload: artifact.payload, error: null, kind: kindById(definition && definition.kind) };++ }let payload = row.payload;if (typeof payload === 'string') { try { payload = JSON.parse(payload); } catch { payload = {}; } }return { id: row.id, payload: payload || {}, error: row.error || null, kind: kindById(row.kind_id) };@@@ -102,6 +102,24 @@ function runDoc(benchmark, over = {}) {assert.strictEqual(submitted.json().run.executor.kind, 'submitted');});++ await check('a human judged run publishes null scores and keeps its output on the framed render path', async () => {++ const id = t.store.newId('brn');++ const url = `https://bench-render.openvibe.services/render/w/${id}~dDE`;++ const pending = runDoc(`${benchmarkId}@${version}`, {++ id, score: null,++ tasks: [{ task_id: 't1', score: null, cost_usd: 0.01, ms: 1200, tokens: 20,++ artifacts: [{ kind: 'games.play', url, payload: { text: '<!doctype html><html><body><h1>Rendered benchmark output</h1></body></html>' } }] }],++ });++ const published = await t.get('/api/v1/runs', { bearer: publisher, json: pending });++ assert.strictEqual(published.status, 201, published.text);++ assert.strictEqual(published.json().run.score, null);++ assert.strictEqual(published.json().run.tasks[0].score, null);++ assert.strictEqual(published.json().run.tasks[0].artifacts[0].payload, undefined);++ const rendered = await t.get(`/render/e/${id}~dDE`);++ assert.strictEqual(rendered.status, 200, rendered.text);++ assert.match(rendered.text, /Rendered benchmark output/);++ });++await check('a run is read back with its tasks', async () => {const list = await t.get(`/api/v1/benchmarks/${benchmarkId}`);const runs = await t.get(`/api/v1/runs/${list.json().definition ? '' : ''}`);@@@ -158,6 +176,12 @@ function runDoc(benchmark, over = {}) {assert.strictEqual(anonRun.status, 401, anonRun.text);});++ await check('a signed-in person sees a clear unavailable response until Codes runs are configured', async () => {++ const response = await t.get(`/api/v1/benchmarks/${benchmarkId}/run`, { as: alice, json: { models: ['m'], harnesses: ['h'], repeat: 1 } });++ assert.strictEqual(response.status, 503, response.text);++ assert.strictEqual(response.json().code, 'bench.runner_unavailable');++ });++await check('the compare endpoint puts two runs side by side', async () => {const runs = await t.get(`/api/v1/compare?runs=nope`);assert.strictEqual(runs.status, 404, runs.text);
new file mode 100644
new file mode 100644
@@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
+-- phase: expand
+-- B3: verified human-judged runs publish their outputs before a score exists.
+ALTER TABLE bench_runs ALTER COLUMN score DROP NOT NULL;
+ALTER TABLE bench_runs ALTER COLUMN score DROP DEFAULT;
+ALTER TABLE bench_run_tasks ALTER COLUMN score DROP NOT NULL;
+ALTER TABLE bench_run_tasks ALTER COLUMN score DROP DEFAULT;
@@ -79,7 +79,7 @@ async function createApp(opts = {}) {
@@ -79,7 +79,7 @@ async function createApp(opts = {}) {
: async () => { throw new Error('OV_OAUTH_CLIENT_SECRET is not set: Bench cannot answer account events'); });
: async () => { throw new Error('OV_OAUTH_CLIENT_SECRET is not set: Bench cannot answer account events'); });
const ctx = {
const ctx = {
- config, s, keys, sso, principal, bench, registry, ai, jobs, publisher, accountData, accountSend, log,
+ config, s, keys, sso, principal, bench, registry, ai, jobs, publisher, accountData, accountSend, log, fetchImpl,
random: opts.random || Math.random,
random: opts.random || Math.random,
};
};
@@ -15,7 +15,7 @@
@@ -15,7 +15,7 @@
*/
*/
const crypto = require('crypto');
const crypto = require('crypto');
const { ids } = require('openvibe-contracts');
const { ids } = require('openvibe-contracts');
-const { contractRun, checkContract } = require('./validate');
+ const { contractRun, checkContract } = require('./validate');
const num = (v, d = 0) => (v == null || v === '' || Number.isNaN(Number(v)) ? d : Number(v));
const num = (v, d = 0) => (v == null || v === '' || Number.isNaN(Number(v)) ? d : Number(v));
const iso = (v) => (v == null ? null : (v instanceof Date ? v.toISOString() : String(v)));
const iso = (v) => (v == null ? null : (v instanceof Date ? v.toISOString() : String(v)));
@@ -97,7 +97,7 @@ function wireRun(row, tasks = null) {
@@ -97,7 +97,7 @@ function wireRun(row, tasks = null) {
settings: json(row.settings, {}),
settings: json(row.settings, {}),
executor: json(row.executor, {}),
executor: json(row.executor, {}),
hardware: json(row.hardware, null),
hardware: json(row.hardware, null),
- score: num(row.score),
+ score: row.score == null ? null : num(row.score),
cost_usd: num(row.cost_usd),
cost_usd: num(row.cost_usd),
ms: num(row.ms),
ms: num(row.ms),
tokens: num(row.tokens),
tokens: num(row.tokens),
@@ -132,11 +132,11 @@ function wireTask(row) {
@@ -132,11 +132,11 @@ function wireTask(row) {
if (!row) return null;
if (!row) return null;
return {
return {
task_id: row.task_id,
task_id: row.task_id,
- score: num(row.score),
+ score: row.score == null ? null : num(row.score),
cost_usd: num(row.cost_usd),
cost_usd: num(row.cost_usd),
ms: num(row.ms),
ms: num(row.ms),
tokens: num(row.tokens),
tokens: num(row.tokens),
- artifacts: Array.isArray(json(row.artifacts, [])) ? json(row.artifacts, []) : [],
+ artifacts: (Array.isArray(json(row.artifacts, [])) ? json(row.artifacts, []) : []).map((a) => ({ kind: a.kind, url: a.url })).filter((a) => a.url),
transcript_url: row.transcript_url || null,
transcript_url: row.transcript_url || null,
};
};
}
}
@@ -348,6 +348,15 @@ function createRegistry(s, { log = console } = {}) {
@@ -348,6 +348,15 @@ function createRegistry(s, { log = console } = {}) {
* run, which is badged and kept out of the default rankings.
* run, which is badged and kept out of the default rankings.
*/
*/
async function publishRun(doc, { provenance, subject }) {
async function publishRun(doc, { provenance, subject }) {
+ if (doc.id) {
+ const prior = await getRun(String(doc.id));
+ if (prior) {
+ if (prior.benchmark === String(doc.benchmark) && prior.model === String(doc.model) && prior.session === (doc.session || null)) {
+ return { ok: true, run: prior };
+ }
+ return refusal(409, 'bench.run_id_conflict', 'That run id already names a different result.');
+ }
+ }
const benchmarkId = String(doc.benchmark_id || (doc.benchmark || '').split('@')[0] || '');
const benchmarkId = String(doc.benchmark_id || (doc.benchmark || '').split('@')[0] || '');
const version = num(doc.benchmark_version || (doc.benchmark || '').split('@')[1]);
const version = num(doc.benchmark_version || (doc.benchmark || '').split('@')[1]);
const versionRow = await getVersion(benchmarkId, version);
const versionRow = await getVersion(benchmarkId, version);
@@ -369,7 +378,7 @@ function createRegistry(s, { log = console } = {}) {
@@ -369,7 +378,7 @@ function createRegistry(s, { log = console } = {}) {
// A submitted run has no executor: whatever the body says, the provenance decides.
// A submitted run has no executor: whatever the body says, the provenance decides.
const executorKind = provenance === 'submitted' ? 'submitted' : String((doc.executor && doc.executor.kind) || 'codes');
const executorKind = provenance === 'submitted' ? 'submitted' : String((doc.executor && doc.executor.kind) || 'codes');
const executor = { kind: executorKind, id: String((doc.executor && doc.executor.id) || subject || 'unknown') };
const executor = { kind: executorKind, id: String((doc.executor && doc.executor.id) || subject || 'unknown') };
- const id = s.newId('brn');
+ const id = String(doc.id || s.newId('brn'));
// The published contract has the last word: a run that is not a bench.run@1 document is refused.
// The published contract has the last word: a run that is not a bench.run@1 document is refused.
const contract = contractRun({
const contract = contractRun({
...doc,
...doc,
@@ -377,17 +386,17 @@ function createRegistry(s, { log = console } = {}) {
@@ -377,17 +386,17 @@ function createRegistry(s, { log = console } = {}) {
benchmark: `${benchmarkId}@${version}`,
benchmark: `${benchmarkId}@${version}`,
executor,
executor,
tasks,
tasks,
- score: num(doc.score, 0),
+ score: doc.score == null ? null : num(doc.score, 0),
cost_usd: num(doc.cost_usd, 0),
cost_usd: num(doc.cost_usd, 0),
ms: num(doc.ms, 0),
ms: num(doc.ms, 0),
tokens: num(doc.tokens, 0),
tokens: num(doc.tokens, 0),
provenance,
provenance,
}, s.iso());
}, s.iso());
- const contractCheck = checkContract('bench.run@1', contract);
+ const contractCheck = checkContract('bench.run@2', contract);
if (!contractCheck.valid) {
if (!contractCheck.valid) {
return refusal(400, 'bench.invalid', contractCheck.errors.map((e) => e.message).join('; '));
return refusal(400, 'bench.invalid', contractCheck.errors.map((e) => e.message).join('; '));
}
}
- const score = num(doc.score, 0);
+ const score = doc.score == null ? null : num(doc.score, 0);
const cost = num(doc.cost_usd, 0);
const cost = num(doc.cost_usd, 0);
const ms = num(doc.ms, 0);
const ms = num(doc.ms, 0);
const tokens = num(doc.tokens, 0);
const tokens = num(doc.tokens, 0);
@@ -405,7 +414,7 @@ function createRegistry(s, { log = console } = {}) {
@@ -405,7 +414,7 @@ function createRegistry(s, { log = console } = {}) {
await db.exec(
await db.exec(
`INSERT INTO bench_run_tasks (id, run_id, task_id, score, cost_usd, ms, tokens, artifacts, transcript_url, created_at)
`INSERT INTO bench_run_tasks (id, run_id, task_id, score, cost_usd, ms, tokens, artifacts, transcript_url, created_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10::timestamptz)`,
VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10::timestamptz)`,
- [s.newId('brt'), id, String(t.task_id), num(t.score), num(t.cost_usd), num(t.ms), num(t.tokens),
+ [s.newId('brt'), id, String(t.task_id), t.score == null ? null : num(t.score), num(t.cost_usd), num(t.ms), num(t.tokens),
JSON.stringify(Array.isArray(t.artifacts) ? t.artifacts : []), t.transcript_url == null ? null : String(t.transcript_url), s.iso()],
JSON.stringify(Array.isArray(t.artifacts) ? t.artifacts : []), t.transcript_url == null ? null : String(t.transcript_url), s.iso()],
);
);
}
}
@@ -483,6 +492,7 @@ function createRegistry(s, { log = console } = {}) {
@@ -483,6 +492,7 @@ function createRegistry(s, { log = console } = {}) {
const medMs = median(times);
const medMs = median(times);
const sessions = new Map();
const sessions = new Map();
for (const r of list) {
for (const r of list) {
+ if (r.score == null) continue;
const key = r.session || 'default';
const key = r.session || 'default';
if (!sessions.has(key)) sessions.set(key, []);
if (!sessions.has(key)) sessions.set(key, []);
sessions.get(key).push(r.score);
sessions.get(key).push(r.score);
@@ -524,7 +534,7 @@ function createRegistry(s, { log = console } = {}) {
@@ -524,7 +534,7 @@ function createRegistry(s, { log = console } = {}) {
params.push(String(model));
params.push(String(model));
if (ref.version != null) { params.push(ref.version); sql += ' AND benchmark_version = $3'; }
if (ref.version != null) { params.push(ref.version); sql += ' AND benchmark_version = $3'; }
const rows = await db.many(sql, params);
const rows = await db.many(sql, params);
- const scores = rows.map((r) => num(r.score));
+ const scores = rows.filter((r) => r.score != null).map((r) => num(r.score));
if (!scores.length) return { benchmark: ref.id, model: String(model), runs: 0, percentile: null, score: num(score) };
if (!scores.length) return { benchmark: ref.id, model: String(model), runs: 0, percentile: null, score: num(score) };
const value = num(score);
const value = num(score);
const below = scores.filter((v) => v < value).length;
const below = scores.filter((v) => v < value).length;
@@ -90,14 +90,14 @@ function validateRun(doc) {
@@ -90,14 +90,14 @@ function validateRun(doc) {
doc.tasks.forEach((t, i) => {
doc.tasks.forEach((t, i) => {
if (!isObj(t)) { e.add(`/tasks/${i}`, 'must be an object'); return; }
if (!isObj(t)) { e.add(`/tasks/${i}`, 'must be an object'); return; }
if (!isStr(t.task_id) || !t.task_id.trim()) e.add(`/tasks/${i}/task_id`, 'is required');
if (!isStr(t.task_id) || !t.task_id.trim()) e.add(`/tasks/${i}/task_id`, 'is required');
- if (!isNum(t.score) || t.score < 0 || t.score > 100) e.add(`/tasks/${i}/score`, 'must be 0-100');
+ if (t.score != null && (!isNum(t.score) || t.score < 0 || t.score > 100)) e.add(`/tasks/${i}/score`, 'must be null or 0-100');
if (!isNum(t.cost_usd) || t.cost_usd < 0) e.add(`/tasks/${i}/cost_usd`, 'must be 0 or more');
if (!isNum(t.cost_usd) || t.cost_usd < 0) e.add(`/tasks/${i}/cost_usd`, 'must be 0 or more');
if (!isNum(t.ms) || t.ms < 0) e.add(`/tasks/${i}/ms`, 'must be 0 or more');
if (!isNum(t.ms) || t.ms < 0) e.add(`/tasks/${i}/ms`, 'must be 0 or more');
if (!isNum(t.tokens) || t.tokens < 0) e.add(`/tasks/${i}/tokens`, 'must be 0 or more');
if (!isNum(t.tokens) || t.tokens < 0) e.add(`/tasks/${i}/tokens`, 'must be 0 or more');
if (!Array.isArray(t.artifacts)) e.add(`/tasks/${i}/artifacts`, 'must be an array');
if (!Array.isArray(t.artifacts)) e.add(`/tasks/${i}/artifacts`, 'must be an array');
});
});
}
}
- if (!isNum(doc.score) || doc.score < 0 || doc.score > 100) e.add('/score', 'must be 0-100');
+ if (doc.score != null && (!isNum(doc.score) || doc.score < 0 || doc.score > 100)) e.add('/score', 'must be null or 0-100');
if (!isNum(doc.cost_usd) || doc.cost_usd < 0) e.add('/cost_usd', 'must be 0 or more');
if (!isNum(doc.cost_usd) || doc.cost_usd < 0) e.add('/cost_usd', 'must be 0 or more');
if (!isNum(doc.ms) || doc.ms < 0) e.add('/ms', 'must be 0 or more');
if (!isNum(doc.ms) || doc.ms < 0) e.add('/ms', 'must be 0 or more');
if (!isNum(doc.tokens) || doc.tokens < 0) e.add('/tokens', 'must be 0 or more');
if (!isNum(doc.tokens) || doc.tokens < 0) e.add('/tokens', 'must be 0 or more');
@@ -118,7 +118,7 @@ function contractRun(doc, createdAt) {
@@ -118,7 +118,7 @@ function contractRun(doc, createdAt) {
const tasks = (Array.isArray(doc.tasks) ? doc.tasks : []).map((t) => {
const tasks = (Array.isArray(doc.tasks) ? doc.tasks : []).map((t) => {
const out = {
const out = {
task_id: String(t.task_id || ''),
task_id: String(t.task_id || ''),
- score: num(t.score),
+ score: t.score == null ? null : num(t.score),
cost_usd: num(t.cost_usd),
cost_usd: num(t.cost_usd),
ms: Math.round(num(t.ms)),
ms: Math.round(num(t.ms)),
tokens: Math.round(num(t.tokens)),
tokens: Math.round(num(t.tokens)),
@@ -142,7 +142,7 @@ function contractRun(doc, createdAt) {
@@ -142,7 +142,7 @@ function contractRun(doc, createdAt) {
},
},
executor: { kind: String(executor.kind || ''), id: String(executor.id || '') },
executor: { kind: String(executor.kind || ''), id: String(executor.id || '') },
tasks,
tasks,
- score: num(doc.score),
+ score: doc.score == null ? null : num(doc.score),
cost_usd: num(doc.cost_usd),
cost_usd: num(doc.cost_usd),
ms: Math.round(num(doc.ms)),
ms: Math.round(num(doc.ms)),
tokens: Math.round(num(doc.tokens)),
tokens: Math.round(num(doc.tokens)),
@@ -23,12 +23,18 @@ const { http } = require('openvibe-contracts');
@@ -23,12 +23,18 @@ const { http } = require('openvibe-contracts');
const { asyncRouter } = require('./router');
const { asyncRouter } = require('./router');
const { sameOrigin } = require('./same-origin');
const { sameOrigin } = require('./same-origin');
const { validateBenchmark, validateRun } = require('../bench/validate');
const { validateBenchmark, validateRun } = require('../bench/validate');
+const { createServiceTokenClient } = require('openvibe-sdk/auth');
function createRegistryApi(ctx) {
function createRegistryApi(ctx) {
const { config, principal, registry } = ctx;
const { config, principal, registry } = ctx;
+ const fetchImpl = ctx.fetchImpl || globalThis.fetch;
+ const codesTokens = config.oauth.clientSecret ? createServiceTokenClient({
+ tokenUrl: `${config.networkInternalUrl}/oauth/token`, clientId: config.oauth.clientId,
+ clientSecret: config.oauth.clientSecret, audience: 'openvibe.codes', scope: 'codes.task.start', fetch: fetchImpl,
+ }) : null;
const r = asyncRouter();
const r = asyncRouter();
- r.use(express.json({ limit: '256kb' }));
+ r.use(express.json({ limit: '16mb' }));
r.use((req, res, next) => { res.set('Cache-Control', 'no-store'); next(); });
r.use((req, res, next) => { res.set('Cache-Control', 'no-store'); next(); });
r.use(principal.middleware);
r.use(principal.middleware);
@@ -165,6 +171,37 @@ function createRegistryApi(ctx) {
@@ -165,6 +171,37 @@ function createRegistryApi(ctx) {
} catch (err) { return failed(res, err); }
} catch (err) { return failed(res, err); }
});
});
+ // Hand execution to Codes, which owns the person's keys and the benchmark task lifecycle.
+ r.post('/benchmarks/:id/run', actor('bench.run.publish'), async (req, res) => {
+ const p = req.principal;
+ if (p.kind !== 'user') return problem(res, 403, 'bench.person_required', 'Sign in as the person who will run this benchmark.');
+ if (!config.codesRunUrl) return problem(res, 503, 'bench.runner_unavailable', 'Codes benchmark runs are not configured.');
+ const b = req.body && typeof req.body === 'object' ? req.body : {};
+ const models = Array.isArray(b.models) ? b.models : [];
+ const harnesses = Array.isArray(b.harnesses) ? b.harnesses : [];
+ const repeat = Number(b.repeat);
+ if (!models.length || !harnesses.length || models.length > 20 || harnesses.length > 20 || !Number.isInteger(repeat) || repeat < 1 || repeat > 20) {
+ return invalid(res, 'models and harnesses must be non-empty arrays (at most 20 each), and repeat must be 1–20');
+ }
+ const definition = await registry.getDefinition(String(req.params.id), { reveal: false });
+ if (!definition) return problem(res, 404, 'bench.benchmark_not_found', 'No such benchmark.');
+ const costEstimate = (definition.definition.tasks || []).length * models.length * harnesses.length * repeat
+ * Number(definition.definition.limits && definition.definition.limits.usd || 0);
+ if (costEstimate > 1 && b.confirm !== true) return res.status(409).json({ code: 'bench.confirm_cost', cost_estimate_usd: costEstimate, confirm_required: true });
+ try {
+ if (!codesTokens) return problem(res, 503, 'bench.runner_unavailable', 'Bench has no Network client credentials for Codes.');
+ const codesRunApi = new URL(`/api/v1/benchmarks/${encodeURIComponent(String(req.params.id))}/run`, config.codesRunUrl).toString();
+ const response = await fetchImpl(codesRunApi, {
+ method: 'POST', headers: { ...(await codesTokens.authHeaders()), 'content-type': 'application/json', 'x-ov-subject': p.subject, 'x-ov-user-token': p.token },
+ body: JSON.stringify({ benchmark: String(req.params.id), models, harnesses, repeat }),
+ signal: AbortSignal.timeout(15000),
+ });
+ const body = await response.json().catch(() => ({}));
+ if (!response.ok) return problem(res, response.status < 500 ? response.status : 502, body.code || 'bench.runner_failed', body.detail || body.message || 'Codes could not start this benchmark.');
+ return res.status(202).json(body);
+ } catch (err) { return failed(res, err); }
+ });
+
// ── Runs ─────────────────────────────────────────────────
// ── Runs ─────────────────────────────────────────────────
r.post('/runs', actor('bench.run.publish'), async (req, res) => {
r.post('/runs', actor('bench.run.publish'), async (req, res) => {
const b = req.body && typeof req.body === 'object' ? req.body : {};
const b = req.body && typeof req.body === 'object' ? req.body : {};
@@ -18,7 +18,24 @@ function createRenderRoutes(ctx) {
@@ -18,7 +18,24 @@ function createRenderRoutes(ctx) {
const row = await ctx.s.db.maybe(
const row = await ctx.s.db.maybe(
'SELECT e.id, e.payload, e.error, r.kind_id FROM bench_entries e JOIN bench_rounds r ON r.id = e.round_id WHERE e.id = $1',
'SELECT e.id, e.payload, e.error, r.kind_id FROM bench_entries e JOIN bench_rounds r ON r.id = e.round_id WHERE e.id = $1',
[String(id || '')]);
[String(id || '')]);
- if (!row) return null;
+ if (!row) {
+ const match = /^((?:brn_)[0-9A-HJKMNP-TV-Z]{26})~([A-Za-z0-9_-]{1,120})$/.exec(String(id || ''));
+ if (!match) return null;
+ const taskId = Buffer.from(match[2], 'base64url').toString('utf8');
+ const runTask = await ctx.s.db.maybe(
+ `SELECT t.artifacts, v.definition FROM bench_run_tasks t
+ JOIN bench_runs r ON r.id = t.run_id
+ JOIN bench_benchmark_versions v ON v.benchmark_id = r.benchmark_id AND v.version = r.benchmark_version
+ WHERE t.run_id = $1 AND t.task_id = $2`, [match[1], taskId]);
+ if (!runTask) return null;
+ let artifacts = runTask.artifacts;
+ let definition = runTask.definition;
+ if (typeof artifacts === 'string') { try { artifacts = JSON.parse(artifacts); } catch { artifacts = []; } }
+ if (typeof definition === 'string') { try { definition = JSON.parse(definition); } catch { definition = {}; } }
+ const artifact = (Array.isArray(artifacts) ? artifacts : []).find((a) => a && a.payload);
+ if (!artifact) return null;
+ return { id: String(id), payload: artifact.payload, error: null, kind: kindById(definition && definition.kind) };
+ }
let payload = row.payload;
let payload = row.payload;
if (typeof payload === 'string') { try { payload = JSON.parse(payload); } catch { payload = {}; } }
if (typeof payload === 'string') { try { payload = JSON.parse(payload); } catch { payload = {}; } }
return { id: row.id, payload: payload || {}, error: row.error || null, kind: kindById(row.kind_id) };
return { id: row.id, payload: payload || {}, error: row.error || null, kind: kindById(row.kind_id) };
@@ -102,6 +102,24 @@ function runDoc(benchmark, over = {}) {
@@ -102,6 +102,24 @@ function runDoc(benchmark, over = {}) {
assert.strictEqual(submitted.json().run.executor.kind, 'submitted');
assert.strictEqual(submitted.json().run.executor.kind, 'submitted');
});
});
+ await check('a human judged run publishes null scores and keeps its output on the framed render path', async () => {
+ const id = t.store.newId('brn');
+ const url = `https://bench-render.openvibe.services/render/w/${id}~dDE`;
+ const pending = runDoc(`${benchmarkId}@${version}`, {
+ id, score: null,
+ tasks: [{ task_id: 't1', score: null, cost_usd: 0.01, ms: 1200, tokens: 20,
+ artifacts: [{ kind: 'games.play', url, payload: { text: '<!doctype html><html><body><h1>Rendered benchmark output</h1></body></html>' } }] }],
+ });
+ const published = await t.get('/api/v1/runs', { bearer: publisher, json: pending });
+ assert.strictEqual(published.status, 201, published.text);
+ assert.strictEqual(published.json().run.score, null);
+ assert.strictEqual(published.json().run.tasks[0].score, null);
+ assert.strictEqual(published.json().run.tasks[0].artifacts[0].payload, undefined);
+ const rendered = await t.get(`/render/e/${id}~dDE`);
+ assert.strictEqual(rendered.status, 200, rendered.text);
+ assert.match(rendered.text, /Rendered benchmark output/);
+ });
+
await check('a run is read back with its tasks', async () => {
await check('a run is read back with its tasks', async () => {
const list = await t.get(`/api/v1/benchmarks/${benchmarkId}`);
const list = await t.get(`/api/v1/benchmarks/${benchmarkId}`);
const runs = await t.get(`/api/v1/runs/${list.json().definition ? '' : ''}`);
const runs = await t.get(`/api/v1/runs/${list.json().definition ? '' : ''}`);
@@ -158,6 +176,12 @@ function runDoc(benchmark, over = {}) {
@@ -158,6 +176,12 @@ function runDoc(benchmark, over = {}) {
assert.strictEqual(anonRun.status, 401, anonRun.text);
assert.strictEqual(anonRun.status, 401, anonRun.text);
});
});
+ await check('a signed-in person sees a clear unavailable response until Codes runs are configured', async () => {
+ const response = await t.get(`/api/v1/benchmarks/${benchmarkId}/run`, { as: alice, json: { models: ['m'], harnesses: ['h'], repeat: 1 } });
+ assert.strictEqual(response.status, 503, response.text);
+ assert.strictEqual(response.json().code, 'bench.runner_unavailable');
+ });
+
await check('the compare endpoint puts two runs side by side', async () => {
await check('the compare endpoint puts two runs side by side', async () => {
const runs = await t.get(`/api/v1/compare?runs=nope`);
const runs = await t.get(`/api/v1/compare?runs=nope`);
assert.strictEqual(runs.status, 404, runs.text);
assert.strictEqual(runs.status, 404, runs.text);