Colocar um LLM atrás de uma tela de chat leva uma tarde. Transformar isso num produto que cobra, aguenta mais de um cliente e não quebra a sua margem leva bem mais — e o modelo é a parte fácil. O difícil é medir, cobrar e isolar.

Este guia é a planta de uma plataforma de IA multi-inquilino: as camadas, o que fica em cada uma, e as contas que decidem se o negócio fecha. Os erros listados aqui são erros reais de quem opera uma plataforma de IA cobrando em reais — inclusive os nossos.

⚡ Resumo

Seis camadas: identidade → chaves → limites → roteamento → medição → cobrança. A medição é o coração; se ela estiver errada, todo o resto está. Regra de ouro: cobre pelo efeito, nunca pela chamada.

A arquitetura em seis camadas

Desenhe assim e cada problema cai numa camada só:

CamadaResponsabilidadeErro clássico
1. IdentidadeQuem é o usuário e a que conta pertenceConfundir usuário com conta — aí ninguém consegue ter equipe
2. ChavesCredenciais de máquina, revogáveis, com escopoGuardar a chave em texto puro no banco
3. LimitesQuota, rate limit, saldo mínimoChecar saldo depois de gastar
4. RoteamentoEscolher o modelo/máquina que atendeAmarrar o nome do fornecedor no identificador público
5. MediçãoRegistrar o consumo real de cada chamadaMedir o que pediu em vez do que aconteceu
6. CobrançaConverter consumo em lançamento financeiroCobrar por chamada em operação idempotente

Camada 2: chaves de API que dá para revogar

Usuário faz login; máquina usa chave. São coisas diferentes e o mesmo sistema não deve servir as duas. A chave precisa nascer com prefixo reconhecível, ser guardada como hash e poder morrer sem derrubar a conta.

import secrets, hashlib

def gerar_chave(conta_id: int):
    bruta = "sk_live_" + secrets.token_urlsafe(32)
    return bruta, {                                  # a bruta aparece UMA vez, na criação
        "conta_id":  conta_id,
        "hash":      hashlib.sha256(bruta.encode()).hexdigest(),
        "prefixo":   bruta[:16],                     # p/ o usuário reconhecer na lista
        "criada_em": agora(),
        "ultima_uso": None,
    }

def autenticar(cabecalho: str):
    bruta = cabecalho.removeprefix("Bearer ").strip()
    h = hashlib.sha256(bruta.encode()).hexdigest()
    return db.buscar_um("SELECT * FROM chaves WHERE hash=? AND revogada_em IS NULL", [h])

Guardar só o hash não é preciosismo: significa que um vazamento do seu banco não entrega as chaves dos clientes. E o campo prefixo existe para o cliente conseguir dizer "revoga a que começa com sk_live_a3f" sem você precisar do valor completo.

Camada 3: limites — e o erro que derruba cliente

Você precisa de três limites distintos, e confundi-los é fonte de incidente:

⚠️ Reserva não é gasto

Este erro nos custou um incidente real. Quando uma chamada está em voo, você reserva um valor estimado para não deixar o saldo furar. Se algum outro processo — o que desliga máquinas por falta de saldo, por exemplo — ler o saldo cru sem descontar as reservas, ele conclui que o cliente está no vermelho e desliga um recurso que estava pago.

Solução: exponha uma única função saldo_efetivo() = saldo − reservas em voo, e proíba qualquer código de decisão de ler a coluna de saldo diretamente.

def saldo_efetivo(conta_id: int) -> float:
    saldo    = db.escalar("SELECT saldo FROM contas WHERE id=?", [conta_id])
    em_voo   = db.escalar("""SELECT COALESCE(SUM(valor),0) FROM reservas
                             WHERE conta_id=? AND liquidada_em IS NULL""", [conta_id])
    return saldo - em_voo

# TODO código que decide "pode gastar?" ou "desliga por falta de saldo?" usa ESTA função.

Camada 4: roteamento e a fronteira da sua marca

Se a sua plataforma serve modelos ou máquinas de terceiros, o identificador público não pode ser o identificador do fornecedor. Não é só uma questão de segredo comercial: é liberdade operacional.

Enquanto o cliente pedir modelo-x-v3-0731, você está preso àquele fornecedor e àquele nome. No dia em que trocar, ou em que ele renomear, você quebra a integração de todo mundo. Com um identificador próprio, a troca é uma linha num mapa:

MODELOS = {                      # público → interno
    "plataforma-fast": {"upstream": "modelo-pequeno-atual", "nome": "Rápido"},
    "plataforma-plus": {"upstream": "modelo-medio-atual",   "nome": "Padrão"},
    "plataforma-max":  {"upstream": "modelo-grande-atual",  "nome": "Máximo"},
}

# Aceite os nomes genéricos na ENTRADA por conveniência,
# mas NUNCA devolva o identificador interno na SAÍDA.
def resolver(pedido: str):
    return MODELOS.get(pedido) or MODELOS.get(ALIASES.get(pedido, ""))

Cuidado com os vazamentos indiretos, que são os que escapam na revisão: campos de metadados que o fornecedor devolve e você repassa sem filtrar, mensagens de erro cruas, headers de resposta, e comentário em HTML ou JavaScript — view-source é público. Filtre a saída num ponto único, não em cada rota.

Camada 5: medição — o coração da plataforma

Meça o que aconteceu, nunca o que foi pedido. A diferença aparece o tempo todo: a resposta cortou no meio, o cliente cancelou a conexão, o modelo usou cache de contexto, a chamada falhou depois de processar metade.

def registrar_uso(conta_id, chave_id, modelo, uso, desfecho):
    custo = (uso.entrada  / 1e6) * PRECO[modelo]["entrada"] \
          + (uso.saida    / 1e6) * PRECO[modelo]["saida"]   \
          + (uso.cache    / 1e6) * PRECO[modelo]["cache"]

    db.inserir("eventos_uso", {
        "conta_id": conta_id, "chave_id": chave_id, "modelo": modelo,
        "tokens_entrada": uso.entrada, "tokens_saida": uso.saida,
        "tokens_cache": uso.cache, "custo_brl": round(custo, 6),
        "desfecho": desfecho,             # ok | cancelado | erro_upstream | truncado
        "quando": agora(),
    })
    return custo

Grave desfecho desde o primeiro dia. É o campo que responde "por que a receita não bate com o número de chamadas?" — e sem ele você reconstrói isso a mão, meses depois, sem os dados.

Arredonde a favor da casa, mas com honestidade

Chamadas individuais custam frações de centavo. Se você arredondar para baixo em cada uma, some um milhão de chamadas e o prejuízo é real. Arredonde para cima, sempre na mesma direção, e documente isso na página de preços. Cliente aceita arredondamento previsível; o que ele não aceita é conta que não fecha.

Camada 6: cobrança — cobre pelo efeito, nunca pela chamada

⚠️ O incidente que ensina essa regra

Uma plataforma cobra 1 hora pré-paga ao ligar uma máquina. Um cliente com pipeline automatizado chamava /start antes de cada tarefa. A máquina já estava ligada — do lado da infraestrutura, aquilo era operação sem efeito. Mas a cobrança rodava na chamada, não no efeito. Resultado: 7 horas cobradas por 2 horas de uso, estornadas depois.

A correção tem três partes: (1) checar o estado antes; (2) usar uma trava atômica, porque duas chamadas simultâneas passam pela checagem juntas; (3) responder 200 com {"jaEstavaLigada": true}, não 4xx — devolver erro quebraria a integração de quem chama de forma idempotente de propósito.

-- A trava é o UPDATE condicional, não o SELECT antes dele.
-- Só segue quem conseguiu mudar a linha (changes = 1).
UPDATE instancias SET status='ligando'
 WHERE id = ? AND status NOT IN ('ligada','ligando','criando');
alterou = db.executar(SQL_ACIMA, [inst_id]).linhas_afetadas
if alterou == 0:
    return {"ok": True, "jaEstavaLigada": True}       # 200, sem cobrar

try:
    cobrar_reserva(conta_id, valor_hora)
    provedor.ligar(inst_id)
except Exception:
    db.executar("UPDATE instancias SET status='parada' WHERE id=?", [inst_id])
    estornar_reserva(conta_id, valor_hora)            # o rollback é obrigatório
    raise

O padrão vale para qualquer coisa cobrada: se a operação não produziu efeito, ela não gera lançamento. E se produziu efeito e a cobrança falhou, você precisa desfazer o efeito — senão entrega de graça.

Convenção de sinais no extrato

Decida no primeiro dia e não mude: cobrança é negativa, crédito é positivo. Parece trivial e não é. Um lançamento de cobrança gravado com valor positivo debita o saldo certo (porque outro trecho de código faz a subtração) mas aparece como crédito no extrato do cliente — e vira bomba-relógio no dia em que alguém recalcular o saldo com um SUM().

Isolamento entre inquilinos

Três níveis, do mais barato ao mais caro. Escolha por cliente, não para a plataforma inteira:

NívelComoCustoPara quem
Lógicoconta_id em toda tabela e toda consultaZeroPadrão, a maioria
DadosBanco ou schema por clienteBaixoExigência de contrato
ComputaçãoInstância de GPU dedicada por clienteAltoCorporativo, dado sensível

No isolamento lógico, a proteção que importa não é a boa intenção do desenvolvedor: é o middleware que injeta conta_id e a revisão que rejeita qualquer consulta a tabela multi-inquilino sem ele. Uma consulta esquecida vaza dado de um cliente para outro — o pior incidente possível numa plataforma B2B.

🔒 Quando o cliente pede isolamento de verdade

Para o cliente corporativo que não aceita API compartilhada, a resposta é uma instância dedicada: o modelo roda numa máquina que é só dele, e ele controla os pesos, os logs e os prompts, sem API de terceiro no caminho. Isso é argumento de controle — e é o que vale em análise de LGPD, porque define quem processa o quê, não onde a máquina está.

A conta que decide o negócio

Modelo de custo de uma plataforma de IA tem três parcelas. Duas todo mundo lembra; a terceira mata margem em silêncio.

Token ou GPU dedicada, por cliente

Com números reais. Suponha um cliente que consome 40 milhões de tokens de entrada e 8 milhões de saída por mês:

OpçãoContaCusto/mês
Por token (gpub-plus)40 × R$ 0,69 + 8 × R$ 3,99R$ 59,52
Por token (gpub-fast)40 × R$ 0,49 + 8 × R$ 1,09R$ 28,32
GPU dedicada 24×7 (RTX 4090, Econômica)R$ 2,38/h × 730 hR$ 1.737
GPU dedicada só em horário comercial (10 h/dia útil)R$ 2,38/h × 220 hR$ 523

A leitura: nesse volume, token ganha com folga. A GPU dedicada só passa a fazer sentido quando o consumo é contínuo — porque o preço da GPU é por hora ligada, e não por trabalho feito. Regra prática: token para carga irregular, GPU para carga constante, e desligue a máquina fora do horário se a carga tem hora para acontecer.

💡 O modelo híbrido é o que costuma vencer

Sirva a base de clientes por token, e ofereça GPU dedicada como plano superior para quem tem volume ou exigência de isolamento. Você fica sem custo ocioso na base e cobra caro exatamente onde o custo é alto. Bônus: o mesmo código atende os dois — se a sua GPU serve uma interface compatível com a da OpenAI, muda só a URL base.

Precificação: três armadilhas

O que monitorar desde o primeiro cliente

⚠️ Todo script agendado carrega credencial por caminho absoluto

Um monitor nosso lia o arquivo de ambiente por caminho relativo. Rodando na mão, funcionava. No agendador, o diretório de trabalho era outro — ele rodava sem nenhuma credencial, lia saldo como NaN e falhava em silêncio por semanas. Em qualquer tarefa agendada, resolva o caminho a partir do arquivo do próprio script.

A infraestrutura da sua plataforma, cobrada em reais

API por token compatível com a da OpenAI e GPUs sob demanda no mesmo saldo. Sem assinatura, sem câmbio, sem IOF — e suporte em português.

Criar conta →

Roteiro de 30 dias

  1. Semana 1: identidade, chaves com hash, uma rota que chama o modelo e grava o evento de uso. Sem cobrança ainda.
  2. Semana 2: medição correta (com desfecho), painel de consumo para o cliente ver. Ainda de graça.
  3. Semana 3: saldo, reserva, saldo_efetivo(), cobrança pelo efeito. Teste os caminhos de falha — não só o feliz.
  4. Semana 4: limites, alertas de saldo, reconciliação e o monitor de câmbio. Só então abra para clientes pagantes.

A ordem importa. Plataforma que abre cobrança antes de ter medição confiável passa os meses seguintes estornando.

Conclusão

Uma plataforma de IA é 20% modelo e 80% contabilidade. As decisões que definem se ela sobrevive — cobrar pelo efeito, medir o que aconteceu, saldo efetivo em vez de saldo cru, piso de custo no preço, fonte única de câmbio — não têm nada a ver com IA. São as mesmas de qualquer sistema que mexe com dinheiro, só que com um custo variável que muda sozinho.

Construa a medição primeiro. Todo o resto se conserta depois; dado de consumo que você não gravou está perdido para sempre.

Continue: o agente que roda em cima dela · RAG de produção · Econômica ou Dedicada?