88 lines
2.7 KiB
TypeScript
88 lines
2.7 KiB
TypeScript
/**
|
|
* 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<BudgetStatus> {
|
|
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 (limit 1000 — suffisant pour budget MVP)
|
|
const url = `${nocodbUrl}/api/v1/db/data/noco/${process.env.NOCODB_BASE_ID || 'p_nav_v2'}/${statsTableId}`
|
|
|
|
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
|
|
}
|