import { prisma } from '@/lib/prisma' export type Period = '7d' | '30d' | '90d' | 'mtd' export interface CostKpi { totalCostUsd: number totalTokens: number jobCount: number avgPerDayUsd: number cacheSavingsUsd: number topModelId: string | null topModelCostUsd: number } export interface CostByDayRow { day: string costUsd: number } export interface CostByModelRow { modelId: string costUsd: number jobCount: number } export interface CostByKindRow { kind: string costUsd: number jobCount: number } export interface CacheEfficiency { cacheReadTokens: number uncachedInputTokens: number cacheHitRatio: number savingsUsd: number spentOnCacheWriteUsd: number } function periodToDays(period: Period, now: Date = new Date()): number { switch (period) { case '7d': return 7 case '30d': return 30 case '90d': return 90 case 'mtd': return now.getUTCDate() } } function periodStart(period: Period, now: Date = new Date()): Date { if (period === 'mtd') { return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)) } const days = periodToDays(period, now) return new Date(now.getTime() - days * 86_400_000) } type RawKpiRow = { total_cost: number | null total_tokens: bigint job_count: bigint cache_savings: number | null } type RawTopModelRow = { model_id: string | null cost: number | null } export async function getCostKpi(userId: string, period: Period): Promise { const start = periodStart(period) const days = Math.max(periodToDays(period), 1) const [kpiRows, topModelRows] = await Promise.all([ prisma.$queryRaw` SELECT SUM( cj.input_tokens * mp.input_price_per_1m / 1000000.0 + cj.output_tokens * mp.output_price_per_1m / 1000000.0 + cj.cache_read_tokens * mp.cache_read_price_per_1m / 1000000.0 + cj.cache_write_tokens * mp.cache_write_price_per_1m / 1000000.0 + COALESCE(cj.actual_thinking_tokens, 0) * mp.input_price_per_1m / 1000000.0 ) FILTER (WHERE cj.input_tokens IS NOT NULL) AS total_cost, COALESCE(SUM( cj.input_tokens + cj.output_tokens + cj.cache_read_tokens + cj.cache_write_tokens + COALESCE(cj.actual_thinking_tokens, 0) ), 0) AS total_tokens, COUNT(*) FILTER (WHERE cj.input_tokens IS NOT NULL) AS job_count, SUM( cj.cache_read_tokens * (mp.input_price_per_1m - mp.cache_read_price_per_1m) / 1000000.0 ) FILTER (WHERE cj.cache_read_tokens IS NOT NULL) AS cache_savings FROM claude_jobs cj LEFT JOIN model_prices mp ON mp.model_id = cj.model_id WHERE cj.user_id = ${userId} AND cj.status = 'DONE' AND cj.finished_at >= ${start} `, prisma.$queryRaw` SELECT cj.model_id, SUM( cj.input_tokens * mp.input_price_per_1m / 1000000.0 + cj.output_tokens * mp.output_price_per_1m / 1000000.0 + cj.cache_read_tokens * mp.cache_read_price_per_1m / 1000000.0 + cj.cache_write_tokens * mp.cache_write_price_per_1m / 1000000.0 + COALESCE(cj.actual_thinking_tokens, 0) * mp.input_price_per_1m / 1000000.0 ) AS cost FROM claude_jobs cj LEFT JOIN model_prices mp ON mp.model_id = cj.model_id WHERE cj.user_id = ${userId} AND cj.status = 'DONE' AND cj.finished_at >= ${start} AND cj.input_tokens IS NOT NULL AND cj.model_id IS NOT NULL GROUP BY cj.model_id ORDER BY cost DESC NULLS LAST LIMIT 1 `, ]) const kpi = kpiRows[0] const totalCost = Number(kpi?.total_cost ?? 0) const top = topModelRows[0] return { totalCostUsd: totalCost, totalTokens: Number(kpi?.total_tokens ?? 0), jobCount: Number(kpi?.job_count ?? 0), avgPerDayUsd: totalCost / days, cacheSavingsUsd: Number(kpi?.cache_savings ?? 0), topModelId: top?.model_id ?? null, topModelCostUsd: Number(top?.cost ?? 0), } } type RawDayRow = { day: Date cost: number | null } export async function getCostByDay(userId: string, period: Period): Promise { const start = periodStart(period) const rows = await prisma.$queryRaw` SELECT DATE(cj.finished_at) AS day, SUM( cj.input_tokens * mp.input_price_per_1m / 1000000.0 + cj.output_tokens * mp.output_price_per_1m / 1000000.0 + cj.cache_read_tokens * mp.cache_read_price_per_1m / 1000000.0 + cj.cache_write_tokens * mp.cache_write_price_per_1m / 1000000.0 + COALESCE(cj.actual_thinking_tokens, 0) * mp.input_price_per_1m / 1000000.0 ) FILTER (WHERE cj.input_tokens IS NOT NULL) AS cost FROM claude_jobs cj LEFT JOIN model_prices mp ON mp.model_id = cj.model_id WHERE cj.user_id = ${userId} AND cj.status = 'DONE' AND cj.finished_at >= ${start} GROUP BY DATE(cj.finished_at) ORDER BY day ASC ` return rows.map(r => ({ day: r.day.toISOString().slice(0, 10), costUsd: Number(r.cost ?? 0), })) } type RawModelRow = { model_id: string cost: number | null job_count: bigint } export async function getCostByModel(userId: string, period: Period): Promise { const start = periodStart(period) const rows = await prisma.$queryRaw` SELECT cj.model_id, SUM( cj.input_tokens * mp.input_price_per_1m / 1000000.0 + cj.output_tokens * mp.output_price_per_1m / 1000000.0 + cj.cache_read_tokens * mp.cache_read_price_per_1m / 1000000.0 + cj.cache_write_tokens * mp.cache_write_price_per_1m / 1000000.0 + COALESCE(cj.actual_thinking_tokens, 0) * mp.input_price_per_1m / 1000000.0 ) AS cost, COUNT(*) AS job_count FROM claude_jobs cj LEFT JOIN model_prices mp ON mp.model_id = cj.model_id WHERE cj.user_id = ${userId} AND cj.status = 'DONE' AND cj.finished_at >= ${start} AND cj.input_tokens IS NOT NULL AND cj.model_id IS NOT NULL GROUP BY cj.model_id ORDER BY cost DESC NULLS LAST LIMIT 5 ` return rows.map(r => ({ modelId: r.model_id, costUsd: Number(r.cost ?? 0), jobCount: Number(r.job_count), })) } type RawKindRow = { kind: string cost: number | null job_count: bigint } export async function getCostByKind(userId: string, period: Period): Promise { const start = periodStart(period) const rows = await prisma.$queryRaw` SELECT cj.kind::text AS kind, SUM( cj.input_tokens * mp.input_price_per_1m / 1000000.0 + cj.output_tokens * mp.output_price_per_1m / 1000000.0 + cj.cache_read_tokens * mp.cache_read_price_per_1m / 1000000.0 + cj.cache_write_tokens * mp.cache_write_price_per_1m / 1000000.0 + COALESCE(cj.actual_thinking_tokens, 0) * mp.input_price_per_1m / 1000000.0 ) AS cost, COUNT(*) AS job_count FROM claude_jobs cj LEFT JOIN model_prices mp ON mp.model_id = cj.model_id WHERE cj.user_id = ${userId} AND cj.status = 'DONE' AND cj.finished_at >= ${start} AND cj.input_tokens IS NOT NULL GROUP BY cj.kind ORDER BY cost DESC NULLS LAST LIMIT 5 ` return rows.map(r => ({ kind: r.kind, costUsd: Number(r.cost ?? 0), jobCount: Number(r.job_count), })) } type RawCacheRow = { cache_read_tokens: bigint uncached_input_tokens: bigint savings: number | null cache_write_cost: number | null } export async function getCacheEfficiency( userId: string, period: Period, ): Promise { const start = periodStart(period) const rows = await prisma.$queryRaw` SELECT COALESCE(SUM(cj.cache_read_tokens), 0) AS cache_read_tokens, COALESCE(SUM(cj.input_tokens), 0) AS uncached_input_tokens, SUM( cj.cache_read_tokens * (mp.input_price_per_1m - mp.cache_read_price_per_1m) / 1000000.0 ) FILTER (WHERE cj.cache_read_tokens IS NOT NULL) AS savings, SUM( cj.cache_write_tokens * mp.cache_write_price_per_1m / 1000000.0 ) FILTER (WHERE cj.cache_write_tokens IS NOT NULL) AS cache_write_cost FROM claude_jobs cj LEFT JOIN model_prices mp ON mp.model_id = cj.model_id WHERE cj.user_id = ${userId} AND cj.status = 'DONE' AND cj.finished_at >= ${start} ` const r = rows[0] const cacheReadTokens = Number(r?.cache_read_tokens ?? 0) const uncachedInputTokens = Number(r?.uncached_input_tokens ?? 0) const totalInputLike = cacheReadTokens + uncachedInputTokens return { cacheReadTokens, uncachedInputTokens, cacheHitRatio: totalInputLike > 0 ? cacheReadTokens / totalInputLike : 0, savingsUsd: Number(r?.savings ?? 0), spentOnCacheWriteUsd: Number(r?.cache_write_cost ?? 0), } }