feat(stats): include free tier usage
This commit is contained in:
@@ -7,6 +7,7 @@ import { RETIRED_STAT_MODELS, RETIRED_STAT_PROVIDERS } from "./model-normalizati
|
||||
import {
|
||||
chunks,
|
||||
collapseRows,
|
||||
DATA_SITE_TIERS,
|
||||
inserted,
|
||||
isMissingUniqueUsersColumn,
|
||||
omitUniqueUsers,
|
||||
@@ -93,7 +94,7 @@ export class GeoStatRepo extends Context.Service<GeoStatRepo, GeoStatRepo.Servic
|
||||
eq(geoStat.grain, "day"),
|
||||
eq(geoStat.client, "all"),
|
||||
eq(geoStat.source, "all"),
|
||||
inArray(geoStat.tier, ["Go", "go"]),
|
||||
inArray(geoStat.tier, DATA_SITE_TIERS),
|
||||
scope,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ import { DatabaseError } from "../database"
|
||||
import type { GeoStatMetric } from "./geo"
|
||||
import { ModelStatRepo, type ModelStatMetric } from "./model"
|
||||
import type { ProviderStatMetric } from "./provider"
|
||||
import { DATA_SITE_TIERS, normalizeTier } from "./stat"
|
||||
|
||||
export type UsageProduct = "All Users" | "Zen" | "Go" | "Enterprise"
|
||||
export type TokenProduct = "Zen" | "Go" | "Enterprise"
|
||||
@@ -129,7 +130,9 @@ const TOKEN_SCALE = 1_000_000
|
||||
const DOLLARS_PER_MICROCENT = 1 / 100_000_000
|
||||
const METRIC_MODEL_LIMIT = 10
|
||||
const TOP_MODEL_SEGMENT_LIMIT = 9
|
||||
// Preserve the response shape while the public site presents Go and Free as one cohort.
|
||||
const SITE_PRODUCT = "Go"
|
||||
const SITE_TIER_PLACEHOLDERS = DATA_SITE_TIERS.map(() => "?").join(", ")
|
||||
const LEADERBOARD_CHANGE_MIN_MULTIPLE = 10
|
||||
const months = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"] as const
|
||||
|
||||
@@ -212,10 +215,13 @@ export function getStatsLabData(provider: string): Effect.Effect<StatsLabData |
|
||||
|
||||
async function listModelDaily(): Promise<ModelStatMetric[]> {
|
||||
return (
|
||||
await queryRows(`select period_key, updated_at, tier, provider, model, sessions, unique_users, input_tokens,
|
||||
await queryRows(
|
||||
`select period_key, updated_at, tier, provider, model, sessions, unique_users, input_tokens,
|
||||
output_tokens, reasoning_tokens, cache_read_tokens, total_tokens, input_cost_microcents, output_cost_microcents,
|
||||
total_cost_microcents from model_stat where grain = 'day' and client = 'all' and source = 'all'
|
||||
and tier in ('Go', 'go') order by period_key`)
|
||||
and tier in (${SITE_TIER_PLACEHOLDERS}) order by period_key`,
|
||||
DATA_SITE_TIERS,
|
||||
)
|
||||
).map((row) => ({
|
||||
periodKey: stringValue(row.period_key),
|
||||
updatedAt: dateValue(row.updated_at),
|
||||
@@ -237,8 +243,12 @@ async function listModelDaily(): Promise<ModelStatMetric[]> {
|
||||
|
||||
async function listProviderDaily(): Promise<ProviderStatMetric[]> {
|
||||
return (
|
||||
await queryRows(`select period_key, updated_at, tier, provider, total_tokens from provider_stat
|
||||
where grain = 'day' and client = 'all' and source = 'all' and tier in ('Go', 'go') order by period_key`)
|
||||
await queryRows(
|
||||
`select period_key, updated_at, tier, provider, total_tokens from provider_stat
|
||||
where grain = 'day' and client = 'all' and source = 'all'
|
||||
and tier in (${SITE_TIER_PLACEHOLDERS}) order by period_key`,
|
||||
DATA_SITE_TIERS,
|
||||
)
|
||||
).map((row) => ({
|
||||
periodKey: stringValue(row.period_key),
|
||||
updatedAt: dateValue(row.updated_at),
|
||||
@@ -259,8 +269,9 @@ async function listGeoDaily(opts?: { provider?: string; model?: string }): Promi
|
||||
return (
|
||||
await queryRows(
|
||||
`select period_key, updated_at, tier, provider, model, country, continent, total_tokens from geo_stat
|
||||
where grain = 'day' and client = 'all' and source = 'all' and tier in ('Go', 'go') ${scope} order by period_key`,
|
||||
params,
|
||||
where grain = 'day' and client = 'all' and source = 'all'
|
||||
and tier in (${SITE_TIER_PLACEHOLDERS}) ${scope} order by period_key`,
|
||||
[...DATA_SITE_TIERS, ...params],
|
||||
)
|
||||
).map((row) => ({
|
||||
periodKey: stringValue(row.period_key),
|
||||
@@ -735,6 +746,7 @@ function rowsForProduct<T extends { periodStart: number; tier: string }>(
|
||||
end: number,
|
||||
) {
|
||||
const windowRows = rows.filter((row) => row.periodStart >= start && row.periodStart < end)
|
||||
if (product === SITE_PRODUCT) return windowRows.filter((row) => row.tier === "Go" || row.tier === "Free")
|
||||
if (product !== "All Users") return windowRows.filter((row) => row.tier === product)
|
||||
|
||||
const allRows = windowRows.filter((row) => row.tier === "all")
|
||||
@@ -944,15 +956,6 @@ function normalizeGeoRow(row: GeoStatMetric): GeoMetricRow[] {
|
||||
]
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -97,7 +97,12 @@ describe("inference stat normalization", () => {
|
||||
expect(queries[6]).toContain("'2026-08-12' AS period_key")
|
||||
expect(queries[0]).toContain('FROM "inference"."generation"')
|
||||
expect(queries[0]).toContain("event_type = 'generation.completed'")
|
||||
expect(queries[0]).toContain("product = 'go'")
|
||||
expect(queries[0]).toContain("AND (product = 'go' OR (lower(COALESCE(model_tier, '')) = 'free'")
|
||||
expect(queries[0]).toContain("COALESCE(NULLIF(lower(model_tier), ''), '') AS raw_tier")
|
||||
expect(queries[0]).toContain("WHEN lower(COALESCE(raw_tier, '')) = 'free'")
|
||||
expect(queries[0]).toContain("OR lower(raw_model) IN ('gpt-5-nano', 'grok-code', 'big-pickle')")
|
||||
expect(queries[0]).toContain("OR lower(raw_model) LIKE '%-free'")
|
||||
expect(queries[0]).toContain("THEN 'Free'")
|
||||
expect(queries[0]).toContain("LIMIT 10000")
|
||||
expect(queries[0]).toContain("approx_distinct(session) AS sessions")
|
||||
expect(queries[1]).toContain("'geo_model' ELSE 'geo'")
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { GeoStatAggregate } from "./geo"
|
||||
import type { ModelStatAggregate } from "./model"
|
||||
import {
|
||||
EXCLUDED_MODELS,
|
||||
FREE_MODELS,
|
||||
MODEL_AUTHOR_RULES,
|
||||
RETIRED_STAT_PROVIDERS,
|
||||
statModel,
|
||||
@@ -53,6 +54,7 @@ function buildStatsQuery(
|
||||
const periodEndValue = sqlString(period.end.toISOString())
|
||||
const ingestEndValue = sqlString(new Date(period.end.getTime() + DAY_MS).toISOString())
|
||||
const sourceTable = [source.namespace, source.table].map(sqlIdentifier).join(".")
|
||||
const sourceFreeTier = freeTierSql("model_tier", "model_requested")
|
||||
const dimensions =
|
||||
family === "usage"
|
||||
? `CASE WHEN grouping(model) = 0 THEN 'model' ELSE 'provider' END AS dimension,
|
||||
@@ -107,6 +109,7 @@ function buildStatsQuery(
|
||||
WITH normalized AS (
|
||||
SELECT
|
||||
model_requested AS raw_model,
|
||||
COALESCE(NULLIF(lower(model_tier), ''), '') AS raw_tier,
|
||||
${statModelSql("model_requested", "route_model")} AS model,
|
||||
COALESCE(NULLIF(route_model, ''), '') AS provider_model,
|
||||
COALESCE(NULLIF(provider_id, ''), '') AS raw_provider,
|
||||
@@ -138,7 +141,7 @@ WITH normalized AS (
|
||||
(source = 'inference-legacy' AND started_at < ${sqlString(LIVE_SOURCE_START)})
|
||||
OR (source = 'inference' AND started_at >= ${sqlString(LIVE_SOURCE_START)})
|
||||
)
|
||||
AND product = 'go'
|
||||
AND (product = 'go' OR (${sourceFreeTier}))
|
||||
AND model_requested IS NOT NULL
|
||||
AND model_requested <> ''
|
||||
AND __ingest_ts >= ${periodStartValue}
|
||||
@@ -147,7 +150,11 @@ WITH normalized AS (
|
||||
AND started_at < ${periodEndValue}
|
||||
), filtered AS (
|
||||
SELECT
|
||||
'Go' AS tier,
|
||||
CASE
|
||||
WHEN ${freeTierSql("raw_tier", "raw_model")}
|
||||
THEN 'Free'
|
||||
ELSE 'Go'
|
||||
END AS tier,
|
||||
${statProviderSql("model", "provider_model", "raw_provider")} AS provider,
|
||||
provider_model,
|
||||
model,
|
||||
@@ -299,6 +306,13 @@ function statModelSql(model: string, providerModel: string) {
|
||||
END, '(-free|:global)+$', ''), ''), 'unknown')`
|
||||
}
|
||||
|
||||
function freeTierSql(tier: string, model: string) {
|
||||
return `lower(COALESCE(${tier}, '')) = 'free'
|
||||
OR lower(${model}) IN (${[...FREE_MODELS].map(sqlString).join(", ")})
|
||||
OR lower(${model}) LIKE '%-free'
|
||||
OR lower(${model}) LIKE '%-free:global'`
|
||||
}
|
||||
|
||||
function statProviderSql(model: string, providerModel: string, provider: string) {
|
||||
return `CASE
|
||||
${MODEL_AUTHOR_RULES.map((item) => ` WHEN strpos(lower(${providerModel}), ${sqlString(item.match)}) > 0 THEN ${sqlString(item.author)}`).join("\n")}
|
||||
|
||||
@@ -13,6 +13,7 @@ export const MODEL_AUTHOR_RULES = [
|
||||
{ match: "qwen", author: "qwen" },
|
||||
] as const
|
||||
export const EXCLUDED_MODELS = new Set(["alpha-gpt-next"])
|
||||
export const FREE_MODELS = new Set(["gpt-5-nano", "grok-code", "big-pickle"])
|
||||
export const RETIRED_STAT_MODELS = ["big-pickle"]
|
||||
export const RETIRED_STAT_PROVIDERS = ["opencode"]
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { RETIRED_STAT_MODELS, RETIRED_STAT_PROVIDERS } from "./model-normalizati
|
||||
import {
|
||||
chunks,
|
||||
collapseRows,
|
||||
DATA_SITE_TIERS,
|
||||
inserted,
|
||||
isMissingUniqueUsersColumn,
|
||||
omitUniqueUsers,
|
||||
@@ -211,7 +212,7 @@ function modelDailyScope() {
|
||||
eq(modelStat.grain, "day"),
|
||||
eq(modelStat.client, "all"),
|
||||
eq(modelStat.source, "all"),
|
||||
inArray(modelStat.tier, ["Go", "go"]),
|
||||
inArray(modelStat.tier, DATA_SITE_TIERS),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { RETIRED_STAT_PROVIDERS } from "./model-normalization"
|
||||
import {
|
||||
chunks,
|
||||
collapseRows,
|
||||
DATA_SITE_TIERS,
|
||||
inserted,
|
||||
isMissingUniqueUsersColumn,
|
||||
omitUniqueUsers,
|
||||
@@ -69,7 +70,7 @@ export class ProviderStatRepo extends Context.Service<ProviderStatRepo, Provider
|
||||
eq(providerStat.grain, "day"),
|
||||
eq(providerStat.client, "all"),
|
||||
eq(providerStat.source, "all"),
|
||||
inArray(providerStat.tier, ["Go", "go"]),
|
||||
inArray(providerStat.tier, DATA_SITE_TIERS),
|
||||
),
|
||||
)
|
||||
.orderBy(asc(providerStat.period_key)),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { sql } from "drizzle-orm"
|
||||
|
||||
export const UPSERT_CHUNK_SIZE = 500
|
||||
export const DATA_SITE_TIERS = ["Go", "go", "Free", "free"]
|
||||
const DAY_MS = 86_400_000
|
||||
|
||||
export type StatGrain = "day" | "week"
|
||||
@@ -276,7 +277,12 @@ export function weightedAverage(
|
||||
}
|
||||
|
||||
export function normalizeTier(value: string) {
|
||||
if (value === "Paid") return "Zen"
|
||||
const normalized = value.toLowerCase()
|
||||
if (normalized === "paid" || normalized === "zen") return "Zen"
|
||||
if (normalized === "go") return "Go"
|
||||
if (normalized === "free") return "Free"
|
||||
if (normalized === "enterprise") return "Enterprise"
|
||||
if (normalized === "all") return "all"
|
||||
return value
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { readdir } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { drizzle } from "drizzle-orm/planetscale-serverless"
|
||||
import { geoStat, modelStat, providerStat } from "./database/schema"
|
||||
import { statModel, statProvider } from "./domain/model-normalization"
|
||||
import { FREE_MODELS, statModel, statProvider } from "./domain/model-normalization"
|
||||
import {
|
||||
chunks,
|
||||
collapseRows,
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
const DAY_MS = 86_400_000
|
||||
const DEFAULT_UPSERT_CHUNK_SIZE = 100
|
||||
const DEFAULT_TIERS = ["Go", "Free", "Paid"]
|
||||
const FREE_MODELS = new Set(["gpt-5-nano", "grok-code", "big-pickle"])
|
||||
|
||||
type Grain = "day" | "week"
|
||||
type MetricDimension = "model" | "provider" | "geo" | "geo-model"
|
||||
|
||||
Reference in New Issue
Block a user