wip: stats
This commit is contained in:
@@ -627,6 +627,7 @@
|
||||
"name": "@opencode-ai/stats-core",
|
||||
"version": "1.14.50",
|
||||
"dependencies": {
|
||||
"@planetscale/database": "1.19.0",
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
},
|
||||
|
||||
+1
-1
@@ -87,7 +87,7 @@ export const app = new sst.aws.SolidStart("Stats", {
|
||||
////////////////
|
||||
|
||||
export const statSync = new sst.aws.Cron("StatsSync", {
|
||||
schedule: "rate(1 hour)",
|
||||
schedule: "rate(1 minute)",
|
||||
function: {
|
||||
handler: "packages/stats/core/src/cron/stat.handler",
|
||||
runtime: "nodejs22.x",
|
||||
|
||||
@@ -295,6 +295,30 @@
|
||||
display: grid;
|
||||
}
|
||||
|
||||
[data-page="rankings"] [data-component="empty-state"] {
|
||||
display: grid;
|
||||
place-content: center;
|
||||
gap: 12px;
|
||||
min-height: 280px;
|
||||
padding: 32px;
|
||||
background: var(--rankings-layer);
|
||||
border: 1px solid var(--rankings-line);
|
||||
color: var(--rankings-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
[data-page="rankings"] [data-component="empty-state"] strong {
|
||||
color: var(--rankings-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
[data-page="rankings"] [data-component="empty-state"] p {
|
||||
max-width: 34rem;
|
||||
color: var(--rankings-muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
[data-page="rankings"] [data-component="usage-chart"] svg,
|
||||
[data-page="rankings"] [data-component="country-map"] svg {
|
||||
display: block;
|
||||
|
||||
@@ -1,191 +1,75 @@
|
||||
import "./rankings.css"
|
||||
import { Meta, Title } from "@solidjs/meta"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import {
|
||||
getRankingsData as fetchRankingsData,
|
||||
type LeaderboardEntry,
|
||||
type MarketDay,
|
||||
type RankingsData,
|
||||
type SessionCostEntry,
|
||||
type TokenCostEntry,
|
||||
type UsagePoint,
|
||||
} from "@opencode-ai/stats-core/domain/ranking"
|
||||
import { runtime } from "@opencode-ai/stats-core/runtime"
|
||||
import { createAsync, query } from "@solidjs/router"
|
||||
import { scaleBand, scaleLinear } from "d3-scale"
|
||||
import { createMemo, createSignal, For, Show, type JSX } from "solid-js"
|
||||
import { getRequestEvent } from "solid-js/web"
|
||||
|
||||
const products = ["All Users", "Zen", "Go", "Enterprise"] as const
|
||||
const tokenProducts = ["Zen", "Go", "Enterprise"] as const
|
||||
const ranges = ["1D", "1W", "1M", "3M", "YTD", "ALL"] as const
|
||||
const usageColors = ["#ff5d64", "#ff8a00", "#8bef00", "#12c8b3", "#18c7dc", "#6c7dff", "#9d73f7"]
|
||||
const marketColors = ["#ed6aff", "#a684ff", "#7c86ff", "#51a2ff", "#00d3f2", "#00d5be", "#00bc7d", "#9ae600", "#ffb900"]
|
||||
const usageModels = [
|
||||
"minimax-m2.5-free",
|
||||
"big-pickle",
|
||||
"kimi-k2.5",
|
||||
"gpt-5-nano",
|
||||
"nemotron-3-super-free",
|
||||
"claude-opus-4-6",
|
||||
"Other",
|
||||
] as const
|
||||
|
||||
type UsageProduct = (typeof products)[number]
|
||||
type TokenProduct = (typeof tokenProducts)[number]
|
||||
type UsageRange = (typeof ranges)[number]
|
||||
type UsagePoint = { date: string; segments: { model: string; value: number }[] }
|
||||
type MarketDay = { date: string; total: number; authors: { author: string; share: number; tokens: number }[] }
|
||||
type LeaderboardEntry = {
|
||||
model: string
|
||||
author: string
|
||||
tokens: number
|
||||
change: number
|
||||
products: readonly UsageProduct[]
|
||||
}
|
||||
|
||||
const usageValues = [
|
||||
[0.42, 0.34, 0.22, 0.18, 0.16, 0.1, 0.58],
|
||||
[0.76, 0.66, 0.5, 0.34, 0.27, 0.2, 1.18],
|
||||
[0.92, 0.72, 0.48, 0.32, 0.26, 0.19, 1.11],
|
||||
[0.58, 0.46, 0.35, 0.26, 0.22, 0.17, 0.76],
|
||||
[1.8, 1.5, 0.27, 0.08, 0.23, 0.12, 0.75],
|
||||
[1.74, 1.38, 1.02, 0.78, 0.68, 0.56, 1.34],
|
||||
[1.94, 1.58, 1.18, 0.88, 0.73, 0.64, 1.48],
|
||||
] as const
|
||||
|
||||
const usageDates = {
|
||||
"1D": ["12AM", "4AM", "8AM", "12PM", "4PM", "8PM", "NOW"],
|
||||
"1W": ["MAR 6", "MAR 7", "MAR 8", "MAR 9", "MAR 10", "MAR 11", "MAR 12"],
|
||||
"1M": ["FEB 14", "FEB 19", "FEB 24", "MAR 1", "MAR 6", "MAR 11", "MAR 16"],
|
||||
"3M": ["DEC 16", "JAN 1", "JAN 17", "FEB 2", "FEB 18", "MAR 6", "MAR 16"],
|
||||
YTD: ["JAN", "JAN", "FEB", "FEB", "MAR", "MAR", "NOW"],
|
||||
ALL: ["2024", "Q3", "Q4", "JAN", "FEB", "MAR", "NOW"],
|
||||
} satisfies Record<UsageRange, readonly string[]>
|
||||
|
||||
const usageProductMultipliers = {
|
||||
"All Users": 1,
|
||||
Zen: 0.46,
|
||||
Go: 0.34,
|
||||
Enterprise: 0.22,
|
||||
} satisfies Record<UsageProduct, number>
|
||||
|
||||
const usageRangeMultipliers = {
|
||||
"1D": 0.14,
|
||||
"1W": 1,
|
||||
"1M": 3.8,
|
||||
"3M": 10.6,
|
||||
YTD: 18.4,
|
||||
ALL: 31.2,
|
||||
} satisfies Record<UsageRange, number>
|
||||
|
||||
const marketTotals = {
|
||||
"1D": [0.32, 0.61, 0.68, 0.47, 0.51, 0.84, 0.9],
|
||||
"1W": [2, 3.9, 4, 2.8, 2.8, 7.5, 7.5],
|
||||
"1M": [8.4, 10.6, 12.8, 11.9, 13.2, 15.7, 16.1],
|
||||
"3M": [22.4, 28.1, 32.7, 30.4, 34.8, 39.9, 42.2],
|
||||
YTD: [34.8, 43.1, 52.6, 58.2, 64.1, 72.8, 79.4],
|
||||
ALL: [91.2, 118.4, 142.7, 166.3, 188.9, 221.6, 246.8],
|
||||
} satisfies Record<UsageRange, readonly number[]>
|
||||
|
||||
const leaderboard: readonly LeaderboardEntry[] = [
|
||||
{ model: "GPT-5.4-mini", author: "OpenAI", tokens: 314, change: 17, products: ["All Users", "Zen", "Go"] },
|
||||
{ model: "minimax-m2.5-free", author: "MiniMax", tokens: 286, change: 11, products: ["All Users", "Zen"] },
|
||||
{ model: "kimi-k2.5", author: "Moonshot", tokens: 252, change: -3, products: ["All Users", "Go", "Enterprise"] },
|
||||
{ model: "claude-sonnet-4-6", author: "Anthropic", tokens: 219, change: -2, products: ["All Users", "Enterprise"] },
|
||||
{ model: "gemini-3-flash", author: "Google", tokens: 201, change: 6, products: ["All Users", "Go"] },
|
||||
{ model: "glm-5", author: "Zhipu", tokens: 177, change: -8, products: ["All Users", "Enterprise"] },
|
||||
{ model: "gpt-5.3-codex", author: "OpenAI", tokens: 152, change: 10, products: ["All Users", "Zen", "Enterprise"] },
|
||||
{ model: "claude-haiku-4-5", author: "Anthropic", tokens: 130, change: 14, products: ["All Users", "Zen"] },
|
||||
{ model: "nemotron-3-super-free", author: "Nvidia", tokens: 117, change: -5, products: ["All Users", "Go"] },
|
||||
{ model: "gemini-3.1-pro", author: "Google", tokens: 96, change: 1, products: ["All Users", "Enterprise"] },
|
||||
{ model: "minimax-m2.7", author: "MiniMax", tokens: 81, change: -4, products: ["All Users", "Go"] },
|
||||
{ model: "gpt-5.4", author: "OpenAI", tokens: 64, change: 5, products: ["All Users", "Enterprise"] },
|
||||
{ model: "claude-opus-4-6", author: "Anthropic", tokens: 52, change: 2, products: ["All Users", "Enterprise"] },
|
||||
]
|
||||
|
||||
const market = [
|
||||
{ author: "OpenCode", share: 23.1, tokens: "1.33T", values: [18, 19, 23, 22, 24, 23, 25] },
|
||||
{ author: "Minimax", share: 19.4, tokens: "1.12T", values: [14, 18, 17, 20, 18, 21, 19] },
|
||||
{ author: "Xiaomi", share: 13.2, tokens: "762B", values: [10, 12, 14, 13, 13, 15, 14] },
|
||||
{ author: "Moonshot", share: 11.8, tokens: "681B", values: [13, 11, 12, 10, 12, 11, 12] },
|
||||
{ author: "Nvidia", share: 9.7, tokens: "560B", values: [8, 9, 8, 10, 9, 9, 10] },
|
||||
{ author: "OpenAI", share: 8.6, tokens: "496B", values: [12, 10, 9, 8, 9, 8, 8] },
|
||||
{ author: "Anthropic", share: 6.9, tokens: "398B", values: [7, 6, 7, 7, 6, 7, 7] },
|
||||
{ author: "Zhipu", share: 4.1, tokens: "236B", values: [4, 5, 4, 4, 4, 4, 4] },
|
||||
{ author: "Other", share: 3.2, tokens: "184B", values: [14, 10, 6, 6, 5, 2, 1] },
|
||||
]
|
||||
|
||||
const tokenCosts = [
|
||||
["minimax-m2.5", 0.06],
|
||||
["minimax-m2.7", 0.09],
|
||||
["gemini-3-flash", 0.11],
|
||||
["kimi-k2.5", 0.16],
|
||||
["gpt-5.4-mini", 0.16],
|
||||
["claude-haiku-4-5", 0.2],
|
||||
["glm-5", 0.28],
|
||||
["gpt-5.3-codex", 0.41],
|
||||
["gemini-3.1-pro", 0.43],
|
||||
["gpt-5.4", 0.54],
|
||||
["claude-sonnet-4-6", 0.61],
|
||||
["claude-sonnet-4-5", 0.61],
|
||||
["claude-opus-4-6", 1.02],
|
||||
] as const
|
||||
|
||||
const sessionCosts = [
|
||||
["gpt-5.4-nano", 0.0228, "212K"],
|
||||
["gpt-5.1-codex-mini", 0.0716, "534K"],
|
||||
["minimax-m2.5", 0.1058, "898K"],
|
||||
["claude-haiku-4-5", 0.1456, "734K"],
|
||||
["gpt-5.4-mini", 0.1817, "646K"],
|
||||
["minimax-m2.7", 0.2035, "715K"],
|
||||
["gpt-5.4", 0.2228, "488K"],
|
||||
["kimi-k2.5", 0.2646, "1.1M"],
|
||||
["gemini-3-flash", 0.273, "829K"],
|
||||
["glm-5", 0.3591, "925K"],
|
||||
["claude-sonnet-4-6", 0.7608, "1.4M"],
|
||||
["gpt-5.3-codex", 0.7784, "1.2M"],
|
||||
["claude-sonnet-4-5", 1.001, "1.6M"],
|
||||
["gemini-3.1-pro", 1.0831, "1.5M"],
|
||||
["claude-opus-4-6", 2.6844, "2.2M"],
|
||||
["claude-opus-4-5", 2.2732, "2.0M"],
|
||||
["gpt-5.4-pr", 5.186, "3.3M"],
|
||||
] as const
|
||||
|
||||
const countries = [
|
||||
["United States", "520B", 30, 44, 28],
|
||||
["Canada", "130B", 24, 30, 16],
|
||||
["Brazil", "88B", 37, 70, 12],
|
||||
["Germany", "112B", 52, 39, 14],
|
||||
["India", "184B", 68, 58, 18],
|
||||
["Japan", "92B", 84, 50, 12],
|
||||
] as const
|
||||
const getData = query(async () => {
|
||||
"use server"
|
||||
return runtime.runPromise(fetchRankingsData())
|
||||
}, "getRankingsData")
|
||||
|
||||
export default function Rankings() {
|
||||
getRequestEvent()?.response.headers.set(
|
||||
"Cache-Control",
|
||||
"public, max-age=60, s-maxage=300, stale-while-revalidate=86400",
|
||||
)
|
||||
const data = createAsync(() => getData())
|
||||
|
||||
return (
|
||||
<main data-page="rankings">
|
||||
<Title>Model Rankings | opencode</Title>
|
||||
<Meta
|
||||
name="description"
|
||||
content="OpenCode model rankings across usage, market share, token cost, session cost, and country trends."
|
||||
content="OpenCode model rankings across usage, market share, token cost, and session cost."
|
||||
/>
|
||||
<div data-component="container">
|
||||
<Header />
|
||||
<div data-component="content">
|
||||
<section data-section="hero">
|
||||
<div>
|
||||
<h1>Model Rankings</h1>
|
||||
<p data-slot="meta">
|
||||
<svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16">
|
||||
<rect x="3" y="3" width="10" height="10" fill="currentColor" />
|
||||
<rect x="7" y="6.5" width="2" height="4.5" fill="var(--rankings-layer-2)" />
|
||||
<rect x="7" y="5" width="2" height="1" fill="var(--rankings-layer-2)" />
|
||||
</svg>
|
||||
<span>OpenCode data</span> <b>·</b> <em>Showing 25%</em>
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
See which models are winning real usage, how the mix shifts over time, and where momentum is moving each
|
||||
week.
|
||||
</p>
|
||||
</section>
|
||||
<UsageSection />
|
||||
<LeaderboardSection />
|
||||
<MarketShareSection />
|
||||
<TokenCostSection />
|
||||
<SessionCostSection />
|
||||
<ChartSection title="Token by Country" controls={<Controls includeProducts={true} />}>
|
||||
<CountryMap />
|
||||
</ChartSection>
|
||||
<Newsletter />
|
||||
<Show when={data()} fallback={<RankingsLoading />}>
|
||||
{(rankings) => (
|
||||
<>
|
||||
<Hero updatedAt={rankings().updatedAt} />
|
||||
<UsageSection data={rankings().usage} />
|
||||
<LeaderboardSection data={rankings().leaderboard} />
|
||||
<MarketShareSection data={rankings().market} />
|
||||
<TokenCostSection data={rankings().tokenCost} />
|
||||
<SessionCostSection data={rankings().sessionCost} />
|
||||
<ChartSection
|
||||
title="Token by Country"
|
||||
description="Country-level token data is not present in the stat table yet."
|
||||
>
|
||||
<EmptyState
|
||||
title="No country dimension"
|
||||
description="Add a country or region column to stat to power this chart."
|
||||
/>
|
||||
</ChartSection>
|
||||
<Newsletter />
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
@@ -194,6 +78,39 @@ export default function Rankings() {
|
||||
)
|
||||
}
|
||||
|
||||
function Hero(props: { updatedAt: string | null }) {
|
||||
return (
|
||||
<section data-section="hero">
|
||||
<div>
|
||||
<h1>Model Rankings</h1>
|
||||
<p data-slot="meta">
|
||||
<svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16">
|
||||
<rect x="3" y="3" width="10" height="10" fill="currentColor" />
|
||||
<rect x="7" y="6.5" width="2" height="4.5" fill="var(--rankings-layer-2)" />
|
||||
<rect x="7" y="5" width="2" height="1" fill="var(--rankings-layer-2)" />
|
||||
</svg>
|
||||
<span>OpenCode data</span> <b>·</b>{" "}
|
||||
<em>{props.updatedAt ? `Updated ${formatUpdatedAt(props.updatedAt)}` : "No rows yet"}</em>
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
See which models are winning real usage, how the mix shifts over time, and where momentum is moving each week.
|
||||
</p>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function RankingsLoading() {
|
||||
return (
|
||||
<>
|
||||
<Hero updatedAt={null} />
|
||||
<ChartSection title="Usage">
|
||||
<EmptyState title="Loading rankings" description="Reading model aggregates from the stat table." />
|
||||
</ChartSection>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function ChartSection(props: { title: string; description?: string; controls?: JSX.Element; children: JSX.Element }) {
|
||||
return (
|
||||
<section data-section="chart">
|
||||
@@ -209,23 +126,41 @@ function ChartSection(props: { title: string; description?: string; controls?: J
|
||||
)
|
||||
}
|
||||
|
||||
function Controls(props: { includeProducts?: boolean }) {
|
||||
function EmptyState(props: { title: string; description: string }) {
|
||||
return (
|
||||
<div data-component="controls">
|
||||
{props.includeProducts && <ProductPills />}
|
||||
<RangePills />
|
||||
<div data-component="empty-state">
|
||||
<strong>{props.title}</strong>
|
||||
<p>{props.description}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function UsageSection() {
|
||||
function formatUpdatedAt(value: string) {
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return "just now"
|
||||
return new Intl.DateTimeFormat("en", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
timeZone: "UTC",
|
||||
timeZoneName: "short",
|
||||
}).format(date)
|
||||
}
|
||||
|
||||
function UsageSection(props: { data: RankingsData["usage"] }) {
|
||||
const [product, setProduct] = createSignal<UsageProduct>("All Users")
|
||||
const [range, setRange] = createSignal<UsageRange>("1W")
|
||||
const data = createMemo(() => getUsageData(product(), range()))
|
||||
const data = createMemo(() => props.data[product()][range()])
|
||||
|
||||
return (
|
||||
<ChartSection title="Usage">
|
||||
<UsageChart data={data()} />
|
||||
<Show
|
||||
when={data().some((item) => usageTotal(item) > 0)}
|
||||
fallback={<EmptyState title="No usage data" description="No stat rows matched this product and range." />}
|
||||
>
|
||||
<UsageChart data={data()} />
|
||||
</Show>
|
||||
<div data-slot="chart-footer">
|
||||
<RankingFilters product={product()} range={range()} onProductSelect={setProduct} onRangeSelect={setRange} />
|
||||
</div>
|
||||
@@ -285,26 +220,6 @@ function FilterPills<T extends string>(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function ProductPills(props: { live?: boolean }) {
|
||||
return (
|
||||
<div data-component="pills" aria-label="Product filter">
|
||||
<For each={props.live ? ["Zen", "Go", "Enterprise", "Live"] : products}>
|
||||
{(item, index) => <button data-active={index() === 0 ? "true" : undefined}>{item}</button>}
|
||||
</For>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RangePills() {
|
||||
return (
|
||||
<div data-component="pills" aria-label="Date range">
|
||||
<For each={ranges}>
|
||||
{(item, index) => <button data-active={index() === 1 ? "true" : undefined}>{item}</button>}
|
||||
</For>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function UsageChart(props: { data: UsagePoint[] }) {
|
||||
const [activeIndex, setActiveIndex] = createSignal<number>()
|
||||
const [activeSegment, setActiveSegment] = createSignal<number>()
|
||||
@@ -312,7 +227,7 @@ function UsageChart(props: { data: UsagePoint[] }) {
|
||||
const width = 920
|
||||
const headerOffset = 46
|
||||
const segmentGap = 2
|
||||
const maxTotal = createMemo(() => Math.max(...props.data.map((item) => usageTotal(item))) * 1.02)
|
||||
const maxTotal = createMemo(() => Math.max(1, Math.max(...props.data.map((item) => usageTotal(item))) * 1.02))
|
||||
const activePoint = createMemo(() => props.data[activeIndex() ?? -1])
|
||||
const y = createMemo(() => scaleLinear([0, maxTotal()], [height, 0]))
|
||||
const x = createMemo(() =>
|
||||
@@ -454,25 +369,6 @@ function UsageChart(props: { data: UsagePoint[] }) {
|
||||
)
|
||||
}
|
||||
|
||||
function getUsageData(product: UsageProduct, range: UsageRange) {
|
||||
return usageDates[range].map((date, dayIndex) => ({
|
||||
date,
|
||||
segments: (usageValues[(dayIndex + ranges.indexOf(range)) % usageValues.length] ?? []).map(
|
||||
(value, segmentIndex) => ({
|
||||
model: usageModels[segmentIndex] ?? "Other",
|
||||
value: Number(
|
||||
(
|
||||
value *
|
||||
usageProductMultipliers[product] *
|
||||
usageRangeMultipliers[range] *
|
||||
(1 + (((dayIndex + segmentIndex + products.indexOf(product) + ranges.indexOf(range)) % 5) - 2) * 0.055)
|
||||
).toFixed(2),
|
||||
),
|
||||
}),
|
||||
),
|
||||
}))
|
||||
}
|
||||
|
||||
function getUsageTooltipStyle(barX: number, barWidth: number, width: number) {
|
||||
if (barX > width * 0.62) return { left: "auto", right: `${((width - barX + 12) / width) * 100}%` }
|
||||
return { left: `${((barX + barWidth + 12) / width) * 100}%`, right: "auto" }
|
||||
@@ -493,17 +389,22 @@ function formatTokens(value: number) {
|
||||
return `${Math.round(value * 1000)}B`
|
||||
}
|
||||
|
||||
function LeaderboardSection() {
|
||||
function LeaderboardSection(props: { data: RankingsData["leaderboard"] }) {
|
||||
const [product, setProduct] = createSignal<UsageProduct>("All Users")
|
||||
const [range, setRange] = createSignal<UsageRange>("1W")
|
||||
const data = createMemo(() => getLeaderboardData(product(), range()))
|
||||
const data = createMemo(() => props.data[product()][range()])
|
||||
|
||||
return (
|
||||
<ChartSection
|
||||
title="Leaderboard"
|
||||
description="Shown are the sum of prompt and completion tokens per model, including reasoning tokens."
|
||||
>
|
||||
<Leaderboard data={data()} />
|
||||
<Show
|
||||
when={data().length > 0}
|
||||
fallback={<EmptyState title="No leaderboard data" description="No stat rows matched this product and range." />}
|
||||
>
|
||||
<Leaderboard data={data()} />
|
||||
</Show>
|
||||
<div data-slot="chart-footer">
|
||||
<RankingFilters product={product()} range={range()} onProductSelect={setProduct} onRangeSelect={setRange} />
|
||||
</div>
|
||||
@@ -511,7 +412,7 @@ function LeaderboardSection() {
|
||||
)
|
||||
}
|
||||
|
||||
function Leaderboard(props: { data: (LeaderboardEntry & { rank: number })[] }) {
|
||||
function Leaderboard(props: { data: LeaderboardEntry[] }) {
|
||||
return (
|
||||
<div data-component="leaderboard" aria-label="Model token leaderboard">
|
||||
<div data-slot="leaderboard-grid">
|
||||
@@ -526,7 +427,7 @@ function Leaderboard(props: { data: (LeaderboardEntry & { rank: number })[] }) {
|
||||
)
|
||||
}
|
||||
|
||||
function LeaderboardCard(props: { entry: LeaderboardEntry & { rank: number }; size: "featured" | "compact" }) {
|
||||
function LeaderboardCard(props: { entry: LeaderboardEntry; size: "featured" | "compact" }) {
|
||||
return (
|
||||
<article data-component="leader-card" data-size={props.size}>
|
||||
<span data-slot="rank">{String(props.entry.rank).padStart(2, "0")}</span>
|
||||
@@ -550,18 +451,6 @@ function LeaderboardCard(props: { entry: LeaderboardEntry & { rank: number }; si
|
||||
)
|
||||
}
|
||||
|
||||
function getLeaderboardData(product: UsageProduct, range: UsageRange) {
|
||||
return leaderboard
|
||||
.filter((entry) => entry.products.includes(product))
|
||||
.map((entry, index) => ({
|
||||
...entry,
|
||||
tokens: Math.round(entry.tokens * usageProductMultipliers[product] * usageRangeMultipliers[range]),
|
||||
change: entry.change + (((index + products.indexOf(product) + ranges.indexOf(range)) % 5) - 2),
|
||||
}))
|
||||
.sort((a, b) => b.tokens - a.tokens)
|
||||
.map((entry, index) => ({ ...entry, rank: index + 1 }))
|
||||
}
|
||||
|
||||
function getProviderIconId(author: string) {
|
||||
if (author === "MiniMax") return "minimax"
|
||||
if (author === "Moonshot") return "moonshotai"
|
||||
@@ -579,20 +468,30 @@ function formatChange(value: number) {
|
||||
return `${value}%`
|
||||
}
|
||||
|
||||
function MarketShareSection() {
|
||||
function MarketShareSection(props: { data: RankingsData["market"] }) {
|
||||
const [range, setRange] = createSignal<UsageRange>("1W")
|
||||
const [activeIndex, setActiveIndex] = createSignal(2)
|
||||
const data = createMemo(() => getMarketData(range()))
|
||||
const activeDay = createMemo(() => data()[activeIndex()] ?? data()[0])
|
||||
const data = createMemo(() => props.data[range()])
|
||||
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(data().length - 1, 0)))
|
||||
const activeDay = createMemo(() => data()[selectedIndex()])
|
||||
|
||||
return (
|
||||
<ChartSection title="Market Share" description="Compare token share by model author.">
|
||||
<MarketShare data={data()} activeIndex={activeIndex()} onActiveIndexChange={setActiveIndex} />
|
||||
<MarketShareList data={activeDay().authors} />
|
||||
<Show
|
||||
when={activeDay()}
|
||||
fallback={<EmptyState title="No market data" description="No stat rows matched this range." />}
|
||||
>
|
||||
{(day) => (
|
||||
<>
|
||||
<MarketShare data={data()} activeIndex={selectedIndex()} onActiveIndexChange={setActiveIndex} />
|
||||
<MarketShareList data={day().authors} />
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
<div data-slot="market-footer">
|
||||
<p>
|
||||
<span>[*]</span>
|
||||
<strong>{activeDay().date} 2026</strong>
|
||||
<strong>{activeDay()?.date ?? "No data"}</strong>
|
||||
</p>
|
||||
<FilterPills items={ranges} selected={range()} label="Date range" variant="range" onSelect={setRange} />
|
||||
</div>
|
||||
@@ -662,44 +561,26 @@ function MarketShareList(props: { data: MarketDay["authors"] }) {
|
||||
)
|
||||
}
|
||||
|
||||
function getMarketData(range: UsageRange) {
|
||||
return usageDates[range].map((date, dayIndex) => {
|
||||
const authors = market.map((item, authorIndex) => ({
|
||||
author: item.author,
|
||||
share: Number(
|
||||
Math.max(
|
||||
1.2,
|
||||
item.values[dayIndex] + (((dayIndex + authorIndex + ranges.indexOf(range)) % 5) - 2) * 0.4,
|
||||
).toFixed(1),
|
||||
),
|
||||
tokens: 0,
|
||||
}))
|
||||
const totalShare = authors.reduce((sum, item) => sum + item.share, 0)
|
||||
return {
|
||||
date,
|
||||
total: marketTotals[range][dayIndex] ?? 0,
|
||||
authors: authors.map((item) => ({
|
||||
...item,
|
||||
share: Number(((item.share / totalShare) * 100).toFixed(1)),
|
||||
tokens: Number((((marketTotals[range][dayIndex] ?? 0) * item.share) / totalShare).toFixed(2)),
|
||||
})),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function formatTrillions(value: number) {
|
||||
return `${value.toFixed(value >= 10 ? 0 : 1)}T`
|
||||
}
|
||||
|
||||
function TokenCostSection() {
|
||||
function TokenCostSection(props: { data: RankingsData["tokenCost"] }) {
|
||||
const [product, setProduct] = createSignal<TokenProduct>("Zen")
|
||||
const [live, setLive] = createSignal(true)
|
||||
const [activeIndex, setActiveIndex] = createSignal(2)
|
||||
const data = createMemo(() => getTokenCostData(product(), live()))
|
||||
const data = createMemo(() => props.data[product()])
|
||||
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(data().length - 1, 0)))
|
||||
|
||||
return (
|
||||
<ChartSection title="Token Cost" description="Price per 1M tokens.">
|
||||
<TokenCostChart data={data()} activeIndex={activeIndex()} onActiveIndexChange={setActiveIndex} />
|
||||
<Show
|
||||
when={data().length > 0}
|
||||
fallback={
|
||||
<EmptyState title="No token cost data" description="No cost-bearing stat rows matched this product." />
|
||||
}
|
||||
>
|
||||
<TokenCostChart data={data()} activeIndex={selectedIndex()} onActiveIndexChange={setActiveIndex} />
|
||||
</Show>
|
||||
<div data-slot="token-footer">
|
||||
<FilterPills
|
||||
items={tokenProducts}
|
||||
@@ -708,25 +589,17 @@ function TokenCostSection() {
|
||||
variant="product"
|
||||
onSelect={setProduct}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
data-component="live-filter"
|
||||
data-active={live() ? "true" : undefined}
|
||||
onClick={() => setLive(!live())}
|
||||
>
|
||||
Live
|
||||
</button>
|
||||
</div>
|
||||
</ChartSection>
|
||||
)
|
||||
}
|
||||
|
||||
function TokenCostChart(props: {
|
||||
data: ReturnType<typeof getTokenCostData>
|
||||
data: TokenCostEntry[]
|
||||
activeIndex: number
|
||||
onActiveIndexChange: (index: number) => void
|
||||
}) {
|
||||
const max = createMemo(() => Math.max(...props.data.map((item) => item.total)))
|
||||
const max = createMemo(() => Math.max(1, ...props.data.map((item) => item.total)))
|
||||
const active = createMemo(() => props.data[props.activeIndex] ?? props.data[0])
|
||||
|
||||
return (
|
||||
@@ -768,20 +641,6 @@ function TokenCostChart(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function getTokenCostData(product: TokenProduct, live: boolean) {
|
||||
return tokenCosts.map((item, index) => {
|
||||
const multiplier = (product === "Zen" ? 1 : product === "Go" ? 0.88 : 1.18) * (live ? 1 : 0.94)
|
||||
const total = Number((item[1] * multiplier).toFixed(2))
|
||||
return {
|
||||
model: item[0],
|
||||
total,
|
||||
input: Number((total * (index < 3 ? 0.18 : 0.22)).toFixed(2)),
|
||||
output: Number((total * (index < 3 ? 1.1 : 1.34)).toFixed(2)),
|
||||
cached: Number((total * 0.1).toFixed(2)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function formatDollars(value: number) {
|
||||
return `$${value.toFixed(2)}`
|
||||
}
|
||||
@@ -789,21 +648,28 @@ function formatDollars(value: number) {
|
||||
function MetricBar(props: { value: number; max: number; active: boolean }) {
|
||||
return (
|
||||
<i data-component="metric-bar" data-active={props.active ? "true" : undefined}>
|
||||
<b style={{ "flex-grow": Math.max(props.value / props.max, 0.05) }} />
|
||||
<b style={{ "flex-grow": Math.max(props.value / Math.max(props.max, 1), 0.05) }} />
|
||||
<em />
|
||||
</i>
|
||||
)
|
||||
}
|
||||
|
||||
function SessionCostSection() {
|
||||
function SessionCostSection(props: { data: RankingsData["sessionCost"] }) {
|
||||
const [product, setProduct] = createSignal<TokenProduct>("Zen")
|
||||
const [live, setLive] = createSignal(true)
|
||||
const [activeIndex, setActiveIndex] = createSignal(2)
|
||||
const data = createMemo(() => getSessionCostData(product(), live()))
|
||||
const data = createMemo(() => props.data[product()])
|
||||
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(data().length - 1, 0)))
|
||||
|
||||
return (
|
||||
<ChartSection title="Session Cost" description="Average cost per session.">
|
||||
<SessionCostChart data={data()} activeIndex={activeIndex()} onActiveIndexChange={setActiveIndex} />
|
||||
<Show
|
||||
when={data().length > 0}
|
||||
fallback={
|
||||
<EmptyState title="No session cost data" description="No session-bearing stat rows matched this product." />
|
||||
}
|
||||
>
|
||||
<SessionCostChart data={data()} activeIndex={selectedIndex()} onActiveIndexChange={setActiveIndex} />
|
||||
</Show>
|
||||
<div data-slot="token-footer">
|
||||
<FilterPills
|
||||
items={tokenProducts}
|
||||
@@ -812,26 +678,18 @@ function SessionCostSection() {
|
||||
variant="product"
|
||||
onSelect={setProduct}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
data-component="live-filter"
|
||||
data-active={live() ? "true" : undefined}
|
||||
onClick={() => setLive(!live())}
|
||||
>
|
||||
Live
|
||||
</button>
|
||||
</div>
|
||||
</ChartSection>
|
||||
)
|
||||
}
|
||||
|
||||
function SessionCostChart(props: {
|
||||
data: ReturnType<typeof getSessionCostData>
|
||||
data: SessionCostEntry[]
|
||||
activeIndex: number
|
||||
onActiveIndexChange: (index: number) => void
|
||||
}) {
|
||||
const maxCost = createMemo(() => Math.max(...props.data.map((item) => item.cost)))
|
||||
const maxTokens = createMemo(() => Math.max(...props.data.map((item) => item.tokens)))
|
||||
const maxCost = createMemo(() => Math.max(1, ...props.data.map((item) => item.cost)))
|
||||
const maxTokens = createMemo(() => Math.max(1, ...props.data.map((item) => item.tokens)))
|
||||
const active = createMemo(() => props.data[props.activeIndex] ?? props.data[0])
|
||||
|
||||
return (
|
||||
@@ -880,22 +738,6 @@ function SessionCostChart(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function getSessionCostData(product: TokenProduct, live: boolean) {
|
||||
return sessionCosts.map((item) => {
|
||||
const multiplier = (product === "Zen" ? 1 : product === "Go" ? 0.9 : 1.2) * (live ? 1 : 0.94)
|
||||
return {
|
||||
model: item[0],
|
||||
cost: Number((item[1] * multiplier).toFixed(4)),
|
||||
tokens: Math.round(parseTokenCount(item[2]) * (product === "Enterprise" ? 1.12 : product === "Go" ? 0.92 : 1)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function parseTokenCount(value: string) {
|
||||
if (value.endsWith("M")) return Number(value.slice(0, -1)) * 1_000_000
|
||||
return Number(value.slice(0, -1)) * 1_000
|
||||
}
|
||||
|
||||
function formatTokenCount(value: number) {
|
||||
if (value >= 1_000_000) return `${Number((value / 1_000_000).toFixed(1))}M`
|
||||
return `${Math.round(value / 1_000)}K`
|
||||
@@ -905,35 +747,6 @@ function formatSessionCost(value: number) {
|
||||
return `$${value.toFixed(4)}`
|
||||
}
|
||||
|
||||
function CountryMap() {
|
||||
return (
|
||||
<div data-component="country-map">
|
||||
<svg viewBox="0 0 920 420" role="img" aria-label="Tokens by country map">
|
||||
<defs>
|
||||
<pattern id="rankings-dot-grid" width="18" height="18" patternUnits="userSpaceOnUse">
|
||||
<circle cx="2" cy="2" r="1.5" fill="#d4d4d4" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="920" height="420" fill="url(#rankings-dot-grid)" />
|
||||
<For each={countries}>
|
||||
{(item) => (
|
||||
<g>
|
||||
<circle cx={(item[2] / 100) * 920} cy={(item[3] / 100) * 420} r={item[4]} />
|
||||
<text x={(item[2] / 100) * 920 + item[4] + 8} y={(item[3] / 100) * 420 + 4}>
|
||||
{item[0]} {item[1]}
|
||||
</text>
|
||||
</g>
|
||||
)}
|
||||
</For>
|
||||
</svg>
|
||||
<div data-component="map-tooltip">
|
||||
<strong>Canada</strong>
|
||||
<span>130B</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Newsletter() {
|
||||
return (
|
||||
<section data-section="newsletter">
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
import { Resource } from "sst"
|
||||
import { defineConfig } from "drizzle-kit"
|
||||
|
||||
export default defineConfig({
|
||||
dialect: "mysql",
|
||||
schema: ["./src/database/schema.ts"],
|
||||
out: "./migrations",
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL ?? "mysql://root:changeme@localhost:3306/opencode_stats",
|
||||
},
|
||||
// schema: ["./src/**/*.sql.ts"],
|
||||
out: "./migrations/",
|
||||
strict: true,
|
||||
verbose: true,
|
||||
dbCredentials: {
|
||||
database: Resource.StatsDatabase.database,
|
||||
host: Resource.StatsDatabase.host,
|
||||
user: Resource.StatsDatabase.username,
|
||||
password: Resource.StatsDatabase.password,
|
||||
port: Resource.StatsDatabase.port,
|
||||
ssl: {
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
CREATE TABLE `stat` (
|
||||
`id` bigint AUTO_INCREMENT PRIMARY KEY,
|
||||
`grain` varchar(16) NOT NULL,
|
||||
`period_start` datetime NOT NULL,
|
||||
`period_end` datetime NOT NULL,
|
||||
`dataset` varchar(64) NOT NULL DEFAULT 'all',
|
||||
`tier` varchar(64) NOT NULL DEFAULT 'all',
|
||||
`client` varchar(64) NOT NULL DEFAULT 'all',
|
||||
`source` varchar(64) NOT NULL DEFAULT 'all',
|
||||
`provider` varchar(128) NOT NULL,
|
||||
`model` varchar(256) NOT NULL,
|
||||
`provider_model` varchar(256) NOT NULL DEFAULT '',
|
||||
`sessions` bigint NOT NULL DEFAULT 0,
|
||||
`requests` bigint NOT NULL DEFAULT 0,
|
||||
`input_tokens` bigint NOT NULL DEFAULT 0,
|
||||
`output_tokens` bigint NOT NULL DEFAULT 0,
|
||||
`reasoning_tokens` bigint NOT NULL DEFAULT 0,
|
||||
`cache_read_tokens` bigint NOT NULL DEFAULT 0,
|
||||
`total_tokens` bigint NOT NULL DEFAULT 0,
|
||||
`input_cost_microcents` bigint NOT NULL DEFAULT 0,
|
||||
`output_cost_microcents` bigint NOT NULL DEFAULT 0,
|
||||
`total_cost_microcents` bigint NOT NULL DEFAULT 0,
|
||||
`avg_duration_ms` decimal(12,2),
|
||||
`p50_duration_ms` int,
|
||||
`p95_duration_ms` int,
|
||||
`avg_ttfb_ms` decimal(12,2),
|
||||
`p50_ttfb_ms` int,
|
||||
`p95_ttfb_ms` int,
|
||||
`avg_output_tps` decimal(12,4),
|
||||
`success_count` bigint NOT NULL DEFAULT 0,
|
||||
`error_count` bigint NOT NULL DEFAULT 0,
|
||||
`sample_count` bigint NOT NULL DEFAULT 0,
|
||||
`rank_by_tokens` int,
|
||||
`rank_by_requests` int,
|
||||
`rank_by_cost` int,
|
||||
`created_at` datetime NOT NULL DEFAULT (now()),
|
||||
`updated_at` datetime NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT `uniq_model_period` UNIQUE INDEX(`grain`,`period_start`,`dataset`,`tier`,`client`,`source`,`provider`,`model`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `idx_leaderboard_tokens` ON `stat` (`grain`,`period_start`,`dataset`,`tier`,`total_tokens`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_model` ON `stat` (`model`,`grain`,`period_start`);
|
||||
@@ -0,0 +1,627 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "mysql",
|
||||
"id": "72655266-65da-408e-bfd8-9f3a4ad817a5",
|
||||
"prevIds": [
|
||||
"00000000-0000-0000-0000-000000000000"
|
||||
],
|
||||
"ddl": [
|
||||
{
|
||||
"name": "stat",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"type": "bigint",
|
||||
"notNull": true,
|
||||
"autoIncrement": true,
|
||||
"default": null,
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "varchar(16)",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": null,
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "grain",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "datetime",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": null,
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "period_start",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "datetime",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": null,
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "period_end",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "varchar(64)",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "'all'",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "dataset",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "varchar(64)",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "'all'",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "tier",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "varchar(64)",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "'all'",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "client",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "varchar(64)",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "'all'",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "source",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "varchar(128)",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": null,
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "provider",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "varchar(256)",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": null,
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "model",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "varchar(256)",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "''",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "provider_model",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "bigint",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "0",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "sessions",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "bigint",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "0",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "requests",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "bigint",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "0",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "input_tokens",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "bigint",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "0",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "output_tokens",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "bigint",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "0",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "reasoning_tokens",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "bigint",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "0",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "cache_read_tokens",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "bigint",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "0",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "total_tokens",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "bigint",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "0",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "input_cost_microcents",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "bigint",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "0",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "output_cost_microcents",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "bigint",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "0",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "total_cost_microcents",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "decimal(12,2)",
|
||||
"notNull": false,
|
||||
"autoIncrement": false,
|
||||
"default": null,
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "avg_duration_ms",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "int",
|
||||
"notNull": false,
|
||||
"autoIncrement": false,
|
||||
"default": null,
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "p50_duration_ms",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "int",
|
||||
"notNull": false,
|
||||
"autoIncrement": false,
|
||||
"default": null,
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "p95_duration_ms",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "decimal(12,2)",
|
||||
"notNull": false,
|
||||
"autoIncrement": false,
|
||||
"default": null,
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "avg_ttfb_ms",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "int",
|
||||
"notNull": false,
|
||||
"autoIncrement": false,
|
||||
"default": null,
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "p50_ttfb_ms",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "int",
|
||||
"notNull": false,
|
||||
"autoIncrement": false,
|
||||
"default": null,
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "p95_ttfb_ms",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "decimal(12,4)",
|
||||
"notNull": false,
|
||||
"autoIncrement": false,
|
||||
"default": null,
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "avg_output_tps",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "bigint",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "0",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "success_count",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "bigint",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "0",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "error_count",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "bigint",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "0",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "sample_count",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "int",
|
||||
"notNull": false,
|
||||
"autoIncrement": false,
|
||||
"default": null,
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "rank_by_tokens",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "int",
|
||||
"notNull": false,
|
||||
"autoIncrement": false,
|
||||
"default": null,
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "rank_by_requests",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "int",
|
||||
"notNull": false,
|
||||
"autoIncrement": false,
|
||||
"default": null,
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "rank_by_cost",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "datetime",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "(now())",
|
||||
"onUpdateNow": false,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "created_at",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"type": "datetime",
|
||||
"notNull": true,
|
||||
"autoIncrement": false,
|
||||
"default": "(now())",
|
||||
"onUpdateNow": true,
|
||||
"onUpdateNowFsp": null,
|
||||
"charSet": null,
|
||||
"collation": null,
|
||||
"generated": null,
|
||||
"name": "updated_at",
|
||||
"entityType": "columns",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"name": "PRIMARY",
|
||||
"table": "stat",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "grain",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "period_start",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "dataset",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "tier",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "client",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "source",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "provider",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "model",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"using": null,
|
||||
"algorithm": null,
|
||||
"lock": null,
|
||||
"nameExplicit": true,
|
||||
"name": "uniq_model_period",
|
||||
"entityType": "indexes",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "grain",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "period_start",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "dataset",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "tier",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "total_tokens",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"using": null,
|
||||
"algorithm": null,
|
||||
"lock": null,
|
||||
"nameExplicit": true,
|
||||
"name": "idx_leaderboard_tokens",
|
||||
"entityType": "indexes",
|
||||
"table": "stat"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "model",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "grain",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "period_start",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"using": null,
|
||||
"algorithm": null,
|
||||
"lock": null,
|
||||
"nameExplicit": true,
|
||||
"name": "idx_model",
|
||||
"entityType": "indexes",
|
||||
"table": "stat"
|
||||
}
|
||||
],
|
||||
"renames": []
|
||||
}
|
||||
@@ -15,11 +15,13 @@
|
||||
},
|
||||
"scripts": {
|
||||
"db:generate": "drizzle-kit generate --config=drizzle.config.ts",
|
||||
"db:migrate": "bun src/migrate.ts",
|
||||
"db:push": "drizzle-kit push --config=drizzle.config.ts",
|
||||
"db:studio": "drizzle-kit studio --config=drizzle.config.ts",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@planetscale/database": "1.19.0",
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:"
|
||||
},
|
||||
|
||||
@@ -1,17 +1,500 @@
|
||||
import { createHash } from "node:crypto"
|
||||
import { Client } from "@planetscale/database"
|
||||
import { sql } from "drizzle-orm"
|
||||
import { drizzle } from "drizzle-orm/planetscale-serverless"
|
||||
import { DateTime, Effect, Option, Schema } from "effect"
|
||||
import { Resource } from "sst"
|
||||
import { stat } from "../database/schema"
|
||||
|
||||
export async function handler() {
|
||||
const startedAt = new Date().toISOString()
|
||||
const HONEYCOMB_API_URL = "https://api.honeycomb.io"
|
||||
const HONEYCOMB_DATASET = "zen"
|
||||
const DAY_SECONDS = 86_400
|
||||
const MAX_POLL_ATTEMPTS = 15
|
||||
const UPSERT_CHUNK_SIZE = 500
|
||||
|
||||
console.log("stats sync stub", {
|
||||
startedAt,
|
||||
stage: Resource.App.stage,
|
||||
hasDatabaseUrl: Boolean(Resource.StatsDatabase.url),
|
||||
hasHoneycombApiKey: Boolean(Resource.HONEYCOMB_API_KEY.value),
|
||||
type HoneycombScalar = string | number | boolean | null
|
||||
type HoneycombData = Record<string, HoneycombScalar>
|
||||
type HoneycombQueryResult = { results: HoneycombData[]; series: { time: Date; data: HoneycombData }[] }
|
||||
type StatRow = typeof stat.$inferInsert
|
||||
type SyncResult = { ok: true; rows: number; startedAt: string; periodStart: string; periodEnd: string }
|
||||
type SyncError = HoneycombApiError | HoneycombQueryTimeoutError | StatDatabaseError
|
||||
|
||||
class HoneycombApiError extends Schema.TaggedErrorClass<HoneycombApiError>()("HoneycombApiError", {
|
||||
message: Schema.String,
|
||||
status: Schema.optional(Schema.Number),
|
||||
cause: Schema.optional(Schema.Defect),
|
||||
}) {}
|
||||
|
||||
class HoneycombQueryTimeoutError extends Schema.TaggedErrorClass<HoneycombQueryTimeoutError>()(
|
||||
"HoneycombQueryTimeoutError",
|
||||
{
|
||||
message: Schema.String,
|
||||
resultId: Schema.String,
|
||||
},
|
||||
) {}
|
||||
|
||||
class StatDatabaseError extends Schema.TaggedErrorClass<StatDatabaseError>()("StatDatabaseError", {
|
||||
message: Schema.String,
|
||||
cause: Schema.optional(Schema.Defect),
|
||||
}) {}
|
||||
|
||||
const decodeJson = Schema.decodeUnknownOption(Schema.UnknownFromJsonString)
|
||||
|
||||
const calculations = [
|
||||
{ op: "COUNT" },
|
||||
{ op: "COUNT_DISTINCT", column: "session" },
|
||||
{ op: "SUM", column: "tokens.input" },
|
||||
{ op: "SUM", column: "tokens.output" },
|
||||
{ op: "SUM", column: "tokens.reasoning" },
|
||||
{ op: "SUM", column: "tokens.cache_read" },
|
||||
{ op: "SUM", column: "tokens" },
|
||||
{ op: "SUM", column: "cost.input.microcents" },
|
||||
{ op: "SUM", column: "cost.output.microcents" },
|
||||
{ op: "SUM", column: "cost.total.microcents" },
|
||||
{ op: "AVG", column: "duration" },
|
||||
{ op: "P50", column: "duration" },
|
||||
{ op: "P95", column: "duration" },
|
||||
{ op: "AVG", column: "time_to_first_byte" },
|
||||
{ op: "P50", column: "time_to_first_byte" },
|
||||
{ op: "P95", column: "time_to_first_byte" },
|
||||
{ op: "AVG", column: "tps.output" },
|
||||
{ op: "SUM", column: "stats_success" },
|
||||
{ op: "SUM", column: "stats_error" },
|
||||
] as const
|
||||
|
||||
export function handler(): Promise<SyncResult> {
|
||||
return Effect.runPromise(syncStats())
|
||||
}
|
||||
|
||||
const syncStats: () => Effect.Effect<SyncResult, SyncError, never> = Effect.fn("StatsCron.sync")(function* () {
|
||||
const startedAt = yield* DateTime.nowAsDate
|
||||
const periodEnd = new Date(Math.floor(startedAt.getTime() / 3_600_000) * 3_600_000)
|
||||
const periodStart = new Date(
|
||||
Date.UTC(periodEnd.getUTCFullYear(), periodEnd.getUTCMonth(), periodEnd.getUTCDate() - 6),
|
||||
)
|
||||
|
||||
yield* logHoneycombRuntimeCheck()
|
||||
|
||||
const result = yield* runHoneycombQuery({
|
||||
start_time: Math.floor(periodStart.getTime() / 1000),
|
||||
end_time: Math.floor(periodEnd.getTime() / 1000),
|
||||
granularity: DAY_SECONDS,
|
||||
breakdowns: ["tier", "provider", "model"],
|
||||
calculations,
|
||||
calculated_fields: [
|
||||
{
|
||||
name: "stats_success",
|
||||
expression: `IF(AND(GTE($status, "200"), LT($status, "400")), 1, 0)`,
|
||||
},
|
||||
{
|
||||
name: "stats_error",
|
||||
expression: `IF(GTE($status, "400"), 1, 0)`,
|
||||
},
|
||||
],
|
||||
filters: [
|
||||
{ column: "event_type", op: "=", value: "completions" },
|
||||
{ column: "model", op: "exists" },
|
||||
{ column: "user_agent", op: "contains", value: "opencode" },
|
||||
],
|
||||
filter_combination: "AND",
|
||||
orders: [{ column: "tokens", op: "SUM", order: "descending" }],
|
||||
limit: 1000,
|
||||
})
|
||||
const rows = rankRows([
|
||||
...synthesizeAllTierRows(
|
||||
collapseRows(result.results.map((item) => toStatRow("week", periodStart, periodEnd, item))),
|
||||
),
|
||||
...synthesizeAllTierRows(
|
||||
collapseRows(
|
||||
result.series.map((item) =>
|
||||
toStatRow(
|
||||
"day",
|
||||
item.time,
|
||||
new Date(Math.min(item.time.getTime() + DAY_SECONDS * 1000, periodEnd.getTime())),
|
||||
item.data,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
])
|
||||
|
||||
yield* saveRows(rows)
|
||||
|
||||
yield* Effect.logInfo("stats sync complete").pipe(
|
||||
Effect.annotateLogs({
|
||||
startedAt: startedAt.toISOString(),
|
||||
periodStart: periodStart.toISOString(),
|
||||
periodEnd: periodEnd.toISOString(),
|
||||
rows: rows.length,
|
||||
stage: Resource.App.stage,
|
||||
}),
|
||||
)
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
startedAt,
|
||||
rows: rows.length,
|
||||
startedAt: startedAt.toISOString(),
|
||||
periodStart: periodStart.toISOString(),
|
||||
periodEnd: periodEnd.toISOString(),
|
||||
}
|
||||
})
|
||||
|
||||
const runHoneycombQuery: (
|
||||
query: Record<string, unknown>,
|
||||
) => Effect.Effect<HoneycombQueryResult, HoneycombApiError | HoneycombQueryTimeoutError, never> = Effect.fn(
|
||||
"StatsCron.runHoneycombQuery",
|
||||
)(function* (query: Record<string, unknown>) {
|
||||
const created = asRecord(yield* honeycombRequest(`/1/queries/${HONEYCOMB_DATASET}`, "POST", query))
|
||||
const queryId = asString(created.id)
|
||||
if (!queryId) return yield* new HoneycombApiError({ message: "Honeycomb did not return a query id" })
|
||||
|
||||
const queued = asRecord(
|
||||
yield* honeycombRequest(`/1/query_results/${HONEYCOMB_DATASET}`, "POST", {
|
||||
query_id: queryId,
|
||||
disable_series: false,
|
||||
disable_total_by_aggregate: true,
|
||||
disable_other_by_aggregate: true,
|
||||
limit: 1000,
|
||||
}),
|
||||
)
|
||||
const resultId = asString(queued.id)
|
||||
if (!resultId) return yield* new HoneycombApiError({ message: "Honeycomb did not return a query result id" })
|
||||
|
||||
return yield* pollHoneycombResult(resultId)
|
||||
})
|
||||
|
||||
const pollHoneycombResult: (
|
||||
resultId: string,
|
||||
attempt?: number,
|
||||
) => Effect.Effect<HoneycombQueryResult, HoneycombApiError | HoneycombQueryTimeoutError, never> = Effect.fn(
|
||||
"StatsCron.pollHoneycombResult",
|
||||
)(function* (resultId: string, attempt = 0) {
|
||||
if (attempt > 0) yield* Effect.sleep("1000 millis")
|
||||
const result = asRecord(yield* honeycombRequest(`/1/query_results/${HONEYCOMB_DATASET}/${resultId}`, "GET"))
|
||||
|
||||
if (result.complete === true) {
|
||||
const data = asRecord(result.data)
|
||||
return {
|
||||
results: asArray(data.results).map((item) => asData(asRecord(item).data)),
|
||||
series: asArray(data.series).flatMap((item) => {
|
||||
const time = new Date(String(asRecord(item).time ?? ""))
|
||||
if (Number.isNaN(time.getTime())) return []
|
||||
return [{ time, data: asData(asRecord(item).data) }]
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
if (attempt >= MAX_POLL_ATTEMPTS - 1)
|
||||
return yield* new HoneycombQueryTimeoutError({
|
||||
message: `Honeycomb query result ${resultId} did not complete`,
|
||||
resultId,
|
||||
})
|
||||
|
||||
return yield* pollHoneycombResult(resultId, attempt + 1)
|
||||
})
|
||||
|
||||
const honeycombRequest: (
|
||||
path: string,
|
||||
method: "GET" | "POST",
|
||||
body?: Record<string, unknown>,
|
||||
) => Effect.Effect<unknown, HoneycombApiError, never> = Effect.fn("StatsCron.honeycombRequest")(function* (
|
||||
path: string,
|
||||
method: "GET" | "POST",
|
||||
body?: Record<string, unknown>,
|
||||
) {
|
||||
const response = yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
fetch(`${HONEYCOMB_API_URL}${path}`, {
|
||||
method,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Honeycomb-Team": Resource.HONEYCOMB_API_KEY.value,
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
}),
|
||||
catch: (cause) => new HoneycombApiError({ message: `Honeycomb ${method} ${path} request failed`, cause }),
|
||||
})
|
||||
const text = yield* Effect.tryPromise({
|
||||
try: () => response.text(),
|
||||
catch: (cause) => new HoneycombApiError({ message: `Honeycomb ${method} ${path} response read failed`, cause }),
|
||||
})
|
||||
|
||||
if (!response.ok)
|
||||
return yield* new HoneycombApiError({
|
||||
message: `Honeycomb ${method} ${path} failed: ${response.status} ${text.slice(0, 500)}`,
|
||||
status: response.status,
|
||||
})
|
||||
if (!text) return {}
|
||||
|
||||
const parsed = decodeJson(text)
|
||||
if (Option.isNone(parsed))
|
||||
return yield* new HoneycombApiError({ message: `Honeycomb ${method} ${path} returned invalid JSON` })
|
||||
return parsed.value
|
||||
})
|
||||
|
||||
const saveRows: (rows: StatRow[]) => Effect.Effect<void, StatDatabaseError, never> = Effect.fn("StatsCron.saveRows")(
|
||||
function* (rows: StatRow[]) {
|
||||
const db = drizzle({
|
||||
client: new Client({
|
||||
host: Resource.StatsDatabase.host,
|
||||
username: Resource.StatsDatabase.username,
|
||||
password: Resource.StatsDatabase.password,
|
||||
}),
|
||||
})
|
||||
|
||||
yield* Effect.forEach(
|
||||
chunks(rows, UPSERT_CHUNK_SIZE),
|
||||
(chunk) =>
|
||||
Effect.tryPromise({
|
||||
try: () =>
|
||||
db
|
||||
.insert(stat)
|
||||
.values(chunk)
|
||||
.onDuplicateKeyUpdate({
|
||||
set: {
|
||||
period_end: inserted("period_end"),
|
||||
provider_model: inserted("provider_model"),
|
||||
sessions: inserted("sessions"),
|
||||
requests: inserted("requests"),
|
||||
input_tokens: inserted("input_tokens"),
|
||||
output_tokens: inserted("output_tokens"),
|
||||
reasoning_tokens: inserted("reasoning_tokens"),
|
||||
cache_read_tokens: inserted("cache_read_tokens"),
|
||||
total_tokens: inserted("total_tokens"),
|
||||
input_cost_microcents: inserted("input_cost_microcents"),
|
||||
output_cost_microcents: inserted("output_cost_microcents"),
|
||||
total_cost_microcents: inserted("total_cost_microcents"),
|
||||
avg_duration_ms: inserted("avg_duration_ms"),
|
||||
p50_duration_ms: inserted("p50_duration_ms"),
|
||||
p95_duration_ms: inserted("p95_duration_ms"),
|
||||
avg_ttfb_ms: inserted("avg_ttfb_ms"),
|
||||
p50_ttfb_ms: inserted("p50_ttfb_ms"),
|
||||
p95_ttfb_ms: inserted("p95_ttfb_ms"),
|
||||
avg_output_tps: inserted("avg_output_tps"),
|
||||
success_count: inserted("success_count"),
|
||||
error_count: inserted("error_count"),
|
||||
sample_count: inserted("sample_count"),
|
||||
rank_by_tokens: inserted("rank_by_tokens"),
|
||||
rank_by_requests: inserted("rank_by_requests"),
|
||||
rank_by_cost: inserted("rank_by_cost"),
|
||||
},
|
||||
}),
|
||||
catch: (cause) => new StatDatabaseError({ message: "Failed to upsert stats rows", cause }),
|
||||
}),
|
||||
{ discard: true },
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
function logHoneycombRuntimeCheck() {
|
||||
return Effect.logInfo("honeycomb api key runtime check").pipe(
|
||||
Effect.annotateLogs({
|
||||
hasHoneycombApiKey: Boolean(Resource.HONEYCOMB_API_KEY.value),
|
||||
honeycombApiKeyLength: Resource.HONEYCOMB_API_KEY.value.length,
|
||||
honeycombApiKeySha256: createHash("sha256").update(Resource.HONEYCOMB_API_KEY.value).digest("hex").slice(0, 12),
|
||||
honeycombApiUrl: HONEYCOMB_API_URL,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function inserted(column: string) {
|
||||
return sql.raw(`values(\`${column}\`)`)
|
||||
}
|
||||
|
||||
function toStatRow(grain: "day" | "week", periodStart: Date, periodEnd: Date, data: HoneycombData): StatRow {
|
||||
return {
|
||||
grain,
|
||||
period_start: periodStart,
|
||||
period_end: periodEnd,
|
||||
dataset: HONEYCOMB_DATASET,
|
||||
tier: normalizeTier(asString(data.tier) || "unknown"),
|
||||
client: "all",
|
||||
source: "all",
|
||||
provider: asString(data.provider) || "unknown",
|
||||
model: asString(data.model) || "unknown",
|
||||
provider_model: "",
|
||||
sessions: Math.round(number(data, "COUNT_DISTINCT(session)")),
|
||||
requests: Math.round(number(data, "COUNT")),
|
||||
input_tokens: Math.round(number(data, "SUM(tokens.input)")),
|
||||
output_tokens: Math.round(number(data, "SUM(tokens.output)")),
|
||||
reasoning_tokens: Math.round(number(data, "SUM(tokens.reasoning)")),
|
||||
cache_read_tokens: Math.round(number(data, "SUM(tokens.cache_read)")),
|
||||
total_tokens: Math.round(number(data, "SUM(tokens)")),
|
||||
input_cost_microcents: Math.round(number(data, "SUM(cost.input.microcents)")),
|
||||
output_cost_microcents: Math.round(number(data, "SUM(cost.output.microcents)")),
|
||||
total_cost_microcents: Math.round(number(data, "SUM(cost.total.microcents)")),
|
||||
avg_duration_ms: nullableNumber(data, "AVG(duration)"),
|
||||
p50_duration_ms: nullableInteger(data, "P50(duration)"),
|
||||
p95_duration_ms: nullableInteger(data, "P95(duration)"),
|
||||
avg_ttfb_ms: nullableNumber(data, "AVG(time_to_first_byte)"),
|
||||
p50_ttfb_ms: nullableInteger(data, "P50(time_to_first_byte)"),
|
||||
p95_ttfb_ms: nullableInteger(data, "P95(time_to_first_byte)"),
|
||||
avg_output_tps: nullableNumber(data, "AVG(tps.output)"),
|
||||
success_count: Math.round(number(data, "SUM(stats_success)")),
|
||||
error_count: Math.round(number(data, "SUM(stats_error)")),
|
||||
sample_count: Math.round(number(data, "COUNT")),
|
||||
}
|
||||
}
|
||||
|
||||
function synthesizeAllTierRows(rows: StatRow[]) {
|
||||
return [
|
||||
...rows,
|
||||
...Object.values(
|
||||
rows.reduce<Record<string, StatRow>>((result, row) => {
|
||||
const key = [
|
||||
row.grain,
|
||||
row.period_start.toISOString(),
|
||||
row.dataset,
|
||||
row.client,
|
||||
row.source,
|
||||
row.provider,
|
||||
row.model,
|
||||
].join("\u0000")
|
||||
result[key] = result[key] ? combineRows(result[key], row) : { ...row, tier: "all" }
|
||||
return result
|
||||
}, {}),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
function collapseRows(rows: StatRow[]) {
|
||||
return Object.values(
|
||||
rows.reduce<Record<string, StatRow>>((result, row) => {
|
||||
const key = [
|
||||
row.grain,
|
||||
row.period_start.toISOString(),
|
||||
row.dataset,
|
||||
row.tier,
|
||||
row.client,
|
||||
row.source,
|
||||
row.provider,
|
||||
row.model,
|
||||
].join("\u0000")
|
||||
result[key] = result[key] ? combineRows(result[key], row) : row
|
||||
return result
|
||||
}, {}),
|
||||
)
|
||||
}
|
||||
|
||||
function combineRows(left: StatRow, right: StatRow): StatRow {
|
||||
return {
|
||||
...left,
|
||||
period_end: right.period_end > left.period_end ? right.period_end : left.period_end,
|
||||
sessions: (left.sessions ?? 0) + (right.sessions ?? 0),
|
||||
requests: (left.requests ?? 0) + (right.requests ?? 0),
|
||||
input_tokens: (left.input_tokens ?? 0) + (right.input_tokens ?? 0),
|
||||
output_tokens: (left.output_tokens ?? 0) + (right.output_tokens ?? 0),
|
||||
reasoning_tokens: (left.reasoning_tokens ?? 0) + (right.reasoning_tokens ?? 0),
|
||||
cache_read_tokens: (left.cache_read_tokens ?? 0) + (right.cache_read_tokens ?? 0),
|
||||
total_tokens: (left.total_tokens ?? 0) + (right.total_tokens ?? 0),
|
||||
input_cost_microcents: (left.input_cost_microcents ?? 0) + (right.input_cost_microcents ?? 0),
|
||||
output_cost_microcents: (left.output_cost_microcents ?? 0) + (right.output_cost_microcents ?? 0),
|
||||
total_cost_microcents: (left.total_cost_microcents ?? 0) + (right.total_cost_microcents ?? 0),
|
||||
avg_duration_ms: weightedAverage(left.avg_duration_ms, left.requests, right.avg_duration_ms, right.requests),
|
||||
p50_duration_ms: null,
|
||||
p95_duration_ms: null,
|
||||
avg_ttfb_ms: weightedAverage(left.avg_ttfb_ms, left.requests, right.avg_ttfb_ms, right.requests),
|
||||
p50_ttfb_ms: null,
|
||||
p95_ttfb_ms: null,
|
||||
avg_output_tps: weightedAverage(left.avg_output_tps, left.requests, right.avg_output_tps, right.requests),
|
||||
success_count: (left.success_count ?? 0) + (right.success_count ?? 0),
|
||||
error_count: (left.error_count ?? 0) + (right.error_count ?? 0),
|
||||
sample_count: (left.sample_count ?? 0) + (right.sample_count ?? 0),
|
||||
}
|
||||
}
|
||||
|
||||
function rankRows(rows: StatRow[]) {
|
||||
return Object.values(
|
||||
rows.reduce<Record<string, StatRow[]>>((result, row) => {
|
||||
const key = [row.grain, row.period_start.toISOString(), row.dataset, row.tier, row.client, row.source].join(
|
||||
"\u0000",
|
||||
)
|
||||
result[key] = [...(result[key] ?? []), row]
|
||||
return result
|
||||
}, {}),
|
||||
).flatMap((group) => {
|
||||
const tokenRanks = rankBy(group, (row) => row.total_tokens ?? 0)
|
||||
const requestRanks = rankBy(group, (row) => row.requests ?? 0)
|
||||
const costRanks = rankBy(group, (row) => row.total_cost_microcents ?? 0)
|
||||
return group.map((row) => ({
|
||||
...row,
|
||||
rank_by_tokens: tokenRanks.get(row) ?? null,
|
||||
rank_by_requests: requestRanks.get(row) ?? null,
|
||||
rank_by_cost: costRanks.get(row) ?? null,
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
function rankBy(rows: StatRow[], value: (row: StatRow) => number) {
|
||||
return new Map(rows.toSorted((a, b) => value(b) - value(a)).map((row, index) => [row, index + 1]))
|
||||
}
|
||||
|
||||
function chunks<T>(items: T[], size: number) {
|
||||
return Array.from({ length: Math.ceil(items.length / size) }, (_, index) =>
|
||||
items.slice(index * size, (index + 1) * size),
|
||||
)
|
||||
}
|
||||
|
||||
function weightedAverage(
|
||||
left: number | null | undefined,
|
||||
leftWeight = 0,
|
||||
right: number | null | undefined,
|
||||
rightWeight = 0,
|
||||
) {
|
||||
const totalWeight =
|
||||
(left === null || left === undefined ? 0 : leftWeight) + (right === null || right === undefined ? 0 : rightWeight)
|
||||
if (totalWeight === 0) return null
|
||||
return Number((((left ?? 0) * leftWeight + (right ?? 0) * rightWeight) / totalWeight).toFixed(2))
|
||||
}
|
||||
|
||||
function normalizeTier(value: string) {
|
||||
if (value === "Paid") return "Zen"
|
||||
return value
|
||||
}
|
||||
|
||||
function number(data: HoneycombData, key: string) {
|
||||
const value = data[key]
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value : 0
|
||||
if (typeof value === "string") {
|
||||
const parsed = Number(value)
|
||||
return Number.isFinite(parsed) ? parsed : 0
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function nullableNumber(data: HoneycombData, key: string) {
|
||||
const value = number(data, key)
|
||||
if (value === 0 && data[key] === undefined) return null
|
||||
return Number(value.toFixed(2))
|
||||
}
|
||||
|
||||
function nullableInteger(data: HoneycombData, key: string) {
|
||||
if (data[key] === undefined) return null
|
||||
return Math.round(number(data, key))
|
||||
}
|
||||
|
||||
function asData(value: unknown): HoneycombData {
|
||||
return Object.fromEntries(
|
||||
Object.entries(asRecord(value)).flatMap(([key, item]) => {
|
||||
if (typeof item === "string" || typeof item === "number" || typeof item === "boolean" || item === null)
|
||||
return [[key, item]]
|
||||
return []
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return {}
|
||||
return value as Record<string, unknown>
|
||||
}
|
||||
|
||||
function asArray(value: unknown) {
|
||||
if (!Array.isArray(value)) return []
|
||||
return value
|
||||
}
|
||||
|
||||
function asString(value: unknown) {
|
||||
if (typeof value === "string") return value
|
||||
if (typeof value === "number" || typeof value === "boolean") return String(value)
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Client } from "@planetscale/database"
|
||||
import { drizzle } from "drizzle-orm/planetscale-serverless"
|
||||
import { migrate as drizzleMigrate } from "drizzle-orm/planetscale-serverless/migrator"
|
||||
import { Config, ConfigProvider, Effect, Layer, Schema } from "effect"
|
||||
import * as Context from "effect/Context"
|
||||
import * as schema from "./database/schema"
|
||||
import { Resource } from "sst"
|
||||
|
||||
export const DatabaseUrl = Schema.NonEmptyString.pipe(Schema.brand("DatabaseUrl"))
|
||||
export type DatabaseUrl = typeof DatabaseUrl.Type
|
||||
@@ -13,9 +17,7 @@ export class DatabaseSettings extends Schema.Class<DatabaseSettings>("DatabaseSe
|
||||
const decodeDatabaseSettings = Schema.decodeUnknownSync(DatabaseSettings)
|
||||
|
||||
const config = Config.all({
|
||||
url: Config.nonEmptyString("DATABASE_URL").pipe(
|
||||
Config.withDefault("mysql://root:changeme@localhost:3306/opencode_stats"),
|
||||
),
|
||||
url: Config.nonEmptyString("DATABASE_URL").pipe(Config.withDefault(Resource.StatsDatabase.url)),
|
||||
migrationsDir: Config.nonEmptyString("DATABASE_MIGRATIONS_DIR").pipe(Config.withDefault("./migrations")),
|
||||
}).pipe(Config.map(decodeDatabaseSettings))
|
||||
|
||||
@@ -53,7 +55,21 @@ export class MigrationError extends Schema.TaggedErrorClass<MigrationError>()("M
|
||||
|
||||
export const migrate = Effect.fn("Database.migrate")(function* () {
|
||||
const settings = yield* DatabaseConfig
|
||||
yield* Effect.logInfo("database migrations are not wired yet").pipe(
|
||||
yield* Effect.logInfo("applying database migrations").pipe(
|
||||
Effect.annotateLogs({ migrationsDir: settings.migrationsDir }),
|
||||
)
|
||||
const result = yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
drizzleMigrate(drizzle({ client: new Client({ url: settings.url }) }), {
|
||||
migrationsFolder: settings.migrationsDir,
|
||||
}),
|
||||
catch: (cause) => new MigrationError({ message: "Failed to apply database migrations", cause }),
|
||||
})
|
||||
if (result)
|
||||
return yield* new MigrationError({
|
||||
message: `Failed to initialize database migrations: ${result.exitCode}`,
|
||||
})
|
||||
yield* Effect.logInfo("database migrations complete").pipe(
|
||||
Effect.annotateLogs({ migrationsDir: settings.migrationsDir }),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { Schema } from "effect"
|
||||
import { Client } from "@planetscale/database"
|
||||
import { and, asc, eq } from "drizzle-orm"
|
||||
import { drizzle } from "drizzle-orm/planetscale-serverless"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { DatabaseConfig } from "../database"
|
||||
import { stat } from "../database/schema"
|
||||
|
||||
export const RankingSnapshotId = Schema.String.check(Schema.isStartsWith("rank_"), Schema.isMaxLength(64)).pipe(
|
||||
Schema.brand("RankingSnapshotId"),
|
||||
@@ -20,3 +25,418 @@ export class RankingSnapshot extends Schema.Class<RankingSnapshot>("RankingSnaps
|
||||
capturedAt: Schema.Date,
|
||||
createdAt: Schema.Date,
|
||||
}) {}
|
||||
|
||||
export type UsageProduct = "All Users" | "Zen" | "Go" | "Enterprise"
|
||||
export type TokenProduct = "Zen" | "Go" | "Enterprise"
|
||||
export type UsageRange = "1D" | "1W" | "1M" | "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 TokenCostEntry = { model: string; total: number; input: number; output: number; cached: number }
|
||||
export type SessionCostEntry = { model: string; cost: number; tokens: number }
|
||||
export type RankingsData = {
|
||||
updatedAt: string | null
|
||||
usage: Record<UsageProduct, Record<UsageRange, UsagePoint[]>>
|
||||
leaderboard: Record<UsageProduct, Record<UsageRange, LeaderboardEntry[]>>
|
||||
market: Record<UsageRange, MarketDay[]>
|
||||
tokenCost: Record<TokenProduct, TokenCostEntry[]>
|
||||
sessionCost: Record<TokenProduct, SessionCostEntry[]>
|
||||
}
|
||||
|
||||
export class RankingQueryError extends Schema.TaggedErrorClass<RankingQueryError>()("RankingQueryError", {
|
||||
message: Schema.String,
|
||||
cause: Schema.optional(Schema.Defect),
|
||||
}) {}
|
||||
|
||||
const DAY_MS = 86_400_000
|
||||
const TOKEN_SCALE = 1_000_000
|
||||
const DOLLARS_PER_MICROCENT = 1 / 100_000_000
|
||||
const months = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"] as const
|
||||
|
||||
type StatQueryRow = {
|
||||
periodStart: Date
|
||||
periodEnd: Date
|
||||
tier: string
|
||||
provider: string
|
||||
model: string
|
||||
sessions: number
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
reasoningTokens: number
|
||||
cacheReadTokens: number
|
||||
totalTokens: number
|
||||
inputCostMicrocents: number
|
||||
outputCostMicrocents: number
|
||||
totalCostMicrocents: number
|
||||
}
|
||||
|
||||
type StatMetricRow = Omit<StatQueryRow, "periodStart" | "periodEnd"> & {
|
||||
periodStart: number
|
||||
periodEnd: number
|
||||
}
|
||||
|
||||
type DateWindow = { start: number; end: number; previousStart: number; previousEnd: number }
|
||||
type Bucket = { start: number; end: number; label: string }
|
||||
type ModelAggregate = {
|
||||
model: string
|
||||
provider: string
|
||||
sessions: number
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
reasoningTokens: number
|
||||
cacheReadTokens: number
|
||||
totalTokens: number
|
||||
inputCostMicrocents: number
|
||||
outputCostMicrocents: number
|
||||
totalCostMicrocents: number
|
||||
}
|
||||
|
||||
export const getRankingsData = Effect.fn("Ranking.getRankingsData")(function* () {
|
||||
const settings = yield* DatabaseConfig
|
||||
const db = drizzle({ client: new Client({ url: settings.url }) })
|
||||
const rows = yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
db
|
||||
.select({
|
||||
periodStart: stat.period_start,
|
||||
periodEnd: stat.period_end,
|
||||
tier: stat.tier,
|
||||
provider: stat.provider,
|
||||
model: stat.model,
|
||||
sessions: stat.sessions,
|
||||
inputTokens: stat.input_tokens,
|
||||
outputTokens: stat.output_tokens,
|
||||
reasoningTokens: stat.reasoning_tokens,
|
||||
cacheReadTokens: stat.cache_read_tokens,
|
||||
totalTokens: stat.total_tokens,
|
||||
inputCostMicrocents: stat.input_cost_microcents,
|
||||
outputCostMicrocents: stat.output_cost_microcents,
|
||||
totalCostMicrocents: stat.total_cost_microcents,
|
||||
})
|
||||
.from(stat)
|
||||
.where(and(eq(stat.grain, "day"), eq(stat.client, "all"), eq(stat.source, "all")))
|
||||
.orderBy(asc(stat.period_start)),
|
||||
catch: (cause) => new RankingQueryError({ message: "Failed to load rankings stats", cause }),
|
||||
})
|
||||
return buildRankingsData(rows)
|
||||
})
|
||||
|
||||
function buildRankingsData(rows: StatQueryRow[]): RankingsData {
|
||||
const normalized = rows.flatMap(normalizeStatRow)
|
||||
if (normalized.length === 0) return emptyRankingsData()
|
||||
|
||||
const earliest = Math.min(...normalized.map((row) => row.periodStart))
|
||||
const latest = Math.max(...normalized.map((row) => row.periodStart))
|
||||
const latestEnd = Math.max(...normalized.map((row) => row.periodEnd))
|
||||
|
||||
return {
|
||||
updatedAt: new Date(latestEnd).toISOString(),
|
||||
usage: createUsageProductRecord((product) =>
|
||||
createRangeRecord((range) => buildUsagePoints(normalized, product, range, getWindow(range, earliest, latest))),
|
||||
),
|
||||
leaderboard: createUsageProductRecord((product) =>
|
||||
createRangeRecord((range) => buildLeaderboard(normalized, product, getWindow(range, earliest, latest))),
|
||||
),
|
||||
market: createRangeRecord((range) => buildMarketShare(normalized, range, getWindow(range, earliest, latest))),
|
||||
tokenCost: createTokenProductRecord((product) =>
|
||||
buildTokenCost(normalized, product, getWindow("1W", earliest, latest)),
|
||||
),
|
||||
sessionCost: createTokenProductRecord((product) =>
|
||||
buildSessionCost(normalized, product, getWindow("1W", earliest, latest)),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
function emptyRankingsData(): RankingsData {
|
||||
return {
|
||||
updatedAt: null,
|
||||
usage: createUsageProductRecord(() => createRangeRecord(() => [])),
|
||||
leaderboard: createUsageProductRecord(() => createRangeRecord(() => [])),
|
||||
market: createRangeRecord(() => []),
|
||||
tokenCost: createTokenProductRecord(() => []),
|
||||
sessionCost: createTokenProductRecord(() => []),
|
||||
}
|
||||
}
|
||||
|
||||
function buildUsagePoints(rows: StatMetricRow[], product: UsageProduct, range: UsageRange, window: DateWindow) {
|
||||
const windowRows = rowsForProduct(rows, product, window.start, window.end)
|
||||
const modelOrder = aggregateByModel(windowRows)
|
||||
.toSorted((a, b) => b.totalTokens - a.totalTokens)
|
||||
.slice(0, 6)
|
||||
.map((item) => ({ key: modelKey(item.provider, item.model), model: item.model }))
|
||||
|
||||
return createBuckets(window, range).map((bucket) => {
|
||||
const bucketRows = aggregateByModel(rowsForProduct(rows, product, bucket.start, bucket.end))
|
||||
const byModel = new Map(bucketRows.map((item) => [modelKey(item.provider, item.model), item.totalTokens]))
|
||||
const segmentTokens = modelOrder.map((model) => ({ model: model.model, tokens: byModel.get(model.key) ?? 0 }))
|
||||
const knownTokens = segmentTokens.reduce((sum, item) => sum + item.tokens, 0)
|
||||
const totalTokens = bucketRows.reduce((sum, item) => sum + item.totalTokens, 0)
|
||||
return {
|
||||
date: bucket.label,
|
||||
segments: [
|
||||
...segmentTokens.map((item) => ({ model: item.model, value: round(item.tokens / 1_000_000_000_000, 2) })),
|
||||
{ model: "Other", value: round(Math.max(totalTokens - knownTokens, 0) / 1_000_000_000_000, 2) },
|
||||
].filter((item) => item.value > 0),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function buildLeaderboard(rows: StatMetricRow[], product: UsageProduct, window: DateWindow) {
|
||||
const previous = new Map(
|
||||
aggregateByModel(rowsForProduct(rows, product, window.previousStart, window.previousEnd)).map((item) => [
|
||||
modelKey(item.provider, item.model),
|
||||
item.totalTokens,
|
||||
]),
|
||||
)
|
||||
|
||||
return aggregateByModel(rowsForProduct(rows, product, window.start, window.end))
|
||||
.toSorted((a, b) => b.totalTokens - a.totalTokens)
|
||||
.slice(0, 13)
|
||||
.map((item, index) => ({
|
||||
model: item.model,
|
||||
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),
|
||||
rank: index + 1,
|
||||
}))
|
||||
}
|
||||
|
||||
function buildMarketShare(rows: StatMetricRow[], range: UsageRange, window: DateWindow) {
|
||||
return createBuckets(window, range).flatMap((bucket) => {
|
||||
const total = aggregateByProvider(rowsForProduct(rows, "All Users", bucket.start, bucket.end)).toSorted(
|
||||
(a, b) => b.tokens - a.tokens,
|
||||
)
|
||||
const totalTokens = total.reduce((sum, item) => sum + item.tokens, 0)
|
||||
if (totalTokens === 0) return []
|
||||
|
||||
const authors = total.slice(0, 8)
|
||||
const knownTokens = authors.reduce((sum, item) => sum + item.tokens, 0)
|
||||
const withOther = [...authors, { provider: "Other", tokens: Math.max(totalTokens - knownTokens, 0) }].filter(
|
||||
(item) => item.tokens > 0,
|
||||
)
|
||||
|
||||
return [
|
||||
{
|
||||
date: bucket.label,
|
||||
total: round(totalTokens / 1_000_000_000_000, 2),
|
||||
authors: withOther.map((item) => ({
|
||||
author: item.provider === "Other" ? "Other" : formatProvider(item.provider),
|
||||
share: round((item.tokens / totalTokens) * 100, 1),
|
||||
tokens: round(item.tokens / 1_000_000_000_000, 2),
|
||||
})),
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
function buildTokenCost(rows: StatMetricRow[], product: TokenProduct, window: DateWindow) {
|
||||
return aggregateByModel(rowsForProduct(rows, product, window.start, window.end))
|
||||
.flatMap((item) => {
|
||||
const total = costPerMillion(item.totalCostMicrocents, item.totalTokens)
|
||||
if (total === 0) return []
|
||||
return [
|
||||
{
|
||||
model: item.model,
|
||||
total,
|
||||
input: costPerMillion(item.inputCostMicrocents, item.inputTokens),
|
||||
output: costPerMillion(item.outputCostMicrocents, item.outputTokens + item.reasoningTokens),
|
||||
cached: costPerMillion(item.inputCostMicrocents, item.inputTokens + item.cacheReadTokens),
|
||||
},
|
||||
]
|
||||
})
|
||||
.toSorted((a, b) => a.total - b.total)
|
||||
.slice(0, 17)
|
||||
}
|
||||
|
||||
function buildSessionCost(rows: StatMetricRow[], product: TokenProduct, window: DateWindow) {
|
||||
return aggregateByModel(rowsForProduct(rows, product, window.start, window.end))
|
||||
.flatMap((item) => {
|
||||
if (item.sessions === 0) return []
|
||||
const cost = round(microcentsToDollars(item.totalCostMicrocents) / item.sessions, 4)
|
||||
if (cost === 0) return []
|
||||
return [{ model: item.model, cost, tokens: Math.round(item.totalTokens / item.sessions) }]
|
||||
})
|
||||
.toSorted((a, b) => a.cost - b.cost)
|
||||
.slice(0, 17)
|
||||
}
|
||||
|
||||
function rowsForProduct(rows: StatMetricRow[], product: UsageProduct, start: number, end: number) {
|
||||
const windowRows = rows.filter((row) => row.periodStart >= start && row.periodStart < end)
|
||||
if (product !== "All Users") return windowRows.filter((row) => row.tier === product)
|
||||
|
||||
const allRows = windowRows.filter((row) => row.tier === "all")
|
||||
if (allRows.length > 0) return allRows
|
||||
return windowRows.filter((row) => row.tier !== "all")
|
||||
}
|
||||
|
||||
function aggregateByModel(rows: StatMetricRow[]) {
|
||||
return Object.values(
|
||||
rows.reduce<Record<string, ModelAggregate>>((result, row) => {
|
||||
const key = modelKey(row.provider, row.model)
|
||||
result[key] = combineModelAggregate(result[key], row)
|
||||
return result
|
||||
}, {}),
|
||||
)
|
||||
}
|
||||
|
||||
function aggregateByProvider(rows: StatMetricRow[]) {
|
||||
return Object.values(
|
||||
rows.reduce<Record<string, { provider: string; tokens: number }>>((result, row) => {
|
||||
result[row.provider] = {
|
||||
provider: row.provider,
|
||||
tokens: (result[row.provider]?.tokens ?? 0) + row.totalTokens,
|
||||
}
|
||||
return result
|
||||
}, {}),
|
||||
)
|
||||
}
|
||||
|
||||
function combineModelAggregate(current: ModelAggregate | undefined, row: StatMetricRow): ModelAggregate {
|
||||
return {
|
||||
model: row.model,
|
||||
provider: row.provider,
|
||||
sessions: (current?.sessions ?? 0) + row.sessions,
|
||||
inputTokens: (current?.inputTokens ?? 0) + row.inputTokens,
|
||||
outputTokens: (current?.outputTokens ?? 0) + row.outputTokens,
|
||||
reasoningTokens: (current?.reasoningTokens ?? 0) + row.reasoningTokens,
|
||||
cacheReadTokens: (current?.cacheReadTokens ?? 0) + row.cacheReadTokens,
|
||||
totalTokens: (current?.totalTokens ?? 0) + row.totalTokens,
|
||||
inputCostMicrocents: (current?.inputCostMicrocents ?? 0) + row.inputCostMicrocents,
|
||||
outputCostMicrocents: (current?.outputCostMicrocents ?? 0) + row.outputCostMicrocents,
|
||||
totalCostMicrocents: (current?.totalCostMicrocents ?? 0) + row.totalCostMicrocents,
|
||||
}
|
||||
}
|
||||
|
||||
function getWindow(range: UsageRange, earliest: number, latest: number): DateWindow {
|
||||
const end = latest + DAY_MS
|
||||
const start = Math.max(
|
||||
earliest,
|
||||
range === "1D"
|
||||
? latest
|
||||
: range === "1W"
|
||||
? latest - 6 * DAY_MS
|
||||
: range === "1M"
|
||||
? latest - 29 * DAY_MS
|
||||
: range === "3M"
|
||||
? latest - 89 * DAY_MS
|
||||
: range === "YTD"
|
||||
? Date.UTC(new Date(latest).getUTCFullYear(), 0, 1)
|
||||
: earliest,
|
||||
)
|
||||
const duration = end - start
|
||||
return { start, end, previousStart: start - duration, previousEnd: start }
|
||||
}
|
||||
|
||||
function createBuckets(window: DateWindow, range: UsageRange): Bucket[] {
|
||||
const span = Math.max(window.end - window.start, DAY_MS)
|
||||
const count = Math.max(1, Math.min(7, Math.ceil(span / DAY_MS)))
|
||||
const size = span / count
|
||||
return Array.from({ length: count }, (_, index) => {
|
||||
const start = window.start + index * size
|
||||
const end = index === count - 1 ? window.end : window.start + (index + 1) * size
|
||||
return { start, end, label: formatBucketLabel(start, range) }
|
||||
})
|
||||
}
|
||||
|
||||
function createUsageProductRecord<T>(value: (product: UsageProduct) => T): Record<UsageProduct, T> {
|
||||
return {
|
||||
"All Users": value("All Users"),
|
||||
Zen: value("Zen"),
|
||||
Go: value("Go"),
|
||||
Enterprise: value("Enterprise"),
|
||||
}
|
||||
}
|
||||
|
||||
function createTokenProductRecord<T>(value: (product: TokenProduct) => T): Record<TokenProduct, T> {
|
||||
return {
|
||||
Zen: value("Zen"),
|
||||
Go: value("Go"),
|
||||
Enterprise: value("Enterprise"),
|
||||
}
|
||||
}
|
||||
|
||||
function createRangeRecord<T>(value: (range: UsageRange) => T): Record<UsageRange, T> {
|
||||
return {
|
||||
"1D": value("1D"),
|
||||
"1W": value("1W"),
|
||||
"1M": value("1M"),
|
||||
"3M": value("3M"),
|
||||
YTD: value("YTD"),
|
||||
ALL: value("ALL"),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeStatRow(row: StatQueryRow): StatMetricRow[] {
|
||||
const periodStart = dateTime(row.periodStart)
|
||||
const periodEnd = dateTime(row.periodEnd)
|
||||
if (!Number.isFinite(periodStart) || !Number.isFinite(periodEnd)) return []
|
||||
return [
|
||||
{
|
||||
...row,
|
||||
periodStart,
|
||||
periodEnd,
|
||||
tier: normalizeTier(row.tier),
|
||||
provider: row.provider || "unknown",
|
||||
model: row.model || "unknown",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function normalizeTier(value: string) {
|
||||
const normalized = value.toLowerCase()
|
||||
if (normalized === "paid" || normalized === "zen") return "Zen"
|
||||
if (normalized === "go") return "Go"
|
||||
if (normalized === "enterprise") return "Enterprise"
|
||||
if (normalized === "all") return "all"
|
||||
return value
|
||||
}
|
||||
|
||||
function dateTime(value: Date | string) {
|
||||
return (value instanceof Date ? value : new Date(value)).getTime()
|
||||
}
|
||||
|
||||
function formatBucketLabel(value: number, range: UsageRange) {
|
||||
const date = new Date(value)
|
||||
if (range === "YTD") return months[date.getUTCMonth()]
|
||||
if (range === "ALL")
|
||||
return date.getUTCFullYear() === new Date().getUTCFullYear()
|
||||
? months[date.getUTCMonth()]
|
||||
: String(date.getUTCFullYear())
|
||||
return `${months[date.getUTCMonth()]} ${date.getUTCDate()}`
|
||||
}
|
||||
|
||||
function formatProvider(provider: string) {
|
||||
const known: Record<string, string> = {
|
||||
anthropic: "Anthropic",
|
||||
google: "Google",
|
||||
minimax: "MiniMax",
|
||||
moonshotai: "Moonshot",
|
||||
nvidia: "Nvidia",
|
||||
openai: "OpenAI",
|
||||
zhipuai: "Zhipu",
|
||||
}
|
||||
const normalized = provider.toLowerCase().replace(/[^a-z0-9]/g, "")
|
||||
return known[normalized] ?? provider.replace(/[-_]/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase())
|
||||
}
|
||||
|
||||
function modelKey(provider: string, model: string) {
|
||||
return `${provider}\u0000${model}`
|
||||
}
|
||||
|
||||
function costPerMillion(costMicrocents: number, tokens: number) {
|
||||
if (tokens <= 0 || costMicrocents <= 0) return 0
|
||||
return round((microcentsToDollars(costMicrocents) / tokens) * TOKEN_SCALE, 2)
|
||||
}
|
||||
|
||||
function microcentsToDollars(value: number) {
|
||||
return value * DOLLARS_PER_MICROCENT
|
||||
}
|
||||
|
||||
function percentChange(current: number, previous: number) {
|
||||
if (previous <= 0) return current > 0 ? 100 : 0
|
||||
return Math.round(((current - previous) / previous) * 100)
|
||||
}
|
||||
|
||||
function round(value: number, digits: number) {
|
||||
return Number(value.toFixed(digits))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import { Effect } from "effect"
|
||||
import { layer, migrate } from "./database"
|
||||
|
||||
await Effect.runPromise(migrate().pipe(Effect.provide(layer)))
|
||||
Reference in New Issue
Block a user