>_ josetilabs.com
← blog

24 out 2026 · ia, custo, observabilidade · 4 min

Custo por consumidor: quanto cada serviço gasta de modelo

Sem saber quem gasta, o custo de modelo vira surpresa no fim do mês. Registrar cada chamada com consumidor, modelo e dia, e agrupar, mostra onde o dinheiro vai. Na prática, a maior parte do gasto costuma estar num único consumidor.

O problema

Um agente, um bot e um script de resumo compartilham a mesma chave de API. No fim do mês a fatura chega, mas ninguém sabe quem pediu o quê. A conta não tem rótulo, só total.

A saída é registrar cada chamada com três campos além do modelo: quem chamou, quantos tokens e quanto custou. Com isso, um script agrupa por consumidor, modelo e dia, e o ranking aparece.

O formato do registro

Um arquivo JSONL, uma linha por chamada ou por lote. Campos mínimos:

json
{"ts": "2026-10-20T09:12:00Z", "consumer": "bot-telegram", "model": "gpt-4o-mini", "calls": 1, "prompt_tokens": 45000, "completion_tokens": 3000, "cost_usd": 0.0090}

ts aceita ISO 8601 ou epoch. calls é opcional e vale 1 por padrão. cost_usd é opcional; se faltar, o script soma zero e você calcula depois com a tabela de preços do provedor.

O artefato

python
#!/usr/bin/env python3
"""Agrupa gasto de modelo por consumidor, modelo e dia.

Lê um arquivo JSONL com um registro por chamada (ou por lote) e imprime
o ranking de consumidores e o detalhe por dia e modelo.

Campos aceitos em cada linha:
  ts ou date        instante (ISO 8601 ou epoch)
  consumer          nome do serviço ou tarefa que fez a chamada
  model             nome do modelo
  calls             quantas chamadas (opcional, padrão 1)
  prompt_tokens     tokens de entrada
  completion_tokens tokens de saída
  cost_usd          custo da chamada em dólares (opcional)

Uso:
  python3 custo_por_consumidor.py uso.jsonl
  python3 custo_por_consumidor.py            # roda com exemplo embutido
"""
import json
import sys
from collections import defaultdict
from datetime import datetime, timezone


def para_data(valor):
    if isinstance(valor, (int, float)):
        dt = datetime.fromtimestamp(valor, tz=timezone.utc)
    else:
        s = str(valor).strip()
        try:
            dt = datetime.fromisoformat(s.replace("Z", "+00:00"))
        except ValueError:
            dt = datetime.strptime(s[:10], "%Y-%m-%d").replace(tzinfo=timezone.utc)
    return dt.date().isoformat()


def carregar(caminho):
    with open(caminho, encoding="utf-8") as f:
        for linha in f:
            linha = linha.strip()
            if not linha:
                continue
            yield json.loads(linha)


EXEMPLO = [
    {"ts": "2026-10-20T09:12:00Z", "consumer": "bot-telegram", "model": "gpt-4o-mini",
     "calls": 1, "prompt_tokens": 45000, "completion_tokens": 3000, "cost_usd": 0.0090},
    {"ts": "2026-10-20T10:00:00Z", "consumer": "resumo-diario", "model": "gpt-4o-mini",
     "calls": 1, "prompt_tokens": 12000, "completion_tokens": 800, "cost_usd": 0.0024},
    {"ts": "2026-10-21T08:00:00Z", "consumer": "resumo-diario", "model": "gpt-4o-mini",
     "calls": 1, "prompt_tokens": 11000, "completion_tokens": 700, "cost_usd": 0.0022},
    {"ts": "2026-10-21T11:30:00Z", "consumer": "transcricao", "model": "whisper-1",
     "calls": 1, "prompt_tokens": 0, "completion_tokens": 0, "cost_usd": 0.0120},
    {"ts": "2026-10-22T14:00:00Z", "consumer": "bot-telegram", "model": "gpt-4o-mini",
     "calls": 1, "prompt_tokens": 60000, "completion_tokens": 4000, "cost_usd": 0.0120},
]


def main():
    if len(sys.argv) > 1:
        registros = list(carregar(sys.argv[1]))
    else:
        registros = EXEMPLO

    grupos = defaultdict(lambda: {"chamadas": 0, "prompt": 0,
                                 "completion": 0, "custo": 0.0})
    for r in registros:
        chave = (r.get("consumer", "?"), r.get("model", "?"),
                 para_data(r.get("ts") or r.get("date")))
        g = grupos[chave]
        g["chamadas"] += int(r.get("calls", 1) or 1)
        g["prompt"] += int(r.get("prompt_tokens", 0) or 0)
        g["completion"] += int(r.get("completion_tokens", 0) or 0)
        g["custo"] += float(r.get("cost_usd", 0) or 0)

    por_consumidor = defaultdict(lambda: {"chamadas": 0, "prompt": 0,
                                         "completion": 0, "custo": 0.0})
    for (consumidor, modelo, dia), g in grupos.items():
        c = por_consumidor[consumidor]
        c["chamadas"] += g["chamadas"]
        c["prompt"] += g["prompt"]
        c["completion"] += g["completion"]
        c["custo"] += g["custo"]

    print("CONSUMO POR CONSUMIDOR (total da janela)")
    print(f"{'consumidor':20} {'chamadas':>9} {'prompt':>10} "
          f"{'completion':>11} {'custo_usd':>10}")
    for consumidor, c in sorted(por_consumidor.items(),
                               key=lambda x: -x[1]["custo"]):
        print(f"{consumidor:20} {c['chamadas']:9} {c['prompt']:10} "
              f"{c['completion']:11} {c['custo']:10.4f}")

    print("\nDETALHE POR DIA E MODELO")
    for (consumidor, modelo, dia), g in sorted(grupos.items()):
        print(f"{dia}  {consumidor:20} {modelo:14} chamadas={g['chamadas']:4} "
              f"prompt={g['prompt']:8} completion={g['completion']:7} "
              f"custo={g['custo']:.4f}")

    total = sum(c["custo"] for c in por_consumidor.values())
    print(f"\nTOTAL: {len(registros)} registros, "
          f"{len(por_consumidor)} consumidores, "
          f"custo total = {total:.4f} USD")


if __name__ == "__main__":
    main()

Rode com seus dados:

bash
python3 custo_por_consumidor.py uso.jsonl

Sem arquivo, roda com um exemplo embutido e imprime o mesmo formato.

Como registrar a chamada

Se você chama o provedor direto, envolva a chamada num wrapper que escreve a linha após a resposta. O objeto de resposta traz usage.prompt_tokens e usage.completion_tokens.

python
import json, time

def chamar(consumer, model, fn, *args, **kwargs):
    inicio = time.time()
    resp = fn(*args, **kwargs)
    uso = getattr(resp, "usage", None)
    registro = {
        "ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
        "consumer": consumer,
        "model": model,
        "calls": 1,
        "prompt_tokens": getattr(uso, "prompt_tokens", 0) if uso else 0,
        "completion_tokens": getattr(uso, "completion_tokens", 0) if uso else 0,
        "cost_usd": 0.0,
    }
    with open("uso.jsonl", "a", encoding="utf-8") as f:
        f.write(json.dumps(registro) + "\n")
    return resp

O cost_usd fica 0.0 e você preenche depois com a tabela de preços do provedor, ou calcula na hora se tiver o preço por mil tokens.

Como verificar

A saída esperada tem duas partes. Primeiro o ranking:

CONSUMO POR CONSUMIDOR (total da janela)
consumidor           chamadas     prompt  completion  custo_usd
bot-telegram                2     105000        7000     0.0210
transcricao                 1          0           0     0.0120
resumo-diario               2      23000        1500     0.0046

TOTAL: 5 registros, 3 consumidores, custo total = 0.0376 USD

Depois o detalhe por dia e modelo. Se os números batem com a fatura do provedor, o registro está certo.

Se der errado

KeyError: 'consumer'. Causa: alguma linha do JSONL não tem o campo. Correção: padronize o produtor para sempre escrever consumer, mesmo que seja "desconhecido".

json.decoder.JSONDecodeError. Causa: linha quebrada ou vírgula sobrando. Correção: valide o arquivo com python3 -m json.tool linha por linha, ou use jq -c ..

Totais não batem com a fatura. Causa: cache de tokens não registrado, ou custo calculado com preço errado. Correção: some cache_read_tokens e cache_write_tokens se o provedor cobrar por eles, e confira a tabela de preços na data da chamada.

Exemplo real

Numa janela de 26 dias, o maior consumidor foi a tarefa de conversa, com 4194 chamadas e 3,58 USD. O segundo foi a revisão em segundo plano, com 0,63 USD. O terceiro, aprovação, com 0,18 USD. O total da janela foi 4,55 USD.

Gráfico de barras com custo em dólares por consumidor: conversa 3.58, revisão em segundo plano 0.63, aprovação 0.18, compressão 0.09, visão 0.07
Custo por consumidor numa janela de 26 dias. A conversa responde por quase 80% do gasto.

FAQ

Preciso de um gateway como LiteLLM? Não para começar. O script lê o que você já registra. Um gateway centraliza e adiciona chave virtual por consumidor, o que ajuda quando o número de serviços cresce. Para poucos consumidores, o JSONL basta.

Como registrar se uso SDK direto? Envolva a chamada num wrapper que escreve a linha após a resposta. O objeto de resposta traz usage.prompt_tokens e usage.completion_tokens. O custo você calcula com o preço do modelo.

E se o provedor não retorna tokens? Estime: 1 token vale cerca de 4 caracteres em inglês e 3 em português. Marque como estimativa no campo cost_status.

Vale a pena otimizar o maior consumidor primeiro? Sim. A regra de Pareto vale aqui: o topo do ranking concentra o gasto. Reduzir o prompt do maior consumidor em 20% economiza mais do que cortar todos os outros.

E se eu tiver muitos consumidores? Agrupe por serviço, não por requisição. O ranking continua útil e o arquivo não explode. Para dezenas de serviços, um gateway centralizado facilita.

Como saber se a estimativa de tokens está certa? Compare com o tokenizer do provedor. Para português, 1 token vale cerca de 3 caracteres. Se a sua estimativa foge muito, ajuste o fator.

Fontes

Documentação do LiteLLM: docs.litellm.ai.

LiteLLM, gateway open source com rastreamento de gasto: https://github.com/BerriAI/litellm Boas práticas de produção da OpenAI: https://developers.openai.com/api/docs/guides/production-best-practices

Links internos: veja servidores MCP prontos e cron do jeito certo.

Leia também

// Por e-mail

Os textos publicados aqui também vão por e-mail. O envio ainda não começou: quem deixar o endereço agora entra na primeira lista.

Guardo só o seu e-mail, a data e o registro desta autorização. Sem cookies, sem rastreador e nada compartilhado com terceiros. Detalhes na política de privacidade.