/** * Circuit breaker budget IA * Spec F §6 — seuil 20€/mois * * Avant chaque appel IA (worker ou chatbot) : * const { blocked } = await checkBudget(config) * if (blocked) throw createError({ statusCode: 503, ... }) * * Paliers : * >= 15€ → email Jules (géré par le worker) * >= 18€ → flag budget_warning (bandeau site) * >= 20€ → hard stop, HTTP 503 */ export const BUDGET_MAX_EUR = 20 export const BUDGET_WARN_EUR = 18 export interface BudgetStatus { cumulEur: number blocked: boolean warning: boolean } /** * Calcule le cumul de dépenses IA du mois courant depuis stats_usage NocoDB. * Retourne blocked=true si le budget est atteint. */ export async function checkBudget(config: { nocodbUrl: string nocodbToken: string statsTableId: string }): Promise { const { nocodbUrl, nocodbToken, statsTableId } = config // Premier du mois courant à minuit UTC const now = new Date() const monthStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)) const monthStartIso = monthStart.toISOString() try { // Fetch toutes les entrées du mois courant (NocoDB v2) const url = `${nocodbUrl}/api/v2/tables/${statsTableId}/records` const res = await $fetch<{ list: { cout_eur: number | null; timestamp: string }[] }>( url, { headers: { 'xc-token': nocodbToken }, query: { where: `(timestamp,gte,${monthStartIso})`, limit: 1000, fields: 'cout_eur,timestamp', }, }, ) const rows = res?.list ?? [] const cumulEur = rows.reduce((sum, row) => sum + (Number(row.cout_eur) || 0), 0) return { cumulEur, blocked: cumulEur >= BUDGET_MAX_EUR, warning: cumulEur >= BUDGET_WARN_EUR, } } catch (e) { // En cas d'erreur de lecture, on ne bloque PAS pour ne pas pénaliser les utilisateurs console.warn('[circuitBreaker] Erreur lecture stats_usage — budget non vérifié:', (e as Error).message) return { cumulEur: 0, blocked: false, warning: false } } } /** * Calcule le coût en EUR d'un appel Mistral Small. * Prix : $0.20/M tokens_in, $0.60/M tokens_out (converti en EUR @0.93) */ export function calcCoutMistralSmall(tokensIn: number, tokensOut: number): number { const usd = (tokensIn / 1_000_000) * 0.2 + (tokensOut / 1_000_000) * 0.6 return usd * 0.93 } /** * Calcule le coût en EUR d'un appel Mistral Nemo. * Prix : $0.02/M tokens_in, $0.04/M tokens_out (converti en EUR @0.93) */ export function calcCoutMistralNemo(tokensIn: number, tokensOut: number): number { const usd = (tokensIn / 1_000_000) * 0.02 + (tokensOut / 1_000_000) * 0.04 return usd * 0.93 }