chore: merge dev into v2 (#35962)

Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: James Long <longster@gmail.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Dustin Deus <deusdustin@gmail.com>
Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box>
Co-authored-by: Frank <frank@anoma.ly>
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Co-authored-by: Dax <mail@thdxr.com>
Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com>
Co-authored-by: Jay <53023+jayair@users.noreply.github.com>
Co-authored-by: Brendan Allan <git@brendonovich.dev>
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
Co-authored-by: runvip <164729189+runvip@users.noreply.github.com>
Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>
Co-authored-by: Jack <jack@anoma.ly>
Co-authored-by: opencode <opencode@sst.dev>
Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com>
Co-authored-by: Julian Coy <julian@ex-machina.co>
Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com>
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
Co-authored-by: Kit Langton <kit.langton@gmail.com>
Co-authored-by: Simon Klee <hello@simonklee.dk>
Co-authored-by: Jay <air@live.ca>
Co-authored-by: David Hill <1879069+iamdavidhill@users.noreply.github.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>
This commit is contained in:
opencode-agent[bot] 2026-07-08 17:58:55 -05:00 committed by GitHub
commit 687dbba6a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
150 changed files with 8987 additions and 772 deletions

View file

@ -210,25 +210,30 @@ const en = {
"model.totalModels": "{{count}} models",
"model.momentum": "Momentum",
"model.vsPreviousWindow": "vs previous window",
"model.usageDescription": "Daily OpenCode Go token volume over the recent two-month window.",
"model.usageDescription": "Daily token volume.",
"model.noUsageTitle": "No usage",
"model.noUsageDescription": "No usage landed in the current window.",
"model.dailyTokenChart": "Daily token usage chart",
"model.usersDescription": "Daily unique OpenCode Go users over the recent two-month window.",
"model.usersDescription": "Daily unique users.",
"model.noUsersTitle": "No user data",
"model.noUsersDescription": "No user-bearing rows landed in the current window.",
"model.dailyUserChart": "Daily unique user chart",
"model.efficiencyDescription": "Cost, cache behavior, and average OpenCode Go session shape.",
"model.efficiencyDescription": "Cost, cache behavior, and average session shape.",
"model.noEfficiencyTitle": "No efficiency data",
"model.noEfficiencyDescription": "Efficiency data appears after usage lands.",
"model.totalSpendLabel": "Total spend",
"model.cost": "Cost",
"model.totalSpend": "total spend",
"model.costPerMillion": "Cost / 1M",
"model.costInput": "Cost input",
"model.costOutput": "Cost output",
"model.inputOutput": "input / output",
"model.observedTokens": "observed all tokens",
"model.costSession": "Cost / Session",
"model.averageCostSession": "Average cost / Session",
"model.average": "average",
"model.tokensSession": "Tokens / Session",
"model.averageTokensSession": "Average tokens / session",
"model.cacheRatio": "Cache Ratio",
"model.inputTokens": "input tokens",
"model.geoDescription": "OpenCode Go model tokens used by country.",

View file

@ -202,6 +202,11 @@ export const dict = {
"model.efficiencyDescription": "التكلفة، وسلوك التخزين المؤقت، ومتوسط شكل جلسة OpenCode Go.",
"model.noEfficiencyTitle": "لا توجد بيانات كفاءة",
"model.noEfficiencyDescription": "تظهر بيانات الكفاءة بعد وصول الاستخدام.",
"model.totalSpendLabel": "Total spend",
"model.costInput": "Cost input",
"model.costOutput": "Cost output",
"model.averageCostSession": "Average cost / Session",
"model.averageTokensSession": "Average tokens / session",
"model.cost": "التكلفة",
"model.totalSpend": "إجمالي الإنفاق",
"model.costPerMillion": "التكلفة / 1M",

View file

@ -204,6 +204,11 @@ export const dict = {
"model.efficiencyDescription": "Custo, comportamento de cache e formato médio da sessão do OpenCode Go.",
"model.noEfficiencyTitle": "Sem dados de eficiência",
"model.noEfficiencyDescription": "Os dados de eficiência aparecem depois que o uso chega.",
"model.totalSpendLabel": "Total spend",
"model.costInput": "Cost input",
"model.costOutput": "Cost output",
"model.averageCostSession": "Average cost / Session",
"model.averageTokensSession": "Average tokens / session",
"model.cost": "Custo",
"model.totalSpend": "gasto total",
"model.costPerMillion": "Custo / 1M",

View file

@ -203,6 +203,11 @@ export const dict = {
"model.efficiencyDescription": "Omkostning, cacheadfærd og gennemsnitlig OpenCode Go-sessionsform.",
"model.noEfficiencyTitle": "Ingen effektivitetsdata",
"model.noEfficiencyDescription": "Effektivitetsdata vises, når brug lander.",
"model.totalSpendLabel": "Total spend",
"model.costInput": "Cost input",
"model.costOutput": "Cost output",
"model.averageCostSession": "Average cost / Session",
"model.averageTokensSession": "Average tokens / session",
"model.cost": "Omkostning",
"model.totalSpend": "samlet forbrug",
"model.costPerMillion": "Omkostning / 1 mio.",

View file

@ -204,6 +204,11 @@ export const dict = {
"model.efficiencyDescription": "Kosten, Cache-Verhalten und durchschnittliche OpenCode Go-Sitzungsform.",
"model.noEfficiencyTitle": "Keine Effizienzdaten",
"model.noEfficiencyDescription": "Effizienzdaten erscheinen, nachdem Nutzung eingegangen ist.",
"model.totalSpendLabel": "Total spend",
"model.costInput": "Cost input",
"model.costOutput": "Cost output",
"model.averageCostSession": "Average cost / Session",
"model.averageTokensSession": "Average tokens / session",
"model.cost": "Kosten",
"model.totalSpend": "Gesamtausgaben",
"model.costPerMillion": "Kosten / 1 Mio.",

View file

@ -203,6 +203,11 @@ export const dict = {
"model.efficiencyDescription": "Coste, comportamiento de caché y forma media de sesión de OpenCode Go.",
"model.noEfficiencyTitle": "Sin datos de eficiencia",
"model.noEfficiencyDescription": "Los datos de eficiencia aparecen después de que llegue uso.",
"model.totalSpendLabel": "Total spend",
"model.costInput": "Cost input",
"model.costOutput": "Cost output",
"model.averageCostSession": "Average cost / Session",
"model.averageTokensSession": "Average tokens / session",
"model.cost": "Coste",
"model.totalSpend": "gasto total",
"model.costPerMillion": "Coste / 1 M",

View file

@ -205,6 +205,11 @@ export const dict = {
"model.efficiencyDescription": "Coût, comportement du cache et forme moyenne des sessions OpenCode Go.",
"model.noEfficiencyTitle": "Aucune donnée d'efficacité",
"model.noEfficiencyDescription": "Les données d'efficacité apparaissent après l'arrivée de l'utilisation.",
"model.totalSpendLabel": "Total spend",
"model.costInput": "Cost input",
"model.costOutput": "Cost output",
"model.averageCostSession": "Average cost / Session",
"model.averageTokensSession": "Average tokens / session",
"model.cost": "Coût",
"model.totalSpend": "dépense totale",
"model.costPerMillion": "Coût / 1 M",

View file

@ -204,6 +204,11 @@ export const dict = {
"model.efficiencyDescription": "Costi, comportamento della cache e forma media della sessione OpenCode Go.",
"model.noEfficiencyTitle": "Nessun dato di efficienza",
"model.noEfficiencyDescription": "I dati di efficienza appaiono dopo l'arrivo dell'utilizzo.",
"model.totalSpendLabel": "Total spend",
"model.costInput": "Cost input",
"model.costOutput": "Cost output",
"model.averageCostSession": "Average cost / Session",
"model.averageTokensSession": "Average tokens / session",
"model.cost": "Costo",
"model.totalSpend": "spesa totale",
"model.costPerMillion": "Costo / 1 M",

View file

@ -205,6 +205,11 @@ export const dict = {
"model.efficiencyDescription": "コスト、キャッシュ動作、平均的なOpenCode Goセッションの形。",
"model.noEfficiencyTitle": "効率データがありません",
"model.noEfficiencyDescription": "使用量が届くと効率データが表示されます。",
"model.totalSpendLabel": "Total spend",
"model.costInput": "Cost input",
"model.costOutput": "Cost output",
"model.averageCostSession": "Average cost / Session",
"model.averageTokensSession": "Average tokens / session",
"model.cost": "コスト",
"model.totalSpend": "総支出",
"model.costPerMillion": "100万あたりのコスト",

View file

@ -205,6 +205,11 @@ export const dict = {
"model.efficiencyDescription": "비용, 캐시 동작, 평균 OpenCode Go 세션 형태입니다.",
"model.noEfficiencyTitle": "효율 데이터 없음",
"model.noEfficiencyDescription": "사용량이 들어오면 효율 데이터가 표시됩니다.",
"model.totalSpendLabel": "Total spend",
"model.costInput": "Cost input",
"model.costOutput": "Cost output",
"model.averageCostSession": "Average cost / Session",
"model.averageTokensSession": "Average tokens / session",
"model.cost": "비용",
"model.totalSpend": "총 지출",
"model.costPerMillion": "100만당 비용",

View file

@ -203,6 +203,11 @@ export const dict = {
"model.efficiencyDescription": "Kostnad, cacheatferd og gjennomsnittlig OpenCode Go-øktform.",
"model.noEfficiencyTitle": "Ingen effektivitetsdata",
"model.noEfficiencyDescription": "Effektivitetsdata vises etter at bruk lander.",
"model.totalSpendLabel": "Total spend",
"model.costInput": "Cost input",
"model.costOutput": "Cost output",
"model.averageCostSession": "Average cost / Session",
"model.averageTokensSession": "Average tokens / session",
"model.cost": "Kostnad",
"model.totalSpend": "totalt forbruk",
"model.costPerMillion": "Kostnad / 1 mill.",

View file

@ -202,6 +202,11 @@ export const dict = {
"model.efficiencyDescription": "Koszt, zachowanie cache i średni kształt sesji OpenCode Go.",
"model.noEfficiencyTitle": "Brak danych wydajności",
"model.noEfficiencyDescription": "Dane wydajności pojawią się po nadejściu użycia.",
"model.totalSpendLabel": "Total spend",
"model.costInput": "Cost input",
"model.costOutput": "Cost output",
"model.averageCostSession": "Average cost / Session",
"model.averageTokensSession": "Average tokens / session",
"model.cost": "Koszt",
"model.totalSpend": "łączne wydatki",
"model.costPerMillion": "Koszt / 1 mln",

View file

@ -204,6 +204,11 @@ export const dict = {
"model.efficiencyDescription": "Стоимость, поведение кэша и средняя форма сеанса OpenCode Go.",
"model.noEfficiencyTitle": "Нет данных эффективности",
"model.noEfficiencyDescription": "Данные эффективности появятся после использования.",
"model.totalSpendLabel": "Total spend",
"model.costInput": "Cost input",
"model.costOutput": "Cost output",
"model.averageCostSession": "Average cost / Session",
"model.averageTokensSession": "Average tokens / session",
"model.cost": "Стоимость",
"model.totalSpend": "общие расходы",
"model.costPerMillion": "Стоимость / 1 млн",

View file

@ -204,6 +204,11 @@ export const dict = {
"model.efficiencyDescription": "ต้นทุน พฤติกรรมแคช และรูปแบบเซสชัน OpenCode Go โดยเฉลี่ย",
"model.noEfficiencyTitle": "ไม่มีข้อมูลประสิทธิภาพ",
"model.noEfficiencyDescription": "ข้อมูลประสิทธิภาพจะแสดงหลังจากมีการใช้งานเข้ามา",
"model.totalSpendLabel": "Total spend",
"model.costInput": "Cost input",
"model.costOutput": "Cost output",
"model.averageCostSession": "Average cost / Session",
"model.averageTokensSession": "Average tokens / session",
"model.cost": "ต้นทุน",
"model.totalSpend": "ยอดใช้จ่ายรวม",
"model.costPerMillion": "ต้นทุน / 1 ล้าน",

View file

@ -204,6 +204,11 @@ export const dict = {
"model.efficiencyDescription": "Maliyet, önbellek davranışı ve ortalama OpenCode Go oturum yapısı.",
"model.noEfficiencyTitle": "Verimlilik verisi yok",
"model.noEfficiencyDescription": "Verimlilik verileri kullanım geldikten sonra görünür.",
"model.totalSpendLabel": "Total spend",
"model.costInput": "Cost input",
"model.costOutput": "Cost output",
"model.averageCostSession": "Average cost / Session",
"model.averageTokensSession": "Average tokens / session",
"model.cost": "Maliyet",
"model.totalSpend": "toplam harcama",
"model.costPerMillion": "Maliyet / 1M",

View file

@ -204,6 +204,11 @@ export const dict = {
"model.efficiencyDescription": "Вартість, поведінка кешу та середня форма сеансу OpenCode Go.",
"model.noEfficiencyTitle": "Немає даних ефективності",
"model.noEfficiencyDescription": "Дані ефективності з'являться після використання.",
"model.totalSpendLabel": "Total spend",
"model.costInput": "Cost input",
"model.costOutput": "Cost output",
"model.averageCostSession": "Average cost / Session",
"model.averageTokensSession": "Average tokens / session",
"model.cost": "Вартість",
"model.totalSpend": "загальні витрати",
"model.costPerMillion": "Вартість / 1 млн",

View file

@ -203,6 +203,11 @@ export const dict = {
"model.efficiencyDescription": "成本、缓存行为和平均 OpenCode Go 会话形态。",
"model.noEfficiencyTitle": "无效率数据",
"model.noEfficiencyDescription": "使用量到达后会显示效率数据。",
"model.totalSpendLabel": "Total spend",
"model.costInput": "Cost input",
"model.costOutput": "Cost output",
"model.averageCostSession": "Average cost / Session",
"model.averageTokensSession": "Average tokens / session",
"model.cost": "成本",
"model.totalSpend": "总支出",
"model.costPerMillion": "每 100 万成本",

View file

@ -203,6 +203,11 @@ export const dict = {
"model.efficiencyDescription": "成本、快取行為和平均 OpenCode Go 工作階段形態。",
"model.noEfficiencyTitle": "無效率數據",
"model.noEfficiencyDescription": "使用量到達後會顯示效率數據。",
"model.totalSpendLabel": "Total spend",
"model.costInput": "Cost input",
"model.costOutput": "Cost output",
"model.averageCostSession": "Average cost / Session",
"model.averageTokensSession": "Average tokens / session",
"model.cost": "成本",
"model.totalSpend": "總支出",
"model.costPerMillion": "每 100 萬成本",

View file

@ -15,7 +15,7 @@ import {
type UsageRange,
} from "@opencode-ai/stats-core/domain/home"
import { createAsync, query, useParams } from "@solidjs/router"
import { createMemo, createSignal, For, onMount, Show, type JSX } from "solid-js"
import { createMemo, createSignal, createUniqueId, For, onMount, Show, type JSX } from "solid-js"
import { getRequestEvent } from "solid-js/web"
import type { FeatureCollection, GeometryObject, GeoJsonProperties } from "geojson"
import type { GeometryCollection, Topology } from "topojson-specification"
@ -28,12 +28,17 @@ import {
formatCatalogLabName,
getModelCatalog,
type ModelCatalog,
type ModelCatalogCost,
type ModelCatalogEntry,
} from "../model-catalog"
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,
@ -121,6 +126,8 @@ export default function StatsModel() {
const modelHeaderLinks = createMemo<readonly HeaderLink[]>(() => [
{ href: "#overview", label: i18n.t("nav.overview") },
{ href: "#momentum", label: i18n.t("model.momentum") },
{ href: "#usage", label: i18n.t("nav.usage") },
{ href: "#unique-users", label: i18n.t("model.uniqueUsers") },
{ href: "#efficiency", label: i18n.t("nav.efficiency") },
{ href: "#geo-breakdown", label: i18n.t("nav.geoBreakdown") },
{ href: "#peers", label: i18n.t("nav.peers") },
@ -183,9 +190,16 @@ export default function StatsModel() {
/>
<ModelOverview catalog={catalogEntry() ?? null} />
<ModelMomentumSection data={stats() ?? null} />
<ModelUsageSection data={stats() ?? null} />
<ModelUniqueUsersSection data={stats() ?? null} />
<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>
@ -194,6 +208,7 @@ export default function StatsModel() {
themePreference={themePreference()}
onThemePreferenceChange={updateThemePreference}
links={modelFooterLinks()}
bridge={{ href: "#model-comparison", label: "MODEL COMPARISONS" }}
/>
</div>
</main>
@ -561,6 +576,303 @@ function MomentumMetric(props: { label: string; value: string; watermark?: strin
)
}
function ModelUsageSection(props: { data: StatsModelData | null }) {
const i18n = useI18n()
return (
<ModelTrendSection
data={props.data}
id="usage"
title={i18n.t("nav.usage")}
description={i18n.t("model.usageDescription")}
ariaLabel={i18n.t("model.dailyTokenChart")}
emptyTitle={i18n.t("model.noUsageTitle")}
emptyDescription={i18n.t("model.noUsageDescription")}
value={(point) => point.tokens}
formatValue={formatTokens}
valueUnit={i18n.t("lab.tokens")}
rowLabel={i18n.t("lab.dailyTokens")}
lineTone="muted"
/>
)
}
function ModelUniqueUsersSection(props: { data: StatsModelData | null }) {
const i18n = useI18n()
return (
<ModelTrendSection
data={props.data}
id="unique-users"
title={i18n.t("model.uniqueUsers")}
description={i18n.t("model.usersDescription")}
ariaLabel={i18n.t("model.dailyUserChart")}
emptyTitle={i18n.t("model.noUsersTitle")}
emptyDescription={i18n.t("model.noUsersDescription")}
value={(point) => point.users}
formatValue={formatUsers}
valueUnit={i18n.t("format.users")}
rowLabel={i18n.t("model.uniqueUsers")}
lineTone="active"
activeLineBaseTone="muted"
highlightBars={false}
area
/>
)
}
function ModelTrendSection(props: {
data: StatsModelData | null
id: string
title: string
description: string
ariaLabel: string
emptyTitle: string
emptyDescription: string
value: (point: ModelUsagePoint) => number
formatValue: (value: number) => string
valueUnit: string
rowLabel: string
lineTone: "muted" | "active"
activeLineBaseTone?: "muted" | "active"
highlightBars?: boolean
area?: boolean
}) {
const activeLineClipId = createUniqueId()
const activeLineMaskId = createUniqueId()
const areaGradientId = createUniqueId()
const [activeIndex, setActiveIndex] = createSignal<number>()
const highlightBars = createMemo(() => props.highlightBars ?? true)
const lineScale = 326 / 450
const areaBottom = 100 / lineScale
const usage = createMemo(() => props.data?.usage ?? [])
const valueMax = createMemo(() => Math.max(0, ...usage().map((item) => props.value(item))) || 1)
const linePoints = createMemo(() =>
usage().map((point, index) => ({
point,
x: modelUsagePointX(index, usage().length),
y: modelUsageLineY(props.value(point), valueMax()),
})),
)
const linePath = createMemo(() => modelUsageLinePath(linePoints()))
const areaPath = createMemo(() => modelUsageAreaPath(linePoints(), areaBottom))
const activeLineBreak = createMemo(() => {
const index = activeIndex()
if (index === undefined || linePoints().length < 2) return undefined
return modelUsageColumnBounds(index, linePoints().length)
})
const activeLineClip = createMemo(() => {
const index = activeIndex()
if (index === undefined || linePoints().length < 2) return undefined
return modelUsageColumnInnerBounds(index, linePoints().length)
})
const activeTooltip = createMemo(() => {
const index = activeIndex()
const point = index === undefined ? undefined : usage()[index]
if (index === undefined || !point) return undefined
const bounds = modelUsageColumnBounds(index, usage().length)
return {
bounds,
index,
point,
tooltipY: (linePoints()[index]?.y ?? 100) * lineScale,
}
})
const monthTicks = createMemo(() => modelUsageMonthTicks(usage(), props.data?.updatedAt ?? null))
return (
<section id={props.id} data-section="model-panel">
<SectionTitle href={`#${props.id}`} title={props.title} description={props.description} />
<Show
when={usage().some((item) => props.value(item) > 0)}
fallback={<ModelEmptyState title={props.emptyTitle} description={props.emptyDescription} />}
>
<div
data-component="model-usage-chart"
data-variant="model-trend"
data-highlight-bars={highlightBars() ? "true" : undefined}
role="img"
aria-label={props.ariaLabel}
style={{ "--model-usage-count": usage().length } as JSX.CSSProperties}
onPointerLeave={(event) => {
if (event.pointerType === "touch") return
setActiveIndex(undefined)
}}
>
<div data-slot="model-trend-plot">
<Show when={linePath()}>
{(path) => (
<>
<Show when={props.area && areaPath()}>
{(area) => (
<svg
data-slot="model-trend-area-layer"
viewBox={`0 0 100 ${formatUsagePathNumber(areaBottom)}`}
preserveAspectRatio="none"
aria-hidden="true"
>
<defs>
<linearGradient id={areaGradientId} x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="var(--model-trend-active)" stop-opacity="0.1" />
<stop offset="66%" stop-color="var(--model-trend-active)" stop-opacity="0.045" />
<stop offset="100%" stop-color="var(--model-trend-active)" stop-opacity="0" />
</linearGradient>
</defs>
<path data-slot="model-trend-area" d={area()} fill={`url(#${areaGradientId})`} />
</svg>
)}
</Show>
<svg
data-slot="model-trend-line"
data-layer="base"
viewBox="0 0 100 100"
preserveAspectRatio="none"
aria-hidden="true"
>
<Show
when={activeLineBreak()}
fallback={<path data-slot="model-trend-line-base" data-tone={props.lineTone} d={path()} />}
>
{(lineBreak) => (
<>
<defs>
<mask id={activeLineMaskId} maskUnits="userSpaceOnUse">
<rect x="0" y="-2" width="100" height="104" fill="white" />
<rect x={lineBreak().x} y="-2" width={lineBreak().width} height="104" fill="black" />
</mask>
</defs>
<path
data-slot="model-trend-line-base"
data-tone={props.activeLineBaseTone ?? props.lineTone}
d={path()}
mask={`url(#${activeLineMaskId})`}
/>
</>
)}
</Show>
</svg>
<Show when={activeLineClip()}>
{(clip) => (
<svg
data-slot="model-trend-line"
data-layer="active"
viewBox="0 0 100 100"
preserveAspectRatio="none"
aria-hidden="true"
>
<defs>
<clipPath id={activeLineClipId} clipPathUnits="userSpaceOnUse">
<rect x={clip().x} y="-2" width={clip().width} height="104" />
</clipPath>
</defs>
<path data-slot="model-trend-line-active" d={path()} clip-path={`url(#${activeLineClipId})`} />
</svg>
)}
</Show>
</>
)}
</Show>
<Show when={linePoints().at(-1)}>
{(point) => (
<span
data-slot="model-trend-end-marker"
data-tone={props.lineTone}
aria-hidden="true"
style={
{
"--model-trend-end-x": `${point().x}%`,
"--model-trend-end-top": `${point().y * lineScale}%`,
} as JSX.CSSProperties
}
/>
)}
</Show>
<div data-slot="model-trend-bars">
<For each={usage()}>
{(point, index) => (
<div
data-slot="model-trend-column"
role="button"
tabIndex={0}
aria-label={`${point.date} ${props.formatValue(props.value(point))} ${props.valueUnit}`}
data-active={activeIndex() === index() ? "true" : undefined}
data-muted={
highlightBars() && activeIndex() !== undefined && activeIndex() !== index() ? "true" : undefined
}
style={
{
"--model-trend-token-height": modelUsageStripHeight(props.value(point), valueMax()),
} as JSX.CSSProperties
}
onPointerDown={(event) => {
if (event.pointerType !== "touch") return
setActiveIndex(index())
}}
onPointerEnter={() => setActiveIndex(index())}
onPointerMove={(event) => {
if (event.pointerType === "touch") return
setActiveIndex(index())
}}
onClick={() => setActiveIndex(index())}
onFocus={() => setActiveIndex(index())}
onBlur={() => setActiveIndex(undefined)}
onKeyDown={(event) => {
if (event.key !== "Enter" && event.key !== " ") return
event.preventDefault()
setActiveIndex(index())
}}
>
<div data-slot="model-trend-token-band">
<div data-slot="model-trend-token-bar" />
</div>
</div>
)}
</For>
</div>
<Show when={activeTooltip()} keyed>
{(active) => (
<div
data-component="chart-tooltip"
data-placement={active.index > usage().length * 0.62 ? "left" : "right"}
style={
{
"--model-trend-tooltip-left": `${active.bounds.x}%`,
"--model-trend-tooltip-right": `${active.bounds.x + active.bounds.width}%`,
"--model-trend-tooltip-y": `${active.tooltipY}`,
} as JSX.CSSProperties
}
>
<strong>{formatModelUsageTooltipDate(active.point.date, props.data?.updatedAt ?? null)}</strong>
<span>
{props.formatValue(props.value(active.point))} {props.valueUnit}
</span>
<div data-slot="tooltip-divider" />
<p>
<span data-slot="tooltip-label">
<i data-kind={props.lineTone === "active" ? "users" : "tokens"} /> {props.rowLabel}
</span>
<b>{props.formatValue(props.value(active.point))}</b>
</p>
</div>
)}
</Show>
</div>
<div data-slot="model-trend-months" aria-hidden="true">
<For each={monthTicks()}>
{(tick) => (
<span
data-align={tick.align}
style={{ "--model-trend-month-left": `${tick.left}%` } as JSX.CSSProperties}
>
{tick.label}
</span>
)}
</For>
</div>
</div>
</Show>
</section>
)
}
function ModelEfficiencySection(props: { data: StatsModelData | null; catalog: ModelCatalogEntry | null }) {
const i18n = useI18n()
return (
@ -580,35 +892,32 @@ function ModelEfficiencySection(props: { data: StatsModelData | null; catalog: M
}
>
{(data) => (
<div data-component="model-metric-grid" data-variant="dense">
<MetricCard
label={i18n.t("model.cost")}
value={formatMoney(data().totals.cost)}
detail={i18n.t("model.totalSpend")}
/>
<MetricCard
label={i18n.t("model.costPerMillion")}
value={
props.catalog?.cost ? formatCatalogPrice(props.catalog.cost) : formatMoney(data().totals.costPerMillion)
}
detail={props.catalog?.cost ? i18n.t("model.inputOutput") : i18n.t("model.observedTokens")}
/>
<MetricCard
label={i18n.t("model.costSession")}
value={formatSessionCost(data().totals.costPerSession)}
detail={i18n.t("model.average")}
/>
<MetricCard
label={i18n.t("model.tokensSession")}
value={formatTokens(data().totals.tokensPerSession)}
detail={i18n.t("model.average")}
/>
<MetricCard
label={i18n.t("model.cacheRatio")}
value={formatPercent(data().totals.cacheRatio)}
detail={i18n.t("model.inputTokens")}
/>
</div>
<>
<div data-slot="model-efficiency-pattern" aria-hidden="true" />
<div data-component="model-efficiency-grid">
<MetricCard label={i18n.t("model.totalSpendLabel")} value={formatMoney(data().totals.cost)} />
<MetricCard
label={i18n.t("model.costInput")}
value={formatCatalogUnitPrice(props.catalog?.cost?.input)}
/>
<MetricCard
label={i18n.t("model.costOutput")}
value={formatCatalogUnitPrice(props.catalog?.cost?.output)}
/>
<MetricCard
label={i18n.t("model.averageCostSession")}
value={formatSessionCost(data().totals.costPerSession)}
/>
<MetricCard
label={i18n.t("model.averageTokensSession")}
value={formatTokens(data().totals.tokensPerSession)}
/>
<MetricCard
label={i18n.t("model.cacheRatio")}
value={`${formatPercent(data().totals.cacheRatio)} ${i18n.t("model.inputTokens")}`}
/>
</div>
</>
)}
</Show>
</section>
@ -828,12 +1137,12 @@ function ModelPeersSection(props: { data: StatsModelData | null }) {
)
}
function MetricCard(props: { label: string; value: string; detail: string; state?: "positive" | "negative" }) {
function MetricCard(props: { label: string; value: string; detail?: string; state?: "positive" | "negative" }) {
return (
<article data-component="model-metric" data-state={props.state}>
<span>{props.label}</span>
<strong>{props.value}</strong>
<p>{props.detail}</p>
<Show when={props.detail}>{(detail) => <p>{detail()}</p>}</Show>
</article>
)
}
@ -846,10 +1155,16 @@ function PeerRow(props: { peer: ModelPeerEntry; active: boolean }) {
href={language.route(`${import.meta.env.BASE_URL}${providerSlug(props.peer.provider)}/${props.peer.slug}`)}
data-active={props.active ? "true" : undefined}
>
<span>{String(props.peer.rank).padStart(2, "0")}</span>
<ProviderIcon aria-hidden="true" id={getProviderIconId(props.peer.author)} />
<strong>{props.peer.model}</strong>
<em>{props.peer.author}</em>
<span data-slot="model-peer-rank" aria-label={props.active ? `Rank ${props.peer.rank}` : undefined}>
<Show when={!props.active}>{String(props.peer.rank).padStart(2, "0")}</Show>
</span>
<span data-slot="model-peer-avatar">
<ProviderIcon aria-hidden="true" id={getProviderIconId(props.peer.author)} />
</span>
<span data-slot="model-peer-copy">
<strong>{props.peer.model}</strong>
<em>{props.peer.author}</em>
</span>
<b>{formatTokens(props.peer.tokens)}</b>
</a>
</li>
@ -869,6 +1184,55 @@ 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"
@ -1014,6 +1378,101 @@ function formatMomentumDateLabel(date: string) {
return `${shortMonths[parsed.getUTCMonth()]} ${parsed.getUTCDate()}`
}
function modelUsageStripHeight(value: number, max: number) {
if (value <= 0 || max <= 0) return "0px"
return `max(2px, ${(value / max) * 100}%)`
}
function modelUsageLineY(value: number, max: number) {
if (value <= 0 || max <= 0) return 100
return Math.max(0, 100 - (value / max) * 100)
}
function modelUsagePointX(index: number, count: number) {
if (count <= 1) return 50
return ((index + 0.5) / count) * 100
}
function modelUsageColumnBounds(index: number, count: number) {
if (count <= 0) return { x: 0, width: 100 }
return { x: (index / count) * 100, width: 100 / count }
}
function modelUsageColumnInnerBounds(index: number, count: number) {
const bounds = modelUsageColumnBounds(index, count)
const inset = Math.min(bounds.width * 0.1, 0.24)
return { x: bounds.x + inset, width: Math.max(0.001, bounds.width - inset * 2) }
}
type ModelUsageLinePoint = { x: number; y: number }
function modelUsageLinePath(points: ModelUsageLinePoint[]) {
if (points.length === 0) return ""
if (points.length === 1) return `M ${formatUsagePathNumber(points[0].x)} ${formatUsagePathNumber(points[0].y)}`
return points.slice(0, -1).reduce(
(path, point, index) => {
const next = points[index + 1]
const previous = points[index - 1] ?? point
const afterNext = points[index + 2] ?? next
const controlA = {
x: point.x + (next.x - previous.x) / 6,
y: clampUsagePercent(point.y + (next.y - previous.y) / 6),
}
const controlB = {
x: next.x - (afterNext.x - point.x) / 6,
y: clampUsagePercent(next.y - (afterNext.y - point.y) / 6),
}
return `${path} C ${formatUsagePathNumber(controlA.x)} ${formatUsagePathNumber(controlA.y)}, ${formatUsagePathNumber(controlB.x)} ${formatUsagePathNumber(controlB.y)}, ${formatUsagePathNumber(next.x)} ${formatUsagePathNumber(next.y)}`
},
`M ${formatUsagePathNumber(points[0].x)} ${formatUsagePathNumber(points[0].y)}`,
)
}
function modelUsageAreaPath(points: ModelUsageLinePoint[], bottom: number) {
const path = modelUsageLinePath(points)
if (!path || points.length === 0) return ""
const first = points[0]
const last = points[points.length - 1]
return `${path} L ${formatUsagePathNumber(last.x)} ${formatUsagePathNumber(bottom)} L ${formatUsagePathNumber(first.x)} ${formatUsagePathNumber(bottom)} Z`
}
function formatUsagePathNumber(value: number) {
return Number(value.toFixed(3))
}
function clampUsagePercent(value: number) {
return Math.max(0, Math.min(100, value))
}
function modelUsageMonthTicks(points: ModelUsagePoint[], updatedAt: string | null) {
const seen = new Set<string>()
return points.flatMap((point, index) => {
const parsed = parseMomentumDate(point.date, updatedAt)
const label = shortMonths[parsed.getUTCMonth()]
if (!label || seen.has(label)) return []
seen.add(label)
return [
{
label,
left: modelUsagePointX(index, points.length),
align: index === 0 ? "start" : index >= points.length - 2 ? "end" : "center",
},
]
})
}
function formatModelUsageTooltipDate(value: string, updatedAt: string | null) {
const date = parseMomentumDate(value, updatedAt)
if (date.getUTCFullYear() === 1970) return value
return new Intl.DateTimeFormat("en", {
month: "short",
day: "numeric",
year: "numeric",
timeZone: "UTC",
}).format(date)
}
function formatRankLabel(rank: number | null) {
if (rank === null) return "--"
return `#${String(rank).padStart(2, "0")}`
@ -1121,8 +1580,9 @@ function formatMoney(value: number) {
return `$${value.toFixed(value >= 10 ? 0 : 2)}`
}
function formatCatalogPrice(value: ModelCatalogCost) {
return `${formatModelPrice(value.input)} / ${formatModelPrice(value.output)}`
function formatCatalogUnitPrice(value: number | undefined) {
if (value === undefined) return "-"
return `${formatModelPrice(value)} / 1M`
}
function formatModelPrice(value: number) {

View file

@ -28,6 +28,7 @@ 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 +150,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 +164,7 @@ export default function StatsLab() {
themePreference={themePreference()}
onThemePreferenceChange={updateThemePreference}
links={labFooterLinks()}
bridge={{ href: "#model-comparison", label: "MODEL COMPARISONS" }}
/>
</div>
</main>
@ -732,6 +739,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,611 @@
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,642 @@
import "../index.css"
import { Meta, Title } from "@solidjs/meta"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { createAsync } from "@solidjs/router"
import { createEffect, createMemo, createSignal, For, onMount, Show } from "solid-js"
import { getRequestEvent } from "solid-js/web"
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 { formatCatalogLabName, getModelCatalog, type ModelCatalogEntry } from "../model-catalog"
import { setStatsPageCacheHeaders } from "../stats-cache"
import {
applyThemePreference,
Footer,
getGitHubStars,
Header,
isThemePreference,
themeStorageKey,
type HeaderLink,
type ThemePreference,
} from "../stats-shell"
const comparePath = "/data/compare"
const compareTitle = "AI Model Comparison"
const compareDescription =
"Compare AI models on key metrics including benchmarks, price, context length, and other model features."
const statsUnfurlPath = "banner.png"
const uptimeBars = Array.from({ length: 10 }, (_, index) => index)
const heroLabs = [
{ lab: "deepseek", label: "DeepSeek" },
{ lab: "openai", label: "OpenAI" },
{ lab: "anthropic", label: "Anthropic" },
] as const
const categoryTemplates = [
{
title: "Flagship models",
description: "The latest top-tier model from major labs.",
kind: "flagship",
},
{
title: "Most affordable",
description: "Compact, lower-cost picks for high-throughput use.",
kind: "affordable",
},
{
title: "Best for code",
description: "Frequently chosen for programming tasks.",
kind: "code",
},
{
title: "Image generation",
description: "Compare top image models on cost and quality.",
kind: "image",
},
] as const
type CompareCategory = {
title: string
description: string
first: ComparisonModelRef
second: ComparisonModelRef
avatars: ComparisonModelRef[]
}
type CompareSlot = "first" | "second"
export default function ModelCompareIndex() {
const i18n = useI18n()
const language = useLanguage()
const event = getRequestEvent()
setStatsPageCacheHeaders(event?.response.headers)
const catalog = createAsync(() => getModelCatalog())
const githubStars = createAsync(() => getGitHubStars())
const [themePreference, setThemePreference] = createSignal<ThemePreference>("system")
const compareUrl = createMemo(() => localizedUrl(language.locale(), comparePath))
const statsUnfurlUrl = new URL(statsUnfurlPath, localizedUrl("en", "/data/")).toString()
const featuredModels = createMemo(() => (catalog()?.models ?? []).slice(0, 120))
const categories = createMemo(() => buildComparisonCategories(featuredModels()))
const compareHeaderLinks = createMemo<readonly HeaderLink[]>(() => [
{ href: `${import.meta.env.BASE_URL}#top-models`, label: i18n.t("nav.topModels") },
{ href: `${import.meta.env.BASE_URL}#leaderboard`, label: i18n.t("nav.leaderboard") },
{ href: `${import.meta.env.BASE_URL}#market-share`, label: i18n.t("nav.marketShare") },
{ href: `${import.meta.env.BASE_URL}#token-cost`, label: i18n.t("nav.tokenCost") },
{ href: `${import.meta.env.BASE_URL}#session-cost`, label: i18n.t("nav.sessionCost") },
])
const compareFooterLinks = createMemo<readonly HeaderLink[]>(() => [
{ href: `${import.meta.env.BASE_URL}#top-models`, label: i18n.t("nav.topModels") },
{ href: `${import.meta.env.BASE_URL}#leaderboard`, label: i18n.t("nav.leaderboard") },
{ href: `${import.meta.env.BASE_URL}#market-share`, label: i18n.t("nav.marketShare") },
{ href: `${import.meta.env.BASE_URL}#token-cost`, label: i18n.t("nav.tokenCost") },
{ href: `${import.meta.env.BASE_URL}#session-cost`, label: i18n.t("nav.sessionCost") },
])
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} />
<LocaleLinks path={comparePath} />
<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 property="og:image" content={statsUnfurlUrl} />
<Meta property="og:image:type" content="image/png" />
<Meta property="og:image:width" content="1200" />
<Meta property="og:image:height" content="630" />
<Meta property="og:image:alt" content={i18n.t("app.unfurlAlt")} />
<Meta name="twitter:card" content="summary_large_image" />
<Meta name="twitter:title" content={compareTitle} />
<Meta name="twitter:description" content={compareDescription} />
<Meta name="twitter:image" content={statsUnfurlUrl} />
<Meta name="twitter:image:alt" content={i18n.t("app.unfurlAlt")} />
<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="compare-home-hero">
<nav data-component="compare-home-breadcrumb" aria-label="Breadcrumb">
<a data-slot="compare-home-crumb" href={language.route(import.meta.env.BASE_URL)}>
Data
</a>
<span data-slot="compare-home-separator">/</span>
<span data-slot="compare-home-crumb" data-current="true">
Compare
</span>
</nav>
<div data-slot="compare-home-hero-grid">
<h1 aria-label="Compare AI models">
<span>Compare</span>
<HeroModelStack />
<span>AI models</span>
</h1>
<p>{compareDescription}</p>
</div>
<div data-slot="compare-home-pattern" aria-hidden="true" />
</section>
<section data-section="compare-home-selector" aria-label="Choose models to compare">
<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>
}
>
<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>
</div>
<Footer
themePreference={themePreference()}
onThemePreferenceChange={updateThemePreference}
links={compareFooterLinks()}
bridge={null}
/>
</div>
</main>
)
}
function CompareHomeSelector(props: { models: ModelCatalogEntry[] }) {
const [firstId, setFirstId] = createSignal("")
const [secondId, setSecondId] = createSignal("")
const [activeSlot, setActiveSlot] = createSignal<CompareSlot>()
const modelById = createMemo(() => new Map(props.models.map((model) => [model.id, model])))
const first = createMemo(() => modelById().get(firstId()))
const second = createMemo(() => modelById().get(secondId()))
const activeSelected = createMemo(() => (activeSlot() === "first" ? first() : second()))
const activeBlockedId = createMemo(() => (activeSlot() === "first" ? secondId() : firstId()))
const href = createMemo(() => {
const firstModel = first()
const secondModel = second()
if (!firstModel || !secondModel || firstModel.id === secondModel.id) return undefined
return comparisonHref(modelRefFromCatalog(firstModel), modelRefFromCatalog(secondModel))
})
createEffect(() => {
if (!secondId() || secondId() !== firstId()) return
setSecondId("")
})
createEffect(() => {
const url = href()
if (!url || typeof window === "undefined") return
window.location.href = url
})
return (
<form
data-component="compare-home-selector"
aria-label="Model comparison selector"
onSubmit={(event) => event.preventDefault()}
>
<CompareHomeSelect
selected={first()}
label="First model"
expanded={activeSlot() === "first"}
onOpen={() => setActiveSlot("first")}
/>
<CompareHomeSelect
selected={second()}
label="Second model"
expanded={activeSlot() === "second"}
onOpen={() => setActiveSlot("second")}
/>
<Show when={activeSlot()}>
{(slot) => (
<CompareModelSelectModal
models={props.models}
selected={activeSelected()}
blockedId={activeBlockedId()}
label={slot() === "first" ? "Choose first model" : "Choose second model"}
onClose={() => setActiveSlot(undefined)}
onSelect={(model) => {
if (slot() === "first") setFirstId(model.id)
if (slot() === "second") setSecondId(model.id)
setActiveSlot(undefined)
}}
/>
)}
</Show>
</form>
)
}
function CompareHomeSelect(props: {
selected: ModelCatalogEntry | undefined
label: string
expanded: boolean
onOpen: () => void
}) {
return (
<button
type="button"
data-slot="compare-home-select-panel"
data-selected={props.selected ? "true" : undefined}
aria-label={props.label}
aria-haspopup="dialog"
aria-expanded={props.expanded}
onClick={props.onOpen}
>
<span data-slot="compare-home-select-copy">
<span data-slot="compare-home-plus" aria-hidden="true">
+
</span>
<Show when={props.selected}>{(model) => <ModelAvatar model={model()} size="tiny" />}</Show>
<span data-slot="compare-home-select-name">{props.selected?.name ?? "Select a model"}</span>
</span>
</button>
)
}
function CompareModelSelectModal(props: {
models: ModelCatalogEntry[]
selected: ModelCatalogEntry | undefined
blockedId: string
label: string
onClose: () => void
onSelect: (model: ModelCatalogEntry) => void
}) {
let searchInput: HTMLInputElement | undefined
const [search, setSearch] = createSignal("")
const [previewId, setPreviewId] = createSignal(props.selected?.id ?? "")
const availableModels = createMemo(() => props.models.filter((model) => model.id !== props.blockedId))
const filteredModels = createMemo(() => {
const terms = search().trim().toLowerCase().split(/\s+/).filter(Boolean)
if (terms.length === 0) return availableModels()
return availableModels().filter((model) => terms.every((term) => modelSearchText(model).includes(term)))
})
const preview = createMemo(
() => filteredModels().find((model) => model.id === previewId()) ?? filteredModels()[0] ?? availableModels()[0],
)
createEffect(() => {
const selected = props.selected
const fallback = selected && selected.id !== props.blockedId ? selected.id : filteredModels()[0]?.id
if (!fallback) return
if (availableModels().some((model) => model.id === previewId())) return
setPreviewId(fallback)
})
createEffect(() => {
const models = filteredModels()
if (models.some((model) => model.id === previewId())) return
setPreviewId(models[0]?.id ?? "")
})
createEffect(() => {
if (typeof window === "undefined") return
window.requestAnimationFrame(() => searchInput?.focus())
})
return (
<div
data-component="compare-model-modal-scrim"
role="presentation"
onClick={props.onClose}
onKeyDown={(event) => {
if (event.key !== "Escape") return
props.onClose()
}}
>
<div
data-component="compare-model-modal"
role="dialog"
aria-modal="true"
aria-label={props.label}
onClick={(event) => event.stopPropagation()}
>
<div data-slot="compare-model-modal-list">
<label data-slot="compare-model-modal-search">
<span data-slot="compare-model-modal-search-icon" aria-hidden="true" />
<input
ref={searchInput}
value={search()}
placeholder="Search models"
aria-label="Search models"
onInput={(event) => setSearch(event.currentTarget.value)}
/>
</label>
<div data-slot="compare-model-modal-results">
<Show
when={filteredModels().length > 0}
fallback={
<div data-slot="compare-model-modal-empty">
<strong>No models found</strong>
<span>Try another search.</span>
</div>
}
>
<For each={filteredModels()}>
{(model) => (
<button
type="button"
data-slot="compare-model-modal-row"
data-active={preview()?.id === model.id ? "true" : undefined}
onPointerEnter={() => setPreviewId(model.id)}
onFocus={() => setPreviewId(model.id)}
onClick={() => props.onSelect(model)}
>
<span data-slot="compare-model-modal-row-main">
<ModelAvatar model={model} size="tiny" />
<span>{model.name}</span>
</span>
<Show when={isFreeModel(model)}>
<span data-slot="compare-model-modal-badge">Free</span>
</Show>
</button>
)}
</For>
</Show>
</div>
</div>
<div data-slot="compare-model-modal-divider" aria-hidden="true" />
<Show when={preview()}>{(model) => <CompareModelDetail model={model()} />}</Show>
</div>
</div>
)
}
function CompareModelDetail(props: { model: ModelCatalogEntry }) {
return (
<aside data-slot="compare-model-modal-detail">
<header data-slot="compare-model-modal-detail-header">
<ModelAvatar model={props.model} size="small" />
<strong>{props.model.name}</strong>
</header>
<div data-slot="compare-model-modal-description">
<p>
{props.model.description ??
`${props.model.name} is an AI model from ${formatCatalogLabName(props.model.lab)}.`}
</p>
<span aria-hidden="true" />
</div>
<dl data-slot="compare-model-modal-facts">
<CompareModelFact label="Release" value={formatCatalogDate(props.model.releaseDate)} />
<CompareModelFact label="Context" value={formatCatalogLimit(props.model.limit?.context)} />
<CompareModelFact label="Input" value={formatCatalogUnitPrice(props.model.cost?.input)} />
<CompareModelFact label="Output" value={formatCatalogUnitPrice(props.model.cost?.output)} />
<div data-slot="compare-model-modal-fact">
<dt>Uptime</dt>
<dd data-slot="compare-model-modal-uptime">
<For each={uptimeBars}>{(bar) => <span data-active={bar < 7 ? "true" : undefined} />}</For>
</dd>
</div>
<div data-slot="compare-model-modal-fact">
<dt>URL</dt>
<dd>
<a href={modelHref(props.model)}>{formatModelUrl(props.model)}</a>
</dd>
</div>
</dl>
</aside>
)
}
function CompareModelFact(props: { label: string; value: string }) {
return (
<div data-slot="compare-model-modal-fact">
<dt>{props.label}</dt>
<dd>{props.value}</dd>
</div>
)
}
function HeroModelStack() {
return (
<span data-slot="compare-home-avatar-stack" aria-hidden="true">
<For each={heroLabs}>
{(lab) => (
<span data-slot="compare-home-avatar-frame">
<LabLogo lab={lab.lab} label={lab.label} size="large" />
</span>
)}
</For>
</span>
)
}
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} />
}
function LabLogo(props: { lab: string; label: string; size: "large" | "small" | "tiny" }) {
const iconId = () => getProviderIconId(props.lab)
return (
<span data-slot="compare-home-avatar" data-lab={iconId()} data-size={props.size} aria-label={props.label}>
<ProviderIcon aria-hidden="true" id={iconId()} />
</span>
)
}
function buildComparisonCategories(models: ModelCatalogEntry[]): CompareCategory[] {
return categoryTemplates.reduce<{ keys: Set<string>; categories: CompareCategory[] }>(
(result, template, index) => {
const candidates = categoryCandidates(template.kind, models)
const pair = categoryPair(candidates, models, index, result.keys)
if (!pair) return result
result.keys.add(comparisonKey(pair.first, pair.second))
const first = modelRefFromCatalog(pair.first)
const second = modelRefFromCatalog(pair.second)
result.categories.push({
title: template.title,
description: template.description,
first,
second,
avatars: [first, second],
})
return result
},
{ keys: new Set(), categories: [] },
).categories
}
function categoryPair(
candidates: ModelCatalogEntry[],
fallback: ModelCatalogEntry[],
offset: number,
usedKeys: Set<string>,
) {
const pool = uniqueModels([...candidates, ...fallback.slice(offset), ...fallback])
return pool
.flatMap((first, firstIndex) =>
pool.slice(firstIndex + 1).map((second) => ({
first,
second,
key: comparisonKey(first, second),
})),
)
.find((pair) => !usedKeys.has(pair.key))
}
function categoryCandidates(kind: (typeof categoryTemplates)[number]["kind"], models: ModelCatalogEntry[]) {
if (kind === "affordable")
return models
.filter((model) => model.cost)
.toSorted(
(a, b) => modelCost(a) - modelCost(b) || displayDateTime(b.releaseDate) - displayDateTime(a.releaseDate),
)
if (kind === "code") return models.filter((model) => model.toolCall || model.reasoning).toSorted(recentModelSort)
if (kind === "image")
return models
.filter((model) => model.modalities.output.some((modality) => modality.toLowerCase().includes("image")))
.toSorted(recentModelSort)
return models.toSorted(recentModelSort)
}
function uniqueModels(models: (ModelCatalogEntry | undefined)[]) {
return models.reduce<{ keys: Set<string>; models: ModelCatalogEntry[] }>(
(result, model) => {
if (!model || result.keys.has(model.id)) return result
result.keys.add(model.id)
result.models.push(model)
return result
},
{ keys: new Set(), models: [] },
).models
}
function recentModelSort(a: ModelCatalogEntry, b: ModelCatalogEntry) {
return displayDateTime(b.releaseDate) - displayDateTime(a.releaseDate) || a.name.localeCompare(b.name)
}
function comparisonKey(first: ModelCatalogEntry, second: ModelCatalogEntry) {
return [first.id, second.id].toSorted().join("|")
}
function modelCost(model: ModelCatalogEntry) {
return (model.cost?.input ?? Number.POSITIVE_INFINITY) + (model.cost?.output ?? Number.POSITIVE_INFINITY)
}
function modelSearchText(model: ModelCatalogEntry) {
return `${model.name} ${formatCatalogLabName(model.lab)} ${model.id}`.toLowerCase()
}
function isFreeModel(model: ModelCatalogEntry) {
return model.cost?.input === 0 && model.cost.output === 0
}
function modelHref(model: ModelCatalogEntry) {
return `${import.meta.env.BASE_URL}${model.lab}/${model.slug}`
}
function formatModelUrl(model: ModelCatalogEntry) {
return `.../${model.slug}`
}
function formatCatalogLimit(value: number | undefined) {
return value === undefined ? "Unknown" : formatTokens(value)
}
function formatCatalogUnitPrice(value: number | undefined) {
if (value === undefined) return "Unknown"
return `${formatModelPrice(value)} / 1M`
}
function formatModelPrice(value: number) {
if (value > 0 && value < 0.01) return `$${value.toFixed(4)}`
return formatMoney(value)
}
function formatMoney(value: number) {
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 `$${value.toFixed(value >= 10 ? 0 : 2)}`
}
function formatCatalogDate(value: string | undefined) {
if (!value) return "Unknown"
const match = /^(\d{4})(?:-(\d{2}))?(?:-(\d{2}))?$/.exec(value)
if (!match) return value
return new Intl.DateTimeFormat("en", {
month: match[2] ? "short" : undefined,
day: match[3] ? "numeric" : undefined,
year: "numeric",
timeZone: "UTC",
}).format(new Date(Date.UTC(Number(match[1]), match[2] ? Number(match[2]) - 1 : 0, match[3] ? Number(match[3]) : 1)))
}
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 trimNumber(value: number, digits: number) {
return Number(value.toFixed(digits)).toLocaleString("en")
}
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
}
function getProviderIconId(provider: string) {
const id = provider.toLowerCase().replace(/[^a-z0-9]+/g, "")
if (id === "moonshot") return "moonshotai"
if (id === "zhipu") return "zhipuai"
return id
}

File diff suppressed because it is too large Load diff

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,14 @@ 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 />