feat(stats): add model comparison pages

This commit is contained in:
Adam 2026-06-24 10:43:56 -05:00
parent 77429f5982
commit 5a533e366f
No known key found for this signature in database
GPG key ID: 9CB48779AF150E75
10 changed files with 1373 additions and 5 deletions

View file

@ -33,6 +33,12 @@ import {
import { SectionHeading } from "../section-heading"
import { runStatsEffect } from "../../stats-runtime"
import { setStatsPageCacheHeaders } from "../stats-cache"
import {
ComparisonCardsSection,
modelRefFromCatalog,
uniqueComparisonPairs,
type ComparisonModelRef,
} from "../compare-cards"
import {
applyThemePreference,
Footer,
@ -189,6 +195,11 @@ export default function StatsModel() {
<ModelEfficiencySection data={stats() ?? null} catalog={catalogEntry() ?? null} />
<ModelGeoBreakdownSection data={stats()?.country ?? emptyCountryRecord()} />
<ModelPeersSection data={stats() ?? null} />
<ComparisonCardsSection
pairs={modelComparisonPairs(catalog(), catalogEntry() ?? null, stats() ?? null)}
title="Compare This Model"
description="Other models to compare with this one."
/>
</>
</Show>
</Show>
@ -197,6 +208,7 @@ export default function StatsModel() {
themePreference={themePreference()}
onThemePreferenceChange={updateThemePreference}
links={modelFooterLinks()}
bridge={{ href: "#model-comparison", label: "MODEL COMPARISONS" }}
/>
</div>
</main>
@ -1172,6 +1184,53 @@ function ModelEmptyState(props: { title: string; description: string; compact?:
)
}
function modelComparisonPairs(
catalog: ModelCatalog | undefined,
catalogEntry: ModelCatalogEntry | null,
data: StatsModelData | null,
) {
const current = modelComparisonRef(catalogEntry, data)
if (!current) return []
const peerPairs = (data?.peers ?? [])
.filter((peer) => peer.model !== data?.model)
.slice(0, 3)
.map((peer) => ({
first: current,
second: {
name: peer.model,
lab: peer.provider,
slug: peer.slug,
labName: peer.author,
metric: `#${peer.rank} / ${formatTokens(peer.tokens)}`,
},
detail: "Usage peer",
}))
const catalogPairs = (catalogEntry && catalog ? catalog.labs.find((lab) => lab.id === catalogEntry.lab)?.models ?? [] : [])
.filter((model) => model.id !== catalogEntry?.id)
.slice(0, 3)
.map((model) => ({
first: current,
second: modelRefFromCatalog(model),
detail: "Same lab pair",
}))
return uniqueComparisonPairs([...peerPairs, ...catalogPairs])
}
function modelComparisonRef(
catalogEntry: ModelCatalogEntry | null,
data: StatsModelData | null,
): ComparisonModelRef | undefined {
if (catalogEntry) return modelRefFromCatalog(catalogEntry)
if (!data) return undefined
return {
name: data.model,
lab: data.provider,
slug: data.slug,
labName: data.author,
metric: `#${data.rank}`,
}
}
function getProviderIconId(author: string) {
if (author === "MiniMax") return "minimax"
if (author === "Moonshot") return "moonshotai"

View file

@ -28,6 +28,11 @@ import {
import { SectionHeading } from "../section-heading"
import { runStatsEffect } from "../../stats-runtime"
import { setStatsPageCacheHeaders } from "../stats-cache"
import {
ComparisonCardsSection,
modelRefFromCatalog,
uniqueComparisonPairs,
} from "../compare-cards"
import {
applyThemePreference,
Footer,
@ -149,6 +154,11 @@ export default function StatsLab() {
labs={catalog()?.labs ?? []}
market={homeStats()?.market["2M"] ?? []}
/>
<ComparisonCardsSection
pairs={labComparisonPairs(data(), stats()?.models ?? [])}
title={`${data().name} Model Comparisons`}
description="Model pairs from this lab."
/>
</>
)}
</Show>
@ -158,6 +168,7 @@ export default function StatsLab() {
themePreference={themePreference()}
onThemePreferenceChange={updateThemePreference}
links={labFooterLinks()}
bridge={{ href: "#model-comparison", label: "MODEL COMPARISONS" }}
/>
</div>
</main>
@ -732,6 +743,31 @@ function LabEmptyState(props: { title: string; description: string }) {
)
}
function labComparisonPairs(lab: ModelCatalogLab, usage: LabUsageModelEntry[]) {
const usageRefs = usage.slice(0, 4).map((model) => ({
name: model.model,
lab: model.provider,
slug: model.slug,
labName: model.author,
metric: formatTokens(model.tokens),
}))
const refs = usageRefs.length > 1 ? usageRefs : lab.models.slice(0, 4).map(modelRefFromCatalog)
return uniqueComparisonPairs(
(
[
[0, 1, "Most-used lab pair"],
[0, 2, "Lab alternative"],
[1, 2, "Adjacent lab pair"],
[2, 3, "Same lab pair"],
] as const
).flatMap(([firstIndex, secondIndex, detail]) => {
const first = refs[firstIndex]
const second = refs[secondIndex]
return first && second ? [{ first, second, detail }] : []
}),
)
}
type RelatedLabEntry = { lab: ModelCatalogLab; share: number; tokens: number }
function relatedLabs(current: ModelCatalogLab, labs: ModelCatalogLab[], market: MarketDay[]): RelatedLabEntry[] {

View file

@ -0,0 +1,88 @@
import { For, Show } from "solid-js"
import { catalogSlug, formatCatalogLabName, type ModelCatalogEntry } from "./model-catalog"
export type ComparisonModelRef = {
name: string
lab: string
slug: string
labName?: string
metric?: string
}
export type ComparisonPair = {
first: ComparisonModelRef
second: ComparisonModelRef
detail: string
}
export function modelRefFromCatalog(entry: ModelCatalogEntry): ComparisonModelRef {
return {
name: entry.name,
lab: entry.lab,
slug: entry.slug,
labName: formatCatalogLabName(entry.lab),
}
}
export function comparisonHref(first: ComparisonModelRef, second: ComparisonModelRef) {
return `${import.meta.env.BASE_URL}compare/${catalogSlug(first.lab)}/${catalogSlug(first.slug)}/${catalogSlug(
second.lab,
)}/${catalogSlug(second.slug)}`
}
export function uniqueComparisonPairs(pairs: ComparisonPair[]) {
return pairs.reduce<{ keys: Set<string>; pairs: ComparisonPair[] }>(
(result, pair) => {
const key = [modelKey(pair.first), modelKey(pair.second)].toSorted().join("|")
if (result.keys.has(key) || modelKey(pair.first) === modelKey(pair.second)) return result
result.keys.add(key)
result.pairs.push(pair)
return result
},
{ keys: new Set(), pairs: [] },
).pairs
}
export function ComparisonCardsSection(props: {
pairs: ComparisonPair[]
title?: string
description?: string
compact?: boolean
}) {
return (
<Show when={props.pairs.length > 0}>
<section id="model-comparison" data-section="model-panel" data-variant={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>
)}
</For>
</div>
</section>
</Show>
)
}
function modelKey(model: ComparisonModelRef) {
return `${catalogSlug(model.lab)}/${catalogSlug(model.slug)}`
}

View file

@ -0,0 +1,83 @@
import { createEffect, createMemo, createSignal, For } from "solid-js"
import { comparisonHref, modelRefFromCatalog } from "./compare-cards"
import type { ModelCatalogEntry } from "./model-catalog"
export function ComparisonSelector(props: {
models: ModelCatalogEntry[]
firstId?: string
secondId?: string
label?: string
}) {
const [firstId, setFirstId] = createSignal(props.firstId ?? props.models[0]?.id ?? "")
const [secondId, setSecondId] = createSignal(differentModelId(props.models, firstId(), props.secondId))
const modelById = createMemo(() => new Map(props.models.map((model) => [model.id, model])))
const href = createMemo(() => {
const first = modelById().get(firstId())
const second = modelById().get(secondId())
if (!first || !second || first.id === second.id) return undefined
return comparisonHref(modelRefFromCatalog(first), modelRefFromCatalog(second))
})
createEffect(() => {
if (firstId() && modelById().has(firstId())) return
const next = props.firstId && modelById().has(props.firstId) ? props.firstId : props.models[0]?.id
if (next) setFirstId(next)
})
createEffect(() => {
if (secondId() && secondId() !== firstId() && modelById().has(secondId())) return
setSecondId(differentModelId(props.models, firstId(), props.secondId))
})
return (
<form
data-component="comparison-selector"
aria-label={props.label ?? "Model comparison selector"}
onSubmit={(event) => {
event.preventDefault()
const url = href()
if (!url || typeof window === "undefined") return
window.location.href = url
}}
>
<label>
<span>First model</span>
<select value={firstId()} onInput={(event) => setFirstId(event.currentTarget.value)} required>
<option value="" disabled>
Select model
</option>
<For each={props.models}>
{(model) => (
<option value={model.id}>
{model.name} ({model.lab})
</option>
)}
</For>
</select>
</label>
<label>
<span>Second model</span>
<select value={secondId()} onInput={(event) => setSecondId(event.currentTarget.value)} required>
<option value="" disabled>
Select model
</option>
<For each={props.models}>
{(model) => (
<option value={model.id} disabled={model.id === firstId()}>
{model.name} ({model.lab})
</option>
)}
</For>
</select>
</label>
<button type="submit" disabled={!href()}>
Compare models
</button>
</form>
)
}
function differentModelId(models: ModelCatalogEntry[], firstId: string, preferredId: string | undefined) {
if (preferredId && preferredId !== firstId && models.some((model) => model.id === preferredId)) return preferredId
return models.find((model) => model.id !== firstId)?.id ?? ""
}

View file

@ -0,0 +1,608 @@
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")
}

View file

@ -0,0 +1,148 @@
import "../index.css"
import { Link, Meta, Title } from "@solidjs/meta"
import { createAsync } from "@solidjs/router"
import { createMemo, createSignal, onMount, Show } from "solid-js"
import { getRequestEvent } from "solid-js/web"
import { ComparisonCardsSection, modelRefFromCatalog, uniqueComparisonPairs } from "../compare-cards"
import { ComparisonSelector } from "../compare-selector"
import { getModelCatalog, 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 compareTitle = "AI Model Comparison"
const compareDescription =
"Compare AI models used in OpenCode by context, output, release date, usage, rank, token share, and cost."
const compareHeaderLinks: readonly HeaderLink[] = [
{ href: "#compare-tool", label: "Compare" },
{ href: "#model-comparison", label: "Popular" },
]
const compareFooterLinks: readonly HeaderLink[] = [
{ href: import.meta.env.BASE_URL, label: "Data Home" },
{ href: `${import.meta.env.BASE_URL}#top-models`, label: "Top Models" },
{ href: `${import.meta.env.BASE_URL}#token-cost`, label: "Token Cost" },
{ href: `${import.meta.env.BASE_URL}compare`, label: "Model Compare" },
]
export default function ModelCompareIndex() {
const event = getRequestEvent()
event?.response.headers.set("Cache-Control", "public, max-age=60, s-maxage=300, stale-while-revalidate=86400")
const catalog = createAsync(() => getModelCatalog())
const githubStars = createAsync(() => getGitHubStars())
const [themePreference, setThemePreference] = createSignal<ThemePreference>("system")
const compareUrl = createMemo(() =>
new URL(
`${import.meta.env.BASE_URL}compare`,
event?.request.url ?? (typeof window === "undefined" ? compareFallbackUrl : window.location.href),
).toString(),
)
const featuredModels = createMemo(() => (catalog()?.models ?? []).slice(0, 120))
const pairs = createMemo(() => buildPopularPairs(featuredModels()))
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>{compareTitle}</Title>
<Meta name="description" content={compareDescription} />
<Link rel="canonical" href={compareUrl()} />
<Meta property="og:type" content="website" />
<Meta property="og:site_name" content="OpenCode" />
<Meta property="og:title" content={compareTitle} />
<Meta property="og:description" content={compareDescription} />
<Meta property="og:url" content={compareUrl()} />
<Meta name="twitter:card" content="summary" />
<Meta name="twitter:title" content={compareTitle} />
<Meta name="twitter:description" content={compareDescription} />
<Header githubStars={githubStars() ?? "150K"} links={compareHeaderLinks} brandHref={import.meta.env.BASE_URL} />
<div data-component="container">
<div data-component="content">
<section id="compare-tool" data-section="model-hero">
<a data-slot="model-back-link" href={import.meta.env.BASE_URL}>
Data
</a>
<div data-slot="model-hero-grid">
<div data-slot="model-hero-copy">
<span data-slot="model-id-tag">/compare</span>
<h1>Model Comparison</h1>
<p>Choose two models and compare usage, cost, limits, and features.</p>
</div>
<div data-component="model-rank-panel">
<span>Model Pairs</span>
<strong>{formatPairCount(featuredModels().length)}</strong>
<p>Available pairings from the model list.</p>
</div>
</div>
<div data-slot="model-hero-pattern" aria-hidden="true" />
<Show
when={featuredModels().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={featuredModels()} />
</Show>
</section>
<ComparisonCardsSection
pairs={pairs()}
title="Popular Model Comparisons"
description="Common model pairs to start with."
/>
</div>
<Footer
themePreference={themePreference()}
onThemePreferenceChange={updateThemePreference}
links={compareFooterLinks}
bridge={{ href: "#compare-tool", label: "MODEL COMPARE" }}
/>
</div>
</main>
)
}
function buildPopularPairs(models: ModelCatalogEntry[]) {
return uniqueComparisonPairs(
(
[
[0, 1, "Popular pair"],
[0, 2, "Top alternative"],
[1, 2, "Nearby pair"],
[2, 3, "Recent model pair"],
[3, 4, "Model pair"],
[4, 5, "Model pair"],
] as const
).flatMap(([firstIndex, secondIndex, detail]) => {
const first = models[firstIndex]
const second = models[secondIndex]
return first && second ? [{ first: modelRefFromCatalog(first), second: modelRefFromCatalog(second), detail }] : []
}),
)
}
function formatPairCount(count: number) {
if (count < 2) return "0"
return new Intl.NumberFormat("en", { notation: "compact", maximumFractionDigits: 1 }).format((count * (count - 1)) / 2)
}

View file

@ -5252,6 +5252,243 @@
color: var(--stats-text);
}
[data-page="stats"] [data-section="model-panel"][data-variant="compact"] {
padding-top: 56px;
padding-bottom: 56px;
}
[data-page="stats"] [data-component="comparison-card-grid"] {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
}
[data-page="stats"] [data-component="comparison-card"] {
display: grid;
align-content: space-between;
gap: 18px;
min-height: 168px;
min-width: 0;
padding: 16px;
border: 1px solid var(--stats-line);
background: var(--stats-layer);
color: var(--stats-text);
text-decoration: none;
}
[data-page="stats"] [data-component="comparison-card"]:hover,
[data-page="stats"] [data-component="comparison-card"]:focus-visible {
border-color: var(--stats-text);
outline: none;
background: var(--stats-bg);
text-decoration: none;
}
[data-page="stats"] [data-component="comparison-card"] span,
[data-page="stats"] [data-component="comparison-card"] p,
[data-page="stats"] [data-component="comparison-card"] small {
color: var(--stats-muted);
font-size: 11px;
font-weight: 500;
line-height: 1.35;
}
[data-page="stats"] [data-component="comparison-card"] strong {
min-width: 0;
color: var(--stats-text);
font-size: 20px;
font-weight: 500;
line-height: 1.08;
overflow-wrap: anywhere;
}
[data-page="stats"] [data-component="comparison-card"] em {
color: var(--stats-faint);
font: inherit;
}
[data-page="stats"] [data-component="comparison-card"] p {
display: flex;
align-items: center;
gap: 8px;
margin: 0;
}
[data-page="stats"] [data-component="comparison-card"] b {
min-width: 0;
overflow: hidden;
font: inherit;
text-overflow: ellipsis;
white-space: nowrap;
}
[data-page="stats"] [data-component="comparison-card"] i {
flex: 0 0 auto;
width: 16px;
height: 1px;
background: var(--stats-line-strong);
}
[data-page="stats"] [data-component="comparison-selector"] {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
gap: 8px;
align-items: end;
max-width: 920px;
}
[data-page="stats"] [data-component="comparison-selector"] label {
display: grid;
gap: 8px;
min-width: 0;
}
[data-page="stats"] [data-component="comparison-selector"] label span {
color: var(--stats-muted);
font-size: 11px;
font-weight: 500;
line-height: 1.2;
}
[data-page="stats"] [data-component="comparison-selector"] select,
[data-page="stats"] [data-component="comparison-selector"] button {
box-sizing: border-box;
height: 40px;
min-width: 0;
border: 1px solid var(--stats-line);
border-radius: 0;
appearance: none;
font: inherit;
font-size: 13px;
font-weight: 500;
line-height: 1;
}
[data-page="stats"] [data-component="comparison-selector"] select {
width: 100%;
padding: 0 36px 0 12px;
background:
linear-gradient(45deg, transparent 50%, var(--stats-muted) 50%) calc(100% - 18px) 17px / 5px 5px no-repeat,
linear-gradient(135deg, var(--stats-muted) 50%, transparent 50%) calc(100% - 13px) 17px / 5px 5px no-repeat,
var(--stats-bg);
color: var(--stats-text);
}
[data-page="stats"] [data-component="comparison-selector"] button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 14px;
background: var(--stats-text);
color: var(--stats-bg);
cursor: pointer;
text-decoration: none;
white-space: nowrap;
}
[data-page="stats"] [data-component="comparison-selector"] button:disabled {
cursor: not-allowed;
opacity: 0.45;
}
[data-page="stats"] [data-component="comparison-selector"] select:focus-visible,
[data-page="stats"] [data-component="comparison-selector"] button:focus-visible {
outline: 2px solid var(--stats-accent);
outline-offset: 2px;
}
[data-page="stats"] [data-component="comparison-table-wrap"] {
overflow-x: auto;
border: 1px solid var(--stats-line);
background: var(--stats-layer);
}
[data-page="stats"] [data-component="comparison-table"] {
width: 100%;
min-width: 720px;
border-collapse: collapse;
color: var(--stats-text);
font-size: 13px;
line-height: 1.35;
}
[data-page="stats"] [data-component="comparison-table"] caption {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
[data-page="stats"] [data-component="comparison-table"] th,
[data-page="stats"] [data-component="comparison-table"] td {
min-width: 0;
padding: 14px 16px;
border-bottom: 1px solid var(--stats-line);
border-left: 1px solid var(--stats-line);
text-align: left;
vertical-align: top;
}
[data-page="stats"] [data-component="comparison-table"] th:first-child {
border-left: 0;
}
[data-page="stats"] [data-component="comparison-table"] thead th {
color: var(--stats-muted);
font-size: 11px;
font-weight: 600;
line-height: 1.2;
text-transform: uppercase;
}
[data-page="stats"] [data-component="comparison-table"] tbody tr:last-child th,
[data-page="stats"] [data-component="comparison-table"] tbody tr:last-child td {
border-bottom: 0;
}
[data-page="stats"] [data-component="comparison-table"] tbody th {
width: 34%;
background: var(--stats-bg);
}
[data-page="stats"] [data-component="comparison-table"] tbody th strong,
[data-page="stats"] [data-component="comparison-table"] td strong {
display: block;
color: var(--stats-text);
font-size: 13px;
font-weight: 600;
line-height: 1.25;
overflow-wrap: anywhere;
}
[data-page="stats"] [data-component="comparison-table"] tbody th span,
[data-page="stats"] [data-component="comparison-table"] td span {
display: block;
margin-top: 5px;
color: var(--stats-muted);
font-size: 11px;
font-weight: 500;
line-height: 1.35;
}
[data-page="stats"] [data-component="comparison-table"] td[data-best="true"] {
position: relative;
background: var(--stats-bg);
box-shadow: inset 0 0 0 1px var(--stats-accent);
}
[data-page="stats"] [data-component="comparison-table"] td[data-best="true"]::before {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 3px;
content: "";
background: var(--stats-accent);
}
[data-page="stats"][data-theme="dark"],
:root[data-stats-theme="dark"] [data-page="stats"]:not([data-theme="light"]) {
color-scheme: dark;
@ -5784,10 +6021,19 @@
[data-page="stats"] [data-component="model-metric-grid"],
[data-page="stats"] [data-component="model-metric-grid"][data-variant="dense"],
[data-page="stats"] [data-component="model-efficiency-grid"],
[data-page="stats"] [data-component="lab-model-grid"] {
[data-page="stats"] [data-component="lab-model-grid"],
[data-page="stats"] [data-component="comparison-card-grid"] {
grid-template-columns: 1fr 1fr;
}
[data-page="stats"] [data-component="comparison-selector"] {
grid-template-columns: 1fr;
}
[data-page="stats"] [data-component="comparison-selector"] button {
width: 100%;
}
[data-page="stats"] [data-component="model-peer-list"] a {
grid-template-columns: 32px 32px minmax(0, 1fr) minmax(64px, auto);
gap: 12px;
@ -6212,10 +6458,15 @@
[data-page="stats"] [data-component="model-metric-grid"],
[data-page="stats"] [data-component="model-metric-grid"][data-variant="dense"],
[data-page="stats"] [data-component="model-efficiency-grid"],
[data-page="stats"] [data-component="lab-model-grid"] {
[data-page="stats"] [data-component="lab-model-grid"],
[data-page="stats"] [data-component="comparison-card-grid"] {
grid-template-columns: 1fr;
}
[data-page="stats"] [data-component="comparison-table"] {
min-width: 640px;
}
[data-page="stats"] [data-component="model-metric"] {
min-height: 112px;
}

View file

@ -34,6 +34,7 @@ import { localizedUrl } from "../lib/language"
import { findModelCatalogEntry, getModelCatalog, type ModelCatalog } from "./model-catalog"
import { SectionHeading } from "./section-heading"
import { setStatsPageCacheHeaders } from "./stats-cache"
import { ComparisonCardsSection, uniqueComparisonPairs, type ComparisonModelRef } from "./compare-cards"
import {
applyThemePreference,
Footer,
@ -48,6 +49,12 @@ import {
const products = ["All Users", "Zen", "Go"] as const
const tokenProducts = ["Zen", "Go"] as const
const ranges = ["1D", "1W", "2W", "1M", "2M"] as const
const comparisonPairIndexes = [
[0, 1, "Top two by recent usage"],
[0, 2, "Leader vs challenger"],
[1, 2, "Adjacent leaderboard pair"],
[2, 3, "Top model alternative"],
] as const
const statsUnfurlPath = "banner.jpg"
const usageColors = [
"#ed6aff",
@ -183,11 +190,20 @@ export default function StatsHome() {
<CacheRatioSection data={stats().cacheRatio} />
<MarketShareSection data={stats().market} />
<GeoBreakdownSection data={stats().country} />
<ComparisonCardsSection
pairs={homeComparisonPairs(stats().leaderboard["All Users"]["2M"])}
title="Model Comparisons"
description="Popular model pairs from the leaderboard."
/>
</>
)}
</Show>
</div>
<Footer themePreference={themePreference()} onThemePreferenceChange={updateThemePreference} />
<Footer
themePreference={themePreference()}
onThemePreferenceChange={updateThemePreference}
bridge={{ href: "#model-comparison", label: "MODEL COMPARISONS" }}
/>
</div>
</main>
)
@ -1969,6 +1985,26 @@ function formatSessionCost(value: number) {
return `$${value.toFixed(4)}`
}
function homeComparisonPairs(leaderboard: LeaderboardEntry[]) {
return uniqueComparisonPairs(
comparisonPairIndexes.flatMap(([firstIndex, secondIndex, detail]) => {
const first = leaderboard[firstIndex]
const second = leaderboard[secondIndex]
return first && second ? [{ first: leaderboardRef(first), second: leaderboardRef(second), detail }] : []
}),
)
}
function leaderboardRef(entry: LeaderboardEntry): ComparisonModelRef {
return {
name: entry.model,
lab: entry.provider,
slug: modelSlug(entry.model),
labName: entry.author,
metric: `#${entry.rank} / ${formatBillions(entry.tokens)}`,
}
}
function modelSlug(value: string) {
return value
.trim()

View file

@ -216,6 +216,7 @@ export function Footer(props: {
themePreference: ThemePreference
onThemePreferenceChange: (preference: ThemePreference) => void
links?: readonly HeaderLink[]
bridge?: HeaderLink | null
}) {
const i18n = useI18n()
const language = useLanguage()
@ -241,10 +242,12 @@ export function Footer(props: {
{ href: githubLink.href, label: i18n.t("header.github") },
{ href: "https://www.youtube.com/@anomaly-co", label: i18n.t("footer.youtube") },
]
const bridge = () =>
props.bridge === undefined ? { href: "#geo-breakdown", label: i18n.t("nav.geoBreakdown").toUpperCase() } : props.bridge
return (
<footer data-component="footer">
<SectionBridge label={i18n.t("nav.geoBreakdown").toUpperCase()} href="#geo-breakdown" />
<Show when={bridge()}>{(link) => <SectionBridge label={link().label} href={link().href} />}</Show>
<div data-slot="footer-grid">
<a data-slot="footer-mark" href="https://opencode.ai" aria-label={i18n.t("footer.homeAria")}>
<OpenCodeMark />

View file

@ -1,8 +1,9 @@
import { Client } from "@planetscale/database"
import { Effect } from "effect"
import { Resource } from "sst/resource"
import { DatabaseError } from "../database"
import type { GeoStatMetric } from "./geo"
import type { ModelStatMetric } from "./model"
import { ModelStatRepo, type ModelStatMetric } from "./model"
import type { ProviderStatMetric } from "./provider"
export type UsageProduct = "All Users" | "Zen" | "Go" | "Enterprise"
@ -81,6 +82,23 @@ export type StatsLabData = {
usage: ModelUsagePoint[]
models: LabUsageModelEntry[]
}
export type StatsModelComparisonEntry = {
updatedAt: string | null
model: string
slug: string
provider: string
author: string
rank: number | null
previousRank: number | null
totalModels: number
tokenShare: number
tokenChange: number
totals: StatsModelData["totals"]
}
export type StatsModelComparisonData = {
updatedAt: string | null
models: [StatsModelComparisonEntry | null, StatsModelComparisonEntry | null]
}
export type StatsHomeData = {
updatedAt: string | null
usage: Record<UsageProduct, Record<UsageRange, UsagePoint[]>>
@ -271,6 +289,27 @@ 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) {
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]
.flatMap((value) => (value ? [dateTime(value)] : []))
.toSorted((a, b) => b - a)[0]
return {
updatedAt: latest === undefined ? null : new Date(latest).toISOString(),
models: [first, second],
}
},
)
function buildStatsHomeData(
modelRows: ModelStatMetric[],
providerRows: ProviderStatMetric[],
@ -448,6 +487,23 @@ function buildStatsLabData(providerParam: string, modelRows: ModelStatMetric[]):
}
}
function toComparisonEntry(data: StatsModelData | null): StatsModelComparisonEntry | null {
if (!data) return null
return {
updatedAt: data.updatedAt,
model: data.model,
slug: data.slug,
provider: data.provider,
author: data.author,
rank: data.rank,
previousRank: data.previousRank,
totalModels: data.totalModels,
tokenShare: data.tokenShare,
tokenChange: data.tokenChange,
totals: data.totals,
}
}
function emptyStatsHomeData(): StatsHomeData {
return {
updatedAt: null,