Aller au contenu principal

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