openvibers/openvibe.report Commits
The AI model comparison moves to OpenVibe.Compare: /ai pages answer 301 (#215)
Changes
ddeleted file mode 100644@@@ -1,111 +0,0 @@--/* Report's AI comparison pages. Report's own tokens only (--bg-card, --border, --text-*, --accent, --room-*); the kit-- draws tables (stacked into cards under 600 px), stats, sections, folds and the search pill. */--.ai-page{display:grid;gap:clamp(20px,3vw,32px);padding-block:clamp(12px,2vw,24px) 48px}--.ai-page>*{min-width:0}----/* Headers */--.ai-head{display:grid;gap:14px;padding:clamp(18px,3.4vw,36px) 0 4px;position:relative}--.ai-head::before{content:"";position:absolute;inset:-24px -16px auto;height:220px;z-index:-1;pointer-events:none;-- background:radial-gradient(60% 100% at 18% 0,color-mix(in srgb,var(--room-accent,var(--accent)) 16%,transparent),transparent 70%)}--.ai-head h1{margin:0;font-size:clamp(1.85rem,4.6vw,3.1rem);line-height:1.06;letter-spacing:-.02em;max-width:20ch;text-wrap:balance}--.ai-head--page h1{font-size:clamp(1.6rem,3.6vw,2.4rem)}--.ai-kicker{margin:0;font-size:.78rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--room-accent,var(--accent))}--.ai-kicker a{color:inherit;text-decoration:none}--.ai-kicker a:hover{text-decoration:underline}--.ai-lede{margin:0;max-width:62ch;color:var(--text-secondary);font-size:clamp(.98rem,1.6vw,1.08rem);line-height:1.5}--.ai-lede code{font-size:.85em;color:var(--text-muted);overflow-wrap:anywhere}--.ai-head .ovk-find{max-width:520px}--.ai-fine,.ai-data{margin:10px 0 0;color:var(--text-muted);font-size:.86rem}--.ai-data a{color:var(--text-secondary)}--.ai-quiet{color:var(--text-muted);font-size:.9em}--.ai-bad{color:var(--danger)}----/* A model: mark, name, maker and tags */--.ai-model{display:grid;grid-template-columns:auto minmax(0,1fr);align-items:center;gap:10px;color:inherit;text-decoration:none;min-width:0}--.ai-model-text{display:grid;gap:2px;min-width:0}--.ai-model-text b{font-weight:650;overflow-wrap:anywhere}--.ai-model:hover b{color:var(--accent);text-decoration:underline;text-underline-offset:3px}--.ai-model-text small{display:flex;flex-wrap:wrap;align-items:center;gap:6px;color:var(--text-muted);font-size:.8rem}--.ai-model-text .ovk-badge{font-size:.7rem}----/* Numbers in cells: the value, then what it rests on */--.ai-board td b,.ai-board td .ai-rating b,.ai-price b{font-variant-numeric:tabular-nums;font-weight:650}--.ai-board td small,.ai-rating small,.ai-price small{display:block;margin-top:2px;color:var(--text-muted);font-size:.78rem;font-weight:400}--.ai-rank{display:inline-grid;place-items:center;min-width:2.1em;height:2.1em;padding:0 .4em;border-radius:999px;font-weight:700;font-variant-numeric:tabular-nums;-- background:var(--bg-secondary,color-mix(in srgb,var(--text-primary) 6%,transparent));color:var(--text-secondary);font-size:.85rem}--.ai-rank.is-top{background:color-mix(in srgb,var(--room-accent,var(--accent)) 22%,transparent);color:var(--text-primary)}--.ai-board th.is-primary,.ai-board td:nth-child(2){min-width:13rem}----/* Inside a kit section, the page's own blocks sit on its padding */--.ovk-section>.ai-fine,.ovk-section>.ai-data,.ovk-section>.ai-note,.ovk-section>.ai-picks,.ovk-section>.ai-chips,.ovk-section>.ai-answers,.ovk-section>.ai-prompt,.ovk-section>ul{margin:0 14px 14px}--.ovk-section>.ovk-table-wrap+.ai-fine,.ovk-section>.ovk-table-wrap+.ai-data{margin-top:12px}--.ai-note{margin:0 0 12px;padding:12px 14px;border:1px solid color-mix(in srgb,var(--accent) 35%,var(--border));border-radius:12px;background:color-mix(in srgb,var(--accent) 8%,transparent);color:var(--text-secondary);font-size:.92rem}--.ovk-section>.ai-note{margin:0 14px 12px}--.ai-page .ovk-table td small{display:block;margin-top:2px;color:var(--text-muted);font-size:.78rem;font-weight:400}--.ai-page .ovk-table td b{font-variant-numeric:tabular-nums}----/* Top picks */--.ai-picks{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,230px),1fr));gap:12px}--.ai-pick{height:100%;box-sizing:border-box;display:grid;align-content:start;gap:8px;padding:16px;border:1px solid var(--border);border-radius:16px;background:var(--bg-card);color:inherit;text-decoration:none;-- transition:border-color .15s,transform .15s}--.ai-pick:hover{border-color:color-mix(in srgb,var(--room-accent,var(--accent)) 60%,var(--border));transform:translateY(-1px)}--.ai-pick-k{font-size:.74rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--room-accent,var(--accent))}--.ai-pick-m{display:flex;align-items:center;gap:10px;min-width:0}--.ai-pick-m b{font-size:1.08rem;overflow-wrap:anywhere}--.ai-pick-n{font-size:1.25rem;font-weight:700;font-variant-numeric:tabular-nums}--.ai-pick small{color:var(--text-muted);font-size:.82rem;line-height:1.4}----/* Chip links: tasks, comparisons */--.ai-chips{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:8px}--.ai-chips a{display:inline-flex;align-items:center;min-height:40px;padding:0 14px;border:1px solid var(--border);border-radius:999px;background:var(--bg-card);color:var(--text-primary);text-decoration:none;font-size:.9rem}--.ai-chips a:hover{border-color:var(--accent);color:var(--accent)}----/* Providers */--.ai-prov{display:inline-flex;align-items:center;gap:8px}----/* Filters */--.ai-filters{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,150px),1fr));gap:10px 12px;align-items:end;padding:14px;border:1px solid var(--border);border-radius:16px;background:var(--bg-card)}--.ai-filter{display:grid;gap:5px;min-width:0;font-size:.8rem;color:var(--text-secondary)}--.ai-filter--q{grid-column:1/-1}--.ai-filter input,.ai-filter select{box-sizing:border-box;width:100%;min-width:0;min-height:42px;padding:0 12px;border:1px solid var(--border);border-radius:10px;background:var(--bg-primary,transparent);color:var(--text-primary);font:inherit;font-size:.95rem}--.ai-filter input:focus,.ai-filter select:focus{outline:2px solid var(--accent);outline-offset:1px}--.ai-filters .ovk-btn{min-height:42px}----/* A model page */--.ai-model-head{display:flex;align-items:center;gap:16px;min-width:0}--.ai-model-head>div{display:grid;gap:6px;min-width:0}--.ai-model-head h1{overflow-wrap:anywhere}--.ai-head--model .ai-lede{display:flex;flex-wrap:wrap;align-items:center;gap:6px}--.ai-try{position:sticky;bottom:calc(12px + env(safe-area-inset-bottom));z-index:3;display:flex;flex-wrap:wrap;gap:10px;justify-content:center;padding:10px;border:1px solid var(--border);border-radius:999px;-- background:color-mix(in srgb,var(--bg-card) 92%,transparent);backdrop-filter:blur(10px);width:max-content;max-width:100%;margin-inline:auto;box-shadow:0 10px 30px rgba(0,0,0,.25)}----/* Compare */--.ai-vs{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,280px),1fr));gap:12px}--.ai-vs-card{display:grid;gap:14px;padding:18px;border:1px solid var(--border);border-radius:16px;background:var(--bg-card);min-width:0}--.ai-vs-card dl{margin:0;display:grid;gap:10px}--.ai-vs-card dl div{display:grid;grid-template-columns:7.5rem minmax(0,1fr);gap:10px;align-items:baseline}--.ai-vs-card dt{color:var(--text-muted);font-size:.82rem}--.ai-vs-card dd{margin:0}--.ai-vs-form .ovk-btn{align-self:end}----/* One prompt, many answers */--.ai-prompt{margin:0 0 12px;font-size:1.05rem;font-weight:600}--.ai-answers{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,260px),1fr));gap:12px}--.ai-answers article{display:grid;gap:10px;padding:16px;border:1px solid var(--border);border-radius:16px;background:var(--bg-card);min-width:0}--.ai-answers p{margin:0;white-space:pre-wrap;overflow-wrap:anywhere;color:var(--text-secondary)}----@media (max-width:600px){-- .ai-board th.is-primary,.ai-board td:nth-child(2){min-width:0}-- .ai-filters{grid-template-columns:1fr 1fr}-- .ai-model-head{gap:12px}--}----/* Every section reads as a section: the kit's small label for an action-less section becomes the same heading the-- sections with actions have, so the page's hierarchy is one level, not two. */--.ai-page .ovk-section>h2{padding:14px 14px 10px;color:var(--text-primary);font-size:1.02rem;font-weight:700;letter-spacing:0;text-transform:none}--.ai-cell{display:block;min-width:0}--@media (max-width:600px){.ai-page .ovk-table--stack td .ai-cell{text-align:right}}--/* Numbers in a header wrap into a grid instead of scrolling off a phone. */--.ai-head .ovk-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,140px),1fr));overflow:visible}--@media (max-width:700px){.ai-try{position:static;width:auto;border-radius:16px;box-shadow:none}}--.ai-warn{color:var(--warning,#f59e0b)}@@@ -6,26 +6,16 @@const { normaliseDays } = require('./data/source');// /design and /rundown since 11-03: llms.txt listed them as HTML pages with no twin.--const twinPath = (path) => /^\/(?:|stats|developers|design|rundown|ai|ai\/[^/]+|ai\/models\/[^/]+)$/.test(String(path || '/'));++// The /ai pages moved to OpenVibe.Compare on 2026-11-13 (their twins with them).++const twinPath = (path) => /^\/(?:|stats|developers|design|rundown)$/.test(String(path || '/'));--function createAgentPages({ publicPages, bench }) {++function createAgentPages({ publicPages }) {async function renderPublicPage(path, { query } = {}) {let page = null;-- const model = /^\/ai\/models\/([^/]+)$/.exec(path);-- const kind = /^\/ai\/([^/]+)$/.exec(path);if (path === '/') page = await publicPages.home();else if (path === '/developers') page = publicPages.developers();else if (path === '/design') page = publicPages.design();else if (path === '/stats') page = await publicPages.stats(normaliseDays(query && query.get('days')));-- else if (path === '/ai') page = await publicPages.aiIndex();-- else if (path === '/ai/method') page = await publicPages.aiMethod();-- else if (path === '/ai/compare') {-- const models = String((query && query.get('models')) || '').split(',').map((x) => x.trim()).filter(Boolean);-- page = await publicPages.aiCompare(models);-- }-- else if (model) page = await publicPages.aiModel(decodeURIComponent(model[1]));-- else if (kind && bench.isBenchmarkId(kind[1])) page = await publicPages.aiBenchmark(kind[1]);-- else if (kind) page = await publicPages.aiBoard(kind[1], bench.normaliseTab(query && query.get('tab')));return page ? { html: page.html } : null;}@@@ -10,7 +10,7 @@* /me, /u/:username a person's stats: private by default, the published* sections at /u (the owner, 2026-11-09)* /auth/* sign-in with OpenVibe.Network (openvibe-sdk/sso)-- * /ai, /ai/models/:model, /ai/:kind the AI boards, from OpenVibe.Bench (http/pages.js)++ * /ai… 301 to OpenVibe.Compare, except /ai/arena (http/arena.js) and /ai/exchange* /api/v1/* the JSON API (http/api.js)* /robots.txt, /sitemap.xml, /llms.txt crawl artifacts (http/discovery.js)* /api/health, /api/ready, /release.json, /release-metrics the plumbing (loopback scrape for /metrics)@@@ -342,7 +342,7 @@ async function createApp(opts = {}) {app.use(createPageRoutes({ ...ctx, publicPages }));require('openvibe-shared/agent-layer').mount(app, {descriptor: require('./agent-descriptor'),-- render: require('./agent-pages').createAgentPages({ publicPages, bench }).renderPublicPage,++ render: require('./agent-pages').createAgentPages({ publicPages }).renderPublicPage,});app.use((_req, res) => {res.status(404).set('Cache-Control', PAGE_CACHE).type('html').send(views.notFound());@@@ -46,14 +46,11 @@ function createDiscoveryRoutes({ config, live, bench, sessionReports = null, meS});r.get('/sitemap.xml', async (_req, res) => {-- const [ai, overview, reports, people] = await Promise.all([-- bench.kinds(), bench.overview(), sessionReports ? sessionReports.listPublic({ limit: 500 }) : [],++ // The /ai pages moved to OpenVibe.Compare on 2026-11-13; its sitemap lists them.++ const [reports, people] = await Promise.all([++ sessionReports ? sessionReports.listPublic({ limit: 500 }) : [],meStats ? meStats.listPublished({ limit: 500 }).catch(() => []) : [],]);-- const kinds = (ai && Array.isArray(ai.data.kinds)) ? ai.data.kinds : [];-- const o = overview && overview.data ? overview.data : {};-- const benches = Array.isArray(o.list) ? o.list : [];-- const models = Array.isArray(o.model_list) ? o.model_list.map((m) => m.model).filter(Boolean) : [...new Set((Array.isArray(o.recent) ? o.recent : []).map((r) => r && r.model).filter(Boolean))];const lastmod = siteUpdated();// Only the pages this site owns. /rundown and its topic, episode and speaker pages carry rel=canonical to// OpenVibe.Live's Rundown, so listing them here would advertise a duplicate.@@@ -61,12 +58,6 @@ function createDiscoveryRoutes({ config, live, bench, sessionReports = null, meS{ loc: abs('/'), changefreq: 'daily', priority: 1.0 },{ loc: abs('/stats'), changefreq: 'daily', priority: 0.9 },{ loc: abs('/design'), changefreq: 'weekly', priority: 0.6 },-- { loc: abs('/ai'), changefreq: 'daily', priority: 0.9 },-- { loc: abs('/ai/leaderboard'), changefreq: 'daily', priority: 0.9 },-- { loc: abs('/ai/providers'), changefreq: 'daily', priority: 0.7 },-- { loc: abs('/ai/arena'), changefreq: 'daily', priority: 0.6 },-- { loc: abs('/ai/method'), changefreq: 'weekly', priority: 0.6 },-- { loc: abs('/ai/compare'), changefreq: 'weekly', priority: 0.6 },...reports.map((rep) => ({ loc: abs('/s/' + encodeURIComponent(rep.id)), changefreq: 'daily', priority: 0.6 })),// A person's public stats page is crawlable only when they published a section; the private default is// never in the sitemap. The last modification is the snapshot they last computed.@@@ -74,19 +65,6 @@ function createDiscoveryRoutes({ config, live, bench, sessionReports = null, meSloc: abs(`/u/${encodeURIComponent(p.username)}`), changefreq: 'weekly', priority: 0.5,...(dayOf(p.generated_at) ? { lastmod: dayOf(p.generated_at) } : {}),})),-- ...kinds.filter((k) => k && bench.isKindId(k.id)).map((k) => ({-- loc: abs(`/ai/${encodeURIComponent(k.id)}`), changefreq: 'daily', priority: 0.7,-- })),-- ...benches.filter((b) => b && bench.isBenchmarkId(b.id)).map((b) => ({-- loc: abs(`/ai/${encodeURIComponent(b.id)}`), changefreq: 'daily', priority: 0.8,-- ...(dayOf(b.last_run) ? { lastmod: dayOf(b.last_run) } : {}),-- })),-- ...models.filter((m) => bench.isModelName(m)).map((m) => ({-- loc: abs(`/ai/models/${encodeURIComponent(m)}`), changefreq: 'daily', priority: 0.6,-- })),-- ...models.filter((m) => bench.isModelName(m)).slice(0, 200).flatMap((a, i, list) => list.slice(i + 1).map((b) => ({-- loc: abs(`/ai/compare/${encodeURIComponent(a)}-vs-${encodeURIComponent(b)}`), changefreq: 'weekly', priority: 0.5,-- }))).slice(0, 200),].map((p) => ({ ...p, ...(lastmod && !p.lastmod ? { lastmod } : {}) }));res.type('application/xml').set('Cache-Control', CACHE).send(seo.sitemapXml(pages));});@@@ -104,9 +82,8 @@ function createDiscoveryRoutes({ config, live, bench, sessionReports = null, meS{ title: 'Open numbers', url: abs('/stats'), note: 'page views, accounts and traffic for every OpenVibe site' },{ title: 'Design health', url: abs('/design'), note: 'how closely each site follows the shared design kit, read from its code; per site at /design/<site>' },{ title: 'The Rundown', url: 'https://openvibe.live/rundown', note: 'what streamers talked about, topic by topic (OpenVibe.Live is its canonical home)' },-- { title: 'AI', url: abs('/ai'), note: 'OpenVibe.Bench results: quality, price and speed for every task kind and benchmark' },-- { title: 'How AI results are judged', url: abs('/ai/method'), note: 'scores, verified runs, cost per solved, speed and the frontier' },-- { title: 'Compare AI models', url: abs('/ai/compare'), note: 'two or three models on the same benchmarks' },++ { title: 'AI model comparison', url: 'https://openvibe.compare/ai', note: 'the leaderboard, ratings, prices and measured speed of every AI model now live on OpenVibe.Compare' },++ { title: 'The AI Arena', url: abs('/ai/arena'), note: 'two anonymous answers to one prompt; your vote moves the ratings' },] },// A JSON API is what an agent came for, not something to skip (AGT-5 review).{ title: 'API', links: [@@@ -114,7 +91,7 @@ function createDiscoveryRoutes({ config, live, bench, sessionReports = null, meS{ title: 'Design health (JSON)', url: abs('/api/v1/design-health'), note: 'every site; /api/v1/design-health/<site> for one' },{ title: 'The Rundown (JSON)', url: abs('/api/v1/rundown') },{ title: 'Topics (JSON)', url: abs('/api/v1/rundown/topics') },-- { title: 'New AI results (RSS)', url: abs('/ai/feed.xml') },++ { title: 'New AI results (RSS)', url: 'https://openvibe.compare/ai/feed.xml' },{ title: 'New AI results (JSON)', url: abs('/api/v1/ai/results') },] },],@@@ -11,12 +11,8 @@* GET /rundown/s/:username one streamer* GET /after-show/:streamId one after-show report* GET /c/:username one channel's after-show reports-- * GET /ai OpenVibe.Bench's public results: task kinds, benchmarks, runs and last runs-- * GET /ai/method how a score, a cost and a speed are made-- * GET /ai/compare two or three models side by side-- * GET /ai/feed.xml new runs as RSS-- * GET /ai/:ref one task kind (Elo/price/speed) or one public benchmark (bmk_…)-- * GET /ai/models/:model one model's runs over time, tasks, cost and recent runs++ * GET /ai… 301 to OpenVibe.Compare (the AI model comparison moved there on 2026-11-13), except++ * the Arena's rounds and votes (http/arena.js) and the staff page /ai/exchange** A bad path parameter is a 404 without a source request; so is a source that answers nothing. Pages are public* for two minutes.@@@ -25,7 +21,6 @@* Markdown twin render the very same page; `agent: true` in those views adds the discovery links.*/const express = require('express');--const { findCatalogModel, makerOf } = require('../data/ai-join');const contracts = require('openvibe-contracts');const views = require('../views');const { normaliseDays, isSlug, isStreamId, isUsername, streamIdOf } = require('../data/source');@@@ -33,9 +28,10 @@ const { twinPath } = require('../agent-pages');const designViews = require('../views/design');const designHealth = require('../data/design-health');const sharing = require('../sharing');--const aiComparisonViews = require('../views/ai-comparison');const CACHE = 'public, max-age=120';++/** Where the AI model comparison lives since 2026-11-13. */++const COMPARE = 'https://openvibe.compare';/** The pages the agent layer twins: their data, then the view. Each returns { html } or null (nothing to show). */function createPublicPages({ sessionReports, network, live, bench, aiCatalogue = null, analytics = null, viewsData = null }) {@@@ -56,190 +52,6 @@ function createPublicPages({ sessionReports, network, live, bench, aiCatalogue =return { html: views.developers() };}-- async function aiIndex() {-- return aiComparisonHub();-- }---- async function comparisonData() {-- const [kindsResult, catalogue] = await Promise.all([bench.kinds(), require('../data/ai-market').comparisonCatalogue(aiCatalogue)]);-- const kinds = kindsResult && kindsResult.data && kindsResult.data.kinds || [];-- const boards = await Promise.all(kinds.map((kind) => bench.board(kind.id, 'elo')));-- const catModels = (catalogue && catalogue.models || []).map((model) => ({ ...model, offers: (model.offers || []).map((offer) => ({ ...offer, source: offer.source || { name: 'OpenVibe.AI', date: catalogue.generated_at || null, sample_size: offer.requests_7d ?? null } })) }));-- const findCat = (id) => findCatalogModel(catModels, id);-- const rows = [];-- for (let i = 0; i < boards.length; i += 1) {-- const list = boards[i] && boards[i].data && boards[i].data.board && boards[i].data.board.rows || [];-- for (const entry of list) {-- const id = entry.subject || entry.model || entry.name;-- const cat = findCat(id);-- rows.push({ id, routed: cat ? cat.routed !== false : false, kind: kinds[i].id, model_kind: cat && cat.kind || 'chat', maker: makerOf(cat, id), weights: cat && (cat.weights || (cat.open_weights == null ? null : cat.open_weights ? 'open' : 'closed')), status: cat && cat.status || 'current', uncensored: Boolean(cat && cat.uncensored), community: Boolean(cat && cat.community), rating: { value: entry.elo == null || Number(entry.games) === 0 ? null : Number(entry.elo), interval: Number(entry.games) === 0 ? [null, null] : [entry.ci_lower ?? null, entry.ci_upper ?? null], source: 'OpenVibe.Bench', date: boards[i].at || null, sample_size: entry.games ?? null }, benchmark: { value: entry.score == null ? null : Number(entry.score), reason: entry.score == null ? 'No benchmark score.' : undefined }, provider_offers: cat && Array.isArray(cat.offers) ? cat.offers : [], source: { name: 'OpenVibe.Bench', date: boards[i].at || null, sample_size: entry.games ?? null } });-- }-- }-- const grouped = new Map();-- for (const row of rows) {-- const existing = grouped.get(row.id);-- if (existing) existing.task_ratings.push({ kind: row.kind, rating: row.rating, benchmark: row.benchmark });-- else grouped.set(row.id, { ...row, task_ratings: [{ kind: row.kind, rating: row.rating, benchmark: row.benchmark }] });-- }-- rows.length = 0;-- rows.push(...grouped.values());-- // Every model the Exchange sells is listed with its prices and speed, rated or not: a model without-- // Bench games yet shows as not rated, never as missing.-- const ratedCats = new Set(rows.map((row) => findCat(row.id)).filter(Boolean));-- for (const cat of catModels) {-- if (ratedCats.has(cat)) continue;-- const id = cat.slug || cat.canonical || cat.id;-- if (!id) continue;-- const source = { name: 'OpenVibe.AI', date: catalogue.generated_at || null, sample_size: null };-- rows.push({ id, routed: cat.routed !== false, kind: cat.kind || null, model_kind: cat.kind || 'chat', maker: makerOf(cat, id), weights: cat.weights || (cat.open_weights == null ? null : cat.open_weights ? 'open' : 'closed'), status: cat.status || 'current', uncensored: Boolean(cat.uncensored), community: Boolean(cat.community), rating: { value: null, interval: [null, null], source: 'OpenVibe.Bench', date: null, sample_size: 0 }, benchmark: { value: null, reason: 'Not rated yet.' }, provider_offers: Array.isArray(cat.offers) ? cat.offers : [], source, task_ratings: [], unrated: true });-- }-- for (const kind of kinds) {-- const ranked = rows.map((row) => ({ row, task: row.task_ratings.find((rating) => rating.kind === kind.id) })).filter((x) => x.task).sort((a, b) => (b.task.rating.value ?? -Infinity) - (a.task.rating.value ?? -Infinity));-- ranked.forEach(({ task }, i) => {-- const [lo, hi] = task.rating.interval || [];-- if (lo == null || hi == null) return;-- const overlap = ranked.map((other, j) => ({ other, rank: j + 1 })).filter(({ other }) => other.task.rating.interval && other.task.rating.interval[0] != null && other.task.rating.interval[1] != null && other.task.rating.interval[0] <= hi && other.task.rating.interval[1] >= lo).map(({ rank }) => rank);-- task.rank = overlap.length ? { min: Math.min(...overlap), max: Math.max(...overlap), position: i + 1 } : null;-- });-- }-- const outPrice = (r) => Math.min(...(r.provider_offers || []).map((o) => (o.price && o.price.out != null ? Number(o.price.out) : NaN)).filter(Number.isFinite), Infinity);-- rows.sort((a, b) => (b.rating.value ?? -Infinity) - (a.rating.value ?? -Infinity)-- || (b.routed === true) - (a.routed === true)-- || outPrice(a) - outPrice(b)-- || String(a.id).localeCompare(String(b.id)));-- rows.forEach((row, i) => {-- const [lo, hi] = row.rating.interval || [];-- if (lo == null || hi == null) row.rank = null;-- else {-- const overlap = rows.map((other, j) => ({ other, rank: j + 1 })).filter(({ other }) => other.rating.interval && other.rating.interval[0] != null && other.rating.interval[1] != null && other.rating.interval[0] <= hi && other.rating.interval[1] >= lo).map(({ rank }) => rank);-- row.rank = overlap.length ? { min: Math.min(...overlap), max: Math.max(...overlap), position: i + 1 } : null;-- }-- });-- const tasks = kinds.map((kind) => ({ id: kind.id, name: kind.name }));-- const providers = catModels.flatMap((m) => (m.offers || []).map((o) => ({ ...o, routed: o.routed !== false, model: m.slug })));-- return { rows, providers, tasks, picks: [], catalogue };-- }---- async function aiComparisonHub() {-- const data = await comparisonData();-- const num = (x) => (x == null || x === '' ? NaN : Number(x));-- const chat = data.rows.filter((r) => (r.model_kind || 'chat') === 'chat');-- const rated = chat.filter((r) => r.rating.value != null);-- const best = (list) => [...list].sort((a, b) => b.rating.value - a.rating.value)[0];-- const cheapest = (r) => Math.min(...(r.provider_offers || []).map((o) => num(o.price && o.price.out)).filter(Number.isFinite), Infinity);-- const fastest = (r) => Math.min(...(r.provider_offers || []).filter((o) => Number(o.requests_7d) > 0).map((o) => num(o.speed && o.speed.p50_ms)).filter(Number.isFinite), Infinity);-- const free = (r) => (r.provider_offers || []).some((o) => o.free === true || o.free_now === true || num(o.price && o.price.out) === 0);-- const taskRows = (kind) => rated.map((row) => { const task = row.task_ratings.find((x) => x.kind === kind && x.rating && x.rating.value != null); return task ? { ...row, kind, rating: task.rating, benchmark: task.benchmark } : null; }).filter(Boolean);-- const choose = (id, model, why) => ({ id, model: model || null, why: model ? why : 'Not enough source data to establish this pick.' });-- data.picks = [-- choose('overall', best(rated), 'Highest rating from blind votes and open runs.'),-- choose('value', rated.filter((r) => Number.isFinite(cheapest(r))).sort((a, b) => cheapest(a) - cheapest(b))[0], 'Lowest output price among rated models.'),-- choose('fastest', chat.filter((r) => Number.isFinite(fastest(r))).sort((a, b) => fastest(a) - fastest(b))[0], 'Fastest typical reply on real traffic this week.'),-- choose('open', best(rated.filter((r) => ['open', 'open_weights'].includes(r.weights))), 'Highest-rated model with open weights.'),-- choose('code', best(taskRows('code')), 'Highest rating on code tasks.'),-- choose('writing', best(taskRows('writing')), 'Highest rating on writing tasks.'),-- choose('uncensored', best(rated.filter((r) => r.uncensored)), 'Highest-rated uncensored model; 18+.'),-- choose('free', best(rated.filter(free)) || chat.find(free), 'Free to use now through the Exchange.'),-- ];-- data.others = data.rows.length - chat.length;-- data.rows = chat;-- return { html: aiComparisonViews.hub(data) };-- }-- async function aiComparisonLeaderboard(query = {}) {-- const nz = (x) => (x == null || x === '' ? NaN : Number(x));-- const data = await comparisonData();-- let rows = data.rows;-- if (query.kind) rows = rows.map((x) => {-- const task = x.task_ratings.find((rating) => rating.kind === query.kind);-- return task ? { ...x, kind: query.kind, rating: task.rating, benchmark: task.benchmark } : x.kind === query.kind ? x : null;-- }).filter(Boolean);-- const type = ['chat', 'embedding', 'stt', 'all'].includes(query.type) ? query.type : 'chat';-- if (type !== 'all') rows = rows.filter((x) => (x.model_kind || 'chat') === type);-- if (query.maker) rows = rows.filter((x) => String(x.maker || '').toLowerCase() === String(query.maker).toLowerCase());-- if (query.weights) rows = rows.filter((x) => x.weights === query.weights || (query.weights === 'open' && x.weights === 'open_weights'));-- if (query.status !== 'all') rows = rows.filter((x) => x.status === (query.status || 'current'));-- if (query.uncensored) rows = rows.filter((x) => x.uncensored);-- if (query.community) rows = rows.filter((x) => x.community);-- if (query.price) rows = rows.filter((x) => x.provider_offers.some((o) => {-- const p = nz(o.price && o.price.out);-- return query.price === 'free' ? (o.free === true || o.free_now === true || p === 0) : !Number.isFinite(p) ? false : query.price === 'low' ? p <= 1 : query.price === 'mid' ? p > 1 && p <= 10 : query.price === 'high' ? p > 10 : true;-- }));-- if (query.q) rows = rows.filter((x) => `${x.id} ${x.maker || ''}`.toLowerCase().includes(String(query.q).toLowerCase()));-- if (query.sort === 'name') rows.sort((a, b) => String(a.id).localeCompare(String(b.id)));-- if (query.sort === 'price') rows.sort((a, b) => Math.min(...a.provider_offers.map((o) => nz(o.price && o.price.out)).filter(Number.isFinite), Infinity) - Math.min(...b.provider_offers.map((o) => nz(o.price && o.price.out)).filter(Number.isFinite), Infinity));-- if (query.sort === 'speed') rows.sort((a, b) => Math.min(...a.provider_offers.filter((o) => Number(o.requests_7d) > 0).map((o) => nz(o.speed && o.speed.p50_ms)).filter(Number.isFinite), Infinity) - Math.min(...b.provider_offers.filter((o) => Number(o.requests_7d) > 0).map((o) => nz(o.speed && o.speed.p50_ms)).filter(Number.isFinite), Infinity));-- if (query.sort === 'rating') rows.sort((a, b) => (b.rating.value ?? -Infinity) - (a.rating.value ?? -Infinity));-- return { html: aiComparisonViews.leaderboard({ rows, filters: query }) };-- }-- async function aiComparisonProviders() { const data = await comparisonData(); return { html: aiComparisonViews.providers(data) }; }-- async function aiComparisonModel(id) {-- if (!bench.isModelName(id)) return null;-- const data = await comparisonData();-- const row = data.rows.find((x) => x.id === id);-- const detail = await bench.modelDetail(id);-- return { html: aiComparisonViews.model({ id, detail: detail && detail.data, sourceDate: detail && detail.at, row, providers: data.providers }) };-- }-- async function aiComparison({ models = [] } = {}) {-- const data = await comparisonData();-- const selected = models.map((id) => data.rows.find((x) => x.id === id) || { id, reason: 'No public model data.' });-- return { html: aiComparisonViews.compare({ models: selected, options: data.rows.map((x) => x.id) }) };-- }-- function aiArena() { return { html: aiComparisonViews.arena() }; }---- async function aiBoard(kindId, tab) {-- if (!bench.isKindId(kindId)) return null;-- const [kinds, board, cost, benchmarks, runs, overview] = await Promise.all([-- bench.kinds(),-- bench.board(kindId, tab),-- tab === 'pareto' ? bench.board(kindId, 'cost') : Promise.resolve(null),-- bench.benchmarks(),-- bench.runs({ kind: kindId, limit: 8 }),-- bench.overview(),-- ]);-- const kind = kinds && Array.isArray(kinds.data.kinds) ? kinds.data.kinds.find((k) => k.id === kindId) : null;-- if (kinds && !kind) return null;-- const kindBenchmarks = benchmarks && benchmarks.data && Array.isArray(benchmarks.data.benchmarks)-- ? benchmarks.data.benchmarks.filter((b) => b.kind === kindId) : [];-- const rankings = await Promise.all(kindBenchmarks.map((b) => bench.rankings(b.id, 'score')));-- return { html: views.aiBoard({ kindId, kind, tab, board, cost, kinds, benchmarks, rankings, runs, overview }) };-- }---- async function aiBenchmark(benchmarkId) {-- if (!bench.isBenchmarkId(benchmarkId)) return null;-- const [benchmark, rankings, runs, overview] = await Promise.all([-- bench.benchmark(benchmarkId), bench.rankings(benchmarkId, 'score'), bench.runs({ benchmark: benchmarkId, limit: 20 }), bench.overview(),-- ]);-- // A public id the overview does not list is a 404; a Bench that did not answer at all is the honest unavailable page.-- const list = overview && overview.data && Array.isArray(overview.data.list) ? overview.data.list : null;-- if (list && !list.some((b) => b.id === benchmarkId)) return null;-- return { html: views.aiBenchmark({ benchmark, rankings, runs }) };-- }---- async function aiModel(model) {-- if (!bench.isModelName(model)) return null;-- const [detail, legacy] = await Promise.all([bench.modelDetail(model), bench.modelSummary(model)]);-- return { html: views.aiModel({ model, detail, legacy }) };-- }---- async function aiMethod() {-- const [overview, kinds] = await Promise.all([bench.overview(), bench.kinds()]);-- return { html: views.aiMethod({ overview, kinds }) };-- }---- async function aiCompare(models = [], errors = []) {-- const clean = [...new Set((models || []).filter((m) => bench.isModelName(m)))].slice(0, 3);-- const [details, legacy] = await Promise.all([-- Promise.all(clean.map((m) => bench.modelDetail(m))),-- Promise.all(clean.map((m) => bench.modelSummary(m))),-- ]);-- return { models: clean, html: views.aiCompare({ models: clean, details, legacy, errors }) };-- }---- async function aiFeed() {-- const [runs, overview] = await Promise.all([bench.runs({ limit: 20 }), bench.overview()]);-- return { runs, overview };-- }--function design() { return { html: designViews.designHealth(designHealth.all()) }; }async function rundown() {@@@ -250,7 +62,7 @@ function createPublicPages({ sessionReports, network, live, bench, aiCatalogue =return { html: views.rundown({ rundown: r, topics, reports }) };}-- return { home, stats, developers, design, rundown, aiIndex, aiBoard, aiBenchmark, aiModel, aiMethod, aiCompare, aiFeed, aiComparisonHub, aiComparisonLeaderboard, aiComparisonProviders, aiComparisonModel, aiComparison, aiArena };++ return { home, stats, developers, design, rundown };}/** The new-results feed: one item per public run, with its own Bench date and no invented number. */@@@ -319,49 +131,13 @@ function createPageRoutes(ctx) {return send(req, res, page.html);});-- r.get('/ai', async (_req, res) => send(_req, res, (await publicPages.aiComparisonHub()).html));---- r.get('/ai/leaderboard', async (req, res) => send(req, res, (await publicPages.aiComparisonLeaderboard(req.query)).html));-- r.get('/ai/providers', async (req, res) => send(req, res, (await publicPages.aiComparisonProviders()).html));-- r.get('/ai/arena', (_req, res) => send(_req, res, publicPages.aiArena().html));-- r.get('/ai/compare', async (req, res) => {-- // The picker sends a and b; older links send models=a,b.-- const picked = [req.query.a, req.query.b].map((x) => String(x || '').trim()).filter(Boolean);-- const models = picked.length ? picked : String(req.query.models || '').split(',').map((x) => x.trim()).filter(Boolean);-- if (models.length === 2) return res.redirect(301, `/ai/compare/${encodeURIComponent(models[0])}-vs-${encodeURIComponent(models[1])}`);-- return send(req, res, (await publicPages.aiComparison({ models })).html);-- });-- r.get('/ai/compare/:pair', async (req, res) => {-- const match = String(req.params.pair).match(/^(.+)-vs-(.+)$/);-- if (!match) return notFound(req, res);-- const ordered = [decodeURIComponent(match[1]), decodeURIComponent(match[2])].sort();-- const canonical = `/ai/compare/${encodeURIComponent(ordered[0])}-vs-${encodeURIComponent(ordered[1])}`;-- if (req.path !== canonical) return res.redirect(301, canonical);-- return send(req, res, (await publicPages.aiComparison({ models: ordered })).html);-- });---- r.get('/ai/models/:model', async (req, res) => {-- const page = await publicPages.aiComparisonModel(req.params.model);-- if (!page) return notFound(req, res);-- return send(req, res, page.html);-- });---- // The AI section's method, comparison and feed. They must sit before /ai/:ref, which would read them as a kind.-- r.get('/ai/method', async (req, res) => send(req, res, (await publicPages.aiMethod()).html, 200, 'no-store'));---- r.get('/ai/compare', async (req, res) => {-- const raw = String(req.query.models || '').split(',').map((x) => x.trim()).filter(Boolean);-- const errors = [];-- const unique = [...new Set(raw)];-- if (unique.length < 2) errors.push('Pick at least two model ids, comma separated.');-- if (unique.length > 3) errors.push('At most three models can be compared at once.');-- const page = await publicPages.aiCompare(unique.slice(0, 3), errors);-- return send(req, res, page.html, 200, 'no-store');-- });---- r.get('/ai/feed.xml', async (req, res) => {-- const feed = await publicPages.aiFeed();-- res.status(200).set('Cache-Control', 'public, max-age=120').type('application/rss+xml; charset=utf-8').send(rssFeed({ ...feed, base: ctx.config.baseUrl }));++ // The AI model comparison moved to OpenVibe.Compare (the owner, 2026-11-13: "This kinda shit is supposed to be a part++ // of openvibe.compare"). Every /ai page answers 301 there with its path and query; the Arena's rounds and votes++ // (http/arena.js, mounted before these routes) and the staff page /ai/exchange below stay here. Report keeps the++ // ledgers and the JSON API (/api/v1/ai/*) Compare reads.++ r.get(/^\/ai(?:\/.*)?$/, (req, res, next) => {++ if (/^\/ai\/(?:arena|exchange)(?:\/|$)/.test(req.path)) return next();++ return res.redirect(301, `${COMPARE}${req.originalUrl}`);});// The AI Exchange's daily aggregates (plan G-P9), staff-only, behind REPORT_AI_EXCHANGE. The gate is the signed-in@@@ -382,15 +158,6 @@ function createPageRoutes(ctx) {}});-- r.get('/ai/:ref', async (req, res) => {-- const ref = String(req.params.ref || '');-- const page = bench.isBenchmarkId(ref)-- ? await publicPages.aiBenchmark(ref)-- : await publicPages.aiBoard(ref, bench.normaliseTab(req.query.tab));-- if (!page) return notFound(req, res);-- return send(req, res, page.html, 200, 'no-store');-- });--r.get('/rundown', async (req, res) => {const [rundown, topics, reports] = await Promise.all([live.rundown(), live.topics(),ddeleted file mode 100644@@@ -1,292 +0,0 @@--'use strict';----// Report's AI comparison pages: the hub, the full leaderboard, providers, a model and a comparison.--// One way to show each thing everywhere:--// - a model is its maker's mark, a readable name and its tags;--// - a rating is the number with its spread and the votes it rests on, or "Not rated yet";--// - a price is the cheapest provider's own-page price;--// - speed and uptime are measured on the Exchange's real traffic, shown only where traffic went.--// A column or section earns its place: it shows when some row has a value, and a missing value reads in words.--const kit = require('openvibe-shared/kit');--const { parts, PARTS_CSS } = require('openvibe-shared/showcase');--const marks = require('openvibe-shared/ai-marks');--const { asset } = require('../assets');--const { layout } = require('./index');----const esc = kit.esc;--const value = (x) => x == null || !Number.isFinite(Number(x)) ? null : Number(x);--const idPath = (id) => `/ai/models/${encodeURIComponent(id || '')}`;--const act = (href, label) => kit.button({ href, label, kind: 'quiet', icon: 'forward' });--const quiet = (text) => `<span class="ai-quiet">${esc(text)}</span>`;--const money = (x) => value(x) == null ? null : Number(x) === 0 ? 'Free' : kit.num(x, 'money');--const ms = (x) => value(x) == null ? null : kit.num(x, 'ms');--const share = (x) => { const v = x != null && typeof x === 'object' ? x.value : x; return value(v) == null ? null : `${(Number(v) * 100).toFixed(Number(v) > 0 && Number(v) < 0.01 ? 2 : 1)}%`; };--const sourceHint = (s) => { const x = s && typeof s === 'object' ? s : { name: s }; return `Source: ${x.name || 'not listed'}${x.date ? ` · ${String(x.date).slice(0, 10)}` : ''}${x.sample_size != null ? ` · n = ${x.sample_size}` : ''}`; };----// ── A model ──--function makerName(row) {-- const key = marks.family(row && row.maker) || marks.family(row && row.id);-- return key ? marks.NAMES[key] : (row && row.maker) || null;--}--// "openai/gpt-5-nano" reads as "gpt-5-nano" beside OpenAI's mark; the full id stays in the link and the title.--const shortName = (id) => String(id || '').replace(/^[^/]+\//, '') || String(id || '');--function tags(row) {-- const t = [];-- if (typeof row.routed === 'boolean') t.push(kit.badge(row.routed ? 'Routed here' : 'Not routed here'));-- if (row.weights === 'open' || row.weights === 'open_weights') t.push(kit.badge('Open weights'));-- if (row.uncensored) t.push(kit.badge('18+', 'warn'));-- if (row.community) t.push(kit.badge('Community machines'));-- return t.join('');--}--function modelCell(row, { size = 28 } = {}) {-- const maker = makerName(row);-- return `<a class="ai-model" href="${idPath(row.id)}" title="${esc(row.id)}">${marks.mark(row.maker || row.id, { size, label: false })}`-- + `<span class="ai-model-text"><b>${esc(shortName(row.id))}</b><small>${esc(maker || 'Maker not listed')}${tags(row) ? ` ${tags(row)}` : ''}</small></span></a>`;--}----// ── Numbers on a model ──--const rated = (row) => row && row.rating && value(row.rating.value) != null;--function ratingCell(row, { range = false } = {}) {-- if (!rated(row)) return quiet('Not rated yet');-- const v = Math.round(Number(row.rating.value));-- const [lo, hi] = (row.rating.interval || []).map(value);-- const n = value(row.rating.sample_size);-- const spread = lo != null && hi != null ? (range ? `${Math.round(lo)}–${Math.round(hi)}` : `± ${Math.round((hi - lo) / 2)}`) : '';-- const votes = n != null ? `${n.toLocaleString('en-US')} ${n === 1 ? 'game' : 'games'}` : '';-- return `<span class="ai-rating" title="${esc(sourceHint(row.rating))}"><b>${v}</b>${spread || votes ? `<small>${esc([spread, votes].filter(Boolean).join(' · '))}</small>` : ''}</span>`;--}--const rankText = (row, i) => row.rank && row.rank.min != null ? (row.rank.min === row.rank.max ? String(row.rank.min) : `${row.rank.min}–${row.rank.max}`) : (rated(row) ? String(i + 1) : '–');--const offersOf = (row) => (row && row.provider_offers || []).filter(Boolean);--const measured = (o) => o.routed !== false && Number(o.requests_7d) > 0;--function cheapest(row) {-- const priced = offersOf(row).filter((o) => o.price && value(o.price.out) != null);-- return priced.sort((a, b) => Number(a.price.out) - Number(b.price.out))[0] || null;--}--function fastest(row) {-- const timed = offersOf(row).filter((o) => measured(o) && o.speed && value(o.speed.p50_ms) != null);-- return timed.sort((a, b) => Number(a.speed.p50_ms) - Number(b.speed.p50_ms))[0] || null;--}--function priceCell(row) {-- const o = cheapest(row);-- if (!o) return quiet('No price listed');-- const n = offersOf(row).filter((x) => x.price && value(x.price.out) != null).length;-- const via = n > 1 ? `cheapest of ${n} providers` : `via ${providerName(o)}`;-- return `<span class="ai-price" title="${esc(sourceHint(o.source || { name: o.provider_name }))}"><b>${esc(money(o.price.out))}</b> out · ${esc(money(o.price.in) || '–')} in<small>per 1M tokens, ${esc(via)}</small></span>`;--}--function speedCell(row) {-- const o = fastest(row);-- if (!o) return quiet('No traffic yet');-- return `<span title="${esc(sourceHint({ name: 'OpenVibe.AI, real traffic', sample_size: o.requests_7d }))}"><b>${esc(ms(o.speed.p50_ms))}</b><small>typical reply · p95 ${esc(ms(o.speed.p95_ms) || '–')}</small></span>`;--}--function uptimeCell(row) {-- const o = fastest(row) || offersOf(row).find(measured);-- if (!o || share(o.uptime) == null) return quiet('No traffic yet');-- return `<b${Number(o.uptime) < 0.95 ? ' class="ai-bad"' : ''}>${esc(share(o.uptime))}</b><small>${esc(Number(o.requests_7d).toLocaleString('en-US'))} requests in 7 days</small>`;--}----// ── The leaderboard table ──--function board(rows, { full = false } = {}) {-- const has = (fn) => rows.some(fn);-- const columns = [-- has(rated) && { key: 'rank', label: 'Rank', num: true, html: true },-- { key: 'model', label: 'Model', primary: true, html: true },-- has(rated) && { key: 'rating', label: 'Rating', html: true },-- has((r) => cheapest(r)) && { key: 'price', label: 'Price', html: true },-- has((r) => fastest(r)) && { key: 'speed', label: 'Reply time', html: true },-- full && has((r) => offersOf(r).some((o) => measured(o) && share(o.uptime) != null)) && { key: 'uptime', label: 'Uptime 7 d', html: true },-- full && has((r) => r.benchmark && value(r.benchmark.value) != null) && { key: 'bench', label: 'Bench score', html: true },-- ].filter(Boolean);-- const data = rows.map((r, i) => ({-- rank: `<span class="ai-rank${rated(r) && i < 3 ? ' is-top' : ''}">${esc(rankText(r, i))}</span>`,-- model: modelCell(r),-- rating: ratingCell(r),-- price: priceCell(r),-- speed: speedCell(r),-- uptime: uptimeCell(r),-- bench: r.benchmark && value(r.benchmark.value) != null ? `<b>${esc(kit.num(r.benchmark.value))}</b>` : quiet('No runs yet'),-- }));-- const none = rows.length && !has(rated) ? `<p class="ai-note">No model has blind votes or runs yet, so nothing is ranked: these are the models with their prices and measured speed. <a href="https://openvibe.exchange/arena">Vote in the Arena</a> to rate them.</p>` : '';-- const wrapped = data.map((d) => Object.fromEntries(Object.entries(d).map(([k, v]) => [k, k === 'model' ? v : `<span class="ai-cell">${v}</span>`])));-- return none + kit.table({ columns, rows: wrapped, label: 'AI models', empty: 'No model matches these filters.' }).replace('class="ovk-table"', 'class="ovk-table ai-board"');--}----function page(title, description, body, path) {-- return layout({ title, description, body: `<link rel="stylesheet" href="${asset('css/ai-comparison.css')}"><style>${marks.CSS}\n${PARTS_CSS}</style><div class="ai-page">${body}</div>`, active: '/ai', path });--}--const dataLinks = () => `<p class="ai-data">Data: <a href="/api/v1/ai/leaderboard">JSON</a> · <a href="/api/v1/ai/leaderboard?format=csv&limit=100">CSV</a> · <a href="/ai/method">How it is measured</a></p>`;----// ── /ai ──--const PICK_LABELS = { overall: 'Best overall', value: 'Best value', fastest: 'Fastest', open: 'Best open weights', code: 'Best for code', writing: 'Best for writing', uncensored: 'Best uncensored 18+', free: 'Best free' };--function pickNumber(id, row) {-- if (id === 'value' || id === 'free') { const o = cheapest(row); return !o ? '' : Number(o.price.out) === 0 ? 'Free' : `${money(o.price.out)} per 1M out`; }-- if (id === 'fastest') { const o = fastest(row); return o ? `${ms(o.speed.p50_ms)} typical reply` : ''; }-- return rated(row) ? `Rating ${Math.round(row.rating.value)}` : '';--}--function hub({ rows = [], picks = [], providers = [], others = 0, month = new Date().toLocaleDateString('en-US', { month: 'long', year: 'numeric', timeZone: 'UTC' }), changed = [], tasks = [], arena = null } = {}) {-- const ratedRows = rows.filter(rated);-- const providerNames = new Set(providers.map(providerName).filter(Boolean));-- const measuredOffers = providers.filter(measured);-- const made = picks.filter((p) => p.model);-- const pending = picks.filter((p) => !p.model).map((p) => PICK_LABELS[p.id] || p.id);-- const pickList = made.map((p) => `<li><a class="ai-pick" href="${idPath(p.model.id)}"><span class="ai-pick-k">${esc(PICK_LABELS[p.id] || p.id)}</span>`-- + `<span class="ai-pick-m">${marks.mark(p.model.maker || p.model.id, { size: 32, label: false })}<b>${esc(shortName(p.model.id))}</b></span>`-- + `${pickNumber(p.id, p.model) ? `<span class="ai-pick-n">${esc(pickNumber(p.id, p.model))}</span>` : ''}<small>${esc(p.why || '')}</small></a></li>`).join('');-- const pairs = ratedRows.slice(0, 16).reduce((a, r, i, all) => { if (i % 2 === 0 && all[i + 1]) a.push([r, all[i + 1]]); return a; }, []).slice(0, 8);-- const sourceDate = rows.map((r) => r.source && r.source.date).find(Boolean) || 'date not reported by source';-- const faq = [-- ['How are ratings made?', `${ratedRows.length} models have a rating from blind pairwise votes and open runs on OpenVibe.Bench (Bradley-Terry with bootstrap intervals), dated ${sourceDate}.`],-- ['Where do prices come from?', `Each provider's own price page, read by OpenVibe.AI's pricebook; ${providers.length} provider and model offers are listed.`],-- ['What does the rank band mean?', 'Models whose rating intervals overlap share a band: the data cannot tell them apart yet.'],-- ['Why does a model say "Not rated yet"?', 'No blind votes or runs on it so far. Its prices and measured speed are still listed.'],-- ['How is speed measured?', `On real requests through OpenVibe.Exchange over seven days: ${measuredOffers.length} provider endpoints had traffic.`],-- ['How often does this change?', 'Ratings and prices refresh within two minutes of the sources changing.'],-- ];-- const body = `<header class="ai-head"><p class="ai-kicker">AI comparison · ${esc(month)}</p><h1>AI model leaderboard — ${esc(month)}</h1>`-- + `<p class="ai-lede">${rows.length} models from ${providerNames.size} providers. Ratings come from blind votes, prices from each provider's own page, and speed and uptime from real traffic.</p>`-- + kit.find({ action: '/ai/leaderboard', placeholder: 'Search a model or maker', label: 'Search models' })-- + kit.stats([[ratedRows.length, 'rated'], [rows.length, 'models listed'], [providerNames.size, 'providers'], [measuredOffers.length, 'measured endpoints']], { label: 'The leaderboard in numbers' })-- + `</header>`-- + (pickList || pending.length ? kit.section({ title: 'Top picks', body: `${pickList ? `<ul class="ai-picks">${pickList}</ul>` : ''}${pending.length ? `<p class="ai-fine">Not enough data yet for ${esc(pending.join(', '))}.</p>` : ''}` }) : '')-- // A phone shows the parts below the top picks one at a time as tabs (the owner, 11-11: "self-contained tabs, so-- // the page isn't so freaking huge … at least on mobile"); every part stays in the HTML, a wide screen stacks them.-- + parts({ name: 'board', label: 'The leaderboard\'s parts', items: [-- { id: 'leaderboard', label: 'Leaderboard', count: rows.length, html: kit.section({ title: 'Leaderboard', count: rows.length, actions: [act('/ai/leaderboard', rows.length > 25 ? `All ${rows.length} models` : 'Filter and sort')], body: board(rows.slice(0, 25)) + (others ? `<p class="ai-fine">Chat models only. ${others} embedding and speech ${others === 1 ? 'model is' : 'models are'} in the <a href="/ai/leaderboard?type=all">full list</a>.</p>` : '') + dataLinks() }) },-- { id: 'answers', label: 'Answers', html: (arena && (arena.answers || []).length ? kit.section({ title: 'One prompt, many answers', body: `<p class="ai-prompt">${esc(arena.prompt || '')}</p><div class="ai-answers">${arena.answers.slice(0, 3).map((a) => `<article>${modelCell({ id: a.model || 'Model', maker: a.maker })}<p>${esc(a.text || '')}</p></article>`).join('')}</div>` }) : '') },-- { id: 'tasks', label: 'By task', count: tasks.length || '', html: (tasks.length ? kit.section({ title: 'Boards by task', body: `<ul class="ai-chips">${tasks.slice(0, 16).map((x) => `<li><a href="/ai/${encodeURIComponent(x.id)}">${esc(x.name || x.id)}</a></li>`).join('')}</ul>` }) : '') },-- { id: 'providers', label: 'Providers', count: measuredOffers.length || '', html: (measuredOffers.length ? kit.section({ title: 'Providers', actions: [act('/ai/providers', 'Every provider')], body: providerTable(measuredOffers.slice(0, 6)) }) : '') },-- { id: 'compare', label: 'Compare', html: (pairs.length ? kit.section({ title: 'Popular comparisons', body: `<ul class="ai-chips">${pairs.map(([a, b]) => `<li><a href="/ai/compare/${encodeURIComponent(a.id)}-vs-${encodeURIComponent(b.id)}">${esc(shortName(a.id))} vs ${esc(shortName(b.id))}</a></li>`).join('')}</ul>` }) : '') },-- { id: 'changes', label: 'Changes', html: (changed.length ? kit.section({ title: 'What changed this month', body: `<ul>${changed.slice(0, 8).map((x) => `<li>${esc(x)}</li>`).join('')}</ul>` }) : '') },-- { id: 'questions', label: 'Questions', html: kit.section({ title: 'Questions', body: faq.map(([q, a]) => kit.fold({ title: q, body: `<p>${esc(a)}</p>` })).join('') }) },-- ] })-- + `<script type="application/ld+json">${JSON.stringify({ '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map(([q, a]) => ({ '@type': 'Question', name: q, acceptedAnswer: { '@type': 'Answer', text: a } })) }).replace(/</g, '\\u003c')}</script>`-- + `<script type="application/ld+json">${JSON.stringify({ '@context': 'https://schema.org', '@type': 'Dataset', name: 'OpenVibe AI model leaderboard', url: 'https://openvibe.report/ai', variableMeasured: ['Bench rating', 'Provider price', 'Provider reply time', 'Provider uptime'], distribution: { '@type': 'DataDownload', contentUrl: 'https://openvibe.report/api/v1/ai/leaderboard', encodingFormat: 'application/json' } }).replace(/</g, '\\u003c')}</script>`;-- return page(`AI model leaderboard — ${month}`, `${rows.length} AI models compared: ratings from blind votes, provider prices and measured speed.`, body, '/ai');--}----// ── /ai/leaderboard ──--function leaderboard({ rows = [], filters = {} } = {}) {-- const select = (name, label, opts) => `<label class="ai-filter"><span>${esc(label)}</span><select name="${name}">${opts.map(([v, t]) => `<option value="${esc(v)}"${String(filters[name] || '') === v ? ' selected' : ''}>${esc(t)}</option>`).join('')}</select></label>`;-- const text = (name, label, placeholder) => `<label class="ai-filter"><span>${esc(label)}</span><input name="${name}" value="${esc(filters[name] || '')}" placeholder="${esc(placeholder)}"></label>`;-- const form = `<form class="ai-filters" action="/ai/leaderboard">`-- + `<label class="ai-filter ai-filter--q"><span>Search</span><input name="q" type="search" value="${esc(filters.q || '')}" placeholder="Model or maker"></label>`-- + select('type', 'Type', [['chat', 'Chat models'], ['embedding', 'Embeddings'], ['stt', 'Speech to text'], ['all', 'All']])-- + select('sort', 'Sort by', [['rating', 'Rating'], ['price', 'Output price'], ['speed', 'Reply time'], ['name', 'Name']])-- + select('weights', 'Weights', [['', 'Any'], ['open', 'Open weights'], ['closed', 'Closed']])-- + select('price', 'Price', [['', 'Any price'], ['free', 'Free'], ['low', 'Low'], ['mid', 'Mid'], ['high', 'High']])-- + text('maker', 'Maker', 'e.g. openai')-- + text('kind', 'Task', 'e.g. design.page')-- + select('status', 'Status', [['current', 'Current'], ['all', 'All']])-- + select('uncensored', '18+', [['', 'Any'], ['true', 'Uncensored only']])-- + select('community', 'Runs on', [['', 'Anywhere'], ['true', 'Community machines']])-- + `<button class="ovk-btn ovk-btn--primary">Apply</button></form>`;-- const body = `<header class="ai-head ai-head--page"><p class="ai-kicker"><a href="/ai">AI comparison</a></p><h1>AI model leaderboard</h1>`-- + `<p class="ai-lede">${rows.length} ${rows.length === 1 ? 'model' : 'models'}${rows.filter(rated).length ? `, ${rows.filter(rated).length} rated` : ''}. Rated models first, then the rest with their prices.</p></header>`-- + form + board(rows, { full: true }) + dataLinks();-- return page('AI model leaderboard', 'Every AI model with its rating, provider prices and measured speed; filter and sort.', body, '/ai/leaderboard');--}----// ── /ai/providers ──--const ownServer = (p) => /^(local|whisper)$/i.test(String(p.provider || '')) || /on this host|\blocal\b/i.test(String(p.provider_name || ''));--const providerName = (p) => ownServer(p) ? 'OpenVibe server' : String(p.provider_name || p.provider || 'Provider').replace(/\s*\((?:the |an? )[^)]*\)\s*$/i, '');--const providerMark = (p) => ownServer(p) ? marks.mark('openvibe', { size: 24, label: false }) : marks.mark(p.provider || p.provider_name, { size: 24, label: false });--function providerTable(list) {-- // Status is a word (the state-in-words rule): Up, Degraded with the warning tone, Down with the bad one, each with-- // its plain reason and since when. The column shows only when some offer is not up (a column earns its place).-- const notUp = (p) => p.routed !== false && ['down', 'degraded'].includes(String(p.status || 'up'));-- const statusCell = (p) => {-- if (p.routed === false) return quiet('Not routed here');-- if (!notUp(p)) return 'Up';-- const down = String(p.status) === 'down';-- const why = p.status_reason ? `<small>${esc(p.status_reason)}</small>` : '';-- const since = p.status_since ? `<small>since ${esc(String(p.status_since).slice(0, 16).replace('T', ' '))} UTC</small>` : '';-- return `<b class="${down ? 'ai-bad' : 'ai-warn'}">${down ? 'Down' : 'Degraded'}</b>${why}${since}`;-- };-- const rows = list.map((p) => ({-- provider: `<span class="ai-prov">${providerMark(p)}<b>${esc(providerName(p))}</b></span>`,-- model: `<a href="${idPath(p.model)}">${esc(shortName(p.model))}</a>`,-- status: statusCell(p),-- price: p.price && value(p.price.out) != null ? `<b>${esc(money(p.price.out))}</b> out · ${esc(money(p.price.in) || '–')} in` : quiet('No price listed'),-- reply: p.routed === false ? quiet('Not routed here') : measured(p) && p.speed && value(p.speed.p50_ms) != null ? `<b>${esc(ms(p.speed.p50_ms))}</b><small>p95 ${esc(ms(p.speed.p95_ms) || '–')}</small>` : quiet('Not measured yet'),-- errors: p.routed === false ? quiet('Not routed here') : measured(p) && share(p.error_rate) != null ? `<b class="${Number(typeof p.error_rate === 'object' ? p.error_rate.value : p.error_rate) > 0.05 ? 'ai-bad' : ''}">${esc(share(p.error_rate))}</b>` : quiet('Not measured yet'),-- uptime: p.routed === false ? quiet('Not routed here') : measured(p) && share(p.uptime) != null ? `<b class="${Number(p.uptime) < 0.95 ? 'ai-bad' : ''}">${esc(share(p.uptime))}</b>` : quiet('Not measured yet'),-- requests: p.routed === false ? quiet('Not routed here') : measured(p) ? Number(p.requests_7d).toLocaleString('en-US') : quiet('Not measured yet'),-- }));-- return kit.table({-- columns: [-- { key: 'provider', label: 'Provider', primary: true, html: true },-- { key: 'model', label: 'Model', html: true },-- list.some(notUp) && { key: 'status', label: 'Status', html: true },-- { key: 'price', label: 'Price per 1M', html: true },-- { key: 'reply', label: 'Reply time', html: true },-- { key: 'errors', label: 'Errors', html: true },-- { key: 'uptime', label: 'Uptime 7 d', html: true },-- { key: 'requests', label: 'Requests 7 d', html: true },-- ],-- rows: rows.map((d) => Object.fromEntries(Object.entries(d).map(([k, v]) => [k, k === 'provider' ? v : `<span class="ai-cell">${v}</span>`]))), label: 'Providers', empty: 'No provider is listed yet.',-- });--}--function providers({ providers: list = [] } = {}) {-- const sorted = [...list].sort((a, b) => (Number(b.requests_7d) || 0) - (Number(a.requests_7d) || 0));-- const body = `<header class="ai-head ai-head--page"><p class="ai-kicker"><a href="/ai">AI comparison</a></p><h1>AI providers</h1>`-- + `<p class="ai-lede">Every model and provider offer in the market. Prices come from each provider's own page. Reply time, errors and uptime are measured on real requests over the last seven days; offers outside the router say “Not routed here”, while routed offers without measurements say “Not measured yet”. Status is measured from the Exchange catalogue: down while an offer\'s circuit is open, degraded when more than 5% of at least 10 requests in the last hour failed, with the plain reason.</p></header>`-- + providerTable(sorted);-- return page('AI providers', 'AI providers compared: price per million tokens, reply time, error rate and uptime measured on real traffic.', body, '/ai/providers');--}----// ── /ai/models/:id ──--function model({ id, detail = {}, row = null, providers: list = [], sourceDate = null } = {}) {-- detail = detail || {};-- const self = row || { id, provider_offers: list.filter((p) => p.model === id) };-- const offers = offersOf(self).length ? offersOf(self) : list.filter((p) => p.model === id);-- const tasks = (self.task_ratings || []).filter((t) => t.rating && value(t.rating.value) != null);-- const maker = makerName(self);-- const facts = kit.stats([-- [rated(self) ? Math.round(self.rating.value) : 'Not rated yet', 'rating'],-- [cheapest({ provider_offers: offers }) ? money(cheapest({ provider_offers: offers }).price.out) : null, 'cheapest output / 1M'],-- [fastest({ provider_offers: offers }) ? ms(fastest({ provider_offers: offers }).speed.p50_ms) : null, 'typical reply'],-- [offers.length || null, offers.length === 1 ? 'provider' : 'providers'],-- ], { label: `${id} in numbers` });-- const taskTable = tasks.length ? kit.table({-- columns: [{ key: 'task', label: 'Task', primary: true, html: true }, { key: 'rank', label: 'Rank' }, { key: 'rating', label: 'Rating', html: true }],-- rows: tasks.map((t) => ({ task: `<a href="/ai/${encodeURIComponent(t.kind)}">${esc(t.kind)}</a>`, rank: t.rank ? (t.rank.min === t.rank.max ? String(t.rank.min) : `${t.rank.min}–${t.rank.max}`) : 'Not enough data', rating: ratingCell({ rating: t.rating }, { range: true }) })),-- label: 'Task ratings',-- }) : `<p class="ai-fine">Not rated on any task yet: no blind votes or runs on this model so far.</p>`;-- const benchmarks = (detail.benchmarks || []).length ? kit.table({-- columns: [{ key: 'name', label: 'Benchmark', primary: true }, { key: 'score', label: 'Score' }, { key: 'runs', label: 'Runs' }, { key: 'last', label: 'Last run' }],-- rows: detail.benchmarks.map((b) => ({ name: b.title || b.benchmark_id, score: b.score == null ? 'Not measured' : String(b.score), runs: b.runs == null ? 'Not reported' : String(b.runs), last: b.last_run ? String(b.last_run).slice(0, 10) : '–' })),-- label: 'Benchmarks', caption: `OpenVibe.Bench${sourceDate ? ` · snapshot ${String(sourceDate).slice(0, 10)}` : ''}`,-- }) : '';-- const body = `<header class="ai-head ai-head--model"><p class="ai-kicker"><a href="/ai">AI comparison</a></p>`-- + `<div class="ai-model-head">${marks.mark(self.maker || id, { size: 56 })}<div><h1>${esc(shortName(id))}</h1><p class="ai-lede">${esc(maker || 'Maker not listed')}${tags(self) ? ` ${tags(self)}` : ''} · <code>${esc(id)}</code></p></div></div>`-- + facts + `</header>`-- + kit.section({ title: 'Providers', count: offers.length, body: providerTable(offers) })-- + kit.section({ title: 'Task ratings', count: tasks.length || null, body: taskTable + benchmarks })-- + `<aside class="ai-try"><a class="ovk-btn ovk-btn--primary" href="https://openvibe.chat/vibe?models=${encodeURIComponent(id)}">Try it in Chat</a><a class="ovk-btn ovk-btn--quiet" href="https://openvibe.exchange/models/${encodeURIComponent(id)}">Use it from code</a></aside>`-- + `<script type="application/ld+json">${JSON.stringify({ '@context': 'https://schema.org', '@type': 'Product', name: id, brand: maker || 'Unknown maker' }).replace(/</g, '\\u003c')}</script>`;-- return page(`${shortName(id)} — AI model`, `${id}: rating, prices across providers and measured speed.`, body, idPath(id));--}----// ── /ai/compare ──--function compare({ models = [], options = [] } = {}) {-- const two = models.length === 2;-- const facts = (m) => [-- ['Rating', ratingCell(m, { range: true })],-- ['Price', priceCell(m)],-- ['Reply time', speedCell(m)],-- ['Providers', offersOf(m).length ? String(offersOf(m).length) : quiet('None listed')],-- ];-- const cards = models.map((m) => `<article class="ai-vs-card">${modelCell(m, { size: 40 })}<dl>${facts(m).map(([k, v]) => `<div><dt>${esc(k)}</dt><dd>${v}</dd></div>`).join('')}</dl></article>`).join('');-- const list = `<datalist id="ai-models">${options.slice(0, 500).map((o) => `<option value="${esc(o)}"></option>`).join('')}</datalist>`;-- const form = `<form class="ai-filters ai-vs-form" action="/ai/compare"><label class="ai-filter"><span>First model</span><input name="a" list="ai-models" value="${esc(models[0] && models[0].id || '')}" required></label>`-- + `<label class="ai-filter"><span>Second model</span><input name="b" list="ai-models" value="${esc(models[1] && models[1].id || '')}" required></label><button class="ovk-btn ovk-btn--primary">Compare</button>${list}</form>`;-- const body = `<header class="ai-head ai-head--page"><p class="ai-kicker"><a href="/ai">AI comparison</a></p><h1>${two ? `${esc(shortName(models[0].id))} vs ${esc(shortName(models[1].id))}` : 'Compare AI models'}</h1>`-- + `${two ? '' : '<p class="ai-lede">Choose two models to compare: their ratings, prices and measured speed side by side.</p>'}</header>`-- + (cards ? `<div class="ai-vs">${cards}</div>` : '') + form;-- return page(two ? `${shortName(models[0].id)} vs ${shortName(models[1].id)}` : 'Compare AI models', 'Compare AI models side by side: ratings, prices and measured speed.', body, '/ai/compare');--}----function arena() {-- return page('AI Arena', 'Compare anonymous AI answers and vote.', `<header class="ai-head ai-head--page"><p class="ai-kicker"><a href="/ai">AI comparison</a></p><h1>AI Arena</h1><p class="ai-lede">Two anonymous answers to one prompt; you pick the better one, then see which models wrote them. Every vote moves the ratings here.</p></header><p><a class="ovk-btn ovk-btn--primary" href="https://openvibe.exchange/arena">Vote in the Arena</a></p>`, '/ai/arena');--}----module.exports = { hub, leaderboard, providers, model, compare, arena };@@@ -97,7 +97,6 @@ const NAV = [{ label: 'You', href: '/you', icon: 'fa-user' },{ label: 'Numbers', href: '/stats', icon: 'fa-chart-line' },{ label: 'The Rundown', href: '/rundown', icon: 'fa-newspaper' },-- { label: 'AI', href: '/ai', icon: 'fa-robot' },{ label: 'API', href: '/developers', icon: 'fa-code', more: true },];@@@ -116,7 +115,7 @@ function layout({ title, description, canonical = null, path = null, robots = 'iservice: 'report',apiBase: NETWORK,links: NAV.map((l) => ({ ...l, active: l.href === active || (l.href !== '/' && active.startsWith(l.href)) })),-- appBar: [{ label: 'Home', href: '/', icon: 'fa-house' }, { label: 'Numbers', href: '/stats', icon: 'fa-chart-line' }, { label: 'Rundown', href: '/rundown', icon: 'fa-newspaper' }, { label: 'AI', href: '/ai', icon: 'fa-robot' }],++ appBar: [{ label: 'Home', href: '/', icon: 'fa-house' }, { label: 'Numbers', href: '/stats', icon: 'fa-chart-line' }, { label: 'Rundown', href: '/rundown', icon: 'fa-newspaper' }],history: { type: 'page', title: title || NAME },// Sign-in is this site's own (openvibe-sdk/sso at /auth): the navbar's buttons and its silent check use it.silentLogin: `${BASE}/auth/login?silent=1&next={url}`,@@@ -147,7 +146,6 @@ function layout({ title, description, canonical = null, path = null, robots = 'i<link rel="stylesheet" href="${esc(ovServe.url('room.css'))}"><link rel="stylesheet" href="${esc(ovServe.url('showcase.css'))}"><link rel="stylesheet" href="${esc(asset('css/app.css'))}">--${path && String(path).startsWith('/ai') ? `<link rel="alternate" type="application/rss+xml" title="AI: new results" href="${esc(`${BASE}/ai/feed.xml`)}">` : ''}<script src="${esc(ovServe.url('kit-web.js'))}" defer></script><script src="${esc(ovServe.url('kit-data-web.js'))}" defer></script><script src="${esc(ovServe.url('motion.js'))}" defer></script>@@@ -281,7 +279,7 @@ function home({ stats, rundown, topics } = {}) {explore: { title: 'More on Report', items: [{ title: 'Every site\'s numbers', text: 'Page views, people, streams and how fast each service answers', href: '/stats', icon: 'ov:gauge' },{ title: 'The Rundown', text: 'What streamers talked about, topic by topic', href: '/rundown', icon: 'ov:news' },-- { title: 'AI', text: 'Which AI does each job best, what it costs and how fast it is: Bench results from every room', href: '/ai', icon: 'ov:ai' },++ { title: 'AI models', text: 'Which AI does each job best, what it costs and how fast it is: the comparison lives on OpenVibe.Compare', href: 'https://openvibe.compare/ai', icon: 'ov:ai' },{ title: 'Status', text: 'Whether every OpenVibe service is up right now', href: 'https://openvibe.network/status', icon: 'ov:ping' },] },dev: { title: 'For developers', lede: 'Every number here is open JSON, readable from any site.', samples: [@@@ -843,353 +841,6 @@ function scoreCostChart(entries) {const TABS = [{ key: 'elo', label: 'Ranking' }, { key: 'pareto', label: 'Quality and cost' }, { key: 'cost', label: 'Cost per solved' }];--function aiIndex({ overview, kinds, boards = [], rankings = [] }) {-- const o = readData(overview) || {};-- const list = kinds && kinds.data && Array.isArray(kinds.data.kinds) ? kinds.data.kinds : [];-- const benches = Array.isArray(o.list) ? o.list : [];-- const lastByKind = byKindFromOverview(o);-- const items = list.map((kind, i) => ({ kind, rows: rowsOf(boards[i]), available: Boolean(boards[i]) }));-- 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({-- label: 'Task kinds and their leaders', columns: [-- { key: 'job', label: 'Job', primary: true, html: true }, { key: 'leader', label: 'Leader', html: true },-- { key: 'quality', label: 'Quality', num: true, html: true }, { key: 'games', label: 'Games', num: true, fmt: 'int' },-- { key: 'cost', label: 'Cost/run', num: true, html: true },-- { key: 'speed', label: 'Speed/run', num: true, html: true }, { key: 'last', label: 'Last run', html: true },-- ],-- rows: rated.map(({ kind, rows }) => {-- const top = rows[0];-- return {-- job: `<a href="${aiPath(kind.id)}">${esc(kind.name || kind.id)}</a>`,-- leader: `${modelLink(top.subject)}${flag(top)}`,-- quality: rating(top),-- games: gamesOf(rows),-- cost: moneyCell(top.median_cost_usd),-- speed: speedCell(top.median_ms),-- last: lastByKind.get(kind.id) ? `<span title="${esc(lastByKind.get(kind.id))}">${esc(dayTime(lastByKind.get(kind.id)))}</span>` : '—',-- };-- }),-- }) : '';-- const waitRows = waiting.map(({ kind, available }) => kit.row({ href: aiPath(kind.id), title: kind.name || kind.id, sub: `${roomName(kind.room)} · ${available ? howRated(kind) : 'board unavailable'}`, trail: kit.icon('chevron') }));-- const waitList = waitRows.length ? kit.list(waitRows, { flush: true }) : '';-- const benchRow = (b, i) => {-- const r = rankings[i] && readData(rankings[i]);-- const top = r && r.rankings && Array.isArray(r.rankings.entries) ? r.rankings.entries[0] : null;-- return {-- benchmark: `<a href="${aiPath(b.id)}">${esc(b.title)}</a>`,-- kind: esc(b.kind || '—'),-- leader: top ? `${modelLink(top.model)} ${scoreCell(top.score)}` : '—',-- runs: b.runs == null ? 0 : b.runs,-- last: b.last_run ? esc(dayTime(b.last_run)) : 'No runs yet',-- };-- };-- const activeBench = benches.filter((b) => Number(b.runs) > 0);-- const idleBench = benches.filter((b) => !(Number(b.runs) > 0));-- const benchTable = activeBench.length ? kit.table({-- label: 'Benchmarks with published runs', columns: [-- { key: 'benchmark', label: 'Benchmark', primary: true, html: true }, { key: 'kind', label: 'Task kind' },-- { key: 'leader', label: 'Leader', html: true }, { key: 'runs', label: 'Runs', num: true, fmt: 'int' }, { key: 'last', label: 'Last run', html: true },-- ], rows: benches.map((b, i) => (Number(b.runs) > 0 ? benchRow(b, i) : null)).filter(Boolean),-- }) : '';-- const idleRows = benches.map((b, i) => (Number(b.runs) > 0 ? null : kit.row({ href: aiPath(b.id), title: b.title, sub: `${b.kind || 'benchmark'} · no run yet`, trail: kit.icon('chevron') }))).filter(Boolean);-- const idleList = idleRows.length ? kit.list(idleRows, { flush: true }) : '';-- const none = !rated.length && list.length;-- const body = kit.head({ title: 'AI', sub: 'Which AI does each job best, what it costs, and how fast it is', sticky: false })-- + benchState(overview, kinds, ...rankings, ...boards)-- + `<p class="rp-fine">Every number on this page is a published Bench result or vote. ${esc(METHOD)} <a href="/ai/method">How the runs are judged</a> · <a href="/ai/compare">Compare models</a> · <a href="/ai/feed.xml">New results (RSS)</a></p>`-- + (!list.length && !benches.length ? kit.empty({ icon: 'trophy', title: 'Bench is unavailable', text: 'The boards come back as soon as Bench answers.' }) : '')-- + (o.benchmarks != null || o.runs != null ? kit.stats([-- [o.benchmarks == null ? '—' : exact(o.benchmarks), o.benchmarks === 1 ? 'benchmark' : 'benchmarks'],-- [o.tasks == null ? '—' : exact(o.tasks), o.tasks === 1 ? 'task' : 'tasks'],-- [o.runs == null ? '—' : exact(o.runs), o.runs === 1 ? 'run' : 'runs'],-- [o.models == null ? '—' : exact(o.models), o.models === 1 ? 'model' : 'models'],-- [o.last_run ? dayTime(o.last_run) : 'No runs yet', 'last run'],-- ], { label: 'AI in numbers' }) : (rated.length ? kit.stats([-- [rated.length, rated.length === 1 ? 'job rated' : 'jobs rated'],-- [rated.reduce((sum, x) => sum + gamesOf(x.rows), 0), 'games played'],-- [list.length, 'jobs tracked'],-- ], { label: 'The boards in numbers' }) : ''))-- + (none ? kit.notice({ kind: 'info', text: `No model is rated on a task kind yet. ${list.length} jobs are set up; each board fills as people vote blind on two answers, or as graders score runs.` }) : '')-- + (leaders ? kit.section({ title: 'Task kinds', count: rated.length, body: leaders }) : '')-- + (benchTable ? kit.section({ title: 'Benchmarks with published runs', count: activeBench.length, body: benchTable }) : '')-- + (idleList ? kit.fold({ title: 'Waiting for their first run', meta: String(idleRows.length), body: idleList }) : '')-- + (waitList ? (none ? kit.fold({ title: 'Jobs being rated', meta: String(waiting.length), body: waitList }) : kit.fold({ title: 'Waiting for their first results', meta: String(waiting.length), body: waitList })) : '')-- + `<p class="rp-fine">The open data is Bench's: <a href="${BENCH}/docs">its API and datasets</a> stay on bench.openvibe.services.</p>`;-- return layout({ title: 'AI', description: 'Which AI does each job best, and what it costs: OpenVibe.Bench results, rendered by OpenVibe.Report.', body, active: '/ai', path: '/ai', agent: true });--}----function aiBoard({ kindId, kind, tab, board, cost, kinds, benchmarks, rankings = [], runs, overview }) {-- const name = kind && kind.name || kindId;-- const rows = rowsOf(board);-- const o = readData(overview) || {};-- const lastByKind = byKindFromOverview(o);-- const kindLast = lastByKind.get(kindId) || null;-- const latest = readData(runs);-- const latestRows = latest && Array.isArray(latest.runs) ? latest.runs : [];-- // The registry's verified runs on this kind's public benchmarks: a second, task-level reading even while the-- // blind board has no votes. A model's numbers across the kind's benchmarks are medians, never a sum of scores.-- const kindBenchmarks = (() => {-- const b = readData(benchmarks);-- return b && Array.isArray(b.benchmarks) ? b.benchmarks.filter((x) => x.kind === kindId) : [];-- })();-- const byModel = new Map();-- kindBenchmarks.forEach((b, i) => {-- const r = rankings[i] && readData(rankings[i]);-- const entries = r && r.rankings && Array.isArray(r.rankings.entries) ? r.rankings.entries : [];-- for (const e of entries) {-- if (!byModel.has(e.model)) byModel.set(e.model, { model: e.model, scores: [], costs: [], times: [], runs: 0, last_run: null });-- const x = byModel.get(e.model);-- if (n(e.score) != null) x.scores.push(n(e.score));-- if (n(e.cost_usd) != null) x.costs.push(n(e.cost_usd));-- if (n(e.ms) != null) x.times.push(n(e.ms));-- x.runs += Number(e.runs) || 0;-- if (e.last_run && (!x.last_run || e.last_run > x.last_run)) x.last_run = e.last_run;-- }-- });-- const median = (list) => { const v = list.filter((x) => x != null).sort((a, b) => a - b); if (!v.length) return null; const m = Math.floor(v.length / 2); return v.length % 2 ? v[m] : (v[m - 1] + v[m]) / 2; };-- const registryRows = [...byModel.values()].map((x) => ({ model: x.model, score: median(x.scores), cost: median(x.costs), speed: median(x.times), runs: x.runs, last_run: x.last_run })).sort((a, b) => (b.score == null ? -1 : b.score) - (a.score == null ? -1 : a.score) || a.model.localeCompare(b.model));-- const registryTable = registryRows.length ? kit.table({ caption: 'Published runs on this kind', columns: [-- { key: 'model', label: 'Model', primary: true, html: true }, { key: 'score', label: 'Median score', num: true, html: true },-- { key: 'runs', label: 'Runs', num: true, fmt: 'int' }, { key: 'cost', label: 'Cost/run', num: true, html: true },-- { key: 'speed', label: 'Speed/run', num: true, html: true }, { key: 'last', label: 'Last run', html: true },-- ], rows: registryRows.map((r) => ({ model: modelLink(r.model), score: scoreText(r.score), runs: r.runs, cost: moneyCell(r.cost), speed: speedCell(r.speed), last: r.last_run ? esc(dayTime(r.last_run)) : '—' })) }) : '';-- 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 (!played(rows) && registryRows.length) content = kit.notice({ kind: 'info', text: `No blind vote on this task kind yet. ${registryRows.length} ${registryRows.length === 1 ? 'model has' : 'models have'} verified runs below.` });-- 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 }, { key: 'speed', label: 'Speed/run', num: true, html: true },-- ], rows: points.map((r) => ({ m: `${modelLink(r.subject)}${r.frontier ? ` ${kit.badge('frontier', 'accent')}` : ''}`, q: share(r.quality), c: usd(r.costPerSolved), speed: speedCell(r.median_ms) })) }) : '');-- } else if (tab === 'cost') {-- const crows = rowsOf(cost).length ? rowsOf(cost) : rows;-- content = kit.table({ caption: 'Cost per solved task', columns: [-- { key: 'm', label: 'Model', primary: true, html: true }, { key: 'solved', label: 'Solved', num: true, fmt: 'int' },-- { key: 'entries', label: 'Entries', num: true, fmt: 'int' }, { key: 'total', label: 'Total cost', num: true, html: true },-- { key: 'per', label: 'Cost per solved', num: true, html: true }, { key: 'speed', label: 'Speed/run', num: true, html: true },-- ], rows: crows.map((r) => ({ m: modelLink(r.subject), solved: r.solved, entries: r.entries, total: usd(r.total_cost_usd), per: usd(r.cost_per_solved), speed: speedCell(r.median_ms) })) });-- } else {-- const floor = Math.floor((Math.min(...rows.map((r) => n(r.ci_lower) ?? n(r.elo) ?? 0)) - 25) / 50) * 50;-- content = kit.rank({ label: `${name}: ranking`, base: floor, fmt: 'int', items: rows.map((r) => {-- const lo = n(r.ci_lower), hi = n(r.ci_upper), elo = n(r.elo);-- const costText = n(r.median_cost_usd) != null ? `${usd(r.median_cost_usd)}/run` : null;-- const speedText = n(r.median_ms) != null ? `${msText(r.median_ms)}/run` : null;-- return { name: r.subject, href: modelPath(r.subject), value: elo, interval: lo != null && hi != null && elo != null ? Math.round(Math.max(hi - elo, elo - lo)) : null,-- note: [n(r.games) != null ? `${exact(r.games)} games` : '', n(r.win_rate) != null ? `${share(r.win_rate)} won` : '', costText, speedText, provisional(r) ? 'provisional' : ''].filter(Boolean).join(' · ') };-- }) }) + `<p class="rp-fine">${esc(METHOD)}</p>`-- + kit.table({ caption: 'Quality, price and speed side by side', columns: [-- { key: 'm', label: 'Model', primary: true, html: true }, { key: 'q', label: 'Quality', num: true, html: true },-- { key: 'games', label: 'Games', num: true, fmt: 'int' }, { key: 'won', label: 'Won', num: true, html: true },-- { key: 'cost', label: 'Cost/run', num: true, html: true }, { key: 'speed', label: 'Speed/run', num: true, html: true },-- ], rows: rows.map((r) => ({ m: modelLink(r.subject), q: rating(r), games: r.games, won: share(r.win_rate), cost: moneyCell(r.median_cost_usd), speed: speedCell(r.median_ms) })) });-- }-- const lastRunTable = latestRows.length ? kit.table({-- caption: 'Latest runs of this kind', columns: [-- { key: 'model', label: 'Model', primary: true, html: true }, { key: 'score', label: 'Score', num: true, html: true },-- { key: 'cost', label: 'Cost', num: true, html: true }, { key: 'speed', label: 'Time', num: true, html: true },-- { key: 'when', label: 'Published', html: true }, { key: 'how', label: 'How', html: true },-- ],-- rows: latestRows.map((r) => ({ model: modelLink(r.model), score: scoreCell(r.score), cost: usd(r.cost_usd), speed: speedCell(r.ms), when: esc(dayTime(r.created_at)), how: provenanceCell(r.provenance) })),-- }) : '<p class="rp-fine">No run of this task kind has been published yet.</p>';-- const head = kit.head({ title: name, sub: kind ? `${roomName(kind.room)} · ${howRated(kind)}${kindLast ? ` · ${lastRunText(kindLast)}` : ''}` : 'Bench results', back: { href: '/ai', label: 'AI' },-- tabs: played(rows) ? kit.tabs({ base: aiPath(kindId), items: TABS, active: tab, label: 'Board view' }) : '' });-- const body = head + benchState(kinds, board, cost, benchmarks, runs, overview)-- + `<section aria-label="${esc(TABS.find((t) => t.key === tab)?.label || 'Board')}">${content}</section>`-- + (registryRows.length ? kit.section({ title: 'Verified runs on this task kind', count: registryRows.length, body: (registryRows.length > 1 ? scoreCostChart(registryRows.map((r) => ({ model: r.model, score: r.score, cost_usd: r.cost, ms: r.speed, runs: r.runs, last_run: r.last_run }))) : '') + registryTable }) : '')-- + kit.section({ title: 'Latest runs', count: latestRows.length, body: lastRunTable })-- + datasetLink(kindId);-- return layout({ title: `${name}: AI`, description: `${name}: which AI does it best, what it costs and how fast it is.`, body, active: '/ai', path: aiPath(kindId), agent: true });--}----function aiBenchmark({ benchmark, rankings, runs }) {-- const b = readData(benchmark);-- const rank = readData(rankings);-- const latest = readData(runs);-- const entries = rank && rank.rankings && Array.isArray(rank.rankings.entries) ? rank.rankings.entries : [];-- const latestRows = latest && Array.isArray(latest.runs) ? latest.runs : [];-- const meta = b && b.benchmark ? b.benchmark : null;-- const definition = b && b.definition ? b.definition : null;-- const tasks = definition && Array.isArray(definition.tasks) ? definition.tasks : [];-- const version = b && b.version ? b.version.version : null;-- const last = entries.reduce((at, e) => (!at || (e.last_run && e.last_run > at) ? e.last_run : at), null)-- || (latestRows[0] && latestRows[0].created_at) || null;-- if (!meta) {-- const body = kit.head({ title: 'Benchmark', sub: 'Bench results', back: { href: '/ai', label: 'AI' }, sticky: false })-- + benchState(benchmark) + kit.empty({ icon: 'trophy', title: 'Bench is unavailable', text: 'This benchmark comes back as soon as Bench answers.' });-- return layout({ title: 'AI benchmark', description: 'A benchmark of AI runs from OpenVibe.Bench.', body, active: '/ai', path: '/ai', agent: true });-- }-- const chart = scoreCostChart(entries);-- const leaderboard = entries.length ? kit.table({-- caption: 'Verified results', columns: [-- { key: 'model', label: 'Model', primary: true, html: true }, { key: 'score', label: 'Score', num: true, html: true },-- { key: 'runs', label: 'Runs', num: true, fmt: 'int' }, { key: 'range', label: 'Best / worst', num: true, html: true },-- { key: 'cost', label: 'Cost/run', num: true, html: true }, { key: 'speed', label: 'Speed/run', num: true, html: true },-- { key: 'solved', label: 'Cost per solved', num: true, html: true }, { key: 'last', label: 'Last run', html: true },-- ],-- rows: entries.map((e) => ({-- model: modelLink(e.model), score: scoreCell(e.score), runs: e.runs,-- range: `${scoreCell(e.max_score)} / ${scoreCell(e.min_score)}`, cost: moneyCell(e.cost_usd), speed: speedCell(e.ms),-- solved: moneyCell(e.cost_per_solved), last: e.last_run ? esc(dayTime(e.last_run)) : '—',-- })),-- }) : kit.empty({ icon: 'trophy', title: 'No verified results yet', text: 'This benchmark has no verified run yet. Submitted runs are labelled on the latest-runs list and never mixed into the ranking.' });-- const latestTable = latestRows.length ? kit.table({-- caption: 'Latest runs', columns: [-- { key: 'model', label: 'Model', primary: true, html: true }, { key: 'score', label: 'Score', num: true, html: true },-- { key: 'cost', label: 'Cost', num: true, html: true }, { key: 'speed', label: 'Time', num: true, html: true },-- { key: 'when', label: 'Published', html: true }, { key: 'how', label: 'How', html: true },-- ],-- rows: latestRows.map((r) => ({ model: modelLink(r.model), score: scoreCell(r.score), cost: usd(r.cost_usd), speed: speedCell(r.ms), when: esc(dayTime(r.created_at)), how: provenanceCell(r.provenance) })),-- }) : '<p class="rp-fine">No run has been published on this benchmark yet.</p>';-- const body = kit.head({ title: meta.title, sub: `${meta.kind || 'Benchmark'}${version ? ` · version ${version}` : ''} · ${tasks.length} ${tasks.length === 1 ? 'task' : 'tasks'} · ${lastRunText(last)}`, back: { href: '/ai', label: 'AI' }, sticky: false })-- + benchState(benchmark, rankings, runs)-- + (meta.description ? `<p class="rp-lede">${esc(meta.description)}</p>` : '')-- + kit.stats([[entries.length, entries.length === 1 ? 'model ranked' : 'models ranked'], [latestRows.length, 'latest runs'], [tasks.length, tasks.length === 1 ? 'task' : 'tasks'], [last ? dayTime(last) : 'No runs yet', 'last run']], { label: `${meta.title} in numbers` })-- + chart + leaderboard-- + kit.section({ title: 'Latest runs', count: latestRows.length, body: latestTable })-- + `<p class="rp-fine"><a href="/ai/method">How the runs are judged</a> · <a href="/ai/compare?models=${entries.slice(0, 3).map((e) => encodeURIComponent(e.model)).join(',')}">Compare the leaders</a> · <a href="${BENCH}/api/v1/datasets/benchmarks.jsonl">Bench's open data</a></p>`;-- return layout({ title: `${meta.title}: AI benchmark`, description: `${meta.title}: verified AI results, cost and speed from OpenVibe.Bench.`, body, active: '/ai', path: aiPath(meta.id), agent: true });--}----function aiModel({ model, detail, legacy }) {-- const d = readData(detail) || null;-- const l = readData(legacy) || null;-- const runs = d && Array.isArray(d.recent_runs) ? d.recent_runs : [];-- const benchmarks = d && Array.isArray(d.benchmarks) ? d.benchmarks : [];-- const tasks = d && Array.isArray(d.tasks) ? d.tasks : [];-- const ratings = l && Array.isArray(l.ratings) ? l.ratings : [];-- const entries = l && Array.isArray(l.entries) ? l.entries : [];-- const hasRuns = Boolean(d && d.runs > 0);-- const history = runs.filter((r) => n(r.score) != null && ts(r.created_at) != null).sort((a, b) => String(a.created_at).localeCompare(String(b.created_at)));-- const historyChart = history.length >= 2 ? kit.chart({-- type: 'line', title: 'Score over time', note: 'Every public run, newest last.',-- x: { time: true, label: 'Published' }, y: { label: 'Score (0–100)', zero: false, fmt: (v) => `${Math.round(v)}` },-- series: [{ name: model, points: history.map((r) => [r.created_at, n(r.score)]) }], height: 220,-- }) : '';-- const best = tasks.slice(0, 5);-- const worst = tasks.length > 5 ? tasks.slice(-5).reverse() : [];-- const taskRow = (t) => ({-- task: `<span title="${esc(t.benchmark_title)}">${esc(t.task_id)}</span>`,-- benchmark: `<a href="${aiPath(t.benchmark_id)}">${esc(t.benchmark_title)}</a>`,-- score: scoreCell(t.score), runs: t.runs, last: t.last_run ? esc(dayTime(t.last_run)) : '—',-- });-- const taskColumns = [-- { key: 'task', label: 'Task', primary: true, html: true }, { key: 'benchmark', label: 'Benchmark', html: true },-- { key: 'score', label: 'Median score', num: true, html: true }, { key: 'runs', label: 'Runs', num: true, fmt: 'int' },-- { key: 'last', label: 'Last run', html: true },-- ];-- const taskTable = best.length ? kit.table({ caption: 'Best tasks', columns: taskColumns, rows: best.map(taskRow) }) : '';-- const worstTable = worst.length ? kit.table({ caption: 'Worst tasks', columns: taskColumns, rows: worst.map(taskRow) }) : '';-- const benchmarkRows = benchmarks.map((b) => ({-- benchmark: `<a href="${aiPath(b.benchmark_id)}">${esc(b.title)}</a>`, kind: esc(b.kind || '—'), runs: b.runs,-- score: scoreCell(b.score), range: `${scoreCell(b.best)} / ${scoreCell(b.worst)}`, cost: moneyCell(b.cost_usd), speed: speedCell(b.ms), last: b.last_run ? esc(dayTime(b.last_run)) : '—',-- }));-- const runTitles = new Map(benchmarks.map((b) => [b.benchmark_id, b.title]));-- const runRows = runs.slice(0, 12).map((r) => ({-- benchmark: `<a href="${aiPath(r.benchmark_id)}">${esc(runTitles.get(r.benchmark_id) || r.benchmark_id)}</a>`, score: scoreCell(r.score), cost: usd(r.cost_usd), speed: speedCell(r.ms),-- when: esc(dayTime(r.created_at)), how: provenanceCell(r.provenance),-- }));-- const legacyRows = ratings.map((r) => ({ job: `<a href="${aiPath(r.kind_id)}">${esc(r.kind_id)}</a>`, rating: rating(r), games: r.games, won: share(r.win_rate) }));-- const body = kit.head({ title: model, sub: hasRuns ? `${exact(d.runs)} verified ${d.runs === 1 ? 'run' : 'runs'} · ${lastRunText(d.last_run)}` : 'No verified runs yet', back: { href: '/ai', label: 'AI' }, sticky: false })-- + benchState(detail, legacy)-- + (hasRuns ? kit.stats([-- [exact(d.runs), d.runs === 1 ? 'run' : 'runs'], [benchmarks.length, benchmarks.length === 1 ? 'benchmark' : 'benchmarks'],-- [d.best_task ? scoreText(d.best_task.score) : '—', 'best task'], [d.worst_task ? scoreText(d.worst_task.score) : '—', 'worst task'],-- [d.total_cost_usd == null ? '—' : usd(d.total_cost_usd), 'spent on runs'], [d.last_run ? dayTime(d.last_run) : 'No runs yet', 'last run'],-- ], { label: `${model} in numbers` }) : '')-- + (historyChart ? kit.section({ title: 'Runs over time', body: historyChart }) : '')-- + (benchmarkRows.length ? kit.section({ title: 'By benchmark', count: benchmarks.length, body: kit.table({ caption: 'By benchmark', columns: [-- { key: 'benchmark', label: 'Benchmark', primary: true, html: true }, { key: 'kind', label: 'Task kind' }, { key: 'runs', label: 'Runs', num: true, fmt: 'int' },-- { key: 'score', label: 'Score', num: true, html: true }, { key: 'range', label: 'Best / worst', num: true, html: true }, { key: 'cost', label: 'Cost/run', num: true, html: true },-- { key: 'speed', label: 'Speed/run', num: true, html: true }, { key: 'last', label: 'Last run', html: true },-- ], rows: benchmarkRows }) }) : '')-- + (taskTable ? kit.section({ title: 'Best and worst tasks', count: best.length + worst.length, body: taskTable + worstTable }) : '')-- + (runRows.length ? kit.section({ title: 'Recent runs', count: runRows.length, body: kit.table({ caption: 'Recent runs', columns: [-- { key: 'benchmark', label: 'Benchmark', primary: true, html: true }, { key: 'score', label: 'Score', num: true, html: true },-- { key: 'cost', label: 'Cost', num: true, html: true }, { key: 'speed', label: 'Time', num: true, html: true }, { key: 'when', label: 'Published', html: true }, { key: 'how', label: 'How', html: true },-- ], rows: runRows }) }) : '')-- + (!hasRuns ? kit.empty({ icon: 'trophy', title: 'No verified runs yet', text: 'This model appears on a benchmark once a verified run is published. Its blind-board ratings, when it has any, are below.' }) : '')-- + (legacyRows.length ? kit.section({ title: 'Blind-board ratings', count: legacyRows.length, body: kit.table({ caption: 'Ratings by job', columns: [-- { key: 'job', label: 'Job', primary: true, html: true }, { key: 'rating', label: 'Rating', num: true, html: true }, { key: 'games', label: 'Games', num: true, fmt: 'int' }, { key: 'won', label: 'Won', num: true, html: true },-- ], rows: legacyRows }) }) : '')-- + (entries.length && !hasRuns ? `<p class="rp-fine">${exact(entries.length)} ${entries.length === 1 ? 'entry' : 'entries'} on Bench’s blind boards.</p>` : '')-- + `<p class="rp-fine"><a href="/ai/compare?models=${encodeURIComponent(model)}">Compare ${esc(model)} with another model</a> · <a href="/ai/method">How the runs are judged</a> · <a href="${BENCH}/api/v1/models/${encodeURIComponent(model)}">Bench’s own record</a></p>`;-- return layout({ title: `${model}: AI model`, description: `${model} across OpenVibe benchmark jobs: score, cost, speed, runs over time and its best and worst tasks.`, body, active: '/ai', path: modelPath(model), agent: true });--}----function aiCompare({ models = [], details = [], legacy = [], errors = [] }) {-- const rowsByBenchmark = new Map();-- const names = [];-- const cards = [];-- models.forEach((model, i) => {-- const d = readData(details[i]) || null;-- const l = readData(legacy[i]) || null;-- names.push(model);-- const list = d && Array.isArray(d.benchmarks) ? d.benchmarks : [];-- for (const b of list) {-- if (!rowsByBenchmark.has(b.benchmark_id)) rowsByBenchmark.set(b.benchmark_id, { title: b.title, cells: new Map() });-- rowsByBenchmark.get(b.benchmark_id).cells.set(model, b);-- }-- cards.push({-- model, runs: d ? d.runs : 0, cost: d ? d.total_cost_usd : null, last: d ? d.last_run : null,-- ratingCount: l && Array.isArray(l.ratings) ? l.ratings.length : 0,-- });-- });-- const rows = [...rowsByBenchmark.entries()].map(([id, x]) => {-- const row = { benchmark: `<a href="${aiPath(id)}">${esc(x.title)}</a>` };-- for (const model of models) {-- const b = x.cells.get(model);-- row[model] = b ? `<b>${scoreCell(b.score)}</b> <small>${esc(usd(b.cost_usd))}/run · ${esc(msText(b.ms))} · ${exact(b.runs)} runs</small>` : '—';-- }-- return row;-- });-- const columns = [{ key: 'benchmark', label: 'Benchmark', primary: true, html: true }, ...models.map((m) => ({ key: m, label: m, html: true }))];-- const form = `<form class="rp-ai-compare-form" method="get" action="/ai/compare" role="search" aria-label="Compare models">--<label for="rp-compare-models">Two or three model ids, comma separated</label>--<input id="rp-compare-models" name="models" value="${esc(models.join(', '))}" placeholder="gpt-5, claude-opus-4" autocomplete="off">--<button class="rp-btn" type="submit">Compare</button></form>`;-- const cardHtml = cards.length ? kit.stats(cards.map((c) => [`${c.runs} · ${c.cost == null ? '—' : usd(c.cost)} · ${c.last ? day(c.last) : 'no runs'}`, c.model]), { label: 'Models in this comparison' }) : '';-- const body = kit.head({ title: 'Compare AI models', sub: 'The same benchmarks side by side: score, cost per run, speed and the last run', back: { href: '/ai', label: 'AI' }, sticky: false })-- + benchState(...details, ...legacy)-- + form-- + (errors.length ? kit.notice({ kind: 'warn', text: errors.join(' ') }) : '')-- + (models.length < 2 ? kit.empty({ icon: 'trophy', title: 'Pick two or three models', text: 'Put their Bench ids in the box above; the comparison is built from their published runs.' }) : '')-- + (rows.length ? kit.section({ title: 'Side by side', body: kit.table({ caption: 'Models side by side', columns, rows }) }) : (models.length >= 2 ? kit.empty({ icon: 'trophy', title: 'No shared benchmarks yet', text: 'These models have no verified run on a public benchmark in common.' }) : ''))-- + (cardHtml ? `<p class="rp-fine">${esc('Each card: verified runs · total run cost · last run, then the model.')}</p>${cardHtml}` : '')-- + `<p class="rp-fine">Cost and speed are the medians of each model's runs on that benchmark; a score is the median of its run scores. <a href="/ai/method">How the runs are judged</a>.</p>`;-- return layout({ title: 'Compare AI models', description: 'Compare two or three AI models on the same OpenVibe benchmarks: score, cost and speed.', body, active: '/ai', path: '/ai/compare', agent: true });--}----function aiMethod({ overview, kinds }) {-- const o = readData(overview) || {};-- const list = kinds && kinds.data && Array.isArray(kinds.data.kinds) ? kinds.data.kinds : [];-- const byJudge = new Map();-- for (const k of list) {-- if (!byJudge.has(k.judge)) byJudge.set(k.judge, []);-- byJudge.get(k.judge).push(k);-- }-- const judgeRows = [...byJudge.entries()].map(([judge, ks]) => ({-- how: `<b>${esc(JUDGE_SHORT[judge] || judge)}</b>`, judge: esc(judge), jobs: esc(ks.map((k) => k.name || k.id).join(', ')),-- }));-- const body = kit.head({ title: 'How the AI results are judged', sub: 'The method behind every score, price and speed on these pages', back: { href: '/ai', label: 'AI' }, sticky: false })-- + benchState(overview, kinds)-- + `<section class="rp-ai-method"><h2>What a score means</h2>--<p>A score is a run’s result on one benchmark, from 0 to 100. Bench’s frozen benchmark version names its tasks, its judges and how the task scores combine; the default is the mean of the task scores. A model’s score on a benchmark is the median of its <b>verified</b> runs, so one bad run does not decide the page. The best and worst columns are the runs themselves, not a second estimate.</p>--<h2>Quality, price and speed</h2>--<p><b>Quality</b> is that score. <b>Price</b> is the median cost of a run in US dollars, as the service that ran it published it; <b>cost per solved</b> is the run’s cost divided by its success rate (score ÷ 100), so it is the price of one solved task. <b>Cost per solved</b> is the same figure, named plainly. <b>Speed</b> is the median wall-clock time of a run. The quality-against-cost chart marks the frontier: the models nothing beats on both.</p>--<h2>How runs are judged</h2>--<p>Each task kind has one main judge. A benchmark may also name more judges in its definition; the exact list is in its public definition at <code>/api/v1/benchmarks/:id</code>.</p>--${judgeRows.length ? kit.table({ caption: 'Judges in use', columns: [{ key: 'how', label: 'How', html: true }, { key: 'judge', label: 'Bench judge' }, { key: 'jobs', label: 'Task kinds' }], rows: judgeRows }) : `<p class="rp-fine">${esc('Bench did not answer its kind list; the judge rules above are its published contract.')}</p>`}--<h2>Verified and submitted</h2>--<p>A <b>verified</b> run was executed by a service (openvibe.codes, Run, a Node worker); a <b>submitted</b> run was posted with a person’s token. The rankings read verified runs only, and a submitted run is labelled wherever the latest runs are listed.</p>--<h2>How often, and what is missing</h2>--<p>${o.runs == null ? 'Bench did not answer the public counts.' : `${exact(o.benchmarks)} public ${o.benchmarks === 1 ? 'benchmark' : 'benchmarks'}, ${exact(o.tasks)} ${o.tasks === 1 ? 'task' : 'tasks'}, ${exact(o.runs)} ${o.runs === 1 ? 'run' : 'runs'} from ${exact(o.models)} ${o.models === 1 ? 'model' : 'models'}; the newest run was ${o.last_run ? esc(dayTime(o.last_run)) : 'never published'}.`}</p>--<p class="rp-fine">A model with no verified run has no score here; a benchmark with no run says so. The public reads are Bench’s API at <a href="${BENCH}/docs">bench.openvibe.services/docs</a>; the open data is <a href="${BENCH}/api/v1/datasets/benchmarks.jsonl">benchmarks.jsonl</a> and the per-kind datasets. <a href="/ai/feed.xml">New results (RSS)</a> · <a href="/api/v1/ai/results">the same as JSON</a>.</p>`;-- return layout({ title: 'How the AI results are judged', description: 'The method behind OpenVibe.Bench’s scores, costs and speeds, and how to read them.', body, active: '/ai', path: '/ai/method', agent: true });--}--// ── The AI Exchange (plan G-P9): Billing's settlements, summed by day, model and access ────────────────────────// Money is integer nano-USD and stays exact: this formatter works in BigInt, never in floats, and the title carries// the exact nano-USD the authority sent.@@@ -1468,4 +1119,4 @@ function notFound() {return layout({ title: 'Not found', robots: 'noindex, follow', body: `<header class="rp-page-h"><h1>Nothing here</h1><p class="rp-lede">That page does not exist, or its source has nothing for it. <a href="/">Back to Report</a>.</p></header>` });}--module.exports = { home, stats, rundown, sessionReport, sharingChip, topic, episode, speaker, recap, channel, you, youSignedOut, me, meSignedOut, meError, publicStats, publicStatsPrivate, developers, aiIndex, aiBoard, aiBenchmark, aiModel, aiCompare, aiMethod, aiExchange, staffOnly, usdNano, paretoRows, paretoChart, notFound, layout, setRelease, configure };++module.exports = { home, stats, rundown, sessionReport, sharingChip, topic, episode, speaker, recap, channel, you, youSignedOut, me, meSignedOut, meError, publicStats, publicStatsPrivate, developers, aiExchange, staffOnly, usdNano, paretoRows, paretoChart, notFound, layout, setRelease, configure };@@@ -27,36 +27,34 @@ const { boot, check, done, stubFetch, jsonResponse } = require('./helpers/boot')};const t = await boot({ fetchImpl, env: { OV_BENCH_INTERNAL_URL: 'http://127.0.0.1:5230', OV_AI_INTERNAL_URL: 'http://127.0.0.1:5231' } });try {-- await check('a dated Bench name meets its catalogue model: maker and prices', async () => {-- const r = await t.get('/ai/leaderboard');++ // The pages moved to OpenVibe.Compare (2026-11-13); the join is checked through the JSON API Compare reads.++ const rowsOf = async () => {++ const r = await t.get('/api/v1/ai/leaderboard?limit=100');assert.strictEqual(r.status, 200);-- const row = r.text.slice(r.text.indexOf('gpt-5-nano-2025-08-07'));-- assert.ok(r.text.includes('gpt-5-nano-2025-08-07'), 'the rated model');-- assert.ok(row.includes('$0.05') && row.includes('$0.4'), 'its catalogue prices');-- assert.ok(r.text.includes('OpenAI') && r.text.includes('ov-aim--openai'), 'its maker, named and marked');++ return r.json().rows;++ };++ const prices = (row) => row.provider_offers.flatMap((o) => (o.price ? [o.price.in, o.price.out] : [])).filter((x) => x != null).map(Number);++ await check('a dated Bench name meets its catalogue model: maker and prices', async () => {++ const row = (await rowsOf()).find((x) => x.id === 'gpt-5-nano-2025-08-07');++ assert.ok(row, 'the rated model');++ assert.ok(prices(row).includes(0.4) && prices(row).includes(0.05), 'its catalogue prices');++ assert.match(String(row.maker), /openai/i, 'its maker');});await check('a catalogue model without Bench games is listed, not rated', async () => {-- const r = await t.get('/ai/leaderboard');-- assert.ok(r.text.includes('deepseek/deepseek-chat'), 'the unrated model is listed');-- assert.ok(r.text.indexOf('gpt-5-nano-2025-08-07') < r.text.indexOf('deepseek/deepseek-chat'), 'rated models come first');-- assert.ok(r.text.includes('$0.28'), 'with its real price');++ const rows = await rowsOf();++ const ids = rows.map((x) => x.id);++ assert.ok(ids.includes('deepseek/deepseek-chat'), 'the unrated model is listed');++ assert.ok(ids.indexOf('gpt-5-nano-2025-08-07') < ids.indexOf('deepseek/deepseek-chat'), 'rated models come first');++ assert.ok(prices(rows.find((x) => x.id === 'deepseek/deepseek-chat')).includes(0.28), 'with its real price');});await check('a base rating with no games is not a rating', async () => {-- const r = await t.get('/api/v1/ai/leaderboard');-- const zero = r.json().rows.find((x) => x.id === 'model-zero');++ const zero = (await rowsOf()).find((x) => x.id === 'model-zero');assert.strictEqual(zero, undefined, 'a sparse Bench-only subject is withheld, including its count');-- const page = await t.get('/ai/models/model-zero');-- assert.ok(page.text.includes('Not rated on any task yet'), 'the model page says so');-- });-- await check('unrated models list what the Exchange routes first, then the cheapest; a note in brackets is not a name', async () => {-- const views = require('../server/views/ai-comparison');-- const html = views.providers({ providers: [{ provider: 'x', provider_name: 'Abliteration.ai (the uncensored tier)', model: 'm', price: { in: 0, out: 0 }, routed: false }] });-- assert.ok(html.includes('Abliteration.ai<') && !html.includes('the uncensored tier'), 'the provider is named plainly');-- assert.ok(html.includes('Free') && !html.includes('$0.00'), 'a price of zero reads Free');});-- await check('the providers page lists every offer', async () => {-- const r = await t.get('/ai/providers');-- assert.ok(r.text.includes('OpenAI') && r.text.includes('DeepSeek'), 'both providers');++ await check('the AI pages answer 301 to OpenVibe.Compare with their path and query', async () => {++ const r = await t.get('/ai/providers?x=1', { redirect: 'manual' });++ assert.strictEqual(r.status, 301);++ assert.strictEqual(r.headers.get('location'), 'https://openvibe.compare/ai/providers?x=1');});} finally {await t.close();@@@ -47,11 +47,9 @@ const catalogue = { models:[{ slug:'openai/gpt-5-nano', offers:[{provider:'openareturn source(url,opts);}});try {-- await check('pages list every market model, cheapest price, route marks and measured-only routed rows',async()=>{-- const r=await t.get('/ai/leaderboard');assert.strictEqual(r.status,200);assert(r.text.includes('qwen/unrated'));assert(r.text.includes('$0.01'));assert(r.text.includes('Routed here'));assert(r.text.includes('Not routed here'));-- assert(r.text.indexOf('gpt-5-nano-2025-08-07')<r.text.indexOf('qwen/unrated'));-- const p=await t.get('/ai/providers');assert(p.text.includes('Cheaper fixture'));assert(/1\.2[0-9]*\s*s/.test(p.text));assert(p.text.includes('Not routed here'));-- const api=await t.get('/api/v1/ai/leaderboard');assert(api.json().rows.some(r=>r.id==='qwen/unrated'&&!r.routed));++ await check('the API lists every market model, routed and not, rated first',async()=>{++ const api=await t.get('/api/v1/ai/leaderboard?limit=100');assert.strictEqual(api.status,200);const rows=api.json().rows;const ids=rows.map(r=>r.id);++ assert(rows.some(r=>r.id==='qwen/unrated'&&!r.routed),'the unrouted market model is listed');assert(ids.includes('gpt-5-nano-2025-08-07'),'the rated one too');});} finally { await t.close(); }done();nnew file mode 100644@@@ -0,0 +1,41 @@++'use strict';++/**++ * The AI model comparison moved to OpenVibe.Compare on 2026-11-13 (the owner: "This kinda shit is supposed to be a part++ * of openvibe.compare"). Every /ai page answers 301 there with its path and query; the Arena's rounds and votes and the++ * staff page /ai/exchange stay; the bar has no AI tab; the sitemap lists no /ai page; the JSON API Compare reads stays.++ */++const assert = require('assert');++const { boot, check, done } = require('./helpers/boot');++++(async () => {++ const t = await boot();++ try {++ await check('every /ai page answers 301 to the same path on OpenVibe.Compare', async () => {++ for (const path of ['/ai', '/ai/leaderboard?kind=chat.arena', '/ai/providers', '/ai/models/openai%2Fgpt-5-nano', '/ai/compare/a-vs-b',++ '/ai/compare?a=x&b=y', '/ai/method', '/ai/feed.xml', '/ai/chat.arena', '/ai/bmk_abc']) {++ const r = await t.get(path);++ assert.strictEqual(r.status, 301, path);++ assert.strictEqual(r.headers.get('location'), `https://openvibe.compare${path}`, path);++ }++ });++ await check('the Arena and the staff page stay on Report', async () => {++ const arena = await t.get('/ai/arena');++ assert.notStrictEqual(arena.status, 301, 'the Arena is served here');++ const staff = await t.get('/ai/exchange');++ assert.notStrictEqual(staff.status, 301, 'the staff page is not moved');++ });++ await check('no AI tab, no /ai page in the sitemap, the JSON API stays', async () => {++ const home = await t.get('/');++ assert.ok(!/href="\/ai"/.test(home.text), 'the bar has no AI tab');++ const map = await t.get('/sitemap.xml');++ assert.ok(!/openvibe\.report\/ai[/<]/.test(map.text), 'no /ai page in the sitemap');++ const llms = await t.get('/llms.txt');++ assert.match(llms.text, /https:\/\/openvibe\.compare\/ai/, 'llms.txt names where the comparison lives');++ const api = await t.get('/api/v1/ai/picks');++ assert.notStrictEqual(api.status, 301, 'the API is not moved');++ });++ } finally {++ await t.close();++ }++ done();++})().catch((error) => { console.error(error); process.exitCode = 1; });ddeleted file mode 100644@@@ -1,187 +0,0 @@--'use strict';----const assert = require('assert');--const { boot, check, done, stubFetch, jsonResponse } = require('./helpers/boot');----(async () => {-- let time = Date.parse('2026-10-28T12:00:00Z');-- let down = false;-- const source = stubFetch();-- const calls = [];-- const fetchImpl = async (url, opts) => {-- const u = new URL(String(url));-- if (u.port !== '5230') return source(url, opts);-- calls.push(u.pathname + u.search);-- if (down) throw new Error('Bench offline');-- if (u.pathname === '/api/v1/kinds') return jsonResponse({ count: 1, kinds: [{ id: 'design.page', room: 'design', name: 'Page <design>', judge: 'human_pairwise' }] });-- if (['/api/v1/boards/design.page', '/api/v1/ratings/design.page'].includes(u.pathname)) {-- const type = u.pathname.includes('/ratings/') ? 'elo' : u.searchParams.get('type');-- const rows = {-- elo: [-- { subject: 'openai/gpt-4o', elo: 1043, ci_lower: 1005, ci_upper: 1081, games: 20, win_rate: 0.75, provisional: false },-- { subject: 'model-b', elo: 980, ci_lower: 940, ci_upper: 1020, games: 20, win_rate: 0.25, provisional: false },-- ],-- pareto: [-- { subject: 'openai/gpt-4o', quality: 0.8 },-- { subject: 'model-b', quality: 0.6 },-- ],-- cost: [-- { subject: 'model-b', entries: 8, solved: 6, total_cost_usd: 0.3, cost_per_solved: 0.05 },-- { subject: 'openai/gpt-4o', entries: 8, solved: 8, total_cost_usd: 0.8, cost_per_solved: 0.1 },-- ],-- }[type];-- return rows ? jsonResponse({ board: { kind_id: 'design.page', type, rows } }) : jsonResponse({}, 404);-- }-- if (u.pathname === '/api/v1/models/openai%2Fgpt-4o') return jsonResponse({ model: 'openai/gpt-4o', ratings: [{ kind_id: 'design.page', elo: 1043, ci_lower: 1005, ci_upper: 1081, games: 20, win_rate: 0.75 }], entries: [{ kind_id: 'design.page' }], total_cost_usd: 0.8 });-- return jsonResponse({}, 404);-- };-- const t = await boot({ fetchImpl, now: () => time, env: { OV_BENCH_INTERNAL_URL: 'http://127.0.0.1:5230' } });-- try {-- await check('/ai renders the comparison hub and structured data', async () => {-- const r = await t.get('/ai');-- assert.strictEqual(r.status, 200);-- for (const s of ['AI model leaderboard', 'openai/gpt-4o', 'Best overall', 'Dataset', 'FAQPage']) assert.ok(r.text.includes(s), s);-- assert.ok(!r.text.includes('Page <design>'));-- // A phone shows the parts below the top picks as tabs, the leaderboard first; every part stays in the HTML.-- assert.ok(r.text.includes('<div class="ov-parts" data-parts='), 'the parts are tabs on a phone');-- assert.ok(r.text.includes('<label for="ovp-board-leaderboard">Leaderboard <span class="ov-parts-n">'), 'the leaderboard tab names its count');-- assert.ok(r.text.indexOf('ovp-board-leaderboard') < r.text.indexOf('ovp-board-questions'), 'the leaderboard first, the questions last');-- assert.ok(r.text.includes('.ov-parts-bar{display:flex}'), 'its look is in the page, so nothing jumps');-- });---- await check('Shared Elo shows ranks, intervals and win rates', async () => {-- const r = await t.get('/ai/design.page');-- assert.strictEqual(r.status, 200);-- for (const s of ['<ol class="ovk-rank"', '<b>1,043</b><small>± 38</small>', '75% won', 'Ranking', '95% interval', 'dataset JSONL']) assert.ok(r.text.includes(s), s);-- const data = await t.get('/api/v1/ai/ratings/design.page');-- assert.strictEqual(data.status, 200, data.text);-- assert.strictEqual(data.json().board.rows[0].elo, 1043);-- assert.strictEqual(data.json().board.rows[0].games, 20);-- assert.strictEqual(data.headers.get('access-control-allow-origin'), '*');-- const preflight = await t.get('/api/v1/ai/ratings/design.page', { method: 'OPTIONS' });-- assert.strictEqual(preflight.status, 204);-- assert.match(preflight.headers.get('access-control-allow-headers'), /traceparent/);-- });---- await check('Pareto has an inline SVG, frontier labels and an exact-value table', async () => {-- const r = await t.get('/ai/design.page?tab=pareto');-- assert.strictEqual(r.status, 200);-- for (const s of ['ovk-chart--scatter', '<svg', 'class="ovk-chart-front"', 'Quality against cost per solved task', 'Cost per solved task (USD', 'frontier', 'model-b', 'dataset JSONL', '<table']) assert.ok(r.text.includes(s), s);-- assert.ok(r.text.includes('data-label="Quality">80%</td>') && r.text.includes('data-label="Cost per solved">$0.10</td>'), 'the exact numbers, not dashes');-- });---- await check('cost and model views quote Bench numbers', async () => {-- const cost = await t.get('/ai/design.page?tab=cost');-- assert.strictEqual(cost.status, 200);-- assert.ok(cost.text.includes('Cost per solved'));-- assert.ok(cost.text.includes('$0.05'));-- const model = await t.get('/ai/models/openai%2Fgpt-4o');-- assert.strictEqual(model.status, 200);-- assert.ok(model.text.includes('design.page'));-- assert.ok(model.text.includes('1043') && model.text.includes('1005–1081'));-- });---- await check('invalid kinds and models do not call Bench', async () => {-- const before = calls.length;-- assert.strictEqual((await t.get('/ai/bad!kind')).status, 404);-- assert.strictEqual((await t.get('/ai/models/bad!model')).status, 404);-- assert.strictEqual(calls.length, before);-- });---- await check('sitemap includes AI index and every kind', async () => {-- const r = await t.get('/sitemap.xml');-- assert.strictEqual(r.status, 200);-- assert.ok(r.text.includes('<loc>https://openvibe.report/ai</loc>'));-- assert.ok(r.text.includes('<loc>https://openvibe.report/ai/design.page</loc>'));-- });---- await check('the comparison pages remain rendered during a Bench outage', async () => {-- down = true;-- time += 60_001;-- for (const path of ['/ai', '/ai/design.page', '/ai/design.page?tab=pareto', '/ai/models/openai%2Fgpt-4o']) {-- const r = await t.get(path);-- assert.strictEqual(r.status, 200, path);-- }-- });-- } finally { await t.close(); }---- await check('the comparison hub replaces the old AI landing page', async () => {-- const fresh = 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: 2, kinds: [{ id: 'design.page', room: 'design', name: 'Page', judge: 'human_pairwise' }, { id: 'speech.asr', room: 'speech', name: 'Speech', judge: 'automatic_grader' }] });-- if ((/^\/api\/v1\/(boards|ratings)\//.test(u.pathname))) return jsonResponse({ board: { rows: [] } });-- return jsonResponse({}, 404);-- },-- env: { OV_BENCH_INTERNAL_URL: 'http://127.0.0.1:5230' },-- });-- try {-- const r = await fresh.get('/ai');-- assert.ok(r.text.includes('AI model leaderboard') && r.text.includes('Best overall') && r.text.includes('FAQPage'), 'new hub and structured data render');-- assert.ok(!r.text.includes('No model is rated on a task kind yet'), 'old landing page copy is gone');-- const b = await fresh.get('/ai/design.page');-- assert.ok(b.text.includes('No votes yet') && b.text.includes('href="https://openvibe.design/"') && b.text.includes('Vote on Design'), 'a blind-vote job sends people to vote');-- assert.ok(!b.text.includes('class="ovk-tabs'), 'an empty board has no tabs');-- const g = await fresh.get('/ai/speech.asr');-- assert.ok(g.text.includes('No results yet') && !g.text.includes('Vote on'), 'a graded job has nothing to vote on');-- } finally { await fresh.close(); }-- });---- await check('an unjudged model does not become a rated leader', 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 ((/^\/api\/v1\/(boards|ratings)\//.test(u.pathname))) 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('AI model leaderboard') && r.text.includes('Best overall'), 'hub remains available without a rating');-- 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 {-- const r = await empty.get('/ai');-- assert.strictEqual(r.status, 200);-- assert.ok(r.text.includes('AI model leaderboard') && r.text.includes('Leaderboard'));-- } finally { await empty.close(); }-- });---- await check('comparison pages show fixture measurements, filter parameters and canonical model pairs', async () => {-- const fixture = await boot({-- aiCatalogue: { catalog: async () => ({ generated_at: '2026-11-09T00:00:00Z', models: [{ slug: 'maker/model-a', maker: 'Maker A', weights: 'open', status: 'current', offers: [{ provider: 'Provider One', price: { in: 1, out: 0.5 }, speed: { p50_ms: 120 }, uptime: 0.99, status: 'down', status_reason: 'the provider says the account is out of credit', status_since: '2026-11-09T09:00:00Z' }] }, { slug: 'maker/model-b', maker: 'Maker B', weights: 'closed', status: 'current', offers: [] }] }) },-- fetchImpl: async (url, opts) => {-- const u = new URL(String(url));-- if (u.port === '5230' && u.pathname === '/api/v1/kinds') return jsonResponse({ kinds: [{ id: 'code', name: 'Code' }] });-- if (u.port === '5230' && u.pathname === '/api/v1/ratings/code') return jsonResponse({ board: { rows: [{ subject: 'maker/model-a', elo: 1250, ci_lower: 1200, ci_upper: 1300, games: 40 }, { subject: 'maker/model-b', elo: 1270, ci_lower: 1250, ci_upper: 1300, games: 20 }] } });-- if (u.port === '5230' && u.pathname.includes('/summary')) return jsonResponse({ model: 'maker/model-a', runs: 4 });-- return source(url, opts);-- }, env: { OV_BENCH_INTERNAL_URL: 'http://127.0.0.1:5230' },-- });-- try {-- const hub = await fixture.get('/ai');-- assert.ok(hub.text.includes('AI model leaderboard — November 2026'));-- assert.ok(hub.text.includes('± 50') && hub.text.includes('40 games') && hub.text.includes('1–2') && hub.text.includes('Dataset') && hub.text.includes('FAQPage'));-- assert.ok(hub.text.includes('Maker A') && !hub.text.includes('letter-avatar'));-- const filtered = await fixture.get('/ai/leaderboard?maker=Maker%20A&weights=open&price=low&status=current&kind=code');-- assert.ok(filtered.text.includes('maker/model-a') && !filtered.text.includes('maker/model-b'));-- const provider = await fixture.get('/ai/providers');-- assert.ok(provider.text.includes('Provider One') && provider.text.includes('Not measured yet'), 'an offer without requests has no measurement');-- assert.ok(provider.text.includes('Down') && provider.text.includes('the provider says the account is out of credit') && provider.text.includes('since 2026-11-09 09:00 UTC'), 'the measured status and its reason are shown');-- assert.strictEqual((await fixture.get('/ai/compare?models=maker%2Fmodel-a,maker%2Fmodel-b')).status, 301);-- assert.strictEqual((await fixture.get('/ai/compare/maker%2Fmodel-a-vs-maker%2Fmodel-b')).status, 200);-- assert.strictEqual((await fixture.get('/ai/compare/maker%2Fmodel-b-vs-maker%2Fmodel-a')).status, 301);-- } finally { await fixture.close(); }-- });---- done();--})().catch((e) => { console.error(e); process.exit(1); });ddeleted file mode 100644@@@ -1,164 +0,0 @@--'use strict';----/**-- * The AI section (owner, 2026-11-08): Bench's pages moved into Report. The overview, one task kind, one benchmark,-- * one model, the comparison, the method page and the new-results feeds read only Bench's public answers.-- */--const assert = require('assert');--const { boot, check, done, stubFetch, jsonResponse } = require('./helpers/boot');----const BMK = 'bmk_01M65KA3H4Y4ZENBYYZQA8SRNN';--const RUN_A = 'brn_01M65KA3H4Y4ZENBYYZQA8SRN1';--const RUN_B = 'brn_01M65KA3H4Y4ZENBYYZQA8SRN2';----const overview = {-- kinds: 2, benchmarks: 1, tasks: 2, runs: 3, models: 2, last_run: '2026-11-07T10:00:00.000Z',-- list: [{ id: BMK, title: 'Playable games arena', kind: 'design.page', description: 'A test benchmark.', latest_version: 1, runs: 3, last_run: '2026-11-07T10:00:00.000Z' }],-- recent: [-- { id: RUN_B, benchmark_id: BMK, model: 'model-b', score: 70, cost_usd: 0.04, ms: 2000, provenance: 'verified', created_at: '2026-11-07T10:00:00.000Z' },-- { id: RUN_A, benchmark_id: BMK, model: 'model-a', score: 90, cost_usd: 0.02, ms: 1000, provenance: 'verified', created_at: '2026-11-06T10:00:00.000Z' },-- ],--};----const runs = {-- count: 2, offset: 0, last_run: '2026-11-07T10:00:00.000Z',-- runs: [-- { id: RUN_B, benchmark_id: BMK, model: 'model-b', score: 70, cost_usd: 0.04, ms: 2000, tokens: 20, provenance: 'verified', created_at: '2026-11-07T10:00:00.000Z' },-- { id: RUN_A, benchmark_id: BMK, model: 'model-a', score: 90, cost_usd: 0.02, ms: 1000, tokens: 10, provenance: 'verified', created_at: '2026-11-06T10:00:00.000Z' },-- ],--};----const rankings = { rankings: { benchmark: BMK, metric: 'score', entries: [-- { model: 'model-a', runs: 2, score: 85, cost_usd: 0.02, ms: 1000, cost_per_solved: 0.024, min_score: 80, max_score: 90, last_run: '2026-11-06T10:00:00.000Z' },-- { model: 'model-b', runs: 1, score: 70, cost_usd: 0.04, ms: 2000, cost_per_solved: 0.057, min_score: 70, max_score: 70, last_run: '2026-11-07T10:00:00.000Z' },--] } };----const benchmark = {-- benchmark: { id: BMK, title: 'Playable games arena', kind: 'design.page', description: 'A test benchmark.' },-- version: { version: 1, created_at: '2026-10-30T05:07:52.000Z' },-- definition: { tasks: [{ id: 't1', prompt: 'Build one screen.' }, { id: 't2', prompt: 'Build another.' }] },-- task_meta: [],--};----const modelDetail = {-- model: 'model-a', runs: 2,-- benchmarks: [{ benchmark_id: BMK, title: 'Playable games arena', kind: 'design.page', runs: 2, score: 85, best: 90, worst: 80, cost_usd: 0.02, ms: 1000, last_run: '2026-11-06T10:00:00.000Z' }],-- tasks: [-- { benchmark_id: BMK, benchmark_title: 'Playable games arena', task_id: 't1', runs: 2, score: 90, best: 90, worst: 90, last_run: '2026-11-06T10:00:00.000Z' },-- { benchmark_id: BMK, benchmark_title: 'Playable games arena', task_id: 't2', runs: 2, score: 80, best: 80, worst: 80, last_run: '2026-11-06T10:00:00.000Z' },-- ],-- best_task: { task_id: 't1', score: 90 }, worst_task: { task_id: 't2', score: 80 },-- total_cost_usd: 0.04, total_ms: 2000, total_tokens: 20, last_run: '2026-11-06T10:00:00.000Z',-- recent_runs: [-- ...runs.runs.filter((r) => r.model === 'model-a'),-- { id: 'brn_01M65KA3H4Y4ZENBYYZQA8SRN3', benchmark_id: BMK, model: 'model-a', score: 80, cost_usd: 0.02, ms: 1200, tokens: 10, provenance: 'verified', created_at: '2026-11-05T10:00:00.000Z' },-- ],--};----const legacy = { model: 'model-a', ratings: [{ kind_id: 'design.page', elo: 1043, ci_lower: 1005, ci_upper: 1081, games: 20, win_rate: 0.75 }], entries: [], total_cost_usd: 0.04 };----(async () => {-- const source = stubFetch();-- const fetchImpl = async (url, opts) => {-- const u = new URL(String(url));-- if (u.port !== '5230') return source(url, opts);-- const p = u.pathname;-- if (p === '/api/v1/kinds') return jsonResponse({ count: 1, kinds: [{ id: 'design.page', room: 'design', name: 'Design page arena', judge: 'human_pairwise' }] });-- if (['/api/v1/boards/design.page', '/api/v1/ratings/design.page'].includes(p)) {-- const type = p.includes('/ratings/') ? 'elo' : u.searchParams.get('type');-- const rows = type === 'elo'-- ? [{ subject: 'model-a', elo: 1043, ci_lower: 1005, ci_upper: 1081, games: 20, win_rate: 0.75, median_cost_usd: 0.02, median_ms: 1000, provisional: false }]-- : type === 'pareto' ? [{ subject: 'model-a', quality: 0.8 }] : [{ subject: 'model-a', entries: 2, solved: 2, total_cost_usd: 0.04, cost_per_solved: 0.02, median_ms: 1000 }];-- return jsonResponse({ board: { kind_id: 'design.page', type, rows } });-- }-- if (p === '/api/v1/overview') return jsonResponse(overview);-- if (p === '/api/v1/benchmarks') return jsonResponse({ count: 1, benchmarks: overview.list });-- if (p === `/api/v1/benchmarks/${BMK}`) return jsonResponse(benchmark);-- if (p === '/api/v1/rankings') return jsonResponse(rankings);-- if (p === '/api/v1/runs') return jsonResponse(runs);-- if (p === '/api/v1/models/model-a') return jsonResponse(legacy);-- if (p === '/api/v1/models/model-a/summary') return jsonResponse(modelDetail);-- if (p === '/api/v1/models/model-b/summary') return jsonResponse({ ...modelDetail, model: 'model-b', runs: 1, total_cost_usd: 0.04, recent_runs: runs.runs.filter((r) => r.model === 'model-b') });-- return jsonResponse({ error: 'no stub for ' + p }, 404);-- };-- const t = await boot({ fetchImpl, env: { OV_BENCH_INTERNAL_URL: 'http://127.0.0.1:5230' } });-- try {-- await check('the AI overview renders the leaderboard hub from the Bench fixture', async () => {-- const r = await t.get('/ai');-- assert.strictEqual(r.status, 200);-- for (const s of ['AI model leaderboard', 'model-a', '1043', 'Best overall', 'FAQPage', 'Dataset']) assert.ok(r.text.includes(s), s);-- assert.ok(r.text.includes('href="/ai/leaderboard"') && r.text.includes('/api/v1/ai/leaderboard'));-- });---- await check('one task kind shows quality, price and speed side by side and its latest runs', async () => {-- const r = await t.get('/ai/design.page');-- assert.strictEqual(r.status, 200);-- for (const s of ['Quality, price and speed side by side', 'Cost/run', 'Speed/run', '$0.02', '1 s', 'Latest runs', 'model-a', 'Nov 7']) assert.ok(r.text.includes(s), s);-- });---- await check('one benchmark page has a ranking, an SVG and the latest runs', async () => {-- const r = await t.get(`/ai/${BMK}`);-- assert.strictEqual(r.status, 200);-- for (const s of ['Playable games arena', 'Score against cost per run', 'ovk-chart--scatter', 'model-a', '85', '$0.02', 'Cost per solved', 'Latest runs', 'version 1']) assert.ok(r.text.includes(s), s);-- });---- await check('one model page uses the comparison profile and available Bench task data', async () => {-- const r = await t.get('/ai/models/model-a');-- assert.strictEqual(r.status, 200);-- for (const s of ['Providers', 'Task ratings', 'Playable games arena', 'snapshot', 'Product', 'Try it in Chat', 'Use it from code']) assert.ok(r.text.includes(s), s);-- assert.ok(!r.text.includes('<span title='), 'a stat is escaped HTML, not markup');-- });---- await check('the old query compare redirects to a canonical pair page', async () => {-- const two = await t.get('/ai/compare?models=model-a,model-b');-- assert.strictEqual(two.status, 301);-- assert.strictEqual(two.headers.get('location'), '/ai/compare/model-a-vs-model-b');-- const pair = await t.get('/ai/compare/model-a-vs-model-b');-- assert.strictEqual(pair.status, 200);-- assert.ok(pair.text.includes('model-a') && pair.text.includes('model-b'), 'both models');-- const one = await t.get('/ai/compare?models=model-a');-- assert.strictEqual(one.status, 200);-- assert.ok(one.text.includes('Choose two models to compare'), 'ask for a second model');-- });---- await check('an unknown benchmark id is a 404 once Bench has answered its list', async () => {-- assert.strictEqual((await t.get('/ai/bmk_01M65KA3H4Y4ZENBYYZQA8SRN9')).status, 404);-- });---- await check('the method page explains judging, verification and cost', async () => {-- const r = await t.get('/ai/method');-- assert.strictEqual(r.status, 200);-- for (const s of ['What a score means', 'How runs are judged', 'Verified and submitted', 'Cost per solved', 'blind votes', 'benchmarks.jsonl']) assert.ok(r.text.includes(s), s);-- });---- await check('new results are RSS and JSON, from the same runs', async () => {-- const rss = await t.get('/ai/feed.xml');-- assert.strictEqual(rss.status, 200);-- assert.match(rss.headers.get('content-type') || '', /application\/rss\+xml/);-- assert.ok(rss.text.includes('<title>model-a on Playable games arena</title>') && rss.text.includes(RUN_A), 'the run is in the feed');-- const json = await t.get('/api/v1/ai/results');-- assert.strictEqual(json.status, 200);-- const body = json.json();-- assert.strictEqual(body.count, 2);-- assert.strictEqual(body.results[0].id, RUN_B);-- assert.strictEqual(body.results[0].benchmark, 'Playable games arena');-- assert.ok(body.results[0].url.endsWith('/ai/' + BMK));-- });---- await check('the AI pages have Markdown twins, and the benchmark twin names its page', async () => {-- for (const [path, expected] of [[`/ai/${BMK}.md`, 'Playable games arena'], ['/ai/method.md', 'How the AI results are judged'], ['/ai/models/model-a.md', 'model-a']]) {-- const r = await t.get(path);-- assert.strictEqual(r.status, 200, path);-- assert.match(r.headers.get('content-type') || '', /^text\/markdown/, path);-- assert.ok(r.text.includes(expected), `${path}: ${expected}`);-- }-- });---- await check('the sitemap lists the AI pages and the benchmark', async () => {-- const r = await t.get('/sitemap.xml');-- assert.strictEqual(r.status, 200);-- for (const s of ['/ai</loc>', '/ai/method', '/ai/compare', `/ai/${BMK}`, '/ai/design.page', '/ai/models/model-a']) assert.ok(r.text.includes(s), s);-- });-- } finally { await t.close(); }-- done();--})().catch((e) => { console.error(e); process.exit(1); });@@@ -31,7 +31,7 @@ const STATUS_UPDATED = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'STassert.strictEqual(r.status, 200);assert.match(r.headers.get('content-type') || '', /^application\/xml/);assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=3600');-- for (const p of ['/', '/stats', '/design', '/ai']) {++ for (const p of ['/', '/stats', '/design']) { // /ai moved to OpenVibe.Compare (2026-11-13)assert.ok(r.text.includes(`<loc>https://openvibe.report${p}</loc>`), `no ${p} entry`);}for (const p of ['/rundown', '/rundown/t/seattle', '/rundown/t/ai-progress']) {
deleted file mode 100644
deleted file mode 100644
@@ -1,111 +0,0 @@
@@ -1,111 +0,0 @@
-/* Report's AI comparison pages. Report's own tokens only (--bg-card, --border, --text-*, --accent, --room-*); the kit
- draws tables (stacked into cards under 600 px), stats, sections, folds and the search pill. */
-.ai-page{display:grid;gap:clamp(20px,3vw,32px);padding-block:clamp(12px,2vw,24px) 48px}
-.ai-page>*{min-width:0}
-
-/* Headers */
-.ai-head{display:grid;gap:14px;padding:clamp(18px,3.4vw,36px) 0 4px;position:relative}
-.ai-head::before{content:"";position:absolute;inset:-24px -16px auto;height:220px;z-index:-1;pointer-events:none;
- background:radial-gradient(60% 100% at 18% 0,color-mix(in srgb,var(--room-accent,var(--accent)) 16%,transparent),transparent 70%)}
-.ai-head h1{margin:0;font-size:clamp(1.85rem,4.6vw,3.1rem);line-height:1.06;letter-spacing:-.02em;max-width:20ch;text-wrap:balance}
-.ai-head--page h1{font-size:clamp(1.6rem,3.6vw,2.4rem)}
-.ai-kicker{margin:0;font-size:.78rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--room-accent,var(--accent))}
-.ai-kicker a{color:inherit;text-decoration:none}
-.ai-kicker a:hover{text-decoration:underline}
-.ai-lede{margin:0;max-width:62ch;color:var(--text-secondary);font-size:clamp(.98rem,1.6vw,1.08rem);line-height:1.5}
-.ai-lede code{font-size:.85em;color:var(--text-muted);overflow-wrap:anywhere}
-.ai-head .ovk-find{max-width:520px}
-.ai-fine,.ai-data{margin:10px 0 0;color:var(--text-muted);font-size:.86rem}
-.ai-data a{color:var(--text-secondary)}
-.ai-quiet{color:var(--text-muted);font-size:.9em}
-.ai-bad{color:var(--danger)}
-
-/* A model: mark, name, maker and tags */
-.ai-model{display:grid;grid-template-columns:auto minmax(0,1fr);align-items:center;gap:10px;color:inherit;text-decoration:none;min-width:0}
-.ai-model-text{display:grid;gap:2px;min-width:0}
-.ai-model-text b{font-weight:650;overflow-wrap:anywhere}
-.ai-model:hover b{color:var(--accent);text-decoration:underline;text-underline-offset:3px}
-.ai-model-text small{display:flex;flex-wrap:wrap;align-items:center;gap:6px;color:var(--text-muted);font-size:.8rem}
-.ai-model-text .ovk-badge{font-size:.7rem}
-
-/* Numbers in cells: the value, then what it rests on */
-.ai-board td b,.ai-board td .ai-rating b,.ai-price b{font-variant-numeric:tabular-nums;font-weight:650}
-.ai-board td small,.ai-rating small,.ai-price small{display:block;margin-top:2px;color:var(--text-muted);font-size:.78rem;font-weight:400}
-.ai-rank{display:inline-grid;place-items:center;min-width:2.1em;height:2.1em;padding:0 .4em;border-radius:999px;font-weight:700;font-variant-numeric:tabular-nums;
- background:var(--bg-secondary,color-mix(in srgb,var(--text-primary) 6%,transparent));color:var(--text-secondary);font-size:.85rem}
-.ai-rank.is-top{background:color-mix(in srgb,var(--room-accent,var(--accent)) 22%,transparent);color:var(--text-primary)}
-.ai-board th.is-primary,.ai-board td:nth-child(2){min-width:13rem}
-
-/* Inside a kit section, the page's own blocks sit on its padding */
-.ovk-section>.ai-fine,.ovk-section>.ai-data,.ovk-section>.ai-note,.ovk-section>.ai-picks,.ovk-section>.ai-chips,.ovk-section>.ai-answers,.ovk-section>.ai-prompt,.ovk-section>ul{margin:0 14px 14px}
-.ovk-section>.ovk-table-wrap+.ai-fine,.ovk-section>.ovk-table-wrap+.ai-data{margin-top:12px}
-.ai-note{margin:0 0 12px;padding:12px 14px;border:1px solid color-mix(in srgb,var(--accent) 35%,var(--border));border-radius:12px;background:color-mix(in srgb,var(--accent) 8%,transparent);color:var(--text-secondary);font-size:.92rem}
-.ovk-section>.ai-note{margin:0 14px 12px}
-.ai-page .ovk-table td small{display:block;margin-top:2px;color:var(--text-muted);font-size:.78rem;font-weight:400}
-.ai-page .ovk-table td b{font-variant-numeric:tabular-nums}
-
-/* Top picks */
-.ai-picks{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,230px),1fr));gap:12px}
-.ai-pick{height:100%;box-sizing:border-box;display:grid;align-content:start;gap:8px;padding:16px;border:1px solid var(--border);border-radius:16px;background:var(--bg-card);color:inherit;text-decoration:none;
- transition:border-color .15s,transform .15s}
-.ai-pick:hover{border-color:color-mix(in srgb,var(--room-accent,var(--accent)) 60%,var(--border));transform:translateY(-1px)}
-.ai-pick-k{font-size:.74rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--room-accent,var(--accent))}
-.ai-pick-m{display:flex;align-items:center;gap:10px;min-width:0}
-.ai-pick-m b{font-size:1.08rem;overflow-wrap:anywhere}
-.ai-pick-n{font-size:1.25rem;font-weight:700;font-variant-numeric:tabular-nums}
-.ai-pick small{color:var(--text-muted);font-size:.82rem;line-height:1.4}
-
-/* Chip links: tasks, comparisons */
-.ai-chips{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:8px}
-.ai-chips a{display:inline-flex;align-items:center;min-height:40px;padding:0 14px;border:1px solid var(--border);border-radius:999px;background:var(--bg-card);color:var(--text-primary);text-decoration:none;font-size:.9rem}
-.ai-chips a:hover{border-color:var(--accent);color:var(--accent)}
-
-/* Providers */
-.ai-prov{display:inline-flex;align-items:center;gap:8px}
-
-/* Filters */
-.ai-filters{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,150px),1fr));gap:10px 12px;align-items:end;padding:14px;border:1px solid var(--border);border-radius:16px;background:var(--bg-card)}
-.ai-filter{display:grid;gap:5px;min-width:0;font-size:.8rem;color:var(--text-secondary)}
-.ai-filter--q{grid-column:1/-1}
-.ai-filter input,.ai-filter select{box-sizing:border-box;width:100%;min-width:0;min-height:42px;padding:0 12px;border:1px solid var(--border);border-radius:10px;background:var(--bg-primary,transparent);color:var(--text-primary);font:inherit;font-size:.95rem}
-.ai-filter input:focus,.ai-filter select:focus{outline:2px solid var(--accent);outline-offset:1px}
-.ai-filters .ovk-btn{min-height:42px}
-
-/* A model page */
-.ai-model-head{display:flex;align-items:center;gap:16px;min-width:0}
-.ai-model-head>div{display:grid;gap:6px;min-width:0}
-.ai-model-head h1{overflow-wrap:anywhere}
-.ai-head--model .ai-lede{display:flex;flex-wrap:wrap;align-items:center;gap:6px}
-.ai-try{position:sticky;bottom:calc(12px + env(safe-area-inset-bottom));z-index:3;display:flex;flex-wrap:wrap;gap:10px;justify-content:center;padding:10px;border:1px solid var(--border);border-radius:999px;
- background:color-mix(in srgb,var(--bg-card) 92%,transparent);backdrop-filter:blur(10px);width:max-content;max-width:100%;margin-inline:auto;box-shadow:0 10px 30px rgba(0,0,0,.25)}
-
-/* Compare */
-.ai-vs{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,280px),1fr));gap:12px}
-.ai-vs-card{display:grid;gap:14px;padding:18px;border:1px solid var(--border);border-radius:16px;background:var(--bg-card);min-width:0}
-.ai-vs-card dl{margin:0;display:grid;gap:10px}
-.ai-vs-card dl div{display:grid;grid-template-columns:7.5rem minmax(0,1fr);gap:10px;align-items:baseline}
-.ai-vs-card dt{color:var(--text-muted);font-size:.82rem}
-.ai-vs-card dd{margin:0}
-.ai-vs-form .ovk-btn{align-self:end}
-
-/* One prompt, many answers */
-.ai-prompt{margin:0 0 12px;font-size:1.05rem;font-weight:600}
-.ai-answers{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,260px),1fr));gap:12px}
-.ai-answers article{display:grid;gap:10px;padding:16px;border:1px solid var(--border);border-radius:16px;background:var(--bg-card);min-width:0}
-.ai-answers p{margin:0;white-space:pre-wrap;overflow-wrap:anywhere;color:var(--text-secondary)}
-
-@media (max-width:600px){
- .ai-board th.is-primary,.ai-board td:nth-child(2){min-width:0}
- .ai-filters{grid-template-columns:1fr 1fr}
- .ai-model-head{gap:12px}
-}
-
-/* Every section reads as a section: the kit's small label for an action-less section becomes the same heading the
- sections with actions have, so the page's hierarchy is one level, not two. */
-.ai-page .ovk-section>h2{padding:14px 14px 10px;color:var(--text-primary);font-size:1.02rem;font-weight:700;letter-spacing:0;text-transform:none}
-.ai-cell{display:block;min-width:0}
-@media (max-width:600px){.ai-page .ovk-table--stack td .ai-cell{text-align:right}}
-/* Numbers in a header wrap into a grid instead of scrolling off a phone. */
-.ai-head .ovk-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,140px),1fr));overflow:visible}
-@media (max-width:700px){.ai-try{position:static;width:auto;border-radius:16px;box-shadow:none}}
-.ai-warn{color:var(--warning,#f59e0b)}
@@ -6,26 +6,16 @@
@@ -6,26 +6,16 @@
const { normaliseDays } = require('./data/source');
const { normaliseDays } = require('./data/source');
// /design and /rundown since 11-03: llms.txt listed them as HTML pages with no twin.
// /design and /rundown since 11-03: llms.txt listed them as HTML pages with no twin.
-const twinPath = (path) => /^\/(?:|stats|developers|design|rundown|ai|ai\/[^/]+|ai\/models\/[^/]+)$/.test(String(path || '/'));
+// The /ai pages moved to OpenVibe.Compare on 2026-11-13 (their twins with them).
+const twinPath = (path) => /^\/(?:|stats|developers|design|rundown)$/.test(String(path || '/'));
-function createAgentPages({ publicPages, bench }) {
+function createAgentPages({ publicPages }) {
async function renderPublicPage(path, { query } = {}) {
async function renderPublicPage(path, { query } = {}) {
let page = null;
let page = null;
- const model = /^\/ai\/models\/([^/]+)$/.exec(path);
- const kind = /^\/ai\/([^/]+)$/.exec(path);
if (path === '/') page = await publicPages.home();
if (path === '/') page = await publicPages.home();
else if (path === '/developers') page = publicPages.developers();
else if (path === '/developers') page = publicPages.developers();
else if (path === '/design') page = publicPages.design();
else if (path === '/design') page = publicPages.design();
else if (path === '/stats') page = await publicPages.stats(normaliseDays(query && query.get('days')));
else if (path === '/stats') page = await publicPages.stats(normaliseDays(query && query.get('days')));
- else if (path === '/ai') page = await publicPages.aiIndex();
- else if (path === '/ai/method') page = await publicPages.aiMethod();
- else if (path === '/ai/compare') {
- const models = String((query && query.get('models')) || '').split(',').map((x) => x.trim()).filter(Boolean);
- page = await publicPages.aiCompare(models);
- }
- else if (model) page = await publicPages.aiModel(decodeURIComponent(model[1]));
- else if (kind && bench.isBenchmarkId(kind[1])) page = await publicPages.aiBenchmark(kind[1]);
- else if (kind) page = await publicPages.aiBoard(kind[1], bench.normaliseTab(query && query.get('tab')));
return page ? { html: page.html } : null;
return page ? { html: page.html } : null;
}
}
@@ -10,7 +10,7 @@
@@ -10,7 +10,7 @@
* /me, /u/:username a person's stats: private by default, the published
* /me, /u/:username a person's stats: private by default, the published
* sections at /u (the owner, 2026-11-09)
* sections at /u (the owner, 2026-11-09)
* /auth/* sign-in with OpenVibe.Network (openvibe-sdk/sso)
* /auth/* sign-in with OpenVibe.Network (openvibe-sdk/sso)
- * /ai, /ai/models/:model, /ai/:kind the AI boards, from OpenVibe.Bench (http/pages.js)
+ * /ai… 301 to OpenVibe.Compare, except /ai/arena (http/arena.js) and /ai/exchange
* /api/v1/* the JSON API (http/api.js)
* /api/v1/* the JSON API (http/api.js)
* /robots.txt, /sitemap.xml, /llms.txt crawl artifacts (http/discovery.js)
* /robots.txt, /sitemap.xml, /llms.txt crawl artifacts (http/discovery.js)
* /api/health, /api/ready, /release.json, /release-metrics the plumbing (loopback scrape for /metrics)
* /api/health, /api/ready, /release.json, /release-metrics the plumbing (loopback scrape for /metrics)
@@ -342,7 +342,7 @@ async function createApp(opts = {}) {
@@ -342,7 +342,7 @@ async function createApp(opts = {}) {
app.use(createPageRoutes({ ...ctx, publicPages }));
app.use(createPageRoutes({ ...ctx, publicPages }));
require('openvibe-shared/agent-layer').mount(app, {
require('openvibe-shared/agent-layer').mount(app, {
descriptor: require('./agent-descriptor'),
descriptor: require('./agent-descriptor'),
- render: require('./agent-pages').createAgentPages({ publicPages, bench }).renderPublicPage,
+ render: require('./agent-pages').createAgentPages({ publicPages }).renderPublicPage,
});
});
app.use((_req, res) => {
app.use((_req, res) => {
res.status(404).set('Cache-Control', PAGE_CACHE).type('html').send(views.notFound());
res.status(404).set('Cache-Control', PAGE_CACHE).type('html').send(views.notFound());
@@ -46,14 +46,11 @@ function createDiscoveryRoutes({ config, live, bench, sessionReports = null, meS
@@ -46,14 +46,11 @@ function createDiscoveryRoutes({ config, live, bench, sessionReports = null, meS
});
});
r.get('/sitemap.xml', async (_req, res) => {
r.get('/sitemap.xml', async (_req, res) => {
- const [ai, overview, reports, people] = await Promise.all([
- bench.kinds(), bench.overview(), sessionReports ? sessionReports.listPublic({ limit: 500 }) : [],
+ // The /ai pages moved to OpenVibe.Compare on 2026-11-13; its sitemap lists them.
+ const [reports, people] = await Promise.all([
+ sessionReports ? sessionReports.listPublic({ limit: 500 }) : [],
meStats ? meStats.listPublished({ limit: 500 }).catch(() => []) : [],
meStats ? meStats.listPublished({ limit: 500 }).catch(() => []) : [],
]);
]);
- const kinds = (ai && Array.isArray(ai.data.kinds)) ? ai.data.kinds : [];
- const o = overview && overview.data ? overview.data : {};
- const benches = Array.isArray(o.list) ? o.list : [];
- const models = Array.isArray(o.model_list) ? o.model_list.map((m) => m.model).filter(Boolean) : [...new Set((Array.isArray(o.recent) ? o.recent : []).map((r) => r && r.model).filter(Boolean))];
const lastmod = siteUpdated();
const lastmod = siteUpdated();
// Only the pages this site owns. /rundown and its topic, episode and speaker pages carry rel=canonical to
// Only the pages this site owns. /rundown and its topic, episode and speaker pages carry rel=canonical to
// OpenVibe.Live's Rundown, so listing them here would advertise a duplicate.
// OpenVibe.Live's Rundown, so listing them here would advertise a duplicate.
@@ -61,12 +58,6 @@ function createDiscoveryRoutes({ config, live, bench, sessionReports = null, meS
@@ -61,12 +58,6 @@ function createDiscoveryRoutes({ config, live, bench, sessionReports = null, meS
{ loc: abs('/'), changefreq: 'daily', priority: 1.0 },
{ loc: abs('/'), changefreq: 'daily', priority: 1.0 },
{ loc: abs('/stats'), changefreq: 'daily', priority: 0.9 },
{ loc: abs('/stats'), changefreq: 'daily', priority: 0.9 },
{ loc: abs('/design'), changefreq: 'weekly', priority: 0.6 },
{ loc: abs('/design'), changefreq: 'weekly', priority: 0.6 },
- { loc: abs('/ai'), changefreq: 'daily', priority: 0.9 },
- { loc: abs('/ai/leaderboard'), changefreq: 'daily', priority: 0.9 },
- { loc: abs('/ai/providers'), changefreq: 'daily', priority: 0.7 },
- { loc: abs('/ai/arena'), changefreq: 'daily', priority: 0.6 },
- { loc: abs('/ai/method'), changefreq: 'weekly', priority: 0.6 },
- { loc: abs('/ai/compare'), changefreq: 'weekly', priority: 0.6 },
...reports.map((rep) => ({ loc: abs('/s/' + encodeURIComponent(rep.id)), changefreq: 'daily', priority: 0.6 })),
...reports.map((rep) => ({ loc: abs('/s/' + encodeURIComponent(rep.id)), changefreq: 'daily', priority: 0.6 })),
// A person's public stats page is crawlable only when they published a section; the private default is
// A person's public stats page is crawlable only when they published a section; the private default is
// never in the sitemap. The last modification is the snapshot they last computed.
// never in the sitemap. The last modification is the snapshot they last computed.
@@ -74,19 +65,6 @@ function createDiscoveryRoutes({ config, live, bench, sessionReports = null, meS
@@ -74,19 +65,6 @@ function createDiscoveryRoutes({ config, live, bench, sessionReports = null, meS
loc: abs(`/u/${encodeURIComponent(p.username)}`), changefreq: 'weekly', priority: 0.5,
loc: abs(`/u/${encodeURIComponent(p.username)}`), changefreq: 'weekly', priority: 0.5,
...(dayOf(p.generated_at) ? { lastmod: dayOf(p.generated_at) } : {}),
...(dayOf(p.generated_at) ? { lastmod: dayOf(p.generated_at) } : {}),
})),
})),
- ...kinds.filter((k) => k && bench.isKindId(k.id)).map((k) => ({
- loc: abs(`/ai/${encodeURIComponent(k.id)}`), changefreq: 'daily', priority: 0.7,
- })),
- ...benches.filter((b) => b && bench.isBenchmarkId(b.id)).map((b) => ({
- loc: abs(`/ai/${encodeURIComponent(b.id)}`), changefreq: 'daily', priority: 0.8,
- ...(dayOf(b.last_run) ? { lastmod: dayOf(b.last_run) } : {}),
- })),
- ...models.filter((m) => bench.isModelName(m)).map((m) => ({
- loc: abs(`/ai/models/${encodeURIComponent(m)}`), changefreq: 'daily', priority: 0.6,
- })),
- ...models.filter((m) => bench.isModelName(m)).slice(0, 200).flatMap((a, i, list) => list.slice(i + 1).map((b) => ({
- loc: abs(`/ai/compare/${encodeURIComponent(a)}-vs-${encodeURIComponent(b)}`), changefreq: 'weekly', priority: 0.5,
- }))).slice(0, 200),
].map((p) => ({ ...p, ...(lastmod && !p.lastmod ? { lastmod } : {}) }));
].map((p) => ({ ...p, ...(lastmod && !p.lastmod ? { lastmod } : {}) }));
res.type('application/xml').set('Cache-Control', CACHE).send(seo.sitemapXml(pages));
res.type('application/xml').set('Cache-Control', CACHE).send(seo.sitemapXml(pages));
});
});
@@ -104,9 +82,8 @@ function createDiscoveryRoutes({ config, live, bench, sessionReports = null, meS
@@ -104,9 +82,8 @@ function createDiscoveryRoutes({ config, live, bench, sessionReports = null, meS
{ title: 'Open numbers', url: abs('/stats'), note: 'page views, accounts and traffic for every OpenVibe site' },
{ title: 'Open numbers', url: abs('/stats'), note: 'page views, accounts and traffic for every OpenVibe site' },
{ title: 'Design health', url: abs('/design'), note: 'how closely each site follows the shared design kit, read from its code; per site at /design/<site>' },
{ title: 'Design health', url: abs('/design'), note: 'how closely each site follows the shared design kit, read from its code; per site at /design/<site>' },
{ title: 'The Rundown', url: 'https://openvibe.live/rundown', note: 'what streamers talked about, topic by topic (OpenVibe.Live is its canonical home)' },
{ title: 'The Rundown', url: 'https://openvibe.live/rundown', note: 'what streamers talked about, topic by topic (OpenVibe.Live is its canonical home)' },
- { title: 'AI', url: abs('/ai'), note: 'OpenVibe.Bench results: quality, price and speed for every task kind and benchmark' },
- { title: 'How AI results are judged', url: abs('/ai/method'), note: 'scores, verified runs, cost per solved, speed and the frontier' },
- { title: 'Compare AI models', url: abs('/ai/compare'), note: 'two or three models on the same benchmarks' },
+ { title: 'AI model comparison', url: 'https://openvibe.compare/ai', note: 'the leaderboard, ratings, prices and measured speed of every AI model now live on OpenVibe.Compare' },
+ { title: 'The AI Arena', url: abs('/ai/arena'), note: 'two anonymous answers to one prompt; your vote moves the ratings' },
] },
] },
// A JSON API is what an agent came for, not something to skip (AGT-5 review).
// A JSON API is what an agent came for, not something to skip (AGT-5 review).
{ title: 'API', links: [
{ title: 'API', links: [
@@ -114,7 +91,7 @@ function createDiscoveryRoutes({ config, live, bench, sessionReports = null, meS
@@ -114,7 +91,7 @@ function createDiscoveryRoutes({ config, live, bench, sessionReports = null, meS
{ title: 'Design health (JSON)', url: abs('/api/v1/design-health'), note: 'every site; /api/v1/design-health/<site> for one' },
{ title: 'Design health (JSON)', url: abs('/api/v1/design-health'), note: 'every site; /api/v1/design-health/<site> for one' },
{ title: 'The Rundown (JSON)', url: abs('/api/v1/rundown') },
{ title: 'The Rundown (JSON)', url: abs('/api/v1/rundown') },
{ title: 'Topics (JSON)', url: abs('/api/v1/rundown/topics') },
{ title: 'Topics (JSON)', url: abs('/api/v1/rundown/topics') },
- { title: 'New AI results (RSS)', url: abs('/ai/feed.xml') },
+ { title: 'New AI results (RSS)', url: 'https://openvibe.compare/ai/feed.xml' },
{ title: 'New AI results (JSON)', url: abs('/api/v1/ai/results') },
{ title: 'New AI results (JSON)', url: abs('/api/v1/ai/results') },
] },
] },
],
],
@@ -11,12 +11,8 @@
@@ -11,12 +11,8 @@
* GET /rundown/s/:username one streamer
* GET /rundown/s/:username one streamer
* GET /after-show/:streamId one after-show report
* GET /after-show/:streamId one after-show report
* GET /c/:username one channel's after-show reports
* GET /c/:username one channel's after-show reports
- * GET /ai OpenVibe.Bench's public results: task kinds, benchmarks, runs and last runs
- * GET /ai/method how a score, a cost and a speed are made
- * GET /ai/compare two or three models side by side
- * GET /ai/feed.xml new runs as RSS
- * GET /ai/:ref one task kind (Elo/price/speed) or one public benchmark (bmk_…)
- * GET /ai/models/:model one model's runs over time, tasks, cost and recent runs
+ * GET /ai… 301 to OpenVibe.Compare (the AI model comparison moved there on 2026-11-13), except
+ * the Arena's rounds and votes (http/arena.js) and the staff page /ai/exchange
*
*
* A bad path parameter is a 404 without a source request; so is a source that answers nothing. Pages are public
* A bad path parameter is a 404 without a source request; so is a source that answers nothing. Pages are public
* for two minutes.
* for two minutes.
@@ -25,7 +21,6 @@
@@ -25,7 +21,6 @@
* Markdown twin render the very same page; `agent: true` in those views adds the discovery links.
* Markdown twin render the very same page; `agent: true` in those views adds the discovery links.
*/
*/
const express = require('express');
const express = require('express');
-const { findCatalogModel, makerOf } = require('../data/ai-join');
const contracts = require('openvibe-contracts');
const contracts = require('openvibe-contracts');
const views = require('../views');
const views = require('../views');
const { normaliseDays, isSlug, isStreamId, isUsername, streamIdOf } = require('../data/source');
const { normaliseDays, isSlug, isStreamId, isUsername, streamIdOf } = require('../data/source');
@@ -33,9 +28,10 @@ const { twinPath } = require('../agent-pages');
@@ -33,9 +28,10 @@ const { twinPath } = require('../agent-pages');
const designViews = require('../views/design');
const designViews = require('../views/design');
const designHealth = require('../data/design-health');
const designHealth = require('../data/design-health');
const sharing = require('../sharing');
const sharing = require('../sharing');
-const aiComparisonViews = require('../views/ai-comparison');
const CACHE = 'public, max-age=120';
const CACHE = 'public, max-age=120';
+/** Where the AI model comparison lives since 2026-11-13. */
+const COMPARE = 'https://openvibe.compare';
/** The pages the agent layer twins: their data, then the view. Each returns { html } or null (nothing to show). */
/** The pages the agent layer twins: their data, then the view. Each returns { html } or null (nothing to show). */
function createPublicPages({ sessionReports, network, live, bench, aiCatalogue = null, analytics = null, viewsData = null }) {
function createPublicPages({ sessionReports, network, live, bench, aiCatalogue = null, analytics = null, viewsData = null }) {
@@ -56,190 +52,6 @@ function createPublicPages({ sessionReports, network, live, bench, aiCatalogue =
@@ -56,190 +52,6 @@ function createPublicPages({ sessionReports, network, live, bench, aiCatalogue =
return { html: views.developers() };
return { html: views.developers() };
}
}
- async function aiIndex() {
- return aiComparisonHub();
- }
-
- async function comparisonData() {
- const [kindsResult, catalogue] = await Promise.all([bench.kinds(), require('../data/ai-market').comparisonCatalogue(aiCatalogue)]);
- const kinds = kindsResult && kindsResult.data && kindsResult.data.kinds || [];
- const boards = await Promise.all(kinds.map((kind) => bench.board(kind.id, 'elo')));
- const catModels = (catalogue && catalogue.models || []).map((model) => ({ ...model, offers: (model.offers || []).map((offer) => ({ ...offer, source: offer.source || { name: 'OpenVibe.AI', date: catalogue.generated_at || null, sample_size: offer.requests_7d ?? null } })) }));
- const findCat = (id) => findCatalogModel(catModels, id);
- const rows = [];
- for (let i = 0; i < boards.length; i += 1) {
- const list = boards[i] && boards[i].data && boards[i].data.board && boards[i].data.board.rows || [];
- for (const entry of list) {
- const id = entry.subject || entry.model || entry.name;
- const cat = findCat(id);
- rows.push({ id, routed: cat ? cat.routed !== false : false, kind: kinds[i].id, model_kind: cat && cat.kind || 'chat', maker: makerOf(cat, id), weights: cat && (cat.weights || (cat.open_weights == null ? null : cat.open_weights ? 'open' : 'closed')), status: cat && cat.status || 'current', uncensored: Boolean(cat && cat.uncensored), community: Boolean(cat && cat.community), rating: { value: entry.elo == null || Number(entry.games) === 0 ? null : Number(entry.elo), interval: Number(entry.games) === 0 ? [null, null] : [entry.ci_lower ?? null, entry.ci_upper ?? null], source: 'OpenVibe.Bench', date: boards[i].at || null, sample_size: entry.games ?? null }, benchmark: { value: entry.score == null ? null : Number(entry.score), reason: entry.score == null ? 'No benchmark score.' : undefined }, provider_offers: cat && Array.isArray(cat.offers) ? cat.offers : [], source: { name: 'OpenVibe.Bench', date: boards[i].at || null, sample_size: entry.games ?? null } });
- }
- }
- const grouped = new Map();
- for (const row of rows) {
- const existing = grouped.get(row.id);
- if (existing) existing.task_ratings.push({ kind: row.kind, rating: row.rating, benchmark: row.benchmark });
- else grouped.set(row.id, { ...row, task_ratings: [{ kind: row.kind, rating: row.rating, benchmark: row.benchmark }] });
- }
- rows.length = 0;
- rows.push(...grouped.values());
- // Every model the Exchange sells is listed with its prices and speed, rated or not: a model without
- // Bench games yet shows as not rated, never as missing.
- const ratedCats = new Set(rows.map((row) => findCat(row.id)).filter(Boolean));
- for (const cat of catModels) {
- if (ratedCats.has(cat)) continue;
- const id = cat.slug || cat.canonical || cat.id;
- if (!id) continue;
- const source = { name: 'OpenVibe.AI', date: catalogue.generated_at || null, sample_size: null };
- rows.push({ id, routed: cat.routed !== false, kind: cat.kind || null, model_kind: cat.kind || 'chat', maker: makerOf(cat, id), weights: cat.weights || (cat.open_weights == null ? null : cat.open_weights ? 'open' : 'closed'), status: cat.status || 'current', uncensored: Boolean(cat.uncensored), community: Boolean(cat.community), rating: { value: null, interval: [null, null], source: 'OpenVibe.Bench', date: null, sample_size: 0 }, benchmark: { value: null, reason: 'Not rated yet.' }, provider_offers: Array.isArray(cat.offers) ? cat.offers : [], source, task_ratings: [], unrated: true });
- }
- for (const kind of kinds) {
- const ranked = rows.map((row) => ({ row, task: row.task_ratings.find((rating) => rating.kind === kind.id) })).filter((x) => x.task).sort((a, b) => (b.task.rating.value ?? -Infinity) - (a.task.rating.value ?? -Infinity));
- ranked.forEach(({ task }, i) => {
- const [lo, hi] = task.rating.interval || [];
- if (lo == null || hi == null) return;
- const overlap = ranked.map((other, j) => ({ other, rank: j + 1 })).filter(({ other }) => other.task.rating.interval && other.task.rating.interval[0] != null && other.task.rating.interval[1] != null && other.task.rating.interval[0] <= hi && other.task.rating.interval[1] >= lo).map(({ rank }) => rank);
- task.rank = overlap.length ? { min: Math.min(...overlap), max: Math.max(...overlap), position: i + 1 } : null;
- });
- }
- const outPrice = (r) => Math.min(...(r.provider_offers || []).map((o) => (o.price && o.price.out != null ? Number(o.price.out) : NaN)).filter(Number.isFinite), Infinity);
- rows.sort((a, b) => (b.rating.value ?? -Infinity) - (a.rating.value ?? -Infinity)
- || (b.routed === true) - (a.routed === true)
- || outPrice(a) - outPrice(b)
- || String(a.id).localeCompare(String(b.id)));
- rows.forEach((row, i) => {
- const [lo, hi] = row.rating.interval || [];
- if (lo == null || hi == null) row.rank = null;
- else {
- const overlap = rows.map((other, j) => ({ other, rank: j + 1 })).filter(({ other }) => other.rating.interval && other.rating.interval[0] != null && other.rating.interval[1] != null && other.rating.interval[0] <= hi && other.rating.interval[1] >= lo).map(({ rank }) => rank);
- row.rank = overlap.length ? { min: Math.min(...overlap), max: Math.max(...overlap), position: i + 1 } : null;
- }
- });
- const tasks = kinds.map((kind) => ({ id: kind.id, name: kind.name }));
- const providers = catModels.flatMap((m) => (m.offers || []).map((o) => ({ ...o, routed: o.routed !== false, model: m.slug })));
- return { rows, providers, tasks, picks: [], catalogue };
- }
-
- async function aiComparisonHub() {
- const data = await comparisonData();
- const num = (x) => (x == null || x === '' ? NaN : Number(x));
- const chat = data.rows.filter((r) => (r.model_kind || 'chat') === 'chat');
- const rated = chat.filter((r) => r.rating.value != null);
- const best = (list) => [...list].sort((a, b) => b.rating.value - a.rating.value)[0];
- const cheapest = (r) => Math.min(...(r.provider_offers || []).map((o) => num(o.price && o.price.out)).filter(Number.isFinite), Infinity);
- const fastest = (r) => Math.min(...(r.provider_offers || []).filter((o) => Number(o.requests_7d) > 0).map((o) => num(o.speed && o.speed.p50_ms)).filter(Number.isFinite), Infinity);
- const free = (r) => (r.provider_offers || []).some((o) => o.free === true || o.free_now === true || num(o.price && o.price.out) === 0);
- const taskRows = (kind) => rated.map((row) => { const task = row.task_ratings.find((x) => x.kind === kind && x.rating && x.rating.value != null); return task ? { ...row, kind, rating: task.rating, benchmark: task.benchmark } : null; }).filter(Boolean);
- const choose = (id, model, why) => ({ id, model: model || null, why: model ? why : 'Not enough source data to establish this pick.' });
- data.picks = [
- choose('overall', best(rated), 'Highest rating from blind votes and open runs.'),
- choose('value', rated.filter((r) => Number.isFinite(cheapest(r))).sort((a, b) => cheapest(a) - cheapest(b))[0], 'Lowest output price among rated models.'),
- choose('fastest', chat.filter((r) => Number.isFinite(fastest(r))).sort((a, b) => fastest(a) - fastest(b))[0], 'Fastest typical reply on real traffic this week.'),
- choose('open', best(rated.filter((r) => ['open', 'open_weights'].includes(r.weights))), 'Highest-rated model with open weights.'),
- choose('code', best(taskRows('code')), 'Highest rating on code tasks.'),
- choose('writing', best(taskRows('writing')), 'Highest rating on writing tasks.'),
- choose('uncensored', best(rated.filter((r) => r.uncensored)), 'Highest-rated uncensored model; 18+.'),
- choose('free', best(rated.filter(free)) || chat.find(free), 'Free to use now through the Exchange.'),
- ];
- data.others = data.rows.length - chat.length;
- data.rows = chat;
- return { html: aiComparisonViews.hub(data) };
- }
- async function aiComparisonLeaderboard(query = {}) {
- const nz = (x) => (x == null || x === '' ? NaN : Number(x));
- const data = await comparisonData();
- let rows = data.rows;
- if (query.kind) rows = rows.map((x) => {
- const task = x.task_ratings.find((rating) => rating.kind === query.kind);
- return task ? { ...x, kind: query.kind, rating: task.rating, benchmark: task.benchmark } : x.kind === query.kind ? x : null;
- }).filter(Boolean);
- const type = ['chat', 'embedding', 'stt', 'all'].includes(query.type) ? query.type : 'chat';
- if (type !== 'all') rows = rows.filter((x) => (x.model_kind || 'chat') === type);
- if (query.maker) rows = rows.filter((x) => String(x.maker || '').toLowerCase() === String(query.maker).toLowerCase());
- if (query.weights) rows = rows.filter((x) => x.weights === query.weights || (query.weights === 'open' && x.weights === 'open_weights'));
- if (query.status !== 'all') rows = rows.filter((x) => x.status === (query.status || 'current'));
- if (query.uncensored) rows = rows.filter((x) => x.uncensored);
- if (query.community) rows = rows.filter((x) => x.community);
- if (query.price) rows = rows.filter((x) => x.provider_offers.some((o) => {
- const p = nz(o.price && o.price.out);
- return query.price === 'free' ? (o.free === true || o.free_now === true || p === 0) : !Number.isFinite(p) ? false : query.price === 'low' ? p <= 1 : query.price === 'mid' ? p > 1 && p <= 10 : query.price === 'high' ? p > 10 : true;
- }));
- if (query.q) rows = rows.filter((x) => `${x.id} ${x.maker || ''}`.toLowerCase().includes(String(query.q).toLowerCase()));
- if (query.sort === 'name') rows.sort((a, b) => String(a.id).localeCompare(String(b.id)));
- if (query.sort === 'price') rows.sort((a, b) => Math.min(...a.provider_offers.map((o) => nz(o.price && o.price.out)).filter(Number.isFinite), Infinity) - Math.min(...b.provider_offers.map((o) => nz(o.price && o.price.out)).filter(Number.isFinite), Infinity));
- if (query.sort === 'speed') rows.sort((a, b) => Math.min(...a.provider_offers.filter((o) => Number(o.requests_7d) > 0).map((o) => nz(o.speed && o.speed.p50_ms)).filter(Number.isFinite), Infinity) - Math.min(...b.provider_offers.filter((o) => Number(o.requests_7d) > 0).map((o) => nz(o.speed && o.speed.p50_ms)).filter(Number.isFinite), Infinity));
- if (query.sort === 'rating') rows.sort((a, b) => (b.rating.value ?? -Infinity) - (a.rating.value ?? -Infinity));
- return { html: aiComparisonViews.leaderboard({ rows, filters: query }) };
- }
- async function aiComparisonProviders() { const data = await comparisonData(); return { html: aiComparisonViews.providers(data) }; }
- async function aiComparisonModel(id) {
- if (!bench.isModelName(id)) return null;
- const data = await comparisonData();
- const row = data.rows.find((x) => x.id === id);
- const detail = await bench.modelDetail(id);
- return { html: aiComparisonViews.model({ id, detail: detail && detail.data, sourceDate: detail && detail.at, row, providers: data.providers }) };
- }
- async function aiComparison({ models = [] } = {}) {
- const data = await comparisonData();
- const selected = models.map((id) => data.rows.find((x) => x.id === id) || { id, reason: 'No public model data.' });
- return { html: aiComparisonViews.compare({ models: selected, options: data.rows.map((x) => x.id) }) };
- }
- function aiArena() { return { html: aiComparisonViews.arena() }; }
-
- async function aiBoard(kindId, tab) {
- if (!bench.isKindId(kindId)) return null;
- const [kinds, board, cost, benchmarks, runs, overview] = await Promise.all([
- bench.kinds(),
- bench.board(kindId, tab),
- tab === 'pareto' ? bench.board(kindId, 'cost') : Promise.resolve(null),
- bench.benchmarks(),
- bench.runs({ kind: kindId, limit: 8 }),
- bench.overview(),
- ]);
- const kind = kinds && Array.isArray(kinds.data.kinds) ? kinds.data.kinds.find((k) => k.id === kindId) : null;
- if (kinds && !kind) return null;
- const kindBenchmarks = benchmarks && benchmarks.data && Array.isArray(benchmarks.data.benchmarks)
- ? benchmarks.data.benchmarks.filter((b) => b.kind === kindId) : [];
- const rankings = await Promise.all(kindBenchmarks.map((b) => bench.rankings(b.id, 'score')));
- return { html: views.aiBoard({ kindId, kind, tab, board, cost, kinds, benchmarks, rankings, runs, overview }) };
- }
-
- async function aiBenchmark(benchmarkId) {
- if (!bench.isBenchmarkId(benchmarkId)) return null;
- const [benchmark, rankings, runs, overview] = await Promise.all([
- bench.benchmark(benchmarkId), bench.rankings(benchmarkId, 'score'), bench.runs({ benchmark: benchmarkId, limit: 20 }), bench.overview(),
- ]);
- // A public id the overview does not list is a 404; a Bench that did not answer at all is the honest unavailable page.
- const list = overview && overview.data && Array.isArray(overview.data.list) ? overview.data.list : null;
- if (list && !list.some((b) => b.id === benchmarkId)) return null;
- return { html: views.aiBenchmark({ benchmark, rankings, runs }) };
- }
-
- async function aiModel(model) {
- if (!bench.isModelName(model)) return null;
- const [detail, legacy] = await Promise.all([bench.modelDetail(model), bench.modelSummary(model)]);
- return { html: views.aiModel({ model, detail, legacy }) };
- }
-
- async function aiMethod() {
- const [overview, kinds] = await Promise.all([bench.overview(), bench.kinds()]);
- return { html: views.aiMethod({ overview, kinds }) };
- }
-
- async function aiCompare(models = [], errors = []) {
- const clean = [...new Set((models || []).filter((m) => bench.isModelName(m)))].slice(0, 3);
- const [details, legacy] = await Promise.all([
- Promise.all(clean.map((m) => bench.modelDetail(m))),
- Promise.all(clean.map((m) => bench.modelSummary(m))),
- ]);
- return { models: clean, html: views.aiCompare({ models: clean, details, legacy, errors }) };
- }
-
- async function aiFeed() {
- const [runs, overview] = await Promise.all([bench.runs({ limit: 20 }), bench.overview()]);
- return { runs, overview };
- }
-
function design() { return { html: designViews.designHealth(designHealth.all()) }; }
function design() { return { html: designViews.designHealth(designHealth.all()) }; }
async function rundown() {
async function rundown() {
@@ -250,7 +62,7 @@ function createPublicPages({ sessionReports, network, live, bench, aiCatalogue =
@@ -250,7 +62,7 @@ function createPublicPages({ sessionReports, network, live, bench, aiCatalogue =
return { html: views.rundown({ rundown: r, topics, reports }) };
return { html: views.rundown({ rundown: r, topics, reports }) };
}
}
- return { home, stats, developers, design, rundown, aiIndex, aiBoard, aiBenchmark, aiModel, aiMethod, aiCompare, aiFeed, aiComparisonHub, aiComparisonLeaderboard, aiComparisonProviders, aiComparisonModel, aiComparison, aiArena };
+ return { home, stats, developers, design, rundown };
}
}
/** The new-results feed: one item per public run, with its own Bench date and no invented number. */
/** The new-results feed: one item per public run, with its own Bench date and no invented number. */
@@ -319,49 +131,13 @@ function createPageRoutes(ctx) {
@@ -319,49 +131,13 @@ function createPageRoutes(ctx) {
return send(req, res, page.html);
return send(req, res, page.html);
});
});
- r.get('/ai', async (_req, res) => send(_req, res, (await publicPages.aiComparisonHub()).html));
-
- r.get('/ai/leaderboard', async (req, res) => send(req, res, (await publicPages.aiComparisonLeaderboard(req.query)).html));
- r.get('/ai/providers', async (req, res) => send(req, res, (await publicPages.aiComparisonProviders()).html));
- r.get('/ai/arena', (_req, res) => send(_req, res, publicPages.aiArena().html));
- r.get('/ai/compare', async (req, res) => {
- // The picker sends a and b; older links send models=a,b.
- const picked = [req.query.a, req.query.b].map((x) => String(x || '').trim()).filter(Boolean);
- const models = picked.length ? picked : String(req.query.models || '').split(',').map((x) => x.trim()).filter(Boolean);
- if (models.length === 2) return res.redirect(301, `/ai/compare/${encodeURIComponent(models[0])}-vs-${encodeURIComponent(models[1])}`);
- return send(req, res, (await publicPages.aiComparison({ models })).html);
- });
- r.get('/ai/compare/:pair', async (req, res) => {
- const match = String(req.params.pair).match(/^(.+)-vs-(.+)$/);
- if (!match) return notFound(req, res);
- const ordered = [decodeURIComponent(match[1]), decodeURIComponent(match[2])].sort();
- const canonical = `/ai/compare/${encodeURIComponent(ordered[0])}-vs-${encodeURIComponent(ordered[1])}`;
- if (req.path !== canonical) return res.redirect(301, canonical);
- return send(req, res, (await publicPages.aiComparison({ models: ordered })).html);
- });
-
- r.get('/ai/models/:model', async (req, res) => {
- const page = await publicPages.aiComparisonModel(req.params.model);
- if (!page) return notFound(req, res);
- return send(req, res, page.html);
- });
-
- // The AI section's method, comparison and feed. They must sit before /ai/:ref, which would read them as a kind.
- r.get('/ai/method', async (req, res) => send(req, res, (await publicPages.aiMethod()).html, 200, 'no-store'));
-
- r.get('/ai/compare', async (req, res) => {
- const raw = String(req.query.models || '').split(',').map((x) => x.trim()).filter(Boolean);
- const errors = [];
- const unique = [...new Set(raw)];
- if (unique.length < 2) errors.push('Pick at least two model ids, comma separated.');
- if (unique.length > 3) errors.push('At most three models can be compared at once.');
- const page = await publicPages.aiCompare(unique.slice(0, 3), errors);
- return send(req, res, page.html, 200, 'no-store');
- });
-
- r.get('/ai/feed.xml', async (req, res) => {
- const feed = await publicPages.aiFeed();
- res.status(200).set('Cache-Control', 'public, max-age=120').type('application/rss+xml; charset=utf-8').send(rssFeed({ ...feed, base: ctx.config.baseUrl }));
+ // The AI model comparison moved to OpenVibe.Compare (the owner, 2026-11-13: "This kinda shit is supposed to be a part
+ // of openvibe.compare"). Every /ai page answers 301 there with its path and query; the Arena's rounds and votes
+ // (http/arena.js, mounted before these routes) and the staff page /ai/exchange below stay here. Report keeps the
+ // ledgers and the JSON API (/api/v1/ai/*) Compare reads.
+ r.get(/^\/ai(?:\/.*)?$/, (req, res, next) => {
+ if (/^\/ai\/(?:arena|exchange)(?:\/|$)/.test(req.path)) return next();
+ return res.redirect(301, `${COMPARE}${req.originalUrl}`);
});
});
// The AI Exchange's daily aggregates (plan G-P9), staff-only, behind REPORT_AI_EXCHANGE. The gate is the signed-in
// The AI Exchange's daily aggregates (plan G-P9), staff-only, behind REPORT_AI_EXCHANGE. The gate is the signed-in
@@ -382,15 +158,6 @@ function createPageRoutes(ctx) {
@@ -382,15 +158,6 @@ function createPageRoutes(ctx) {
}
}
});
});
- r.get('/ai/:ref', async (req, res) => {
- const ref = String(req.params.ref || '');
- const page = bench.isBenchmarkId(ref)
- ? await publicPages.aiBenchmark(ref)
- : await publicPages.aiBoard(ref, bench.normaliseTab(req.query.tab));
- if (!page) return notFound(req, res);
- return send(req, res, page.html, 200, 'no-store');
- });
-
r.get('/rundown', async (req, res) => {
r.get('/rundown', async (req, res) => {
const [rundown, topics, reports] = await Promise.all([
const [rundown, topics, reports] = await Promise.all([
live.rundown(), live.topics(),
live.rundown(), live.topics(),
deleted file mode 100644
deleted file mode 100644
@@ -1,292 +0,0 @@
@@ -1,292 +0,0 @@
-'use strict';
-
-// Report's AI comparison pages: the hub, the full leaderboard, providers, a model and a comparison.
-// One way to show each thing everywhere:
-// - a model is its maker's mark, a readable name and its tags;
-// - a rating is the number with its spread and the votes it rests on, or "Not rated yet";
-// - a price is the cheapest provider's own-page price;
-// - speed and uptime are measured on the Exchange's real traffic, shown only where traffic went.
-// A column or section earns its place: it shows when some row has a value, and a missing value reads in words.
-const kit = require('openvibe-shared/kit');
-const { parts, PARTS_CSS } = require('openvibe-shared/showcase');
-const marks = require('openvibe-shared/ai-marks');
-const { asset } = require('../assets');
-const { layout } = require('./index');
-
-const esc = kit.esc;
-const value = (x) => x == null || !Number.isFinite(Number(x)) ? null : Number(x);
-const idPath = (id) => `/ai/models/${encodeURIComponent(id || '')}`;
-const act = (href, label) => kit.button({ href, label, kind: 'quiet', icon: 'forward' });
-const quiet = (text) => `<span class="ai-quiet">${esc(text)}</span>`;
-const money = (x) => value(x) == null ? null : Number(x) === 0 ? 'Free' : kit.num(x, 'money');
-const ms = (x) => value(x) == null ? null : kit.num(x, 'ms');
-const share = (x) => { const v = x != null && typeof x === 'object' ? x.value : x; return value(v) == null ? null : `${(Number(v) * 100).toFixed(Number(v) > 0 && Number(v) < 0.01 ? 2 : 1)}%`; };
-const sourceHint = (s) => { const x = s && typeof s === 'object' ? s : { name: s }; return `Source: ${x.name || 'not listed'}${x.date ? ` · ${String(x.date).slice(0, 10)}` : ''}${x.sample_size != null ? ` · n = ${x.sample_size}` : ''}`; };
-
-// ── A model ──
-function makerName(row) {
- const key = marks.family(row && row.maker) || marks.family(row && row.id);
- return key ? marks.NAMES[key] : (row && row.maker) || null;
-}
-// "openai/gpt-5-nano" reads as "gpt-5-nano" beside OpenAI's mark; the full id stays in the link and the title.
-const shortName = (id) => String(id || '').replace(/^[^/]+\//, '') || String(id || '');
-function tags(row) {
- const t = [];
- if (typeof row.routed === 'boolean') t.push(kit.badge(row.routed ? 'Routed here' : 'Not routed here'));
- if (row.weights === 'open' || row.weights === 'open_weights') t.push(kit.badge('Open weights'));
- if (row.uncensored) t.push(kit.badge('18+', 'warn'));
- if (row.community) t.push(kit.badge('Community machines'));
- return t.join('');
-}
-function modelCell(row, { size = 28 } = {}) {
- const maker = makerName(row);
- return `<a class="ai-model" href="${idPath(row.id)}" title="${esc(row.id)}">${marks.mark(row.maker || row.id, { size, label: false })}`
- + `<span class="ai-model-text"><b>${esc(shortName(row.id))}</b><small>${esc(maker || 'Maker not listed')}${tags(row) ? ` ${tags(row)}` : ''}</small></span></a>`;
-}
-
-// ── Numbers on a model ──
-const rated = (row) => row && row.rating && value(row.rating.value) != null;
-function ratingCell(row, { range = false } = {}) {
- if (!rated(row)) return quiet('Not rated yet');
- const v = Math.round(Number(row.rating.value));
- const [lo, hi] = (row.rating.interval || []).map(value);
- const n = value(row.rating.sample_size);
- const spread = lo != null && hi != null ? (range ? `${Math.round(lo)}–${Math.round(hi)}` : `± ${Math.round((hi - lo) / 2)}`) : '';
- const votes = n != null ? `${n.toLocaleString('en-US')} ${n === 1 ? 'game' : 'games'}` : '';
- return `<span class="ai-rating" title="${esc(sourceHint(row.rating))}"><b>${v}</b>${spread || votes ? `<small>${esc([spread, votes].filter(Boolean).join(' · '))}</small>` : ''}</span>`;
-}
-const rankText = (row, i) => row.rank && row.rank.min != null ? (row.rank.min === row.rank.max ? String(row.rank.min) : `${row.rank.min}–${row.rank.max}`) : (rated(row) ? String(i + 1) : '–');
-const offersOf = (row) => (row && row.provider_offers || []).filter(Boolean);
-const measured = (o) => o.routed !== false && Number(o.requests_7d) > 0;
-function cheapest(row) {
- const priced = offersOf(row).filter((o) => o.price && value(o.price.out) != null);
- return priced.sort((a, b) => Number(a.price.out) - Number(b.price.out))[0] || null;
-}
-function fastest(row) {
- const timed = offersOf(row).filter((o) => measured(o) && o.speed && value(o.speed.p50_ms) != null);
- return timed.sort((a, b) => Number(a.speed.p50_ms) - Number(b.speed.p50_ms))[0] || null;
-}
-function priceCell(row) {
- const o = cheapest(row);
- if (!o) return quiet('No price listed');
- const n = offersOf(row).filter((x) => x.price && value(x.price.out) != null).length;
- const via = n > 1 ? `cheapest of ${n} providers` : `via ${providerName(o)}`;
- return `<span class="ai-price" title="${esc(sourceHint(o.source || { name: o.provider_name }))}"><b>${esc(money(o.price.out))}</b> out · ${esc(money(o.price.in) || '–')} in<small>per 1M tokens, ${esc(via)}</small></span>`;
-}
-function speedCell(row) {
- const o = fastest(row);
- if (!o) return quiet('No traffic yet');
- return `<span title="${esc(sourceHint({ name: 'OpenVibe.AI, real traffic', sample_size: o.requests_7d }))}"><b>${esc(ms(o.speed.p50_ms))}</b><small>typical reply · p95 ${esc(ms(o.speed.p95_ms) || '–')}</small></span>`;
-}
-function uptimeCell(row) {
- const o = fastest(row) || offersOf(row).find(measured);
- if (!o || share(o.uptime) == null) return quiet('No traffic yet');
- return `<b${Number(o.uptime) < 0.95 ? ' class="ai-bad"' : ''}>${esc(share(o.uptime))}</b><small>${esc(Number(o.requests_7d).toLocaleString('en-US'))} requests in 7 days</small>`;
-}
-
-// ── The leaderboard table ──
-function board(rows, { full = false } = {}) {
- const has = (fn) => rows.some(fn);
- const columns = [
- has(rated) && { key: 'rank', label: 'Rank', num: true, html: true },
- { key: 'model', label: 'Model', primary: true, html: true },
- has(rated) && { key: 'rating', label: 'Rating', html: true },
- has((r) => cheapest(r)) && { key: 'price', label: 'Price', html: true },
- has((r) => fastest(r)) && { key: 'speed', label: 'Reply time', html: true },
- full && has((r) => offersOf(r).some((o) => measured(o) && share(o.uptime) != null)) && { key: 'uptime', label: 'Uptime 7 d', html: true },
- full && has((r) => r.benchmark && value(r.benchmark.value) != null) && { key: 'bench', label: 'Bench score', html: true },
- ].filter(Boolean);
- const data = rows.map((r, i) => ({
- rank: `<span class="ai-rank${rated(r) && i < 3 ? ' is-top' : ''}">${esc(rankText(r, i))}</span>`,
- model: modelCell(r),
- rating: ratingCell(r),
- price: priceCell(r),
- speed: speedCell(r),
- uptime: uptimeCell(r),
- bench: r.benchmark && value(r.benchmark.value) != null ? `<b>${esc(kit.num(r.benchmark.value))}</b>` : quiet('No runs yet'),
- }));
- const none = rows.length && !has(rated) ? `<p class="ai-note">No model has blind votes or runs yet, so nothing is ranked: these are the models with their prices and measured speed. <a href="https://openvibe.exchange/arena">Vote in the Arena</a> to rate them.</p>` : '';
- const wrapped = data.map((d) => Object.fromEntries(Object.entries(d).map(([k, v]) => [k, k === 'model' ? v : `<span class="ai-cell">${v}</span>`])));
- return none + kit.table({ columns, rows: wrapped, label: 'AI models', empty: 'No model matches these filters.' }).replace('class="ovk-table"', 'class="ovk-table ai-board"');
-}
-
-function page(title, description, body, path) {
- return layout({ title, description, body: `<link rel="stylesheet" href="${asset('css/ai-comparison.css')}"><style>${marks.CSS}\n${PARTS_CSS}</style><div class="ai-page">${body}</div>`, active: '/ai', path });
-}
-const dataLinks = () => `<p class="ai-data">Data: <a href="/api/v1/ai/leaderboard">JSON</a> · <a href="/api/v1/ai/leaderboard?format=csv&limit=100">CSV</a> · <a href="/ai/method">How it is measured</a></p>`;
-
-// ── /ai ──
-const PICK_LABELS = { overall: 'Best overall', value: 'Best value', fastest: 'Fastest', open: 'Best open weights', code: 'Best for code', writing: 'Best for writing', uncensored: 'Best uncensored 18+', free: 'Best free' };
-function pickNumber(id, row) {
- if (id === 'value' || id === 'free') { const o = cheapest(row); return !o ? '' : Number(o.price.out) === 0 ? 'Free' : `${money(o.price.out)} per 1M out`; }
- if (id === 'fastest') { const o = fastest(row); return o ? `${ms(o.speed.p50_ms)} typical reply` : ''; }
- return rated(row) ? `Rating ${Math.round(row.rating.value)}` : '';
-}
-function hub({ rows = [], picks = [], providers = [], others = 0, month = new Date().toLocaleDateString('en-US', { month: 'long', year: 'numeric', timeZone: 'UTC' }), changed = [], tasks = [], arena = null } = {}) {
- const ratedRows = rows.filter(rated);
- const providerNames = new Set(providers.map(providerName).filter(Boolean));
- const measuredOffers = providers.filter(measured);
- const made = picks.filter((p) => p.model);
- const pending = picks.filter((p) => !p.model).map((p) => PICK_LABELS[p.id] || p.id);
- const pickList = made.map((p) => `<li><a class="ai-pick" href="${idPath(p.model.id)}"><span class="ai-pick-k">${esc(PICK_LABELS[p.id] || p.id)}</span>`
- + `<span class="ai-pick-m">${marks.mark(p.model.maker || p.model.id, { size: 32, label: false })}<b>${esc(shortName(p.model.id))}</b></span>`
- + `${pickNumber(p.id, p.model) ? `<span class="ai-pick-n">${esc(pickNumber(p.id, p.model))}</span>` : ''}<small>${esc(p.why || '')}</small></a></li>`).join('');
- const pairs = ratedRows.slice(0, 16).reduce((a, r, i, all) => { if (i % 2 === 0 && all[i + 1]) a.push([r, all[i + 1]]); return a; }, []).slice(0, 8);
- const sourceDate = rows.map((r) => r.source && r.source.date).find(Boolean) || 'date not reported by source';
- const faq = [
- ['How are ratings made?', `${ratedRows.length} models have a rating from blind pairwise votes and open runs on OpenVibe.Bench (Bradley-Terry with bootstrap intervals), dated ${sourceDate}.`],
- ['Where do prices come from?', `Each provider's own price page, read by OpenVibe.AI's pricebook; ${providers.length} provider and model offers are listed.`],
- ['What does the rank band mean?', 'Models whose rating intervals overlap share a band: the data cannot tell them apart yet.'],
- ['Why does a model say "Not rated yet"?', 'No blind votes or runs on it so far. Its prices and measured speed are still listed.'],
- ['How is speed measured?', `On real requests through OpenVibe.Exchange over seven days: ${measuredOffers.length} provider endpoints had traffic.`],
- ['How often does this change?', 'Ratings and prices refresh within two minutes of the sources changing.'],
- ];
- const body = `<header class="ai-head"><p class="ai-kicker">AI comparison · ${esc(month)}</p><h1>AI model leaderboard — ${esc(month)}</h1>`
- + `<p class="ai-lede">${rows.length} models from ${providerNames.size} providers. Ratings come from blind votes, prices from each provider's own page, and speed and uptime from real traffic.</p>`
- + kit.find({ action: '/ai/leaderboard', placeholder: 'Search a model or maker', label: 'Search models' })
- + kit.stats([[ratedRows.length, 'rated'], [rows.length, 'models listed'], [providerNames.size, 'providers'], [measuredOffers.length, 'measured endpoints']], { label: 'The leaderboard in numbers' })
- + `</header>`
- + (pickList || pending.length ? kit.section({ title: 'Top picks', body: `${pickList ? `<ul class="ai-picks">${pickList}</ul>` : ''}${pending.length ? `<p class="ai-fine">Not enough data yet for ${esc(pending.join(', '))}.</p>` : ''}` }) : '')
- // A phone shows the parts below the top picks one at a time as tabs (the owner, 11-11: "self-contained tabs, so
- // the page isn't so freaking huge … at least on mobile"); every part stays in the HTML, a wide screen stacks them.
- + parts({ name: 'board', label: 'The leaderboard\'s parts', items: [
- { id: 'leaderboard', label: 'Leaderboard', count: rows.length, html: kit.section({ title: 'Leaderboard', count: rows.length, actions: [act('/ai/leaderboard', rows.length > 25 ? `All ${rows.length} models` : 'Filter and sort')], body: board(rows.slice(0, 25)) + (others ? `<p class="ai-fine">Chat models only. ${others} embedding and speech ${others === 1 ? 'model is' : 'models are'} in the <a href="/ai/leaderboard?type=all">full list</a>.</p>` : '') + dataLinks() }) },
- { id: 'answers', label: 'Answers', html: (arena && (arena.answers || []).length ? kit.section({ title: 'One prompt, many answers', body: `<p class="ai-prompt">${esc(arena.prompt || '')}</p><div class="ai-answers">${arena.answers.slice(0, 3).map((a) => `<article>${modelCell({ id: a.model || 'Model', maker: a.maker })}<p>${esc(a.text || '')}</p></article>`).join('')}</div>` }) : '') },
- { id: 'tasks', label: 'By task', count: tasks.length || '', html: (tasks.length ? kit.section({ title: 'Boards by task', body: `<ul class="ai-chips">${tasks.slice(0, 16).map((x) => `<li><a href="/ai/${encodeURIComponent(x.id)}">${esc(x.name || x.id)}</a></li>`).join('')}</ul>` }) : '') },
- { id: 'providers', label: 'Providers', count: measuredOffers.length || '', html: (measuredOffers.length ? kit.section({ title: 'Providers', actions: [act('/ai/providers', 'Every provider')], body: providerTable(measuredOffers.slice(0, 6)) }) : '') },
- { id: 'compare', label: 'Compare', html: (pairs.length ? kit.section({ title: 'Popular comparisons', body: `<ul class="ai-chips">${pairs.map(([a, b]) => `<li><a href="/ai/compare/${encodeURIComponent(a.id)}-vs-${encodeURIComponent(b.id)}">${esc(shortName(a.id))} vs ${esc(shortName(b.id))}</a></li>`).join('')}</ul>` }) : '') },
- { id: 'changes', label: 'Changes', html: (changed.length ? kit.section({ title: 'What changed this month', body: `<ul>${changed.slice(0, 8).map((x) => `<li>${esc(x)}</li>`).join('')}</ul>` }) : '') },
- { id: 'questions', label: 'Questions', html: kit.section({ title: 'Questions', body: faq.map(([q, a]) => kit.fold({ title: q, body: `<p>${esc(a)}</p>` })).join('') }) },
- ] })
- + `<script type="application/ld+json">${JSON.stringify({ '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map(([q, a]) => ({ '@type': 'Question', name: q, acceptedAnswer: { '@type': 'Answer', text: a } })) }).replace(/</g, '\\u003c')}</script>`
- + `<script type="application/ld+json">${JSON.stringify({ '@context': 'https://schema.org', '@type': 'Dataset', name: 'OpenVibe AI model leaderboard', url: 'https://openvibe.report/ai', variableMeasured: ['Bench rating', 'Provider price', 'Provider reply time', 'Provider uptime'], distribution: { '@type': 'DataDownload', contentUrl: 'https://openvibe.report/api/v1/ai/leaderboard', encodingFormat: 'application/json' } }).replace(/</g, '\\u003c')}</script>`;
- return page(`AI model leaderboard — ${month}`, `${rows.length} AI models compared: ratings from blind votes, provider prices and measured speed.`, body, '/ai');
-}
-
-// ── /ai/leaderboard ──
-function leaderboard({ rows = [], filters = {} } = {}) {
- const select = (name, label, opts) => `<label class="ai-filter"><span>${esc(label)}</span><select name="${name}">${opts.map(([v, t]) => `<option value="${esc(v)}"${String(filters[name] || '') === v ? ' selected' : ''}>${esc(t)}</option>`).join('')}</select></label>`;
- const text = (name, label, placeholder) => `<label class="ai-filter"><span>${esc(label)}</span><input name="${name}" value="${esc(filters[name] || '')}" placeholder="${esc(placeholder)}"></label>`;
- const form = `<form class="ai-filters" action="/ai/leaderboard">`
- + `<label class="ai-filter ai-filter--q"><span>Search</span><input name="q" type="search" value="${esc(filters.q || '')}" placeholder="Model or maker"></label>`
- + select('type', 'Type', [['chat', 'Chat models'], ['embedding', 'Embeddings'], ['stt', 'Speech to text'], ['all', 'All']])
- + select('sort', 'Sort by', [['rating', 'Rating'], ['price', 'Output price'], ['speed', 'Reply time'], ['name', 'Name']])
- + select('weights', 'Weights', [['', 'Any'], ['open', 'Open weights'], ['closed', 'Closed']])
- + select('price', 'Price', [['', 'Any price'], ['free', 'Free'], ['low', 'Low'], ['mid', 'Mid'], ['high', 'High']])
- + text('maker', 'Maker', 'e.g. openai')
- + text('kind', 'Task', 'e.g. design.page')
- + select('status', 'Status', [['current', 'Current'], ['all', 'All']])
- + select('uncensored', '18+', [['', 'Any'], ['true', 'Uncensored only']])
- + select('community', 'Runs on', [['', 'Anywhere'], ['true', 'Community machines']])
- + `<button class="ovk-btn ovk-btn--primary">Apply</button></form>`;
- const body = `<header class="ai-head ai-head--page"><p class="ai-kicker"><a href="/ai">AI comparison</a></p><h1>AI model leaderboard</h1>`
- + `<p class="ai-lede">${rows.length} ${rows.length === 1 ? 'model' : 'models'}${rows.filter(rated).length ? `, ${rows.filter(rated).length} rated` : ''}. Rated models first, then the rest with their prices.</p></header>`
- + form + board(rows, { full: true }) + dataLinks();
- return page('AI model leaderboard', 'Every AI model with its rating, provider prices and measured speed; filter and sort.', body, '/ai/leaderboard');
-}
-
-// ── /ai/providers ──
-const ownServer = (p) => /^(local|whisper)$/i.test(String(p.provider || '')) || /on this host|\blocal\b/i.test(String(p.provider_name || ''));
-const providerName = (p) => ownServer(p) ? 'OpenVibe server' : String(p.provider_name || p.provider || 'Provider').replace(/\s*\((?:the |an? )[^)]*\)\s*$/i, '');
-const providerMark = (p) => ownServer(p) ? marks.mark('openvibe', { size: 24, label: false }) : marks.mark(p.provider || p.provider_name, { size: 24, label: false });
-function providerTable(list) {
- // Status is a word (the state-in-words rule): Up, Degraded with the warning tone, Down with the bad one, each with
- // its plain reason and since when. The column shows only when some offer is not up (a column earns its place).
- const notUp = (p) => p.routed !== false && ['down', 'degraded'].includes(String(p.status || 'up'));
- const statusCell = (p) => {
- if (p.routed === false) return quiet('Not routed here');
- if (!notUp(p)) return 'Up';
- const down = String(p.status) === 'down';
- const why = p.status_reason ? `<small>${esc(p.status_reason)}</small>` : '';
- const since = p.status_since ? `<small>since ${esc(String(p.status_since).slice(0, 16).replace('T', ' '))} UTC</small>` : '';
- return `<b class="${down ? 'ai-bad' : 'ai-warn'}">${down ? 'Down' : 'Degraded'}</b>${why}${since}`;
- };
- const rows = list.map((p) => ({
- provider: `<span class="ai-prov">${providerMark(p)}<b>${esc(providerName(p))}</b></span>`,
- model: `<a href="${idPath(p.model)}">${esc(shortName(p.model))}</a>`,
- status: statusCell(p),
- price: p.price && value(p.price.out) != null ? `<b>${esc(money(p.price.out))}</b> out · ${esc(money(p.price.in) || '–')} in` : quiet('No price listed'),
- reply: p.routed === false ? quiet('Not routed here') : measured(p) && p.speed && value(p.speed.p50_ms) != null ? `<b>${esc(ms(p.speed.p50_ms))}</b><small>p95 ${esc(ms(p.speed.p95_ms) || '–')}</small>` : quiet('Not measured yet'),
- errors: p.routed === false ? quiet('Not routed here') : measured(p) && share(p.error_rate) != null ? `<b class="${Number(typeof p.error_rate === 'object' ? p.error_rate.value : p.error_rate) > 0.05 ? 'ai-bad' : ''}">${esc(share(p.error_rate))}</b>` : quiet('Not measured yet'),
- uptime: p.routed === false ? quiet('Not routed here') : measured(p) && share(p.uptime) != null ? `<b class="${Number(p.uptime) < 0.95 ? 'ai-bad' : ''}">${esc(share(p.uptime))}</b>` : quiet('Not measured yet'),
- requests: p.routed === false ? quiet('Not routed here') : measured(p) ? Number(p.requests_7d).toLocaleString('en-US') : quiet('Not measured yet'),
- }));
- return kit.table({
- columns: [
- { key: 'provider', label: 'Provider', primary: true, html: true },
- { key: 'model', label: 'Model', html: true },
- list.some(notUp) && { key: 'status', label: 'Status', html: true },
- { key: 'price', label: 'Price per 1M', html: true },
- { key: 'reply', label: 'Reply time', html: true },
- { key: 'errors', label: 'Errors', html: true },
- { key: 'uptime', label: 'Uptime 7 d', html: true },
- { key: 'requests', label: 'Requests 7 d', html: true },
- ],
- rows: rows.map((d) => Object.fromEntries(Object.entries(d).map(([k, v]) => [k, k === 'provider' ? v : `<span class="ai-cell">${v}</span>`]))), label: 'Providers', empty: 'No provider is listed yet.',
- });
-}
-function providers({ providers: list = [] } = {}) {
- const sorted = [...list].sort((a, b) => (Number(b.requests_7d) || 0) - (Number(a.requests_7d) || 0));
- const body = `<header class="ai-head ai-head--page"><p class="ai-kicker"><a href="/ai">AI comparison</a></p><h1>AI providers</h1>`
- + `<p class="ai-lede">Every model and provider offer in the market. Prices come from each provider's own page. Reply time, errors and uptime are measured on real requests over the last seven days; offers outside the router say “Not routed here”, while routed offers without measurements say “Not measured yet”. Status is measured from the Exchange catalogue: down while an offer\'s circuit is open, degraded when more than 5% of at least 10 requests in the last hour failed, with the plain reason.</p></header>`
- + providerTable(sorted);
- return page('AI providers', 'AI providers compared: price per million tokens, reply time, error rate and uptime measured on real traffic.', body, '/ai/providers');
-}
-
-// ── /ai/models/:id ──
-function model({ id, detail = {}, row = null, providers: list = [], sourceDate = null } = {}) {
- detail = detail || {};
- const self = row || { id, provider_offers: list.filter((p) => p.model === id) };
- const offers = offersOf(self).length ? offersOf(self) : list.filter((p) => p.model === id);
- const tasks = (self.task_ratings || []).filter((t) => t.rating && value(t.rating.value) != null);
- const maker = makerName(self);
- const facts = kit.stats([
- [rated(self) ? Math.round(self.rating.value) : 'Not rated yet', 'rating'],
- [cheapest({ provider_offers: offers }) ? money(cheapest({ provider_offers: offers }).price.out) : null, 'cheapest output / 1M'],
- [fastest({ provider_offers: offers }) ? ms(fastest({ provider_offers: offers }).speed.p50_ms) : null, 'typical reply'],
- [offers.length || null, offers.length === 1 ? 'provider' : 'providers'],
- ], { label: `${id} in numbers` });
- const taskTable = tasks.length ? kit.table({
- columns: [{ key: 'task', label: 'Task', primary: true, html: true }, { key: 'rank', label: 'Rank' }, { key: 'rating', label: 'Rating', html: true }],
- rows: tasks.map((t) => ({ task: `<a href="/ai/${encodeURIComponent(t.kind)}">${esc(t.kind)}</a>`, rank: t.rank ? (t.rank.min === t.rank.max ? String(t.rank.min) : `${t.rank.min}–${t.rank.max}`) : 'Not enough data', rating: ratingCell({ rating: t.rating }, { range: true }) })),
- label: 'Task ratings',
- }) : `<p class="ai-fine">Not rated on any task yet: no blind votes or runs on this model so far.</p>`;
- const benchmarks = (detail.benchmarks || []).length ? kit.table({
- columns: [{ key: 'name', label: 'Benchmark', primary: true }, { key: 'score', label: 'Score' }, { key: 'runs', label: 'Runs' }, { key: 'last', label: 'Last run' }],
- rows: detail.benchmarks.map((b) => ({ name: b.title || b.benchmark_id, score: b.score == null ? 'Not measured' : String(b.score), runs: b.runs == null ? 'Not reported' : String(b.runs), last: b.last_run ? String(b.last_run).slice(0, 10) : '–' })),
- label: 'Benchmarks', caption: `OpenVibe.Bench${sourceDate ? ` · snapshot ${String(sourceDate).slice(0, 10)}` : ''}`,
- }) : '';
- const body = `<header class="ai-head ai-head--model"><p class="ai-kicker"><a href="/ai">AI comparison</a></p>`
- + `<div class="ai-model-head">${marks.mark(self.maker || id, { size: 56 })}<div><h1>${esc(shortName(id))}</h1><p class="ai-lede">${esc(maker || 'Maker not listed')}${tags(self) ? ` ${tags(self)}` : ''} · <code>${esc(id)}</code></p></div></div>`
- + facts + `</header>`
- + kit.section({ title: 'Providers', count: offers.length, body: providerTable(offers) })
- + kit.section({ title: 'Task ratings', count: tasks.length || null, body: taskTable + benchmarks })
- + `<aside class="ai-try"><a class="ovk-btn ovk-btn--primary" href="https://openvibe.chat/vibe?models=${encodeURIComponent(id)}">Try it in Chat</a><a class="ovk-btn ovk-btn--quiet" href="https://openvibe.exchange/models/${encodeURIComponent(id)}">Use it from code</a></aside>`
- + `<script type="application/ld+json">${JSON.stringify({ '@context': 'https://schema.org', '@type': 'Product', name: id, brand: maker || 'Unknown maker' }).replace(/</g, '\\u003c')}</script>`;
- return page(`${shortName(id)} — AI model`, `${id}: rating, prices across providers and measured speed.`, body, idPath(id));
-}
-
-// ── /ai/compare ──
-function compare({ models = [], options = [] } = {}) {
- const two = models.length === 2;
- const facts = (m) => [
- ['Rating', ratingCell(m, { range: true })],
- ['Price', priceCell(m)],
- ['Reply time', speedCell(m)],
- ['Providers', offersOf(m).length ? String(offersOf(m).length) : quiet('None listed')],
- ];
- const cards = models.map((m) => `<article class="ai-vs-card">${modelCell(m, { size: 40 })}<dl>${facts(m).map(([k, v]) => `<div><dt>${esc(k)}</dt><dd>${v}</dd></div>`).join('')}</dl></article>`).join('');
- const list = `<datalist id="ai-models">${options.slice(0, 500).map((o) => `<option value="${esc(o)}"></option>`).join('')}</datalist>`;
- const form = `<form class="ai-filters ai-vs-form" action="/ai/compare"><label class="ai-filter"><span>First model</span><input name="a" list="ai-models" value="${esc(models[0] && models[0].id || '')}" required></label>`
- + `<label class="ai-filter"><span>Second model</span><input name="b" list="ai-models" value="${esc(models[1] && models[1].id || '')}" required></label><button class="ovk-btn ovk-btn--primary">Compare</button>${list}</form>`;
- const body = `<header class="ai-head ai-head--page"><p class="ai-kicker"><a href="/ai">AI comparison</a></p><h1>${two ? `${esc(shortName(models[0].id))} vs ${esc(shortName(models[1].id))}` : 'Compare AI models'}</h1>`
- + `${two ? '' : '<p class="ai-lede">Choose two models to compare: their ratings, prices and measured speed side by side.</p>'}</header>`
- + (cards ? `<div class="ai-vs">${cards}</div>` : '') + form;
- return page(two ? `${shortName(models[0].id)} vs ${shortName(models[1].id)}` : 'Compare AI models', 'Compare AI models side by side: ratings, prices and measured speed.', body, '/ai/compare');
-}
-
-function arena() {
- return page('AI Arena', 'Compare anonymous AI answers and vote.', `<header class="ai-head ai-head--page"><p class="ai-kicker"><a href="/ai">AI comparison</a></p><h1>AI Arena</h1><p class="ai-lede">Two anonymous answers to one prompt; you pick the better one, then see which models wrote them. Every vote moves the ratings here.</p></header><p><a class="ovk-btn ovk-btn--primary" href="https://openvibe.exchange/arena">Vote in the Arena</a></p>`, '/ai/arena');
-}
-
-module.exports = { hub, leaderboard, providers, model, compare, arena };
@@ -97,7 +97,6 @@ const NAV = [
@@ -97,7 +97,6 @@ const NAV = [
{ label: 'You', href: '/you', icon: 'fa-user' },
{ label: 'You', href: '/you', icon: 'fa-user' },
{ label: 'Numbers', href: '/stats', icon: 'fa-chart-line' },
{ label: 'Numbers', href: '/stats', icon: 'fa-chart-line' },
{ label: 'The Rundown', href: '/rundown', icon: 'fa-newspaper' },
{ label: 'The Rundown', href: '/rundown', icon: 'fa-newspaper' },
- { label: 'AI', href: '/ai', icon: 'fa-robot' },
{ label: 'API', href: '/developers', icon: 'fa-code', more: true },
{ label: 'API', href: '/developers', icon: 'fa-code', more: true },
];
];
@@ -116,7 +115,7 @@ function layout({ title, description, canonical = null, path = null, robots = 'i
@@ -116,7 +115,7 @@ function layout({ title, description, canonical = null, path = null, robots = 'i
service: 'report',
service: 'report',
apiBase: NETWORK,
apiBase: NETWORK,
links: NAV.map((l) => ({ ...l, active: l.href === active || (l.href !== '/' && active.startsWith(l.href)) })),
links: NAV.map((l) => ({ ...l, active: l.href === active || (l.href !== '/' && active.startsWith(l.href)) })),
- appBar: [{ label: 'Home', href: '/', icon: 'fa-house' }, { label: 'Numbers', href: '/stats', icon: 'fa-chart-line' }, { label: 'Rundown', href: '/rundown', icon: 'fa-newspaper' }, { label: 'AI', href: '/ai', icon: 'fa-robot' }],
+ appBar: [{ label: 'Home', href: '/', icon: 'fa-house' }, { label: 'Numbers', href: '/stats', icon: 'fa-chart-line' }, { label: 'Rundown', href: '/rundown', icon: 'fa-newspaper' }],
history: { type: 'page', title: title || NAME },
history: { type: 'page', title: title || NAME },
// Sign-in is this site's own (openvibe-sdk/sso at /auth): the navbar's buttons and its silent check use it.
// Sign-in is this site's own (openvibe-sdk/sso at /auth): the navbar's buttons and its silent check use it.
silentLogin: `${BASE}/auth/login?silent=1&next={url}`,
silentLogin: `${BASE}/auth/login?silent=1&next={url}`,
@@ -147,7 +146,6 @@ function layout({ title, description, canonical = null, path = null, robots = 'i
@@ -147,7 +146,6 @@ function layout({ title, description, canonical = null, path = null, robots = 'i
<link rel="stylesheet" href="${esc(ovServe.url('room.css'))}">
<link rel="stylesheet" href="${esc(ovServe.url('room.css'))}">
<link rel="stylesheet" href="${esc(ovServe.url('showcase.css'))}">
<link rel="stylesheet" href="${esc(ovServe.url('showcase.css'))}">
<link rel="stylesheet" href="${esc(asset('css/app.css'))}">
<link rel="stylesheet" href="${esc(asset('css/app.css'))}">
-${path && String(path).startsWith('/ai') ? `<link rel="alternate" type="application/rss+xml" title="AI: new results" href="${esc(`${BASE}/ai/feed.xml`)}">` : ''}
<script src="${esc(ovServe.url('kit-web.js'))}" defer></script>
<script src="${esc(ovServe.url('kit-web.js'))}" defer></script>
<script src="${esc(ovServe.url('kit-data-web.js'))}" defer></script>
<script src="${esc(ovServe.url('kit-data-web.js'))}" defer></script>
<script src="${esc(ovServe.url('motion.js'))}" defer></script>
<script src="${esc(ovServe.url('motion.js'))}" defer></script>
@@ -281,7 +279,7 @@ function home({ stats, rundown, topics } = {}) {
@@ -281,7 +279,7 @@ function home({ stats, rundown, topics } = {}) {
explore: { title: 'More on Report', items: [
explore: { title: 'More on Report', items: [
{ title: 'Every site\'s numbers', text: 'Page views, people, streams and how fast each service answers', href: '/stats', icon: 'ov:gauge' },
{ title: 'Every site\'s numbers', text: 'Page views, people, streams and how fast each service answers', href: '/stats', icon: 'ov:gauge' },
{ title: 'The Rundown', text: 'What streamers talked about, topic by topic', href: '/rundown', icon: 'ov:news' },
{ title: 'The Rundown', text: 'What streamers talked about, topic by topic', href: '/rundown', icon: 'ov:news' },
- { title: 'AI', text: 'Which AI does each job best, what it costs and how fast it is: Bench results from every room', href: '/ai', icon: 'ov:ai' },
+ { title: 'AI models', text: 'Which AI does each job best, what it costs and how fast it is: the comparison lives on OpenVibe.Compare', href: 'https://openvibe.compare/ai', icon: 'ov:ai' },
{ title: 'Status', text: 'Whether every OpenVibe service is up right now', href: 'https://openvibe.network/status', icon: 'ov:ping' },
{ title: 'Status', text: 'Whether every OpenVibe service is up right now', href: 'https://openvibe.network/status', icon: 'ov:ping' },
] },
] },
dev: { title: 'For developers', lede: 'Every number here is open JSON, readable from any site.', samples: [
dev: { title: 'For developers', lede: 'Every number here is open JSON, readable from any site.', samples: [
@@ -843,353 +841,6 @@ function scoreCostChart(entries) {
@@ -843,353 +841,6 @@ function scoreCostChart(entries) {
const TABS = [{ key: 'elo', label: 'Ranking' }, { key: 'pareto', label: 'Quality and cost' }, { key: 'cost', label: 'Cost per solved' }];
const TABS = [{ key: 'elo', label: 'Ranking' }, { key: 'pareto', label: 'Quality and cost' }, { key: 'cost', label: 'Cost per solved' }];
-function aiIndex({ overview, kinds, boards = [], rankings = [] }) {
- const o = readData(overview) || {};
- const list = kinds && kinds.data && Array.isArray(kinds.data.kinds) ? kinds.data.kinds : [];
- const benches = Array.isArray(o.list) ? o.list : [];
- const lastByKind = byKindFromOverview(o);
- const items = list.map((kind, i) => ({ kind, rows: rowsOf(boards[i]), available: Boolean(boards[i]) }));
- 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({
- label: 'Task kinds and their leaders', columns: [
- { key: 'job', label: 'Job', primary: true, html: true }, { key: 'leader', label: 'Leader', html: true },
- { key: 'quality', label: 'Quality', num: true, html: true }, { key: 'games', label: 'Games', num: true, fmt: 'int' },
- { key: 'cost', label: 'Cost/run', num: true, html: true },
- { key: 'speed', label: 'Speed/run', num: true, html: true }, { key: 'last', label: 'Last run', html: true },
- ],
- rows: rated.map(({ kind, rows }) => {
- const top = rows[0];
- return {
- job: `<a href="${aiPath(kind.id)}">${esc(kind.name || kind.id)}</a>`,
- leader: `${modelLink(top.subject)}${flag(top)}`,
- quality: rating(top),
- games: gamesOf(rows),
- cost: moneyCell(top.median_cost_usd),
- speed: speedCell(top.median_ms),
- last: lastByKind.get(kind.id) ? `<span title="${esc(lastByKind.get(kind.id))}">${esc(dayTime(lastByKind.get(kind.id)))}</span>` : '—',
- };
- }),
- }) : '';
- const waitRows = waiting.map(({ kind, available }) => kit.row({ href: aiPath(kind.id), title: kind.name || kind.id, sub: `${roomName(kind.room)} · ${available ? howRated(kind) : 'board unavailable'}`, trail: kit.icon('chevron') }));
- const waitList = waitRows.length ? kit.list(waitRows, { flush: true }) : '';
- const benchRow = (b, i) => {
- const r = rankings[i] && readData(rankings[i]);
- const top = r && r.rankings && Array.isArray(r.rankings.entries) ? r.rankings.entries[0] : null;
- return {
- benchmark: `<a href="${aiPath(b.id)}">${esc(b.title)}</a>`,
- kind: esc(b.kind || '—'),
- leader: top ? `${modelLink(top.model)} ${scoreCell(top.score)}` : '—',
- runs: b.runs == null ? 0 : b.runs,
- last: b.last_run ? esc(dayTime(b.last_run)) : 'No runs yet',
- };
- };
- const activeBench = benches.filter((b) => Number(b.runs) > 0);
- const idleBench = benches.filter((b) => !(Number(b.runs) > 0));
- const benchTable = activeBench.length ? kit.table({
- label: 'Benchmarks with published runs', columns: [
- { key: 'benchmark', label: 'Benchmark', primary: true, html: true }, { key: 'kind', label: 'Task kind' },
- { key: 'leader', label: 'Leader', html: true }, { key: 'runs', label: 'Runs', num: true, fmt: 'int' }, { key: 'last', label: 'Last run', html: true },
- ], rows: benches.map((b, i) => (Number(b.runs) > 0 ? benchRow(b, i) : null)).filter(Boolean),
- }) : '';
- const idleRows = benches.map((b, i) => (Number(b.runs) > 0 ? null : kit.row({ href: aiPath(b.id), title: b.title, sub: `${b.kind || 'benchmark'} · no run yet`, trail: kit.icon('chevron') }))).filter(Boolean);
- const idleList = idleRows.length ? kit.list(idleRows, { flush: true }) : '';
- const none = !rated.length && list.length;
- const body = kit.head({ title: 'AI', sub: 'Which AI does each job best, what it costs, and how fast it is', sticky: false })
- + benchState(overview, kinds, ...rankings, ...boards)
- + `<p class="rp-fine">Every number on this page is a published Bench result or vote. ${esc(METHOD)} <a href="/ai/method">How the runs are judged</a> · <a href="/ai/compare">Compare models</a> · <a href="/ai/feed.xml">New results (RSS)</a></p>`
- + (!list.length && !benches.length ? kit.empty({ icon: 'trophy', title: 'Bench is unavailable', text: 'The boards come back as soon as Bench answers.' }) : '')
- + (o.benchmarks != null || o.runs != null ? kit.stats([
- [o.benchmarks == null ? '—' : exact(o.benchmarks), o.benchmarks === 1 ? 'benchmark' : 'benchmarks'],
- [o.tasks == null ? '—' : exact(o.tasks), o.tasks === 1 ? 'task' : 'tasks'],
- [o.runs == null ? '—' : exact(o.runs), o.runs === 1 ? 'run' : 'runs'],
- [o.models == null ? '—' : exact(o.models), o.models === 1 ? 'model' : 'models'],
- [o.last_run ? dayTime(o.last_run) : 'No runs yet', 'last run'],
- ], { label: 'AI in numbers' }) : (rated.length ? kit.stats([
- [rated.length, rated.length === 1 ? 'job rated' : 'jobs rated'],
- [rated.reduce((sum, x) => sum + gamesOf(x.rows), 0), 'games played'],
- [list.length, 'jobs tracked'],
- ], { label: 'The boards in numbers' }) : ''))
- + (none ? kit.notice({ kind: 'info', text: `No model is rated on a task kind yet. ${list.length} jobs are set up; each board fills as people vote blind on two answers, or as graders score runs.` }) : '')
- + (leaders ? kit.section({ title: 'Task kinds', count: rated.length, body: leaders }) : '')
- + (benchTable ? kit.section({ title: 'Benchmarks with published runs', count: activeBench.length, body: benchTable }) : '')
- + (idleList ? kit.fold({ title: 'Waiting for their first run', meta: String(idleRows.length), body: idleList }) : '')
- + (waitList ? (none ? kit.fold({ title: 'Jobs being rated', meta: String(waiting.length), body: waitList }) : kit.fold({ title: 'Waiting for their first results', meta: String(waiting.length), body: waitList })) : '')
- + `<p class="rp-fine">The open data is Bench's: <a href="${BENCH}/docs">its API and datasets</a> stay on bench.openvibe.services.</p>`;
- return layout({ title: 'AI', description: 'Which AI does each job best, and what it costs: OpenVibe.Bench results, rendered by OpenVibe.Report.', body, active: '/ai', path: '/ai', agent: true });
-}
-
-function aiBoard({ kindId, kind, tab, board, cost, kinds, benchmarks, rankings = [], runs, overview }) {
- const name = kind && kind.name || kindId;
- const rows = rowsOf(board);
- const o = readData(overview) || {};
- const lastByKind = byKindFromOverview(o);
- const kindLast = lastByKind.get(kindId) || null;
- const latest = readData(runs);
- const latestRows = latest && Array.isArray(latest.runs) ? latest.runs : [];
- // The registry's verified runs on this kind's public benchmarks: a second, task-level reading even while the
- // blind board has no votes. A model's numbers across the kind's benchmarks are medians, never a sum of scores.
- const kindBenchmarks = (() => {
- const b = readData(benchmarks);
- return b && Array.isArray(b.benchmarks) ? b.benchmarks.filter((x) => x.kind === kindId) : [];
- })();
- const byModel = new Map();
- kindBenchmarks.forEach((b, i) => {
- const r = rankings[i] && readData(rankings[i]);
- const entries = r && r.rankings && Array.isArray(r.rankings.entries) ? r.rankings.entries : [];
- for (const e of entries) {
- if (!byModel.has(e.model)) byModel.set(e.model, { model: e.model, scores: [], costs: [], times: [], runs: 0, last_run: null });
- const x = byModel.get(e.model);
- if (n(e.score) != null) x.scores.push(n(e.score));
- if (n(e.cost_usd) != null) x.costs.push(n(e.cost_usd));
- if (n(e.ms) != null) x.times.push(n(e.ms));
- x.runs += Number(e.runs) || 0;
- if (e.last_run && (!x.last_run || e.last_run > x.last_run)) x.last_run = e.last_run;
- }
- });
- const median = (list) => { const v = list.filter((x) => x != null).sort((a, b) => a - b); if (!v.length) return null; const m = Math.floor(v.length / 2); return v.length % 2 ? v[m] : (v[m - 1] + v[m]) / 2; };
- const registryRows = [...byModel.values()].map((x) => ({ model: x.model, score: median(x.scores), cost: median(x.costs), speed: median(x.times), runs: x.runs, last_run: x.last_run })).sort((a, b) => (b.score == null ? -1 : b.score) - (a.score == null ? -1 : a.score) || a.model.localeCompare(b.model));
- const registryTable = registryRows.length ? kit.table({ caption: 'Published runs on this kind', columns: [
- { key: 'model', label: 'Model', primary: true, html: true }, { key: 'score', label: 'Median score', num: true, html: true },
- { key: 'runs', label: 'Runs', num: true, fmt: 'int' }, { key: 'cost', label: 'Cost/run', num: true, html: true },
- { key: 'speed', label: 'Speed/run', num: true, html: true }, { key: 'last', label: 'Last run', html: true },
- ], rows: registryRows.map((r) => ({ model: modelLink(r.model), score: scoreText(r.score), runs: r.runs, cost: moneyCell(r.cost), speed: speedCell(r.speed), last: r.last_run ? esc(dayTime(r.last_run)) : '—' })) }) : '';
- 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 (!played(rows) && registryRows.length) content = kit.notice({ kind: 'info', text: `No blind vote on this task kind yet. ${registryRows.length} ${registryRows.length === 1 ? 'model has' : 'models have'} verified runs below.` });
- 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 }, { key: 'speed', label: 'Speed/run', num: true, html: true },
- ], rows: points.map((r) => ({ m: `${modelLink(r.subject)}${r.frontier ? ` ${kit.badge('frontier', 'accent')}` : ''}`, q: share(r.quality), c: usd(r.costPerSolved), speed: speedCell(r.median_ms) })) }) : '');
- } else if (tab === 'cost') {
- const crows = rowsOf(cost).length ? rowsOf(cost) : rows;
- content = kit.table({ caption: 'Cost per solved task', columns: [
- { key: 'm', label: 'Model', primary: true, html: true }, { key: 'solved', label: 'Solved', num: true, fmt: 'int' },
- { key: 'entries', label: 'Entries', num: true, fmt: 'int' }, { key: 'total', label: 'Total cost', num: true, html: true },
- { key: 'per', label: 'Cost per solved', num: true, html: true }, { key: 'speed', label: 'Speed/run', num: true, html: true },
- ], rows: crows.map((r) => ({ m: modelLink(r.subject), solved: r.solved, entries: r.entries, total: usd(r.total_cost_usd), per: usd(r.cost_per_solved), speed: speedCell(r.median_ms) })) });
- } else {
- const floor = Math.floor((Math.min(...rows.map((r) => n(r.ci_lower) ?? n(r.elo) ?? 0)) - 25) / 50) * 50;
- content = kit.rank({ label: `${name}: ranking`, base: floor, fmt: 'int', items: rows.map((r) => {
- const lo = n(r.ci_lower), hi = n(r.ci_upper), elo = n(r.elo);
- const costText = n(r.median_cost_usd) != null ? `${usd(r.median_cost_usd)}/run` : null;
- const speedText = n(r.median_ms) != null ? `${msText(r.median_ms)}/run` : null;
- return { name: r.subject, href: modelPath(r.subject), value: elo, interval: lo != null && hi != null && elo != null ? Math.round(Math.max(hi - elo, elo - lo)) : null,
- note: [n(r.games) != null ? `${exact(r.games)} games` : '', n(r.win_rate) != null ? `${share(r.win_rate)} won` : '', costText, speedText, provisional(r) ? 'provisional' : ''].filter(Boolean).join(' · ') };
- }) }) + `<p class="rp-fine">${esc(METHOD)}</p>`
- + kit.table({ caption: 'Quality, price and speed side by side', columns: [
- { key: 'm', label: 'Model', primary: true, html: true }, { key: 'q', label: 'Quality', num: true, html: true },
- { key: 'games', label: 'Games', num: true, fmt: 'int' }, { key: 'won', label: 'Won', num: true, html: true },
- { key: 'cost', label: 'Cost/run', num: true, html: true }, { key: 'speed', label: 'Speed/run', num: true, html: true },
- ], rows: rows.map((r) => ({ m: modelLink(r.subject), q: rating(r), games: r.games, won: share(r.win_rate), cost: moneyCell(r.median_cost_usd), speed: speedCell(r.median_ms) })) });
- }
- const lastRunTable = latestRows.length ? kit.table({
- caption: 'Latest runs of this kind', columns: [
- { key: 'model', label: 'Model', primary: true, html: true }, { key: 'score', label: 'Score', num: true, html: true },
- { key: 'cost', label: 'Cost', num: true, html: true }, { key: 'speed', label: 'Time', num: true, html: true },
- { key: 'when', label: 'Published', html: true }, { key: 'how', label: 'How', html: true },
- ],
- rows: latestRows.map((r) => ({ model: modelLink(r.model), score: scoreCell(r.score), cost: usd(r.cost_usd), speed: speedCell(r.ms), when: esc(dayTime(r.created_at)), how: provenanceCell(r.provenance) })),
- }) : '<p class="rp-fine">No run of this task kind has been published yet.</p>';
- const head = kit.head({ title: name, sub: kind ? `${roomName(kind.room)} · ${howRated(kind)}${kindLast ? ` · ${lastRunText(kindLast)}` : ''}` : 'Bench results', back: { href: '/ai', label: 'AI' },
- tabs: played(rows) ? kit.tabs({ base: aiPath(kindId), items: TABS, active: tab, label: 'Board view' }) : '' });
- const body = head + benchState(kinds, board, cost, benchmarks, runs, overview)
- + `<section aria-label="${esc(TABS.find((t) => t.key === tab)?.label || 'Board')}">${content}</section>`
- + (registryRows.length ? kit.section({ title: 'Verified runs on this task kind', count: registryRows.length, body: (registryRows.length > 1 ? scoreCostChart(registryRows.map((r) => ({ model: r.model, score: r.score, cost_usd: r.cost, ms: r.speed, runs: r.runs, last_run: r.last_run }))) : '') + registryTable }) : '')
- + kit.section({ title: 'Latest runs', count: latestRows.length, body: lastRunTable })
- + datasetLink(kindId);
- return layout({ title: `${name}: AI`, description: `${name}: which AI does it best, what it costs and how fast it is.`, body, active: '/ai', path: aiPath(kindId), agent: true });
-}
-
-function aiBenchmark({ benchmark, rankings, runs }) {
- const b = readData(benchmark);
- const rank = readData(rankings);
- const latest = readData(runs);
- const entries = rank && rank.rankings && Array.isArray(rank.rankings.entries) ? rank.rankings.entries : [];
- const latestRows = latest && Array.isArray(latest.runs) ? latest.runs : [];
- const meta = b && b.benchmark ? b.benchmark : null;
- const definition = b && b.definition ? b.definition : null;
- const tasks = definition && Array.isArray(definition.tasks) ? definition.tasks : [];
- const version = b && b.version ? b.version.version : null;
- const last = entries.reduce((at, e) => (!at || (e.last_run && e.last_run > at) ? e.last_run : at), null)
- || (latestRows[0] && latestRows[0].created_at) || null;
- if (!meta) {
- const body = kit.head({ title: 'Benchmark', sub: 'Bench results', back: { href: '/ai', label: 'AI' }, sticky: false })
- + benchState(benchmark) + kit.empty({ icon: 'trophy', title: 'Bench is unavailable', text: 'This benchmark comes back as soon as Bench answers.' });
- return layout({ title: 'AI benchmark', description: 'A benchmark of AI runs from OpenVibe.Bench.', body, active: '/ai', path: '/ai', agent: true });
- }
- const chart = scoreCostChart(entries);
- const leaderboard = entries.length ? kit.table({
- caption: 'Verified results', columns: [
- { key: 'model', label: 'Model', primary: true, html: true }, { key: 'score', label: 'Score', num: true, html: true },
- { key: 'runs', label: 'Runs', num: true, fmt: 'int' }, { key: 'range', label: 'Best / worst', num: true, html: true },
- { key: 'cost', label: 'Cost/run', num: true, html: true }, { key: 'speed', label: 'Speed/run', num: true, html: true },
- { key: 'solved', label: 'Cost per solved', num: true, html: true }, { key: 'last', label: 'Last run', html: true },
- ],
- rows: entries.map((e) => ({
- model: modelLink(e.model), score: scoreCell(e.score), runs: e.runs,
- range: `${scoreCell(e.max_score)} / ${scoreCell(e.min_score)}`, cost: moneyCell(e.cost_usd), speed: speedCell(e.ms),
- solved: moneyCell(e.cost_per_solved), last: e.last_run ? esc(dayTime(e.last_run)) : '—',
- })),
- }) : kit.empty({ icon: 'trophy', title: 'No verified results yet', text: 'This benchmark has no verified run yet. Submitted runs are labelled on the latest-runs list and never mixed into the ranking.' });
- const latestTable = latestRows.length ? kit.table({
- caption: 'Latest runs', columns: [
- { key: 'model', label: 'Model', primary: true, html: true }, { key: 'score', label: 'Score', num: true, html: true },
- { key: 'cost', label: 'Cost', num: true, html: true }, { key: 'speed', label: 'Time', num: true, html: true },
- { key: 'when', label: 'Published', html: true }, { key: 'how', label: 'How', html: true },
- ],
- rows: latestRows.map((r) => ({ model: modelLink(r.model), score: scoreCell(r.score), cost: usd(r.cost_usd), speed: speedCell(r.ms), when: esc(dayTime(r.created_at)), how: provenanceCell(r.provenance) })),
- }) : '<p class="rp-fine">No run has been published on this benchmark yet.</p>';
- const body = kit.head({ title: meta.title, sub: `${meta.kind || 'Benchmark'}${version ? ` · version ${version}` : ''} · ${tasks.length} ${tasks.length === 1 ? 'task' : 'tasks'} · ${lastRunText(last)}`, back: { href: '/ai', label: 'AI' }, sticky: false })
- + benchState(benchmark, rankings, runs)
- + (meta.description ? `<p class="rp-lede">${esc(meta.description)}</p>` : '')
- + kit.stats([[entries.length, entries.length === 1 ? 'model ranked' : 'models ranked'], [latestRows.length, 'latest runs'], [tasks.length, tasks.length === 1 ? 'task' : 'tasks'], [last ? dayTime(last) : 'No runs yet', 'last run']], { label: `${meta.title} in numbers` })
- + chart + leaderboard
- + kit.section({ title: 'Latest runs', count: latestRows.length, body: latestTable })
- + `<p class="rp-fine"><a href="/ai/method">How the runs are judged</a> · <a href="/ai/compare?models=${entries.slice(0, 3).map((e) => encodeURIComponent(e.model)).join(',')}">Compare the leaders</a> · <a href="${BENCH}/api/v1/datasets/benchmarks.jsonl">Bench's open data</a></p>`;
- return layout({ title: `${meta.title}: AI benchmark`, description: `${meta.title}: verified AI results, cost and speed from OpenVibe.Bench.`, body, active: '/ai', path: aiPath(meta.id), agent: true });
-}
-
-function aiModel({ model, detail, legacy }) {
- const d = readData(detail) || null;
- const l = readData(legacy) || null;
- const runs = d && Array.isArray(d.recent_runs) ? d.recent_runs : [];
- const benchmarks = d && Array.isArray(d.benchmarks) ? d.benchmarks : [];
- const tasks = d && Array.isArray(d.tasks) ? d.tasks : [];
- const ratings = l && Array.isArray(l.ratings) ? l.ratings : [];
- const entries = l && Array.isArray(l.entries) ? l.entries : [];
- const hasRuns = Boolean(d && d.runs > 0);
- const history = runs.filter((r) => n(r.score) != null && ts(r.created_at) != null).sort((a, b) => String(a.created_at).localeCompare(String(b.created_at)));
- const historyChart = history.length >= 2 ? kit.chart({
- type: 'line', title: 'Score over time', note: 'Every public run, newest last.',
- x: { time: true, label: 'Published' }, y: { label: 'Score (0–100)', zero: false, fmt: (v) => `${Math.round(v)}` },
- series: [{ name: model, points: history.map((r) => [r.created_at, n(r.score)]) }], height: 220,
- }) : '';
- const best = tasks.slice(0, 5);
- const worst = tasks.length > 5 ? tasks.slice(-5).reverse() : [];
- const taskRow = (t) => ({
- task: `<span title="${esc(t.benchmark_title)}">${esc(t.task_id)}</span>`,
- benchmark: `<a href="${aiPath(t.benchmark_id)}">${esc(t.benchmark_title)}</a>`,
- score: scoreCell(t.score), runs: t.runs, last: t.last_run ? esc(dayTime(t.last_run)) : '—',
- });
- const taskColumns = [
- { key: 'task', label: 'Task', primary: true, html: true }, { key: 'benchmark', label: 'Benchmark', html: true },
- { key: 'score', label: 'Median score', num: true, html: true }, { key: 'runs', label: 'Runs', num: true, fmt: 'int' },
- { key: 'last', label: 'Last run', html: true },
- ];
- const taskTable = best.length ? kit.table({ caption: 'Best tasks', columns: taskColumns, rows: best.map(taskRow) }) : '';
- const worstTable = worst.length ? kit.table({ caption: 'Worst tasks', columns: taskColumns, rows: worst.map(taskRow) }) : '';
- const benchmarkRows = benchmarks.map((b) => ({
- benchmark: `<a href="${aiPath(b.benchmark_id)}">${esc(b.title)}</a>`, kind: esc(b.kind || '—'), runs: b.runs,
- score: scoreCell(b.score), range: `${scoreCell(b.best)} / ${scoreCell(b.worst)}`, cost: moneyCell(b.cost_usd), speed: speedCell(b.ms), last: b.last_run ? esc(dayTime(b.last_run)) : '—',
- }));
- const runTitles = new Map(benchmarks.map((b) => [b.benchmark_id, b.title]));
- const runRows = runs.slice(0, 12).map((r) => ({
- benchmark: `<a href="${aiPath(r.benchmark_id)}">${esc(runTitles.get(r.benchmark_id) || r.benchmark_id)}</a>`, score: scoreCell(r.score), cost: usd(r.cost_usd), speed: speedCell(r.ms),
- when: esc(dayTime(r.created_at)), how: provenanceCell(r.provenance),
- }));
- const legacyRows = ratings.map((r) => ({ job: `<a href="${aiPath(r.kind_id)}">${esc(r.kind_id)}</a>`, rating: rating(r), games: r.games, won: share(r.win_rate) }));
- const body = kit.head({ title: model, sub: hasRuns ? `${exact(d.runs)} verified ${d.runs === 1 ? 'run' : 'runs'} · ${lastRunText(d.last_run)}` : 'No verified runs yet', back: { href: '/ai', label: 'AI' }, sticky: false })
- + benchState(detail, legacy)
- + (hasRuns ? kit.stats([
- [exact(d.runs), d.runs === 1 ? 'run' : 'runs'], [benchmarks.length, benchmarks.length === 1 ? 'benchmark' : 'benchmarks'],
- [d.best_task ? scoreText(d.best_task.score) : '—', 'best task'], [d.worst_task ? scoreText(d.worst_task.score) : '—', 'worst task'],
- [d.total_cost_usd == null ? '—' : usd(d.total_cost_usd), 'spent on runs'], [d.last_run ? dayTime(d.last_run) : 'No runs yet', 'last run'],
- ], { label: `${model} in numbers` }) : '')
- + (historyChart ? kit.section({ title: 'Runs over time', body: historyChart }) : '')
- + (benchmarkRows.length ? kit.section({ title: 'By benchmark', count: benchmarks.length, body: kit.table({ caption: 'By benchmark', columns: [
- { key: 'benchmark', label: 'Benchmark', primary: true, html: true }, { key: 'kind', label: 'Task kind' }, { key: 'runs', label: 'Runs', num: true, fmt: 'int' },
- { key: 'score', label: 'Score', num: true, html: true }, { key: 'range', label: 'Best / worst', num: true, html: true }, { key: 'cost', label: 'Cost/run', num: true, html: true },
- { key: 'speed', label: 'Speed/run', num: true, html: true }, { key: 'last', label: 'Last run', html: true },
- ], rows: benchmarkRows }) }) : '')
- + (taskTable ? kit.section({ title: 'Best and worst tasks', count: best.length + worst.length, body: taskTable + worstTable }) : '')
- + (runRows.length ? kit.section({ title: 'Recent runs', count: runRows.length, body: kit.table({ caption: 'Recent runs', columns: [
- { key: 'benchmark', label: 'Benchmark', primary: true, html: true }, { key: 'score', label: 'Score', num: true, html: true },
- { key: 'cost', label: 'Cost', num: true, html: true }, { key: 'speed', label: 'Time', num: true, html: true }, { key: 'when', label: 'Published', html: true }, { key: 'how', label: 'How', html: true },
- ], rows: runRows }) }) : '')
- + (!hasRuns ? kit.empty({ icon: 'trophy', title: 'No verified runs yet', text: 'This model appears on a benchmark once a verified run is published. Its blind-board ratings, when it has any, are below.' }) : '')
- + (legacyRows.length ? kit.section({ title: 'Blind-board ratings', count: legacyRows.length, body: kit.table({ caption: 'Ratings by job', columns: [
- { key: 'job', label: 'Job', primary: true, html: true }, { key: 'rating', label: 'Rating', num: true, html: true }, { key: 'games', label: 'Games', num: true, fmt: 'int' }, { key: 'won', label: 'Won', num: true, html: true },
- ], rows: legacyRows }) }) : '')
- + (entries.length && !hasRuns ? `<p class="rp-fine">${exact(entries.length)} ${entries.length === 1 ? 'entry' : 'entries'} on Bench’s blind boards.</p>` : '')
- + `<p class="rp-fine"><a href="/ai/compare?models=${encodeURIComponent(model)}">Compare ${esc(model)} with another model</a> · <a href="/ai/method">How the runs are judged</a> · <a href="${BENCH}/api/v1/models/${encodeURIComponent(model)}">Bench’s own record</a></p>`;
- return layout({ title: `${model}: AI model`, description: `${model} across OpenVibe benchmark jobs: score, cost, speed, runs over time and its best and worst tasks.`, body, active: '/ai', path: modelPath(model), agent: true });
-}
-
-function aiCompare({ models = [], details = [], legacy = [], errors = [] }) {
- const rowsByBenchmark = new Map();
- const names = [];
- const cards = [];
- models.forEach((model, i) => {
- const d = readData(details[i]) || null;
- const l = readData(legacy[i]) || null;
- names.push(model);
- const list = d && Array.isArray(d.benchmarks) ? d.benchmarks : [];
- for (const b of list) {
- if (!rowsByBenchmark.has(b.benchmark_id)) rowsByBenchmark.set(b.benchmark_id, { title: b.title, cells: new Map() });
- rowsByBenchmark.get(b.benchmark_id).cells.set(model, b);
- }
- cards.push({
- model, runs: d ? d.runs : 0, cost: d ? d.total_cost_usd : null, last: d ? d.last_run : null,
- ratingCount: l && Array.isArray(l.ratings) ? l.ratings.length : 0,
- });
- });
- const rows = [...rowsByBenchmark.entries()].map(([id, x]) => {
- const row = { benchmark: `<a href="${aiPath(id)}">${esc(x.title)}</a>` };
- for (const model of models) {
- const b = x.cells.get(model);
- row[model] = b ? `<b>${scoreCell(b.score)}</b> <small>${esc(usd(b.cost_usd))}/run · ${esc(msText(b.ms))} · ${exact(b.runs)} runs</small>` : '—';
- }
- return row;
- });
- const columns = [{ key: 'benchmark', label: 'Benchmark', primary: true, html: true }, ...models.map((m) => ({ key: m, label: m, html: true }))];
- const form = `<form class="rp-ai-compare-form" method="get" action="/ai/compare" role="search" aria-label="Compare models">
-<label for="rp-compare-models">Two or three model ids, comma separated</label>
-<input id="rp-compare-models" name="models" value="${esc(models.join(', '))}" placeholder="gpt-5, claude-opus-4" autocomplete="off">
-<button class="rp-btn" type="submit">Compare</button></form>`;
- const cardHtml = cards.length ? kit.stats(cards.map((c) => [`${c.runs} · ${c.cost == null ? '—' : usd(c.cost)} · ${c.last ? day(c.last) : 'no runs'}`, c.model]), { label: 'Models in this comparison' }) : '';
- const body = kit.head({ title: 'Compare AI models', sub: 'The same benchmarks side by side: score, cost per run, speed and the last run', back: { href: '/ai', label: 'AI' }, sticky: false })
- + benchState(...details, ...legacy)
- + form
- + (errors.length ? kit.notice({ kind: 'warn', text: errors.join(' ') }) : '')
- + (models.length < 2 ? kit.empty({ icon: 'trophy', title: 'Pick two or three models', text: 'Put their Bench ids in the box above; the comparison is built from their published runs.' }) : '')
- + (rows.length ? kit.section({ title: 'Side by side', body: kit.table({ caption: 'Models side by side', columns, rows }) }) : (models.length >= 2 ? kit.empty({ icon: 'trophy', title: 'No shared benchmarks yet', text: 'These models have no verified run on a public benchmark in common.' }) : ''))
- + (cardHtml ? `<p class="rp-fine">${esc('Each card: verified runs · total run cost · last run, then the model.')}</p>${cardHtml}` : '')
- + `<p class="rp-fine">Cost and speed are the medians of each model's runs on that benchmark; a score is the median of its run scores. <a href="/ai/method">How the runs are judged</a>.</p>`;
- return layout({ title: 'Compare AI models', description: 'Compare two or three AI models on the same OpenVibe benchmarks: score, cost and speed.', body, active: '/ai', path: '/ai/compare', agent: true });
-}
-
-function aiMethod({ overview, kinds }) {
- const o = readData(overview) || {};
- const list = kinds && kinds.data && Array.isArray(kinds.data.kinds) ? kinds.data.kinds : [];
- const byJudge = new Map();
- for (const k of list) {
- if (!byJudge.has(k.judge)) byJudge.set(k.judge, []);
- byJudge.get(k.judge).push(k);
- }
- const judgeRows = [...byJudge.entries()].map(([judge, ks]) => ({
- how: `<b>${esc(JUDGE_SHORT[judge] || judge)}</b>`, judge: esc(judge), jobs: esc(ks.map((k) => k.name || k.id).join(', ')),
- }));
- const body = kit.head({ title: 'How the AI results are judged', sub: 'The method behind every score, price and speed on these pages', back: { href: '/ai', label: 'AI' }, sticky: false })
- + benchState(overview, kinds)
- + `<section class="rp-ai-method"><h2>What a score means</h2>
-<p>A score is a run’s result on one benchmark, from 0 to 100. Bench’s frozen benchmark version names its tasks, its judges and how the task scores combine; the default is the mean of the task scores. A model’s score on a benchmark is the median of its <b>verified</b> runs, so one bad run does not decide the page. The best and worst columns are the runs themselves, not a second estimate.</p>
-<h2>Quality, price and speed</h2>
-<p><b>Quality</b> is that score. <b>Price</b> is the median cost of a run in US dollars, as the service that ran it published it; <b>cost per solved</b> is the run’s cost divided by its success rate (score ÷ 100), so it is the price of one solved task. <b>Cost per solved</b> is the same figure, named plainly. <b>Speed</b> is the median wall-clock time of a run. The quality-against-cost chart marks the frontier: the models nothing beats on both.</p>
-<h2>How runs are judged</h2>
-<p>Each task kind has one main judge. A benchmark may also name more judges in its definition; the exact list is in its public definition at <code>/api/v1/benchmarks/:id</code>.</p>
-${judgeRows.length ? kit.table({ caption: 'Judges in use', columns: [{ key: 'how', label: 'How', html: true }, { key: 'judge', label: 'Bench judge' }, { key: 'jobs', label: 'Task kinds' }], rows: judgeRows }) : `<p class="rp-fine">${esc('Bench did not answer its kind list; the judge rules above are its published contract.')}</p>`}
-<h2>Verified and submitted</h2>
-<p>A <b>verified</b> run was executed by a service (openvibe.codes, Run, a Node worker); a <b>submitted</b> run was posted with a person’s token. The rankings read verified runs only, and a submitted run is labelled wherever the latest runs are listed.</p>
-<h2>How often, and what is missing</h2>
-<p>${o.runs == null ? 'Bench did not answer the public counts.' : `${exact(o.benchmarks)} public ${o.benchmarks === 1 ? 'benchmark' : 'benchmarks'}, ${exact(o.tasks)} ${o.tasks === 1 ? 'task' : 'tasks'}, ${exact(o.runs)} ${o.runs === 1 ? 'run' : 'runs'} from ${exact(o.models)} ${o.models === 1 ? 'model' : 'models'}; the newest run was ${o.last_run ? esc(dayTime(o.last_run)) : 'never published'}.`}</p>
-<p class="rp-fine">A model with no verified run has no score here; a benchmark with no run says so. The public reads are Bench’s API at <a href="${BENCH}/docs">bench.openvibe.services/docs</a>; the open data is <a href="${BENCH}/api/v1/datasets/benchmarks.jsonl">benchmarks.jsonl</a> and the per-kind datasets. <a href="/ai/feed.xml">New results (RSS)</a> · <a href="/api/v1/ai/results">the same as JSON</a>.</p>`;
- return layout({ title: 'How the AI results are judged', description: 'The method behind OpenVibe.Bench’s scores, costs and speeds, and how to read them.', body, active: '/ai', path: '/ai/method', agent: true });
-}
-
// ── The AI Exchange (plan G-P9): Billing's settlements, summed by day, model and access ────────────────────────
// ── The AI Exchange (plan G-P9): Billing's settlements, summed by day, model and access ────────────────────────
// Money is integer nano-USD and stays exact: this formatter works in BigInt, never in floats, and the title carries
// Money is integer nano-USD and stays exact: this formatter works in BigInt, never in floats, and the title carries
// the exact nano-USD the authority sent.
// the exact nano-USD the authority sent.
@@ -1468,4 +1119,4 @@ function notFound() {
@@ -1468,4 +1119,4 @@ function notFound() {
return layout({ title: 'Not found', robots: 'noindex, follow', body: `<header class="rp-page-h"><h1>Nothing here</h1><p class="rp-lede">That page does not exist, or its source has nothing for it. <a href="/">Back to Report</a>.</p></header>` });
return layout({ title: 'Not found', robots: 'noindex, follow', body: `<header class="rp-page-h"><h1>Nothing here</h1><p class="rp-lede">That page does not exist, or its source has nothing for it. <a href="/">Back to Report</a>.</p></header>` });
}
}
-module.exports = { home, stats, rundown, sessionReport, sharingChip, topic, episode, speaker, recap, channel, you, youSignedOut, me, meSignedOut, meError, publicStats, publicStatsPrivate, developers, aiIndex, aiBoard, aiBenchmark, aiModel, aiCompare, aiMethod, aiExchange, staffOnly, usdNano, paretoRows, paretoChart, notFound, layout, setRelease, configure };
+module.exports = { home, stats, rundown, sessionReport, sharingChip, topic, episode, speaker, recap, channel, you, youSignedOut, me, meSignedOut, meError, publicStats, publicStatsPrivate, developers, aiExchange, staffOnly, usdNano, paretoRows, paretoChart, notFound, layout, setRelease, configure };
@@ -27,36 +27,34 @@ const { boot, check, done, stubFetch, jsonResponse } = require('./helpers/boot')
@@ -27,36 +27,34 @@ const { boot, check, done, stubFetch, jsonResponse } = require('./helpers/boot')
};
};
const t = await boot({ fetchImpl, env: { OV_BENCH_INTERNAL_URL: 'http://127.0.0.1:5230', OV_AI_INTERNAL_URL: 'http://127.0.0.1:5231' } });
const t = await boot({ fetchImpl, env: { OV_BENCH_INTERNAL_URL: 'http://127.0.0.1:5230', OV_AI_INTERNAL_URL: 'http://127.0.0.1:5231' } });
try {
try {
- await check('a dated Bench name meets its catalogue model: maker and prices', async () => {
- const r = await t.get('/ai/leaderboard');
+ // The pages moved to OpenVibe.Compare (2026-11-13); the join is checked through the JSON API Compare reads.
+ const rowsOf = async () => {
+ const r = await t.get('/api/v1/ai/leaderboard?limit=100');
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
- const row = r.text.slice(r.text.indexOf('gpt-5-nano-2025-08-07'));
- assert.ok(r.text.includes('gpt-5-nano-2025-08-07'), 'the rated model');
- assert.ok(row.includes('$0.05') && row.includes('$0.4'), 'its catalogue prices');
- assert.ok(r.text.includes('OpenAI') && r.text.includes('ov-aim--openai'), 'its maker, named and marked');
+ return r.json().rows;
+ };
+ const prices = (row) => row.provider_offers.flatMap((o) => (o.price ? [o.price.in, o.price.out] : [])).filter((x) => x != null).map(Number);
+ await check('a dated Bench name meets its catalogue model: maker and prices', async () => {
+ const row = (await rowsOf()).find((x) => x.id === 'gpt-5-nano-2025-08-07');
+ assert.ok(row, 'the rated model');
+ assert.ok(prices(row).includes(0.4) && prices(row).includes(0.05), 'its catalogue prices');
+ assert.match(String(row.maker), /openai/i, 'its maker');
});
});
await check('a catalogue model without Bench games is listed, not rated', async () => {
await check('a catalogue model without Bench games is listed, not rated', async () => {
- const r = await t.get('/ai/leaderboard');
- assert.ok(r.text.includes('deepseek/deepseek-chat'), 'the unrated model is listed');
- assert.ok(r.text.indexOf('gpt-5-nano-2025-08-07') < r.text.indexOf('deepseek/deepseek-chat'), 'rated models come first');
- assert.ok(r.text.includes('$0.28'), 'with its real price');
+ const rows = await rowsOf();
+ const ids = rows.map((x) => x.id);
+ assert.ok(ids.includes('deepseek/deepseek-chat'), 'the unrated model is listed');
+ assert.ok(ids.indexOf('gpt-5-nano-2025-08-07') < ids.indexOf('deepseek/deepseek-chat'), 'rated models come first');
+ assert.ok(prices(rows.find((x) => x.id === 'deepseek/deepseek-chat')).includes(0.28), 'with its real price');
});
});
await check('a base rating with no games is not a rating', async () => {
await check('a base rating with no games is not a rating', async () => {
- const r = await t.get('/api/v1/ai/leaderboard');
- const zero = r.json().rows.find((x) => x.id === 'model-zero');
+ const zero = (await rowsOf()).find((x) => x.id === 'model-zero');
assert.strictEqual(zero, undefined, 'a sparse Bench-only subject is withheld, including its count');
assert.strictEqual(zero, undefined, 'a sparse Bench-only subject is withheld, including its count');
- const page = await t.get('/ai/models/model-zero');
- assert.ok(page.text.includes('Not rated on any task yet'), 'the model page says so');
- });
- await check('unrated models list what the Exchange routes first, then the cheapest; a note in brackets is not a name', async () => {
- const views = require('../server/views/ai-comparison');
- const html = views.providers({ providers: [{ provider: 'x', provider_name: 'Abliteration.ai (the uncensored tier)', model: 'm', price: { in: 0, out: 0 }, routed: false }] });
- assert.ok(html.includes('Abliteration.ai<') && !html.includes('the uncensored tier'), 'the provider is named plainly');
- assert.ok(html.includes('Free') && !html.includes('$0.00'), 'a price of zero reads Free');
});
});
- await check('the providers page lists every offer', async () => {
- const r = await t.get('/ai/providers');
- assert.ok(r.text.includes('OpenAI') && r.text.includes('DeepSeek'), 'both providers');
+ await check('the AI pages answer 301 to OpenVibe.Compare with their path and query', async () => {
+ const r = await t.get('/ai/providers?x=1', { redirect: 'manual' });
+ assert.strictEqual(r.status, 301);
+ assert.strictEqual(r.headers.get('location'), 'https://openvibe.compare/ai/providers?x=1');
});
});
} finally {
} finally {
await t.close();
await t.close();
@@ -47,11 +47,9 @@ const catalogue = { models:[{ slug:'openai/gpt-5-nano', offers:[{provider:'opena
@@ -47,11 +47,9 @@ const catalogue = { models:[{ slug:'openai/gpt-5-nano', offers:[{provider:'opena
return source(url,opts);
return source(url,opts);
}});
}});
try {
try {
- await check('pages list every market model, cheapest price, route marks and measured-only routed rows',async()=>{
- const r=await t.get('/ai/leaderboard');assert.strictEqual(r.status,200);assert(r.text.includes('qwen/unrated'));assert(r.text.includes('$0.01'));assert(r.text.includes('Routed here'));assert(r.text.includes('Not routed here'));
- assert(r.text.indexOf('gpt-5-nano-2025-08-07')<r.text.indexOf('qwen/unrated'));
- const p=await t.get('/ai/providers');assert(p.text.includes('Cheaper fixture'));assert(/1\.2[0-9]*\s*s/.test(p.text));assert(p.text.includes('Not routed here'));
- const api=await t.get('/api/v1/ai/leaderboard');assert(api.json().rows.some(r=>r.id==='qwen/unrated'&&!r.routed));
+ await check('the API lists every market model, routed and not, rated first',async()=>{
+ const api=await t.get('/api/v1/ai/leaderboard?limit=100');assert.strictEqual(api.status,200);const rows=api.json().rows;const ids=rows.map(r=>r.id);
+ assert(rows.some(r=>r.id==='qwen/unrated'&&!r.routed),'the unrouted market model is listed');assert(ids.includes('gpt-5-nano-2025-08-07'),'the rated one too');
});
});
} finally { await t.close(); }
} finally { await t.close(); }
done();
done();
new file mode 100644
new file mode 100644
@@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
+'use strict';
+/**
+ * The AI model comparison moved to OpenVibe.Compare on 2026-11-13 (the owner: "This kinda shit is supposed to be a part
+ * of openvibe.compare"). Every /ai page answers 301 there with its path and query; the Arena's rounds and votes and the
+ * staff page /ai/exchange stay; the bar has no AI tab; the sitemap lists no /ai page; the JSON API Compare reads stays.
+ */
+const assert = require('assert');
+const { boot, check, done } = require('./helpers/boot');
+
+(async () => {
+ const t = await boot();
+ try {
+ await check('every /ai page answers 301 to the same path on OpenVibe.Compare', async () => {
+ for (const path of ['/ai', '/ai/leaderboard?kind=chat.arena', '/ai/providers', '/ai/models/openai%2Fgpt-5-nano', '/ai/compare/a-vs-b',
+ '/ai/compare?a=x&b=y', '/ai/method', '/ai/feed.xml', '/ai/chat.arena', '/ai/bmk_abc']) {
+ const r = await t.get(path);
+ assert.strictEqual(r.status, 301, path);
+ assert.strictEqual(r.headers.get('location'), `https://openvibe.compare${path}`, path);
+ }
+ });
+ await check('the Arena and the staff page stay on Report', async () => {
+ const arena = await t.get('/ai/arena');
+ assert.notStrictEqual(arena.status, 301, 'the Arena is served here');
+ const staff = await t.get('/ai/exchange');
+ assert.notStrictEqual(staff.status, 301, 'the staff page is not moved');
+ });
+ await check('no AI tab, no /ai page in the sitemap, the JSON API stays', async () => {
+ const home = await t.get('/');
+ assert.ok(!/href="\/ai"/.test(home.text), 'the bar has no AI tab');
+ const map = await t.get('/sitemap.xml');
+ assert.ok(!/openvibe\.report\/ai[/<]/.test(map.text), 'no /ai page in the sitemap');
+ const llms = await t.get('/llms.txt');
+ assert.match(llms.text, /https:\/\/openvibe\.compare\/ai/, 'llms.txt names where the comparison lives');
+ const api = await t.get('/api/v1/ai/picks');
+ assert.notStrictEqual(api.status, 301, 'the API is not moved');
+ });
+ } finally {
+ await t.close();
+ }
+ done();
+})().catch((error) => { console.error(error); process.exitCode = 1; });
deleted file mode 100644
deleted file mode 100644
@@ -1,187 +0,0 @@
@@ -1,187 +0,0 @@
-'use strict';
-
-const assert = require('assert');
-const { boot, check, done, stubFetch, jsonResponse } = require('./helpers/boot');
-
-(async () => {
- let time = Date.parse('2026-10-28T12:00:00Z');
- let down = false;
- const source = stubFetch();
- const calls = [];
- const fetchImpl = async (url, opts) => {
- const u = new URL(String(url));
- if (u.port !== '5230') return source(url, opts);
- calls.push(u.pathname + u.search);
- if (down) throw new Error('Bench offline');
- if (u.pathname === '/api/v1/kinds') return jsonResponse({ count: 1, kinds: [{ id: 'design.page', room: 'design', name: 'Page <design>', judge: 'human_pairwise' }] });
- if (['/api/v1/boards/design.page', '/api/v1/ratings/design.page'].includes(u.pathname)) {
- const type = u.pathname.includes('/ratings/') ? 'elo' : u.searchParams.get('type');
- const rows = {
- elo: [
- { subject: 'openai/gpt-4o', elo: 1043, ci_lower: 1005, ci_upper: 1081, games: 20, win_rate: 0.75, provisional: false },
- { subject: 'model-b', elo: 980, ci_lower: 940, ci_upper: 1020, games: 20, win_rate: 0.25, provisional: false },
- ],
- pareto: [
- { subject: 'openai/gpt-4o', quality: 0.8 },
- { subject: 'model-b', quality: 0.6 },
- ],
- cost: [
- { subject: 'model-b', entries: 8, solved: 6, total_cost_usd: 0.3, cost_per_solved: 0.05 },
- { subject: 'openai/gpt-4o', entries: 8, solved: 8, total_cost_usd: 0.8, cost_per_solved: 0.1 },
- ],
- }[type];
- return rows ? jsonResponse({ board: { kind_id: 'design.page', type, rows } }) : jsonResponse({}, 404);
- }
- if (u.pathname === '/api/v1/models/openai%2Fgpt-4o') return jsonResponse({ model: 'openai/gpt-4o', ratings: [{ kind_id: 'design.page', elo: 1043, ci_lower: 1005, ci_upper: 1081, games: 20, win_rate: 0.75 }], entries: [{ kind_id: 'design.page' }], total_cost_usd: 0.8 });
- return jsonResponse({}, 404);
- };
- const t = await boot({ fetchImpl, now: () => time, env: { OV_BENCH_INTERNAL_URL: 'http://127.0.0.1:5230' } });
- try {
- await check('/ai renders the comparison hub and structured data', async () => {
- const r = await t.get('/ai');
- assert.strictEqual(r.status, 200);
- for (const s of ['AI model leaderboard', 'openai/gpt-4o', 'Best overall', 'Dataset', 'FAQPage']) assert.ok(r.text.includes(s), s);
- assert.ok(!r.text.includes('Page <design>'));
- // A phone shows the parts below the top picks as tabs, the leaderboard first; every part stays in the HTML.
- assert.ok(r.text.includes('<div class="ov-parts" data-parts='), 'the parts are tabs on a phone');
- assert.ok(r.text.includes('<label for="ovp-board-leaderboard">Leaderboard <span class="ov-parts-n">'), 'the leaderboard tab names its count');
- assert.ok(r.text.indexOf('ovp-board-leaderboard') < r.text.indexOf('ovp-board-questions'), 'the leaderboard first, the questions last');
- assert.ok(r.text.includes('.ov-parts-bar{display:flex}'), 'its look is in the page, so nothing jumps');
- });
-
- await check('Shared Elo shows ranks, intervals and win rates', async () => {
- const r = await t.get('/ai/design.page');
- assert.strictEqual(r.status, 200);
- for (const s of ['<ol class="ovk-rank"', '<b>1,043</b><small>± 38</small>', '75% won', 'Ranking', '95% interval', 'dataset JSONL']) assert.ok(r.text.includes(s), s);
- const data = await t.get('/api/v1/ai/ratings/design.page');
- assert.strictEqual(data.status, 200, data.text);
- assert.strictEqual(data.json().board.rows[0].elo, 1043);
- assert.strictEqual(data.json().board.rows[0].games, 20);
- assert.strictEqual(data.headers.get('access-control-allow-origin'), '*');
- const preflight = await t.get('/api/v1/ai/ratings/design.page', { method: 'OPTIONS' });
- assert.strictEqual(preflight.status, 204);
- assert.match(preflight.headers.get('access-control-allow-headers'), /traceparent/);
- });
-
- await check('Pareto has an inline SVG, frontier labels and an exact-value table', async () => {
- const r = await t.get('/ai/design.page?tab=pareto');
- assert.strictEqual(r.status, 200);
- for (const s of ['ovk-chart--scatter', '<svg', 'class="ovk-chart-front"', 'Quality against cost per solved task', 'Cost per solved task (USD', 'frontier', 'model-b', 'dataset JSONL', '<table']) assert.ok(r.text.includes(s), s);
- assert.ok(r.text.includes('data-label="Quality">80%</td>') && r.text.includes('data-label="Cost per solved">$0.10</td>'), 'the exact numbers, not dashes');
- });
-
- await check('cost and model views quote Bench numbers', async () => {
- const cost = await t.get('/ai/design.page?tab=cost');
- assert.strictEqual(cost.status, 200);
- assert.ok(cost.text.includes('Cost per solved'));
- assert.ok(cost.text.includes('$0.05'));
- const model = await t.get('/ai/models/openai%2Fgpt-4o');
- assert.strictEqual(model.status, 200);
- assert.ok(model.text.includes('design.page'));
- assert.ok(model.text.includes('1043') && model.text.includes('1005–1081'));
- });
-
- await check('invalid kinds and models do not call Bench', async () => {
- const before = calls.length;
- assert.strictEqual((await t.get('/ai/bad!kind')).status, 404);
- assert.strictEqual((await t.get('/ai/models/bad!model')).status, 404);
- assert.strictEqual(calls.length, before);
- });
-
- await check('sitemap includes AI index and every kind', async () => {
- const r = await t.get('/sitemap.xml');
- assert.strictEqual(r.status, 200);
- assert.ok(r.text.includes('<loc>https://openvibe.report/ai</loc>'));
- assert.ok(r.text.includes('<loc>https://openvibe.report/ai/design.page</loc>'));
- });
-
- await check('the comparison pages remain rendered during a Bench outage', async () => {
- down = true;
- time += 60_001;
- for (const path of ['/ai', '/ai/design.page', '/ai/design.page?tab=pareto', '/ai/models/openai%2Fgpt-4o']) {
- const r = await t.get(path);
- assert.strictEqual(r.status, 200, path);
- }
- });
- } finally { await t.close(); }
-
- await check('the comparison hub replaces the old AI landing page', async () => {
- const fresh = 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: 2, kinds: [{ id: 'design.page', room: 'design', name: 'Page', judge: 'human_pairwise' }, { id: 'speech.asr', room: 'speech', name: 'Speech', judge: 'automatic_grader' }] });
- if ((/^\/api\/v1\/(boards|ratings)\//.test(u.pathname))) return jsonResponse({ board: { rows: [] } });
- return jsonResponse({}, 404);
- },
- env: { OV_BENCH_INTERNAL_URL: 'http://127.0.0.1:5230' },
- });
- try {
- const r = await fresh.get('/ai');
- assert.ok(r.text.includes('AI model leaderboard') && r.text.includes('Best overall') && r.text.includes('FAQPage'), 'new hub and structured data render');
- assert.ok(!r.text.includes('No model is rated on a task kind yet'), 'old landing page copy is gone');
- const b = await fresh.get('/ai/design.page');
- assert.ok(b.text.includes('No votes yet') && b.text.includes('href="https://openvibe.design/"') && b.text.includes('Vote on Design'), 'a blind-vote job sends people to vote');
- assert.ok(!b.text.includes('class="ovk-tabs'), 'an empty board has no tabs');
- const g = await fresh.get('/ai/speech.asr');
- assert.ok(g.text.includes('No results yet') && !g.text.includes('Vote on'), 'a graded job has nothing to vote on');
- } finally { await fresh.close(); }
- });
-
- await check('an unjudged model does not become a rated leader', 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 ((/^\/api\/v1\/(boards|ratings)\//.test(u.pathname))) 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('AI model leaderboard') && r.text.includes('Best overall'), 'hub remains available without a rating');
- 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 {
- const r = await empty.get('/ai');
- assert.strictEqual(r.status, 200);
- assert.ok(r.text.includes('AI model leaderboard') && r.text.includes('Leaderboard'));
- } finally { await empty.close(); }
- });
-
- await check('comparison pages show fixture measurements, filter parameters and canonical model pairs', async () => {
- const fixture = await boot({
- aiCatalogue: { catalog: async () => ({ generated_at: '2026-11-09T00:00:00Z', models: [{ slug: 'maker/model-a', maker: 'Maker A', weights: 'open', status: 'current', offers: [{ provider: 'Provider One', price: { in: 1, out: 0.5 }, speed: { p50_ms: 120 }, uptime: 0.99, status: 'down', status_reason: 'the provider says the account is out of credit', status_since: '2026-11-09T09:00:00Z' }] }, { slug: 'maker/model-b', maker: 'Maker B', weights: 'closed', status: 'current', offers: [] }] }) },
- fetchImpl: async (url, opts) => {
- const u = new URL(String(url));
- if (u.port === '5230' && u.pathname === '/api/v1/kinds') return jsonResponse({ kinds: [{ id: 'code', name: 'Code' }] });
- if (u.port === '5230' && u.pathname === '/api/v1/ratings/code') return jsonResponse({ board: { rows: [{ subject: 'maker/model-a', elo: 1250, ci_lower: 1200, ci_upper: 1300, games: 40 }, { subject: 'maker/model-b', elo: 1270, ci_lower: 1250, ci_upper: 1300, games: 20 }] } });
- if (u.port === '5230' && u.pathname.includes('/summary')) return jsonResponse({ model: 'maker/model-a', runs: 4 });
- return source(url, opts);
- }, env: { OV_BENCH_INTERNAL_URL: 'http://127.0.0.1:5230' },
- });
- try {
- const hub = await fixture.get('/ai');
- assert.ok(hub.text.includes('AI model leaderboard — November 2026'));
- assert.ok(hub.text.includes('± 50') && hub.text.includes('40 games') && hub.text.includes('1–2') && hub.text.includes('Dataset') && hub.text.includes('FAQPage'));
- assert.ok(hub.text.includes('Maker A') && !hub.text.includes('letter-avatar'));
- const filtered = await fixture.get('/ai/leaderboard?maker=Maker%20A&weights=open&price=low&status=current&kind=code');
- assert.ok(filtered.text.includes('maker/model-a') && !filtered.text.includes('maker/model-b'));
- const provider = await fixture.get('/ai/providers');
- assert.ok(provider.text.includes('Provider One') && provider.text.includes('Not measured yet'), 'an offer without requests has no measurement');
- assert.ok(provider.text.includes('Down') && provider.text.includes('the provider says the account is out of credit') && provider.text.includes('since 2026-11-09 09:00 UTC'), 'the measured status and its reason are shown');
- assert.strictEqual((await fixture.get('/ai/compare?models=maker%2Fmodel-a,maker%2Fmodel-b')).status, 301);
- assert.strictEqual((await fixture.get('/ai/compare/maker%2Fmodel-a-vs-maker%2Fmodel-b')).status, 200);
- assert.strictEqual((await fixture.get('/ai/compare/maker%2Fmodel-b-vs-maker%2Fmodel-a')).status, 301);
- } finally { await fixture.close(); }
- });
-
- done();
-})().catch((e) => { console.error(e); process.exit(1); });
deleted file mode 100644
deleted file mode 100644
@@ -1,164 +0,0 @@
@@ -1,164 +0,0 @@
-'use strict';
-
-/**
- * The AI section (owner, 2026-11-08): Bench's pages moved into Report. The overview, one task kind, one benchmark,
- * one model, the comparison, the method page and the new-results feeds read only Bench's public answers.
- */
-const assert = require('assert');
-const { boot, check, done, stubFetch, jsonResponse } = require('./helpers/boot');
-
-const BMK = 'bmk_01M65KA3H4Y4ZENBYYZQA8SRNN';
-const RUN_A = 'brn_01M65KA3H4Y4ZENBYYZQA8SRN1';
-const RUN_B = 'brn_01M65KA3H4Y4ZENBYYZQA8SRN2';
-
-const overview = {
- kinds: 2, benchmarks: 1, tasks: 2, runs: 3, models: 2, last_run: '2026-11-07T10:00:00.000Z',
- list: [{ id: BMK, title: 'Playable games arena', kind: 'design.page', description: 'A test benchmark.', latest_version: 1, runs: 3, last_run: '2026-11-07T10:00:00.000Z' }],
- recent: [
- { id: RUN_B, benchmark_id: BMK, model: 'model-b', score: 70, cost_usd: 0.04, ms: 2000, provenance: 'verified', created_at: '2026-11-07T10:00:00.000Z' },
- { id: RUN_A, benchmark_id: BMK, model: 'model-a', score: 90, cost_usd: 0.02, ms: 1000, provenance: 'verified', created_at: '2026-11-06T10:00:00.000Z' },
- ],
-};
-
-const runs = {
- count: 2, offset: 0, last_run: '2026-11-07T10:00:00.000Z',
- runs: [
- { id: RUN_B, benchmark_id: BMK, model: 'model-b', score: 70, cost_usd: 0.04, ms: 2000, tokens: 20, provenance: 'verified', created_at: '2026-11-07T10:00:00.000Z' },
- { id: RUN_A, benchmark_id: BMK, model: 'model-a', score: 90, cost_usd: 0.02, ms: 1000, tokens: 10, provenance: 'verified', created_at: '2026-11-06T10:00:00.000Z' },
- ],
-};
-
-const rankings = { rankings: { benchmark: BMK, metric: 'score', entries: [
- { model: 'model-a', runs: 2, score: 85, cost_usd: 0.02, ms: 1000, cost_per_solved: 0.024, min_score: 80, max_score: 90, last_run: '2026-11-06T10:00:00.000Z' },
- { model: 'model-b', runs: 1, score: 70, cost_usd: 0.04, ms: 2000, cost_per_solved: 0.057, min_score: 70, max_score: 70, last_run: '2026-11-07T10:00:00.000Z' },
-] } };
-
-const benchmark = {
- benchmark: { id: BMK, title: 'Playable games arena', kind: 'design.page', description: 'A test benchmark.' },
- version: { version: 1, created_at: '2026-10-30T05:07:52.000Z' },
- definition: { tasks: [{ id: 't1', prompt: 'Build one screen.' }, { id: 't2', prompt: 'Build another.' }] },
- task_meta: [],
-};
-
-const modelDetail = {
- model: 'model-a', runs: 2,
- benchmarks: [{ benchmark_id: BMK, title: 'Playable games arena', kind: 'design.page', runs: 2, score: 85, best: 90, worst: 80, cost_usd: 0.02, ms: 1000, last_run: '2026-11-06T10:00:00.000Z' }],
- tasks: [
- { benchmark_id: BMK, benchmark_title: 'Playable games arena', task_id: 't1', runs: 2, score: 90, best: 90, worst: 90, last_run: '2026-11-06T10:00:00.000Z' },
- { benchmark_id: BMK, benchmark_title: 'Playable games arena', task_id: 't2', runs: 2, score: 80, best: 80, worst: 80, last_run: '2026-11-06T10:00:00.000Z' },
- ],
- best_task: { task_id: 't1', score: 90 }, worst_task: { task_id: 't2', score: 80 },
- total_cost_usd: 0.04, total_ms: 2000, total_tokens: 20, last_run: '2026-11-06T10:00:00.000Z',
- recent_runs: [
- ...runs.runs.filter((r) => r.model === 'model-a'),
- { id: 'brn_01M65KA3H4Y4ZENBYYZQA8SRN3', benchmark_id: BMK, model: 'model-a', score: 80, cost_usd: 0.02, ms: 1200, tokens: 10, provenance: 'verified', created_at: '2026-11-05T10:00:00.000Z' },
- ],
-};
-
-const legacy = { model: 'model-a', ratings: [{ kind_id: 'design.page', elo: 1043, ci_lower: 1005, ci_upper: 1081, games: 20, win_rate: 0.75 }], entries: [], total_cost_usd: 0.04 };
-
-(async () => {
- const source = stubFetch();
- const fetchImpl = async (url, opts) => {
- const u = new URL(String(url));
- if (u.port !== '5230') return source(url, opts);
- const p = u.pathname;
- if (p === '/api/v1/kinds') return jsonResponse({ count: 1, kinds: [{ id: 'design.page', room: 'design', name: 'Design page arena', judge: 'human_pairwise' }] });
- if (['/api/v1/boards/design.page', '/api/v1/ratings/design.page'].includes(p)) {
- const type = p.includes('/ratings/') ? 'elo' : u.searchParams.get('type');
- const rows = type === 'elo'
- ? [{ subject: 'model-a', elo: 1043, ci_lower: 1005, ci_upper: 1081, games: 20, win_rate: 0.75, median_cost_usd: 0.02, median_ms: 1000, provisional: false }]
- : type === 'pareto' ? [{ subject: 'model-a', quality: 0.8 }] : [{ subject: 'model-a', entries: 2, solved: 2, total_cost_usd: 0.04, cost_per_solved: 0.02, median_ms: 1000 }];
- return jsonResponse({ board: { kind_id: 'design.page', type, rows } });
- }
- if (p === '/api/v1/overview') return jsonResponse(overview);
- if (p === '/api/v1/benchmarks') return jsonResponse({ count: 1, benchmarks: overview.list });
- if (p === `/api/v1/benchmarks/${BMK}`) return jsonResponse(benchmark);
- if (p === '/api/v1/rankings') return jsonResponse(rankings);
- if (p === '/api/v1/runs') return jsonResponse(runs);
- if (p === '/api/v1/models/model-a') return jsonResponse(legacy);
- if (p === '/api/v1/models/model-a/summary') return jsonResponse(modelDetail);
- if (p === '/api/v1/models/model-b/summary') return jsonResponse({ ...modelDetail, model: 'model-b', runs: 1, total_cost_usd: 0.04, recent_runs: runs.runs.filter((r) => r.model === 'model-b') });
- return jsonResponse({ error: 'no stub for ' + p }, 404);
- };
- const t = await boot({ fetchImpl, env: { OV_BENCH_INTERNAL_URL: 'http://127.0.0.1:5230' } });
- try {
- await check('the AI overview renders the leaderboard hub from the Bench fixture', async () => {
- const r = await t.get('/ai');
- assert.strictEqual(r.status, 200);
- for (const s of ['AI model leaderboard', 'model-a', '1043', 'Best overall', 'FAQPage', 'Dataset']) assert.ok(r.text.includes(s), s);
- assert.ok(r.text.includes('href="/ai/leaderboard"') && r.text.includes('/api/v1/ai/leaderboard'));
- });
-
- await check('one task kind shows quality, price and speed side by side and its latest runs', async () => {
- const r = await t.get('/ai/design.page');
- assert.strictEqual(r.status, 200);
- for (const s of ['Quality, price and speed side by side', 'Cost/run', 'Speed/run', '$0.02', '1 s', 'Latest runs', 'model-a', 'Nov 7']) assert.ok(r.text.includes(s), s);
- });
-
- await check('one benchmark page has a ranking, an SVG and the latest runs', async () => {
- const r = await t.get(`/ai/${BMK}`);
- assert.strictEqual(r.status, 200);
- for (const s of ['Playable games arena', 'Score against cost per run', 'ovk-chart--scatter', 'model-a', '85', '$0.02', 'Cost per solved', 'Latest runs', 'version 1']) assert.ok(r.text.includes(s), s);
- });
-
- await check('one model page uses the comparison profile and available Bench task data', async () => {
- const r = await t.get('/ai/models/model-a');
- assert.strictEqual(r.status, 200);
- for (const s of ['Providers', 'Task ratings', 'Playable games arena', 'snapshot', 'Product', 'Try it in Chat', 'Use it from code']) assert.ok(r.text.includes(s), s);
- assert.ok(!r.text.includes('<span title='), 'a stat is escaped HTML, not markup');
- });
-
- await check('the old query compare redirects to a canonical pair page', async () => {
- const two = await t.get('/ai/compare?models=model-a,model-b');
- assert.strictEqual(two.status, 301);
- assert.strictEqual(two.headers.get('location'), '/ai/compare/model-a-vs-model-b');
- const pair = await t.get('/ai/compare/model-a-vs-model-b');
- assert.strictEqual(pair.status, 200);
- assert.ok(pair.text.includes('model-a') && pair.text.includes('model-b'), 'both models');
- const one = await t.get('/ai/compare?models=model-a');
- assert.strictEqual(one.status, 200);
- assert.ok(one.text.includes('Choose two models to compare'), 'ask for a second model');
- });
-
- await check('an unknown benchmark id is a 404 once Bench has answered its list', async () => {
- assert.strictEqual((await t.get('/ai/bmk_01M65KA3H4Y4ZENBYYZQA8SRN9')).status, 404);
- });
-
- await check('the method page explains judging, verification and cost', async () => {
- const r = await t.get('/ai/method');
- assert.strictEqual(r.status, 200);
- for (const s of ['What a score means', 'How runs are judged', 'Verified and submitted', 'Cost per solved', 'blind votes', 'benchmarks.jsonl']) assert.ok(r.text.includes(s), s);
- });
-
- await check('new results are RSS and JSON, from the same runs', async () => {
- const rss = await t.get('/ai/feed.xml');
- assert.strictEqual(rss.status, 200);
- assert.match(rss.headers.get('content-type') || '', /application\/rss\+xml/);
- assert.ok(rss.text.includes('<title>model-a on Playable games arena</title>') && rss.text.includes(RUN_A), 'the run is in the feed');
- const json = await t.get('/api/v1/ai/results');
- assert.strictEqual(json.status, 200);
- const body = json.json();
- assert.strictEqual(body.count, 2);
- assert.strictEqual(body.results[0].id, RUN_B);
- assert.strictEqual(body.results[0].benchmark, 'Playable games arena');
- assert.ok(body.results[0].url.endsWith('/ai/' + BMK));
- });
-
- await check('the AI pages have Markdown twins, and the benchmark twin names its page', async () => {
- for (const [path, expected] of [[`/ai/${BMK}.md`, 'Playable games arena'], ['/ai/method.md', 'How the AI results are judged'], ['/ai/models/model-a.md', 'model-a']]) {
- const r = await t.get(path);
- assert.strictEqual(r.status, 200, path);
- assert.match(r.headers.get('content-type') || '', /^text\/markdown/, path);
- assert.ok(r.text.includes(expected), `${path}: ${expected}`);
- }
- });
-
- await check('the sitemap lists the AI pages and the benchmark', async () => {
- const r = await t.get('/sitemap.xml');
- assert.strictEqual(r.status, 200);
- for (const s of ['/ai</loc>', '/ai/method', '/ai/compare', `/ai/${BMK}`, '/ai/design.page', '/ai/models/model-a']) assert.ok(r.text.includes(s), s);
- });
- } finally { await t.close(); }
- done();
-})().catch((e) => { console.error(e); process.exit(1); });
@@ -31,7 +31,7 @@ const STATUS_UPDATED = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'ST
@@ -31,7 +31,7 @@ const STATUS_UPDATED = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'ST
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
assert.match(r.headers.get('content-type') || '', /^application\/xml/);
assert.match(r.headers.get('content-type') || '', /^application\/xml/);
assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=3600');
assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=3600');
- for (const p of ['/', '/stats', '/design', '/ai']) {
+ for (const p of ['/', '/stats', '/design']) { // /ai moved to OpenVibe.Compare (2026-11-13)
assert.ok(r.text.includes(`<loc>https://openvibe.report${p}</loc>`), `no ${p} entry`);
assert.ok(r.text.includes(`<loc>https://openvibe.report${p}</loc>`), `no ${p} entry`);
}
}
for (const p of ['/rundown', '/rundown/t/seattle', '/rundown/t/ai-progress']) {
for (const p of ['/rundown', '/rundown/t/seattle', '/rundown/t/ai-progress']) {