Brahim Bousnguar

Notes · Article

Vous comptez les tokens de Claude Code depuis les logs ? Votre total est sans doute 2,5× trop élevé

Par · · 3 min de lecture

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, 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 :

{"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.

Une réponse

msg_01X…

  1. texte
  2. appel d’outil

Le log, une ligne par partie

  1. uuid a1…id msg_01X…entrée 12 · sortie 480
  2. uuid b2…id msg_01X…entrée 12 · sortie 480

Somme des lignessortie 960

Compter la réponsesortie 480

Cinq appels d’outilsUne réponse avec cinq appels d’outils est comptée six ou sept fois.

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.

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.

Les transcriptions de mon Mac

Lignes de l’assistant

44 158

Réponses réelles

18 701

Compté par ligne, face au vrai total

Tous les tokens

2,5×

Tokens de sortie

4,4×

Pourquoi ça compteSurestimez l’usage et l’abonnement paraît bien plus avantageux qu’il ne l’est.

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é.

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 :

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.

La clé d’une réponse(session_id, message.id)

Pas de message.id ? Le uuid de la ligne, donc rien ne se perd.

Une réponse, trois enregistrements, pendant le streaming

  1. msg_01X…

  2. msg_01X…

  3. msg_01X…

  • Prendre le premier

    trop bas
  • Les additionner

    trop haut
  • Garder le plus haut

    juste, avec l’horodatage le plus ancien
Comment le collecteur de Meterlex fusionne les enregistrements qui partagent une clé. Un schéma : les longueurs de barres sont indicatives.

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.

Renvoi avec la nouvelle clé seule

Trois copies

  • a1…stockée, par ligne
  • b2…stockée, par ligne
  • msg_01X…une troisième copie

La nouvelle entrée porte alt_keys

Une seule entrée

  • a1…fusionnée
  • b2…fusionnée
  • msg_01X…alt_keys : a1…, b2…

À la prochaine lectureLes anciennes données se nettoient d’elles-mêmes à la prochaine lecture d’un fichier.

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.

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.

Le fichier d’un projet Gemini CLI

Lignes

56 651

Messages

908

  • Claude CodeUne ligne par partie de réponse, chacune avec l’usage de toute la réponseclé : id du message
  • Gemini CLIUn message ajouté de nouveau à chaque mise à jourclé : id du message
  • Codex, en plus légerUn événement token_count répété avec un total cumulé inchangéignorer la répétition
Autres outils, même piège. Les chiffres de Gemini viennent du fichier d’un seul projet, barres à l’échelle.

À 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.

Lignes

11

Id de message distincts

5

6lignes en trop. L’écart, c’est la surestimation.

Où regarderClaude Code range ses transcriptions dans ~/.claude/projects/**/*.jsonl.

Essayez sur l’une de vos transcriptions. Celle-ci est inventée : 11 lignes, 5 réponses.

Meterlex fait tout ça pour Claude Code, Codex, Gemini CLI, Antigravity, Ollama, Copilot CLI et OpenClaw, et il est sous licence MIT sur GitHub. Si vos chiffres vous semblent faux d’une autre façon, ouvrez une issue.

Texte brut: claude-code-token-overcount.md · Flux Atom · Toutes les notes

Contact

Écrivez-moi.

b.bousnguar@gmail.com

Intégration SAP Commerce Cloud, MuleSoft et Salesforce · Nantes · FR / EN