Retour au blog

IA pour débutantsGuide

Plafonner le coût d'un lot LLM en Python : un registre et un budget strict

Journalisez les tokens et le coût de chaque appel API dans un fichier JSONL, arrêtez le lot à un budget et estimez la facture avant de l'exécuter. Code Python inclus.

Par CheapTokens TeamPublié le 6 min de lecture

Terminal affichant une exécution par lots qui s'arrête lorsque le budget est atteint
Sur cette page
  1. Ce que fait l'exécution Clocktower que vous pouvez copier
  2. Le registre : un fichier, une ligne par appel
  3. La boucle par lots : ignorer ce qui est fait, s'arrêter au plafond
  4. Estimer la facture avant d'appuyer sur exécuter
  5. Quand la facture est plus grosse que prévu : lire le registre
  6. Avant de lancer

Votre script boucle sur 200 prompts, vous fermez le capot de l'ordinateur portable, et au matin vous découvrez ce que la nuit a coûté. Deux petites habitudes règlent cela. Après chaque appel API, ajoutez une ligne avec les tokens et le coût dans un fichier de registre. Avant chaque appel, comparez le total cumulé avec un budget et arrêtez s'il est atteint. Cela fait moins de 50 lignes de Python, et une exécution d'essai sur 5 prompts vous donne la facture avant que l'exécution réelle ne commence.

Ce que fait l'exécution Clocktower que vous pouvez copier#

Dwarkesh Patel, animateur du Dwarkesh Podcast, a fait s'affronter 11 modèles dans Blood on the Clocktower, 386 parties selon le README, et a publié le code, les logs et les classements (github.com/dwarkeshsp/clocktower, consulté le 8 novembre 2026). Deux détails du dépôt valent la peine d'être repris. Chaque répertoire de partie conserve un log par appel avec les tokens et le coût, et les dépenses vont dans games/ledger.jsonl. Un flag --budget plafonne la dépense totale ; la valeur par défaut est de 1 000 $. Le site du benchmark ajoute une troisième garde : chaque modèle dispose du même plafond de 8 000 tokens par tour (clocktowerbench.com).

Donc : un plafond sur chaque appel, un log de chaque appel, un plafond sur le total. Un tournoi a besoin des trois, et votre exécution nocturne aussi, en plus petit.

Considérez le registre comme le rouleau de tickets d'une caisse de magasin. Chaque vente imprime une ligne ; le gérant peut additionner le rouleau à tout moment et sait quand la caisse du jour est épuisée.

Le registre : un fichier, une ligne par appel#

Voici l'ensemble. Il utilise le paquet openai pointé vers CheapTokens (https://api.cheaptokens.dev/v1, voir la documentation pour la clé et les identifiants de modèles).

python
import json
import os
from pathlib import Path

from openai import OpenAI

client = OpenAI(
    base_url='https://api.cheaptokens.dev/v1',
    api_key=os.environ['CHEAPTOKENS_API_KEY'],
)
MODEL = 'deepseek-ai/DeepSeek-V4-Flash-0731'
PRICE_IN, PRICE_OUT = 1.00, 2.00  # USD per 1M tokens: example, substitute current ones


class BudgetExceeded(Exception):
    pass


class Ledger:
    def __init__(self, path, budget_usd):
        self.path = Path(path)
        self.budget = budget_usd
        self.rows = []
        if self.path.exists():
            with self.path.open() as f:
                self.rows = [json.loads(line) for line in f if line.strip()]

    @property
    def spent(self):
        return sum(r['cost_usd'] for r in self.rows)

    def done(self):
        return {r['unit'] for r in self.rows}

    def check(self, unit):
        if self.spent >= self.budget:
            raise BudgetExceeded(
                f'stopped before {unit}: spent {self.spent:.4f} of {self.budget} USD'
            )

    def add(self, unit, usage, finish_reason):
        cost = (usage.prompt_tokens * PRICE_IN
                + usage.completion_tokens * PRICE_OUT) / 1_000_000
        row = {
            'unit': unit,
            'model': MODEL,
            'prompt_tokens': usage.prompt_tokens,
            'completion_tokens': usage.completion_tokens,
            'finish_reason': finish_reason,
            'cost_usd': round(cost, 6),
        }
        self.rows.append(row)
        with self.path.open('a') as f:
            f.write(json.dumps(row) + '\n')


def ask(ledger, unit, prompt, max_tokens=400):
    ledger.check(unit)
    resp = client.chat.completions.create(
        model=MODEL,
        messages=[{'role': 'user', 'content': prompt}],
        max_tokens=max_tokens,
    )
    ledger.add(unit, resp.usage, resp.choices[0].finish_reason)
    return resp.choices[0].message.content

Chaque appel passe par les mêmes quatre étapes :

Quatre étapes autour de chaque appel API : vérifier le budget, appeler, lire l'usage, ajouter au registre
Le cycle derrière chaque appel dans le wrapper

Trois choix y sont délibérés.

  • Un objet JSON par ligne (JSONL), ouvert en mode ajout. Si le script plante à l'appel 130, les appels 1 à 129 sont déjà sur le disque.
  • Le total vient du fichier, pas d'une variable. Redémarrez le script et le plafond tient toujours, car le registre se souvient de ce que l'exécution précédente a dépensé.
  • finish_reason est journalisé à côté des tokens. Une ligne qui indique length signifie que la réponse a atteint votre max_tokens et a été tronquée. Vous voudrez le savoir plus tard.

Les deux constantes PRICE_ sont des espaces réservés. Prenez les chiffres actuels dans la table de tarifs. Pour DeepSeek-V4-Flash au moment où vous lisez ceci, c'est $0.015 par 1M de tokens d'entrée et $0.10 par 1M de tokens de sortie. Remplacez MODEL par zai-org/GLM-5.3-Flash ou MiniMaxAI/MiniMax-M2.7 et les deux constantes, et le même registre fonctionne pour eux.

La boucle par lots : ignorer ce qui est fait, s'arrêter au plafond#

La boucle est courte. Disons que vos prompts se trouvent dans un dict indexé par un identifiant d'unité, le nom d'une pièce de travail, comme doc-001 :

python
prompts = {
    'doc-001': 'Summarize this ticket in one sentence: ...',
    'doc-002': 'Summarize this ticket in one sentence: ...',
    # ... your 200 prompts
}

ledger = Ledger('ledger.jsonl', budget_usd=0.50)
already = ledger.done()

for unit, prompt in prompts.items():
    if unit in already:
        continue
    try:
        answer = ask(ledger, unit, prompt)
    except BudgetExceeded as e:
        print(e)
        break
    Path(f'out-{unit}.txt').write_text(answer)

Lancez-le, faites Ctrl+C à mi-chemin, relancez-le : les unités terminées sont ignorées, donc le registre sert aussi de fichier de progression. Laissez le budget s'épuiser et vous obtenez une ligne au lieu d'une surprise, quelque chose comme stopped before doc-142: spent 0.5003 of 0.5 USD.

Remarquez le mot avant. La vérification a lieu avant chaque appel, donc le dernier appel peut faire dépasser légèrement le budget. Le dépassement est borné : un appel ne peut pas coûter plus que votre prompt le plus long plus max_tokens. Pour un prompt de 1 500 tokens et max_tokens=400, c'est au plus $0.000063. C'est pourquoi chaque appel du wrapper a un max_tokens : il transforme un risque ouvert en un nombre autour duquel vous pouvez planifier.

Lignes d'un fichier de registre avec les tokens par appel et un arrêt final sur budget
Exemple de lignes de registre et le message d'arrêt (valeurs illustratives)

Estimer la facture avant d'appuyer sur exécuter#

Maintenant l'autre moitié : connaître le coût avant le lancement. Faites passer les cinq premiers prompts par ask() avec un petit budget et lisez le registre. Disons que les cinq lignes font en moyenne 1 500 tokens d'entrée et 400 tokens de sortie. Ce sont vos chiffres pour tout le lot.

Pour 200 prompts, cela fait 200 × 1 500 = 300 000 tokens d'entrée et 200 × 400 = 80 000 tokens de sortie. Voici cette charge de travail aux tarifs actuels :

Modèle Par requête 200 requêtes OpenRouter, 200 requêtes
GLM-5.3-Flash $0.000046 $0.0092 $0.0167
DeepSeek-V4-Flash $0.000063 $0.0125 $0.0322
MiniMax-M2.7 $0.00014 $0.027 $0.324

1 500 tokens d'entrée et 400 tokens de sortie par requête. Les prix par million de tokens sont actuels et évoluent avec nos tarifs ; la colonne OpenRouter indique des prix de référence pour la même charge de travail.

Le tableau a aussi une colonne OpenRouter. En comptant l'entrée et la sortie ensemble, DeepSeek-V4-Flash revient moins cher qu'OpenRouter pour cette charge, et c'est cette somme que le lot paie réellement.

Les prompts diffèrent en longueur, donc cinq échantillons sont une estimation, pas une promesse. Utilisez le plus long des cinq quand vous voulez une borne supérieure sûre, puis laissez le plafond de budget rattraper le reste. Vous voulez comparer deux modèles sur les mêmes 200 prompts ? Donnez à chaque modèle son propre fichier de registre, par exemple Ledger(f'ledger-{MODEL.split("/")[-1]}.jsonl', 0.50), et les deux totaux se retrouvent côte à côte ensuite.

Essayez votre propre lot ici : définissez les tokens par requête à partir de votre échantillon et le nombre de requêtes que vous prévoyez.

Calculateur de coût

Votre lot : tokens issus de votre échantillon de 5 prompts

Tokens d'entrée par requête: 1 500 · Tokens de sortie par requête: 400 · Requêtes par jour: 200

ModèlePar jourPar mois
GLM-5.3-Flash$0.0092$0.276
DeepSeek-V4-Flash$0.0125$0.375
MiniMax-M2.7$0.027$0.81

Pour en savoir plus sur le comptage des tokens avant l'envoi, voir compter les tokens avec tiktoken.

Quand la facture est plus grosse que prévu : lire le registre#

Le troisième scénario de lecture est le plus désagréable. Une exécution d'agent tourne mal et boucle, et le tableau de bord n'affiche qu'un total. Le registre montre où cela s'est passé. Triez les lignes par coût :

python
rows = [json.loads(line) for line in open('ledger.jsonl')]
for r in sorted(rows, key=lambda r: r['cost_usd'], reverse=True)[:5]:
    print(r['unit'], r['prompt_tokens'], r['completion_tokens'], r['finish_reason'])

Pour un agent, nommez l'unité d'après l'étape (step-17) afin de voir la forme de l'exécution. Quelques motifs reviennent sans cesse ; associez chacun à sa cause habituelle :

Associer les paires

Associez chaque motif de registre à sa cause habituelle

  • prompt_tokens augmente un peu à chaque ligneLa boucle renvoie tout l'historique à chaque étape
  • completion_tokens égale max_tokens et finish_reason vaut lengthLes réponses sont tronquées à votre plafond, et vous payez le texte tronqué
  • La même unité apparaît plusieurs fois de suiteUne boucle de nouvelle tentative, ou un agent qui répète une étape
  • Une ligne coûte bien plus que toutes les autresUne entrée surdimensionnée, comme un fichier entier collé dans le prompt

Chaque motif est visible dans le registre bien avant la facture. Le premier est traité dans pourquoi un agent relit son historique.

Si vous voulez aussi des dépenses par projet et des alertes hors du script, ce guide sur les alertes de budget par projet couvre cet aspect. Le registre ici est la partie qui vit dans votre code et peut dire stop.

Avant de lancer#

Liste de contrôle

Avant la première exécution réelle

La taille de contexte et la limite de réponse de DeepSeek-V4-Flash, utiles quand vous choisissez max_tokens, sont sur sa page : DeepSeek-V4-Flash sur CheapTokens.