Contabilidad de costos
Una cifra de costo solo vale si puedes decir de dónde salió. Aquí está cada paso entre un recuento de tokens y un número en la pantalla.
Se tarifa una vez, en la ingesta
Cuando llega un span con un id de modelo y un recuento de tokens, mcpd busca ese modelo en un libro de precios, calcula lo que costó la llamada y guarda el resultado en el span. Nada se vuelve a calcular al leer.
Esa es la decisión importante de esta página. Los precios cambian. Si el costo se dedujera en el momento de la consulta a partir del libro que estuviera cargado ese día, una bajada de tarifas el mes que viene reescribiría en silencio lo que costó el mes pasado, y el número que le reportaste a alguien ya no estaría cuando fuera a comprobarlo. Tarifar en la ingesta congela la cifra contra las tarifas que eran ciertas cuando se hizo la llamada. Cada costo almacenado anota además el id del libro que lo produjo, así que una cifra se puede rastrear hasta las tarifas que hay detrás.
Un span que ya viene con un costo se deja exactamente como llegó. Si tu
proveedor te da una cifra real y la registras, mcpd conserva la tuya y no
tarifa por encima. El flag is_estimated separa las dos: todo
lo que mcpd dedujo de un libro se marca como estimado, porque lo es.
Las tarifas son por millón de tokens
Toda tarifa de un libro de precios está en unidades de moneda por millón de
tokens, que es como las publican todos los proveedores. Una tarifa copiada
de una página de precios entra en un libro sin cambios, sin ninguna división
que equivocar a las tres de la mañana. La aritmética de un medidor es
tokens × rate ÷ 1,000,000.
El costo vive en el paso, no en la ejecución
Cada llamada al modelo lleva su propia cifra. El total de una ejecución es la suma de sus spans, y mcpd recalcula esa suma a partir de los spans cada vez que llega uno, en lugar de incrementar un contador. Los spans llegan desordenados, se reintentan y se actualizan en el sitio, y un total que se incrementa se desvía con las tres cosas. Un número de cabecera que no cuadra con las filas de debajo destruye la confianza en todos los demás números de la página.
- 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.
Las tres llamadas al modelo de arriba están tarifadas en $0.00015, $0.00018 y $0.00021. El total de la ejecución en la cabecera es su suma y nada más: las llamadas a herramientas, la consulta a memoria y el guardrail no consumieron tokens, así que no llevan costo y no aportan ninguno.
Los medidores
Una llamada al modelo no es un solo número de tokens. Los proveedores facturan varios medidores a tarifas distintas, y un libro de precios da una tarifa para cada uno.
| Tarifa | Se aplica a | Cuando el libro la omite |
|---|---|---|
input | Tokens de entrada normales. | Obligatoria. |
output | Tokens de respuesta normales. | Obligatoria. |
cache_read | Tokens servidos desde una caché de prompts. | Se cobran a input. |
cache_write | Tokens escritos en una caché de prompts. | Se cobran a input. |
cache_write_5m | Escrituras en una caché de cinco minutos, donde el proveedor tarifa aparte cada TTL. | Recae en cache_write, y luego en input. |
cache_write_1h | Escrituras en una caché de una hora. | Recae en cache_write, y luego en input. |
audio_input | Tokens de audio de entrada. | Se cobran a input. |
audio_output | Tokens de audio de salida. | Se cobran a output. |
reasoning | Tokens de razonamiento oculto, donde un proveedor los factura aparte. |
Sin reserva. Se cobran a output como parte del total de
salida, que es como los facturan los grandes proveedores, salvo que el
proveedor haya reportado además un recuento text aparte.
Mira más abajo.
|
Un token en caché se cobra una vez
La forma clásica de inflar una factura es cobrar dos veces un token en caché: una a la tarifa de caché, y otra como parte del total de entrada reportado dentro del cual ya está. Así que cuando un proveedor reporta un desglose, ese desglose manda. mcpd tarifa cada medidor a su propia tarifa y cobra solo el resto a la tarifa de entrada llana.
Una llamada a claude-sonnet-4 que reporta un millón de tokens
de entrada, de los cuales novecientos mil se leyeron de la caché, se tarifa
contra el libro integrado así:
- 900,000 lecturas de caché a $0.30 por millón, que son $0.27.
- 100,000 tokens de entrada restantes a $3.00 por millón, que son $0.30.
- $0.57 de costo de entrada, en lugar de los $3.00 que habría producido el total reportado por su cuenta.
Donde un proveedor da un recuento text explícito, ese recuento
se usa como resto directamente. Donde no lo da, el resto es el total
reportado menos todos los campos medidos.
Eso deja un hueco que conviene nombrar. Un medidor para el que el libro no
tiene tarifa se cobra solo como parte del resto, así que cuando el proveedor
envió además un recuento text no se cobra en absoluto. Los
tokens de razonamiento caen ahí siempre que tu libro no les dé una tarifa
propia, y declarar una lo arregla. Los tokens de imagen caen ahí siempre,
porque un libro no tiene ninguna tarifa de imagen que dar, así que en una
carga muy multimodal lee la cifra como baja y no como exacta.
Un modelo desconocido no tiene costo, no un costo de cero
Si el libro no tiene tarifas para el modelo, mcpd guarda el span sin ningún campo de costo. No guarda un cero.
Cero es una afirmación: esta llamada fue gratis. Ausente es una admisión: no sabemos qué costó esta llamada. Llevan a decisiones distintas, y una herramienta que reporta la segunda como la primera está mintiendo sobre una factura. La misma regla recorre la ruta de lectura, donde una ejecución cuyos spans no suman nada se devuelve sin cifra de costo en lugar de con un costo de cero, así que una columna de costo en blanco significa sin tarifar y nunca gratis.
La consecuencia práctica conviene interiorizarla: un modelo que falta en el
libro falta en todos los totales. Su gasto no aparece en
/v1/api/analytics/cost, y sus ejecuciones quedan excluidas por
cualquier filtro min_cost positivo. Un total semanal que se ve
demasiado bajo suele ser un id de modelo que no resuelve, no una semana
tranquila.
Resolver un id de modelo
El id de un span es el que envió tu cliente, y los clientes lo decoran. Una pasarela le antepone su proveedor, una nube le antepone su región, un proveedor le fija una fecha de publicación. Un libro se indexa por nombres de modelo desnudos, así que la búsqueda prueba cuatro cosas en orden y para en el primer acierto.
| Paso | Ejemplo |
|---|---|
| El id tal cual, normalizado a minúsculas | GPT-4o encuentra gpt-4o |
| La parte posterior a la última barra | openai/gpt-4o encuentra gpt-4o |
| La cola con puntos de un id de nube | us.anthropic.claude-sonnet-4 encuentra
claude-sonnet-4 |
| El id sin la fecha final, con su prefijo o sin él | openai/gpt-4o-2024-08-06 encuentra gpt-4o |
Se reconocen los sufijos -YYYY-MM-DD y
-YYYYMMDD. Fallar los cuatro pasos es un desenlace normal y no
un error: significa sin costo, como arriba.
El libro que se incluye
mcpd lleva un pequeño libro integrado para que docker run
produzca cifras reales sin ninguna configuración. Se llama
builtin, tarifa en USD, y todos los costos que produce anotan
ese nombre.
| Modelo | Input | Output | Lectura de caché | Escritura en caché, 5 min | Escritura en caché, 1 hora |
|---|---|---|---|---|---|
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 | a input | a input |
gpt-4o-mini | 0.15 | 0.60 | 0.075 | a input | a input |
gpt-4.1 | 2.00 | 8.00 | 0.50 | a input | a input |
gpt-4.1-mini | 0.40 | 1.60 | 0.10 | a input | a input |
o3 | 2.00 | 8.00 | 0.50 | a input | a input |
o4-mini | 1.10 | 4.40 | 0.275 | a input | a input |
gemini-2.5-pro | 1.25 | 10.00 | 0.31 | a input | a input |
gemini-2.5-flash | 0.30 | 2.50 | 0.075 | a input | a input |
text-embedding-3-small | 0.02 | 0.00 | a input | a input | a input |
text-embedding-3-large | 0.13 | 0.00 | a input | a input | a input |
USD por millón de tokens, tal como estaba el libro la última vez que se
contrastó con un catálogo de modelos de terceros, fecha que el libro anota
como su as_of. Las entradas de Anthropic llevan las dos tarifas
de escritura en caché porque el daemon recae de la tarifa de una hora en la
de cinco minutos, y un libro que omitiera la larga cobraría de menos en
silencio. OpenAI no cobra nada por escribir en su caché, así que allí una
escritura en caché se factura a la tarifa de entrada.
gemini-2.5-flash lleva además una tarifa de audio de entrada de
1.00; el resto de las entradas tarifan el audio a su tarifa de texto. No
existe claude-haiku-4: el id que sirve Anthropic es
claude-haiku-4-5, y un id que no resuelve a nada se queda sin
tarifar en lugar de adivinado.
Estos son precios de lista publicados, no tus precios. Si tienes tarifas negociadas, o ejecutas un modelo que no está en esta tabla, las cifras que te enseña mcpd son una estimación y hay que leerlas como tal. El libro es corto a propósito: uno largo se quedaría desactualizado en más sitios y daría una impresión de exactitud mayor de la que merece.
Aportar tu propio libro todavía no tiene flag de línea de comandos. El
libro es parte del estado del servidor, y hoy la forma de reemplazarlo es
incrustar mcpd como biblioteca y pasarle uno a
AppState::with_price_book. Un libro es un id, una moneda ISO
4217 y un mapa de id de modelo a tarifas, y cada cifra que produce lleva
estampado su id para poder distinguir dos libros después. Los precios de un
proyecto deberían compartir una moneda; mcpd suma los spans de una ejecución
aritméticamente y no convierte entre monedas.
Leer el dinero de vuelta
GET /v1/api/analytics/cost responde adónde fue el dinero
durante una ventana, sin exportar nada a una hoja de cálculo.
curl -s 'http://localhost:4317/v1/api/analytics/cost?from=-7d&group_by=model' | Parámetro | Significado |
|---|---|
from, to | La ventana. RFC 3339 o la forma abreviada relativa. Por defecto, los últimos siete días, terminando ahora. |
group_by | model por defecto. También
provider, entity_name,
environment, status,
session_id, user_id,
service_name o name.
|
bucket | 30s, 5m, 1h o 1d.
Omítelo para tener solo totales; dalo para obtener además una serie
temporal.
|
project | Qué proyecto leer. Si lo omites, el proyecto por defecto del daemon. |
adónde fue
# Where the money went last week, by model.
curl -s --get http://localhost:4317/v1/api/analytics/cost \
--data-urlencode 'from=-7d' \
--data-urlencode 'group_by=model'
# The same week as a daily series, so you can see the day it changed.
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) Los grupos vuelven de mayor a menor, con un tope de cien filas, y una serie por intervalos tiene un tope de cinco mil puntos.
respuesta
{
"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 }
}
]
}
Una sutileza en count. Agrupar por model,
provider o entity_name lee los spans, así que
count es un número de llamadas al modelo. Agrupar por cualquier
otra cosa lee la proyección de la ejecución, así que count es
un número de ejecuciones. Los dos sirven; no son el mismo número, y
compararlos entre sí es el error más fácil de cometer aquí.
La latencia por grupo de esta respuesta lleva solo una media y un máximo.
Los percentiles vienen de
GET /v1/api/analytics/summary, que los calcula sobre la
ventana.
Adónde ir después
El costo es una columna de una lista de ejecuciones, y la lista de
ejecuciones es donde empieza de verdad una investigación:
Encontrar una ejecución
cubre min_cost y la ordenación por gasto. Si tus cifras se ven
mal porque los payloads se están descartando antes de tarifarlos, revisa
Muestreo, que
decide qué se registra siquiera. Todos los campos nombrados en esta página
están listados en el
modelo de datos.