repositories / will
will
owned by admin
src/service/usage.ts
Raw/**
* 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<DayUsage, 'date'>
projection: UsageProjection
}
/** USD per MTok (input, output, cacheRead); cacheWrite unpriced (unbilled). */
const PRICES_PER_MTOK: Record<string, { input: number; output: number; cacheRead: number }> = {
'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<DayUsage, 'costUsd'> {
priced: boolean
costUsd: number
}
const byDay = new Map<string, Acc>()
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) }
}