diff --git a/packages/stats/app/src/routes/[lab]/[model].tsx b/packages/stats/app/src/routes/[lab]/[model].tsx new file mode 100644 index 000000000000..c7c928bb42c0 --- /dev/null +++ b/packages/stats/app/src/routes/[lab]/[model].tsx @@ -0,0 +1,826 @@ +import "../index.css" +import { Link, Meta, Title } from "@solidjs/meta" +import { ProviderIcon } from "@opencode-ai/ui/provider-icon" +import { geoEquirectangular, geoPath } from "d3-geo" +import { scaleSqrt } from "d3-scale" +import countryCodesSource from "i18n-iso-countries/codes.json?raw" +import { feature, mesh } from "topojson-client" +import countriesTopologySource from "world-atlas/countries-110m.json?raw" +import { + getStatsModelData, + type CountryEntry, + type ModelPeerEntry, + type ModelUsagePoint, + type StatsModelData, + type UsageRange, +} 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, 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" +import { findModelCatalogEntry, formatCatalogLabName, getModelCatalog, type ModelCatalogEntry } from "../model-catalog" +import { + applyThemePreference, + Footer, + getGitHubStars, + Header, + isThemePreference, + themeStorageKey, + type HeaderLink, + type ThemePreference, +} from "../stats-shell" + +const statsModelFallbackUrl = "https://stats.opencode.ai" +const modelHeaderLinks: readonly HeaderLink[] = [ + { href: "#overview", label: "Overview" }, + { href: "#usage", label: "Usage" }, + { href: "#efficiency", label: "Efficiency" }, + { href: "#geo-breakdown", label: "Geo Breakdown" }, + { href: "#peers", label: "Peers" }, +] +const modelFooterLinks: readonly HeaderLink[] = [ + { href: import.meta.env.BASE_URL, label: "Stats Home" }, + { href: `${import.meta.env.BASE_URL}#top-models`, label: "Top Models" }, + { href: `${import.meta.env.BASE_URL}#leaderboard`, label: "Leaderboard" }, + { href: `${import.meta.env.BASE_URL}#session-cost`, label: "Session Cost" }, + { href: `${import.meta.env.BASE_URL}#token-cost`, label: "Token Cost" }, + { href: `${import.meta.env.BASE_URL}#market-share`, label: "Market Share" }, + { href: `${import.meta.env.BASE_URL}#geo-breakdown`, label: "Geo Breakdown" }, +] +const geoMapWidth = 960 +const geoMapHeight = 430 +const countryDisplayNames = new Intl.DisplayNames(["en"], { type: "region" }) + +type IsoCountryCode = readonly [string, string, string] +type WorldCountryProperties = GeoJsonProperties & { name?: string } +type WorldTopology = Topology<{ countries: GeometryCollection }> + +const countryNumericIds = new Map( + (JSON.parse(countryCodesSource) as IsoCountryCode[]).map((country) => [country[0], country[2]] as const), +) +const worldTopology = JSON.parse(countriesTopologySource) as WorldTopology +const worldCountryGeometries: GeometryCollection = { + ...worldTopology.objects.countries, + geometries: worldTopology.objects.countries.geometries.filter((country) => String(country.id ?? "") !== "010"), +} +const worldCountries = feature(worldTopology, worldCountryGeometries) as FeatureCollection< + GeometryObject, + WorldCountryProperties +> +const worldProjection = geoEquirectangular().fitExtent( + [ + [10, 12], + [geoMapWidth - 10, geoMapHeight - 12], + ], + worldCountries, +) +const worldPath = geoPath(worldProjection) +const worldCountryPaths = worldCountries.features.map((country) => ({ + id: String(country.id ?? "").padStart(3, "0"), + path: worldPath(country) ?? "", +})) +const worldBorderPath = worldPath(mesh(worldTopology, worldCountryGeometries, (a, b) => a !== b)) ?? "" + +const getModelData = query(async (lab: string, model: string) => { + "use server" + return runtime.runPromise(getStatsModelData(model, lab)) +}, "getStatsModelData") + +export default function StatsModel() { + const event = getRequestEvent() + event?.response.headers.set("Cache-Control", "public, max-age=60, s-maxage=300, stale-while-revalidate=86400") + const params = useParams() + const labParam = createMemo(() => params.lab ?? "") + const modelParam = createMemo(() => params.model ?? "") + const catalog = createAsync(() => getModelCatalog()) + const catalogEntry = createMemo(() => { + const data = catalog() + if (!data) return undefined + return findModelCatalogEntry(data, modelParam(), labParam()) ?? null + }) + const stats = createAsync(() => { + const entry = catalogEntry() + if (catalog() === undefined || entry === undefined) return Promise.resolve(undefined) + if (!entry && (!labParam() || !modelParam())) return Promise.resolve(null) + return getModelData(labParam(), entry?.slug ?? modelParam()) + }) + const githubStars = createAsync(() => getGitHubStars()) + const [themePreference, setThemePreference] = createSignal("system") + const modelName = createMemo(() => catalogEntry()?.name ?? stats()?.model ?? modelParam() ?? "Model") + const labName = createMemo(() => formatCatalogLabName(catalogEntry()?.lab ?? stats()?.provider ?? labParam())) + const modelTitle = createMemo(() => `${modelName()} Stats`) + const modelDescription = createMemo(() => + stats() + ? `${modelName()} usage, rank, token mix, cost, geo breakdown, and peer stats across OpenCode.` + : `${modelName()} model facts, limits, and OpenCode usage availability.`, + ) + const modelUrl = createMemo(() => + new URL( + `${import.meta.env.BASE_URL}${catalogEntry()?.id ?? `${labParam()}/${stats()?.slug ?? modelParam()}`}`, + event?.request.url ?? (typeof window === "undefined" ? statsModelFallbackUrl : window.location.href), + ).toString(), + ) + 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 ( +
+ {modelTitle()} + + + + + + + + + + +
+
+
+ }> + }> + <> + + + + + + + + + +
+
+
+
+ ) +} + +function ModelLoading() { + return ( + <> +
+
+
+ + Stats + +

Model Stats

+

Reading model aggregates from model_stat.

+
+
+
+
+ +
+ + ) +} + +function ModelNotFound(props: { lab: string; model: string }) { + return ( + <> +
+
+
+ + Stats + +

{props.model || "Model"}

+

No model facts or model_stat rows matched {props.lab ? `${props.lab}/${props.model}` : props.model}.

+
+
+
+
+ +
+ + ) +} + +function ModelHero(props: { data: StatsModelData | null; catalog: ModelCatalogEntry | null; labName: string }) { + const labId = () => props.catalog?.lab ?? props.data?.provider ?? props.labName + const modelId = () => props.catalog?.id ?? props.data?.model ?? "Model" + const weights = () => props.catalog?.weights[0] + return ( +
+ + Stats + +
+
+
+ + + {modelId()} +
+

{props.catalog?.name ?? props.data?.model ?? "Model"}

+ Model facts from the shared model index. OpenCode usage appears once this model has activity.

+ } + > + {(data) => ( +

+ Ranked #{data().rank} across recent OpenCode token usage with {formatPercent(data().tokenShare)} of + observed volume. +

+ )} +
+ + {(weight) => ( + + Model weights: {weight().label} + + )} + +
+ }> + {(data) => ( +
+ Current Rank + #{data().rank} +

{formatRankMoveLabel(data().previousRank, data().rank)}

+
+ )} +
+
+
+ ) +} + +function ModelCatalogCallout(props: { catalog: ModelCatalogEntry | null }) { + return ( +
+ Model Profile + {props.catalog?.releaseDate ? formatCatalogDate(props.catalog.releaseDate) : "Listed"} +

No OpenCode usage in the current stats window.

+
+ ) +} + +function ModelCatalogPanel(props: { data: ModelCatalogEntry }) { + return ( + + ) +} + +function CatalogDatum(props: { label: string; value: string }) { + return ( +
+ {props.label} + {props.value} +
+ ) +} + +function ModelOverview(props: { data: StatsModelData | null }) { + return ( +
+ + } + > + {(data) => ( +
+ + + + +
+ )} +
+
+ ) +} + +function ModelUsageSection(props: { data: ModelUsagePoint[] }) { + const [activeIndex, setActiveIndex] = createSignal() + const max = createMemo(() => Math.max(0, ...props.data.map((item) => item.tokens)) || 1) + const activePoint = createMemo(() => { + const index = activeIndex() + if (index === undefined) return undefined + return props.data[index] + }) + + return ( +
+ + item.tokens > 0)} + fallback={} + > +
{ + if (event.pointerType === "touch") return + setActiveIndex(undefined) + }} + > + +
+ + {(point, index) => ( +
{ + 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()) + }} + > +
+ + {(active) => ( +
props.data.length * 0.62 ? "left" : "right"} + > + {active().date} + {formatTokens(active().tokens)} tokens +
+

+ + Daily tokens + + {formatTokens(active().tokens)} +

+
+ )} + +
+ )} + +
+
+ +
+ ) +} + +function ModelEfficiencySection(props: { data: StatsModelData | null }) { + return ( +
+ + + } + > + {(data) => ( +
+ + + + + +
+ )} +
+
+ ) +} + +function ModelGeoBreakdownSection(props: { data: Record }) { + const [activeCountry, setActiveCountry] = createSignal() + const data = createMemo(() => props.data["2M"]) + const countryById = createMemo( + () => + new Map( + data().flatMap((country) => { + const id = countryNumericId(country.country) + return id ? [[id, country] as const] : [] + }), + ), + ) + const maxTokens = createMemo(() => Math.max(0, ...data().map((country) => country.tokens)) || 1) + const topCountries = createMemo(() => data().slice(0, 15)) + const active = createMemo(() => data().find((country) => country.country === activeCountry()) ?? data()[0]) + + return ( +
{ + if (event.pointerType === "touch") return + setActiveCountry(undefined) + }} + > + + 0} + fallback={} + > +
+
+ + + {(country) => ( +
+ #{String(country().rank).padStart(2, "0")} + {formatCountryName(country().country)} +

+ {formatGeoTokens(country().tokens)} + {formatGeoShare(country().share)} +

+
+ )} +
+
+ +
+
+
+ ) +} + +function GeoWorldMap(props: { + countryById: Map + activeCountry: string | undefined + maxTokens: number + onActiveCountryChange: (country: string | undefined) => void +}) { + const opacityScale = createMemo(() => scaleSqrt().domain([0, props.maxTokens]).range([0.26, 0.96]).clamp(true)) + const countryOpacity = (country: CountryEntry | undefined) => { + if (!country) return 0 + const opacity = opacityScale()(country.tokens) + if (!props.activeCountry || props.activeCountry === country.country) return opacity + return Math.max(0.18, opacity * 0.36) + } + + return ( + + Geo Breakdown map + + + {(country) => { + const entry = () => props.countryById.get(country.id) + return ( + + + + + ) +} + +function GeoCountryList(props: { + data: CountryEntry[] + activeCountry: string | undefined + maxTokens: number + onActiveCountryChange: (country: string | undefined) => void +}) { + const opacityScale = createMemo(() => scaleSqrt().domain([0, props.maxTokens]).range([0.26, 0.96]).clamp(true)) + + return ( +
    + + {(country) => ( +
  1. + +
  2. + )} +
    +
+ ) +} + +function ModelPeersSection(props: { data: StatsModelData | null }) { + return ( +
+ + } + > +
    + + {(peer) => } + +
+
+
+ ) +} + +function MetricCard(props: { label: string; value: string; detail: string; state?: "positive" | "negative" }) { + return ( +
+ {props.label} + {props.value} +

{props.detail}

+
+ ) +} + +function PeerRow(props: { peer: ModelPeerEntry; active: boolean }) { + return ( +
  • + + {String(props.peer.rank).padStart(2, "0")} + +
  • + ) +} + +function SectionTitle(props: { title: string; description: string }) { + return ( +

    + {props.title}. {props.description} +

    + ) +} + +function ModelEmptyState(props: { title: string; description: string; compact?: boolean }) { + return ( +
    + {props.title} +

    {props.description}

    +
    + ) +} + +function getProviderIconId(author: string) { + if (author === "MiniMax") return "minimax" + if (author === "Moonshot") return "moonshotai" + if (author === "Zhipu") return "zhipuai" + return author.toLowerCase().replace(/[^a-z0-9]+/g, "") +} + +function emptyCountryRecord(): Record { + return { + "1D": [], + "1W": [], + "2W": [], + "1M": [], + "2M": [], + "3M": [], + YTD: [], + ALL: [], + } +} + +function countryNumericId(country: string) { + return countryNumericIds.get(country.toUpperCase())?.padStart(3, "0") +} + +function formatCountryName(country: string) { + const code = country.toUpperCase() + if (code === "ZZ") return "Unknown" + if (!countryNumericId(code)) return code + return countryDisplayNames.of(code) ?? code +} + +function formatGeoTokens(value: number) { + return formatTokens(value * 1_000_000_000_000) +} + +function formatGeoShare(value: number) { + return `${value.toFixed(value > 0 && value < 1 ? 1 : 0)}%` +} + +function modelUsageHeight(tokens: number, max: number) { + if (tokens <= 0) return 0 + return Math.max(2, Math.min(100, (tokens / max) * 100)) +} + +function isModelUsageDense(count: number) { + return count > 20 +} + +function isModelUsageLabelHidden(index: number, count: number) { + if (count <= 16) return false + const interval = Math.ceil(count / 8) + return index !== count - 1 && index % interval !== 0 +} + +function formatRankMove(previousRank: number, rank: number) { + const change = previousRank - rank + if (change > 0) return `+${change}` + if (change < 0) return `${change}` + return "Even" +} + +function formatRankMoveLabel(previousRank: number | null, rank: number) { + return previousRank === null ? "New in window" : `${formatRankMove(previousRank, rank)} vs previous window` +} + +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 `${value.toFixed(value > 0 && value < 10 ? 1 : 0)}%` +} + +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 formatSessionCost(value: number) { + return `$${value.toFixed(value > 0 && value < 0.01 ? 4 : 2)}` +} + +function formatChange(value: number) { + if (value > 0) return `+${value}%` + return `${value}%` +} + +function formatCatalogLimit(value: number | undefined) { + return value === undefined ? "Unknown" : formatTokens(value) +} + +function formatCatalogModalities(value: string[]) { + if (value.length === 0) return "Unknown" + return value.map(formatCatalogModality).join(", ") +} + +function formatCatalogModality(value: string) { + if (value === "pdf") return "PDF" + return value.charAt(0).toUpperCase() + value.slice(1) +} + +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 trimNumber(value: number, digits: number) { + return Number(value.toFixed(digits)).toLocaleString("en") +} + +function providerSlug(provider: string) { + return provider + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") + .replace(/-{2,}/g, "-") +} diff --git a/packages/stats/app/src/routes/[lab]/index.tsx b/packages/stats/app/src/routes/[lab]/index.tsx new file mode 100644 index 000000000000..c4a59aad651c --- /dev/null +++ b/packages/stats/app/src/routes/[lab]/index.tsx @@ -0,0 +1,427 @@ +import "../index.css" +import { Link, Meta, Title } from "@solidjs/meta" +import { + getStatsLabData, + type LabUsageModelEntry, + type ModelUsagePoint, + type StatsLabData, +} 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, type JSX } from "solid-js" +import { getRequestEvent } from "solid-js/web" +import { + findModelCatalogLab, + formatCatalogLabName, + getModelCatalog, + type ModelCatalogEntry, + type ModelCatalogLab, +} from "../model-catalog" +import { + applyThemePreference, + Footer, + getGitHubStars, + Header, + isThemePreference, + themeStorageKey, + type HeaderLink, + type ThemePreference, +} from "../stats-shell" + +const statsLabFallbackUrl = "https://stats.opencode.ai" +const labHeaderLinks: readonly HeaderLink[] = [ + { href: "#overview", label: "Overview" }, + { href: "#usage", label: "Usage" }, + { href: "#models", label: "Models" }, +] +const labFooterLinks: readonly HeaderLink[] = [ + { href: import.meta.env.BASE_URL, label: "Stats Home" }, + { href: `${import.meta.env.BASE_URL}#top-models`, label: "Top Models" }, + { href: `${import.meta.env.BASE_URL}#market-share`, label: "Market Share" }, + { href: `${import.meta.env.BASE_URL}#geo-breakdown`, label: "Geo Breakdown" }, +] + +const getLabData = query(async (lab: string) => { + "use server" + return runtime.runPromise(getStatsLabData(lab)) +}, "getStatsLabData") + +export default function StatsLab() { + const event = getRequestEvent() + event?.response.headers.set("Cache-Control", "public, max-age=60, s-maxage=300, stale-while-revalidate=86400") + const params = useParams() + const labParam = createMemo(() => params.lab ?? "") + const catalog = createAsync(() => getModelCatalog()) + const lab = createMemo(() => { + const data = catalog() + if (!data) return undefined + return findModelCatalogLab(data, labParam()) ?? null + }) + const stats = createAsync(() => { + const entry = lab() + if (catalog() === undefined || entry === undefined) return Promise.resolve(undefined) + if (!entry) return Promise.resolve(null) + return getLabData(entry.id) + }) + const githubStars = createAsync(() => getGitHubStars()) + const [themePreference, setThemePreference] = createSignal("system") + const labName = createMemo(() => lab()?.name ?? formatCatalogLabName(labParam())) + const labTitle = createMemo(() => `${labName()} Models`) + const labDescription = createMemo( + () => + `Explore ${labName()} models used in OpenCode, with recent token usage, context windows, release dates, and model-specific stats.`, + ) + const labUrl = createMemo(() => + new URL( + `${import.meta.env.BASE_URL}${lab()?.id ?? labParam()}`, + event?.request.url ?? (typeof window === "undefined" ? statsLabFallbackUrl : window.location.href), + ).toString(), + ) + 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 ( +
    + {labTitle()} + + + + + + + + + + +
    +
    +
    + }> + }> + {(data) => ( + <> + + + + + )} + + +
    +
    +
    +
    + ) +} + +function LabLoading() { + return ( +
    +
    +
    + + Stats + +

    Model Lab

    +

    Reading model availability and recent OpenCode usage.

    +
    +
    +
    + ) +} + +function LabNotFound(props: { lab: string }) { + return ( +
    +
    +
    + + Stats + +

    {formatCatalogLabName(props.lab)}

    +

    No models matched this lab.

    +
    +
    +
    + ) +} + +function LabHero(props: { lab: ModelCatalogLab; stats: StatsLabData | null }) { + const latest = createMemo( + () => + props.lab.models + .map((model) => model.releaseDate) + .filter((value): value is string => value !== undefined) + .toSorted((a, b) => new Date(b).getTime() - new Date(a).getTime())[0], + ) + const featuredModels = createMemo(() => props.lab.models.slice(0, 3).map((model) => model.name)) + + return ( +
    + + Stats + +
    +
    +

    {props.lab.name}

    + +
    + Tokens Processed + {props.stats ? formatTokens(props.stats.totals.tokens) : "Pending"} +

    + {props.stats + ? `${formatPercent(props.stats.tokenShare)} of recent OpenCode usage` + : latest() + ? `Latest release ${formatCatalogDate(latest())}` + : "Usage appears after model activity lands"} +

    +
    +
    +
    + ) +} + +function LabUsageSection(props: { lab: ModelCatalogLab; data: StatsLabData | null }) { + const [activeIndex, setActiveIndex] = createSignal() + const usage = createMemo(() => props.data?.usage ?? []) + const max = createMemo(() => Math.max(0, ...usage().map((item) => item.tokens)) || 1) + const activePoint = createMemo(() => { + const index = activeIndex() + if (index === undefined) return undefined + return usage()[index] + }) + + return ( +
    +

    + {props.lab.name} token usage.{" "} + Daily OpenCode token volume over the last two months. +

    + item.tokens > 0)} + fallback={ + + } + > +
    { + if (event.pointerType === "touch") return + setActiveIndex(undefined) + }} + > + +
    + + {(point, index) => ( +
    { + 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()) + }} + > +
    + + {(active) => ( +
    usage().length * 0.62 ? "left" : "right"} + > + {active().date} + {formatTokens(active().tokens)} tokens +
    +

    + + Daily tokens + + {formatTokens(active().tokens)} +

    +
    + )} + +
    + )} + +
    +
    + +
    + ) +} + +function LabModelsSection(props: { lab: ModelCatalogLab; usage: LabUsageModelEntry[] }) { + const usageBySlug = createMemo(() => new Map(props.usage.map((item) => [item.slug, item]))) + return ( +
    +

    + {props.lab.name} models. Recent usage and limits. +

    +
    + + {(model) => } + +
    +
    + ) +} + +function LabModelCard(props: { model: ModelCatalogEntry; usage: LabUsageModelEntry | undefined }) { + return ( + + {props.model.name} +
    +

    + Usage + {props.usage ? formatTokens(props.usage.tokens) : "—"} +

    +

    + Share + {props.usage ? formatPercent(props.usage.share) : "—"} +

    +

    + Context + {formatCatalogLimit(props.model.limit?.context)} +

    +

    + Output + {formatCatalogLimit(props.model.limit?.output)} +

    +

    + Release + {formatCatalogDate(props.model.releaseDate)} +

    +
    +
    + ) +} + +function LabEmptyState(props: { title: string; description: string }) { + return ( +
    + {props.title} +

    {props.description}

    +
    + ) +} + +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 formatList(values: string[]) { + if (values.length <= 1) return values[0] ?? "" + if (values.length === 2) return `${values[0]} and ${values[1]}` + return `${values.slice(0, -1).join(", ")}, and ${values[values.length - 1]}` +} + +function formatPercent(value: number) { + return `${trimNumber(value, value >= 10 ? 1 : 2)}%` +} + +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 usageHeight(value: number, max: number) { + if (value <= 0 || max <= 0) return 0 + return Math.max(4, (value / max) * 100) +} + +function isLabUsageDense(count: number) { + return count > 20 +} + +function isLabUsageLabelHidden(index: number, count: number) { + if (count <= 14) return false + const cadence = count > 45 ? 7 : count > 28 ? 4 : 2 + return index % cadence !== 0 && index !== count - 1 +} diff --git a/packages/stats/app/src/routes/index.css b/packages/stats/app/src/routes/index.css index 576c28e598dc..f6d65e93e15a 100644 --- a/packages/stats/app/src/routes/index.css +++ b/packages/stats/app/src/routes/index.css @@ -740,8 +740,8 @@ } [data-page="stats"] [data-component="content"] a:hover { - text-decoration: underline; - text-underline-offset: 4px; + color: var(--stats-text); + text-decoration: none; } [data-page="stats"] [data-section="chart"] { @@ -1393,6 +1393,10 @@ line-height: 1.5; } +[data-page="stats"] [data-component="empty-state"][data-compact="true"] { + min-height: 172px; +} + [data-page="stats"] [data-component="usage-chart"] svg { display: block; width: 100%; @@ -1790,6 +1794,11 @@ transform 140ms ease; } +[data-page="stats"] a[data-component="leader-card"], +[data-page="stats"] a[data-component="leader-card"]:hover { + text-decoration: none; +} + [data-page="stats"] [data-slot="rank"], [data-page="stats"] [data-slot="delta"][data-negative="true"] { color: var(--stats-faint); @@ -2603,207 +2612,822 @@ width: fit-content; } -[data-page="stats"][data-theme="dark"], -:root[data-stats-theme="dark"] [data-page="stats"]:not([data-theme="light"]) { - color-scheme: dark; - --color-background: #161616; - --color-background-weak: #242424; - --color-background-weak-hover: #303030; - --color-background-strong: #ffffff; - --color-background-strong-hover: #eeeeee; - --color-text: #d4d4d4; - --color-text-weak: #808080; - --color-text-strong: #ffffff; - --color-text-inverted: #161616; - --color-border-weak: #ffffff1a; - --stats-bg: #161616; - --stats-layer: #242424; - --stats-layer-2: #303030; - --stats-line: #ffffff1a; - --stats-line-strong: #ffffff33; - --stats-text: #ffffff; - --stats-muted: #d4d4d4; - --stats-faint: #808080; - --stats-theme-icon-active: #fafafa; - --stats-bar-idle: #303030; - --stats-dot: #303030; - --stats-hero-muted: #808080; - --stats-hero-pattern: #303030; - --stats-logo-bg: #f1ecec; - --stats-logo-fill: #b7b1b1; - --stats-logo-stroke: #211e1e; +[data-page="stats"] [data-section="model-hero"], +[data-page="stats"] [data-section="lab-hero"], +[data-page="stats"] [data-section="model-panel"] { + position: relative; + box-sizing: border-box; + box-shadow: + inset 0 -1px var(--stats-line), + inset 1px 0 var(--stats-line), + inset -1px 0 var(--stats-line); + color: var(--stats-text); } -[data-page="stats"][data-theme="dark"] [data-component="chart-tooltip"], -:root[data-stats-theme="dark"] [data-page="stats"]:not([data-theme="light"]) [data-component="chart-tooltip"] { - background: #242424f2; +[data-page="stats"] [data-section="model-hero"], +[data-page="stats"] [data-section="lab-hero"] { + display: grid; + gap: 28px; + padding: 120px 40px 64px; } -[data-page="stats"][data-theme="dark"] [data-section="top-models"] [data-component="chart-tooltip"], -:root[data-stats-theme="dark"] - [data-page="stats"]:not([data-theme="light"]) - [data-section="top-models"] - [data-component="chart-tooltip"] { - background: #242424f2; - box-shadow: - 0 0 0 0.5px #ffffff24, - 0 8px 16px #0000003d, - 0 4px 8px #00000052; +[data-page="stats"] [data-slot="model-hero-grid"] { + display: grid; + grid-template-columns: minmax(0, 1fr) 240px; + gap: 40px; + align-items: end; } -[data-page="stats"][data-theme="dark"] [data-section="top-models"] [data-component="chart-tooltip"] > span, -:root[data-stats-theme="dark"] - [data-page="stats"]:not([data-theme="light"]) - [data-section="top-models"] - [data-component="chart-tooltip"] - > span { +[data-page="stats"] [data-slot="model-hero-copy"] { + display: grid; + gap: 20px; + min-width: 0; +} + +[data-page="stats"] [data-slot="model-back-link"] { + width: fit-content; color: var(--stats-faint); + font-size: 11px; + font-weight: 500; + line-height: 1; + text-decoration: none; + text-transform: uppercase; } -[data-page="stats"][data-theme="dark"] [data-component="leader-card"][data-size="compact"], -:root[data-stats-theme="dark"] - [data-page="stats"]:not([data-theme="light"]) - [data-component="leader-card"][data-size="compact"] { - box-shadow: - 0 0 0 0.5px #ffffff1f, - 0 1px 2px -1px #00000052, - 0 2px 4px #0000003d; +[data-page="stats"] [data-slot="model-back-link"]::before { + content: "‹ "; } -[data-page="stats"][data-theme="dark"] [data-component="leader-card"][data-active="true"], -[data-page="stats"][data-theme="dark"] [data-component="leader-card"]:hover, -[data-page="stats"][data-theme="dark"] [data-component="leader-card"]:focus-visible, -:root[data-stats-theme="dark"] - [data-page="stats"]:not([data-theme="light"]) - [data-component="leader-card"][data-active="true"], -:root[data-stats-theme="dark"] [data-page="stats"]:not([data-theme="light"]) [data-component="leader-card"]:hover, -:root[data-stats-theme="dark"] - [data-page="stats"]:not([data-theme="light"]) - [data-component="leader-card"]:focus-visible { - background: #303030; - box-shadow: - 0 0 0 0.5px #ffffff24, - 0 6px 16px #0000003d, - 0 2px 6px #00000052; +[data-page="stats"] [data-slot="model-back-link"]:hover, +[data-page="stats"] [data-slot="model-back-link"]:focus-visible { + color: var(--stats-text); + outline: none; + text-decoration: none; } -[data-page="stats"][data-theme="dark"] - [data-component="leader-card"][data-active="true"] - [data-slot="leader-watermark"], -[data-page="stats"][data-theme="dark"] [data-component="leader-card"]:hover [data-slot="leader-watermark"], -[data-page="stats"][data-theme="dark"] [data-component="leader-card"]:focus-visible [data-slot="leader-watermark"], -:root[data-stats-theme="dark"] - [data-page="stats"]:not([data-theme="light"]) - [data-component="leader-card"][data-active="true"] - [data-slot="leader-watermark"], -:root[data-stats-theme="dark"] - [data-page="stats"]:not([data-theme="light"]) - [data-component="leader-card"]:hover - [data-slot="leader-watermark"], -:root[data-stats-theme="dark"] - [data-page="stats"]:not([data-theme="light"]) - [data-component="leader-card"]:focus-visible - [data-slot="leader-watermark"] { - color: var(--stats-line-strong); - opacity: 0.78; +[data-page="stats"] [data-section="model-hero"] [data-slot="hero-meta"], +[data-page="stats"] [data-section="lab-hero"] [data-slot="hero-meta"] { + display: inline-flex; + align-items: center; + gap: 8px; + padding-left: 6px; + color: var(--stats-hero-muted); + text-decoration: none; } -[data-page="stats"][data-theme="dark"] button[data-component="cache-ratio-row"][data-active="true"], -:root[data-stats-theme="dark"] - [data-page="stats"]:not([data-theme="light"]) - button[data-component="cache-ratio-row"][data-active="true"] { - background: #ffffff0d; +[data-page="stats"] [data-section="model-hero"] a[data-slot="hero-meta"]:hover, +[data-page="stats"] [data-section="model-hero"] a[data-slot="hero-meta"]:focus-visible { + color: var(--stats-text); + outline: none; + text-decoration: none; } -[data-page="stats"][data-theme="dark"] [data-slot="header-button"][data-variant="neutral"], -[data-page="stats"][data-theme="dark"] [data-slot="menu-button"], -:root[data-stats-theme="dark"] - [data-page="stats"]:not([data-theme="light"]) - [data-slot="header-button"][data-variant="neutral"], -:root[data-stats-theme="dark"] [data-page="stats"]:not([data-theme="light"]) [data-slot="menu-button"] { - color: #fafafa; - background: #ffffff0f; - box-shadow: - 0 -0.5px 0 0 #ffffff33, - 0 0 0 0.5px #ffffff33, - 0 1px 2px 0 #00000066; +[data-page="stats"] [data-section="model-hero"] [data-slot="hero-meta"] svg, +[data-page="stats"] [data-section="lab-hero"] [data-slot="hero-meta"] svg { + width: 14px; + height: 14px; } -[data-page="stats"][data-theme="dark"] [data-slot="header-button"][data-variant="neutral"] span, -:root[data-stats-theme="dark"] - [data-page="stats"]:not([data-theme="light"]) - [data-slot="header-button"][data-variant="neutral"] - span { - color: #aeaeae; +[data-page="stats"] [data-slot="model-hero-tags"] { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; + min-width: 0; } -[data-page="stats"][data-theme="dark"] [data-slot="header-button"][data-variant="contrast"], -:root[data-stats-theme="dark"] - [data-page="stats"]:not([data-theme="light"]) - [data-slot="header-button"][data-variant="contrast"] { - color: #ffffff; - background: #5c5c5c; - box-shadow: - 0 -0.5px 0 0 #ffffff4d, - 0 0 0 0.5px #ffffff66, - 0 1px 2px 0 #00000066; +[data-page="stats"] [data-slot="model-id-tag"] { + display: inline-flex; + align-items: center; + max-width: 100%; + height: 24px; + padding: 0 8px; + overflow: hidden; + color: var(--stats-hero-muted); + background: var(--stats-layer-2); + font-size: 13px; + font-weight: 500; + line-height: 1.1; + text-overflow: ellipsis; + white-space: nowrap; } -@media (prefers-color-scheme: dark) { - :root:not([data-stats-theme="light"]) [data-page="stats"]:not([data-theme="light"]) { - color-scheme: dark; - --color-background: #161616; - --color-background-weak: #242424; - --color-background-weak-hover: #303030; - --color-background-strong: #ffffff; - --color-background-strong-hover: #eeeeee; - --color-text: #d4d4d4; - --color-text-weak: #808080; - --color-text-strong: #ffffff; - --color-text-inverted: #161616; - --color-border-weak: #ffffff1a; - --stats-bg: #161616; - --stats-layer: #242424; - --stats-layer-2: #303030; - --stats-line: #ffffff1a; - --stats-line-strong: #ffffff33; - --stats-text: #ffffff; - --stats-muted: #d4d4d4; - --stats-faint: #808080; - --stats-theme-icon-active: #fafafa; - --stats-bar-idle: #303030; - --stats-dot: #303030; - --stats-hero-muted: #808080; - --stats-hero-pattern: #303030; - --stats-logo-bg: #f1ecec; - --stats-logo-fill: #b7b1b1; - --stats-logo-stroke: #211e1e; - } +[data-page="stats"] [data-section="model-hero"] h1, +[data-page="stats"] [data-section="lab-hero"] h1 { + max-width: 980px; + color: var(--stats-text); + font-size: clamp(38px, 7vw, 72px); + font-weight: 500; + line-height: 0.95; + letter-spacing: 0; + overflow-wrap: anywhere; +} - :root:not([data-stats-theme="light"]) [data-page="stats"]:not([data-theme="light"]) [data-component="chart-tooltip"] { - background: #242424f2; - } +[data-page="stats"] [data-slot="model-hero-pattern"] { + width: 100%; + height: 18px; + overflow: hidden; + background: var(--stats-hero-pattern); + mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0H2V2H0V0Z' fill='black'/%3E%3C/svg%3E"); + mask-repeat: repeat; + mask-size: 6px 6px; + -webkit-mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0H2V2H0V0Z' fill='black'/%3E%3C/svg%3E"); + -webkit-mask-repeat: repeat; + -webkit-mask-size: 6px 6px; +} - :root:not([data-stats-theme="light"]) - [data-page="stats"]:not([data-theme="light"]) - [data-section="top-models"] - [data-component="chart-tooltip"] { - background: #242424f2; - box-shadow: - 0 0 0 0.5px #ffffff24, - 0 8px 16px #0000003d, - 0 4px 8px #00000052; - } +[data-page="stats"] [data-slot="model-hero-copy"] p:not([data-slot="hero-meta"]) { + max-width: 720px; + color: var(--stats-muted); + font-size: 16px; + font-weight: 400; + line-height: 1.5; +} - :root:not([data-stats-theme="light"]) - [data-page="stats"]:not([data-theme="light"]) - [data-section="top-models"] - [data-component="chart-tooltip"] - > span { - color: var(--stats-faint); - } +[data-page="stats"] [data-slot="model-weight-link"] { + width: fit-content; + color: var(--stats-muted); + font-size: 12px; + font-weight: 500; + line-height: 1.3; + text-decoration: none; +} - :root:not([data-stats-theme="light"]) +[data-page="stats"] [data-slot="model-weight-link"]::after { + content: " ↗"; + color: var(--stats-muted); +} + +[data-page="stats"] [data-slot="model-weight-link"]:hover, +[data-page="stats"] [data-slot="model-weight-link"]:focus-visible { + color: var(--stats-text); + outline: none; + text-decoration: none; +} + +[data-page="stats"] [data-slot="model-weight-link"]:hover::after, +[data-page="stats"] [data-slot="model-weight-link"]:focus-visible::after { + color: var(--stats-text); +} + +[data-page="stats"] [data-component="model-rank-panel"] { + display: grid; + gap: 10px; + min-height: 164px; + padding: 18px; + border: 1px solid var(--stats-line); + background: var(--stats-layer); +} + +[data-page="stats"] [data-component="model-rank-panel"] span, +[data-page="stats"] [data-component="model-rank-panel"] p { + color: var(--stats-muted); + font-size: 11px; + font-weight: 500; + line-height: 1.4; +} + +[data-page="stats"] [data-component="model-rank-panel"] strong { + color: var(--stats-text); + font-size: 64px; + font-weight: 500; + line-height: 0.95; + letter-spacing: 0; +} + +[data-page="stats"] [data-component="model-catalog"] { + min-width: 0; +} + +[data-page="stats"] [data-component="model-catalog-datum"] span { + color: var(--stats-muted); + font-size: 11px; + font-weight: 500; + line-height: 1.3; +} + +[data-page="stats"] [data-slot="model-catalog-grid"] { + display: flex; + flex-wrap: wrap; + row-gap: 18px; + min-width: 0; +} + +[data-page="stats"] [data-component="model-catalog-datum"] { + display: grid; + align-content: start; + flex: 1 1 132px; + gap: 7px; + max-width: 220px; + min-width: 0; + padding: 0 24px; + border-left: 1px solid var(--stats-line); +} + +[data-page="stats"] [data-component="model-catalog-datum"]:first-child { + padding-left: 0; + border-left: 0; +} + +[data-page="stats"] [data-component="model-catalog-datum"] strong { + min-width: 0; + color: var(--stats-text); + font-size: 20px; + font-weight: 500; + line-height: 1; + overflow-wrap: anywhere; +} + +[data-page="stats"] [data-section="model-panel"] { + padding: 80px 40px; +} + +[data-page="stats"] [data-component="model-metric-grid"] { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 8px; +} + +[data-page="stats"] [data-component="model-metric-grid"][data-variant="dense"] { + grid-template-columns: repeat(5, minmax(0, 1fr)); +} + +[data-page="stats"] [data-component="model-metric"] { + display: grid; + gap: 16px; + min-width: 0; + min-height: 132px; + padding: 16px; + border: 1px solid var(--stats-line); + background: var(--stats-layer); +} + +[data-page="stats"] [data-component="model-metric"] span, +[data-page="stats"] [data-component="model-metric"] p { + color: var(--stats-muted); + font-size: 11px; + font-weight: 500; + line-height: 1.3; +} + +[data-page="stats"] [data-component="model-metric"] strong { + min-width: 0; + color: var(--stats-text); + font-size: 28px; + font-weight: 500; + line-height: 1; + letter-spacing: 0; + overflow-wrap: anywhere; +} + +[data-page="stats"] [data-component="model-metric"][data-state="positive"] strong { + color: #198b43; +} + +[data-page="stats"] [data-component="model-metric"][data-state="negative"] strong { + color: #b82d35; +} + +[data-page="stats"] [data-component="lab-model-grid"] { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; +} + +[data-page="stats"] [data-component="lab-model-card"] { + position: relative; + display: grid; + align-content: space-between; + gap: 28px; + min-height: 220px; + padding: 16px; + overflow: hidden; + border: 1px solid var(--stats-line); + color: var(--stats-text); + background: var(--stats-layer); +} + +[data-page="stats"] [data-component="lab-model-card"]:hover, +[data-page="stats"] [data-component="lab-model-card"]:focus-visible { + z-index: 1; + border-color: var(--stats-text); + outline: none; +} + +[data-page="stats"] [data-component="lab-model-card"] > strong { + align-self: start; + min-width: 0; + color: var(--stats-text); + font-size: 24px; + font-weight: 500; + line-height: 1.05; + letter-spacing: 0; + overflow-wrap: anywhere; +} + +[data-page="stats"] [data-slot="lab-model-card-meta"] { + display: grid; + gap: 8px; +} + +[data-page="stats"] [data-slot="lab-model-card-meta"] p { + display: flex; + justify-content: space-between; + gap: 12px; + margin: 0; + padding-top: 8px; + border-top: 1px solid var(--stats-line); + color: var(--stats-muted); + font-size: 11px; + font-weight: 500; + line-height: 1.3; +} + +[data-page="stats"] [data-slot="lab-model-card-meta"] b, +[data-page="stats"] [data-slot="lab-model-card-meta"] em { + font: inherit; +} + +[data-page="stats"] [data-slot="lab-model-card-meta"] em { + color: var(--stats-text); + text-align: right; +} + +[data-page="stats"] [data-component="model-usage-chart"] { + --model-usage-bar-gap: 6px; + --model-usage-dot-size: 6px; + --model-usage-dot-offset: 2px; + position: relative; + display: grid; + grid-template-rows: 40px minmax(0, 1fr); + gap: 12px; + height: clamp(320px, 42vh, 392px); + min-width: 0; +} + +[data-page="stats"] [data-slot="model-usage-axis"], +[data-page="stats"] [data-slot="model-usage-bars"] { + display: flex; + min-width: 0; +} + +[data-page="stats"] [data-slot="model-usage-axis"] { + gap: var(--model-usage-bar-gap); +} + +[data-page="stats"] [data-slot="model-usage-axis"] > div { + flex: 1 1 0; + min-width: 0; + color: var(--stats-faint); + font-size: 11px; + font-weight: 500; + line-height: 1.35; +} + +[data-page="stats"] [data-slot="model-usage-axis"] > div[data-active="true"] { + color: var(--stats-text); +} + +[data-page="stats"] [data-slot="model-usage-label"] { + display: flex; + flex-direction: column; + min-width: 0; +} + +[data-page="stats"] [data-slot="model-usage-total"] { + color: var(--stats-muted); + font-weight: 600; +} + +[data-page="stats"] [data-slot="model-usage-date"] { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +[data-page="stats"] [data-slot="model-usage-column"] { + position: relative; + box-sizing: border-box; + flex: 1 1 0; + min-width: 0; + height: 100%; + padding-inline: calc(var(--model-usage-bar-gap) / 2); + outline: none; + cursor: pointer; +} + +[data-page="stats"] [data-slot="model-usage-column"]::before { + position: absolute; + top: 0; + bottom: 0; + left: 50%; + width: calc(100% - var(--model-usage-bar-gap)); + transform: translateX(-50%); + content: ""; + background: var(--stats-dot); + mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0H2V2H0V0Z' fill='black'/%3E%3C/svg%3E"); + mask-position: var(--model-usage-dot-offset) top; + mask-repeat: repeat; + mask-size: var(--model-usage-dot-size) var(--model-usage-dot-size); + -webkit-mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0H2V2H0V0Z' fill='black'/%3E%3C/svg%3E"); + -webkit-mask-position: var(--model-usage-dot-offset) top; + -webkit-mask-repeat: repeat; + -webkit-mask-size: var(--model-usage-dot-size) var(--model-usage-dot-size); +} + +@supports (width: round(down, 100%, 1px)) { + [data-page="stats"] [data-slot="model-usage-column"]::before { + width: round(down, calc(100% - var(--model-usage-bar-gap)), var(--model-usage-dot-size)); + } +} + +[data-page="stats"] [data-slot="model-usage-bar"] { + position: absolute; + right: calc(var(--model-usage-bar-gap) / 2); + bottom: 0; + left: calc(var(--model-usage-bar-gap) / 2); + z-index: 1; + height: var(--model-usage-fill); + min-height: 0; + background: var(--stats-accent); + box-shadow: 0 -4px 0 var(--stats-bg); + transition: + background 120ms ease, + opacity 120ms ease; +} + +[data-page="stats"] [data-slot="model-usage-column"][data-muted="true"] [data-slot="model-usage-bar"] { + background: var(--stats-layer-2); +} + +[data-page="stats"] + [data-component="model-usage-chart"][data-dense-labels="true"] + [data-slot="model-usage-axis"] + > div { + position: relative; + display: flex; + align-items: center; + justify-content: center; + height: 40px; + font-weight: 600; +} + +[data-page="stats"] [data-component="model-usage-chart"][data-dense-labels="true"] [data-slot="model-usage-label"] { + position: absolute; + left: 50%; + width: max-content; + max-width: 72px; + transform: rotate(-90deg) translateX(-50%); + transform-origin: left center; +} + +[data-page="stats"] [data-component="model-usage-chart"][data-dense-labels="true"] [data-slot="model-usage-total"] { + display: none; +} + +[data-page="stats"] + [data-slot="model-usage-axis"] + > div[data-label-hidden="true"]:not([data-active="true"]) + [data-slot="model-usage-label"] { + visibility: hidden; +} + +[data-page="stats"] [data-component="model-usage-chart"] [data-component="chart-tooltip"] { + top: 12px; + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: 0; + width: 176px; + min-width: 176px; + padding: 0; + border: 0; + box-shadow: + 0 0 0 0.5px #00000024, + 0 8px 16px #0000000f, + 0 4px 8px #00000014; + color: var(--stats-text); +} + +[data-page="stats"] [data-component="model-usage-chart"] [data-component="chart-tooltip"][data-placement="right"] { + right: auto; + left: calc(100% + 8px); +} + +[data-page="stats"] [data-component="model-usage-chart"] [data-component="chart-tooltip"][data-placement="left"] { + right: calc(100% + 8px); + left: auto; +} + +[data-page="stats"] [data-component="model-usage-chart"] [data-component="chart-tooltip"] strong, +[data-page="stats"] [data-component="model-usage-chart"] [data-component="chart-tooltip"] > span { + display: block; + font-size: 11px; + line-height: 12px; + white-space: nowrap; +} + +[data-page="stats"] [data-component="model-usage-chart"] [data-component="chart-tooltip"] strong { + padding: 8px 8px 0; + font-weight: 500; +} + +[data-page="stats"] [data-component="model-usage-chart"] [data-component="chart-tooltip"] > span { + padding: 4px 8px 8px; + color: var(--stats-muted); +} + +[data-page="stats"] + [data-component="model-usage-chart"] + [data-component="chart-tooltip"] + [data-slot="tooltip-divider"] { + height: 0.5px; + margin: 0; +} + +[data-page="stats"] [data-component="model-usage-chart"] [data-component="chart-tooltip"] p { + grid-template-columns: minmax(0, 1fr) auto; + gap: 4px; + height: 20px; + margin: 8px 0; + padding: 0 8px; + font-size: 11px; + font-weight: 500; + line-height: 12px; +} + +[data-page="stats"] [data-component="model-usage-chart"] [data-component="chart-tooltip"] [data-slot="tooltip-label"] { + grid-template-columns: 16px minmax(0, 1fr); + gap: 4px; +} + +[data-page="stats"] [data-component="model-usage-chart"] [data-component="chart-tooltip"] i { + width: 6px; + height: 6px; + justify-self: center; + background: var(--stats-accent); +} + +[data-page="stats"] [data-component="model-usage-chart"] [data-component="chart-tooltip"] b { + font-weight: 500; +} + +[data-page="stats"] [data-component="model-peer-list"] { + display: grid; + gap: 8px; + margin: 0; + padding: 0; + list-style: none; +} + +[data-page="stats"] [data-component="model-peer-list"] a { + display: grid; + grid-template-columns: 38px 24px minmax(0, 1fr) minmax(96px, auto) minmax(80px, auto); + gap: 12px; + align-items: center; + min-height: 44px; + padding: 0 12px; + border: 1px solid var(--stats-line); + background: var(--stats-layer); + color: var(--stats-text); + text-decoration: none; +} + +[data-page="stats"] [data-component="model-peer-list"] a:hover, +[data-page="stats"] [data-component="model-peer-list"] a[data-active="true"] { + border-color: var(--stats-line-strong); + background: var(--stats-bg); + text-decoration: none; +} + +[data-page="stats"] [data-component="model-peer-list"] span, +[data-page="stats"] [data-component="model-peer-list"] em, +[data-page="stats"] [data-component="model-peer-list"] b { + color: var(--stats-muted); + font-size: 11px; + font-style: normal; + font-weight: 500; + line-height: 1.2; +} + +[data-page="stats"] [data-component="model-peer-list"] svg { + width: 18px; + height: 18px; + color: var(--stats-muted); +} + +[data-page="stats"] [data-component="model-peer-list"] strong { + min-width: 0; + overflow: hidden; + color: var(--stats-text); + font-size: 13px; + font-weight: 600; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +[data-page="stats"][data-theme="dark"], +:root[data-stats-theme="dark"] [data-page="stats"]:not([data-theme="light"]) { + color-scheme: dark; + --color-background: #161616; + --color-background-weak: #242424; + --color-background-weak-hover: #303030; + --color-background-strong: #ffffff; + --color-background-strong-hover: #eeeeee; + --color-text: #d4d4d4; + --color-text-weak: #808080; + --color-text-strong: #ffffff; + --color-text-inverted: #161616; + --color-border-weak: #ffffff1a; + --stats-bg: #161616; + --stats-layer: #242424; + --stats-layer-2: #303030; + --stats-line: #ffffff1a; + --stats-line-strong: #ffffff33; + --stats-text: #ffffff; + --stats-muted: #d4d4d4; + --stats-faint: #808080; + --stats-theme-icon-active: #fafafa; + --stats-bar-idle: #303030; + --stats-dot: #303030; + --stats-hero-muted: #808080; + --stats-hero-pattern: #303030; + --stats-logo-bg: #f1ecec; + --stats-logo-fill: #b7b1b1; + --stats-logo-stroke: #211e1e; +} + +[data-page="stats"][data-theme="dark"] [data-component="chart-tooltip"], +:root[data-stats-theme="dark"] [data-page="stats"]:not([data-theme="light"]) [data-component="chart-tooltip"] { + background: #242424f2; +} + +[data-page="stats"][data-theme="dark"] [data-section="top-models"] [data-component="chart-tooltip"], +:root[data-stats-theme="dark"] + [data-page="stats"]:not([data-theme="light"]) + [data-section="top-models"] + [data-component="chart-tooltip"] { + background: #242424f2; + box-shadow: + 0 0 0 0.5px #ffffff24, + 0 8px 16px #0000003d, + 0 4px 8px #00000052; +} + +[data-page="stats"][data-theme="dark"] [data-section="top-models"] [data-component="chart-tooltip"] > span, +:root[data-stats-theme="dark"] + [data-page="stats"]:not([data-theme="light"]) + [data-section="top-models"] + [data-component="chart-tooltip"] + > span { + color: var(--stats-faint); +} + +[data-page="stats"][data-theme="dark"] [data-component="leader-card"][data-size="compact"], +:root[data-stats-theme="dark"] + [data-page="stats"]:not([data-theme="light"]) + [data-component="leader-card"][data-size="compact"] { + box-shadow: + 0 0 0 0.5px #ffffff1f, + 0 1px 2px -1px #00000052, + 0 2px 4px #0000003d; +} + +[data-page="stats"][data-theme="dark"] [data-component="leader-card"][data-active="true"], +[data-page="stats"][data-theme="dark"] [data-component="leader-card"]:hover, +[data-page="stats"][data-theme="dark"] [data-component="leader-card"]:focus-visible, +:root[data-stats-theme="dark"] + [data-page="stats"]:not([data-theme="light"]) + [data-component="leader-card"][data-active="true"], +:root[data-stats-theme="dark"] [data-page="stats"]:not([data-theme="light"]) [data-component="leader-card"]:hover, +:root[data-stats-theme="dark"] + [data-page="stats"]:not([data-theme="light"]) + [data-component="leader-card"]:focus-visible { + background: #303030; + box-shadow: + 0 0 0 0.5px #ffffff24, + 0 6px 16px #0000003d, + 0 2px 6px #00000052; +} + +[data-page="stats"][data-theme="dark"] + [data-component="leader-card"][data-active="true"] + [data-slot="leader-watermark"], +[data-page="stats"][data-theme="dark"] [data-component="leader-card"]:hover [data-slot="leader-watermark"], +[data-page="stats"][data-theme="dark"] [data-component="leader-card"]:focus-visible [data-slot="leader-watermark"], +:root[data-stats-theme="dark"] + [data-page="stats"]:not([data-theme="light"]) + [data-component="leader-card"][data-active="true"] + [data-slot="leader-watermark"], +:root[data-stats-theme="dark"] + [data-page="stats"]:not([data-theme="light"]) + [data-component="leader-card"]:hover + [data-slot="leader-watermark"], +:root[data-stats-theme="dark"] + [data-page="stats"]:not([data-theme="light"]) + [data-component="leader-card"]:focus-visible + [data-slot="leader-watermark"] { + color: var(--stats-line-strong); + opacity: 0.78; +} + +[data-page="stats"][data-theme="dark"] button[data-component="cache-ratio-row"][data-active="true"], +:root[data-stats-theme="dark"] + [data-page="stats"]:not([data-theme="light"]) + button[data-component="cache-ratio-row"][data-active="true"] { + background: #ffffff0d; +} + +[data-page="stats"][data-theme="dark"] [data-slot="header-button"][data-variant="neutral"], +[data-page="stats"][data-theme="dark"] [data-slot="menu-button"], +:root[data-stats-theme="dark"] + [data-page="stats"]:not([data-theme="light"]) + [data-slot="header-button"][data-variant="neutral"], +:root[data-stats-theme="dark"] [data-page="stats"]:not([data-theme="light"]) [data-slot="menu-button"] { + color: #fafafa; + background: #ffffff0f; + box-shadow: + 0 -0.5px 0 0 #ffffff33, + 0 0 0 0.5px #ffffff33, + 0 1px 2px 0 #00000066; +} + +[data-page="stats"][data-theme="dark"] [data-slot="header-button"][data-variant="neutral"] span, +:root[data-stats-theme="dark"] + [data-page="stats"]:not([data-theme="light"]) + [data-slot="header-button"][data-variant="neutral"] + span { + color: #aeaeae; +} + +[data-page="stats"][data-theme="dark"] [data-slot="header-button"][data-variant="contrast"], +:root[data-stats-theme="dark"] + [data-page="stats"]:not([data-theme="light"]) + [data-slot="header-button"][data-variant="contrast"] { + color: #ffffff; + background: #5c5c5c; + box-shadow: + 0 -0.5px 0 0 #ffffff4d, + 0 0 0 0.5px #ffffff66, + 0 1px 2px 0 #00000066; +} + +@media (prefers-color-scheme: dark) { + :root:not([data-stats-theme="light"]) [data-page="stats"]:not([data-theme="light"]) { + color-scheme: dark; + --color-background: #161616; + --color-background-weak: #242424; + --color-background-weak-hover: #303030; + --color-background-strong: #ffffff; + --color-background-strong-hover: #eeeeee; + --color-text: #d4d4d4; + --color-text-weak: #808080; + --color-text-strong: #ffffff; + --color-text-inverted: #161616; + --color-border-weak: #ffffff1a; + --stats-bg: #161616; + --stats-layer: #242424; + --stats-layer-2: #303030; + --stats-line: #ffffff1a; + --stats-line-strong: #ffffff33; + --stats-text: #ffffff; + --stats-muted: #d4d4d4; + --stats-faint: #808080; + --stats-theme-icon-active: #fafafa; + --stats-bar-idle: #303030; + --stats-dot: #303030; + --stats-hero-muted: #808080; + --stats-hero-pattern: #303030; + --stats-logo-bg: #f1ecec; + --stats-logo-fill: #b7b1b1; + --stats-logo-stroke: #211e1e; + } + + :root:not([data-stats-theme="light"]) [data-page="stats"]:not([data-theme="light"]) [data-component="chart-tooltip"] { + background: #242424f2; + } + + :root:not([data-stats-theme="light"]) + [data-page="stats"]:not([data-theme="light"]) + [data-section="top-models"] + [data-component="chart-tooltip"] { + background: #242424f2; + box-shadow: + 0 0 0 0.5px #ffffff24, + 0 8px 16px #0000003d, + 0 4px 8px #00000052; + } + + :root:not([data-stats-theme="light"]) + [data-page="stats"]:not([data-theme="light"]) + [data-section="top-models"] + [data-component="chart-tooltip"] + > span { + color: var(--stats-faint); + } + + :root:not([data-stats-theme="light"]) [data-page="stats"]:not([data-theme="light"]) [data-component="leader-card"][data-size="compact"] { box-shadow: @@ -2923,7 +3547,10 @@ [data-page="stats"] [data-section="geo-breakdown"], [data-page="stats"] [data-section="token-cost"], [data-page="stats"] [data-section="cache-ratio"], - [data-page="stats"] [data-section="session-cost"] { + [data-page="stats"] [data-section="session-cost"], + [data-page="stats"] [data-section="model-hero"], + [data-page="stats"] [data-section="lab-hero"], + [data-page="stats"] [data-section="model-panel"] { padding: 64px 32px; } } @@ -2951,6 +3578,29 @@ flex-wrap: wrap; } + [data-page="stats"] [data-slot="model-hero-grid"] { + grid-template-columns: 1fr; + align-items: start; + } + + [data-page="stats"] [data-component="model-rank-panel"] { + min-height: 0; + } + + [data-page="stats"] [data-component="model-metric-grid"], + [data-page="stats"] [data-component="model-metric-grid"][data-variant="dense"], + [data-page="stats"] [data-component="lab-model-grid"] { + grid-template-columns: 1fr 1fr; + } + + [data-page="stats"] [data-component="model-peer-list"] a { + grid-template-columns: 32px 22px minmax(0, 1fr) minmax(72px, auto); + } + + [data-page="stats"] [data-component="model-peer-list"] em { + display: none; + } + [data-page="stats"] [data-slot="chart-footer"] { align-items: flex-start; flex-direction: column; @@ -3056,7 +3706,10 @@ [data-page="stats"] [data-section="geo-breakdown"], [data-page="stats"] [data-section="token-cost"], [data-page="stats"] [data-section="cache-ratio"], - [data-page="stats"] [data-section="session-cost"] { + [data-page="stats"] [data-section="session-cost"], + [data-page="stats"] [data-section="model-hero"], + [data-page="stats"] [data-section="lab-hero"], + [data-page="stats"] [data-section="model-panel"] { padding: 48px 24px; } @@ -3078,6 +3731,110 @@ width: 100%; } + [data-page="stats"] [data-section="model-hero"], + [data-page="stats"] [data-section="lab-hero"] { + gap: 20px; + } + + [data-page="stats"] [data-slot="model-hero-grid"] { + gap: 24px; + } + + [data-page="stats"] [data-section="model-hero"] h1, + [data-page="stats"] [data-section="lab-hero"] h1 { + font-size: 38px; + line-height: 1; + } + + [data-page="stats"] [data-slot="model-hero-pattern"] { + height: 14px; + } + + [data-page="stats"] [data-slot="model-catalog-grid"] { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 18px 24px; + } + + [data-page="stats"] [data-component="model-catalog-datum"] { + max-width: none; + padding: 0; + border-left: 0; + } + + [data-page="stats"] [data-component="model-catalog-datum"] strong { + font-size: 18px; + } + + [data-page="stats"] [data-slot="model-hero-copy"] p:not([data-slot="hero-meta"]) { + font-size: 13px; + } + + [data-page="stats"] [data-component="model-metric-grid"], + [data-page="stats"] [data-component="model-metric-grid"][data-variant="dense"], + [data-page="stats"] [data-component="lab-model-grid"] { + grid-template-columns: 1fr; + } + + [data-page="stats"] [data-component="model-metric"] { + min-height: 112px; + } + + [data-page="stats"] [data-component="model-usage-chart"] { + --model-usage-mobile-bar-width: 16px; + --model-usage-mobile-track-width: calc( + var(--model-usage-count) * (var(--model-usage-mobile-bar-width) + var(--model-usage-bar-gap)) - + var(--model-usage-bar-gap) + ); + width: calc(100% + 48px); + height: 360px; + margin-inline: -24px; + padding-inline: 24px; + overflow-x: auto; + overscroll-behavior-x: contain; + scrollbar-width: none; + } + + [data-page="stats"] [data-component="model-usage-chart"]::-webkit-scrollbar { + display: none; + } + + [data-page="stats"] [data-slot="model-usage-axis"], + [data-page="stats"] [data-slot="model-usage-bars"] { + width: var(--model-usage-mobile-track-width); + min-width: var(--model-usage-mobile-track-width); + } + + [data-page="stats"] [data-slot="model-usage-bars"] { + gap: var(--model-usage-bar-gap); + } + + [data-page="stats"] [data-slot="model-usage-axis"] > div, + [data-page="stats"] [data-slot="model-usage-column"] { + flex: 0 0 var(--model-usage-mobile-bar-width); + } + + [data-page="stats"] [data-slot="model-usage-column"] { + padding-inline: 0; + } + + [data-page="stats"] [data-slot="model-usage-column"]::before { + width: 100%; + } + + [data-page="stats"] [data-slot="model-usage-bar"] { + right: 0; + left: 0; + } + + [data-page="stats"] [data-component="model-peer-list"] a { + grid-template-columns: 30px minmax(0, 1fr) minmax(64px, auto); + } + + [data-page="stats"] [data-component="model-peer-list"] svg { + display: none; + } + [data-page="stats"] [data-section="top-models"] [data-slot="chart-footer"] { display: flex; margin-top: 24px; @@ -3267,6 +4024,23 @@ right: 12px; left: 12px; } + + [data-page="stats"] [data-component="model-usage-chart"] [data-component="chart-tooltip"] { + position: fixed; + top: auto; + right: 12px; + bottom: 12px; + left: 12px; + z-index: 40; + width: auto; + min-width: 0; + transform: none; + } + + [data-page="stats"] [data-component="model-usage-chart"] [data-component="chart-tooltip"][data-placement] { + right: 12px; + left: 12px; + } } @media (max-width: 40rem) { diff --git a/packages/stats/app/src/routes/index.tsx b/packages/stats/app/src/routes/index.tsx index 3353fe850802..fb03b0e2f981 100644 --- a/packages/stats/app/src/routes/index.tsx +++ b/packages/stats/app/src/routes/index.tsx @@ -10,7 +10,6 @@ import ibmPlexMonoRegularLatin1 from "@ibm/plex/IBM-Plex-Mono/fonts/split/woff2/ import ibmPlexMonoMediumLatin1 from "@ibm/plex/IBM-Plex-Mono/fonts/split/woff2/IBMPlexMono-Medium-Latin1.woff2?url" import ibmPlexMonoSemiBoldLatin1 from "@ibm/plex/IBM-Plex-Mono/fonts/split/woff2/IBMPlexMono-SemiBold-Latin1.woff2?url" import ibmPlexMonoBoldLatin1 from "@ibm/plex/IBM-Plex-Mono/fonts/split/woff2/IBMPlexMono-Bold-Latin1.woff2?url" -import opencodeWordmarkDark from "../asset/logo-ornate-dark.svg" import { getStatsHomeData, type CacheRatioEntry, @@ -28,6 +27,16 @@ import { createEffect, createMemo, createSignal, For, onCleanup, onMount, Show, import { getRequestEvent } from "solid-js/web" import type { FeatureCollection, GeometryObject, GeoJsonProperties } from "geojson" import type { GeometryCollection, Topology } from "topojson-specification" +import { + applyThemePreference, + Footer, + getGitHubStars, + githubLink, + Header, + isThemePreference, + themeStorageKey, + type ThemePreference, +} from "./stats-shell" const products = ["All Users", "Zen", "Go"] as const const tokenProducts = ["Zen", "Go"] as const @@ -44,25 +53,6 @@ const statsHomeDescription = "OpenCode usage, market share, token cost, and sess const statsHomeFallbackUrl = "https://opencode.ai/stats/" const statsUnfurlPath = "banner.png" const statsUnfurlAlt = "OpenCode Stats wordmark on a dark patterned background" -const headerLinks = [ - { href: "#top-models", label: "Top Models" }, - { href: "#session-cost", label: "Session Cost" }, - { href: "#token-cost", label: "Token Cost" }, - { href: "#cache-ratio", label: "Cache Ratio" }, - { href: "#market-share", label: "Market Share" }, - { href: "#geo-breakdown", label: "Geo Breakdown" }, -] as const -const githubLink = { - href: "https://github.com/anomalyco/opencode", - apiHref: "https://api.github.com/repos/anomalyco/opencode", - label: "GitHub", - fallbackStars: "150K", - ariaLabel: "Star OpenCode on GitHub", -} -const compactNumberFormatter = new Intl.NumberFormat("en", { - notation: "compact", - maximumFractionDigits: 1, -}) const usageColors = [ "#ed6aff", "#a684ff", @@ -77,13 +67,6 @@ const usageColors = [ "#ff6467", ] const marketColors = ["#ed6aff", "#a684ff", "#7c86ff", "#51a2ff", "#00d3f2", "#00d5be", "#00bc7d", "#9ae600", "#ffb900"] -const themePreferences = ["dark", "light", "system"] as const -const themePreferenceLabels = { - dark: "Dark", - light: "Light", - system: "System", -} as const -const themeStorageKey = "opencode:stats-theme" const geoMapWidth = 960 const geoMapHeight = 430 const countryDisplayNames = new Intl.DisplayNames(["en"], { type: "region" }) @@ -91,7 +74,6 @@ const countryDisplayNames = new Intl.DisplayNames(["en"], { type: "region" }) type UsageProduct = (typeof products)[number] type TokenProduct = (typeof tokenProducts)[number] type UsageRange = (typeof ranges)[number] -type ThemePreference = (typeof themePreferences)[number] type IsoCountryCode = readonly [string, string, string] type WorldCountryProperties = GeoJsonProperties & { name?: string } type WorldTopology = Topology<{ countries: GeometryCollection }> @@ -127,23 +109,6 @@ const getData = query(async () => { return runtime.runPromise(getStatsHomeData()) }, "getStatsHomeData") -const getGitHubStars = query(async () => { - "use server" - return fetch(githubLink.apiHref, { - headers: { - Accept: "application/vnd.github+json", - "X-GitHub-Api-Version": "2022-11-28", - }, - }) - .then((response) => (response.ok ? response.json() : undefined)) - .then((body: unknown) => - body && typeof body === "object" && "stargazers_count" in body && typeof body.stargazers_count === "number" - ? compactNumberFormatter.format(body.stargazers_count) - : githubLink.fallbackStars, - ) - .catch(() => githubLink.fallbackStars) -}, "getGitHubStars") - export default function StatsHome() { const event = getRequestEvent() event?.response.headers.set("Cache-Control", "public, max-age=60, s-maxage=300, stale-while-revalidate=86400") @@ -224,20 +189,6 @@ function getStatsHomeUrl(base: string, requestUrl: string) { return url.toString() } -function isThemePreference(value: string | null): value is ThemePreference { - return value === "dark" || value === "light" || value === "system" -} - -function applyThemePreference(preference: ThemePreference) { - if (typeof document === "undefined") return - document.documentElement.dataset.statsTheme = preference - if (preference === "system") { - document.documentElement.style.removeProperty("color-scheme") - return - } - document.documentElement.style.setProperty("color-scheme", preference) -} - function Hero(props: { updatedAt: string | null }) { const [timeZone, setTimeZone] = createSignal("UTC") const [previousTimeZone, setPreviousTimeZone] = createSignal("UTC") @@ -977,10 +928,11 @@ function LeaderboardCard(props: { onActiveModelChange: (model: string | undefined) => void }) { return ( -
    -
    + ) } @@ -1807,415 +1759,11 @@ function formatSessionCost(value: number) { return `$${value.toFixed(4)}` } -function Header(props: { githubStars: string }) { - const [menuOpen, setMenuOpen] = createSignal(false) - const [menuViewport, setMenuViewport] = createSignal(false) - - createEffect(() => { - if (typeof window === "undefined") return - const media = window.matchMedia("(max-width: 74.999rem)") - const update = () => setMenuViewport(media.matches) - update() - media.addEventListener("change", update) - onCleanup(() => media.removeEventListener("change", update)) - }) - - createEffect(() => { - if (!menuOpen()) return - if (!menuViewport()) return - if (typeof document === "undefined") return - const page = document.querySelector('[data-page="stats"]') - const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth - const htmlOverflow = document.documentElement.style.overflow - const pagePaddingRight = page?.style.paddingRight - const bodyOverflow = document.body.style.overflow - document.documentElement.style.overflow = "hidden" - if (scrollbarWidth > 0 && page) page.style.paddingRight = `${scrollbarWidth}px` - document.body.style.overflow = "hidden" - onCleanup(() => { - document.documentElement.style.overflow = htmlOverflow - if (page && pagePaddingRight !== undefined) page.style.paddingRight = pagePaddingRight - document.body.style.overflow = bodyOverflow - }) - }) - - return ( -
    -
    - - - - -
    - - {githubLink.label} - [{props.githubStars}] - - - Try OpenCode - - -
    -
    - -
    - ) -} - -function StatsWordmark() { - return ( - - ) -} - -function StatsMark() { - return ( - - ) -} - -function OpenCodeMark() { - return ( - - ) -} - -function Footer(props: { - themePreference: ThemePreference - onThemePreferenceChange: (preference: ThemePreference) => void -}) { - const [subscribeOpen, setSubscribeOpen] = createSignal(false) - const modelStats = [ - { href: "#top-models", label: "Top Models" }, - { href: "#session-cost", label: "Session Cost" }, - { href: "#token-cost", label: "Token Cost" }, - { href: "#cache-ratio", label: "Cache Ratio" }, - { href: "#market-share", label: "Market Share" }, - { href: "#geo-breakdown", label: "Geo Breakdown" }, - ] - const legal = [ - { href: "https://opencode.ai/legal/terms-of-service", label: "Terms of service" }, - { href: "https://opencode.ai/legal/privacy-policy", label: "Privacy policy" }, - ] - const connect = [ - { href: "mailto:hello@opencode.ai", label: "Contact us" }, - { href: "https://opencode.ai/discord", label: "Community" }, - { href: "https://x.com/opencode", label: "X" }, - githubLink, - { href: "https://www.youtube.com/@anomaly-co", label: "YouTube" }, - ] - - return ( -
    - -
    - - - - - - -
    -

    Newsletter

    -

    Be the first to know about new releases.

    - -
    -
    -
    - ) -} - -function ThemePreferenceIcon(props: { preference: ThemePreference }) { - return ( - - ) -} - -function SubscribeModal(props: { onClose: () => void }) { - const [status, setStatus] = createSignal<"idle" | "pending" | "success" | "error">("idle") - const [message, setMessage] = createSignal("") - let input: HTMLInputElement | undefined - - onMount(() => { - if (typeof document === "undefined") return - const activeElement = document.activeElement instanceof HTMLElement ? document.activeElement : undefined - const htmlOverflow = document.documentElement.style.overflow - const bodyOverflow = document.body.style.overflow - document.documentElement.style.overflow = "hidden" - document.body.style.overflow = "hidden" - const focusTimeout = window.setTimeout(() => input?.focus(), 0) - const onKeyDown = (event: KeyboardEvent) => { - if (event.key === "Escape") props.onClose() - } - document.addEventListener("keydown", onKeyDown) - onCleanup(() => { - window.clearTimeout(focusTimeout) - document.documentElement.style.overflow = htmlOverflow - document.body.style.overflow = bodyOverflow - document.removeEventListener("keydown", onKeyDown) - activeElement?.focus() - }) - }) - - return ( -
    - - ) -} - -function newsletterErrorMessage(response: Response) { - return response.json().then( - (body: unknown) => - body && typeof body === "object" && "error" in body && typeof body.error === "string" - ? body.error - : "Failed to subscribe", - () => "Failed to subscribe", - ) -} - -function FooterColumn(props: { title: string; links: { href: string; label: string }[] }) { - return ( -
    -

    {props.title}

    - -
    - ) +function modelSlug(value: string) { + return value + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") + .replace(/-{2,}/g, "-") } diff --git a/packages/stats/app/src/routes/model-catalog.ts b/packages/stats/app/src/routes/model-catalog.ts new file mode 100644 index 000000000000..8adddd39da7f --- /dev/null +++ b/packages/stats/app/src/routes/model-catalog.ts @@ -0,0 +1,224 @@ +import { query } from "@solidjs/router" + +export const modelCatalogSourceUrl = "https://models.dev/models.json" + +export type ModelCatalogEntry = { + id: string + lab: string + slug: string + name: string + family?: string + knowledge?: string + releaseDate?: string + lastUpdated?: string + limit?: { context?: number; output?: number } + modalities: { input: string[]; output: string[] } + openWeights: boolean + reasoning: boolean + toolCall: boolean + attachment: boolean + temperature: boolean + weights: { label: string; url: string }[] + benchmarks: ModelCatalogBenchmark[] +} + +export type ModelCatalogBenchmark = { + name: string + score: number + metric?: string + harness?: string + variant?: string + dataset?: string + version?: string + source?: string +} + +export type ModelCatalogLab = { + id: string + name: string + models: ModelCatalogEntry[] +} + +export type ModelCatalog = { + models: ModelCatalogEntry[] + labs: ModelCatalogLab[] +} + +export const getModelCatalog = query(async () => { + "use server" + const payload = await fetch(modelCatalogSourceUrl) + .then((response): Promise => (response.ok ? (response.json() as Promise) : Promise.resolve())) + .catch(() => undefined) + return buildModelCatalog(payload) +}, "getModelCatalog") + +export function findModelCatalogEntry(catalog: ModelCatalog, model: string, lab?: string) { + const normalizedId = lab ? `${catalogSlug(lab)}/${catalogSlug(model)}` : model.trim().toLowerCase() + const leaf = catalogSlug(model) + return ( + catalog.models.find((entry) => entry.id.toLowerCase() === normalizedId) ?? + catalog.models.find((entry) => (lab ? entry.lab === catalogSlug(lab) : true) && entry.slug === leaf) ?? + catalog.models.find((entry) => entry.slug === leaf) + ) +} + +export function findModelCatalogLab(catalog: ModelCatalog, lab: string) { + const id = catalogSlug(lab) + return catalog.labs.find((entry) => entry.id === id) +} + +export function formatCatalogLabName(lab: string) { + const known: Record = { + alibaba: "Alibaba", + anthropic: "Anthropic", + cohere: "Cohere", + deepseek: "DeepSeek", + google: "Google", + meta: "Meta", + minimax: "MiniMax", + mistral: "Mistral", + moonshotai: "Moonshot", + openai: "OpenAI", + perplexity: "Perplexity", + stepfun: "StepFun", + tencent: "Tencent", + xai: "xAI", + xiaomi: "Xiaomi", + zai: "Z.ai", + zhipuai: "Zhipu", + } + return known[catalogSlug(lab)] ?? lab.replace(/[-_]/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()) +} + +export function catalogSlug(value: string) { + return value + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") + .replace(/-{2,}/g, "-") +} + +function buildModelCatalog(payload: unknown): ModelCatalog { + const models = (Array.isArray(payload) ? payload : isRecord(payload) ? Object.values(payload) : []) + .flatMap(readModelCatalogEntry) + .toSorted((a, b) => a.lab.localeCompare(b.lab) || displayDateTime(b.releaseDate) - displayDateTime(a.releaseDate)) + return { + models, + labs: Object.values( + models.reduce>((result, model) => { + result[model.lab] = { + id: model.lab, + name: formatCatalogLabName(model.lab), + models: [...(result[model.lab]?.models ?? []), model], + } + return result + }, {}), + ).toSorted((a, b) => a.name.localeCompare(b.name)), + } +} + +function readModelCatalogEntry(value: unknown): ModelCatalogEntry[] { + if (!isRecord(value)) return [] + const id = stringValue(value.id) + const name = stringValue(value.name) + const lab = id?.split("/")[0] + const slug = id?.split("/").slice(1).join("/") + if (!id || !name || !lab || !slug) return [] + return [ + { + id, + lab: catalogSlug(lab), + slug: catalogSlug(slug), + name, + family: stringValue(value.family), + knowledge: stringValue(value.knowledge), + releaseDate: stringValue(value.release_date), + lastUpdated: stringValue(value.last_updated), + limit: readCatalogLimit(value.limit), + modalities: readCatalogModalities(value.modalities), + openWeights: booleanValue(value.open_weights), + reasoning: booleanValue(value.reasoning), + toolCall: booleanValue(value.tool_call), + attachment: booleanValue(value.attachment), + temperature: booleanValue(value.temperature), + weights: readCatalogWeights(value.weights), + benchmarks: readCatalogBenchmarks(value.benchmarks), + }, + ] +} + +function readCatalogLimit(value: unknown) { + if (!isRecord(value)) return undefined + return { + context: numberValue(value.context), + output: numberValue(value.output), + } +} + +function readCatalogModalities(value: unknown) { + if (!isRecord(value)) return { input: [], output: [] } + return { + input: stringArrayValue(value.input), + output: stringArrayValue(value.output), + } +} + +function readCatalogWeights(value: unknown) { + if (!Array.isArray(value)) return [] + return value.flatMap((item) => { + if (!isRecord(item)) return [] + const label = stringValue(item.label) + const url = stringValue(item.url) + return label && url ? [{ label, url }] : [] + }) +} + +function readCatalogBenchmarks(value: unknown) { + if (!Array.isArray(value)) return [] + return value.flatMap((item) => { + if (!isRecord(item)) return [] + const name = stringValue(item.name) + const score = numberValue(item.score) + return name && score !== undefined + ? [ + { + name, + score, + metric: stringValue(item.metric), + harness: stringValue(item.harness), + variant: stringValue(item.variant), + dataset: stringValue(item.dataset), + version: stringValue(item.version), + source: stringValue(item.source), + }, + ] + : [] + }) +} + +function displayDateTime(value: string | undefined) { + return value ? new Date(value).getTime() || 0 : 0 +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value) +} + +function stringValue(value: unknown) { + return typeof value === "string" && value.trim() ? value : undefined +} + +function numberValue(value: unknown) { + return typeof value === "number" && Number.isFinite(value) ? value : undefined +} + +function booleanValue(value: unknown) { + return value === true +} + +function stringArrayValue(value: unknown) { + return Array.isArray(value) + ? value.filter((item): item is string => typeof item === "string" && item.trim() !== "") + : [] +} diff --git a/packages/stats/app/src/routes/stats-shell.tsx b/packages/stats/app/src/routes/stats-shell.tsx new file mode 100644 index 000000000000..80fdc5619bb1 --- /dev/null +++ b/packages/stats/app/src/routes/stats-shell.tsx @@ -0,0 +1,493 @@ +import opencodeWordmarkDark from "../asset/logo-ornate-dark.svg" +import { query } from "@solidjs/router" +import { createEffect, createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-js" + +export type HeaderLink = { href: string; label: string } + +export const headerLinks = [ + { href: "#top-models", label: "Top Models" }, + { href: "#leaderboard", label: "Leaderboard" }, + { href: "#session-cost", label: "Session Cost" }, + { href: "#token-cost", label: "Token Cost" }, + { href: "#cache-ratio", label: "Cache Ratio" }, + { href: "#market-share", label: "Market Share" }, + { href: "#geo-breakdown", label: "Geo Breakdown" }, +] as const +export const githubLink = { + href: "https://github.com/anomalyco/opencode", + apiHref: "https://api.github.com/repos/anomalyco/opencode", + label: "GitHub", + fallbackStars: "150K", + ariaLabel: "Star OpenCode on GitHub", +} +export const themePreferences = ["dark", "light", "system"] as const +export const themeStorageKey = "opencode:stats-theme" +export type ThemePreference = (typeof themePreferences)[number] + +const compactNumberFormatter = new Intl.NumberFormat("en", { + notation: "compact", + maximumFractionDigits: 1, +}) +const themePreferenceLabels = { + dark: "Dark", + light: "Light", + system: "System", +} as const + +export const getGitHubStars = query(async () => { + "use server" + return fetch(githubLink.apiHref, { + headers: { + Accept: "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + }, + }) + .then((response) => (response.ok ? response.json() : undefined)) + .then((body: unknown) => + body && typeof body === "object" && "stargazers_count" in body && typeof body.stargazers_count === "number" + ? compactNumberFormatter.format(body.stargazers_count) + : githubLink.fallbackStars, + ) + .catch(() => githubLink.fallbackStars) +}, "getGitHubStars") + +export function isThemePreference(value: string | null): value is ThemePreference { + return value === "dark" || value === "light" || value === "system" +} + +export function applyThemePreference(preference: ThemePreference) { + if (typeof document === "undefined") return + document.documentElement.dataset.statsTheme = preference + if (preference === "system") { + document.documentElement.style.removeProperty("color-scheme") + return + } + document.documentElement.style.setProperty("color-scheme", preference) +} + +export function Header(props: { githubStars: string; links?: readonly HeaderLink[]; brandHref?: string }) { + const [menuOpen, setMenuOpen] = createSignal(false) + const [menuViewport, setMenuViewport] = createSignal(false) + const links = createMemo(() => props.links ?? headerLinks) + + createEffect(() => { + if (typeof window === "undefined") return + const media = window.matchMedia("(max-width: 74.999rem)") + const update = () => setMenuViewport(media.matches) + update() + media.addEventListener("change", update) + onCleanup(() => media.removeEventListener("change", update)) + }) + + createEffect(() => { + if (!menuOpen()) return + if (!menuViewport()) return + if (typeof document === "undefined") return + const page = document.querySelector('[data-page="stats"]') + const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth + const htmlOverflow = document.documentElement.style.overflow + const pagePaddingRight = page?.style.paddingRight + const bodyOverflow = document.body.style.overflow + document.documentElement.style.overflow = "hidden" + if (scrollbarWidth > 0 && page) page.style.paddingRight = `${scrollbarWidth}px` + document.body.style.overflow = "hidden" + onCleanup(() => { + document.documentElement.style.overflow = htmlOverflow + if (page && pagePaddingRight !== undefined) page.style.paddingRight = pagePaddingRight + document.body.style.overflow = bodyOverflow + }) + }) + + return ( +
    +
    + + + + +
    + + {githubLink.label} + [{props.githubStars}] + + + Try OpenCode + + +
    +
    + +
    + ) +} + +function StatsWordmark() { + return ( + + ) +} + +function StatsMark() { + return ( + + ) +} + +function OpenCodeMark() { + return ( + + ) +} + +export function Footer(props: { + themePreference: ThemePreference + onThemePreferenceChange: (preference: ThemePreference) => void + links?: readonly HeaderLink[] +}) { + const [subscribeOpen, setSubscribeOpen] = createSignal(false) + const modelStats = props.links ?? [ + { href: "#top-models", label: "Top Models" }, + { href: "#leaderboard", label: "Leaderboard" }, + { href: "#session-cost", label: "Session Cost" }, + { href: "#token-cost", label: "Token Cost" }, + { href: "#cache-ratio", label: "Cache Ratio" }, + { href: "#market-share", label: "Market Share" }, + { href: "#geo-breakdown", label: "Geo Breakdown" }, + ] + const legal = [ + { href: "https://opencode.ai/legal/terms-of-service", label: "Terms of service" }, + { href: "https://opencode.ai/legal/privacy-policy", label: "Privacy policy" }, + ] + const connect = [ + { href: "mailto:hello@opencode.ai", label: "Contact us" }, + { href: "https://opencode.ai/discord", label: "Community" }, + { href: "https://x.com/opencode", label: "X" }, + githubLink, + { href: "https://www.youtube.com/@anomaly-co", label: "YouTube" }, + ] + + return ( +
    + +
    + + + + + + +
    +

    Newsletter

    +

    Be the first to know about new releases.

    + +
    +
    +
    + ) +} + +function SectionBridge(props: { label: string; href: string }) { + return ( + + LEAN MORE + + {props.label} + + + ) +} + +function ThemePreferenceIcon(props: { preference: ThemePreference }) { + return ( + + ) +} + +function SubscribeModal(props: { onClose: () => void }) { + const [status, setStatus] = createSignal<"idle" | "pending" | "success" | "error">("idle") + const [message, setMessage] = createSignal("") + let input: HTMLInputElement | undefined + + onMount(() => { + if (typeof document === "undefined") return + const activeElement = document.activeElement instanceof HTMLElement ? document.activeElement : undefined + const htmlOverflow = document.documentElement.style.overflow + const bodyOverflow = document.body.style.overflow + document.documentElement.style.overflow = "hidden" + document.body.style.overflow = "hidden" + const focusTimeout = window.setTimeout(() => input?.focus(), 0) + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") props.onClose() + } + document.addEventListener("keydown", onKeyDown) + onCleanup(() => { + window.clearTimeout(focusTimeout) + document.documentElement.style.overflow = htmlOverflow + document.body.style.overflow = bodyOverflow + document.removeEventListener("keydown", onKeyDown) + activeElement?.focus() + }) + }) + + return ( +
    + + ) +} + +function newsletterErrorMessage(response: Response) { + return response.json().then( + (body: unknown) => + body && typeof body === "object" && "error" in body && typeof body.error === "string" + ? body.error + : "Failed to subscribe", + () => "Failed to subscribe", + ) +} + +function FooterColumn(props: { title: string; links: readonly { href: string; label: string }[] }) { + return ( +
    +

    {props.title}

    + +
    + ) +} diff --git a/packages/stats/core/src/domain/geo.ts b/packages/stats/core/src/domain/geo.ts index 25c5d90145fd..2512ffa4988b 100644 --- a/packages/stats/core/src/domain/geo.ts +++ b/packages/stats/core/src/domain/geo.ts @@ -37,7 +37,10 @@ export type GeoStatMetric = { export declare namespace GeoStatRepo { export interface Service { - readonly listDaily: () => Effect.Effect + readonly listDaily: (opts?: { + readonly provider?: string + readonly model?: string + }) => Effect.Effect readonly listByPeriod: (opts: { readonly grain: string readonly periodKey: string @@ -59,7 +62,16 @@ export class GeoStatRepo extends Context.Service db @@ -74,15 +86,7 @@ export class GeoStatRepo extends Context.Service DatabaseError.make({ cause }), }) diff --git a/packages/stats/core/src/domain/home.ts b/packages/stats/core/src/domain/home.ts index 097a3814ec9f..cc7a3300bdbb 100644 --- a/packages/stats/core/src/domain/home.ts +++ b/packages/stats/core/src/domain/home.ts @@ -9,11 +9,78 @@ export type TokenProduct = "Zen" | "Go" | "Enterprise" export type UsageRange = "1D" | "1W" | "2W" | "1M" | "2M" | "3M" | "YTD" | "ALL" export type UsagePoint = { date: string; segments: { model: string; value: number }[] } export type MarketDay = { date: string; total: number; authors: { author: string; share: number; tokens: number }[] } -export type LeaderboardEntry = { model: string; author: string; tokens: number; change: number; rank: number } +export type LeaderboardEntry = { + model: string + provider: string + author: string + tokens: number + change: number + rank: number +} export type TokenCostEntry = { model: string; total: number; input: number; output: number; cached: number } export type CacheRatioEntry = { model: string; ratio: number; cached: number; uncached: number; total: number } export type SessionCostEntry = { model: string; cost: number; tokens: number } export type CountryEntry = { country: string; continent: string; tokens: number; share: number; rank: number } +export type ModelUsagePoint = { date: string; tokens: number; sessions: number; cost: number } +export type ModelMixEntry = { label: string; tokens: number; share: number } +export type ModelProductEntry = { product: string; tokens: number; sessions: number; share: number } +export type ModelPeerEntry = { + model: string + provider: string + author: string + rank: number + tokens: number + share: number + slug: string +} +export type LabUsageModelEntry = { + model: string + provider: string + author: string + tokens: number + share: number + slug: string +} +export type StatsModelData = { + updatedAt: string | null + model: string + slug: string + provider: string + author: string + rank: number + previousRank: number | null + totalModels: number + tokenShare: number + tokenChange: number + totals: { + sessions: number + tokens: number + cost: number + tokensPerSession: number + costPerSession: number + costPerMillion: number + cacheRatio: number + } + usage: ModelUsagePoint[] + tokenMix: ModelMixEntry[] + productMix: ModelProductEntry[] + country: Record + peers: ModelPeerEntry[] +} +export type StatsLabData = { + updatedAt: string | null + provider: string + author: string + tokenShare: number + tokenChange: number + totals: { + sessions: number + tokens: number + models: number + } + usage: ModelUsagePoint[] + models: LabUsageModelEntry[] +} export type StatsHomeData = { updatedAt: string | null usage: Record> @@ -74,6 +141,35 @@ export const getStatsHomeData: () => Effect.Effect< return buildStatsHomeData(modelRows, providerRows, geoRows) }) +export const getStatsModelData: ( + model: string, + provider?: string, +) => Effect.Effect = Effect.fn("StatsModel.getData")( + function* (model, provider) { + const modelStats = yield* ModelStatRepo + const geoStats = yield* GeoStatRepo + const modelRows = yield* modelStats.listDaily() + const normalized = modelRows.flatMap(normalizeStatRow) + const resolvedModel = resolveModelName(model, normalized, provider) + if (!resolvedModel) return null + return buildStatsModelData( + resolvedModel, + modelRows, + yield* geoStats.listDaily({ + model: resolvedModel, + provider: resolveModelProvider(resolvedModel, normalized, provider), + }), + provider, + ) + }, +) + +export const getStatsLabData: (provider: string) => Effect.Effect = + Effect.fn("StatsLab.getData")(function* (provider) { + const modelStats = yield* ModelStatRepo + return buildStatsLabData(provider, yield* modelStats.listDaily()) + }) + function buildStatsHomeData( modelRows: ModelStatMetric[], providerRows: ProviderStatMetric[], @@ -111,6 +207,119 @@ function buildStatsHomeData( } } +function buildStatsModelData( + modelParam: string, + modelRows: ModelStatMetric[], + geoRows: GeoStatMetric[], + providerParam?: string, +): StatsModelData | null { + const normalized = modelRows.flatMap(normalizeStatRow) + const geo = geoRows.flatMap(normalizeGeoRow) + if (normalized.length === 0) return null + + const model = resolveModelName(modelParam, normalized, providerParam) + if (!model) return null + + const modelScopedRows = normalized.filter((row) => row.model === model) + const earliest = Math.min(...normalized.map((row) => row.periodStart)) + const latest = Math.max(...normalized.map((row) => row.periodStart)) + const latestUpdate = Math.max(...modelScopedRows.map((row) => row.updatedAt)) + const window = getWindow("2M", earliest, latest) + const currentRows = rowsForProduct(modelScopedRows, "All Users", window.start, window.end) + const previousRows = rowsForProduct(modelScopedRows, "All Users", window.previousStart, window.previousEnd) + const current = combineRowsForModel(model, currentRows) + const previous = combineRowsForModel(model, previousRows) + const peers = aggregateByModelName(rowsForProduct(normalized, "All Users", window.start, window.end)) + .filter((item) => item.totalTokens > 0) + .toSorted((a, b) => b.totalTokens - a.totalTokens || a.model.localeCompare(b.model)) + const previousPeers = aggregateByModelName( + rowsForProduct(normalized, "All Users", window.previousStart, window.previousEnd), + ) + .filter((item) => item.totalTokens > 0) + .toSorted((a, b) => b.totalTokens - a.totalTokens || a.model.localeCompare(b.model)) + const rank = Math.max(1, peers.findIndex((item) => item.model === model) + 1) + const previousRankIndex = previousPeers.findIndex((item) => item.model === model) + const totalTokens = peers.reduce((sum, item) => sum + item.totalTokens, 0) + + return { + updatedAt: Number.isFinite(latestUpdate) ? new Date(latestUpdate).toISOString() : null, + model, + slug: modelSlug(model), + provider: current.provider, + author: formatProvider(current.provider), + rank, + previousRank: previousRankIndex >= 0 ? previousRankIndex + 1 : null, + totalModels: peers.length, + tokenShare: totalTokens > 0 ? round((current.totalTokens / totalTokens) * 100, 2) : 0, + tokenChange: percentChange(current.totalTokens, previous.totalTokens), + totals: { + sessions: current.sessions, + tokens: current.totalTokens, + cost: round(microcentsToDollars(current.totalCostMicrocents), 2), + tokensPerSession: current.sessions > 0 ? Math.round(current.totalTokens / current.sessions) : 0, + costPerSession: + current.sessions > 0 ? round(microcentsToDollars(current.totalCostMicrocents) / current.sessions, 4) : 0, + costPerMillion: costPerMillion(current.totalCostMicrocents, current.totalTokens), + cacheRatio: + current.inputTokens + current.cacheReadTokens > 0 + ? round((current.cacheReadTokens / (current.inputTokens + current.cacheReadTokens)) * 100, 1) + : 0, + }, + usage: buildModelUsage(currentRows, window, "2M"), + tokenMix: buildModelTokenMix(current), + productMix: buildModelProductMix(modelScopedRows, window, current), + country: createRangeRecord((range) => buildCountryStats(geo, getWindow(range, earliest, latest))), + peers: buildModelPeers(peers, rank, totalTokens), + } +} + +function buildStatsLabData(providerParam: string, modelRows: ModelStatMetric[]): StatsLabData | null { + const normalized = modelRows.flatMap(normalizeStatRow) + if (normalized.length === 0) return null + + const provider = resolveProviderName(providerParam, normalized) + if (!provider) return null + + const providerRows = normalized.filter((row) => providerMatches(row.provider, provider)) + if (providerRows.length === 0) return null + + const earliest = Math.min(...normalized.map((row) => row.periodStart)) + const latest = Math.max(...normalized.map((row) => row.periodStart)) + const latestUpdate = Math.max(...providerRows.map((row) => row.updatedAt)) + const window = getWindow("2M", earliest, latest) + const currentRows = rowsForProduct(providerRows, "All Users", window.start, window.end) + const previousRows = rowsForProduct(providerRows, "All Users", window.previousStart, window.previousEnd) + const current = combineRowsForModel("", currentRows) + const previous = combineRowsForModel("", previousRows) + const allCurrent = aggregateByModel(rowsForProduct(normalized, "All Users", window.start, window.end)) + const totalTokens = allCurrent.reduce((sum, item) => sum + item.totalTokens, 0) + const models = aggregateByModel(currentRows) + .filter((item) => item.totalTokens > 0) + .toSorted((a, b) => b.totalTokens - a.totalTokens || a.model.localeCompare(b.model)) + + return { + updatedAt: Number.isFinite(latestUpdate) ? new Date(latestUpdate).toISOString() : null, + provider, + author: formatProvider(provider), + tokenShare: totalTokens > 0 ? round((current.totalTokens / totalTokens) * 100, 2) : 0, + tokenChange: percentChange(current.totalTokens, previous.totalTokens), + totals: { + sessions: current.sessions, + tokens: current.totalTokens, + models: models.length, + }, + usage: buildModelUsage(currentRows, window, "2M"), + models: models.map((item) => ({ + model: item.model, + provider: item.provider, + author: formatProvider(item.provider), + tokens: item.totalTokens, + share: current.totalTokens > 0 ? round((item.totalTokens / current.totalTokens) * 100, 2) : 0, + slug: modelSlug(item.model), + })), + } +} + function emptyStatsHomeData(): StatsHomeData { return { updatedAt: null, @@ -160,6 +369,7 @@ function buildLeaderboard(rows: StatMetricRow[], product: UsageProduct, window: .slice(0, 18) .map((item, index) => ({ model: item.model, + provider: item.provider, author: formatProvider(item.provider), tokens: Math.round(item.totalTokens / 1_000_000_000), change: percentChange(item.totalTokens, previous.get(modelKey(item.provider, item.model)) ?? 0), @@ -261,6 +471,66 @@ function buildSessionCost(rows: StatMetricRow[], product: TokenProduct, window: .slice(0, 17) } +function buildModelUsage(rows: StatMetricRow[], window: DateWindow, range: UsageRange) { + return createBuckets(window, range).map((bucket) => { + const aggregate = combineRowsForModel( + "", + rows.filter((row) => row.periodStart >= bucket.start && row.periodStart < bucket.end), + ) + return { + date: bucket.label, + tokens: aggregate.totalTokens, + sessions: aggregate.sessions, + cost: round(microcentsToDollars(aggregate.totalCostMicrocents), 2), + } + }) +} + +function buildModelTokenMix(aggregate: ModelAggregate): ModelMixEntry[] { + const items = [ + { label: "Input", tokens: aggregate.inputTokens }, + { label: "Output", tokens: aggregate.outputTokens }, + { label: "Reasoning", tokens: aggregate.reasoningTokens }, + { label: "Cached", tokens: aggregate.cacheReadTokens }, + ].filter((item) => item.tokens > 0) + const total = items.reduce((sum, item) => sum + item.tokens, 0) + if (total === 0) return [] + return items.map((item) => ({ ...item, share: round((item.tokens / total) * 100, 1) })) +} + +function buildModelProductMix( + rows: StatMetricRow[], + window: DateWindow, + fallback: ModelAggregate, +): ModelProductEntry[] { + const products = ["Go", "Zen", "Enterprise"] as const + const items = products.flatMap((product) => { + const aggregate = combineRowsForModel( + fallback.model, + rows.filter((row) => row.tier === product && row.periodStart >= window.start && row.periodStart < window.end), + ) + if (aggregate.totalTokens === 0) return [] + return [{ product, tokens: aggregate.totalTokens, sessions: aggregate.sessions }] + }) + const total = items.reduce((sum, item) => sum + item.tokens, 0) + if (total > 0) return items.map((item) => ({ ...item, share: round((item.tokens / total) * 100, 1) })) + if (fallback.totalTokens === 0) return [] + return [{ product: "All Users", tokens: fallback.totalTokens, sessions: fallback.sessions, share: 100 }] +} + +function buildModelPeers(peers: ModelAggregate[], rank: number, totalTokens: number): ModelPeerEntry[] { + const start = Math.max(0, Math.min(rank - 4, Math.max(peers.length - 7, 0))) + return peers.slice(start, start + 7).map((item, index) => ({ + model: item.model, + provider: item.provider, + author: formatProvider(item.provider), + rank: start + index + 1, + tokens: item.totalTokens, + share: totalTokens > 0 ? round((item.totalTokens / totalTokens) * 100, 2) : 0, + slug: modelSlug(item.model), + })) +} + function rowsForProduct( rows: T[], product: UsageProduct, @@ -285,6 +555,15 @@ function aggregateByModel(rows: StatMetricRow[]) { ) } +function aggregateByModelName(rows: StatMetricRow[]) { + return Object.values( + rows.reduce>((result, row) => { + result[row.model] = combineModelAggregate(result[row.model], row) + return result + }, {}), + ) +} + function aggregateByProvider(rows: ProviderMetricRow[]) { return Object.values( rows.reduce>((result, row) => { @@ -310,6 +589,27 @@ function aggregateByCountry(rows: GeoMetricRow[]) { ) } +function combineRowsForModel(model: string, rows: StatMetricRow[]): ModelAggregate { + const aggregate = rows.reduce( + (result, row) => combineModelAggregate(result, row), + undefined, + ) + if (aggregate) return { ...aggregate, model: model || aggregate.model } + return { + model, + provider: "unknown", + sessions: 0, + inputTokens: 0, + outputTokens: 0, + reasoningTokens: 0, + cacheReadTokens: 0, + totalTokens: 0, + inputCostMicrocents: 0, + outputCostMicrocents: 0, + totalCostMicrocents: 0, + } +} + function combineModelAggregate(current: ModelAggregate | undefined, row: StatMetricRow): ModelAggregate { return { model: row.model, @@ -501,6 +801,47 @@ function formatProvider(provider: string) { return known[normalized] ?? provider.replace(/[-_]/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()) } +function resolveModelName(modelParam: string, rows: StatMetricRow[], providerParam?: string) { + const input = modelParam.trim() + if (!input) return undefined + const normalizedInput = input.toLowerCase() + const inputSlug = modelSlug(input) + const candidates = providerParam + ? aggregateByModel(rows).filter((item) => providerMatches(item.provider, providerParam)) + : aggregateByModelName(rows) + return candidates + .filter((item) => item.model.toLowerCase() === normalizedInput || modelSlug(item.model) === inputSlug) + .toSorted((a, b) => b.totalTokens - a.totalTokens || a.model.localeCompare(b.model))[0]?.model +} + +function resolveModelProvider(model: string, rows: StatMetricRow[], providerParam?: string) { + return aggregateByModel(rows) + .filter((item) => item.model === model && (!providerParam || providerMatches(item.provider, providerParam))) + .toSorted((a, b) => b.totalTokens - a.totalTokens || a.provider.localeCompare(b.provider))[0]?.provider +} + +function providerMatches(provider: string, providerParam: string) { + return modelSlug(provider) === modelSlug(providerParam) +} + +function resolveProviderName(providerParam: string, rows: StatMetricRow[]) { + const input = providerParam.trim() + if (!input) return undefined + const inputSlug = modelSlug(input) + return aggregateByModel(rows) + .filter((item) => modelSlug(item.provider) === inputSlug) + .toSorted((a, b) => b.totalTokens - a.totalTokens || a.provider.localeCompare(b.provider))[0]?.provider +} + +export function modelSlug(value: string) { + return value + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") + .replace(/-{2,}/g, "-") +} + function modelKey(provider: string, model: string) { return `${provider}\u0000${model}` }