Tableau de bord des coûts IA
Ce manuel s'adresse aux SUPER_ADMIN, aux ROLE_MANAGER et aux comptes finance qui suivent la consommation IA de la plateforme.
Pourquoi cette feature
MedGuru consomme deux fournisseurs LLM principaux (Anthropic Claude, Google Gemini) sur plusieurs opérations critiques :
- Pipeline d'ingestion :
classify(1 appel par page),extract/extract_deep(1 appel par page-pub détectée),refine_bbox,self_correct. - Catalogue tarifs :
magazine.identify(Claude Vision sur la couverture PDF),rate-card.extract(extraction des grilles tarifaires). - Produit côté UI :
superfilter.query(tracking sans coût LLM, pour analyser les usages produit),guru.chat(volume de messages assistant).
Sans observabilité, impossible de répondre à des questions simples : « Combien coûte un import de 100 pages ? », « Quel magazine consomme le plus de tokens ? », « Qui utilise GURU le plus souvent ? ».
Comment y accéder
URL : /backstage/costs (requiert ROLE_MANAGER ou supérieur).
L'item de navigation est visible dans la sidebar Backstage sous Observabilité.
Lecture des KPIs
En haut de la page, quatre cartes synthétiques :
| KPI | Description |
|---|---|
| Coût total période | Somme des coûts USD calculés via la table ai_model_pricing (input + output + cache). Inclut une tendance % vs la période précédente. |
| Tokens totaux | Somme input_tokens + output_tokens (les cache reads sont comptés à part dans le détail). |
| Appels totaux | Nombre d'événements ai_usage_events enregistrés. |
| Latence moyenne | Moyenne duration_ms sur la période. Utile pour détecter des dégradations Anthropic / Gemini. |
Le sélecteur en haut à droite ajuste la période : 7 jours, 30 jours, 90 jours.
Tendance journalière
Graphique en lignes (Recharts LineChart) qui montre les coûts USD agrégés par jour sur la période choisie. Permet de repérer :
- Pics anormaux (un upload massif de magazines, un crawler rejouant des extractions).
- Tendance de fond (consommation qui grimpe avec l'usage).
- Effets d'optimisation (passage Haiku → Sonnet visible comme une marche).
Le tooltip survol affiche count (appels) + cost_usd (coût USD) pour un jour donné.
Breakdown par opération
Bar chart horizontal qui ventile la dépense par operation :
ingestion.classify,ingestion.extract,ingestion.extract_deep,ingestion.refine_bbox,ingestion.self_correctmagazine.identify,rate-card.extractsuperfilter.query(tracking produit, model='none' donc 0 USD)guru.chat(tracking produit, model='guru.chat' donc 0 USD)
Permet d'identifier d'un coup d'œil où va l'argent : typiquement extract_deep domine, suivi de extract.
Top 10 magazines
Tableau qui liste les magazines triés par coût décroissant. Colonnes :
| Colonne | Description |
|---|---|
| Magazine | Nom du magazine (ou « (sans magazine) » pour les events superfilter.query / guru.chat qui n'ont pas de magazine associé) |
| Coût USD | Somme des coûts sur la période |
| Appels | Nombre d'ai_usage_events |
| Tokens | Total tokens (input + output) |
| Coût moyen / appel | Coût USD ÷ nb appels |
Cliquer sur un magazine ouvre la fiche /admin/magazines/{id} (TODO : pas encore actif).
Top 10 utilisateurs
Identique au top magazines, mais agrégé par actor_id (= user_id).
Permet de repérer :
- Un compte robot qui spamme
superfilter.query(rate limit à mettre en place côté Caddy). - Un utilisateur GURU power-user (positif).
- Des actions admin coûteuses (réingestion forcée d'un gros magazine).
Sources de données
Tous les chiffres viennent de la table ai_usage_events (cf. migration Version20260423030000). La structure :
ai_usage_events (
id UUID PRIMARY KEY,
occurred_at TIMESTAMPTZ NOT NULL,
model VARCHAR(64), -- ex: claude-sonnet-4-6, gemini-3.1-pro, none, guru.chat
operation VARCHAR(64), -- ex: ingestion.classify, superfilter.query, guru.chat
input_tokens INT,
output_tokens INT,
cache_read_tokens INT,
cache_creation_tokens INT,
duration_ms INT,
actor_id INT REFERENCES users(id), -- nullable (pour les events worker)
import_id UUID REFERENCES imports(id), -- nullable
magazine_id UUID, -- nullable
regie_id UUID, -- nullable
metadata JSONB, -- payload libre par opération
success BOOL,
error_code VARCHAR(64)
)Les coûts sont calculés à la lecture via la table ai_model_pricing (USD per 1M tokens, séparé pour input / output / cache_read / cache_creation). Cela permet de rétro-ajuster les prix sans toucher aux events historiques.
Instrumentation
Les callers actuels de AiUsageRecorder :
| Caller | Operation(s) | Localisation |
|---|---|---|
| Worker ingestion | ingestion.classify, ingestion.extract, ingestion.extract_deep, ingestion.refine_bbox, ingestion.self_correct | ingestion-service/src/worker/processImport.ts |
MagazineIdentifier | magazine.identify | data-api/src/Service/Ingestion/MagazineIdentifier.php |
ClaudeRateCardExtractor | rate-card.extract | data-api/src/Service/Catalog/ClaudeRateCardExtractor.php |
SuperfilterController::list() | superfilter.query | data-api/src/Controller/SuperfilterController.php |
ConversationController::addMessage() | guru.chat | data-api/src/Controller/ConversationController.php |
Pour ajouter un nouveau caller :
- Injecter
AiUsageRecorder(nullable pour BC tests) dans le service / controller. - Appeler
record(new UsageEventDto(...))après l'opération, dans untry/catchbest-effort. - Le tracking ne doit jamais bloquer la requête utilisateur.
Endpoints REST
Le frontend consomme 5 endpoints (tous ROLE_MANAGER) :
GET /api/backstage/ai-costs/summary?period=30d— KPI cardsGET /api/backstage/ai-costs/breakdown?period=30d— bar chart par opérationGET /api/backstage/ai-costs/daily-trend?period=30d— line chart journalierGET /api/backstage/ai-costs/top-magazines?period=30d&limit=10GET /api/backstage/ai-costs/top-users?period=30d&limit=10
Implémentés dans BackstageAiCostsController.php.
Cleanup historique
Une command Symfony app:imports:cleanup-old-s3 libère le storage MinIO des imports terminés depuis > 30 jours, sans toucher aux events ai_usage_events qui restent pour analytics. Cron suggéré :
0 3 * * * php bin/console app:imports:cleanup-old-s3 --days=30Limites connues
- Les events
superfilter.queryetguru.chatontmodel='none'oumodel='guru.chat'→ coût USD = 0. Ils sont visibles en volume mais pas en coût. - Le dashboard ne montre pas encore de granularité par utilisateur sur le breakdown par opération (top users croisé avec opération). Issue de suivi : à créer si besoin.
- Le sélecteur de période est limité à 7/30/90j ; pas de date range custom.