Optimiser les coûts à grande échelle
Mis à jour le 28 juillet 2026
Le coût comme contrainte d’ingénierie
Un prompt mal optimisé qui coûte 0,002 $ par appel ne coûte rien tant que vous êtes en développement. Passé le million d’appels, il coûte 2 000 $, et il continuera de les coûter chaque mois tant que personne ne l’aura relu. C’est ce changement d’échelle qui transforme le coût en contrainte d’ingénierie : il ne se règle pas en négociant un contrat, il se règle en modifiant du code.
Le préalable à toute optimisation est la visibilité. Une facture mensuelle vous dit combien vous avez dépensé, pas où. Cette leçon commence donc par l’instrumentation, avant d’aborder les leviers de réduction.
Savoir où part l’argent
Quatre éléments déterminent le coût d’un appel, et ils ne se comportent pas de la même façon. Les tokens en entrée, qui agrègent le prompt système, l’historique de conversation et le message utilisateur, croissent avec la durée d’une session. Les tokens en sortie, c’est-à-dire la réponse générée, sont facturés nettement plus cher que l’entrée. Les tokens mis en cache bénéficient d’une réduction d’environ cinquante pour cent sur le tarif d’entrée, ce qui rend leur suivi indispensable pour comprendre une facture. Enfin le modèle lui-même : GPT-5.6 Luna est bien moins cher que GPT-5.6 Sol, et ce facteur domine tous les autres.
Le suivi consiste à convertir chaque objet usage retourné par l’API en un montant, appliqué au bon tarif et en isolant la part mise en cache.
import openai
from dataclasses import dataclass, field
client = openai.OpenAI()
# Tarifs par million de tokens (juillet 2026) — cached_input indicatif (~50 % de l'input),
# à ajuster selon la grille officielle
TARIFS = {
"gpt-5.6-luna": {"input": 0.20, "output": 1.20, "cached_input": 0.02},
"gpt-5.6-terra": {"input": 2.00, "output": 12.00, "cached_input": 0.20},
"gpt-5.6-sol": {"input": 5.00, "output": 30.00, "cached_input": 0.50},
}
@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.6-terra"])
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()
Le tableau TARIFS est un point d’attention permanent : il vieillit dès que la grille officielle change. Traitez-le comme une donnée de configuration à vérifier, pas comme une constante du programme, et faites du coût moyen par appel une métrique que vous regardez chaque semaine.
Quatre leviers de réduction
Le premier levier, et de loin le plus rentable, consiste à ne pas payer un modèle haut de gamme pour une tâche qui ne l’exige pas. Router explicitement selon la complexité évite la dérive par défaut où tout le trafic finit sur le modèle le plus cher parce qu’il figurait dans le premier exemple copié.
async def routeur_economique(prompt: str, complexite: str) -> str:
"""Route vers le modèle le moins cher capable."""
if complexite == "simple":
modele = "gpt-5.6-luna"
elif complexite == "moyen":
modele = "gpt-5.6-terra"
else:
modele = "gpt-5.6-sol"
response = client.responses.create(model=modele, input=prompt)
suivi.enregistrer(modele, response.usage)
return response.output_text
Le deuxième levier va plus loin lorsque la complexité n’est pas connue d’avance. Plutôt que de classer la demande, la cascade tente d’abord le modèle le moins cher et n’escalade que si la réponse ne passe pas un contrôle de qualité. Le contrôle utilisé ici est délibérément rudimentaire — une longueur minimale et l’absence d’un aveu d’ignorance — et c’est le point que vous devrez travailler : une cascade escalade trop souvent devient plus chère qu’un appel direct au bon modèle.
async def cascade_modeles(prompt: str) -> str:
"""Essaie le modèle le moins cher d'abord."""
modeles = ["gpt-5.6-luna", "gpt-5.6-terra", "gpt-5.6-sol"]
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
Le troisième levier s’attaque aux tokens d’entrée, qui grossissent silencieusement dans toute application conversationnelle : au vingtième tour, vous repayez les dix-neuf précédents à chaque message. Comprimer l’historique en gardant le premier message, qui porte le contexte, et le dernier, qui porte la question, puis en remplissant le budget restant avec les échanges les plus récents, préserve l’essentiel de la cohérence pour une fraction du coût.
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]
Le quatrième levier ne demande aucune finesse algorithmique, seulement de la discipline : la Batch API offre cinquante pour cent de réduction, et il suffit d’identifier ce qui peut attendre. Faites porter l’urgence par la tâche elle-même, dès sa création, plutôt que de la déduire après coup — c’est la seule façon d’éviter que tout finisse par défaut dans la file temps réel.
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
}
Être prévenu avant la facture
Toutes ces optimisations peuvent être défaites par un seul déploiement malheureux : une boucle de retry mal bornée, un prompt système triplé, un modèle changé par erreur. L’alerte de coût est le filet de sécurité qui vous prévient en quelques heures plutôt qu’en fin de mois. Notez le seuil intermédiaire à quatre-vingts pour cent : il transforme une alarme brutale en signal anticipé, celui qui vous laisse le temps d’agir avant le dépassement.
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