import { newId, logger } from '@rareindex/shared'; import type { CostContext, Usage } from './types.js'; export interface CostEvent { provider: string; model: string; role: string; usage: Usage; usdEst: number; units?: number; context?: CostContext; } export type CostSink = (event: CostEvent) => Promise | void; let sink: CostSink | null = null; const buffer: CostEvent[] = []; /** Install the persistence sink (the default DB sink is installed lazily by `installDbCostSink`). */ export function setCostSink(fn: CostSink | null): void { sink = fn; } export async function recordCost(event: CostEvent): Promise { buffer.push(event); if (buffer.length > 500) buffer.shift(); if (!sink) return; try { await sink(event); } catch (err) { logger.warn({ err: err instanceof Error ? err.message : String(err) }, 'ai cost sink failed'); } } /** In-memory tail of recent AI cost events (diagnostics/tests). */ export function recentCosts(): readonly CostEvent[] { return buffer; } /** Persist AI spend into the shared `costs` table (ยง169). Imports the DB lazily to keep this package light. */ export async function installDbCostSink(): Promise { const { getDb, costs } = await import('@rareindex/database'); setCostSink(async (e) => { await getDb() .insert(costs) .values({ id: newId('event'), occurredAt: new Date(), kind: 'ai', provider: e.provider, connectorId: e.context?.connectorId ?? null, categorySlug: e.context?.categorySlug ?? null, endpoint: e.context?.endpoint ?? e.role, userId: e.context?.userId ?? null, units: e.units ?? e.usage.inputTokens + e.usage.outputTokens, credits: 0, usdEst: e.usdEst, metadata: { model: e.model, role: e.role, usage: e.usage, ...(e.context?.metadata ?? {}) }, }); }); }