import "server-only"; import { sql, getSetting } from "./db"; import type { Usage } from "./ai/types"; export interface UsageEventInput { provider: string; modelId: string; kind?: string; conversationId?: string | null; projectId?: string | null; messageId?: string | null; usage?: Usage | null; costEstimated?: number | null; latencyMs?: number | null; ttftMs?: number | null; status?: "ok" | "error" | "cancelled"; error?: string | null; meta?: Record; } export async function recordUsage(e: UsageEventInput) { await sql`INSERT INTO usage_events (provider, model_id, kind, conversation_id, project_id, message_id, input_tokens, output_tokens, cached_tokens, reasoning_tokens, cost_estimated, latency_ms, ttft_ms, status, error, meta) VALUES (${e.provider}, ${e.modelId}, ${e.kind ?? "chat"}, ${e.conversationId ?? null}, ${e.projectId ?? null}, ${e.messageId ?? null}, ${e.usage?.input ?? 0}, ${e.usage?.output ?? 0}, ${e.usage?.cached ?? 0}, ${e.usage?.reasoning ?? 0}, ${e.costEstimated ?? null}, ${e.latencyMs ?? null}, ${e.ttftMs ?? null}, ${e.status ?? "ok"}, ${e.error ?? null}, ${e.meta ? sql.json(e.meta as never) : null})`; } export interface SpendingLimits { dailyLimitUsd: number; monthlyLimitUsd: number; alertAtPercent: number } export async function getSpending(): Promise<{ today: number; month: number; limits: SpendingLimits }> { const limits = await getSetting("spending", { dailyLimitUsd: 10, monthlyLimitUsd: 150, alertAtPercent: 80 }); const r = await sql<{ today: number; month: number }[]>` SELECT coalesce(sum(cost_estimated) FILTER (WHERE at >= date_trunc('day', now())), 0)::float AS today, coalesce(sum(cost_estimated) FILTER (WHERE at >= date_trunc('month', now())), 0)::float AS month FROM usage_events`; return { today: r[0].today, month: r[0].month, limits }; } /** Throws (status 402) when a local spending limit is reached. Provider billing may differ. */ export async function assertWithinBudget(): Promise<{ warning?: string }> { const s = await getSpending(); if (s.limits.dailyLimitUsd > 0 && s.today >= s.limits.dailyLimitUsd) throw Object.assign(new Error(`Limite quotidienne atteinte (${s.today.toFixed(2)} $ / ${s.limits.dailyLimitUsd} $). Ajuste la limite dans Réglages → Dépenses.`), { status: 402 }); if (s.limits.monthlyLimitUsd > 0 && s.month >= s.limits.monthlyLimitUsd) throw Object.assign(new Error(`Limite mensuelle atteinte (${s.month.toFixed(2)} $ / ${s.limits.monthlyLimitUsd} $).`), { status: 402 }); const pct = s.limits.alertAtPercent / 100; if (s.limits.dailyLimitUsd > 0 && s.today >= s.limits.dailyLimitUsd * pct) return { warning: `Dépenses du jour : ${s.today.toFixed(2)} $ sur ${s.limits.dailyLimitUsd} $` }; if (s.limits.monthlyLimitUsd > 0 && s.month >= s.limits.monthlyLimitUsd * pct) return { warning: `Dépenses du mois : ${s.month.toFixed(2)} $ sur ${s.limits.monthlyLimitUsd} $` }; return {}; }