Volver al blog

API y SDKGuía

Rastrea el costo de tokens de un agente de IA en Python: usage, presupuesto, parada

Registra el campo usage de cada solicitud, súmalo por tarea y detén el agente en un presupuesto en dólares. Ejemplo funcional en Python para una API compatible con OpenAI.

Por CheapTokens TeamPublicado el 7 min de lectura

Tarjeta de código estilo terminal: un registro de usage por paso y una verificación de presupuesto que detiene a un agente de IA
En esta página
  1. Dónde están los números: el objeto usage
  2. Un medidor que detiene al agente
  3. A cuánto suma una ejecución de 40 pasos
  4. Cuando el modelo está pensando
  5. Streaming: el usage viene al final
  6. Si los números parecen incorrectos

Tu agente se ejecutó durante diez minutos y la factura es mayor que la tarea. Para ver por qué, lee el objeto usage en cada respuesta de la API, súmalo por tarea y multiplícalo por el precio del modelo por 1M de tokens. Para limitarlo, compara ese total acumulado con un presupuesto después de cada paso y detén el bucle cuando se supere. A continuación se muestra una pequeña clase de Python que hace exactamente esto.

El ejemplo usa el endpoint compatible con OpenAI https://api.cheaptokens.dev/v1 y el id de modelo deepseek-ai/DeepSeek-V4-Flash-0731 (contexto de 380K, hasta 32,768 tokens de salida por respuesta). Si quieres estimar una ejecución antes de iniciarla, eso es otro trabajo, y Cómo estimar el costo de un agente de IA lo cubre. Este artículo trata sobre medir y limitar mientras la ejecución está en curso.

Dónde están los números: el objeto usage#

Cada finalización de chat no transmitida en streaming regresa con un objeto usage. La referencia de la API de DeepSeek (consultada el 26 de octubre de 2026) lista estos campos:

  • prompt_tokens — todo lo que enviaste esta vez: prompt del sistema, historial, resultados de herramientas.
  • completion_tokens — lo que el modelo escribió de vuelta.
  • total_tokens — los dos sumados.

DeepSeek también documenta extras opcionales: prompt_cache_hit_tokens y prompt_cache_miss_tokens (suman prompt_tokens), y completion_tokens_details.reasoning_tokens para modelos de pensamiento. La página no muestra fecha de publicación, y estos extras son extensiones del formato de OpenAI, por lo que el código a continuación los trata como opcionales y se basa solo en los dos primeros campos.

¿Por qué registrar cada paso y no solo el último? Porque un agente reenvía su contexto creciente cada vez. El paso 1 podría enviar 2,000 tokens de entrada, el paso 30 podría enviar 9,000, y la factura es la suma de todos ellos. La razón se explica en por qué un chat largo cuesta más, y la división entrada/salida en tokens de entrada vs salida.

Un medidor que detiene al agente#

Aquí está todo: un medidor que suma tokens, imprime una línea por paso y lanza un error cuando se supera el presupuesto, más un bucle de agente básico a su alrededor. Instala el SDK con pip install openai y pon tu clave en CHEAPTOKENS_API_KEY.

python
import os
from openai import OpenAI

client = OpenAI(
    base_url='https://api.cheaptokens.dev/v1',
    api_key=os.environ['CHEAPTOKENS_API_KEY'],
)
MODEL = 'deepseek-ai/DeepSeek-V4-Flash-0731'

# example prices, USD per 1M tokens: use the current ones from the pricing table
PRICE_IN = 0.10
PRICE_OUT = 0.40


class BudgetExceeded(Exception):
    pass


class TaskMeter:
    def __init__(self, budget_usd):
        self.budget = budget_usd
        self.steps = 0
        self.tokens_in = 0
        self.tokens_out = 0
        self.reasoning = 0

    @property
    def cost(self):
        return (self.tokens_in * PRICE_IN + self.tokens_out * PRICE_OUT) / 1_000_000

    def add(self, usage):
        self.steps += 1
        self.tokens_in += usage.prompt_tokens
        self.tokens_out += usage.completion_tokens
        details = getattr(usage, 'completion_tokens_details', None)
        self.reasoning += getattr(details, 'reasoning_tokens', 0) or 0
        print(f'step {self.steps}: in={usage.prompt_tokens} '
              f'out={usage.completion_tokens} task=${self.cost:.4f}')
        if self.cost > self.budget:
            raise BudgetExceeded(
                f'budget ${self.budget:.2f} crossed at step {self.steps}: '
                f'${self.cost:.4f}')


def run_task(goal, budget_usd=0.50, max_steps=40):
    meter = TaskMeter(budget_usd)
    messages = [
        {'role': 'system', 'content': 'Work step by step. Reply DONE when finished.'},
        {'role': 'user', 'content': goal},
    ]
    for _ in range(max_steps):
        resp = client.chat.completions.create(
            model=MODEL, messages=messages, max_tokens=1024)
        meter.add(resp.usage)  # log first, then decide
        reply = resp.choices[0].message.content or ''
        messages.append({'role': 'assistant', 'content': reply})
        if 'DONE' in reply:
            break
        messages.append({'role': 'user', 'content': 'Continue.'})
    return meter


try:
    meter = run_task('Summarise the main risks of a database migration.')
    print(f'finished: {meter.steps} steps, ${meter.cost:.4f}')
except BudgetExceeded as err:
    print('stopped:', err)

Tres decisiones ahí vale la pena copiar.

Registra antes de decidir. meter.add registra el paso primero y solo entonces verifica el presupuesto, por lo que una ejecución detenida aún deja un rastro completo. La verificación ocurre después de la llamada que cruzó la línea, por lo que puedes excederte como máximo en un paso. max_tokens=1024 limita la salida de ese último paso.

Dos paradas duras, no una. max_steps atrapa a un agente que hace bucles sin gastar mucho; el presupuesto en dólares atrapa a uno que gasta mucho por paso. Quieres ambos.

Los precios viven en un solo lugar. PRICE_IN y PRICE_OUT son números de marcador de posición. Copia los valores actuales para tu modelo desde la página de precios; ahora mismo DeepSeek-V4-Flash es $0.015 por 1M de tokens de entrada y $0.10 por 1M de tokens de salida.

A cuánto suma una ejecución de 40 pasos#

Digamos que tu agente toma 40 pasos, y como el contexto crece, un paso promedio envía 6,000 tokens de entrada y recibe 400 tokens de vuelta. Eso es 40 × 6,000 = 240,000 tokens de entrada y 40 × 400 = 16,000 tokens de salida para la tarea.

El paso 1 envía 2,000 tokens de entrada, el paso 30 envía 9,000; el total de la tarea es la suma de todos los pasos
Ilustración con las suposiciones del artículo, no un resultado de ejecución

En DeepSeek-V4-Flash eso cuesta $0.0052. La tabla ejecuta la misma carga de trabajo en los otros modelos y pone el precio de referencia de OpenRouter junto al nuestro:

Modelo Costo OpenRouter
GLM-5.3-Flash $0.0054 $0.0111
DeepSeek-V4-Flash $0.0052 $0.0139
MiniMax-M2.7 $0.0144 $0.0912

240.000 tokens de entrada y 16.000 tokens de salida en total. Los precios por 1M de tokens son actuales y cambian junto con nuestras tarifas; la columna de OpenRouter muestra precios de referencia para la misma carga de trabajo.

En total, con entrada y salida contadas juntas, DeepSeek-V4-Flash resulta más barato que OpenRouter para esta tarea. Fíjate también dónde están los tokens: 240,000 de los 256,000 son de entrada. El contexto que tu agente arrastra es la factura, por lo que el número in= por paso en tu registro es el que hay que vigilar. Un presupuesto de $0.50 por tarea en el ejemplo anterior es un límite para una fuga, no un objetivo.

Cambiar de modelo solo significa cambiar MODEL. Elige el id de la documentación de CheapTokens: los nombres difieren entre proveedores, y un id corto de una etiqueta de precio no funcionará en la API. Para bucles de agente largos, DeepSeek-V4-Flash tiene un precio a cualquier hora del día, por lo que una tarea cuesta lo mismo al mediodía y a las 3 a.m. Consulta la página de DeepSeek para sus límites.

Cuando el modelo está pensando#

Si una respuesta es mucho más larga en tokens que el texto que ves, mira reasoning_tokens. Los tokens de pensamiento cuentan como salida, por lo que un paso con mucho razonamiento puede producir muchos más tokens de salida de lo que sugiere el texto visible, y se facturan como salida. El medidor anterior los recoge en meter.reasoning cuando la API devuelve el campo y lo deja en 0 cuando no lo hace, por lo que nunca falla por una clave faltante. Imprímelo junto a out= si quieres ver cuánto de la salida fue pensamiento. Esfuerzo de razonamiento y tokens de pensamiento explica por qué.

Streaming: el usage viene al final#

Si tu agente transmite en streaming, no hay usage en los fragmentos normales. En el formato de OpenAI lo pides con stream_options={'include_usage': True}. La referencia de DeepSeek (consultada el 26 de octubre de 2026) dice que la opción funciona solo junto con stream=True (de lo contrario la API responde 400), que usage es null en todos los fragmentos menos uno, y que el último fragmento antes de data: [DONE] lleva los totales de toda la solicitud. Así que en un bucle de streaming, llama a meter.add una vez, en el fragmento donde usage no esté vacío. El bucle no transmitido anterior es el lugar más simple para empezar; streaming en Python tiene el bucle en sí.

Si los números parecen incorrectos#

  • usage es None. Estás transmitiendo en streaming sin include_usage. Añádelo, o cambia a una llamada no transmitida.
  • El total es menor que la caída del saldo. Verifica que cada llamada pase por el medidor: los reintentos, una llamada de resumen o una llamada de selección de herramientas hecha fuera de run_task son fáciles de pasar por alto. Envuelve client.chat.completions.create en una función y enruta todo a través de ella.
  • El costo por paso sigue subiendo. Eso es normal para un bucle que añade todo a messages. Recorta la salida antigua de herramientas o resume el historial; salida de herramientas fuera de contexto muestra la idea.
  • BudgetExceeded se dispara en el paso 2. Tu presupuesto es menor que un paso. Verifica que PRICE_IN sea por 1M de tokens, no por 1K.

Para almacenar los números, escribe meter.steps, meter.tokens_in, meter.tokens_out y meter.cost como una línea JSON por tarea. Una semana de esas líneas te dice los tokens reales por paso para tu agente, que es el número que debes poner en cada estimación posterior. Los modelos, precios y la primera solicitud están en la documentación; las llamadas a herramientas se cubren en llamadas a funciones en Python.