---
title: Vous comptez les tokens de Claude Code depuis les logs ? Votre total est sans doute 2,5× trop élevé
description: Claude Code écrit une ligne de log par partie de réponse, et chaque ligne répète la consommation de tokens de toute la réponse. Comptez les lignes et vous surestimez les tokens d’environ 2,5×, la sortie d’environ 4,4×. La clé : l’id du message. Le correctif de Meterlex, et le même piège dans Gemini CLI.
date: 2026-09-18
tags: Claude Code, outils IA, observabilité, Python
project: https://github.com/brbousnguar/meterlex
---

Si vous additionnez ligne par ligne la consommation de tokens relevée dans les logs de session de Claude Code, votre total est faux. Sur ma machine, il était **2,5×** trop élevé, et les tokens de sortie **4,4×** trop élevés.

Les logs ne mentent pas. Chaque ligne répète simplement l’usage de toute la réponse à laquelle elle appartient. Comptez les réponses, pas les lignes : prenez l’id du message comme clé.

Je suis tombé dessus en construisant [Meterlex](/fr/projets/meterlex.html), un outil qui chiffre mon usage de l’IA pour coder aux tarifs de l’API. Voici ce qui se passe, et le correctif.

## Une réponse, plusieurs lignes

Claude Code range ses transcriptions dans `~/.claude/projects/**/*.jsonl`. Une réponse de l’assistant ne tient pas sur une ligne. Chacune de ses parties est loguée sur sa propre ligne : la réflexion, le texte, chaque appel d’outil.

Et chacune de ces lignes porte le même bloc `usage`, celui de toute la réponse. En simplifiant, avec des chiffres inventés, une réponse avec une partie texte et un appel d’outil ressemble à ça :

```json
{"type": "assistant", "uuid": "a1…", "message": {"id": "msg_01X…", "usage": {"input_tokens": 12, "output_tokens": 480}}}
{"type": "assistant", "uuid": "b2…", "message": {"id": "msg_01X…", "usage": {"input_tokens": 12, "output_tokens": 480}}}
```

Deux lignes, deux `uuid` différents, un seul `message.id`. Additionnez les deux et vous avez compté cette réponse deux fois. Une réponse avec cinq appels d’outils est comptée six ou sept fois.

<figure class="fd-fig cc-lines" aria-labelledby="cc1-cap" data-desc="Une réponse de l’assistant dans le log de Claude Code, avec les chiffres inventés de la note. La réponse a un seul id de message, msg_01X, et deux parties : une partie texte et un appel d’outil. Chaque partie est loguée sur sa propre ligne avec son propre uuid, a1 et b2, et les deux lignes portent le même bloc usage pour toute la réponse : 12 tokens d’entrée et 480 tokens de sortie. Additionner les lignes donne 960 tokens de sortie ; compter la réponse en donne 480. Une réponse avec cinq appels d’outils est comptée six ou sept fois.">
<div class="cc-lines-in">
<div class="cc-reply">
<p class="cc-k">Une réponse</p>
<p class="cc-id">msg_01X…</p>
<ol class="cc-list cc-parts">
<li>texte</li>
<li>appel d’outil</li>
</ol>
</div>
<p class="cc-arrow" aria-hidden="true"><span>→</span></p>
<div class="cc-log">
<p class="cc-k">Le log, une ligne par partie</p>
<ol class="cc-list cc-loglines">
<li><span>uuid <b>a1…</b></span><span>id <b>msg_01X…</b></span><span class="cc-use">entrée 12 · sortie 480</span></li>
<li><span>uuid <b>b2…</b></span><span>id <b>msg_01X…</b></span><span class="cc-use">entrée 12 · sortie 480</span></li>
</ol>
</div>
</div>
<div class="cc-tally">
<p class="cc-bad"><span>Somme des lignes</span><b>sortie 960</b></p>
<p class="cc-good"><span>Compter la réponse</span><b>sortie 480</b></p>
</div>
<p class="mp-strip"><b>Cinq appels d’outils</b><span>Une réponse avec cinq appels d’outils est comptée six ou sept fois.</span></p>
<figcaption id="cc1-cap">L’exemple de la note, avec ses chiffres inventés : deux lignes, deux uuid, un seul id de message, et l’usage de toute la réponse sur les deux.</figcaption>
</figure>

## L’ampleur de l’erreur

Sur les transcriptions de mon Mac : **44 158** lignes de l’assistant, mais seulement **18 701** réponses réelles.

Compter les lignes donnait 2,5× le vrai nombre de tokens. Les tokens de sortie étaient 4,4× trop élevés.

Ce n’est pas un détail si vous faites ce que fait Meterlex : comparer un abonnement forfaitaire avec ce que le même usage coûterait sur l’API. Surestimez l’usage et l’abonnement paraît bien plus avantageux qu’il ne l’est.

<figure class="fd-fig cc-size" aria-labelledby="cc2-cap" data-desc="L’ampleur de l’erreur sur les transcriptions de mon Mac, à l’échelle. Il y avait 44 158 lignes de l’assistant mais seulement 18 701 réponses réelles. Compter les lignes donnait 2,5 fois le vrai total de tokens, et les tokens de sortie étaient 4,4 fois trop élevés. Surestimez l’usage et un abonnement forfaitaire paraît bien plus avantageux qu’il ne l’est face au même usage aux tarifs de l’API.">
<div class="cc-bars">
<p class="cc-k">Les transcriptions de mon Mac</p>
<div class="cc-bar-row"><p class="cc-bar-l">Lignes de l’assistant</p><p class="cc-bar"><i></i></p><p class="cc-bar-n">44 158</p></div>
<div class="cc-bar-row"><p class="cc-bar-l">Réponses réelles</p><p class="cc-bar"><i class="cc-real cc-w-replies"></i></p><p class="cc-bar-n">18 701</p></div>
</div>
<div class="cc-bars">
<p class="cc-k">Compté par ligne, face au vrai total</p>
<div class="cc-bar-row"><p class="cc-bar-l">Tous les tokens</p><p class="cc-bar cc-over"><i class="cc-real cc-w-tokens"></i></p><p class="cc-bar-n">2,5×</p></div>
<div class="cc-bar-row"><p class="cc-bar-l">Tokens de sortie</p><p class="cc-bar cc-over"><i class="cc-real cc-w-output"></i></p><p class="cc-bar-n">4,4×</p></div>
</div>
<ul class="hx-key" aria-hidden="true">
<li><i class="cc-sw-real"></i>le vrai total</li>
<li><i class="cc-sw-over"></i>ajouté en comptant les lignes</li>
</ul>
<p class="mp-strip"><b>Pourquoi ça compte</b><span>Surestimez l’usage et l’abonnement paraît bien plus avantageux qu’il ne l’est.</span></p>
<figcaption id="cc2-cap">De vrais chiffres, venus de mon Mac, chaque barre à l’échelle. Dans la paire du bas, la partie cobalt est le vrai total et la partie en pointillés ce que le comptage par ligne a ajouté.</figcaption>
</figure>

## Le correctif : l’id du message comme clé

Prenez `message.id` comme clé d’une réponse et, quand vous la revoyez, fusionnez au lieu d’additionner. Le seul point subtil : les chiffres d’une réponse peuvent grossir pendant le streaming, donc gardez chaque compteur à sa valeur la plus haute, pas la première ni la somme.

Voici le cœur du code, tiré du collecteur de Meterlex :

```python
def _merge_max(a, b):
    """Two records of the same reply: each token count at its largest
    (a reply's numbers only grow as it streams), the earliest time."""
    m = dict(a)
    for f in TOKEN_FIELDS:
        m[f] = max(a[f], b[f])
    m["ts"] = min(a["ts"], b["ts"])
    return m

key = (session_id, msg.get("id") or uuid)
replies[key] = _merge_max(replies[key], t) if key in replies else t
```

Les lignes sans `message.id` se rabattent sur leur `uuid`, donc rien ne se perd.

<figure class="fd-fig cc-key" aria-labelledby="cc3-cap" data-desc="Le correctif, en schéma, avec des longueurs de barres indicatives. Une réponse a pour clé son id de session et son id de message ; une ligne sans id de message se rabat sur son propre uuid, donc rien ne se perd. Quand une clé revient, les enregistrements sont fusionnés au lieu d’être additionnés. Les chiffres d’une réponse peuvent grossir pendant le streaming, donc trois enregistrements d’une même réponse montrent un compte court, un plus long et un plus long encore. Prendre le premier enregistrement donne trop peu, les additionner donne trop, et garder chaque compteur à sa valeur la plus haute donne le bon résultat. La réponse fusionnée garde l’horodatage le plus ancien.">
<div class="cc-keyrow">
<p class="cc-keychip"><span class="cc-k">La clé d’une réponse</span><b>(session_id, message.id)</b></p>
<p class="cc-fall">Pas de <code>message.id</code> ? Le <code>uuid</code> de la ligne, donc rien ne se perd.</p>
</div>
<div class="cc-recs">
<p class="cc-k">Une réponse, trois enregistrements, pendant le streaming</p>
<ol class="cc-list cc-rec">
<li><span>msg_01X…</span><p class="cc-bar"><i class="cc-w-r1"></i></p></li>
<li><span>msg_01X…</span><p class="cc-bar"><i class="cc-w-r2"></i></p></li>
<li><span>msg_01X…</span><p class="cc-bar"><i class="cc-w-r3"></i></p></li>
</ol>
</div>
<ul class="cc-list cc-pick">
<li class="cc-no"><b>Prendre le premier</b><p class="cc-bar"><i class="cc-w-r1"></i></p><span>trop bas</span></li>
<li class="cc-no"><b>Les additionner</b><p class="cc-bar"><i class="cc-w-sum"></i></p><span>trop haut</span></li>
<li class="cc-ok"><b>Garder le plus haut</b><p class="cc-bar"><i class="cc-real cc-w-r3"></i></p><span>juste, avec l’horodatage le plus ancien</span></li>
</ul>
<figcaption id="cc3-cap">Comment le collecteur de Meterlex fusionne les enregistrements qui partagent une clé. Un schéma : les longueurs de barres sont indicatives.</figcaption>
</figure>

## Migrer les entrées déjà stockées

Si vous stockiez une entrée par ligne de log, renvoyer la même transcription avec la nouvelle clé ajouterait juste une troisième copie. Chaque réponse porte donc aussi ses anciens `uuid` par ligne dans `alt_keys`, et le hub fusionne dans la nouvelle entrée toute entrée stockée sous l’une de ces clés. Les anciennes données se nettoient d’elles-mêmes à la prochaine lecture d’un fichier.

<figure class="fd-fig cc-mig" aria-labelledby="cc4-cap" data-desc="Ce que le hub garde pour une réponse quand la même transcription est renvoyée avec la nouvelle clé. Il avait stocké deux entrées, une par ligne de log, a1 et b2. Avec la nouvelle clé seule, le renvoi ajoute la réponse sous msg_01X, une troisième copie. Quand la nouvelle entrée porte aussi ses anciens uuid dans alt_keys, le hub y fusionne les entrées stockées a1 et b2 et n’en garde qu’une. Les anciennes données se nettoient d’elles-mêmes à la prochaine lecture d’un fichier.">
<div class="mp-two-row">
<div class="mp-two-card">
<p class="mp-two-who">Renvoi avec la nouvelle clé seule</p>
<p class="mp-two-name">Trois copies</p>
<ul class="cc-list cc-rows">
<li>a1…<span>stockée, par ligne</span></li>
<li>b2…<span>stockée, par ligne</span></li>
<li class="cc-dup">msg_01X…<span>une troisième copie</span></li>
</ul>
</div>
<p class="mp-two-vs" aria-hidden="true">vs</p>
<div class="mp-two-card mp-two-peter">
<p class="mp-two-who">La nouvelle entrée porte alt_keys</p>
<p class="mp-two-name">Une seule entrée</p>
<ul class="cc-list cc-rows">
<li class="cc-gone"><s>a1…</s><span>fusionnée</span></li>
<li class="cc-gone"><s>b2…</s><span>fusionnée</span></li>
<li class="cc-new">msg_01X…<span>alt_keys : a1…, b2…</span></li>
</ul>
</div>
</div>
<p class="mp-strip"><b>À la prochaine lecture</b><span>Les anciennes données se nettoient d’elles-mêmes à la prochaine lecture d’un fichier.</span></p>
<figcaption id="cc4-cap">La réponse d’exemple après un renvoi. C’est parce qu’elle porte les anciennes clés que le hub peut fusionner les copies en une seule entrée.</figcaption>
</figure>

## Gemini CLI fait pareil

Autre outil, même piège. Gemini CLI ajoute de nouveau un message en fin de fichier chaque fois qu’il le met à jour. Le fichier d’un projet comptait **56 651** lignes pour **908** messages. Même correctif : l’id du message comme clé.

Codex en a une version plus légère : il peut répéter un événement `token_count` avec un total cumulé inchangé. Meterlex les ignore.

<figure class="fd-fig cc-gem" aria-labelledby="cc5-cap" data-desc="Le même piège dans d’autres outils. Le fichier d’un projet Gemini CLI comptait 56 651 lignes pour 908 messages, à l’échelle. Claude Code écrit une ligne par partie de réponse, chacune avec l’usage de toute la réponse : l’id du message comme clé. Gemini CLI ajoute de nouveau un message en fin de fichier chaque fois qu’il le met à jour : l’id du message comme clé. Codex en a une version plus légère, un événement token_count répété avec un total cumulé inchangé : Meterlex les ignore.">
<div class="cc-bars">
<p class="cc-k">Le fichier d’un projet Gemini CLI</p>
<div class="cc-bar-row"><p class="cc-bar-l">Lignes</p><p class="cc-bar"><i></i></p><p class="cc-bar-n">56 651</p></div>
<div class="cc-bar-row"><p class="cc-bar-l">Messages</p><p class="cc-bar"><i class="cc-real cc-w-gemini"></i></p><p class="cc-bar-n">908</p></div>
</div>
<ul class="cc-list cc-traps">
<li><b>Claude Code</b><span>Une ligne par partie de réponse, chacune avec l’usage de toute la réponse</span><em>clé : id du message</em></li>
<li><b>Gemini CLI</b><span>Un message ajouté de nouveau à chaque mise à jour</span><em>clé : id du message</em></li>
<li><b>Codex, en plus léger</b><span>Un événement <code>token_count</code> répété avec un total cumulé inchangé</span><em>ignorer la répétition</em></li>
</ul>
<figcaption id="cc5-cap">Autres outils, même piège. Les chiffres de Gemini viennent du fichier d’un seul projet, barres à l’échelle.</figcaption>
</figure>

## À vous d’essayer

Si vous suivez votre usage de l’IA à partir de logs locaux, vérifiez vos clés. Sur l’une de vos transcriptions, comparez le nombre d’id de message distincts au nombre de lignes. L’écart, c’est la surestimation.

<figure class="fd-fig cc-try" aria-labelledby="cc6-cap" data-desc="Un schéma de la vérification, sur une transcription inventée de 11 lignes. Les lignes appartiennent à cinq réponses, de A à E, il y a donc 5 id de message distincts. Les 6 lignes en trop sont l’écart, et l’écart, c’est la surestimation. Claude Code range ses transcriptions dans ~/.claude/projects/**/*.jsonl.">
<div class="cc-try-in">
<div class="cc-cnt">
<p class="cc-k">Lignes</p>
<ol class="cc-list cc-ids" aria-hidden="true"><li>A</li><li class="cc-rep">A</li><li class="cc-rep">A</li><li>B</li><li class="cc-rep">B</li><li>C</li><li>D</li><li class="cc-rep">D</li><li class="cc-rep">D</li><li class="cc-rep">D</li><li>E</li></ol>
<p class="cc-n">11</p>
</div>
<div class="cc-cnt">
<p class="cc-k">Id de message distincts</p>
<ol class="cc-list cc-ids" aria-hidden="true"><li class="cc-uniq">A</li><li class="cc-uniq">B</li><li class="cc-uniq">C</li><li class="cc-uniq">D</li><li class="cc-uniq">E</li></ol>
<p class="cc-n cc-n-ok">5</p>
</div>
<p class="cc-gap"><b>6</b><span>lignes en trop. L’écart, c’est la surestimation.</span></p>
</div>
<p class="mp-strip"><b>Où regarder</b><span>Claude Code range ses transcriptions dans <code>~/.claude/projects/**/*.jsonl</code>.</span></p>
<figcaption id="cc6-cap">Essayez sur l’une de vos transcriptions. Celle-ci est inventée : 11 lignes, 5 réponses.</figcaption>
</figure>

Meterlex fait tout ça pour Claude Code, Codex, Gemini CLI, Antigravity, Ollama, Copilot CLI et OpenClaw, et il est sous licence MIT sur [GitHub](https://github.com/brbousnguar/meterlex). Si vos chiffres vous semblent faux d’une autre façon, ouvrez une issue.
