Comptabilité des coûts
Un chiffre de coût ne vaut que si vous pouvez dire d’où il vient. Voici chaque étape entre un compte de tokens et un nombre à l’écran.
Tarifé une fois, à l’ingestion
Quand un span arrive avec un identifiant de modèle et un compte de tokens, mcpd cherche ce modèle dans une grille tarifaire, calcule ce que l’appel a coûté et stocke le résultat sur le span. Rien n’est recalculé à la lecture.
C’est la décision importante de cette page. Les prix changent. Si le coût était dérivé au moment de la requête depuis la grille chargée ce jour-là, une baisse de tarif le mois prochain réécrirait en silence ce que le mois dernier a coûté, et le nombre que vous avez rapporté à quelqu’un ne serait plus là quand cette personne irait le vérifier. Tarifer à l’ingestion fige le chiffre sur les tarifs qui étaient vrais au moment de l’appel. Chaque coût stocké enregistre aussi l’identifiant de la grille qui l’a produit : un chiffre peut donc être remonté jusqu’aux tarifs qui sont derrière.
Un span qui porte déjà un coût est laissé exactement tel qu’il est arrivé.
Si votre fournisseur vous donne un chiffre réel et que vous l’enregistrez,
mcpd garde le vôtre et ne tarife pas par-dessus. L’indicateur
is_estimated sépare les deux : tout ce que mcpd a dérivé d’une
grille est marqué comme estimé, parce qu’il l’est.
Les tarifs sont au million de tokens
Chaque tarif d’une grille est en unités monétaires par million de tokens,
ce qui est la façon dont tous les fournisseurs les publient. Un tarif copié
depuis une page de prix entre dans une grille sans changement, sans division
à rater à trois heures du matin. Le calcul pour un compteur est
tokens × rate ÷ 1,000,000.
Le coût vit sur l’étape, pas sur l’exécution
Chaque appel de modèle porte son propre chiffre. Le total d’une exécution est la somme de ses spans, et mcpd recalcule cette somme depuis les spans chaque fois que l’un d’eux arrive, plutôt que d’incrémenter un compteur. Les spans arrivent dans le désordre, sont réessayés et sont mis à jour sur place, et un total incrémenté dérive sous ces trois effets. Un nombre en tête qui ne correspond pas aux lignes en dessous détruit la confiance dans tous les autres nombres de la page.
- agent support_agent fail
- model chat
- tool returns_policy
- model chat
- tool returns_policy
- model chat
- tool returns_policy
The model called one tool three times and never answered. That is visible in four seconds here, and invisible in a log file.
Les trois appels de modèle ci-dessus sont tarifés à $0.00015, $0.00018 et $0.00021. Le total de l’exécution dans l’en-tête est leur somme et rien d’autre : les appels d’outils, la consultation de mémoire et le garde-fou n’ont consommé aucun token, donc ils ne portent aucun coût et n’en apportent aucun.
Les compteurs
Un appel de modèle n’est pas un seul nombre de tokens. Les fournisseurs facturent plusieurs compteurs à des tarifs différents, et une grille donne un tarif pour chacun.
| Tarif | S’applique à | Quand la grille l’omet |
|---|---|---|
input | Les tokens d’entrée ordinaires. | Obligatoire. |
output | Les tokens de complétion ordinaires. | Obligatoire. |
cache_read | Les tokens servis depuis un cache de prompt. | Facturés au tarif input. |
cache_write | Les tokens écrits dans un cache de prompt. | Facturés au tarif input. |
cache_write_5m | Les écritures dans un cache de cinq minutes, quand le fournisseur tarife les TTL à part. | Se replie sur cache_write, puis sur input. |
cache_write_1h | Les écritures dans un cache d’une heure. | Se replie sur cache_write, puis sur input. |
audio_input | Les tokens audio en entrée. | Facturés au tarif input. |
audio_output | Les tokens audio en sortie. | Facturés au tarif output. |
reasoning | Les tokens de raisonnement cachés, quand un fournisseur les facture à part. |
Pas de repli. Ils sont facturés au tarif output dans le
total de sortie, ce qui est la façon dont les grands fournisseurs les
facturent, sauf si le fournisseur a aussi rapporté un compte
text séparé. Voir plus bas.
|
Un token en cache est facturé une fois
La façon classique de gonfler une facture est de facturer un token en cache deux fois : une fois au tarif du cache, et encore une fois dans le total d’entrée rapporté où il se trouve déjà. Alors, quand un fournisseur rapporte une ventilation, c’est cette ventilation qui l’emporte. mcpd tarife chaque compteur à son propre tarif et ne facture que le reste au tarif d’entrée simple.
Un appel claude-sonnet-4 qui rapporte un million de tokens
d’entrée, dont neuf cent mille lus depuis le cache, se tarife ainsi contre
la grille intégrée :
- 900 000 lectures de cache à $0.30 le million, soit $0.27.
- 100 000 tokens d’entrée restants à $3.00 le million, soit $0.30.
- $0.57 de coût d’entrée, au lieu des $3.00 qu’aurait produits le total rapporté à lui seul.
Là où un fournisseur donne un compte text explicite, ce compte
sert directement de reste. Là où il n’en donne pas, le reste est le total
rapporté moins chaque champ mesuré.
Reste un trou qui mérite d’être nommé. Un compteur pour lequel la grille n’a
aucun tarif n’est facturé que dans le reste : quand le fournisseur a aussi
envoyé un compte text, il n’est donc pas facturé du tout. Les
tokens de raisonnement sont dans ce cas dès que votre grille ne leur donne
pas leur propre tarif, et en déclarer un le corrige. Les tokens d’image y
sont toujours, parce qu’une grille n’a pas de tarif d’image à donner : sur
une charge fortement multimodale, lisez le chiffre comme bas plutôt que
comme exact.
Un modèle inconnu n’a pas de coût, pas un coût de zéro
Si la grille ne détient aucun tarif pour le modèle, mcpd stocke le span sans aucun champ de coût. Il ne stocke pas un zéro.
Zéro est une affirmation : cet appel était gratuit. Absent est un aveu : nous ne savons pas ce que cet appel a coûté. Les deux mènent à des décisions différentes, et un outil qui rapporte le second comme le premier ment sur une facture. La même règle court dans le chemin de lecture, où une exécution dont les spans ne totalisent rien est renvoyée sans chiffre de coût plutôt qu’avec un coût de zéro : une colonne de coût vide veut donc dire non tarifé et jamais gratuit.
La conséquence pratique mérite d’être intégrée : un modèle absent de la
grille est absent de tous les totaux. Sa dépense n’apparaît pas dans
/v1/api/analytics/cost, et ses exécutions sont exclues par tout
filtre min_cost positif. Un total hebdomadaire qui semble trop
bas est en général un identifiant de modèle qui ne se résout pas, pas une
semaine calme.
Résoudre un identifiant de modèle
L’identifiant sur un span est celui que votre client a envoyé, et les clients le décorent. Une passerelle préfixe son éditeur, un cloud préfixe sa région, un fournisseur y épingle une date de sortie. Une grille est indexée sur des noms de modèles nus, donc la recherche essaie quatre choses dans l’ordre et s’arrête à la première qui aboutit.
| Étape | Exemple |
|---|---|
| L’identifiant tel quel, en minuscules | GPT-4o trouve gpt-4o |
| La partie après la dernière barre oblique | openai/gpt-4o trouve gpt-4o |
| La fin pointée d’un identifiant de cloud | us.anthropic.claude-sonnet-4 trouve
claude-sonnet-4 |
| L’identifiant sans la date finale, avec ou sans son préfixe | openai/gpt-4o-2024-08-06 trouve gpt-4o |
Les suffixes -YYYY-MM-DD et -YYYYMMDD sont tous
deux reconnus. Échouer sur les quatre étapes est une issue normale et non
une erreur : cela veut dire aucun coût, comme ci-dessus.
La grille livrée
mcpd embarque une petite grille intégrée pour qu’un docker run
produise de vrais chiffres sans aucune configuration. Elle s’appelle
builtin, elle tarife en USD, et chaque coût qu’elle produit
enregistre ce nom.
| Modèle | Entrée | Sortie | Lecture de cache | Écriture de cache, 5 min | Écriture de cache, 1 heure |
|---|---|---|---|---|---|
claude-opus-4 | 15.00 | 75.00 | 1.50 | 18.75 | 30.00 |
claude-opus-4-1 | 15.00 | 75.00 | 1.50 | 18.75 | 30.00 |
claude-opus-4-5 | 5.00 | 25.00 | 0.50 | 6.25 | 10.00 |
claude-sonnet-4 | 3.00 | 15.00 | 0.30 | 3.75 | 6.00 |
claude-sonnet-4-5 | 3.00 | 15.00 | 0.30 | 3.75 | 6.00 |
claude-haiku-4-5 | 1.00 | 5.00 | 0.10 | 1.25 | 2.00 |
claude-3-5-haiku | 0.80 | 4.00 | 0.08 | 1.00 | 1.60 |
gpt-4o | 2.50 | 10.00 | 1.25 | au tarif d’entrée | au tarif d’entrée |
gpt-4o-mini | 0.15 | 0.60 | 0.075 | au tarif d’entrée | au tarif d’entrée |
gpt-4.1 | 2.00 | 8.00 | 0.50 | au tarif d’entrée | au tarif d’entrée |
gpt-4.1-mini | 0.40 | 1.60 | 0.10 | au tarif d’entrée | au tarif d’entrée |
o3 | 2.00 | 8.00 | 0.50 | au tarif d’entrée | au tarif d’entrée |
o4-mini | 1.10 | 4.40 | 0.275 | au tarif d’entrée | au tarif d’entrée |
gemini-2.5-pro | 1.25 | 10.00 | 0.31 | au tarif d’entrée | au tarif d’entrée |
gemini-2.5-flash | 0.30 | 2.50 | 0.075 | au tarif d’entrée | au tarif d’entrée |
text-embedding-3-small | 0.02 | 0.00 | au tarif d’entrée | au tarif d’entrée | au tarif d’entrée |
text-embedding-3-large | 0.13 | 0.00 | au tarif d’entrée | au tarif d’entrée | au tarif d’entrée |
En USD par million de tokens, tels que la grille se présentait à sa dernière
vérification contre un catalogue de modèles tiers, date que la grille
enregistre sous as_of. Les entrées d’Anthropic portent les deux
tarifs d’écriture de cache parce que le daemon se replie du tarif d’une
heure sur celui de cinq minutes, et une grille qui aurait omis le plus long
sous-facturerait en silence. OpenAI ne facture rien pour écrire dans son
cache, donc une écriture de cache y est facturée au tarif d’entrée.
gemini-2.5-flash porte aussi un tarif d’entrée audio de 1.00 ;
toutes les autres entrées tarifent l’audio à leur tarif texte. Il n’y a pas
de claude-haiku-4 : l’identifiant qu’Anthropic sert est
claude-haiku-4-5, et un identifiant qui ne correspond à rien
est laissé sans prix plutôt que deviné.
Ce sont des prix publics, pas les vôtres. Si vous avez des tarifs négociés, ou si vous exploitez un modèle absent de ce tableau, les chiffres que mcpd vous montre sont une estimation et doivent se lire comme telle. La grille est délibérément courte : une longue vieillirait à plus d’endroits et donnerait une impression d’exactitude plus forte qu’elle ne le mérite.
Fournir votre propre grille n’a pas encore d’option en ligne de commande. La
grille fait partie de l’état du serveur, et la façon de la remplacer
aujourd’hui est d’embarquer mcpd comme bibliothèque et d’en passer une à
AppState::with_price_book. Une grille est un identifiant, une
monnaie ISO 4217 et une table qui associe un identifiant de modèle à des
tarifs, et chaque chiffre qu’elle produit est estampillé de son identifiant,
pour que deux grilles puissent être distinguées après coup. Les prix d’un
même projet devraient partager une seule monnaie : mcpd additionne les spans
d’une exécution arithmétiquement et ne convertit pas entre monnaies.
Relire la dépense
GET /v1/api/analytics/cost répond où est parti l’argent sur une
fenêtre, sans rien exporter vers un tableur.
curl -s 'http://localhost:4317/v1/api/analytics/cost?from=-7d&group_by=model' | Paramètre | Sens |
|---|---|
from, to | La fenêtre. RFC 3339 ou la notation relative abrégée. Vaut par défaut les sept derniers jours, jusqu’à maintenant. |
group_by | model par défaut. Aussi provider,
entity_name, environment,
status, session_id, user_id,
service_name ou name.
|
bucket | 30s, 5m, 1h ou 1d.
Omettez-le pour n’avoir que les totaux ; donnez-le pour obtenir une
série temporelle à côté.
|
project | Quel projet lire. Vaut par défaut le projet par défaut du daemon. |
où c’est parti
# Où est parti l’argent la semaine dernière, par modèle.
curl -s --get http://localhost:4317/v1/api/analytics/cost \
--data-urlencode 'from=-7d' \
--data-urlencode 'group_by=model'
# La même semaine en série quotidienne, pour voir le jour où cela a changé.
curl -s --get http://localhost:4317/v1/api/analytics/cost \
--data-urlencode 'from=-7d' \
--data-urlencode 'group_by=model' \
--data-urlencode 'bucket=1d' import { Mcpd } from '@mcpd/sdk'
const mcpd = new Mcpd()
const spend = await mcpd.analyticsCost({
from: '-7d',
to: '-0s',
groupBy: 'model',
bucket: '1d',
})
console.log(`${spend.currency} ${spend.totalCost.toFixed(4)} over the window`)
for (const group of spend.groups) {
console.log(group.key, group.totalCost, group.inputTokens, group.outputTokens)
} from mcpd import Mcpd
mcpd = Mcpd()
spend = mcpd.analytics_cost("-7d", "-0s", group_by="model", bucket="1d")
print(f"{spend.currency} {spend.total_cost:.4f} over the window")
for group in spend.groups:
print(group.key, group.total_cost, group.input_tokens, group.output_tokens) Les groupes reviennent du plus grand au plus petit, plafonnés à cent lignes, et une série découpée en intervalles est plafonnée à cinq mille points.
réponse
{
"from": "2026-08-20T09:00:00Z",
"to": "2026-08-27T09:00:00Z",
"currency": "USD",
"total_cost": 12.418264,
"groups": [
{
"key": "claude-sonnet-4",
"count": 1841,
"error_count": 12,
"total_cost": 9.612055,
"input_tokens": 2903118,
"output_tokens": 214477,
"latency": { "mean": 1840.2, "max": 24193.0 }
},
{
"key": "gpt-4o-mini",
"count": 6204,
"error_count": 3,
"total_cost": 2.806209,
"input_tokens": 9114882,
"output_tokens": 388310,
"latency": { "mean": 612.4, "max": 8801.0 }
}
]
}
Une subtilité dans count. Grouper par model,
provider ou entity_name lit les spans, donc
count est un nombre d’appels de modèle. Grouper par autre chose
lit la projection des exécutions, donc count est un nombre
d’exécutions. Les deux sont utiles ; ce n’est pas le même nombre, et
comparer l’un à l’autre est l’erreur la plus facile à commettre ici.
La latence par groupe dans cette réponse ne porte qu’une moyenne et un
maximum. Les centiles viennent de
GET /v1/api/analytics/summary, qui les calcule sur la fenêtre.
Où aller ensuite
Le coût est une colonne d’une liste d’exécutions, et cette liste est le
point de départ réel d’une enquête :
Retrouver une exécution
couvre min_cost et le tri par dépense. Si vos chiffres semblent
faux parce que des payloads sont jetés avant d’être tarifés, regardez
Échantillonnage,
qui décide de ce qui est enregistré. Chaque champ nommé sur cette page est
listé dans le
modèle de données.