/** * Cost view math (read-only surface): prices come from the pi model catalog * (zai coding endpoint) and every function here is pure so the projection is * unit-testable without a database or a live clock. */ export interface TurnUsageRow { ts: string payload: unknown } export interface DayUsage { date: string input: number output: number cacheRead: number cacheWrite: number responses: number /** USD; null when any contributing row used a model without a price entry */ costUsd: number | null } export interface UsageProjection { todayUsd: number | null mtdUsd: number | null projectedEomUsd: number | null } export interface UsageReport { days: DayUsage[] totals: Omit projection: UsageProjection } /** USD per MTok (input, output, cacheRead); cacheWrite unpriced (unbilled). */ const PRICES_PER_MTOK: Record = { 'zai/glm-5.3-flash': { input: 0.15, output: 0.5, cacheRead: 0.03 }, } interface UsagePayload { provider?: unknown model?: unknown input?: unknown output?: unknown cacheRead?: unknown cacheWrite?: unknown responses?: unknown } const num = (v: unknown): number => (typeof v === 'number' && Number.isFinite(v) ? v : 0) function costOfRow(payload: unknown): number | null { const p = payload as UsagePayload const provider = typeof p?.provider === 'string' ? p.provider : 'unknown' const model = typeof p?.model === 'string' ? p.model : 'unknown' const price = PRICES_PER_MTOK[`${provider}/${model}`] if (price === undefined) return null return ( (num(p?.input) * price.input + num(p?.output) * price.output + num(p?.cacheRead) * price.cacheRead) / 1e6 ) } const dayKey = (ts: string): string => ts.slice(0, 10) const HOUR_MS = 3_600_000 /** Floor on elapsed-day fraction so a just-past-midnight spike cannot explode the rate */ const MIN_ELAPSED_FRACTION = 0.5 / 24 export function projectUsage(days: DayUsage[], now: Date): UsageProjection { const todayKey = now.toISOString().slice(0, 10) const today = days.find((d) => d.date === todayKey) if (today === undefined || today.costUsd === null) { return { todayUsd: null, mtdUsd: null, projectedEomUsd: null } } const midnight = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()) const elapsed = Math.max((now.getTime() - midnight) / (24 * HOUR_MS), MIN_ELAPSED_FRACTION) const todayRate = today.costUsd / elapsed const month = todayKey.slice(0, 7) const mtd = days .filter((d) => d.date.slice(0, 7) === month && d.costUsd !== null) .reduce((sum, d) => sum + (d.costUsd ?? 0), 0) const monthEnd = Date.UTC( now.getUTCFullYear(), now.getUTCMonth() + 1, 1, // first day of next month == end of this one ) const projectedEom = mtd + todayRate * ((monthEnd - now.getTime()) / (24 * HOUR_MS)) return { todayUsd: todayRate, mtdUsd: mtd, projectedEomUsd: projectedEom } } export function aggregateTurnUsage(rows: TurnUsageRow[], now: Date): UsageReport { interface Acc extends Omit { priced: boolean costUsd: number } const byDay = new Map() for (const row of rows) { const p = row.payload as UsagePayload const date = dayKey(typeof row.ts === 'string' ? row.ts : '') const day = byDay.get(date) ?? { date, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, responses: 0, costUsd: 0, priced: true, } day.input += num(p?.input) day.output += num(p?.output) day.cacheRead += num(p?.cacheRead) day.cacheWrite += num(p?.cacheWrite) day.responses += num(p?.responses) const cost = costOfRow(row.payload) if (cost === null) day.priced = false else day.costUsd += cost byDay.set(date, day) } const days = [...byDay.values()] .map(({ priced, ...d }) => ({ ...d, costUsd: priced ? d.costUsd : null })) .sort((a, b) => (a.date < b.date ? -1 : 1)) const totalsAcc = days.reduce( (t, d) => ({ input: t.input + d.input, output: t.output + d.output, cacheRead: t.cacheRead + d.cacheRead, cacheWrite: t.cacheWrite + d.cacheWrite, responses: t.responses + d.responses, costUsd: t.costUsd + (d.costUsd ?? 0), unpriced: t.unpriced || d.costUsd === null, }), { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, responses: 0, costUsd: 0, unpriced: false, }, ) const totals: UsageReport['totals'] = { input: totalsAcc.input, output: totalsAcc.output, cacheRead: totalsAcc.cacheRead, cacheWrite: totalsAcc.cacheWrite, responses: totalsAcc.responses, costUsd: totalsAcc.unpriced ? null : totalsAcc.costUsd, } return { days, totals, projection: projectUsage(days, now) } }