import { CATALOG, SNAPSHOT } from "./catalog.js";
import { RAW } from "./evals.js";
import { GO_MODELS, FREE_NOTES } from "./data.js";
/* ---------- merge the three layers -----------------------------------------
catalog.js auto-generated — every model currently in OpenCode Go
evals.js auto-generated — benchmark scores per model
data.js hand-written — what each model is targeted for
--------------------------------------------------------------------------- */
const DESC = Object.fromEntries(GO_MODELS.map((m) => [m.id, m]));
const SCORES = Object.fromEntries(RAW.map((m) => [m.id, m]));
const MODELS = CATALOG.map((c) => {
const d = DESC[c.id] || {};
const s = SCORES[c.id] || {};
const m = {
...c,
best: d.best || [],
blurb: d.blurb || c.description || "No description published yet.",
target: d.target || "",
caveats: d.caveats || [],
written: !!d.blurb,
ii: s.ii ?? null,
tasks: {},
src: {}
};
m.blend = m.free ? 0 : 0.7 * m.price.in + 0.3 * m.price.out;
return m;
});
/* ---------- 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"]] }
];
MODELS.forEach((m) => {
TASKS.forEach((t) => {
for (const [f, label] of t.picks) {
const v = SCORES[m.id]?.[f];
if (v !== null && v !== undefined) { m.tasks[t.key] = v; m.src[t.key] = label; break; }
}
});
});
const scored = MODELS.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];
const snapshotDate = (SNAPSHOT.generated || "").slice(0, 10);
/* ---------- header cards -------------------------------------------------- */
el("cards").innerHTML = [
["Models in Go", MODELS.length, "in the OpenCode Go catalogue"],
["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", MODELS.filter((m) => m.free).length, "limited-time free models"],
["Data snapshot", snapshotDate, "refreshed weekly by CI"]
].map(([k, v, s]) => `
`).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 = MODELS.map((m) => c.get(m)).filter((v) => v !== null && v !== undefined);
best[c.key] = vals.length ? Math.max(...vals) : null;
});
const rows = MODELS.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 = "";
cols.forEach((c) => {
h += `| ${c.label}${c.key === sortKey ? (sortDir < 0 ? " ▾" : " ▴") : ""} | `;
});
h += "
";
rows.forEach((m) => {
const ctx = m.ctx ? `${(m.ctx / 1e6).toFixed(m.ctx % 1e6 ? 2 : 0)}M ctx` : "ctx n/a";
h += `| ${m.n}${m.free ? 'free' : ""}${!m.written ? 'no write-up' : ""} ${m.lab || "—"} · $${m.price.in}/${m.price.out} · ${ctx}${m.ii === null ? " · no public evals" : ""} | `;
cols.forEach((c) => {
if (c.key === "name") return;
const v = c.get(m);
if (c.key === "ii") {
h += `${v === null ? 'n/a' : `${v.toFixed(1)}`} | `;
return;
}
if (c.key === "blend") { h += `${m.free ? "$0" : "$" + v.toFixed(2)} | `; return; }
if (v === null || v === undefined) { h += `– | `; return; }
const isBest = best[c.key] !== null && Math.abs(v - best[c.key]) < 0.001;
h += ` | `;
});
h += "
";
});
h += "";
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(`${c.dataset.m}
${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 && x.blend > 0);
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(`${p.n}
Index ${p.ii.toFixed(1)}
${p.free ? "Free" : `$${p.price.in} in / $${p.price.out} 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 && p.blend > 0).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 = 22 + 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 = b === "Free tier" ? "free" : b === "Image generation" ? "imagegen" : "";
const cls = key === "free" ? "tag free" : key === "imagegen" ? "tag warn" : "tag";
return `${b}`;
}).join("");
const idx = m.ii !== null ? `index ${m.ii.toFixed(1)}` : "no public evals";
const ctx = m.ctx ? `${(m.ctx / 1e6).toFixed(m.ctx % 1e6 ? 2 : 0)}M context` : "context n/a";
return `
${m.n}
${m.lab || "undisclosed"}
released ${m.released || "?"} · $${m.price.in}/${m.price.out} per 1M · ${ctx} · ${m.modalities} · ${idx}
${tags}
${m.blurb}
${m.target ? `
Targeted for: ${m.target}
` : `
No write-up yet — this model was added to the catalogue automatically and needs a description.
`}
${m.caveats && m.caveats.length ? `
Caveats${m.caveats.map((c) => `- ${c}
`).join("")}
` : ""}
`;
}
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 = 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" ? `
Also free outside the Go plan
${FREE_NOTES.map((f) => `${f.name} (${f.lab}, ${f.where}) — ${f.target} ${f.note}`).join("
")}
` : "";
const none = list.length ? "" : `No model in the Go catalogue matches this category. Every Go model is text-output — none of them generate images, so image generation needs a different provider.
`;
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]) => `${l}
`).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 = `
How to read the matrix
• 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.
• 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.
• "Index" is the Artificial Analysis Intelligence Index, a weighted composite of those evaluations.
• Prices are OpenCode Go list prices per 1M tokens (input/output). Blended price = 70% input / 30% output.
• Free-tier models are limited-time offers. Contributor-tier models (Gemini 4 Argon, Muse Spark) may use your prompts and completions for training.
• Models marked "no write-up" were added to the catalogue automatically and do not yet have a hand-written description; models with no public third-party evaluation show "n/a" rather than borrowed numbers.
• No model in the Go catalogue generates images — all are text-output. "Image understanding" means the model can read images, video, PDFs or screenshots as input.
Sources: Artificial Analysis (benchmarks, Intelligence Index), models.dev (Go catalogue, pricing, context, modalities), OpenCode Data (usage and retention) and the OpenCode Go docs (allowances, privacy). Vendor announcements for model positioning.
Data snapshot: ${snapshotDate || "n/a"} — refreshed weekly by the weekly-eval.yml workflow.`;