Optimiser les coûts à grande échelle
Le coût comme contrainte d’ingénierie
À grande échelle, chaque token compte. Un prompt mal optimisé qui coûte 0,002 $ par appel devient 2 000 $ pour un million d’appels. Cette leçon vous donne les outils pour analyser, réduire et prévoir vos coûts en production.
Anatomie des coûts API
Les leviers de coût
Chaque appel API a un coût déterminé par :
- Tokens en entrée : prompt système + historique + message utilisateur
- Tokens en sortie : la réponse générée
- Tokens en cache : réduction sur les tokens d’entrée mis en cache (~50 %)
- Modèle utilisé : GPT-5.3 est bien moins cher que o3-pro
Suivi des coûts en temps réel
import openai
from dataclasses import dataclass, field
client = openai.OpenAI()
# Tarifs indicatifs par million de tokens (à ajuster selon les tarifs réels)
TARIFS = {
"gpt-5.3": {"input": 0.50, "output": 1.50, "cached_input": 0.25},
"gpt-5.4": {"input": 2.50, "output": 10.00, "cached_input": 1.25},
"o4-mini": {"input": 1.10, "output": 4.40, "cached_input": 0.55},
"o3-pro": {"input": 20.00, "output": 80.00, "cached_input": 10.00},
}
@dataclass
class SuiviCouts:
"""Suit les coûts en temps réel."""
appels: list[dict] = field(default_factory=list)
def enregistrer(self, modele: str, usage: dict) -> float:
tokens_entree = usage.input_tokens
tokens_cache = getattr(
usage.input_tokens_details, "cached_tokens", 0
)
tokens_sortie = usage.output_tokens
tokens_entree_non_cache = tokens_entree - tokens_cache
tarif = TARIFS.get(modele, TARIFS["gpt-5.3"])
cout = (
(tokens_entree_non_cache / 1_000_000) * tarif["input"]
+ (tokens_cache / 1_000_000) * tarif["cached_input"]
+ (tokens_sortie / 1_000_000) * tarif["output"]
)
self.appels.append({
"modele": modele,
"tokens_entree": tokens_entree,
"tokens_cache": tokens_cache,
"tokens_sortie": tokens_sortie,
"cout": cout,
})
return cout
@property
def cout_total(self) -> float:
return sum(a["cout"] for a in self.appels)
def rapport(self) -> str:
if not self.appels:
return "Aucun appel enregistré."
return (
f"Appels: {len(self.appels)} | "
f"Coût total: ${self.cout_total:.4f} | "
f"Coût moyen: ${self.cout_total / len(self.appels):.6f}"
)
suivi = SuiviCouts()
Stratégies de réduction des coûts
Stratégie 1 : routage par modèle
async def routeur_economique(prompt: str, complexite: str) -> str:
"""Route vers le modèle le moins cher capable."""
if complexite == "simple":
modele = "gpt-5.3"
elif complexite == "moyen":
modele = "o4-mini"
else:
modele = "gpt-5.4"
response = client.responses.create(model=modele, input=prompt)
suivi.enregistrer(modele, response.usage)
return response.output_text
Stratégie 2 : cascade de modèles
Commencez par le modèle le moins cher, escaladez si nécessaire :
async def cascade_modeles(prompt: str) -> str:
"""Essaie le modèle le moins cher d'abord."""
modeles = ["gpt-5.3", "o4-mini", "gpt-5.4"]
for modele in modeles:
response = client.responses.create(
model=modele,
input=prompt,
max_output_tokens=500,
)
suivi.enregistrer(modele, response.usage)
# Vérifier la qualité (heuristique simple)
texte = response.output_text
if len(texte) > 50 and "je ne sais pas" not in texte.lower():
return texte
return texte # Dernier résultat même si imparfait
Stratégie 3 : réduire les tokens d’entrée
def compresser_historique(
messages: list[dict],
max_tokens: int = 4000,
) -> list[dict]:
"""Garde les messages les plus récents dans le budget."""
# Toujours garder le premier (contexte) et le dernier (question)
if len(messages) <= 2:
return messages
premier = messages[0]
dernier = messages[-1]
milieu = messages[1:-1]
# Estimer les tokens (approximation : 1 token ≈ 4 caractères)
tokens_fixes = (len(premier["content"]) + len(dernier["content"])) // 4
budget_milieu = max_tokens - tokens_fixes
# Garder les messages les plus récents
resultat = []
tokens_utilises = 0
for msg in reversed(milieu):
tokens_msg = len(msg["content"]) // 4
if tokens_utilises + tokens_msg > budget_milieu:
break
resultat.insert(0, msg)
tokens_utilises += tokens_msg
return [premier] + resultat + [dernier]
Stratégie 4 : Batch API pour les traitements différés
Rappel : la Batch API offre 50 % de réduction. Identifiez les traitements qui peuvent attendre :
def classifier_urgence(taches: list[dict]) -> dict:
"""Sépare les tâches en temps réel vs batch."""
temps_reel = []
batch = []
for tache in taches:
if tache.get("urgence") == "immediat":
temps_reel.append(tache)
else:
batch.append(tache)
return {
"temps_reel": temps_reel,
"batch": batch,
"economie_estimee": len(batch) * 0.001, # Exemple
}
Alertes de coût
class AlerteCout:
"""Déclenche des alertes quand les coûts dépassent un seuil."""
def __init__(self, seuil_journalier: float = 50.0):
self.seuil = seuil_journalier
self.suivi = SuiviCouts()
def verifier(self) -> str | None:
if self.suivi.cout_total > self.seuil:
return (
f"ALERTE : coût journalier ${self.suivi.cout_total:.2f} "
f"dépasse le seuil de ${self.seuil:.2f}"
)
if self.suivi.cout_total > self.seuil * 0.8:
return (
f"AVERTISSEMENT : coût à {self.suivi.cout_total / self.seuil:.0%} "
f"du seuil journalier"
)
return None
Points clés à retenir
- Suivez vos coûts en temps réel avec
response.usage - Routez vers le modèle le moins cher capable de la tâche
- Utilisez la Batch API (-50 %) pour tout ce qui n’est pas temps réel
- Réduisez les tokens d’entrée en compressant l’historique
- Mettez en place des alertes de coût pour éviter les surprises