feat(stats): refresh stats routes and homepage (#30419)

This commit is contained in:
Adam 2026-06-04 19:32:49 -05:00 committed by GitHub
commit 47f3d8b709
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 3299 additions and 662 deletions

View file

@ -37,7 +37,10 @@ export type GeoStatMetric = {
export declare namespace GeoStatRepo {
export interface Service {
readonly listDaily: () => Effect.Effect<GeoStatMetric[], DatabaseError>
readonly listDaily: (opts?: {
readonly provider?: string
readonly model?: string
}) => Effect.Effect<GeoStatMetric[], DatabaseError>
readonly listByPeriod: (opts: {
readonly grain: string
readonly periodKey: string
@ -59,7 +62,16 @@ export class GeoStatRepo extends Context.Service<GeoStatRepo, GeoStatRepo.Servic
Effect.gen(function* () {
const db = yield* DrizzleClient
const listDaily = Effect.fn("GeoStatRepo.listDaily")(function* () {
const listDaily = Effect.fn("GeoStatRepo.listDaily")(function* (opts?: {
readonly provider?: string
readonly model?: string
}) {
const scope =
opts?.model && opts.provider
? and(eq(geoStat.provider, opts.provider), eq(geoStat.model, opts.model))
: opts?.model
? eq(geoStat.model, opts.model)
: and(eq(geoStat.provider, "all"), eq(geoStat.model, "all"))
return yield* Effect.tryPromise({
try: () =>
db
@ -74,15 +86,7 @@ export class GeoStatRepo extends Context.Service<GeoStatRepo, GeoStatRepo.Servic
totalTokens: geoStat.total_tokens,
})
.from(geoStat)
.where(
and(
eq(geoStat.grain, "day"),
eq(geoStat.client, "all"),
eq(geoStat.source, "all"),
eq(geoStat.provider, "all"),
eq(geoStat.model, "all"),
),
)
.where(and(eq(geoStat.grain, "day"), eq(geoStat.client, "all"), eq(geoStat.source, "all"), scope))
.orderBy(asc(geoStat.period_key)),
catch: (cause) => DatabaseError.make({ cause }),
})

View file

@ -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<UsageRange, CountryEntry[]>
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<UsageProduct, Record<UsageRange, UsagePoint[]>>
@ -74,6 +141,35 @@ export const getStatsHomeData: () => Effect.Effect<
return buildStatsHomeData(modelRows, providerRows, geoRows)
})
export const getStatsModelData: (
model: string,
provider?: string,
) => Effect.Effect<StatsModelData | null, DatabaseError, ModelStatRepo | GeoStatRepo> = 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<StatsLabData | null, DatabaseError, ModelStatRepo> =
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<T extends { periodStart: number; tier: string }>(
rows: T[],
product: UsageProduct,
@ -285,6 +555,15 @@ function aggregateByModel(rows: StatMetricRow[]) {
)
}
function aggregateByModelName(rows: StatMetricRow[]) {
return Object.values(
rows.reduce<Record<string, ModelAggregate>>((result, row) => {
result[row.model] = combineModelAggregate(result[row.model], row)
return result
}, {}),
)
}
function aggregateByProvider(rows: ProviderMetricRow[]) {
return Object.values(
rows.reduce<Record<string, { provider: string; tokens: number }>>((result, row) => {
@ -310,6 +589,27 @@ function aggregateByCountry(rows: GeoMetricRow[]) {
)
}
function combineRowsForModel(model: string, rows: StatMetricRow[]): ModelAggregate {
const aggregate = rows.reduce<ModelAggregate | undefined>(
(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}`
}