openvibers/openvibe.report Commits
AI boards: a model entered but never judged is no leader (#82)
Changes
@@@ -498,6 +498,15 @@ const JUDGE_SHORT = { human_pairwise: 'blind votes', rubric: 'a rubric', automatconst ROOM_HOSTS = { chat: 'openvibe.chat', games: 'openvibe.games', search: 'search.openvibe.network', codes: 'openvibe.codes' };const roomHost = (room) => { const r = rooms.get(room); return (r && r.host) || ROOM_HOSTS[room] || null; };const rowsOf = (read) => (read && read.data && read.data.board && Array.isArray(read.data.board.rows) ? read.data.board.rows : []);++/**++ * Whether a board has results: on a ranking (rows carry games), a game has been played (a model entered but never++ * judged has 0); the quality and cost views carry no games, and any row counts.++ */++const played = (rows) => {++ const list = rows || [];++ if (!list.some((r) => r && r.games != null)) return list.length > 0;++ return list.some((r) => (n(r && r.games) || 0) > 0);++};const howRated = (kind) => `Rated by ${JUDGE_SHORT[kind.judge] || 'Bench'}`;const METHOD = 'Ratings are Bradley–Terry Elo from Bench, with a 95% interval; a model with fewer than ten games is provisional.';@@@ -511,8 +520,10 @@ function emptyFor(kind) {function aiIndex({ kinds, boards = [] }) {const list = kinds && kinds.data && Array.isArray(kinds.data.kinds) ? kinds.data.kinds : [];const items = list.map((kind, i) => ({ kind, rows: rowsOf(boards[i]), available: Boolean(boards[i]) }));-- const rated = items.filter((x) => x.rows.length).sort((a, b) => gamesOf(b.rows) - gamesOf(a.rows));-- const waiting = items.filter((x) => !x.rows.length);++ // A board is rated once a game is played. Bench lists a model as soon as it has an entry, at the starting 1000++ // with 0 games: that is no leader (11-03, every board on /ai named gpt-5-nano "leader" with 0 games).++ const rated = items.filter((x) => played(x.rows)).sort((a, b) => gamesOf(b.rows) - gamesOf(a.rows));++ const waiting = items.filter((x) => !played(x.rows));const leaders = rated.length ? kit.table({caption: 'Leaders', columns: [{ key: 'job', label: 'Job', primary: true, html: true }, { key: 'leader', label: 'Leader', html: true },@@@ -559,7 +570,7 @@ function aiBoard({ kindId, kind, tab, board, cost, kinds }) {const rows = rowsOf(board);let content;if (!board) content = kit.empty({ icon: 'trophy', title: 'Bench is unavailable', text: 'This board comes back as soon as Bench answers.' });-- else if (!rows.length) content = emptyFor(kind);++ else if (!played(rows)) content = emptyFor(kind);else if (tab === 'pareto') {const points = paretoRows(board, cost);content = paretoChart(points) + (points.length ? kit.table({ caption: 'The numbers', columns: [{ key: 'm', label: 'Model', primary: true, html: true }, { key: 'q', label: 'Quality', num: true, html: true }, { key: 'c', label: 'Cost per solved', num: true, html: true }],@@@ -579,7 +590,7 @@ function aiBoard({ kindId, kind, tab, board, cost, kinds }) {}) }) + `<p class="rp-fine">${esc(METHOD)}</p>`;}const head = kit.head({ title: name, sub: kind ? `${roomName(kind.room)} · ${howRated(kind)}` : 'Bench results', back: { href: '/ai', label: 'AI boards' },-- tabs: rows.length ? kit.tabs({ base: aiPath(kindId), items: TABS, active: tab, label: 'Board view' }) : '' });++ tabs: played(rows) ? kit.tabs({ base: aiPath(kindId), items: TABS, active: tab, label: 'Board view' }) : '' });const body = head + benchState(kinds, board, cost) + `<section aria-label="${esc(TABS.find((t) => t.key === tab)?.label || 'Board')}">${content}</section>${datasetLink(kindId)}`;return layout({ title: `${name}: AI board`, description: `${name}: which AI does it best, and what it costs.`, body, active: '/ai', path: aiPath(kindId), agent: true });}@@@ -117,6 +117,26 @@ const { boot, check, done, stubFetch, jsonResponse } = require('./helpers/boot')} finally { await fresh.close(); }});++ await check('a model entered but never judged is no leader: 0 games reads as no votes yet', async () => {++ const unplayed = await boot({++ fetchImpl: async (url, opts) => {++ const u = new URL(String(url));++ if (u.port !== '5230') return source(url, opts);++ if (u.pathname === '/api/v1/kinds') return jsonResponse({ count: 1, kinds: [{ id: 'design.page', room: 'design', name: 'Page', judge: 'human_pairwise' }] });++ if (u.pathname.startsWith('/api/v1/boards/')) return jsonResponse({ board: { rows: [{ subject: 'gpt-5-nano-2025-08-07', elo: 1000, games: 0, provisional: true }] } });++ return jsonResponse({}, 404);++ },++ env: { OV_BENCH_INTERNAL_URL: 'http://127.0.0.1:5230' },++ });++ try {++ const r = await unplayed.get('/ai');++ assert.ok(r.text.includes('No model is rated yet') && !r.text.includes('<caption>Leaders</caption>'), 'no leaders table');++ assert.ok(!r.text.includes('gpt-5-nano-2025-08-07'), 'the unjudged model is named nowhere as a leader');++ const b = await unplayed.get('/ai/design.page');++ assert.ok(b.text.includes('No votes yet') && b.text.includes('Vote on Design') && !b.text.includes('class="ovk-tabs'), 'the board asks for votes');++ } finally { await unplayed.close(); }++ });++await check('an initial Bench outage still renders an AI page', async () => {const empty = await boot({ fetchImpl: async (url, opts) => String(url).includes(':5230') ? Promise.reject(new Error('offline')) : source(url, opts), env: { OV_BENCH_INTERNAL_URL: 'http://127.0.0.1:5230' } });try {
@@ -498,6 +498,15 @@ const JUDGE_SHORT = { human_pairwise: 'blind votes', rubric: 'a rubric', automat
@@ -498,6 +498,15 @@ const JUDGE_SHORT = { human_pairwise: 'blind votes', rubric: 'a rubric', automat
const ROOM_HOSTS = { chat: 'openvibe.chat', games: 'openvibe.games', search: 'search.openvibe.network', codes: 'openvibe.codes' };
const ROOM_HOSTS = { chat: 'openvibe.chat', games: 'openvibe.games', search: 'search.openvibe.network', codes: 'openvibe.codes' };
const roomHost = (room) => { const r = rooms.get(room); return (r && r.host) || ROOM_HOSTS[room] || null; };
const roomHost = (room) => { const r = rooms.get(room); return (r && r.host) || ROOM_HOSTS[room] || null; };
const rowsOf = (read) => (read && read.data && read.data.board && Array.isArray(read.data.board.rows) ? read.data.board.rows : []);
const rowsOf = (read) => (read && read.data && read.data.board && Array.isArray(read.data.board.rows) ? read.data.board.rows : []);
+/**
+ * Whether a board has results: on a ranking (rows carry games), a game has been played (a model entered but never
+ * judged has 0); the quality and cost views carry no games, and any row counts.
+ */
+const played = (rows) => {
+ const list = rows || [];
+ if (!list.some((r) => r && r.games != null)) return list.length > 0;
+ return list.some((r) => (n(r && r.games) || 0) > 0);
+};
const howRated = (kind) => `Rated by ${JUDGE_SHORT[kind.judge] || 'Bench'}`;
const howRated = (kind) => `Rated by ${JUDGE_SHORT[kind.judge] || 'Bench'}`;
const METHOD = 'Ratings are Bradley–Terry Elo from Bench, with a 95% interval; a model with fewer than ten games is provisional.';
const METHOD = 'Ratings are Bradley–Terry Elo from Bench, with a 95% interval; a model with fewer than ten games is provisional.';
@@ -511,8 +520,10 @@ function emptyFor(kind) {
@@ -511,8 +520,10 @@ function emptyFor(kind) {
function aiIndex({ kinds, boards = [] }) {
function aiIndex({ kinds, boards = [] }) {
const list = kinds && kinds.data && Array.isArray(kinds.data.kinds) ? kinds.data.kinds : [];
const list = kinds && kinds.data && Array.isArray(kinds.data.kinds) ? kinds.data.kinds : [];
const items = list.map((kind, i) => ({ kind, rows: rowsOf(boards[i]), available: Boolean(boards[i]) }));
const items = list.map((kind, i) => ({ kind, rows: rowsOf(boards[i]), available: Boolean(boards[i]) }));
- const rated = items.filter((x) => x.rows.length).sort((a, b) => gamesOf(b.rows) - gamesOf(a.rows));
- const waiting = items.filter((x) => !x.rows.length);
+ // A board is rated once a game is played. Bench lists a model as soon as it has an entry, at the starting 1000
+ // with 0 games: that is no leader (11-03, every board on /ai named gpt-5-nano "leader" with 0 games).
+ const rated = items.filter((x) => played(x.rows)).sort((a, b) => gamesOf(b.rows) - gamesOf(a.rows));
+ const waiting = items.filter((x) => !played(x.rows));
const leaders = rated.length ? kit.table({
const leaders = rated.length ? kit.table({
caption: 'Leaders', columns: [
caption: 'Leaders', columns: [
{ key: 'job', label: 'Job', primary: true, html: true }, { key: 'leader', label: 'Leader', html: true },
{ key: 'job', label: 'Job', primary: true, html: true }, { key: 'leader', label: 'Leader', html: true },
@@ -559,7 +570,7 @@ function aiBoard({ kindId, kind, tab, board, cost, kinds }) {
@@ -559,7 +570,7 @@ function aiBoard({ kindId, kind, tab, board, cost, kinds }) {
const rows = rowsOf(board);
const rows = rowsOf(board);
let content;
let content;
if (!board) content = kit.empty({ icon: 'trophy', title: 'Bench is unavailable', text: 'This board comes back as soon as Bench answers.' });
if (!board) content = kit.empty({ icon: 'trophy', title: 'Bench is unavailable', text: 'This board comes back as soon as Bench answers.' });
- else if (!rows.length) content = emptyFor(kind);
+ else if (!played(rows)) content = emptyFor(kind);
else if (tab === 'pareto') {
else if (tab === 'pareto') {
const points = paretoRows(board, cost);
const points = paretoRows(board, cost);
content = paretoChart(points) + (points.length ? kit.table({ caption: 'The numbers', columns: [{ key: 'm', label: 'Model', primary: true, html: true }, { key: 'q', label: 'Quality', num: true, html: true }, { key: 'c', label: 'Cost per solved', num: true, html: true }],
content = paretoChart(points) + (points.length ? kit.table({ caption: 'The numbers', columns: [{ key: 'm', label: 'Model', primary: true, html: true }, { key: 'q', label: 'Quality', num: true, html: true }, { key: 'c', label: 'Cost per solved', num: true, html: true }],
@@ -579,7 +590,7 @@ function aiBoard({ kindId, kind, tab, board, cost, kinds }) {
@@ -579,7 +590,7 @@ function aiBoard({ kindId, kind, tab, board, cost, kinds }) {
}) }) + `<p class="rp-fine">${esc(METHOD)}</p>`;
}) }) + `<p class="rp-fine">${esc(METHOD)}</p>`;
}
}
const head = kit.head({ title: name, sub: kind ? `${roomName(kind.room)} · ${howRated(kind)}` : 'Bench results', back: { href: '/ai', label: 'AI boards' },
const head = kit.head({ title: name, sub: kind ? `${roomName(kind.room)} · ${howRated(kind)}` : 'Bench results', back: { href: '/ai', label: 'AI boards' },
- tabs: rows.length ? kit.tabs({ base: aiPath(kindId), items: TABS, active: tab, label: 'Board view' }) : '' });
+ tabs: played(rows) ? kit.tabs({ base: aiPath(kindId), items: TABS, active: tab, label: 'Board view' }) : '' });
const body = head + benchState(kinds, board, cost) + `<section aria-label="${esc(TABS.find((t) => t.key === tab)?.label || 'Board')}">${content}</section>${datasetLink(kindId)}`;
const body = head + benchState(kinds, board, cost) + `<section aria-label="${esc(TABS.find((t) => t.key === tab)?.label || 'Board')}">${content}</section>${datasetLink(kindId)}`;
return layout({ title: `${name}: AI board`, description: `${name}: which AI does it best, and what it costs.`, body, active: '/ai', path: aiPath(kindId), agent: true });
return layout({ title: `${name}: AI board`, description: `${name}: which AI does it best, and what it costs.`, body, active: '/ai', path: aiPath(kindId), agent: true });
}
}
@@ -117,6 +117,26 @@ const { boot, check, done, stubFetch, jsonResponse } = require('./helpers/boot')
@@ -117,6 +117,26 @@ const { boot, check, done, stubFetch, jsonResponse } = require('./helpers/boot')
} finally { await fresh.close(); }
} finally { await fresh.close(); }
});
});
+ await check('a model entered but never judged is no leader: 0 games reads as no votes yet', async () => {
+ const unplayed = await boot({
+ fetchImpl: async (url, opts) => {
+ const u = new URL(String(url));
+ if (u.port !== '5230') return source(url, opts);
+ if (u.pathname === '/api/v1/kinds') return jsonResponse({ count: 1, kinds: [{ id: 'design.page', room: 'design', name: 'Page', judge: 'human_pairwise' }] });
+ if (u.pathname.startsWith('/api/v1/boards/')) return jsonResponse({ board: { rows: [{ subject: 'gpt-5-nano-2025-08-07', elo: 1000, games: 0, provisional: true }] } });
+ return jsonResponse({}, 404);
+ },
+ env: { OV_BENCH_INTERNAL_URL: 'http://127.0.0.1:5230' },
+ });
+ try {
+ const r = await unplayed.get('/ai');
+ assert.ok(r.text.includes('No model is rated yet') && !r.text.includes('<caption>Leaders</caption>'), 'no leaders table');
+ assert.ok(!r.text.includes('gpt-5-nano-2025-08-07'), 'the unjudged model is named nowhere as a leader');
+ const b = await unplayed.get('/ai/design.page');
+ assert.ok(b.text.includes('No votes yet') && b.text.includes('Vote on Design') && !b.text.includes('class="ovk-tabs'), 'the board asks for votes');
+ } finally { await unplayed.close(); }
+ });
+
await check('an initial Bench outage still renders an AI page', async () => {
await check('an initial Bench outage still renders an AI page', async () => {
const empty = await boot({ fetchImpl: async (url, opts) => String(url).includes(':5230') ? Promise.reject(new Error('offline')) : source(url, opts), env: { OV_BENCH_INTERNAL_URL: 'http://127.0.0.1:5230' } });
const empty = await boot({ fetchImpl: async (url, opts) => String(url).includes(':5230') ? Promise.reject(new Error('offline')) : source(url, opts), env: { OV_BENCH_INTERNAL_URL: 'http://127.0.0.1:5230' } });
try {
try {