chore: merge dev into v2 (#36312)

Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com>
Co-authored-by: Julian Coy <julian@ex-machina.co>
Co-authored-by: Brendan Allan <git@brendonovich.dev>
Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com>
Co-authored-by: opencode <opencode@sst.dev>
Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com>
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
Co-authored-by: Frank <frank@anoma.ly>
Co-authored-by: Jay <53023+jayair@users.noreply.github.com>
Co-authored-by: Dustin Deus <deusdustin@gmail.com>
Co-authored-by: Kit Langton <kit.langton@gmail.com>
Co-authored-by: James Long <longster@gmail.com>
Co-authored-by: Simon Klee <hello@simonklee.dk>
Co-authored-by: Jay <air@live.ca>
Co-authored-by: Jack <jack@anoma.ly>
Co-authored-by: David Hill <1879069+iamdavidhill@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Co-authored-by: James Long <jlongster@users.noreply.github.com>
Co-authored-by: 冯基魁 <56265583+fengjikui@users.noreply.github.com>
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
Co-authored-by: Victor Navarro <vn4varro@gmail.com>
This commit is contained in:
opencode-agent[bot] 2026-07-10 15:11:29 -05:00 committed by GitHub
commit 43ecf3ff1b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
82 changed files with 4519 additions and 1231 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,286 @@
import { catalogSlug, findModelCatalogEntry, type ModelCatalog, type ModelCatalogEntry } from "../routes/model-catalog"
type ComparisonFamilyDefinition = {
slug: string
name: string
lab: string
prefixes: string[]
aliases?: string[]
preferredFamilies?: string[]
}
export type ResolvedComparisonFamily = ComparisonFamilyDefinition & {
model: ModelCatalogEntry
}
export const comparisonFamilies: ComparisonFamilyDefinition[] = [
{
slug: "gpt",
name: "GPT",
lab: "openai",
prefixes: ["gpt", "o"],
aliases: ["openai"],
preferredFamilies: ["gpt", "o"],
},
{
slug: "claude",
name: "Claude",
lab: "anthropic",
prefixes: ["claude"],
aliases: ["anthropic"],
preferredFamilies: ["claude-sonnet", "claude-opus"],
},
{
slug: "gemini",
name: "Gemini",
lab: "google",
prefixes: ["gemini"],
aliases: ["google"],
preferredFamilies: ["gemini-pro", "gemini-flash", "gemini"],
},
{
slug: "deepseek",
name: "DeepSeek",
lab: "deepseek",
prefixes: ["deepseek"],
preferredFamilies: ["deepseek-thinking", "deepseek"],
},
{
slug: "qwen",
name: "Qwen",
lab: "alibaba",
prefixes: ["qwen"],
aliases: ["alibaba"],
preferredFamilies: ["qwen"],
},
{
slug: "glm",
name: "GLM",
lab: "zhipuai",
prefixes: ["glm"],
aliases: ["zhipu", "zhipuai", "zai"],
preferredFamilies: ["glm"],
},
{
slug: "kimi",
name: "Kimi",
lab: "moonshotai",
prefixes: ["kimi"],
aliases: ["moonshot", "moonshotai"],
preferredFamilies: ["kimi-k2", "kimi-thinking"],
},
{
slug: "minimax",
name: "MiniMax",
lab: "minimax",
prefixes: ["minimax"],
},
{
slug: "grok",
name: "Grok",
lab: "xai",
prefixes: ["grok"],
aliases: ["xai"],
preferredFamilies: ["grok"],
},
{
slug: "mistral",
name: "Mistral",
lab: "mistral",
prefixes: ["mistral", "magistral", "devstral", "codestral"],
preferredFamilies: ["mistral-large", "mistral-medium", "mistral-small"],
},
{
slug: "llama",
name: "Llama",
lab: "meta",
prefixes: ["llama"],
aliases: ["meta"],
},
{
slug: "nemotron",
name: "Nemotron",
lab: "nvidia",
prefixes: ["nemotron", "llama-nemotron"],
aliases: ["nvidia"],
},
{
slug: "mimo",
name: "MiMo",
lab: "xiaomi",
prefixes: ["mimo"],
aliases: ["xiaomi"],
},
{
slug: "command",
name: "Command",
lab: "cohere",
prefixes: ["command"],
aliases: ["cohere"],
preferredFamilies: ["command-a", "command-r"],
},
{
slug: "sonar",
name: "Sonar",
lab: "perplexity",
prefixes: ["sonar"],
aliases: ["perplexity"],
preferredFamilies: ["sonar-pro", "sonar-reasoning", "sonar"],
},
{
slug: "longcat",
name: "LongCat",
lab: "meituan",
prefixes: ["longcat"],
aliases: ["meituan"],
},
{
slug: "step",
name: "Step",
lab: "stepfun",
prefixes: ["step"],
aliases: ["stepfun"],
},
{
slug: "mai",
name: "MAI",
lab: "microsoft",
prefixes: ["mai"],
aliases: ["microsoft"],
},
]
export function resolveComparisonFamily(catalog: ModelCatalog, value: string) {
const family = findComparisonFamily(value)
if (!family) return undefined
const model = comparisonFamilyCandidates(catalog, family.slug)[0]
if (!model) return undefined
return { ...family, model } satisfies ResolvedComparisonFamily
}
export function findComparisonFamily(value: string) {
const slug = catalogSlug(value)
return comparisonFamilies.find((family) => family.slug === slug || family.aliases?.includes(slug))
}
export function comparisonFamilyCandidates(catalog: ModelCatalog, value: string) {
const family = findComparisonFamily(value)
if (!family) return []
const matches = catalog.models
.filter((model) => model.lab === family.lab && isFamilyModel(model, family) && isGeneralComparisonModel(model))
.toSorted((a, b) => comparisonFamilyModelSort(a, b, family))
return matches.filter((model) => !isDuplicateAliasModel(model, matches))
}
export function comparisonSitemapModels(
catalog: ModelCatalog,
leaderboard: { model: string; provider: string }[] = [],
) {
return uniqueModels([
...comparisonFamilies.flatMap((family) => comparisonFamilyCandidates(catalog, family.slug).slice(0, 2)),
...leaderboard.flatMap((entry) => {
const model =
findModelCatalogEntry(catalog, entry.model, entry.provider) ?? findModelCatalogEntry(catalog, entry.model)
return model && isGeneralComparisonModel(model) ? [model] : []
}),
]).toSorted((a, b) => a.id.localeCompare(b.id))
}
export function canonicalModelComparisonPath(first: ModelCatalogEntry, second: ModelCatalogEntry) {
const models = [first, second].toSorted((a, b) => a.id.localeCompare(b.id))
return `/data/compare/${models[0].lab}/${models[0].slug}/${models[1].lab}/${models[1].slug}`
}
export function canonicalFamilyComparisonPath(first: ResolvedComparisonFamily, second: ResolvedComparisonFamily) {
const families = [first, second].toSorted((a, b) => a.slug.localeCompare(b.slug))
return `/data/compare/${families[0].slug}/${families[1].slug}`
}
export function latestFamilyComparisonPath(catalog: ModelCatalog, first: ModelCatalogEntry, second: ModelCatalogEntry) {
const firstFamily = comparisonFamilyForModel(catalog, first)
const secondFamily = comparisonFamilyForModel(catalog, second)
if (!firstFamily || !secondFamily || firstFamily.slug === secondFamily.slug) return undefined
if (firstFamily.model.id !== first.id || secondFamily.model.id !== second.id) return undefined
return canonicalFamilyComparisonPath(firstFamily, secondFamily)
}
export function comparisonFamilyForModel(catalog: ModelCatalog, model: ModelCatalogEntry) {
const family = comparisonFamilies.find(
(candidate) => candidate.lab === model.lab && isFamilyModel(model, candidate) && isGeneralComparisonModel(model),
)
if (!family) return undefined
const latest = comparisonFamilyCandidates(catalog, family.slug)[0]
if (!latest) return undefined
return { ...family, model: latest } satisfies ResolvedComparisonFamily
}
function isFamilyModel(model: ModelCatalogEntry, family: ComparisonFamilyDefinition) {
const values = [model.family, model.slug, model.name]
.filter((value): value is string => Boolean(value))
.map(catalogSlug)
return family.prefixes.some((prefix) => values.some((value) => value === prefix || value.startsWith(`${prefix}-`)))
}
function isGeneralComparisonModel(model: ModelCatalogEntry) {
const input = model.modalities.input.map(catalogSlug)
const output = model.modalities.output.map(catalogSlug)
if (!input.includes("text") || !output.includes("text")) return false
return !/(?:^|-)(?:audio|embedding|guard|image|moderation|omni|rerank|safety|speech|transcribe|tts|vision)(?:-|$)/.test(
model.slug,
)
}
function comparisonFamilyModelSort(
first: ModelCatalogEntry,
second: ModelCatalogEntry,
family: ComparisonFamilyDefinition,
) {
return (
displayDateTime(second.releaseDate ?? second.lastUpdated) -
displayDateTime(first.releaseDate ?? first.lastUpdated) ||
preferredFamilyIndex(first, family) - preferredFamilyIndex(second, family) ||
modelVariantPenalty(first) - modelVariantPenalty(second) ||
first.slug.length - second.slug.length ||
first.name.localeCompare(second.name)
)
}
function preferredFamilyIndex(model: ModelCatalogEntry, family: ComparisonFamilyDefinition) {
const index = family.preferredFamilies?.indexOf(catalogSlug(model.family ?? "")) ?? -1
return index === -1 ? (family.preferredFamilies?.length ?? 0) : index
}
function modelVariantPenalty(model: ModelCatalogEntry) {
return /(?:highspeed|latest|preview|turbo|ultraspeed)/.test(model.slug) ? 1 : 0
}
function isDuplicateAliasModel(model: ModelCatalogEntry, models: ModelCatalogEntry[]) {
if (!/(?:-latest|-highspeed|-ultraspeed)$/.test(model.slug)) return false
return models.some(
(candidate) =>
candidate.id !== model.id &&
candidate.releaseDate === model.releaseDate &&
candidate.family === model.family &&
!/(?:-latest|-highspeed|-ultraspeed)$/.test(candidate.slug),
)
}
function uniqueModels(models: ModelCatalogEntry[]) {
return models.reduce<{ ids: Set<string>; models: ModelCatalogEntry[] }>(
(result, model) => {
if (result.ids.has(model.id)) return result
result.ids.add(model.id)
result.models.push(model)
return result
},
{ ids: new Set(), models: [] },
).models
}
function displayDateTime(value: string | undefined) {
if (!value) return 0
const date = new Date(value)
if (!Number.isNaN(date.getTime())) return date.getTime()
const year = Number(value.match(/\d{4}/)?.[0] ?? 0)
return Number.isFinite(year) ? year : 0
}

View file

@ -1,3 +1,4 @@
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { For, Show } from "solid-js"
import { catalogSlug, formatCatalogLabName, type ModelCatalogEntry } from "./model-catalog"
@ -13,6 +14,7 @@ export type ComparisonPair = {
first: ComparisonModelRef
second: ComparisonModelRef
detail: string
description?: string
}
export function modelRefFromCatalog(entry: ModelCatalogEntry): ComparisonModelRef {
@ -30,6 +32,11 @@ export function comparisonHref(first: ComparisonModelRef, second: ComparisonMode
)}/${catalogSlug(second.slug)}`
}
export function canonicalComparisonHref(first: ComparisonModelRef, second: ComparisonModelRef) {
const models = [first, second].toSorted((a, b) => modelKey(a).localeCompare(modelKey(b)))
return comparisonHref(models[0], models[1])
}
export function uniqueComparisonPairs(pairs: ComparisonPair[]) {
return pairs.reduce<{ keys: Set<string>; pairs: ComparisonPair[] }>(
(result, pair) => {
@ -48,34 +55,25 @@ export function ComparisonCardsSection(props: {
title?: string
description?: string
compact?: boolean
variant?: "panel" | "featured"
}) {
const featured = () => props.variant === "featured"
const pairs = () => (featured() ? props.pairs.slice(0, 4) : props.pairs)
return (
<Show when={props.pairs.length > 0}>
<section id="model-comparison" data-section="model-panel" data-variant={props.compact ? "compact" : undefined}>
<section
id="model-comparison"
data-section={featured() ? "compare-home-related" : "model-panel"}
data-variant={!featured() && props.compact ? "compact" : undefined}
>
<p data-slot="section-title">
<strong>{props.title ?? "Model Comparisons"}.</strong>{" "}
<span>{props.description ?? "Compare usage, cost, limits, and features."}</span>
</p>
<div data-component="comparison-card-grid">
<For each={props.pairs}>
{(pair) => (
<a data-component="comparison-card" href={comparisonHref(pair.first, pair.second)}>
<span>{pair.detail}</span>
<strong>
{pair.first.name} <em>vs</em> {pair.second.name}
</strong>
<p>
<b>{pair.first.labName ?? formatCatalogLabName(pair.first.lab)}</b>
<i />
<b>{pair.second.labName ?? formatCatalogLabName(pair.second.lab)}</b>
</p>
<Show when={pair.first.metric || pair.second.metric}>
<small>
{pair.first.metric ?? "Listed"} / {pair.second.metric ?? "Listed"}
</small>
</Show>
</a>
)}
<div data-component={featured() ? "compare-home-card-grid" : "comparison-card-grid"}>
<For each={pairs()}>
{(pair) => (featured() ? <FeaturedComparisonCard pair={pair} /> : <ComparisonPanelCard pair={pair} />)}
</For>
</div>
</section>
@ -83,6 +81,90 @@ export function ComparisonCardsSection(props: {
)
}
function FeaturedComparisonCard(props: { pair: ComparisonPair }) {
return (
<a
data-component="compare-home-card"
href={canonicalComparisonHref(props.pair.first, props.pair.second)}
aria-label={`${props.pair.detail}: ${props.pair.first.name} vs ${props.pair.second.name}`}
>
<span data-slot="compare-home-card-head">
<span>
<strong>{props.pair.detail}</strong>
<em>{props.pair.description ?? `${props.pair.first.name} vs ${props.pair.second.name}`}</em>
</span>
<ComparisonCardIcon />
</span>
<span data-slot="compare-home-card-divider" aria-hidden="true" />
<span data-slot="compare-home-card-models">
<span>{props.pair.first.name}</span>
<i aria-hidden="true">·</i>
<span>{props.pair.second.name}</span>
</span>
<span data-slot="compare-home-card-avatars" aria-hidden="true">
<ComparisonLabLogo model={props.pair.first} />
<ComparisonLabLogo model={props.pair.second} />
</span>
</a>
)
}
function ComparisonCardIcon() {
return (
<b aria-hidden="true">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M12.9509 12.9884L14.4069 14.4444M2.44431 2.44434H6.44431V6.44434H2.44431V2.44434ZM2.44431 9.55542H6.44431V13.5554H2.44431V9.55542ZM9.55539 2.44434H13.5554V6.44434H9.55539V2.44434ZM13.5554 11.5554C13.5554 12.66 12.66 13.5554 11.5554 13.5554C10.4508 13.5554 9.55539 12.66 9.55539 11.5554C9.55539 10.4509 10.4508 9.55542 11.5554 9.55542C12.66 9.55542 13.5554 10.4509 13.5554 11.5554Z"
stroke="#808080"
/>
</svg>
</b>
)
}
function ComparisonPanelCard(props: { pair: ComparisonPair }) {
return (
<a data-component="comparison-card" href={canonicalComparisonHref(props.pair.first, props.pair.second)}>
<span>{props.pair.detail}</span>
<strong>
{props.pair.first.name} <em>vs</em> {props.pair.second.name}
</strong>
<p>
<b>{props.pair.first.labName ?? formatCatalogLabName(props.pair.first.lab)}</b>
<i />
<b>{props.pair.second.labName ?? formatCatalogLabName(props.pair.second.lab)}</b>
</p>
<Show when={props.pair.first.metric || props.pair.second.metric}>
<small>
{props.pair.first.metric ?? "Listed"} / {props.pair.second.metric ?? "Listed"}
</small>
</Show>
</a>
)
}
function ComparisonLabLogo(props: { model: ComparisonModelRef }) {
const iconId = () => providerIconId(props.model.lab)
return (
<span
data-slot="compare-home-avatar"
data-lab={iconId()}
data-size="small"
aria-label={props.model.labName ?? formatCatalogLabName(props.model.lab)}
>
<ProviderIcon aria-hidden="true" id={iconId()} />
</span>
)
}
function modelKey(model: ComparisonModelRef) {
return `${catalogSlug(model.lab)}/${catalogSlug(model.slug)}`
}
function providerIconId(provider: string) {
const id = provider.toLowerCase().replace(/[^a-z0-9]+/g, "")
if (id === "moonshot") return "moonshotai"
if (id === "zhipu") return "zhipuai"
return id
}

View file

@ -0,0 +1,391 @@
import { createMemo, createSignal, For, Show, type JSX } from "solid-js"
import type { ModelCatalogBenchmark, ModelCatalogEntry } from "./model-catalog"
const radarRingCount = 5
const radarColors = ["#294bdb", "#159447", "#d24a3b", "#8a4fd2", "#b47400", "#008c95"] as const
const codingBenchmarkPattern = /(swe|aider|code|coding|nl2repo)/
const reasoningBenchmarkPattern = /(gpqa|humanity|last exam|reasoning|aime|hmmt|math|mmlu|mrcr|charxiv|cti realm)/
const toolUseBenchmarkPattern = /(terminal bench|claw eval|tau ?(?:bench|2|3))/
export type ComparisonRadarModel = {
name: string
labName: string
catalog: ModelCatalogEntry | null
}
type ComparisonRadarProps = {
models: readonly ComparisonRadarModel[]
catalogModels: readonly ModelCatalogEntry[]
}
type RadarAxis = {
label: string
description: string
score: (model: ModelCatalogEntry) => number | undefined
}
type RadarPoint = {
x: number
y: number
}
export function ComparisonRadar(props: ComparisonRadarProps) {
const [activeAxis, setActiveAxis] = createSignal<number>()
const axes = createMemo(() => buildRadarAxes(props.catalogModels))
const series = createMemo(() =>
props.models.map((model, index) => ({
name: model.name,
labName: model.labName,
color: radarColors[index % radarColors.length],
scores: axes().map((axis) => (model.catalog ? axis.score(model.catalog) : undefined)),
})),
)
const accessibleDescription = createMemo(() =>
series()
.map(
(model) =>
`${model.name}: ${axes()
.map((axis, index) => `${axis.label} ${formatRadarScore(model.scores[index])}`)
.join(", ")}`,
)
.join(". "),
)
const clearActiveAxis = (index: number) => setActiveAxis((active) => (active === index ? undefined : active))
return (
<section data-section="compare-radar" aria-label="Model capabilities">
<ol data-slot="compare-radar-legend">
<For each={series()}>
{(model) => (
<li>
<i style={{ background: model.color }} aria-hidden="true" />
<span>
<strong>{model.name}</strong>
<small>{model.labName}</small>
</span>
</li>
)}
</For>
</ol>
<div data-slot="compare-radar-chart" role="img" aria-label={accessibleDescription()}>
<div data-slot="compare-radar-plot" aria-hidden="true">
<svg viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet">
<g data-slot="compare-radar-grid">
<For each={Array.from({ length: radarRingCount })}>
{(_, index) => (
<polygon points={radarPolygonPoints(axes().length, ((index() + 1) / radarRingCount) * 100)} />
)}
</For>
<For each={axes()}>
{(_, index) => {
const point = () => radarPoint(index(), axes().length, 100)
return <line x1="50" y1="50" x2={point().x} y2={point().y} />
}}
</For>
</g>
<For each={series()}>
{(model) => (
<g data-slot="compare-radar-series" style={{ color: model.color }}>
<Show when={radarSeriesPolygon(model.scores)}>
{(points) => <polygon data-slot="compare-radar-area" points={points()} />}
</Show>
<Show when={!radarSeriesPolygon(model.scores)}>
<For each={radarSeriesConnections(model.scores)}>
{(connection) => (
<line
data-slot="compare-radar-line"
x1={connection.start.x}
y1={connection.start.y}
x2={connection.end.x}
y2={connection.end.y}
/>
)}
</For>
</Show>
<For each={model.scores}>
{(score, index) => {
if (score === undefined) return null
const point = () => radarPoint(index(), axes().length, score)
return (
<>
<circle data-slot="compare-radar-point" cx={point().x} cy={point().y} r="0.95" />
<circle
data-slot="compare-radar-point-hit"
cx={point().x}
cy={point().y}
r="3"
onMouseEnter={() => setActiveAxis(index())}
onMouseLeave={() => clearActiveAxis(index())}
/>
</>
)
}}
</For>
</g>
)}
</For>
</svg>
</div>
<For each={axes()}>
{(axis, index) => (
<span
data-slot="compare-radar-axis"
data-active={activeAxis() === index() ? "true" : undefined}
style={radarAxisStyle(index(), axes().length)}
tabIndex="0"
aria-label={`${axis.label}. ${axis.description}`}
onMouseEnter={() => setActiveAxis(index())}
onMouseLeave={() => clearActiveAxis(index())}
onFocus={() => setActiveAxis(index())}
onBlur={() => clearActiveAxis(index())}
onKeyDown={(event) => {
if (event.key === "Escape") event.currentTarget.blur()
}}
>
<span data-slot="compare-radar-axis-label">{axis.label}</span>
</span>
)}
</For>
<Show when={activeAxis() !== undefined}>
<div
data-slot="compare-radar-tooltip"
role="tooltip"
style={radarTooltipStyle(activeAxis() ?? 0, axes().length)}
>
<strong>{axes()[activeAxis() ?? 0]?.label}</strong>
<p>{axes()[activeAxis() ?? 0]?.description}</p>
</div>
</Show>
</div>
<div data-slot="compare-radar-data">
<table>
<caption>Normalized model capability scores</caption>
<thead>
<tr>
<th>Model</th>
<For each={axes()}>{(axis) => <th>{axis.label}</th>}</For>
</tr>
</thead>
<tbody>
<For each={series()}>
{(model) => (
<tr>
<th>{model.name}</th>
<For each={model.scores}>{(score) => <td>{formatRadarScore(score)}</td>}</For>
</tr>
)}
</For>
</tbody>
</table>
</div>
</section>
)
}
function buildRadarAxes(catalogModels: readonly ModelCatalogEntry[]): RadarAxis[] {
const benchmarks = benchmarkScoreGroups(catalogModels)
const toolUseBenchmarks = benchmarkScoreGroups(catalogModels, true)
const costs = catalogModels.flatMap((model) => {
const cost = modelCost(model)
return cost === undefined ? [] : [cost]
})
const contexts = catalogModels.flatMap((model) => (model.limit?.context === undefined ? [] : [model.limit.context]))
const multimodalMaximum = Math.max(...catalogModels.map(multimodalFeatureCount), 0)
// Speed and safety stay out until the catalog exposes comparable values for them.
return [
{
label: "Reasoning",
description: "Ability to solve complex, multi-step problems. Based on reasoning benchmarks when available.",
score: (model) =>
benchmarkPercentile(model, benchmarks, reasoningBenchmarkPattern) ?? (model.reasoning ? 100 : 0),
},
{
label: "Coding",
description: "Performance on software engineering and coding benchmarks.",
score: (model) => benchmarkPercentile(model, benchmarks, codingBenchmarkPattern),
},
{
label: "Cost efficiency",
description: "Relative input and output pricing. Lower-cost models score higher.",
score: (model) => {
const cost = modelCost(model)
if (cost === undefined) return
if (cost === 0) return 100
return percentileScore(cost, costs, "lower")
},
},
{
label: "Context window",
description: "How much input the model can process at once. Larger context windows score higher.",
score: (model) => {
const context = model.limit?.context
if (context === undefined) return
return percentileScore(context, contexts, "higher")
},
},
{
label: "Multimodal",
description: "Support for non-text input and output, including images, audio, and video.",
score: (model) => {
if (multimodalMaximum === 0) return
return (multimodalFeatureCount(model) / multimodalMaximum) * 100
},
},
{
label: "Tool use",
description: "Performance on agent benchmarks including Terminal-Bench, Tau3, and Claw-Eval.",
score: (model) =>
benchmarkPercentile(model, toolUseBenchmarks, toolUseBenchmarkPattern, {
aggregate: "average",
includeHarness: true,
}),
},
]
}
function benchmarkScoreGroups(catalogModels: readonly ModelCatalogEntry[], includeHarness = false) {
return catalogModels.reduce<Map<string, number[]>>((groups, model) => {
model.benchmarks
.reduce<Map<string, number>>((scores, benchmark) => {
const key = benchmarkKey(benchmark, includeHarness)
scores.set(key, Math.max(scores.get(key) ?? -Infinity, benchmark.score))
return scores
}, new Map())
.forEach((score, key) => {
groups.set(key, [...(groups.get(key) ?? []), score])
})
return groups
}, new Map())
}
function benchmarkKey(benchmark: ModelCatalogBenchmark, includeHarness: boolean) {
const name = normalizeBenchmarkName(benchmark.name)
const version = normalizeBenchmarkName(benchmark.version ?? "")
const versioned = version && !name.endsWith(version) ? `${name} ${version}` : name
if (!includeHarness) return versioned
const harness = normalizeBenchmarkName(benchmark.harness ?? benchmark.variant ?? "")
return harness ? `${versioned} | ${harness}` : versioned
}
function benchmarkPercentile(
model: ModelCatalogEntry,
benchmarks: Map<string, number[]>,
pattern: RegExp,
options?: { aggregate?: "average" | "best"; includeHarness?: boolean },
) {
const scores = Object.entries(
model.benchmarks.reduce<Record<string, number>>((result, benchmark) => {
const key = benchmarkKey(benchmark, options?.includeHarness ?? false)
if (!pattern.test(key)) return result
result[key] = Math.max(result[key] ?? -Infinity, benchmark.score)
return result
}, {}),
).flatMap(([key, score]) => {
const values = benchmarks.get(key)
const percentile = values ? percentileScore(score, values, "higher") : undefined
return percentile === undefined ? [] : [percentile]
})
if (scores.length === 0) return
if (options?.aggregate === "average") return scores.reduce((sum, score) => sum + score, 0) / scores.length
// Benchmark coverage varies by model, so additional published results should not lower a model's score.
return Math.max(...scores)
}
function normalizeBenchmarkName(value: string) {
return value
.toLowerCase()
.replace(/\u03c4/g, "tau")
.replace(/\u00b2/g, "2")
.replace(/\u00b3/g, "3")
.replace(/[^a-z0-9]+/g, " ")
.trim()
}
function modelCost(model: ModelCatalogEntry) {
if (!model.cost) return
return model.cost.input + model.cost.output
}
function multimodalFeatureCount(model: ModelCatalogEntry) {
return new Set([
...model.modalities.input.filter((modality) => modality !== "text").map((modality) => `input:${modality}`),
...model.modalities.output.filter((modality) => modality !== "text").map((modality) => `output:${modality}`),
...(model.attachment ? ["attachment"] : []),
]).size
}
function percentileScore(value: number, values: number[], direction: "higher" | "lower") {
const finite = values.filter(Number.isFinite)
if (!Number.isFinite(value) || finite.length < 2) return
const below = finite.filter((candidate) => candidate < value).length
const equal = finite.filter((candidate) => candidate === value).length
const percentile = ((below + (equal - 1) / 2) / (finite.length - 1)) * 100
return direction === "higher" ? percentile : 100 - percentile
}
function radarPoint(index: number, count: number, score: number): RadarPoint {
const angle = -Math.PI / 2 + (index * Math.PI * 2) / count
const radius = Math.max(0, Math.min(100, score)) / 2
return {
x: roundRadarCoordinate(50 + Math.cos(angle) * radius),
y: roundRadarCoordinate(50 + Math.sin(angle) * radius),
}
}
function radarPolygonPoints(count: number, score: number) {
return Array.from({ length: count })
.map((_, index) => radarPoint(index, count, score))
.map((point) => `${point.x},${point.y}`)
.join(" ")
}
function radarSeriesPolygon(scores: (number | undefined)[]) {
if (scores.some((score) => score === undefined)) return
return scores
.map((score, index) => radarPoint(index, scores.length, score ?? 0))
.map((point) => `${point.x},${point.y}`)
.join(" ")
}
function radarSeriesConnections(scores: (number | undefined)[]) {
return scores.flatMap((score, index) => {
const nextIndex = (index + 1) % scores.length
const next = scores[nextIndex]
if (score === undefined || next === undefined) return []
return [
{
start: radarPoint(index, scores.length, score),
end: radarPoint(nextIndex, scores.length, next),
},
]
})
}
function radarAxisStyle(index: number, count: number) {
const angle = -Math.PI / 2 + (index * Math.PI * 2) / count
const horizontal = Math.cos(angle)
return {
"--compare-radar-axis-x": `${roundRadarCoordinate(50 + horizontal * 42)}%`,
"--compare-radar-axis-mobile-x": `${roundRadarCoordinate(50 + horizontal * 36)}%`,
"--compare-radar-axis-y": `${roundRadarCoordinate(50 + Math.sin(angle) * 42)}%`,
"--compare-radar-axis-translate-x": horizontal > 0.25 ? "0%" : horizontal < -0.25 ? "-100%" : "-50%",
} as JSX.CSSProperties
}
function radarTooltipStyle(index: number, count: number) {
const angle = -Math.PI / 2 + (index * Math.PI * 2) / count
return {
"--compare-radar-tooltip-x": `${roundRadarCoordinate(50 + Math.cos(angle) * 42)}%`,
"--compare-radar-tooltip-y": `${roundRadarCoordinate(50 + Math.sin(angle) * 42)}%`,
"--compare-radar-tooltip-translate-y": Math.sin(angle) < -0.9 ? "20px" : "calc(-100% - 12px)",
} as JSX.CSSProperties
}
function roundRadarCoordinate(value: number) {
return Math.round(value * 1000) / 1000
}
function formatRadarScore(score: number | undefined) {
return score === undefined ? "No data" : `${Math.round(score)}/100`
}

View file

@ -0,0 +1,47 @@
import { Meta, Title } from "@solidjs/meta"
import { createAsync, useParams } from "@solidjs/router"
import { createMemo, Show } from "solid-js"
import ModelCompareDetailPage from "../../../component/model-compare-detail"
import { resolveComparisonFamily } from "../../../lib/comparison-pages"
import { getModelCatalog } from "../../model-catalog"
export default function ModelCompareFamily() {
const params = useParams()
const catalog = createAsync(() => getModelCatalog())
const comparison = createMemo(() => {
const source = catalog()
if (!source) return undefined
const first = resolveComparisonFamily(source, params.firstFamily ?? "")
const second = resolveComparisonFamily(source, params.secondFamily ?? "")
if (!first || !second || first.slug === second.slug) return null
return { first, second }
})
return (
<Show
when={comparison()}
fallback={
<Show when={comparison() === null}>
<Title>Model comparison not found</Title>
<Meta name="robots" content="noindex,follow" />
<main data-page="stats">
<div data-component="empty-state">
<strong>Comparison not found</strong>
<p>Choose two model families to compare.</p>
<a href={`${import.meta.env.BASE_URL}compare`}>Compare models</a>
</div>
</main>
</Show>
}
>
{(resolved) => (
<ModelCompareDetailPage
first={{ lab: resolved().first.model.lab, slug: resolved().first.model.slug }}
second={{ lab: resolved().second.model.lab, slug: resolved().second.model.slug }}
family={resolved()}
catalog={catalog()}
/>
)}
</Show>
)
}

View file

@ -1,611 +1 @@
import "../../../../index.css"
import { Link, Meta, Title } from "@solidjs/meta"
import { getStatsModelComparisonData, type StatsModelComparisonEntry } from "@opencode-ai/stats-core/domain/home"
import { runtime } from "@opencode-ai/stats-core/runtime"
import { createAsync, query, useParams } from "@solidjs/router"
import { createMemo, createSignal, For, onMount, Show } from "solid-js"
import { getRequestEvent } from "solid-js/web"
import {
ComparisonCardsSection,
modelRefFromCatalog,
uniqueComparisonPairs,
type ComparisonModelRef,
type ComparisonPair,
} from "../../../../compare-cards"
import { ComparisonSelector } from "../../../../compare-selector"
import {
catalogSlug,
findModelCatalogEntry,
formatCatalogLabName,
getModelCatalog,
type ModelCatalog,
type ModelCatalogEntry,
} from "../../../../model-catalog"
import {
applyThemePreference,
Footer,
getGitHubStars,
Header,
isThemePreference,
themeStorageKey,
type HeaderLink,
type ThemePreference,
} from "../../../../stats-shell"
const compareFallbackUrl = "https://stats.opencode.ai"
const compareHeaderLinks: readonly HeaderLink[] = [
{ href: "#overview", label: "Overview" },
{ href: "#comparison", label: "Comparison" },
{ href: "#compare-tool", label: "Compare" },
{ href: "#model-comparison", label: "Related" },
]
const compareFooterLinks: readonly HeaderLink[] = [
{ href: import.meta.env.BASE_URL, label: "Data Home" },
{ href: `${import.meta.env.BASE_URL}compare`, label: "Model Compare" },
{ href: `${import.meta.env.BASE_URL}#top-models`, label: "Top Models" },
{ href: `${import.meta.env.BASE_URL}#token-cost`, label: "Token Cost" },
]
type ComparisonModel = {
name: string
lab: string
labName: string
slug: string
catalog: ModelCatalogEntry | null
stats: StatsModelComparisonEntry | null
}
type ComparisonDirection = "higher" | "lower"
type ComparisonCell = { value: string; detail?: string; score?: number }
type ComparisonRow = {
label: string
description: string
direction: ComparisonDirection
cells: [ComparisonCell, ComparisonCell]
}
const getComparisonData = query(
async (firstLab: string, firstModel: string, secondLab: string, secondModel: string) => {
"use server"
return runtime.runPromise(getStatsModelComparisonData(firstLab, firstModel, secondLab, secondModel))
},
"getStatsModelComparisonData",
)
export default function ModelComparePair() {
const event = getRequestEvent()
event?.response.headers.set("Cache-Control", "public, max-age=60, s-maxage=300, stale-while-revalidate=86400")
const params = useParams()
const firstLabParam = createMemo(() => params.firstLab ?? "")
const firstModelParam = createMemo(() => params.firstModel ?? "")
const secondLabParam = createMemo(() => params.secondLab ?? "")
const secondModelParam = createMemo(() => params.secondModel ?? "")
const catalog = createAsync(() => getModelCatalog())
const firstCatalog = createMemo(() => resolvedCatalogEntry(catalog(), firstLabParam(), firstModelParam()))
const secondCatalog = createMemo(() => resolvedCatalogEntry(catalog(), secondLabParam(), secondModelParam()))
const stats = createAsync(() => {
if (catalog() === undefined || firstCatalog() === undefined || secondCatalog() === undefined)
return Promise.resolve(undefined)
return getComparisonData(
firstCatalog()?.lab ?? firstLabParam(),
firstCatalog()?.slug ?? firstModelParam(),
secondCatalog()?.lab ?? secondLabParam(),
secondCatalog()?.slug ?? secondModelParam(),
)
})
const githubStars = createAsync(() => getGitHubStars())
const [themePreference, setThemePreference] = createSignal<ThemePreference>("system")
const models = createMemo(
() =>
[
buildComparisonModel(firstLabParam(), firstModelParam(), firstCatalog() ?? null, stats()?.models[0] ?? null),
buildComparisonModel(secondLabParam(), secondModelParam(), secondCatalog() ?? null, stats()?.models[1] ?? null),
] as const,
)
const title = createMemo(() => `${models()[0].name} vs ${models()[1].name} - Model Comparison`)
const description = createMemo(
() =>
`Compare ${models()[0].name} and ${models()[1].name} by usage, rank, context window, output limit, cache ratio, and cost across OpenCode data.`,
)
const canonicalPath = createMemo(
() =>
`${import.meta.env.BASE_URL}compare/${catalogSlug(models()[0].lab)}/${catalogSlug(models()[0].slug)}/${catalogSlug(
models()[1].lab,
)}/${catalogSlug(models()[1].slug)}`,
)
const canonicalUrl = createMemo(() =>
new URL(
canonicalPath(),
event?.request.url ?? (typeof window === "undefined" ? compareFallbackUrl : window.location.href),
).toString(),
)
const rows = createMemo(() => buildComparisonRows(models()[0], models()[1]))
const relatedPairs = createMemo(() => buildRelatedPairs(catalog(), models()[0], models()[1]))
const selectorModels = createMemo(() =>
uniqueCatalogModels([
comparisonCatalogEntry(models()[0]),
comparisonCatalogEntry(models()[1]),
...(catalog()?.models ?? []),
]),
)
const structuredData = createMemo(() =>
JSON.stringify({
"@context": "https://schema.org",
"@type": "WebPage",
name: title(),
description: description(),
url: canonicalUrl(),
about: models().map((model) => ({
"@type": "SoftwareApplication",
name: model.name,
applicationCategory: "AI model",
provider: model.labName,
})),
}),
)
const updateThemePreference = (preference: ThemePreference) => {
applyThemePreference(preference)
setThemePreference(preference)
if (typeof window === "undefined") return
window.localStorage.setItem(themeStorageKey, preference)
}
onMount(() => {
if (typeof window === "undefined") return
const preference = window.localStorage.getItem(themeStorageKey)
const nextPreference = isThemePreference(preference) ? preference : "system"
applyThemePreference(nextPreference)
setThemePreference(nextPreference)
})
return (
<main data-page="stats" data-theme={themePreference()}>
<Title>{title()}</Title>
<Meta name="description" content={description()} />
<Link rel="canonical" href={canonicalUrl()} />
<Meta property="og:type" content="website" />
<Meta property="og:site_name" content="OpenCode" />
<Meta property="og:title" content={title()} />
<Meta property="og:description" content={description()} />
<Meta property="og:url" content={canonicalUrl()} />
<Meta name="twitter:card" content="summary" />
<Meta name="twitter:title" content={title()} />
<Meta name="twitter:description" content={description()} />
<script type="application/ld+json">{structuredData()}</script>
<Header githubStars={githubStars() ?? "150K"} links={compareHeaderLinks} brandHref={import.meta.env.BASE_URL} />
<div data-component="container">
<div data-component="content">
<ComparisonHero models={models()} />
<section id="comparison" data-section="model-panel">
<p data-slot="section-title">
<strong>Comparison Table.</strong> <span>Compare usage, cost, limits, and features.</span>
</p>
<Show
when={stats() !== undefined}
fallback={
<div data-component="empty-state" data-compact="true">
<strong>Loading comparison</strong>
<p>Loading stats for both models.</p>
</div>
}
>
<ComparisonTable models={models()} rows={rows()} />
</Show>
</section>
<section id="compare-tool" data-section="model-panel" data-variant="compact">
<p data-slot="section-title">
<strong>Compare Another Pair.</strong> <span>Choose two models to compare.</span>
</p>
<Show
when={selectorModels().length > 1}
fallback={
<div data-component="empty-state" data-compact="true">
<strong>No models found</strong>
<p>The model list could not be loaded.</p>
</div>
}
>
<ComparisonSelector
models={selectorModels()}
firstId={comparisonCatalogEntry(models()[0]).id}
secondId={comparisonCatalogEntry(models()[1]).id}
/>
</Show>
</section>
<ComparisonCardsSection
pairs={relatedPairs()}
title="Related Model Comparisons"
description="Other model pairs to check."
/>
</div>
<Footer
themePreference={themePreference()}
onThemePreferenceChange={updateThemePreference}
links={compareFooterLinks}
bridge={{ href: "#comparison", label: "COMPARE TABLE" }}
/>
</div>
</main>
)
}
function ComparisonHero(props: { models: readonly [ComparisonModel, ComparisonModel] }) {
return (
<section id="overview" data-section="model-hero">
<a data-slot="model-back-link" href={`${import.meta.env.BASE_URL}compare`}>
Compare
</a>
<div data-slot="model-hero-copy">
<h1>
{props.models[0].name} vs {props.models[1].name}
</h1>
<p>Compare usage, cost, limits, and features for these two models.</p>
</div>
<div data-slot="model-hero-pattern" aria-hidden="true" />
</section>
)
}
function ComparisonTable(props: { models: readonly [ComparisonModel, ComparisonModel]; rows: ComparisonRow[] }) {
return (
<div data-component="comparison-table-wrap">
<table data-component="comparison-table">
<caption>
{props.models[0].name} compared with {props.models[1].name}
</caption>
<thead>
<tr>
<th scope="col">Metric</th>
<For each={props.models}>{(model) => <th scope="col">{model.name}</th>}</For>
</tr>
</thead>
<tbody>
<For each={props.rows}>
{(row) => {
const best = () => bestCellIndex(row)
return (
<tr>
<th scope="row">
<strong>{row.label}</strong>
<span>{row.description}</span>
</th>
<For each={row.cells}>
{(cell, index) => (
<td data-best={best() === index() ? "true" : undefined}>
<strong>{cell.value}</strong>
<Show when={cell.detail}>{(detail) => <span>{detail()}</span>}</Show>
</td>
)}
</For>
</tr>
)
}}
</For>
</tbody>
</table>
</div>
)
}
function resolvedCatalogEntry(catalog: ModelCatalog | undefined, lab: string, model: string) {
if (!catalog) return undefined
return findModelCatalogEntry(catalog, model, lab) ?? null
}
function buildComparisonModel(
labParam: string,
modelParam: string,
catalog: ModelCatalogEntry | null,
stats: StatsModelComparisonEntry | null,
): ComparisonModel {
return {
name: catalog?.name ?? stats?.model ?? formatParamName(modelParam),
lab: catalog?.lab ?? stats?.provider ?? catalogSlug(labParam),
labName: formatCatalogLabName(catalog?.lab ?? stats?.provider ?? labParam),
slug: catalog?.slug ?? stats?.slug ?? catalogSlug(modelParam),
catalog,
stats,
}
}
function comparisonCatalogEntry(model: ComparisonModel): ModelCatalogEntry {
if (model.catalog) return model.catalog
return {
id: `${catalogSlug(model.lab)}/${catalogSlug(model.slug)}`,
lab: catalogSlug(model.lab),
slug: catalogSlug(model.slug),
name: model.name,
modalities: { input: [], output: [] },
openWeights: false,
reasoning: false,
toolCall: false,
attachment: false,
temperature: false,
weights: [],
benchmarks: [],
}
}
function uniqueCatalogModels(models: ModelCatalogEntry[]) {
return Object.values(
models.reduce<Record<string, ModelCatalogEntry>>((result, model) => {
result[model.id] = result[model.id] ?? model
return result
}, {}),
)
}
function buildComparisonRows(first: ComparisonModel, second: ComparisonModel): ComparisonRow[] {
return [
comparisonRow(
"Recent Rank",
"Lower is better.",
{
value: first.stats?.rank == null ? "No usage" : `#${first.stats.rank}`,
score: first.stats?.rank ?? undefined,
},
{
value: second.stats?.rank == null ? "No usage" : `#${second.stats.rank}`,
score: second.stats?.rank ?? undefined,
},
"lower",
),
comparisonRow(
"Token Share",
"Share of recent OpenCode usage.",
{ value: first.stats ? formatPercent(first.stats.tokenShare) : "No usage", score: first.stats?.tokenShare },
{ value: second.stats ? formatPercent(second.stats.tokenShare) : "No usage", score: second.stats?.tokenShare },
"higher",
),
comparisonRow(
"Tokens",
"Recent token volume.",
{ value: first.stats ? formatTokens(first.stats.totals.tokens) : "No usage", score: first.stats?.totals.tokens },
{
value: second.stats ? formatTokens(second.stats.totals.tokens) : "No usage",
score: second.stats?.totals.tokens,
},
"higher",
),
comparisonRow(
"Sessions",
"Recent session count.",
{
value: first.stats ? formatInteger(first.stats.totals.sessions) : "No usage",
score: first.stats?.totals.sessions,
},
{
value: second.stats ? formatInteger(second.stats.totals.sessions) : "No usage",
score: second.stats?.totals.sessions,
},
"higher",
),
comparisonRow(
"Cost / 1M Tokens",
"Lower is better.",
{
value: first.stats ? formatMoney(first.stats.totals.costPerMillion) : "No usage",
score: positiveScore(first.stats?.totals.costPerMillion),
},
{
value: second.stats ? formatMoney(second.stats.totals.costPerMillion) : "No usage",
score: positiveScore(second.stats?.totals.costPerMillion),
},
"lower",
),
comparisonRow(
"Cost / Session",
"Lower is better.",
{
value: first.stats ? formatSessionCost(first.stats.totals.costPerSession) : "No usage",
score: positiveScore(first.stats?.totals.costPerSession),
},
{
value: second.stats ? formatSessionCost(second.stats.totals.costPerSession) : "No usage",
score: positiveScore(second.stats?.totals.costPerSession),
},
"lower",
),
comparisonRow(
"Cache Ratio",
"Higher is better.",
{
value: first.stats ? formatPercent(first.stats.totals.cacheRatio) : "No usage",
score: first.stats?.totals.cacheRatio,
},
{
value: second.stats ? formatPercent(second.stats.totals.cacheRatio) : "No usage",
score: second.stats?.totals.cacheRatio,
},
"higher",
),
comparisonRow(
"Context Window",
"Higher limit is better.",
{
value: formatCatalogLimit(first.catalog?.limit?.context),
score: first.catalog?.limit?.context,
},
{
value: formatCatalogLimit(second.catalog?.limit?.context),
score: second.catalog?.limit?.context,
},
"higher",
),
comparisonRow(
"Output Limit",
"Higher limit is better.",
{
value: formatCatalogLimit(first.catalog?.limit?.output),
score: first.catalog?.limit?.output,
},
{
value: formatCatalogLimit(second.catalog?.limit?.output),
score: second.catalog?.limit?.output,
},
"higher",
),
comparisonRow(
"Release Date",
"Newer release is highlighted.",
{
value: formatCatalogDate(first.catalog?.releaseDate),
score: catalogDateScore(first.catalog?.releaseDate),
},
{
value: formatCatalogDate(second.catalog?.releaseDate),
score: catalogDateScore(second.catalog?.releaseDate),
},
"higher",
),
comparisonRow(
"Reasoning",
"Supports reasoning.",
booleanCell(first.catalog?.reasoning),
booleanCell(second.catalog?.reasoning),
"higher",
),
comparisonRow(
"Tool Calling",
"Supports tool calls.",
booleanCell(first.catalog?.toolCall),
booleanCell(second.catalog?.toolCall),
"higher",
),
comparisonRow(
"Attachments",
"Supports attachments.",
booleanCell(first.catalog?.attachment),
booleanCell(second.catalog?.attachment),
"higher",
),
comparisonRow(
"Open Weights",
"Open weights available.",
booleanCell(first.catalog?.openWeights),
booleanCell(second.catalog?.openWeights),
"higher",
),
]
}
function comparisonRow(
label: string,
description: string,
first: ComparisonCell,
second: ComparisonCell,
direction: ComparisonDirection,
): ComparisonRow {
return { label, description, direction, cells: [first, second] }
}
function bestCellIndex(row: ComparisonRow) {
const [first, second] = row.cells.map((cell) => cell.score)
if (first === undefined || second === undefined || first === second) return undefined
if (row.direction === "higher") return first > second ? 0 : 1
return first < second ? 0 : 1
}
function buildRelatedPairs(
catalog: ModelCatalog | undefined,
first: ComparisonModel,
second: ComparisonModel,
): ComparisonPair[] {
const current = [comparisonRef(first), comparisonRef(second)] as const
const alternatives = (catalog?.models ?? [])
.filter((model) => model.id !== first.catalog?.id && model.id !== second.catalog?.id)
.slice(0, 4)
.map(modelRefFromCatalog)
return uniqueComparisonPairs([
...alternatives.slice(0, 3).flatMap((model, index) => [
{ first: current[0], second: model, detail: index === 0 ? "Nearby alternative" : "Related comparison" },
{ first: current[1], second: model, detail: index === 0 ? "Nearby alternative" : "Related comparison" },
]),
]).slice(0, 6)
}
function comparisonRef(model: ComparisonModel): ComparisonModelRef {
return {
name: model.name,
lab: model.lab,
slug: model.slug,
labName: model.labName,
metric: model.stats ? `#${model.stats.rank}` : "Catalog",
}
}
function positiveScore(value: number | undefined) {
return value && value > 0 ? value : undefined
}
function booleanCell(value: boolean | undefined): ComparisonCell {
if (value === undefined) return { value: "Unknown" }
return { value: value ? "Yes" : "No", score: value ? 1 : 0 }
}
function catalogDateScore(value: string | undefined) {
if (!value) return undefined
const match = /^(\d{4})(?:-(\d{2}))?(?:-(\d{2}))?$/.exec(value)
if (!match) return undefined
return Date.UTC(Number(match[1]), match[2] ? Number(match[2]) - 1 : 0, match[3] ? Number(match[3]) : 1)
}
function formatParamName(value: string) {
return value
.replace(/[-_]/g, " ")
.replace(/\b\w/g, (letter) => letter.toUpperCase())
.trim()
}
function formatCatalogLimit(value: number | undefined) {
return value === undefined ? "Unknown" : formatTokens(value)
}
function formatCatalogDate(value: string | undefined) {
if (!value) return "Unknown"
const match = /^(\d{4})(?:-(\d{2}))?(?:-(\d{2}))?$/.exec(value)
if (!match) return value
const year = Number(match[1])
const month = match[2] ? Number(match[2]) - 1 : 0
const day = match[3] ? Number(match[3]) : 1
return new Intl.DateTimeFormat("en", {
month: match[2] ? "short" : undefined,
day: match[3] ? "numeric" : undefined,
year: "numeric",
timeZone: "UTC",
}).format(new Date(Date.UTC(year, month, day)))
}
function formatTokens(value: number) {
if (value >= 1_000_000_000_000)
return `${trimNumber(value / 1_000_000_000_000, value >= 10_000_000_000_000 ? 0 : 1)}T`
if (value >= 1_000_000_000) return `${trimNumber(value / 1_000_000_000, value >= 10_000_000_000 ? 0 : 1)}B`
if (value >= 1_000_000) return `${trimNumber(value / 1_000_000, value >= 10_000_000 ? 0 : 1)}M`
if (value >= 1_000) return `${trimNumber(value / 1_000, value >= 10_000 ? 0 : 1)}K`
return String(Math.round(value))
}
function formatInteger(value: number) {
return new Intl.NumberFormat("en").format(value)
}
function formatPercent(value: number) {
return `${trimNumber(value, value >= 10 ? 1 : 2)}%`
}
function formatMoney(value: number) {
if (value >= 1) return `$${trimNumber(value, 2)}`
if (value > 0) return `$${value.toFixed(4)}`
return "$0"
}
function formatSessionCost(value: number) {
if (value >= 1) return `$${trimNumber(value, 2)}`
if (value >= 0.01) return `$${value.toFixed(2)}`
if (value > 0) return `$${value.toFixed(4)}`
return "$0"
}
function trimNumber(value: number, digits: number) {
return Number(value.toFixed(digits)).toLocaleString("en")
}
export { default } from "../../../../../component/model-compare-detail"

View file

@ -8,7 +8,13 @@ import { LocaleLinks } from "../../component/locale-links"
import { useI18n } from "../../context/i18n"
import { useLanguage } from "../../context/language"
import { localizedUrl } from "../../lib/language"
import { comparisonHref, modelRefFromCatalog, type ComparisonModelRef } from "../compare-cards"
import {
ComparisonCardsSection,
comparisonHref,
modelRefFromCatalog,
type ComparisonModelRef,
type ComparisonPair,
} from "../compare-cards"
import { formatCatalogLabName, getModelCatalog, type ModelCatalogEntry } from "../model-catalog"
import { setStatsPageCacheHeaders } from "../stats-cache"
import {
@ -56,13 +62,6 @@ const categoryTemplates = [
},
] as const
type CompareCategory = {
title: string
description: string
first: ComparisonModelRef
second: ComparisonModelRef
avatars: ComparisonModelRef[]
}
type CompareSlot = "first" | "second"
export default function ModelCompareIndex() {
@ -162,16 +161,12 @@ export default function ModelCompareIndex() {
<CompareHomeSelector models={featuredModels()} />
</Show>
</section>
<Show when={categories().length > 0}>
<section id="model-comparison" data-section="compare-home-related">
<p data-slot="section-title">
<strong>Related comparisons.</strong> <span>Other model pairs to check.</span>
</p>
<div data-component="compare-home-card-grid">
<For each={categories()}>{(category) => <CompareHomeCard category={category} />}</For>
</div>
</section>
</Show>
<ComparisonCardsSection
pairs={categories()}
title="Related comparisons"
description="Other model pairs to check."
variant="featured"
/>
</div>
<Footer
themePreference={themePreference()}
@ -443,35 +438,6 @@ function HeroModelStack() {
)
}
function CompareHomeCard(props: { category: CompareCategory }) {
return (
<a
data-component="compare-home-card"
href={comparisonHref(props.category.first, props.category.second)}
aria-label={`${props.category.title}: ${props.category.first.name} vs ${props.category.second.name}`}
>
<span data-slot="compare-home-card-head">
<span>
<strong>{props.category.title}</strong>
<em>{props.category.description}</em>
</span>
<b aria-hidden="true" />
</span>
<span data-slot="compare-home-card-divider" aria-hidden="true" />
<span data-slot="compare-home-card-models">
<span>{props.category.first.name}</span>
<i aria-hidden="true">·</i>
<span>{props.category.second.name}</span>
</span>
<span data-slot="compare-home-card-avatars" aria-hidden="true">
<For each={props.category.avatars}>
{(model) => <LabLogo lab={model.lab} label={model.name} size="small" />}
</For>
</span>
</a>
)
}
function ModelAvatar(props: { model: ModelCatalogEntry; size: "large" | "small" | "tiny" }) {
return <LabLogo lab={props.model.lab} label={props.model.name} size={props.size} />
}
@ -486,8 +452,8 @@ function LabLogo(props: { lab: string; label: string; size: "large" | "small" |
)
}
function buildComparisonCategories(models: ModelCatalogEntry[]): CompareCategory[] {
return categoryTemplates.reduce<{ keys: Set<string>; categories: CompareCategory[] }>(
function buildComparisonCategories(models: ModelCatalogEntry[]): ComparisonPair[] {
return categoryTemplates.reduce<{ keys: Set<string>; categories: ComparisonPair[] }>(
(result, template, index) => {
const candidates = categoryCandidates(template.kind, models)
const pair = categoryPair(candidates, models, index, result.keys)
@ -496,11 +462,10 @@ function buildComparisonCategories(models: ModelCatalogEntry[]): CompareCategory
const first = modelRefFromCatalog(pair.first)
const second = modelRefFromCatalog(pair.second)
result.categories.push({
title: template.title,
detail: template.title,
description: template.description,
first,
second,
avatars: [first, second],
})
return result
},

View file

@ -87,6 +87,11 @@
display: none !important;
}
[data-page="stats"][data-layout="compare-detail"] {
/* The table contains its own wide rows; keep the page itself out of the horizontal scroll chain. */
overflow-x: visible;
}
[data-page="stats"] section[id],
[data-page="stats"] [data-component="leaderboard"][id] {
scroll-margin-top: 88px;
@ -5852,7 +5857,6 @@
}
[data-page="stats"] [data-slot="compare-home-card-head"] b {
position: relative;
display: grid;
place-items: center;
width: 32px;
@ -5863,32 +5867,10 @@
box-shadow: 0 1px 1.5px #0000000f;
}
[data-page="stats"] [data-slot="compare-home-card-head"] b::before {
position: absolute;
top: 8px;
left: 8px;
width: 3px;
height: 3px;
content: "";
background: currentColor;
box-shadow:
7px 0 currentColor,
0 7px currentColor;
color: var(--stats-muted);
}
[data-page="stats"] [data-slot="compare-home-card-head"] b::after {
position: absolute;
right: 5px;
bottom: 5px;
width: 12px;
height: 12px;
box-sizing: border-box;
content: "";
background:
radial-gradient(circle at 4px 4px, transparent 2.5px, var(--stats-muted) 2.75px 4px, transparent 4.25px),
linear-gradient(var(--stats-muted) 0 0) 7px 8px / 5px 1.5px no-repeat;
transform: rotate(45deg);
[data-page="stats"] [data-slot="compare-home-card-head"] b svg {
display: block;
width: 16px;
height: 16px;
}
[data-page="stats"] [data-slot="compare-home-card-divider"] {
@ -5923,6 +5905,858 @@
min-width: 0;
}
[data-page="stats"] [data-section="compare-detail-hero"] {
position: relative;
display: grid;
align-content: end;
gap: 24px;
min-height: 316px;
box-sizing: border-box;
padding: 128px 40px 40px;
color: var(--stats-text);
}
[data-page="stats"] [data-slot="compare-detail-hero-grid"] {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 24px;
align-items: center;
}
[data-page="stats"] [data-section="compare-detail-hero"] h1 {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 16px;
min-width: 0;
margin: 0;
color: var(--stats-text);
font-size: 40px;
font-weight: 500;
line-height: 60px;
letter-spacing: 0;
}
[data-page="stats"] [data-slot="compare-detail-actions"] {
display: inline-flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
min-width: 0;
}
[data-page="stats"] button[data-slot="compare-detail-action"],
[data-page="stats"] a[data-slot="compare-detail-action"] {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
height: 32px;
box-sizing: border-box;
padding: 0 12px 0 8px;
overflow: hidden;
border: 0;
border-radius: 0;
color: var(--stats-text);
background: var(--stats-bg);
box-shadow:
0 0 0 0.5px color-mix(in srgb, var(--stats-text) 14%, transparent),
0 1px 1.5px color-mix(in srgb, #000000 10%, transparent);
cursor: pointer;
font: inherit;
font-size: 13px;
font-weight: 500;
line-height: 1.1;
text-decoration: none;
white-space: nowrap;
}
[data-page="stats"] button[data-slot="compare-detail-action"]::before,
[data-page="stats"] a[data-slot="compare-detail-action"]::before {
position: absolute;
inset: 0 0 auto;
height: 16px;
background: linear-gradient(to bottom, rgb(255 255 255 / 7%), transparent);
content: "";
pointer-events: none;
}
[data-page="stats"] [data-slot="compare-detail-action"] > * {
position: relative;
z-index: 1;
}
[data-page="stats"] button[data-slot="compare-detail-action"][aria-pressed] {
gap: 12px;
}
[data-page="stats"] button[data-slot="compare-detail-action"]:hover,
[data-page="stats"] button[data-slot="compare-detail-action"]:focus-visible,
[data-page="stats"] a[data-slot="compare-detail-action"]:hover,
[data-page="stats"] a[data-slot="compare-detail-action"]:focus-visible {
background: var(--stats-layer);
outline: none;
text-decoration: none;
}
[data-page="stats"] button[data-slot="compare-detail-action"]:disabled {
color: var(--stats-muted);
background: var(--stats-bg);
cursor: not-allowed;
}
[data-page="stats"] button[data-slot="compare-detail-action"][data-active="true"] {
color: var(--stats-text);
background: var(--stats-bg);
}
[data-page="stats"] [data-slot="compare-detail-highlight-icon"] {
display: flex;
align-items: center;
gap: 1px;
width: 31px;
height: 16px;
box-sizing: border-box;
padding: 1px;
overflow: hidden;
background: var(--stats-line-strong);
}
[data-page="stats"] [data-slot="compare-detail-highlight-icon"] i {
flex: 0 0 14px;
width: 14px;
height: 14px;
background: transparent;
}
[data-page="stats"]
[data-slot="compare-detail-action"][aria-pressed="true"]
[data-slot="compare-detail-highlight-icon"] {
background: var(--stats-accent);
}
[data-page="stats"]
[data-slot="compare-detail-action"][aria-pressed="false"]
[data-slot="compare-detail-highlight-icon"]
i:first-child,
[data-page="stats"]
[data-slot="compare-detail-action"][aria-pressed="true"]
[data-slot="compare-detail-highlight-icon"]
i:last-child {
background: #fafafa;
box-shadow:
0 0 0 0.5px rgb(0 0 0 / 12%),
0 1px 2px -1px rgb(0 0 0 / 8%),
0 2px 4px rgb(0 0 0 / 4%);
}
[data-page="stats"] [data-slot="compare-detail-action"] [data-slot="compare-home-plus"] {
display: grid;
place-items: center;
width: 16px;
height: 16px;
font-size: 16px;
line-height: 16px;
}
[data-page="stats"] [data-section="compare-radar"] {
position: relative;
display: grid;
grid-template-columns: minmax(0, 1fr) 800px minmax(0, 1fr);
width: 100%;
height: 800px;
box-sizing: border-box;
border-right: 1px solid var(--stats-line);
border-left: 1px solid var(--stats-line);
color: var(--stats-text);
background: var(--stats-bg);
}
[data-page="stats"] [data-slot="compare-radar-legend"] {
display: flex;
flex-direction: column;
gap: 14px;
min-width: 0;
margin: 0;
padding: 40px;
list-style: none;
}
[data-page="stats"] [data-slot="compare-radar-legend"] li {
display: grid;
grid-template-columns: 6px minmax(0, 1fr);
gap: 12px;
align-items: start;
min-width: 0;
}
[data-page="stats"] [data-slot="compare-radar-legend"] li > i {
width: 6px;
height: 6px;
margin-top: 6px;
}
[data-page="stats"] [data-slot="compare-radar-legend"] li > span {
display: grid;
gap: 2px;
min-width: 0;
}
[data-page="stats"] [data-slot="compare-radar-legend"] strong,
[data-page="stats"] [data-slot="compare-radar-legend"] small {
overflow: hidden;
font-size: 13px;
line-height: 18px;
text-overflow: ellipsis;
white-space: nowrap;
}
[data-page="stats"] [data-slot="compare-radar-legend"] strong {
color: var(--stats-text);
font-weight: 500;
}
[data-page="stats"] [data-slot="compare-radar-legend"] small {
color: var(--stats-muted);
font-weight: 400;
}
[data-page="stats"] [data-slot="compare-radar-chart"] {
position: relative;
width: 800px;
height: 800px;
}
[data-page="stats"] [data-slot="compare-radar-plot"] {
position: absolute;
inset: 17.5%;
}
[data-page="stats"] [data-slot="compare-radar-plot"] svg {
display: block;
width: 100%;
height: 100%;
overflow: visible;
}
[data-page="stats"] [data-slot="compare-radar-grid"] polygon,
[data-page="stats"] [data-slot="compare-radar-grid"] line {
fill: none;
stroke: var(--stats-line);
stroke-width: 1px;
vector-effect: non-scaling-stroke;
}
[data-page="stats"] [data-slot="compare-radar-area"],
[data-page="stats"] [data-slot="compare-radar-line"] {
stroke: currentColor;
stroke-width: 1.5px;
stroke-linejoin: round;
vector-effect: non-scaling-stroke;
}
[data-page="stats"] [data-slot="compare-radar-area"] {
fill: currentColor;
fill-opacity: 0.09;
}
[data-page="stats"] [data-slot="compare-radar-line"] {
fill: none;
}
[data-page="stats"] [data-slot="compare-radar-point"] {
fill: currentColor;
stroke: currentColor;
stroke-width: 1px;
vector-effect: non-scaling-stroke;
}
[data-page="stats"] [data-slot="compare-radar-point-hit"] {
fill: transparent;
cursor: pointer;
}
[data-page="stats"] [data-slot="compare-radar-axis"] {
position: absolute;
top: var(--compare-radar-axis-y);
left: var(--compare-radar-axis-x);
max-width: 160px;
color: var(--stats-text);
font-size: 16px;
font-weight: 400;
line-height: 20px;
text-align: center;
cursor: pointer;
transform: translate(var(--compare-radar-axis-translate-x), -50%);
white-space: nowrap;
}
[data-page="stats"] [data-slot="compare-radar-axis"]:focus-visible {
outline: none;
}
[data-page="stats"] [data-slot="compare-radar-axis-label"] {
display: inline-flex;
align-items: center;
height: 24px;
box-sizing: border-box;
padding: 0 8px;
margin: 0 -8px;
}
[data-page="stats"] [data-slot="compare-radar-axis"][data-active="true"] [data-slot="compare-radar-axis-label"] {
background: var(--stats-layer-2);
}
[data-page="stats"] [data-slot="compare-radar-axis"]:focus-visible [data-slot="compare-radar-axis-label"] {
outline: 1px solid var(--stats-text);
outline-offset: 2px;
}
[data-page="stats"] [data-slot="compare-radar-tooltip"] {
position: absolute;
z-index: 5;
top: var(--compare-radar-tooltip-y);
left: clamp(104px, var(--compare-radar-tooltip-x), calc(100% - 104px));
display: flex;
flex-direction: column;
gap: 4px;
width: 192px;
box-sizing: border-box;
padding: 8px;
color: var(--stats-text);
background: var(--stats-layer);
box-shadow:
0 0 0 0.5px color-mix(in srgb, var(--stats-text) 12%, transparent),
0 4px 8px color-mix(in srgb, #000000 8%, transparent),
0 8px 16px color-mix(in srgb, #000000 4%, transparent);
pointer-events: none;
transform: translate(-50%, var(--compare-radar-tooltip-translate-y));
}
[data-page="stats"] [data-slot="compare-radar-tooltip"] strong,
[data-page="stats"] [data-slot="compare-radar-tooltip"] p {
margin: 0;
font-size: 11px;
letter-spacing: 0;
}
[data-page="stats"] [data-slot="compare-radar-tooltip"] strong {
color: var(--stats-text);
font-weight: 500;
line-height: 12px;
}
[data-page="stats"] [data-slot="compare-radar-tooltip"] p {
color: var(--stats-muted);
font-weight: 400;
line-height: 16px;
}
[data-page="stats"] [data-slot="compare-radar-data"] {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
overflow: hidden;
border: 0;
clip-path: inset(50%);
white-space: nowrap;
}
[data-page="stats"] [data-component="compare-detail-table"] {
--compare-detail-label-column: 292px;
--compare-detail-model-column-min: 360px;
position: relative;
}
[data-page="stats"] [data-component="compare-detail-heading-scroll"] {
position: sticky;
top: 72px;
z-index: 9;
overflow-x: auto;
overscroll-behavior-x: none;
background: var(--stats-bg);
scrollbar-width: none;
}
[data-page="stats"] [data-component="compare-detail-heading-scroll"]::-webkit-scrollbar {
display: none;
}
[data-page="stats"] [data-component="compare-detail-body-scroll"] {
overflow-x: auto;
overscroll-behavior-x: none;
}
[data-page="stats"] [data-section="compare-detail-selector"],
[data-page="stats"] [data-section="compare-detail-matrix"] {
min-width: calc(
var(--compare-detail-label-column) + var(--compare-detail-model-column-min) + var(--compare-detail-model-column-min)
);
}
[data-page="stats"] [data-section="compare-detail-selector"] {
position: relative;
height: 98px;
min-height: 98px;
box-sizing: border-box;
border: 0;
}
[data-page="stats"] [data-section="compare-detail-selector"]::after {
position: absolute;
z-index: 5;
inset: 0;
border-top: 1px solid var(--stats-line);
border-bottom: 1px solid var(--stats-line);
content: "";
pointer-events: none;
}
[data-page="stats"] [data-component="compare-detail-selector-grid"] {
display: grid;
grid-template-columns: var(--compare-detail-grid);
height: 100%;
}
[data-page="stats"]
[data-component="compare-detail-table"][data-model-count="3"]
[data-section="compare-detail-selector"],
[data-page="stats"]
[data-component="compare-detail-table"][data-model-count="3"]
[data-section="compare-detail-matrix"] {
min-width: calc(
var(--compare-detail-label-column) + var(--compare-detail-model-column-min) +
var(--compare-detail-model-column-min) + var(--compare-detail-model-column-min)
);
}
[data-page="stats"]
[data-component="compare-detail-table"][data-model-count="4"]
[data-section="compare-detail-selector"],
[data-page="stats"]
[data-component="compare-detail-table"][data-model-count="4"]
[data-section="compare-detail-matrix"] {
min-width: calc(
var(--compare-detail-label-column) + var(--compare-detail-model-column-min) +
var(--compare-detail-model-column-min) + var(--compare-detail-model-column-min) +
var(--compare-detail-model-column-min)
);
}
[data-page="stats"]
[data-component="compare-detail-table"][data-model-count="5"]
[data-section="compare-detail-selector"],
[data-page="stats"]
[data-component="compare-detail-table"][data-model-count="5"]
[data-section="compare-detail-matrix"] {
min-width: calc(
var(--compare-detail-label-column) + var(--compare-detail-model-column-min) +
var(--compare-detail-model-column-min) + var(--compare-detail-model-column-min) +
var(--compare-detail-model-column-min) + var(--compare-detail-model-column-min)
);
}
[data-page="stats"]
[data-component="compare-detail-table"][data-model-count="6"]
[data-section="compare-detail-selector"],
[data-page="stats"]
[data-component="compare-detail-table"][data-model-count="6"]
[data-section="compare-detail-matrix"] {
min-width: calc(
var(--compare-detail-label-column) + var(--compare-detail-model-column-min) +
var(--compare-detail-model-column-min) + var(--compare-detail-model-column-min) +
var(--compare-detail-model-column-min) + var(--compare-detail-model-column-min) +
var(--compare-detail-model-column-min)
);
}
[data-page="stats"] [data-slot="compare-detail-selector-spacer"] {
position: sticky;
left: 0;
z-index: 3;
min-width: 0;
box-sizing: border-box;
border-right: 1px solid var(--stats-line);
border-left: 1px solid var(--stats-line);
background: var(--stats-bg);
}
[data-page="stats"] button[data-slot="compare-detail-select-model"] {
position: relative;
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
height: 100%;
box-sizing: border-box;
padding: 0 40px;
border: 0;
appearance: none;
border-radius: 0;
color: var(--stats-text);
background: var(--stats-bg);
cursor: pointer;
font: inherit;
text-align: left;
}
[data-page="stats"] button[data-slot="compare-detail-select-model"][data-column]:not([data-column="0"]) {
border-left: 1px solid var(--stats-line);
}
[data-page="stats"] button[data-slot="compare-detail-select-model"][data-last="true"] {
border-right: 1px solid var(--stats-line);
}
[data-page="stats"] button[data-slot="compare-detail-select-model"]:hover,
[data-page="stats"] button[data-slot="compare-detail-select-model"]:focus-visible {
background: var(--stats-layer);
outline: none;
}
[data-page="stats"] [data-slot="compare-detail-select-name"] {
min-width: 0;
overflow: hidden;
color: var(--stats-text);
font-size: 13px;
font-weight: 500;
line-height: 18px;
text-overflow: ellipsis;
white-space: nowrap;
}
[data-page="stats"] button[data-slot="compare-detail-select-model"] svg {
flex: 0 0 auto;
color: var(--stats-muted);
}
[data-page="stats"] [data-section="compare-detail-matrix"] {
position: relative;
color: var(--stats-text);
border-bottom: 1px solid var(--stats-line);
}
[data-page="stats"] [data-component="compare-detail-matrix"] {
position: relative;
}
[data-page="stats"] [data-slot="compare-detail-group"] {
display: grid;
grid-template-columns: var(--compare-detail-grid);
}
[data-page="stats"] [data-slot="compare-detail-group"] + [data-slot="compare-detail-group"] {
border-top: 1px solid var(--stats-line);
}
[data-page="stats"] [data-slot="compare-detail-label"],
[data-page="stats"] [data-slot="compare-detail-value"] {
display: flex;
align-items: center;
min-width: 0;
min-height: 56px;
box-sizing: border-box;
padding: 0 40px;
font-size: 14px;
line-height: 24px;
}
[data-page="stats"] [data-slot="compare-detail-label"] {
position: sticky;
left: 0;
z-index: 3;
border-right: 1px solid var(--stats-line);
border-left: 1px solid var(--stats-line);
color: var(--stats-muted);
background: var(--stats-bg);
font-weight: 400;
}
[data-page="stats"] [data-slot="compare-detail-value"][data-column]:not([data-column="0"]) {
border-left: 1px solid var(--stats-line);
}
[data-page="stats"] [data-slot="compare-detail-value"][data-last="true"] {
border-right: 1px solid var(--stats-line);
}
[data-page="stats"] [data-slot="compare-detail-label"][data-spacer="true"],
[data-page="stats"] [data-slot="compare-detail-value"][data-spacer="true"] {
min-height: 40px;
padding: 0;
}
[data-page="stats"] [data-slot="compare-detail-label"][data-heading="true"] {
gap: 12px;
color: var(--stats-text);
font-weight: 500;
}
[data-page="stats"] [data-slot="compare-detail-label"][data-heading="true"] strong {
font-weight: 600;
}
[data-page="stats"] [data-slot="compare-detail-label"][data-heading="true"] span {
display: inline-flex;
align-items: center;
height: 24px;
padding: 0 8px;
color: var(--stats-muted);
background: var(--stats-layer-2);
font-size: 12px;
font-weight: 500;
line-height: 1;
}
[data-page="stats"] [data-slot="compare-detail-value"] {
justify-content: flex-end;
color: var(--stats-text);
font-weight: 400;
text-align: right;
}
[data-page="stats"] [data-slot="compare-detail-value"][data-best="true"] {
background: color-mix(in srgb, #198b43 8%, transparent);
}
[data-page="stats"] [data-slot="compare-detail-value-main"],
[data-page="stats"] [data-slot="compare-detail-value-link"] {
display: inline-flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
min-width: 0;
}
[data-page="stats"] [data-slot="compare-detail-value-link"] {
color: inherit;
text-decoration: underline;
text-decoration-color: color-mix(in srgb, var(--stats-text) 30%, transparent);
text-underline-offset: 2px;
}
[data-page="stats"] [data-slot="compare-detail-unit"] {
color: var(--stats-muted);
}
[data-page="stats"] [data-slot="compare-detail-trend"] {
display: inline-flex;
align-items: center;
height: 16px;
padding: 0 5px;
color: var(--stats-muted);
background: var(--stats-layer-2);
font-size: 11px;
font-weight: 600;
line-height: 1;
}
[data-page="stats"] [data-slot="compare-detail-trend"][data-trend="up"] {
color: #198b43;
background: #e2f8e9;
}
[data-page="stats"] [data-slot="compare-detail-trend"][data-trend="down"] {
color: #c93737;
background: #fae8e8;
}
[data-page="stats"] [data-slot="compare-detail-boolean"] {
display: inline-flex;
align-items: center;
height: 24px;
padding: 0 8px;
font-size: 12px;
font-weight: 700;
line-height: 1;
}
[data-page="stats"] [data-slot="compare-detail-boolean"][data-value="true"] {
color: #198b43;
background: #e2f8e9;
}
[data-page="stats"] [data-slot="compare-detail-boolean"][data-value="false"] {
color: #c93737;
background: #fae8e8;
}
[data-page="stats"] [data-slot="compare-detail-value"][data-chart="true"] {
display: grid;
align-content: center;
gap: 12px;
min-height: 102px;
}
[data-page="stats"] [data-slot="compare-detail-bars"] {
display: flex;
align-items: flex-end;
gap: 2px;
width: 100%;
height: 40px;
}
[data-page="stats"] [data-slot="compare-detail-bars"] i {
flex: 1 1 0;
min-width: 2px;
max-width: 6px;
background: color-mix(in srgb, var(--stats-text) 18%, transparent);
}
[data-page="stats"] [data-slot="compare-detail-bar-dates"] {
display: flex;
justify-content: space-between;
gap: 16px;
color: var(--stats-muted);
font-size: 11px;
font-weight: 500;
line-height: 14px;
}
[data-page="stats"] [data-slot="compare-detail-no-chart"] {
justify-self: end;
color: var(--stats-muted);
font-size: 12px;
}
@media (max-width: 80rem) {
[data-page="stats"] [data-section="compare-detail-hero"] {
min-height: 280px;
padding: 104px 32px 40px;
}
[data-page="stats"] [data-slot="compare-detail-hero-grid"] {
grid-template-columns: 1fr;
align-items: start;
}
[data-page="stats"] [data-slot="compare-detail-actions"] {
justify-content: flex-start;
}
[data-page="stats"] [data-section="compare-radar"] {
grid-template-columns: minmax(180px, 208px) minmax(0, 1fr);
}
[data-page="stats"] [data-slot="compare-radar-legend"] {
padding-right: 24px;
padding-left: 32px;
}
[data-page="stats"] [data-slot="compare-radar-chart"] {
align-self: center;
width: min(100%, 800px);
height: auto;
aspect-ratio: 1;
}
[data-page="stats"] [data-component="compare-detail-table"] {
--compare-detail-label-column: 220px;
--compare-detail-model-column-min: 320px;
}
[data-page="stats"] button[data-slot="compare-detail-select-model"],
[data-page="stats"] [data-slot="compare-detail-label"],
[data-page="stats"] [data-slot="compare-detail-value"] {
padding-right: 32px;
padding-left: 32px;
}
}
@media (max-width: 60rem) {
[data-page="stats"] [data-section="compare-detail-hero"] {
gap: 20px;
min-height: 316px;
padding: 72px 24px 40px;
}
[data-page="stats"] [data-section="compare-detail-hero"] h1 {
gap: 12px;
font-size: 32px;
line-height: 42px;
}
[data-page="stats"] [data-slot="compare-detail-actions"] {
flex-wrap: wrap;
}
[data-page="stats"] [data-section="compare-radar"] {
grid-template-columns: minmax(0, 1fr);
height: auto;
padding: 32px 24px 24px;
}
[data-page="stats"] [data-slot="compare-radar-legend"] {
flex-direction: row;
flex-wrap: wrap;
gap: 16px 32px;
padding: 0 0 16px;
}
[data-page="stats"] [data-slot="compare-radar-legend"] li {
flex: 1 1 180px;
}
[data-page="stats"] [data-slot="compare-radar-chart"] {
justify-self: center;
width: min(100%, 720px);
}
}
@media (max-width: 40rem) {
[data-page="stats"] [data-section="compare-detail-hero"] h1 {
align-items: flex-start;
flex-direction: column;
}
[data-page="stats"] [data-slot="compare-detail-action"] {
width: 100%;
}
[data-page="stats"] [data-section="compare-radar"] {
padding-right: 16px;
padding-left: 16px;
}
[data-page="stats"] [data-slot="compare-radar-legend"] {
gap: 12px;
}
[data-page="stats"] [data-slot="compare-radar-legend"] li {
flex-basis: 140px;
}
[data-page="stats"] [data-slot="compare-radar-axis"] {
left: var(--compare-radar-axis-mobile-x);
max-width: 104px;
font-size: 13px;
line-height: 16px;
white-space: normal;
}
[data-page="stats"] [data-slot="compare-radar-axis-label"] {
padding-right: 6px;
padding-left: 6px;
margin-right: -6px;
margin-left: -6px;
}
[data-page="stats"] [data-component="compare-detail-table"] {
--compare-detail-label-column: 188px;
--compare-detail-model-column-min: 246px;
}
[data-page="stats"] button[data-slot="compare-detail-select-model"],
[data-page="stats"] [data-slot="compare-detail-label"],
[data-page="stats"] [data-slot="compare-detail-value"] {
padding-right: 24px;
padding-left: 24px;
}
}
[data-page="stats"] [data-component="compare-model-modal-scrim"] {
position: fixed;
inset: 0;

View file

@ -194,6 +194,7 @@ export default function StatsHome() {
pairs={homeComparisonPairs(stats().leaderboard["All Users"]["2M"])}
title="Model Comparisons"
description="Popular model pairs from the leaderboard."
variant="featured"
/>
</>
)}

View file

@ -56,14 +56,18 @@ export type ModelCatalog = {
labs: ModelCatalogLab[]
}
export const getModelCatalog = query(async () => {
"use server"
export async function loadModelCatalog() {
const [models, pricing, labs] = await Promise.all([
fetchCatalogPayload(modelCatalogSourceUrl),
fetchCatalogPayload(modelCatalogPricingUrl),
fetchLabCatalogPayload(modelCatalogLabSourceUrl),
])
return buildModelCatalog(models, pricing, labs)
}
export const getModelCatalog = query(async () => {
"use server"
return loadModelCatalog()
}, "getModelCatalog")
export function findModelCatalogEntry(catalog: ModelCatalog, model: string, lab?: string) {

View file

@ -0,0 +1,113 @@
import { getStatsHomeData } from "@opencode-ai/stats-core/domain/home"
import { runtime } from "@opencode-ai/stats-core/runtime"
import {
canonicalFamilyComparisonPath,
canonicalModelComparisonPath,
comparisonFamilies,
comparisonSitemapModels,
latestFamilyComparisonPath,
resolveComparisonFamily,
} from "../lib/comparison-pages"
import { baseUrl } from "../lib/language"
import { loadModelCatalog } from "./model-catalog"
type SitemapEntry = {
path: string
lastmod?: string
}
export async function GET() {
const [catalog, stats] = await Promise.all([
loadModelCatalog(),
runtime.runPromise(getStatsHomeData()).catch(() => undefined),
])
const lastmod = sitemapDate(
stats?.updatedAt,
...catalog.models.map((model) => model.lastUpdated ?? model.releaseDate),
)
const families = comparisonFamilies.flatMap((family) => {
const resolved = resolveComparisonFamily(catalog, family.slug)
return resolved ? [resolved] : []
})
const familyComparisons = families.flatMap((first, index) =>
families.slice(index + 1).map((second) => ({
path: canonicalFamilyComparisonPath(first, second),
lastmod: sitemapDate(
stats?.updatedAt,
first.model.lastUpdated ?? first.model.releaseDate,
second.model.lastUpdated ?? second.model.releaseDate,
),
})),
)
const models = comparisonSitemapModels(catalog, stats?.leaderboard["All Users"]["2M"])
const modelComparisons = models.flatMap((first, index) =>
models.slice(index + 1).flatMap((second) => {
if (latestFamilyComparisonPath(catalog, first, second)) return []
return [
{
path: canonicalModelComparisonPath(first, second),
lastmod: sitemapDate(
stats?.updatedAt,
first.lastUpdated ?? first.releaseDate,
second.lastUpdated ?? second.releaseDate,
),
},
]
}),
)
const entries = uniqueSitemapEntries([{ path: "/data/compare", lastmod }, ...familyComparisons, ...modelComparisons])
return new Response(sitemapXml(entries), {
headers: {
"Cache-Control": "public, max-age=300, s-maxage=3600, stale-while-revalidate=86400",
"Content-Type": "application/xml; charset=utf-8",
},
})
}
function uniqueSitemapEntries(entries: SitemapEntry[]) {
return Object.values(
entries.reduce<Record<string, SitemapEntry>>((result, entry) => {
result[entry.path] = entry
return result
}, {}),
).toSorted((a, b) => a.path.localeCompare(b.path))
}
function sitemapXml(entries: SitemapEntry[]) {
const urls = entries
.map(
(entry) => ` <url>
<loc>${escapeXml(new URL(entry.path, baseUrl).toString())}</loc>${
entry.lastmod
? `
<lastmod>${entry.lastmod}</lastmod>`
: ""
}
</url>`,
)
.join("\n")
return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls}
</urlset>`
}
function sitemapDate(...values: (string | undefined | null)[]) {
const dates = values.flatMap((value) => {
if (!value) return []
const date = new Date(value)
return Number.isNaN(date.getTime()) ? [] : [date]
})
if (dates.length === 0) return undefined
return new Date(Math.min(Date.now(), Math.max(...dates.map((date) => date.getTime())))).toISOString().slice(0, 10)
}
function escapeXml(value: string) {
return value
.replaceAll("&", "&amp;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&apos;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
}

View file

@ -113,32 +113,32 @@ const poll: (
return yield* poll(client, queryExecutionId, attempt + 1)
})
const results: (
client: AwsAthenaClient,
queryExecutionId: string,
nextToken?: string,
) => Effect.Effect<AthenaData[], AthenaQueryError> = Effect.fn("Athena.results")(function* (
client: AwsAthenaClient,
queryExecutionId: string,
nextToken?: string,
) {
const result = yield* Effect.tryPromise({
try: () =>
client.send(
new GetQueryResultsCommand({
QueryExecutionId: queryExecutionId,
NextToken: nextToken,
MaxResults: ATHENA_PAGE_SIZE,
}),
),
catch: (cause) => new AthenaQueryError({ message: "Failed to read Athena stats results", queryExecutionId, cause }),
const results: (client: AwsAthenaClient, queryExecutionId: string) => Effect.Effect<AthenaData[], AthenaQueryError> =
Effect.fn("Athena.results")(function* (client: AwsAthenaClient, queryExecutionId: string) {
// Accumulate pages iteratively; recursive spreads copied every previously
// fetched row per page and blew up memory on large result sets.
const rows: AthenaData[] = []
let nextToken: string | undefined
while (true) {
const result = yield* Effect.tryPromise({
try: () =>
client.send(
new GetQueryResultsCommand({
QueryExecutionId: queryExecutionId,
NextToken: nextToken,
MaxResults: ATHENA_PAGE_SIZE,
}),
),
catch: (cause) =>
new AthenaQueryError({ message: "Failed to read Athena stats results", queryExecutionId, cause }),
})
const columns = result.ResultSet?.ResultSetMetadata?.ColumnInfo?.map((item) => item.Name ?? "") ?? []
// The first page starts with the header row.
for (const row of (result.ResultSet?.Rows ?? []).slice(nextToken ? 0 : 1)) rows.push(rowData(columns, row))
if (!result.NextToken) return rows
nextToken = result.NextToken
}
})
const columns = result.ResultSet?.ResultSetMetadata?.ColumnInfo?.map((item) => item.Name ?? "") ?? []
const rows = (result.ResultSet?.Rows ?? []).slice(nextToken ? 0 : 1).map((row) => rowData(columns, row))
if (!result.NextToken) return rows
return [...rows, ...(yield* results(client, queryExecutionId, result.NextToken))]
})
function rowData(columns: string[], row: Row): AthenaData {
return Object.fromEntries(

View file

@ -94,10 +94,15 @@ export type StatsModelComparisonEntry = {
tokenShare: number
tokenChange: number
totals: StatsModelData["totals"]
usage: ModelUsagePoint[]
}
export type StatsModelComparisonInput = {
provider: string
model: string
}
export type StatsModelComparisonData = {
updatedAt: string | null
models: [StatsModelComparisonEntry | null, StatsModelComparisonEntry | null]
models: (StatsModelComparisonEntry | null)[]
}
export type StatsHomeData = {
updatedAt: string | null
@ -289,27 +294,35 @@ function dateValue(value: unknown) {
return value instanceof Date ? value : new Date(stringValue(value))
}
export const getStatsModelComparisonData: (
firstProvider: string,
firstModel: string,
secondProvider: string,
secondModel: string,
) => Effect.Effect<StatsModelComparisonData, DatabaseError, ModelStatRepo> = Effect.fn("StatsModelComparison.getData")(
function* (firstProvider, firstModel, secondProvider, secondModel) {
export const getStatsModelsComparisonData: (
models: readonly StatsModelComparisonInput[],
) => Effect.Effect<StatsModelComparisonData, DatabaseError, ModelStatRepo> = Effect.fn("StatsModelsComparison.getData")(
function* (models) {
const modelStats = yield* ModelStatRepo
const rows = yield* modelStats.listDaily()
const first = toComparisonEntry(buildStatsModelData(firstModel, rows, [], firstProvider))
const second = toComparisonEntry(buildStatsModelData(secondModel, rows, [], secondProvider))
const latest = [first?.updatedAt, second?.updatedAt]
const entries = models.map((model) => toComparisonEntry(buildStatsModelData(model.model, rows, [], model.provider)))
const latest = entries
.map((model) => model?.updatedAt)
.flatMap((value) => (value ? [dateTime(value)] : []))
.toSorted((a, b) => b - a)[0]
return {
updatedAt: latest === undefined ? null : new Date(latest).toISOString(),
models: [first, second],
models: entries,
}
},
)
export const getStatsModelComparisonData = (
firstProvider: string,
firstModel: string,
secondProvider: string,
secondModel: string,
) =>
getStatsModelsComparisonData([
{ provider: firstProvider, model: firstModel },
{ provider: secondProvider, model: secondModel },
])
function buildStatsHomeData(
modelRows: ModelStatMetric[],
providerRows: ProviderStatMetric[],
@ -501,6 +514,7 @@ function toComparisonEntry(data: StatsModelData | null): StatsModelComparisonEnt
tokenShare: data.tokenShare,
tokenChange: data.tokenChange,
totals: data.totals,
usage: data.usage,
}
}

View file

@ -14,7 +14,10 @@ import { normalizeCountry, normalizeTier, type StatBaseAggregate } from "./stat"
export type StatDimension = "model" | "provider" | "geo" | "geo_model"
export function buildStatsQuery(periodStart: Date, periodEnd: Date, dimension: StatDimension) {
// All stat dimensions and both grains are computed in one query via GROUPING SETS so
// the source table is scanned once per sync pass; separate queries per dimension (and
// the previous weekly/daily UNION ALL) each re-scanned the same events.
export function buildStatsQuery(periodStart: Date, periodEnd: Date) {
const periodStartValue = sqlString(periodStart.toISOString())
const periodEndValue = sqlString(periodEnd.toISOString())
const periodStartDateValue = sqlString(periodStart.toISOString().slice(0, 10))
@ -22,23 +25,6 @@ export function buildStatsQuery(periodStart: Date, periodEnd: Date, dimension: S
const sourceTable = [Resource.InferenceEvent.catalog, Resource.InferenceEvent.database, Resource.InferenceEvent.table]
.map(sqlIdentifier)
.join(".")
const dimensionSql = (() => {
if (dimension === "model")
return {
select: "provider, model, COALESCE(MAX(NULLIF(provider_model, '')), '') AS provider_model",
groupBy: "provider, model",
}
if (dimension === "provider") return { select: "provider", groupBy: "provider" }
if (dimension === "geo_model")
return {
select: "provider, model, country, COALESCE(MAX(NULLIF(continent, '')), '') AS continent",
groupBy: "provider, model, country",
}
return {
select: "'all' AS provider, 'all' AS model, country, COALESCE(MAX(NULLIF(continent, '')), '') AS continent",
groupBy: "country",
}
})()
const aggregateColumns = `
COUNT(DISTINCT session) AS sessions,
COUNT(*) AS requests,
@ -135,34 +121,41 @@ WITH normalized AS (
COALESCE(cost_total_microcents, cost_total * 1000000) AS cost_total_microcents
FROM normalized
WHERE lower(model) NOT IN (${[...EXCLUDED_MODELS].map(sqlString).join(", ")})
), weekly AS (
), periods AS (
SELECT
concat(CAST(year_of_week(event_time) AS varchar), '-W', lpad(CAST(week(event_time) AS varchar), 2, '0')) AS week_key,
substr(to_iso8601(date_trunc('day', event_time)), 1, 10) AS day_key,
*
FROM filtered
), daily AS (
SELECT substr(to_iso8601(date_trunc('day', event_time)), 1, 10) AS day_key, *
FROM filtered
)
SELECT
'week' AS grain,
week_key AS period_key,
CASE WHEN grouping(week_key) = 0 THEN 'week' ELSE 'day' END AS grain,
COALESCE(week_key, day_key) AS period_key,
${sqlString(Resource.StatsSyncConfig.dataset)} AS dataset,
CASE
WHEN grouping(country) = 0 AND grouping(model) = 0 THEN 'geo_model'
WHEN grouping(country) = 0 THEN 'geo'
WHEN grouping(model) = 0 THEN 'model'
ELSE 'provider'
END AS dimension,
tier,
${dimensionSql.select},
CASE WHEN grouping(provider) = 0 THEN provider ELSE 'all' END AS provider,
CASE WHEN grouping(model) = 0 THEN model WHEN grouping(country) = 0 THEN 'all' END AS model,
CASE WHEN grouping(model) = 0 AND grouping(country) = 1 THEN COALESCE(MAX(NULLIF(provider_model, '')), '') END AS provider_model,
CASE WHEN grouping(country) = 0 THEN country END AS country,
CASE WHEN grouping(country) = 0 THEN COALESCE(MAX(NULLIF(continent, '')), '') END AS continent,
${aggregateColumns}
FROM weekly
GROUP BY week_key, tier, ${dimensionSql.groupBy}
UNION ALL
SELECT
'day' AS grain,
day_key AS period_key,
${sqlString(Resource.StatsSyncConfig.dataset)} AS dataset,
tier,
${dimensionSql.select},
${aggregateColumns}
FROM daily
GROUP BY day_key, tier, ${dimensionSql.groupBy}
FROM periods
GROUP BY GROUPING SETS (
(week_key, tier, provider, model),
(week_key, tier, provider),
(week_key, tier, country),
(week_key, tier, provider, model, country),
(day_key, tier, provider, model),
(day_key, tier, provider),
(day_key, tier, country),
(day_key, tier, provider, model, country)
)
ORDER BY grain, period_key, total_tokens DESC
`
}

View file

@ -1,4 +1,4 @@
import { and, asc, eq, inArray, or } from "drizzle-orm"
import { and, asc, eq, inArray, max, or } from "drizzle-orm"
import { Effect, Layer } from "effect"
import * as Context from "effect/Context"
import { DatabaseError, DrizzleClient } from "../database"
@ -43,6 +43,7 @@ export type ModelStatMetric = {
export declare namespace ModelStatRepo {
export interface Service {
readonly listDaily: () => Effect.Effect<ModelStatMetric[], DatabaseError>
readonly lastSyncedAt: () => Effect.Effect<Date | null, DatabaseError>
readonly upsert: (rows: ModelStatRow[]) => Effect.Effect<void, DatabaseError>
readonly deleteRetiredDimensions: (rows: ModelStatRow[]) => Effect.Effect<void, DatabaseError>
}
@ -111,6 +112,14 @@ export class ModelStatRepo extends Context.Service<ModelStatRepo, ModelStatRepo.
})
})
const lastSyncedAt = Effect.fn("ModelStatRepo.lastSyncedAt")(function* () {
const result = yield* Effect.tryPromise({
try: () => db.select({ value: max(modelStat.updated_at) }).from(modelStat),
catch: (cause) => DatabaseError.make({ cause }),
})
return result[0]?.value ?? null
})
const upsert = Effect.fn("ModelStatRepo.upsert")(function* (rows: ModelStatRow[]) {
yield* Effect.forEach(
chunks(rows, UPSERT_CHUNK_SIZE),
@ -192,7 +201,7 @@ export class ModelStatRepo extends Context.Service<ModelStatRepo, ModelStatRepo.
})
})
return ModelStatRepo.of({ listDaily, upsert, deleteRetiredDimensions })
return ModelStatRepo.of({ listDaily, lastSyncedAt, upsert, deleteRetiredDimensions })
}),
)
}

View file

@ -12,85 +12,90 @@ const DATALAKE_INGESTION_LAG_MS = 5 * 60_000
const STATS_DATA_START_MS = new Date("2026-05-28T00:00:00.000Z").getTime()
const WEEK_MS = 7 * 86_400_000
const DISPLAY_WINDOW_MS = 56 * 86_400_000
// Anchor incremental passes to the ISO week containing this lookback, so the pass
// after a week boundary still recomputes the previous week's final aggregates even
// if the boundary pass itself failed.
const INCREMENTAL_LOOKBACK_MS = 2 * 3_600_000
export type SyncStatsResult = { ok: true; rows: number; startedAt: string; periodStart: string; periodEnd: string }
export type SyncStatsError = AthenaQueryError | AthenaQueryTimeoutError | DatabaseError
export const syncStats: () => Effect.Effect<
SyncStatsResult,
SyncStatsError,
Athena | ModelStatRepo | ProviderStatRepo | GeoStatRepo
> = Effect.fn("StatSync.sync")(function* () {
const startedAt = yield* DateTime.nowAsDate
const periodEnd = new Date(Math.floor((startedAt.getTime() - DATALAKE_INGESTION_LAG_MS) / 60_000) * 60_000)
// May 27 was partial, so keep Athena stats anchored at the first complete day.
const periodStart = new Date(
export const syncStats: (options?: {
full?: boolean
}) => Effect.Effect<SyncStatsResult, SyncStatsError, Athena | ModelStatRepo | ProviderStatRepo | GeoStatRepo> =
Effect.fn("StatSync.sync")(function* (options?: { full?: boolean }) {
const startedAt = yield* DateTime.nowAsDate
const periodEnd = new Date(Math.floor((startedAt.getTime() - DATALAKE_INGESTION_LAG_MS) / 60_000) * 60_000)
const periodStart = options?.full ? fullPeriodStart(periodEnd) : incrementalPeriodStart(periodEnd)
const athena = yield* Athena
const modelStats = yield* ModelStatRepo
const providerStats = yield* ProviderStatRepo
const geoStats = yield* GeoStatRepo
yield* logRuntimeCheck()
const rows = yield* athena.query(buildStatsQuery(periodStart, periodEnd))
const modelRows = modelRowsFromAggregates(rows.filter((row) => row.dimension === "model").flatMap(toModelAggregate))
const providerRows = providerRowsFromAggregates(
rows.filter((row) => row.dimension === "provider").flatMap(toProviderAggregate),
)
const geoRows = geoRowsFromAggregates(
rows.filter((row) => row.dimension === "geo" || row.dimension === "geo_model").flatMap(toGeoAggregate),
)
yield* Effect.all([modelStats.upsert(modelRows), providerStats.upsert(providerRows), geoStats.upsert(geoRows)], {
concurrency: "unbounded",
discard: true,
})
yield* Effect.all(
[
modelStats.deleteRetiredDimensions(modelRows),
providerStats.deleteRetiredDimensions(providerRows),
geoStats.deleteRetiredDimensions(geoRows),
],
{ concurrency: "unbounded", discard: true },
)
yield* Effect.logInfo(
`stats sync complete ${JSON.stringify({
startedAt: startedAt.toISOString(),
periodStart: periodStart.toISOString(),
periodEnd: periodEnd.toISOString(),
rows: modelRows.length,
providerRows: providerRows.length,
geoRows: geoRows.length,
stage: Resource.App.stage,
})}`,
)
return {
ok: true,
rows: modelRows.length,
startedAt: startedAt.toISOString(),
periodStart: periodStart.toISOString(),
periodEnd: periodEnd.toISOString(),
}
})
// May 27 was partial, so keep Athena stats anchored at the first complete day.
function fullPeriodStart(periodEnd: Date) {
return new Date(
Math.max(
Math.min(startOfIsoWeek(periodEnd).getTime() - WEEK_MS, periodEnd.getTime() - DISPLAY_WINDOW_MS),
STATS_DATA_START_MS,
),
)
const athena = yield* Athena
const modelStats = yield* ModelStatRepo
const providerStats = yield* ProviderStatRepo
const geoStats = yield* GeoStatRepo
}
yield* logRuntimeCheck()
const [modelAggregates, providerAggregates, geoAggregates, geoModelAggregates] = yield* Effect.all(
[
athena
.query(buildStatsQuery(periodStart, periodEnd, "model"))
.pipe(Effect.map((rows) => rows.flatMap(toModelAggregate))),
athena
.query(buildStatsQuery(periodStart, periodEnd, "provider"))
.pipe(Effect.map((rows) => rows.flatMap(toProviderAggregate))),
athena
.query(buildStatsQuery(periodStart, periodEnd, "geo"))
.pipe(Effect.map((rows) => rows.flatMap(toGeoAggregate))),
athena
.query(buildStatsQuery(periodStart, periodEnd, "geo_model"))
.pipe(Effect.map((rows) => rows.flatMap(toGeoAggregate))),
],
{ concurrency: "unbounded" },
// Events are append-only, so completed periods never change once synced; hourly
// passes only recompute the periods the current ISO week can still touch. The daily
// full pass refreshes the whole display window (normalization changes, retired
// dimension cleanup).
function incrementalPeriodStart(periodEnd: Date) {
return new Date(
Math.max(startOfIsoWeek(new Date(periodEnd.getTime() - INCREMENTAL_LOOKBACK_MS)).getTime(), STATS_DATA_START_MS),
)
const modelRows = modelRowsFromAggregates(modelAggregates)
const providerRows = providerRowsFromAggregates(providerAggregates)
const geoRows = geoRowsFromAggregates([...geoAggregates, ...geoModelAggregates])
yield* Effect.all([modelStats.upsert(modelRows), providerStats.upsert(providerRows), geoStats.upsert(geoRows)], {
concurrency: "unbounded",
discard: true,
})
yield* Effect.all(
[
modelStats.deleteRetiredDimensions(modelRows),
providerStats.deleteRetiredDimensions(providerRows),
geoStats.deleteRetiredDimensions(geoRows),
],
{ concurrency: "unbounded", discard: true },
)
yield* Effect.logInfo(
`stats sync complete ${JSON.stringify({
startedAt: startedAt.toISOString(),
periodStart: periodStart.toISOString(),
periodEnd: periodEnd.toISOString(),
rows: modelRows.length,
providerRows: providerRows.length,
geoRows: geoRows.length,
stage: Resource.App.stage,
})}`,
)
return {
ok: true,
rows: modelRows.length,
startedAt: startedAt.toISOString(),
periodStart: periodStart.toISOString(),
periodEnd: periodEnd.toISOString(),
}
})
}
function logRuntimeCheck() {
return Effect.logInfo(

View file

@ -1,21 +1,49 @@
import * as NodeRuntime from "@effect/platform-node/NodeRuntime"
import { Athena } from "@opencode-ai/stats-core/athena"
import { ModelStatRepo } from "@opencode-ai/stats-core/domain/model"
import { layer as statsLayer } from "@opencode-ai/stats-core/runtime"
import { syncStats } from "@opencode-ai/stats-core/stat-sync"
import { Cause, Effect, Layer, Schedule } from "effect"
import { Cause, Duration, Effect, Layer, Schedule } from "effect"
const SYNC_INTERVAL = "1 hour"
const SYNC_INTERVAL_MS = 3_600_000
const runtimeLayer = Layer.mergeAll(statsLayer, Athena.layer)
const syncPass = syncStats().pipe(
Effect.catchCause((cause) =>
Effect.logWarning(`stats sync failed ${JSON.stringify({ cause: Cause.pretty(cause) })}`),
),
)
const daemon = Effect.logInfo("stats sync daemon started").pipe(
Effect.andThen(syncPass.pipe(Effect.repeat(Schedule.fixed(SYNC_INTERVAL)))),
Effect.forkScoped,
)
const daemon = Effect.gen(function* () {
yield* Effect.logInfo("stats sync daemon started")
yield* initialDelay()
// One full pass per UTC day (including the first pass after boot) refreshes the
// whole display window; every other pass only recomputes the current ISO week.
let lastFullDay = ""
const pass = Effect.gen(function* () {
const today = new Date().toISOString().slice(0, 10)
const full = lastFullDay !== today
yield* syncStats({ full })
if (full) lastFullDay = today
}).pipe(
Effect.catchCause((cause) =>
Effect.logWarning(`stats sync failed ${JSON.stringify({ cause: Cause.pretty(cause) })}`),
),
)
yield* pass.pipe(Effect.repeat(Schedule.fixed(SYNC_INTERVAL)))
}).pipe(Effect.forkScoped)
// A restarted daemon must not immediately re-run the expensive Athena pass; resume
// the hourly cadence from the last completed sync instead. This caps the Athena
// spend of a crash loop at one pass per interval.
const initialDelay = Effect.fnUntraced(function* () {
const modelStats = yield* ModelStatRepo
const lastSynced = yield* modelStats.lastSyncedAt().pipe(Effect.catchCause(() => Effect.succeed(null)))
if (!lastSynced) return
const delayMs = Math.min(SYNC_INTERVAL_MS - (Date.now() - lastSynced.getTime()), SYNC_INTERVAL_MS)
if (delayMs <= 0) return
yield* Effect.logInfo(
`stats sync delaying first pass ${JSON.stringify({ lastSyncedAt: lastSynced.toISOString(), delayMs })}`,
)
yield* Effect.sleep(Duration.millis(delayMs))
})
NodeRuntime.runMain(Layer.launch(Layer.effectDiscard(daemon).pipe(Layer.provide(runtimeLayer))), {
disableErrorReporting: true,