Coût, cache et performance
Comprendre la facture, la diviser par dix avec la mise en cache, traiter en lot, choisir le bon effort, et mesurer au lieu de supposer.
À la fin de ce chapitre, vous saurez
- Décomposer une facture et savoir où part réellement l’argent
- Mettre en cache correctement, et diagnostiquer un cache qui ne prend pas
- Choisir entre appel direct et traitement par lot
- Mesurer un coût avant de déployer plutôt qu’après
Dans ce chapitre
Un projet fondé sur l’API se juge sur trois axes : la qualité, la latence et le coût. Les deux premiers se voient tout de suite. Le troisième se découvre à la première facture, et c’est souvent trop tard pour reconcevoir.
Ce chapitre donne les mécanismes et l’ordre dans lequel les appliquer.
D’où vient la facture
Deux compteurs, avec des prix différents.
Les jetons d’entrée — tout ce que le modèle lit : instruction système, historique, documents, résultats d’outils.
Les jetons de sortie — tout ce qu’il écrit, réflexion comprise. Facturés plusieurs fois plus cher que l’entrée.
Trois conséquences qu’on sous-estime systématiquement.
Une conversation coûte de plus en plus cher à chaque tour. L’API est sans état : au vingtième message, vous payez la relecture des dix-neuf précédents. Le coût d’une conversation croît en carré de sa longueur.
La réflexion approfondie est de la sortie. Elle est facturée au tarif de sortie, même si vous n’en affichez pas le contenu.
Les résultats d’outils sont de l’entrée, et ils s’accumulent. Une recherche web qui ramène quarante mille jetons les fait payer à chaque tour suivant de la même conversation.
La mise en cache
C’est le levier principal, et de très loin. Bien utilisée, elle divise le coût d’entrée par dix sur la partie stable de vos requêtes.
Le principe
Le cache fonctionne par appariement de préfixe exact. Vous marquez un point de coupure ; tout ce qui précède est mis en cache. À l’appel suivant, si les octets sont identiques jusqu’à ce point, la partie cachée est facturée à environ un dixième du prix.
L’ordre de rendu est : tools, puis system, puis messages. Un marqueur sur le dernier bloc système met donc en cache les outils et l’instruction système.
reponse = client.messages.create(
model="claude-opus-5",
max_tokens=4096,
system=[
{
"type": "text",
"text": INSTRUCTION_LONGUE_ET_STABLE,
"cache_control": {"type": "ephemeral"},
}
],
messages=[{"role": "user", "content": question_variable}],
)
print(reponse.usage.cache_creation_input_tokens) # écrits dans le cache
print(reponse.usage.cache_read_input_tokens) # lus depuis le cache
print(reponse.usage.input_tokens) # payés plein tarifL’invariant qui explique tout
Un seul octet différent dans le préfixe invalide tout ce qui suit.
Cela suffit à expliquer la quasi-totalité des caches qui ne prennent pas.
| Ce qui casse le cache | Pourquoi |
|---|---|
| Une date ou une heure dans l’instruction système | Change à chaque requête |
| Un identifiant de session ou de requête en tête | Idem |
json.dumps(d) sans sort_keys=True | L’ordre des clés varie |
| Une liste d’outils construite dynamiquement | L’ordre change |
| Un nom d’utilisateur dans l’instruction système | Préfixe par utilisateur, aucun partage |
| Un changement de modèle | Les caches sont propres à un modèle |
Le diagnostic est immédiat : si cache_read_input_tokens reste à zéro sur des requêtes au préfixe théoriquement identique, il y a un invalidateur. Comparez les octets réellement envoyés entre deux requêtes.
Les instructions en cours de conversation
Cas fréquent : vous voulez ajouter une consigne au milieu d’une conversation — changement de mode, contexte nouveau. Modifier l’instruction système invalide tout l’historique cché.
Sur les modèles qui le prennent en charge, un message de rôle system peut être ajouté dans la liste des messages, après l’historique. Le préfixe caché reste intact, et l’instruction porte l’autorité d’une consigne d’opérateur plutôt que d’un message utilisateur.
messages = [
*historique,
{"role": "user", "content": message_utilisateur},
{"role": "system", "content": "Mode concis : réponses de moins de 40 mots."},
]Le seuil minimal
Un préfixe trop court n’est pas mis en cache, sans erreur ni avertissement : cache_creation_input_tokens reste simplement à zéro. Le seuil varie selon les modèles — quelques centaines à quelques milliers de jetons. Vérifiez avec le compteur d’usage plutôt que de supposer.
L’arithmétique
Une écriture en cache coûte un peu plus cher qu’une lecture normale ; une lecture coûte environ un dixième. Le seuil de rentabilité se situe donc à partir de la deuxième requête avec une durée de vie courte.
Autrement dit : dès que deux requêtes partagent un préfixe substantiel, la mise en cache est rentable. Ce n’est pas une optimisation avancée, c’est un réglage par défaut.
Le traitement par lot
Pour tout ce qui n’a pas besoin d’une réponse immédiate, l’API de lots divise le coût par deux.
lot = client.messages.batches.create(
requests=[
{
"custom_id": f"ticket-{t.id}",
"params": {
"model": "claude-opus-5",
"max_tokens": 1024,
"system": SYSTEME, # partagé, donc cachable
"messages": [{"role": "user", "content": t.texte}],
},
}
for t in tickets
]
)On interroge ensuite le lot jusqu’à ce que son traitement soit terminé, puis on parcourt les résultats. Trois points pratiques.
Les résultats arrivent dans un ordre quelconque. Indexez par custom_id, jamais par position. C’est la source de bug la plus fréquente.
Chaque résultat a son propre statut. Réussite, erreur, annulation, expiration : traitez les quatre.
Le lot se combine avec le cache. Une instruction système partagée par mille requêtes est mise en cache, et l’ensemble bénéficie en plus de la remise de lot.
Bon pour : classification nocturne, analyse de corpus, réindexation, enrichissement de base, évaluations. Inutilisable pour : toute interface où quelqu’un attend.
L’effort, et le résultat contre-intuitif
Le réglage d’effort du chapitre 2 a un impact direct sur la facture, et il ne va pas dans le sens qu’on croit.
Sur une tâche courte, un effort élevé consomme plus pour un résultat identique. Baissez.
Sur une tâche longue et agentique, un effort élevé coûte souvent moins cher. Un modèle qui réfléchit mieux au départ enchaîne moins d’allers-retours ; un modèle qui bâcle recommence trois fois — et chaque reprise fait relire tout l’historique.
Il existe par ailleurs un mécanisme de budget de tâche : on annonce au modèle une enveloppe de jetons pour une boucle entière, et il s’en sert pour se répartir le travail et conclure proprement plutôt que d’être coupé net. À ne pas confondre avec max_tokens, qui est un plafond dur dont le modèle n’a pas conscience.
Choisir le modèle par le coût
Le chapitre 2 donnait la règle qualitative. Voici son pendant économique.
Le modèle rapide pour les tâches mécaniques en volume : classification, extraction, étiquetage, reformulation. Sur des dizaines de milliers d’appels, l’écart de facture est considérable.
Le modèle intermédiaire pour la majorité du travail applicatif.
Le modèle le plus capable pour ce qui le mérite : raisonnement long, agents autonomes, cas où une erreur coûte plus cher que l’appel.
Le mélange est souvent la bonne réponse : un modèle rapide pour trier et pré-filtrer, un modèle capable sur les seuls cas retenus. Attention cependant à la conséquence sur le cache : changer de modèle en cours de conversation invalide le cache. Faites le tri dans un appel séparé plutôt qu’au milieu d’une session.
Un assistant documentaire, du prototype au budget tenable
La situation
Un assistant qui répond sur une documentation interne de 400 pages. Le prototype fonctionne. La projection à mille questions par jour donne un coût mensuel inacceptable.
La décomposition
Chaque appel envoie : l’instruction système (2 000 jetons), la documentation pertinente (30 000 jetons), la question (100 jetons). Soit 32 100 jetons d’entrée par question, mille fois par jour.
Correction 1 — mettre en cache ce qui est stable
L’instruction système et le socle documentaire commun ne changent jamais. Ils passent en tête, avec un point de coupure :
system=[
{"type": "text", "text": INSTRUCTION},
{"type": "text", "text": SOCLE_DOCUMENTAIRE,
"cache_control": {"type": "ephemeral"}},
]Vérification immédiate : cache_read_input_tokens doit devenir non nul à partir du deuxième appel.
Correction 2 — trouver l’invalidateur
Il reste à zéro. L’instruction système contenait f"Date du jour : {date.today()}". Un octet par jour, mais surtout : la date était avant le socle documentaire, donc elle invalidait tout.
La date part dans le message utilisateur. Le cache prend.
Correction 3 — ne pas tout envoyer
Envoyer 400 pages pour répondre à une question sur une seule est absurde, et le chapitre 3 rappelle que la qualité en souffre aussi. Un pré-filtrage sélectionne les sections pertinentes.
Le socle commun reste caché ; seules les sections spécifiques varient.
Correction 4 — le bon modèle pour la bonne étape
Le pré-filtrage — « quelles sections concernent cette question ? » — est une tâche de classification. Elle passe sur le modèle rapide, dans un appel séparé pour ne pas casser le cache de l’appel principal.
Le résultat
Le coût par question s’effondre : la majeure partie de l’entrée est lue en cache, le reste a été réduit, et l’étape mécanique tourne sur un modèle bon marché. Le projet devient viable — et la qualité s’améliore, parce que le contexte est mieux ciblé.
Ce qu’il faut retenir de cet exemple
Aucune des quatre corrections n’est une astuce : ce sont des décisions d’architecture. Elles se prennent bien mieux avant le déploiement qu’après la facture.
Ce qu’il faut retenir
Vérifier que c’est passé
3 questions. Rien n’est enregistré, rien n’est envoyé : c’est pour vous.
Le calcul avant le déploiement
Prenez un usage que vous envisagez. Comptez les jetons d’un appel typique avec l’endpoint de comptage, pas à l’estimation. Multipliez par le volume quotidien attendu, puis par trente. Refaites le calcul en supposant 90 % de lecture en cache sur la partie stable. L’écart entre les deux chiffres vous dira si la mise en cache est un confort ou une condition de viabilité.
Un indice, si vous bloquez
Comptez aussi la sortie. Sur un usage conversationnel, elle représente souvent une part de la facture supérieure à ce qu’on imagine, parce qu’elle est facturée plus cher que l’entrée.