Deploy to Cloudflare Pages / deploy (push) Failing after 23s
- Task matrix (12 task families x 23 Go models) from Artificial Analysis evals - Price/value scatter and value ranking - Model guide: what each model is targeted for (agentic, coding, data analysis, image understanding, research, free tier) with pricing and caveats - MODELS.md markdown version, README, MIT license - wrangler.toml + Gitea Actions workflow deploying site/ to Cloudflare Pages
256 lines
14 KiB
JavaScript
256 lines
14 KiB
JavaScript
import { RAW } from "./evals.js";
|
||
import { GO_MODELS, FREE_NOTES, CATEGORIES } from "./data.js";
|
||
|
||
/* ---------- shared helpers ---------- */
|
||
const el = (id) => document.getElementById(id);
|
||
const tip = el("tip");
|
||
function showTip(html, e) {
|
||
tip.style.display = "block";
|
||
tip.style.left = Math.min(e.clientX + 14, window.innerWidth - 260) + "px";
|
||
tip.style.top = Math.min(e.clientY + 14, window.innerHeight - 140) + "px";
|
||
tip.innerHTML = html;
|
||
}
|
||
const hideTip = () => (tip.style.display = "none");
|
||
|
||
function color(v) {
|
||
if (v === null || v === undefined) return "transparent";
|
||
const stops = [
|
||
[14, 16, 26], [28, 50, 80], [31, 106, 143], [47, 163, 160],
|
||
[87, 204, 138], [182, 227, 106], [242, 214, 75], [243, 156, 61], [239, 83, 80]
|
||
];
|
||
const p = Math.max(0, Math.min(0.999, v / 100)) * (stops.length - 1);
|
||
const i = Math.floor(p), f = p - i, a = stops[i], b = stops[i + 1] || a;
|
||
const c = a.map((x, k) => Math.round(x + (b[k] - x) * f));
|
||
return `rgb(${c[0]},${c[1]},${c[2]})`;
|
||
}
|
||
const txtCol = (v) => (v >= 62 ? "#101319" : "#e6e9f2");
|
||
|
||
const TASKS = [
|
||
{ key: "coding", label: "Frontier coding", picks: [["fc", "FrontierCode"]] },
|
||
{ key: "scicode", label: "Sci coding", picks: [["scicode", "SciCode"]] },
|
||
{ key: "terminal", label: "Terminal agent", picks: [["tb21", "Terminal-Bench 2.1"], ["tb40", "Terminal-Bench 4.0"], ["tbh", "Terminal-Bench Hard"]] },
|
||
{ key: "tools", label: "Tool use (τ-bench)", picks: [["tau2", "τ²-bench"], ["taub", "τ-bench Banking"]] },
|
||
{ key: "agentic", label: "Agent workflows", picks: [["apex", "APEX-Agents"], ["analyst", "Analyst Agent"]] },
|
||
{ key: "auto", label: "Automation", picks: [["auto", "AutomationBench"]] },
|
||
{ key: "science", label: "Science reasoning", picks: [["gpqa", "GPQA Diamond"], ["critpt", "CritPt"]] },
|
||
{ key: "hle", label: "Knowledge (HLE)", picks: [["hle", "Humanity's Last Exam"]] },
|
||
{ key: "lcr", label: "Long context", picks: [["lcr", "AA Long-Context Reasoning"]] },
|
||
{ key: "ifbench", label: "Instruction following", picks: [["ifbench", "IFBench"]] },
|
||
{ key: "vision", label: "Vision (MMMU-Pro)", picks: [["mmmu", "MMMU-Pro"]] },
|
||
{ key: "pro", label: "Professional work", picks: [["gdpv", "GDPval-AA"], ["gdppdf", "GDP-PDF"]] }
|
||
];
|
||
|
||
RAW.forEach((m) => {
|
||
m.tasks = {}; m.src = {};
|
||
TASKS.forEach((t) => {
|
||
for (const [f, label] of t.picks) {
|
||
const v = m[f];
|
||
if (v !== null && v !== undefined) { m.tasks[t.key] = v; m.src[t.key] = label; break; }
|
||
}
|
||
});
|
||
m.blend = m.free ? 0 : 0.7 * m.pi + 0.3 * m.po;
|
||
});
|
||
|
||
const byId = Object.fromEntries(RAW.map((m) => [m.id, m]));
|
||
|
||
/* ---------- header cards ---------- */
|
||
const scored = RAW.filter((m) => m.ii !== null);
|
||
const topOverall = scored.slice().sort((a, b) => b.ii - a.ii)[0];
|
||
const bestValue = scored.filter((m) => !m.free).slice().sort((a, b) => b.ii / b.blend - a.ii / a.blend)[0];
|
||
el("cards").innerHTML = [
|
||
["Models in Go", RAW.length, "in the Go catalog"],
|
||
["With public evals", scored.length, "Artificial Analysis harness"],
|
||
["Top overall", topOverall.n, "index " + topOverall.ii.toFixed(1)],
|
||
["Best value", bestValue.n, "index " + bestValue.ii.toFixed(1) + " at $" + bestValue.blend.toFixed(2) + "/1M"],
|
||
["Free tier", RAW.filter((m) => m.free).length, "limited-time free models"]
|
||
].map(([k, v, s]) => `<div class="card"><div class="k">${k}</div><div class="v">${v}</div><div class="sub" style="font-size:11px">${s}</div></div>`).join("");
|
||
|
||
/* ---------- matrix ---------- */
|
||
const cols = [
|
||
{ key: "name", label: "Model", get: (m) => m.n, txt: true },
|
||
{ key: "ii", label: "Index", get: (m) => m.ii },
|
||
{ key: "blend", label: "Price $/1M", get: (m) => m.blend, price: true },
|
||
...TASKS.map((t) => ({ key: t.key, label: t.label, get: (m) => m.tasks[t.key] }))
|
||
];
|
||
let sortKey = "ii", sortDir = -1;
|
||
|
||
function renderMatrix() {
|
||
const best = {};
|
||
cols.forEach((c) => {
|
||
if (c.key === "name" || c.key === "blend") return;
|
||
const vals = RAW.map((m) => c.get(m)).filter((v) => v !== null && v !== undefined);
|
||
best[c.key] = vals.length ? Math.max(...vals) : null;
|
||
});
|
||
const rows = RAW.slice().sort((a, b) => {
|
||
const c = cols.find((x) => x.key === sortKey);
|
||
const va = c.get(a), vb = c.get(b);
|
||
if (sortKey === "name") return String(va).localeCompare(String(vb)) * sortDir;
|
||
if (va === null || va === undefined) return 1;
|
||
if (vb === null || vb === undefined) return -1;
|
||
return (va - vb) * sortDir;
|
||
});
|
||
let h = "<thead><tr>";
|
||
cols.forEach((c) => {
|
||
h += `<th class="${c.txt ? "tl" : ""} ${c.key === sortKey ? "sorted" : ""}" data-k="${c.key}">${c.label}${c.key === sortKey ? (sortDir < 0 ? " ▾" : " ▴") : ""}</th>`;
|
||
});
|
||
h += "</tr></thead><tbody>";
|
||
rows.forEach((m) => {
|
||
h += `<tr><td class="name">${m.n}${m.free ? '<span class="tag free">free</span>' : ""}<div class="price">${m.lab} · $${m.pi}/${m.po} · ${(m.ctx / 1e6).toFixed(m.ctx % 1e6 ? 2 : 0)}M ctx${m.ii === null ? " · no public evals" : ""}</div></td>`;
|
||
cols.forEach((c) => {
|
||
if (c.key === "name") return;
|
||
const v = c.get(m);
|
||
if (c.key === "ii") {
|
||
h += `<td><div class="cell" style="background:${v === null ? "transparent" : "#1b2233"}">${v === null ? '<span class="na">n/a</span>' : `<b>${v.toFixed(1)}</b>`}</div></td>`;
|
||
return;
|
||
}
|
||
if (c.key === "blend") { h += `<td class="price">${m.free ? "$0" : "$" + v.toFixed(2)}</td>`; return; }
|
||
if (v === null || v === undefined) { h += `<td><div class="cell na">–</div></td>`; return; }
|
||
const isBest = best[c.key] !== null && Math.abs(v - best[c.key]) < 0.001;
|
||
h += `<td><div class="cell ${isBest ? "best" : ""}" style="background:${color(v)};color:${txtCol(v)}" data-m="${m.n}" data-t="${c.label}" data-src="${m.src[c.key]}">${v.toFixed(0)}<div class="bar" style="width:${v}%"></div></div></td>`;
|
||
});
|
||
h += "</tr>";
|
||
});
|
||
h += "</tbody>";
|
||
const t = el("tbl");
|
||
t.innerHTML = h;
|
||
t.querySelectorAll("th").forEach((th) => {
|
||
th.onclick = () => {
|
||
const k = th.dataset.k;
|
||
if (k === sortKey) sortDir *= -1; else { sortKey = k; sortDir = k === "name" ? 1 : -1; }
|
||
renderMatrix();
|
||
};
|
||
});
|
||
t.querySelectorAll(".cell[data-m]").forEach((c) => {
|
||
c.onmousemove = (e) => showTip(`<b>${c.dataset.m}</b><br>${c.dataset.t} — ${c.dataset.src}`, e);
|
||
c.onmouseleave = hideTip;
|
||
});
|
||
}
|
||
renderMatrix();
|
||
|
||
/* ---------- svg helpers ---------- */
|
||
function svgEl(tag, attrs, parent) {
|
||
const e = document.createElementNS("http://www.w3.org/2000/svg", tag);
|
||
for (const k in attrs) e.setAttribute(k, attrs[k]);
|
||
if (parent) parent.appendChild(e);
|
||
return e;
|
||
}
|
||
|
||
/* ---------- scatter ---------- */
|
||
(function scatter() {
|
||
const box = el("scatter"); box.innerHTML = "";
|
||
const W = 520, H = 340, m = { l: 52, r: 16, t: 16, b: 44 };
|
||
const svg = svgEl("svg", { viewBox: `0 0 ${W} ${H}`, width: "100%", height: "auto" }, box);
|
||
const pts = scored.filter((x) => x.ii !== null);
|
||
const maxX = Math.max(...pts.map((p) => p.blend)) * 1.1 || 1, maxY = 60;
|
||
const X = (v) => m.l + (v / maxX) * (W - m.l - m.r);
|
||
const Y = (v) => H - m.b - (v / maxY) * (H - m.t - m.b);
|
||
for (let g = 0; g <= maxY; g += 10) {
|
||
svgEl("line", { x1: m.l, y1: Y(g), x2: W - m.r, y2: Y(g), stroke: "#232838" }, svg);
|
||
svgEl("text", { x: m.l - 8, y: Y(g) + 4, fill: "#8b93a7", "font-size": 10, "text-anchor": "end" }, svg).textContent = g;
|
||
}
|
||
for (let g = 0; g <= maxX; g += 2) {
|
||
svgEl("line", { x1: X(g), y1: m.t, x2: X(g), y2: H - m.b, stroke: "#1c2130" }, svg);
|
||
svgEl("text", { x: X(g), y: H - m.b + 16, fill: "#8b93a7", "font-size": 10, "text-anchor": "middle" }, svg).textContent = "$" + g;
|
||
}
|
||
svgEl("text", { x: (m.l + W - m.r) / 2, y: H - 6, fill: "#8b93a7", "font-size": 11, "text-anchor": "middle" }, svg).textContent = "blended price $/1M tokens";
|
||
svgEl("text", { x: 14, y: (m.t + H - m.b) / 2, fill: "#8b93a7", "font-size": 11, "text-anchor": "middle", transform: `rotate(-90 14 ${(m.t + H - m.b) / 2})` }, svg).textContent = "Intelligence Index";
|
||
pts.forEach((p) => {
|
||
const c = svgEl("circle", { cx: X(p.blend), cy: Y(p.ii), r: 5, fill: p.free ? "#2ecc71" : "#7cc4ff", "fill-opacity": 0.85, stroke: "#0b0d12", "stroke-width": 1.5 }, svg);
|
||
const lbl = svgEl("text", { x: X(p.blend) + 8, y: Y(p.ii) + 3, fill: "#c8cede", "font-size": 10 }, svg);
|
||
lbl.textContent = p.n.length > 22 ? p.n.slice(0, 21) + "…" : p.n;
|
||
c.onmousemove = (e) => showTip(`<b>${p.n}</b><br>Index ${p.ii.toFixed(1)}<br>${p.free ? "Free" : `$${p.pi} in / $${p.po} out per 1M`}`, e);
|
||
c.onmouseleave = hideTip;
|
||
});
|
||
})();
|
||
|
||
/* ---------- value ranking ---------- */
|
||
(function valueChart() {
|
||
const box = el("value"); box.innerHTML = "";
|
||
const W = 520, H = 340, m = { l: 96, r: 16, t: 12, b: 28 };
|
||
const svg = svgEl("svg", { viewBox: `0 0 ${W} ${H}`, width: "100%", height: "auto" }, box);
|
||
const pts = scored.filter((p) => !p.free).map((p) => ({ ...p, v: p.ii / p.blend })).sort((a, b) => b.v - a.v);
|
||
const maxV = Math.max(...pts.map((p) => p.v)) * 1.08;
|
||
const step = (H - m.t - m.b) / Math.max(pts.length, 1);
|
||
pts.forEach((p, i) => {
|
||
const y = 26 + i * step;
|
||
const w = Math.max(2, (p.v / maxV) * (W - m.l - m.r));
|
||
svgEl("rect", { x: m.l, y: y - 9, width: w, height: 15, rx: 3, fill: "#7cc4ff", "fill-opacity": 0.85 }, svg);
|
||
svgEl("text", { x: m.l - 8, y: y + 3, fill: "#c8cede", "font-size": 11, "text-anchor": "end" }, svg).textContent = p.n.length > 17 ? p.n.slice(0, 16) + "…" : p.n;
|
||
svgEl("text", { x: m.l + w + 6, y: y + 3, fill: "#8b93a7", "font-size": 10 }, svg).textContent = p.v.toFixed(0) + " pts/$";
|
||
});
|
||
const free = scored.filter((p) => p.free);
|
||
svgEl("text", { x: m.l, y: H - 6, fill: "#2ecc71", "font-size": 11 }, svg).textContent = "Free models (not ranked): " + free.map((f) => f.n).join(", ");
|
||
})();
|
||
|
||
/* ---------- model guide ---------- */
|
||
function modelCard(m) {
|
||
const tags = (m.best || []).map((b) => {
|
||
const key = Object.keys(CATEGORIES).find((k) => CATEGORIES[k].label.toLowerCase() === b.toLowerCase());
|
||
const cls = key === "free" ? "tag free" : key === "imagegen" ? "tag warn" : "tag";
|
||
return `<span class="${cls}">${b}</span>`;
|
||
}).join("");
|
||
const ev = byId[m.id];
|
||
const idx = ev && ev.ii !== null ? `index ${ev.ii.toFixed(1)}` : "no public evals";
|
||
return `<div class="card2">
|
||
<div class="hd"><h3>${m.name}</h3><span class="lab">${m.lab}</span></div>
|
||
<div class="meta">released ${m.released} · $${m.price.in}/${m.price.out} per 1M · ${m.ctx} context · ${m.modalities} · ${idx}</div>
|
||
<div>${tags}</div>
|
||
<p class="blurb">${m.blurb}</p>
|
||
<p><b>Targeted for:</b> ${m.target}</p>
|
||
${m.caveats && m.caveats.length ? `<div class="caveats"><b>Caveats</b><ul>${m.caveats.map((c) => `<li>${c}</li>`).join("")}</ul></div>` : ""}
|
||
</div>`;
|
||
}
|
||
|
||
// loose keyword matching so a model's plain-language tags still hit the right filter
|
||
const FILTER_WORDS = {
|
||
agentic: ["agent"],
|
||
coding: ["cod", "repo"],
|
||
data: ["data", "spreadsheet", "analys"],
|
||
vision: ["vision", "image", "multimodal", "perception", "visual"],
|
||
imagegen: ["generation", "generat"],
|
||
research: ["research", "knowledge work", "document", "deep"],
|
||
fast: ["cheap", "fast", "high-volume", "efficient", "budget"]
|
||
};
|
||
|
||
function renderGuide(filter) {
|
||
const list = GO_MODELS.filter((m) => {
|
||
if (!filter || filter === "all") return true;
|
||
if (filter === "free") return m.free;
|
||
if (filter === "new") return m.released >= "2026-08-01";
|
||
const words = FILTER_WORDS[filter] || [];
|
||
const hay = m.best.join(" ").toLowerCase();
|
||
return words.some((w) => hay.includes(w));
|
||
});
|
||
const extra = filter === "all" || filter === "free" ? `
|
||
<div class="note"><b>Also free outside the Go plan</b><br>${FREE_NOTES.map((f) => `<b>${f.name}</b> (${f.lab}, ${f.where}) — ${f.target} <i>${f.note}</i>`).join("<br>")}</div>` : "";
|
||
const none = list.length ? "" : `<div class="note">No model in the Go catalog matches this category. Every Go model is text-output — none of them generate images, so image generation needs a different provider.</div>`;
|
||
el("guide").innerHTML = list.map(modelCard).join("") + extra + none;
|
||
}
|
||
|
||
const filters = [
|
||
["all", "All models"], ["free", "Free tier"], ["new", "New (since Aug 2026)"],
|
||
["agentic", "Agentic"], ["coding", "Coding"], ["data", "Data analysis"],
|
||
["vision", "Image understanding"], ["imagegen", "Image generation"],
|
||
["research", "Deep research"], ["fast", "Fast & cheap"]
|
||
];
|
||
el("filters").innerHTML = filters.map(([k, l]) => `<div class="chip${k === "all" ? " on" : ""}" data-f="${k}">${l}</div>`).join("");
|
||
el("filters").querySelectorAll(".chip").forEach((c) => {
|
||
c.onclick = () => {
|
||
el("filters").querySelectorAll(".chip").forEach((x) => x.classList.remove("on"));
|
||
c.classList.add("on");
|
||
renderGuide(c.dataset.f);
|
||
};
|
||
});
|
||
renderGuide("all");
|
||
|
||
el("foot").innerHTML = `
|
||
<b>How to read the matrix</b><br>
|
||
• Each cell is the highest-scoring published evaluation for that task family on a 0–100 scale; the benchmark behind it is named in the hover tooltip.<br>
|
||
• Terminal-agent, tool-use, science and professional-work columns fall back to a second benchmark when a model has not been run on the first (Terminal-Bench 2.1 → 4.0 → Hard; τ²-bench → τ-bench Banking; GPQA → CritPt; GDPval → GDP-PDF). Scores are comparable within a column, but the underlying benchmark can differ per row — hover to check.<br>
|
||
• "Index" is the Artificial Analysis Intelligence Index, a weighted composite of those evaluations.<br>
|
||
• Prices are OpenCode Go list prices per 1M tokens (input/output). Blended price = 70% input / 30% output.<br>
|
||
• Free-tier models (MiniMax M3.1 Flash Free, Beam Free, LongCat 2.5 Preview Free) are limited-time offers with unlimited included usage.<br>
|
||
• Hy4 preview and LongCat 2.5 Preview Free have no public third-party evaluations yet, so they show as "n/a". Qwen3.8 Flash maps to Artificial Analysis' "Qwen3.8-Flash-Next" entry.<br>
|
||
• No model in the Go catalog generates images — all Go models are text-output. "Image understanding" rows mean the model can read images, video, PDFs or screenshots as input.<br><br>
|
||
Sources: <a href="https://artificialanalysis.ai">Artificial Analysis</a> (benchmarks, Intelligence Index), <a href="https://opencode.ai/data">OpenCode Data</a> (catalog, usage, context windows) and <a href="https://opencode.ai/docs/go">OpenCode Go docs</a> (pricing, allowances, privacy). Vendor announcements for model positioning. Retrieved 2026-10-21.`;
|