Notes · Article
Vous comptez les tokens de Claude Code depuis les logs ? Votre total est sans doute 2,5× trop élevé
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…
- texte
- appel d’outil
Le log, une ligne par partie
- uuid a1…id msg_01X…entrée 12 · sortie 480
- 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’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.
Pourquoi ça compteSurestimez l’usage et l’abonnement paraît bien plus avantageux qu’il ne l’est.
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
- msg_01X…
- msg_01X…
- msg_01X…
- Prendre le premiertrop bas
- Les additionnertrop haut
- Garder le plus hautjuste, avec l’horodatage le plus ancien
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éeb2…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.
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.
- 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_countrépété avec un total cumulé inchangéignorer la répétition
À 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.
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