---
title: "Benchmark : grepai vs grep sur Claude Code"
excerpt: "La recherche sémantique fait-elle vraiment économiser des tokens ? J'ai testé grepai sur la codebase de Excalidraw avec Claude Code. Résultat : -27% sur la facture API."
publishDate: 2026-01-20T00:00:00.000Z
tags: ["ia", "claude-code", "grepai", "benchmark", "open-source", "productivite", "anthropic", "api"]
canonical: "https://yoandev.co/grepai-benchmark"
---

**-27% sur la facture API**. C'est le résultat concret que j'ai obtenu en remplaçant les recherches `grep` par de la recherche sémantique dans Claude Code.

Dans [mon précédent article](/grepai-agent-ia-recherche-semantique), j'avançais des arguments théoriques. Cette fois, j'ai voulu **prouver** ces gains avec une expérience contrôlée sur [Excalidraw](https://github.com/excalidraw/excalidraw), un projet open source de 155 000+ lignes de code.

## TL;DR

> 📌 **Les résultats** :
> - **-55% d'appels d'outils** (139 → 62)
> - **-97% d'input tokens** (51 147 → 1 326)
> - **-27,5% sur le coût facturé** ($6.78 → $4.92)
>
> 📌 **Le protocole** :
> - 5 questions identiques sur [Excalidraw](https://github.com/excalidraw/excalidraw) (155k+ lignes de code)
> - Comparaison Claude Code standard vs Claude Code + **grepai**
> - Métriques extraites directement des logs JSON de Claude Code

---

## Le protocole expérimental

### Le terrain de test : Excalidraw

J'ai choisi [Excalidraw](https://github.com/excalidraw/excalidraw) pour plusieurs raisons :

- **Projet réel** : 155 000+ lignes de TypeScript, pas un benchmark artificiel
- **Monorepo complexe** : packages multiples (`@excalidraw/element`, `@excalidraw/math`, etc.)
- **Open source** : n'importe qui peut vérifier

### Les 5 questions du benchmark

J'ai formulé 5 questions qu'un développeur pourrait naturellement poser en découvrant une codebase. Chaque question décrit une **intention** (ce qu'on veut comprendre), pas un nom de fonction qu'on connaîtrait déjà.

| # | Question (EN) |
|---|---------------|
| 1 | *"Locate the exact mathematical function used to determine if a user's cursor is hovering inside a 'diamond' shape."* |
| 2 | *"Explain how the application calculates the intersection point when an arrow is attached to an ellipse."* |
| 3 | *"Find the algorithm responsible for simplifying or smoothing the points of a 'freedraw' line after the user releases the mouse."* |
| 4 | *"Identify the code responsible for snapping dragged elements to the grid."* |
| 5 | *"How does the codebase handle sending an element 'backward' in the z-order?"* |

### Conditions de test

Deux clones du même projet, dans **deux répertoires séparés** :

**Session 1 - Baseline (sans grepai)** :
```bash
git clone https://github.com/excalidraw/excalidraw.git excalidraw_raw
cd excalidraw_raw
claude  # Session fraîche
```

**Session 2 - Avec grepai** :
```bash
git clone https://github.com/excalidraw/excalidraw.git excalidraw_grepai
cd excalidraw_grepai
grepai init && grepai watch
grepai agent-setup --with-subagent  # Configure CLAUDE.md
claude  # Session fraîche
```

> **Configuration grepai utilisée** : Ollama (modèle `nomic-embed-text`) + stockage fichier, sur un MacBook Pro M3 Pro. Voir la [documentation grepai](https://yoanbernabeu.github.io/grepai/) pour les options disponibles.

Les 5 questions sont posées **dans le même ordre**, **sans reformulation**, dans chaque session.

---

## Les résultats

### Tokens facturés par l'API

Voici les métriques **directement extraites des logs JSON** de Claude Code :

| Métrique | Baseline | grepai | Δ |
|:---------|:---------|:-------|:--|
| **Subagents lancés** | 5 | 0 | -100% |
| **Appels d'outils** | 139 | 62 | **-55%** |
| `input_tokens` | 51 147 | 1 326 | **-97%** |
| `cache_read_input_tokens` | 5 973 161 | 7 775 888 | +30% |
| `cache_creation_input_tokens` | 563 883 | 162 289 | **-71%** |
| `output_tokens` | 476 | 347 | -27% |

### Coût facturé (tarifs Claude Opus 4.5)

| Type de token | Baseline | grepai | Δ |
|---------------|----------|--------|---|
| `input_tokens` | $0.26 | $0.01 | **-97%** |
| `cache_read_input_tokens` | $2.99 | $3.89 | +30% |
| `cache_creation_input_tokens` | $3.52 | $1.01 | **-71%** |
| `output_tokens` | $0.01 | $0.01 | -27% |
| **Total** | **$6.78** | **$4.92** | **-27,5%** |

> 💡 **En résumé** : Pour 5 questions d'exploration sur Excalidraw, **grepai** a économisé **$1.86** (de $6.78 à $4.92). Sur une journée de travail intensive avec des dizaines de questions, les économies peuvent atteindre **$10-20/jour**.

> **Note sur les tarifs** : Ces prix sont ceux de l'API Anthropic en accès direct. En pratique, je recommande d'utiliser le **forfait Max de Claude Code** ($100/mois ou $200/mois) pour un usage quotidien. Ce benchmark permet surtout de comprendre les mécanismes sous-jacents.

---

## Comprendre le cache de l'API Claude

Avant d'interpréter ces chiffres, il faut comprendre comment Anthropic facture les tokens. Ce n'est pas aussi simple qu'un tarif unique, et c'est précisément ce mécanisme qui explique pourquoi **grepai** économise 27% malgré une hausse du cache read.

L'API Claude utilise un [système de cache de prompt](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching) qui impacte directement la facturation. Voici comment ça fonctionne :

### Les 4 types de tokens

| Type | Description | Tarif (Opus 4.5) |
|------|-------------|------------------|
| `input_tokens` | Nouveaux tokens à traiter (pas en cache) | $5.00 / M |
| `cache_read_input_tokens` | Tokens déjà en cache, réutilisés | $0.50 / M (90% réduction) |
| `cache_creation_input_tokens` | Tokens mis en cache pour la première fois | $6.25 / M (25% surcoût) |
| `output_tokens` | Tokens générés par Claude | $25.00 / M |

### Cache Read vs Cache Creation : pourquoi ces différences ?

**`cache_creation_input_tokens` (-71% avec grepai)** :

Chaque fois que Claude Code lance un **subagent** (via `Task`), il crée un nouveau contexte. Ce contexte doit être mis en cache → `cache_creation`.

- **Baseline** : 5 subagents lancés = 5 nouveaux contextes = 563 883 tokens créés
- **grepai** : 0 subagent = pas de nouveau contexte = 162 289 tokens créés

C'est la **grosse économie** : avec **grepai**, Claude Code n'a pas besoin de lancer des subagents car il trouve directement les bons fichiers.

**`cache_read_input_tokens` (+30% avec grepai)** :

Le cache read est le contexte **déjà en cache** qui est réutilisé à chaque appel. Il inclut :
- Le system prompt de Claude Code (~15k tokens)
- L'historique de la conversation
- Les résultats des outils précédents

**grepai** a **plus de cache read** car les résultats de recherche s'accumulent dans le contexte de la conversation. Mais ce n'est pas grave : à $0.50/M contre $5/M pour les input tokens, c'est 10× moins cher.

### Pourquoi -97% input tokens mais seulement -27% sur la facture ?

La différence s'explique par le poids du cache read dans le coût total. Même si les input tokens baissent drastiquement, le cache read (facturé 10× moins cher) représente la majorité des tokens consommés dans les deux cas.

Les `input_tokens` (fresh) représentent le **vrai travail de traitement**, c'est-à-dire les tokens que Claude voit pour la première fois à chaque appel.

- **Baseline** : 51 147 tokens (5 subagents × ~10k tokens chacun)
- **grepai** : 1 326 tokens (pas de subagent, recherches directes)

Sans subagents, Claude n'a pas besoin de "réapprendre" le contexte à chaque exploration.

---

## Pourquoi **grepai** évite les subagents ?

C'est le **mécanisme clé** de l'économie.

**Sans grepai**, Claude Code lance des subagents `Explore` pour chercher dans le code :

```
Question → Task(subagent_type: Explore) → Le subagent fait :
   ├── Grep("diamond") → 47 fichiers
   ├── Read(fichier1) → pas le bon
   ├── Grep("hit test") → 12 fichiers
   ├── Read(fichier2) → intéressant
   └── ... (20-40 appels d'outils)
```

Chaque subagent a son propre contexte = nouveaux tokens à créer et facturer.

**Avec grepai**, Claude appelle directement la recherche sémantique :

```
Question → Bash: grepai search "..." → Résultats pertinents
   ├── Read(fichier pertinent) → confirmation
   └── Réponse
```

Pas de subagent = pas de nouveau contexte = économie.

### Les outils utilisés

| Outil | Baseline | grepai |
|-------|----------|--------|
| Bash (dont grepai) | 41 | 9 |
| Grep | 37 | 20 |
| Glob | 13 | 0 |
| Read | 43 | 30 |
| Task (subagents) | 5 | 0 |

**grepai** réduit drastiquement les appels `Glob`. Cet outil permet de chercher des fichiers par pattern (ex: `**/*.ts`, `**/diamond*`), mais retourne souvent des dizaines de résultats que Claude doit ensuite filtrer en lisant chaque fichier. Avec la recherche sémantique, Claude obtient directement les fichiers pertinents sans cette phase d'exploration.

---

## Le script d'analyse

J'ai développé un script Python pour extraire les métriques directement des logs Claude Code.

### Où sont les logs ?

```
~/.claude/
├── projects/
│   └── <projet-hash>/
│       ├── <session-uuid>.jsonl      # Log principal
│       └── <session-uuid>/
│           └── subagents/            # Logs des subagents
│               ├── agent-xxx.jsonl
│               └── agent-yyy.jsonl
```

**Point critique** : les subagents ont leurs propres logs. Le script les inclut automatiquement dans l'analyse.

### Le script complet

<details>
<summary>Voir le script Python (benchmark.py)</summary>

```python
#!/usr/bin/env python3
"""
Grepai Benchmark - Claude Code Session Analyzer

Usage:
    python3 benchmark.py <UUID_BASELINE> <UUID_GREPAI>

Analyzes billed tokens from the Anthropic API and calculates cost.
Automatically includes subagents.
"""

from __future__ import annotations
import json
import glob
import os
import sys
from pathlib import Path
from typing import Optional

# Claude Opus 4.5 pricing ($/million tokens) - January 2025
# Source: https://www.anthropic.com/pricing
PRICING = {
    "input_tokens": 5.0,                  # $5/M
    "cache_read_input_tokens": 0.5,       # $0.50/M (0.1× input)
    "cache_creation_input_tokens": 6.25,  # $6.25/M (1.25× input)
    "output_tokens": 25.0,                # $25/M
}

def find_session_path(uuid: str) -> str | None:
    """Find the JSONL file for a session by its UUID."""
    claude_dir = Path.home() / ".claude"
    for jsonl in claude_dir.rglob(f"*{uuid}*.jsonl"):
        if "subagents" not in str(jsonl):
            return str(jsonl)
    return None

def collect_session_files(main_jsonl: str) -> list[str]:
    """Collect main log file + all subagent logs."""
    files = [main_jsonl]
    session_dir = os.path.dirname(main_jsonl)
    uuid = os.path.basename(main_jsonl).replace('.jsonl', '')
    subagent_dir = os.path.join(session_dir, uuid, "subagents")

    if os.path.exists(subagent_dir):
        files.extend(glob.glob(os.path.join(subagent_dir, "*.jsonl")))

    return files

def analyze_session(uuid: str) -> dict | None:
    """Full analysis of a session (tokens + tools)."""
    main_path = find_session_path(uuid)
    if not main_path:
        print(f"  Session not found: {uuid}")
        return None

    files = collect_session_files(main_path)

    data = {
        "uuid": uuid,
        "files": len(files),
        "subagents": len(files) - 1,
        "api_calls": 0,
        "tool_calls": 0,
        "tokens": {
            "input_tokens": 0,
            "cache_read_input_tokens": 0,
            "cache_creation_input_tokens": 0,
            "output_tokens": 0,
        },
        "tools": {},
    }

    for filepath in files:
        with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
            for line in f:
                if not line.strip():
                    continue
                try:
                    entry = json.loads(line)
                    msg = entry.get('message', {})

                    usage = msg.get('usage')
                    if usage:
                        data["api_calls"] += 1
                        for key in data["tokens"]:
                            data["tokens"][key] += usage.get(key, 0)

                    content = msg.get('content', [])
                    if isinstance(content, list):
                        for c in content:
                            if isinstance(c, dict) and c.get('type') == 'tool_use':
                                data["tool_calls"] += 1
                                tool = c.get('name', 'unknown')
                                data["tools"][tool] = data["tools"].get(tool, 0) + 1

                except json.JSONDecodeError:
                    pass

    return data

def calc_cost(tokens: dict) -> float:
    """Calculate cost in dollars."""
    return sum(tokens[k] * PRICING[k] / 1_000_000 for k in PRICING)

def print_comparison(baseline: dict, grepai: dict):
    """Display comparison of two sessions."""

    print("\n" + "=" * 75)
    print("GREPAI BENCHMARK - RESULTS")
    print("=" * 75)

    print(f"\n{'Metric':<35} {'Baseline':>15} {'Grepai':>15} {'Δ':>10}")
    print("-" * 75)
    print(f"{'Subagents':<35} {baseline['subagents']:>15} {grepai['subagents']:>15}")
    print(f"{'API Calls':<35} {baseline['api_calls']:>15} {grepai['api_calls']:>15}")

    delta_tools = (grepai['tool_calls'] - baseline['tool_calls']) / baseline['tool_calls'] * 100
    print(f"{'Tool Calls':<35} {baseline['tool_calls']:>15} {grepai['tool_calls']:>15} {delta_tools:>+9.0f}%")

    print("-" * 75)
    for key in PRICING:
        b, g = baseline['tokens'][key], grepai['tokens'][key]
        delta = (g - b) / b * 100 if b > 0 else 0
        print(f"{key:<35} {b:>15,} {g:>15,} {delta:>+9.0f}%")

    total_b = sum(baseline['tokens'].values())
    total_g = sum(grepai['tokens'].values())
    delta_total = (total_g - total_b) / total_b * 100
    print("-" * 75)
    print(f"{'TOTAL TOKENS':<35} {total_b:>15,} {total_g:>15,} {delta_total:>+9.0f}%")

    print("\n" + "=" * 75)
    print("BILLED COST (Claude Opus 4.5 pricing)")
    print("=" * 75)
    cost_b = calc_cost(baseline['tokens'])
    cost_g = calc_cost(grepai['tokens'])
    delta_cost = (cost_g - cost_b) / cost_b * 100

    print(f"\n{'Baseline':<35} ${cost_b:>14.4f}")
    print(f"{'Grepai':<35} ${cost_g:>14.4f}")
    print("-" * 55)
    print(f"{'SAVINGS':<35} {delta_cost:>+14.1f}%")

def main():
    if len(sys.argv) < 3:
        print(__doc__)
        sys.exit(1)

    uuid_baseline = sys.argv[1]
    uuid_grepai = sys.argv[2]

    print("Analyzing sessions...")
    baseline = analyze_session(uuid_baseline)
    grepai = analyze_session(uuid_grepai)

    if not baseline or not grepai:
        print("\nUnable to analyze sessions.")
        sys.exit(1)

    print_comparison(baseline, grepai)

if __name__ == "__main__":
    main()
```

</details>

### Usage

```bash
python3 benchmark.py <UUID_BASELINE> <UUID_GREPAI>
```

Les UUIDs se trouvent avec `ls ~/.claude/projects/*/` ou dans le prompt Claude Code.

---

## Limites et honnêteté

### Variabilité du comportement de Claude

Claude Code peut emprunter des chemins différents pour la même question. Parfois il lance des subagents, parfois non. Dans ce benchmark :
- **Baseline** : Claude a lancé 5 subagents
- **grepai** : Claude n'a lancé aucun subagent

J'ai répété l'expérience 5 fois avec des résultats quasi identiques. Le comportement est reproductible : sans **grepai**, Claude lance systématiquement des subagents pour explorer le code. Avec **grepai**, il obtient les réponses directement et n'en a plus besoin.

### Ce que ce benchmark mesure

- ✅ Les tokens **réellement facturés** par l'API
- ✅ Le **coût financier** exact
- ✅ Le nombre d'appels d'outils

### Ce que ce benchmark ne mesure pas

- La **qualité** des réponses (les deux approches trouvent les bonnes réponses)
- Le **temps réel** (variable selon la charge serveur)
- La **reproductibilité** (le comportement de Claude varie)

---

## Conclusion

Sur ce benchmark avec Excalidraw :

| Métrique | Réduction |
|----------|-----------|
| Appels d'outils | **-55%** |
| Input tokens (fresh) | **-97%** |
| Cache creation | **-71%** |
| **Coût facturé** | **-27,5%** (-$1.86) |

Le mécanisme principal : **la recherche sémantique trouve les bons fichiers du premier coup**. Au lieu de tâtonner avec des patterns regex qui retournent des dizaines de résultats à filtrer, **grepai** comprend l'intention de la question et pointe directement vers le code pertinent. Résultat : moins d'itérations, moins de lectures inutiles, et Claude n'a pas besoin de déléguer l'exploration à des subagents coûteux.

Ces résultats sont cohérents avec les benchmarks externes :
- [Morph](https://morphllm.com/benchmarks) : 26% de tours en moins, 39% de tokens en moins
- [mgrep](https://github.com/mixedbread-ai/mgrep) : 2× moins de tokens
- [Claude Context (Zilliz)](https://github.com/zilliztech/claude-context) : 40% de réduction

---

## Sources et références

### Documentation technique
- [Anthropic Pricing](https://www.anthropic.com/pricing) : Tarifs officiels de l'API Claude
- [Claude Prompt Caching](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching) : Fonctionnement du cache
- [grepai Documentation](https://yoanbernabeu.github.io/grepai/) : Guide complet de l'outil
- [Excalidraw](https://github.com/excalidraw/excalidraw) : Projet utilisé pour le benchmark (MIT License)

### Benchmarks externes
- [Morph Benchmarks](https://morphllm.com/benchmarks) : Recherche sémantique vs grep
- [mgrep par Mixedbread AI](https://github.com/mixedbread-ai/mgrep) : Alternative sémantique à grep
- [Claude Context par Zilliz](https://github.com/zilliztech/claude-context) : Réduction de contexte pour agents

### Articles liés
- [La recherche sémantique au service des agents IA](/grepai-agent-ia-recherche-semantique) : Article précédent sur **grepai**

---

**Envie de tester ?** [grepai est open source](https://github.com/yoanbernabeu/grepai) et s'installe en quelques minutes. La [documentation](https://yoanbernabeu.github.io/grepai/) inclut un guide de démarrage rapide pour Claude Code.
