O modelo mais barato por token pode ser o documento mais caro por resultado aceito
O time do RelayOps troca o modelo de extração por um mais barato por token. A fatura mensal da API cai — o dashboard de custo por chamada mostra a economia no mesmo dia. Duas semanas depois, alguém finalmente calcula o número que devia ter olhado desde o início: custo por documento efetivamente aceito. Ele subiu. O modelo mais barato erra o schema com mais frequência, cada erro vira uma nova chamada (que também é paga), e o documento — de um tenant crítico — ainda precisa de revisão humana antes de ser aceito. Este incidente é sintético; não descreve cliente, fatura real ou carga de produção, mas a estrutura do erro é comum o bastante para abrir o capítulo com ela: uma métrica caiu, mas não era a métrica que decidia se a troca valeu a pena.
A causa não é um erro de aritmética — é medir a coisa errada. Preço por token responde "quanto custa uma chamada". Custo por documento aceito responde "quanto custou o resultado que alguém realmente pôde usar", e essa segunda pergunta soma retries, revisão humana, armazenamento, egress e overhead de worker na mesma conta, sem contar nada duas vezes. As duas coisas variam de forma independente: um modelo pode ser mais barato por token e ainda assim mais caro por documento aceito, se ele erra schema com frequência suficiente para que os retries apaguem a economia — e, quando a revisão humana entra na conta, ela pode dominar tanto o total que a escolha de modelo vira um detalhe de segunda ordem perto da decisão de rotear ou não para humano. Este capítulo constrói a calculadora, a política de cache, a política de routing e o orçamento por tenant que tornam essa distinção verificável em vez de intuída.
O capítulo 5 fechou a camada de evals e observabilidade do RelayOps: dataset dev/holdout sem sobreposição de ID, graders determinísticos (schema, campo, política, sucesso da tarefa), um gate de release que reprova regressão em estrato crítico mesmo com média geral maior, e um relatório por estrato no formato { overall, n } — taxa de sucesso medida sobre uma amostra com tamanho conhecido. Este capítulo não reabre nem reimplementa nenhum arquivo daquela camada; ele assume que RelayOps já sabe medir se um resultado está certo, e faz uma pergunta diferente: dado que se sabe medir qualidade, qual routing, qual cache e qual orçamento mantêm essa qualidade dentro de um limite de gasto previsível por tenant — inclusive quando o tráfego não é a média estável de uma planilha? A única coisa que este capítulo reusa do capítulo 5 é a forma da evidência de estrato ({ overall, n }), como contrato de entrada da política de routing — nenhum número do capítulo 5 é copiado ou citado como se fosse deste capítulo.
Antes de qualquer calculadora, a distinção cabe em uma função:
export function tokenCallCost({ model, inputTokens, outputTokens, cacheReadTokens = 0 }, prices) {
const rate = prices.models[model];
const perMTok = (tokens, r) => (tokens / 1_000_000) * r;
return perMTok(inputTokens, rate.input)
+ perMTok(outputTokens, rate.output)
+ perMTok(cacheReadTokens, rate.cacheRead);
}
Essa função responde "quanto custou esta chamada" — nada mais. Ela não sabe se a chamada precisou de um retry, não sabe se o documento foi para revisão humana, não sabe se o tenant tem orçamento para outra chamada. Cada uma dessas perguntas é uma camada separada: documentMarginalCost soma todas as chamadas de um documento (incluindo retries) mais overhead de tool/worker/storage/egress; documentAllocatedCost adiciona revisão humana só quando o routing decidiu por ela; e costPerAcceptedDocument divide o gasto total de um lote pela contagem de documentos que realmente chegaram a um estado aceito — nunca pela contagem de chamadas. Um documento rejeitado continua custando dinheiro e nunca é contado como gratuito; isso é um teste automatizado, não uma frase (a rejected document still costs money and is excluded from acceptedCount, never counted as free).
O próprio guia de engenharia da Anthropic sobre agentes afirma que "sistemas agênticos frequentemente trocam latência e custo por melhor desempenho da tarefa", e recomenda explicitamente rotear "perguntas fáceis/comuns para modelos menores e mais baratos... e perguntas difíceis/incomuns para modelos mais capazes" como o caso de uso central de um workflow de routing. O erro do time do RelayOps na abertura deste capítulo não foi usar um modelo mais barato — foi usar um só modelo mais barato para todo tipo de documento, sem medir se aquele tipo específico tolera o modelo mais barato. A intuição de custo/capacidade de sistemas convencionais também já resolveu um problema parecido: o livro de SRE do Google descreve proteção contra sobrecarga baseada em um sinal de utilização local, e um mecanismo de throttling adaptativo no qual cada cliente rastreia sua própria taxa de aceitação recente e se auto-regula antes que a fila compartilhada seja afetada — a mesma forma que este capítulo usa para orçamento por tenant: um sinal local de "quanto de orçamento resta" que desacelera o próprio tenant antes que ele consuma a capacidade de todos os outros.
RelayOps ganha, neste capítulo, quatro peças novas, independentes da camada de evals e da camada de durabilidade dos capítulos anteriores:
examples/costs/calculate.mjs) sobre um price fixture real e datado — preços de lista da API da Anthropic, capturados em 2026-09-28 — mais premissas operacionais explicitamente sintéticas (armazenamento, egress, worker, revisão humana), nunca misturadas na mesma tabela sem rótulo.examples/src/cache-policy.mjs) com chave determinística por tenant + checksum do documento + versão de schema/prompt/modelo/policy, em dois namespaces que nunca colidem: exact (mesma resposta para o mesmo documento) e semantic (mesmo prompt/schema/policy compilado, reusado entre documentos diferentes do mesmo tenant e tipo).examples/src/budget-policy.mjs) com reserva atômica na fronteira de execução, reconciliação pelo custo real após a chamada, e um sinal de backpressure que cresce conforme o orçamento de um tenant se esgota — para que um tenant barulhento nunca sufoque a fila compartilhada de outro.examples/src/routing-policy.mjs) que decide entre nível barato, nível padrão e fallback humano usando três entradas determinísticas: risco do tipo de documento, orçamento disponível, e uma taxa de sucesso medida (nunca uma confiança relatada pelo próprio modelo).Nenhuma dessas quatro peças chama um modelo real, um banco de dados ou a rede. Todas rodam offline, em Node puro, e os 24 testes que as cobrem rodam em menos de cem milissegundos.
O price fixture que alimenta a calculadora separa, de propósito, duas categorias de número que não podem viver na mesma tabela sem rótulo: os preços de Claude Opus 5.5, Claude Sonnet 5 e Claude Haiku 4.5 — reais, datados, copiados da documentação de preços da Anthropic em 2026-09-28 — e as premissas operacionais (armazenamento, egress, computação de worker, custo por minuto de revisão humana), que são inteiramente sintéticas, escolhidas para deixar a aritmética auditável à mão, não para modelar uma fatura de nuvem real. Misturar as duas categorias numa tabela só de números — sem dizer qual linha é fonte primária e qual é premissa de tutorial — é exatamente o tipo de erro que faz um leitor tratar um exemplo didático como um benchmark. prices-fixture.json marca cada bloco com um campo _type e um aviso explícito por esse motivo, e sources.md registra a URL e a data de acesso ao lado de cada preço real.
A ordem importa mais do que parece. Um sistema que primeiro faz a chamada e só depois verifica se havia orçamento sempre vai, eventualmente, gastar além do limite — a checagem chega tarde demais para impedir o próprio gasto que está checando. BudgetLedger.reserve() inverte essa ordem: o orçamento é retirado do saldo disponível do tenant antes de qualquer chamada acontecer, e só é convertido em gasto real (commit) ou devolvido (release) depois que o resultado da chamada é conhecido. Isso transforma "o orçamento estourou" de um fato que se descobre depois do gasto em uma condição que impede o gasto de acontecer — a diferença entre um alarme e um freio.
O contrato mais importante deste capítulo é a diferença entre três números que soam parecidos e não são intercambiáveis:
O segundo contrato é a chave de cache: tenantId + documentChecksum + schemaVersion + promptVersion + modelId + policyVersion, hasheada de ponta a ponta. Mudar qualquer um desses seis campos precisa produzir uma chave diferente — um teste percorre os seis campos, um de cada vez, e confirma isso (mandatory failure #2). O terceiro contrato é a reserva orçamentária: BudgetLedger.reserve() é síncrona e não tem await na seção crítica, de propósito — em Node, isso garante que duas reservas concorrentes para o mesmo tenant não possam passar pela checagem de orçamento ao mesmo tempo, sem precisar de um lock explícito. Essa garantia depende do event loop de um único processo; uma implantação real com múltiplos processos precisaria da mesma invariante garantida por uma transação de banco de dados ou por um script atômico em um armazenamento compartilhado — uma lacuna documentada, não resolvida aqui.
Sintoma: a fatura de API cai depois de uma troca de modelo; ninguém calcula custo por documento aceito antes de declarar vitória.
Causa: preço por token mede o insumo (uma chamada); unit economics real mede o resultado (um documento que alguém pôde usar). Um modelo mais barato por token que erra schema com mais frequência transforma cada erro em uma chamada extra — ainda paga — e pode, além disso, empurrar mais documentos para revisão humana, que costuma custar ordens de grandeza mais que a própria chamada de LLM.
Resposta: medir sempre costPerAcceptedDocument, nunca só costPerCall. No cenário reproduzido em verification.md, o modelo mais barato precisou de três chamadas (duas retries) para ser aceito, contra uma chamada do modelo mais caro para o mesmo tipo de documento; o custo somente de LLM do caminho "barato" ficou 52% mais alto em dólares absolutos que o caminho "caro" — o preço por token mentiu sobre a direção da economia assim que o retry entrou na conta. Quando a revisão humana some ao total (porque o tenant é crítico), ela domina o número final para os dois casos, o que ensina uma segunda lição: para esse tipo de documento, a alavanca de custo real não é qual modelo usar, é se a revisão humana é necessária — uma decisão de routing e eval, não de preço.
Sintoma: um documento do tenant B recebe a resposta cacheada de um documento parecido do tenant A; ou uma resposta cacheada sob a policy antiga continua sendo servida depois que a policy mudou.
Causa: uma chave de cache construída só a partir do conteúdo do documento (por exemplo, só o checksum) ignora que a mesma entrada de conteúdo pode, legitimamente, precisar de respostas diferentes para tenants diferentes, ou para versões diferentes de schema/prompt/policy do mesmo tenant.
Resposta: a chave exact inclui os seis campos citados acima, e a leitura (CacheStore.get) exige o tenantId de quem está pedindo e lança CacheLeakageError se a entrada armazenada pertencer a outro tenant — mesmo que um bug upstream tenha, por acidente, produzido a mesma chave para dois tenants. Isso move a garantia de isolamento do momento de construir a chave para o momento de ler o cache, que é onde um bug de fato aparece. Um segundo namespace, semantic, existe só para reusar o prompt/schema/policy compilado de um tenant entre documentos diferentes do mesmo tipo — nunca a resposta de um documento específico — e os dois namespaces nunca colidem, mesmo construídos a partir dos mesmos seis campos (menos o checksum, que o semantic não usa).
Sintoma: uma política de retry bem-intencionada continua tentando (ou caindo para um modelo de "reserva") depois que o orçamento do tenant já estourou.
Causa: lógica de retry e lógica de orçamento vivem em lugares diferentes do código, e ninguém garante que a primeira nunca rode sem passar pela segunda.
Resposta: withBudget() reserva orçamento antes de cada tentativa — incluindo retries — e só chama a função de tentativa depois que a reserva foi aceita; se a reserva falhar, a tentativa nunca roda (um teste confirma isso contando quantas vezes a função de tentativa foi de fato invocada). Cada tentativa que roda e falha na validação ainda teve um custo real — tokens foram gastos mesmo que o schema tenha saído errado — e esse custo é comitado no razão, nunca liberado de graça; só uma falha de infraestrutura anterior a qualquer gasto real (por exemplo, uma conexão recusada) libera a reserva sem custo. Um backpressureDelayMs() cresce conforme o orçamento restante de um tenant encolhe, e chega a infinito quando o orçamento acaba — o sinal existe para desacelerar o próprio tenant antes que ele precise ser bloqueado de vez, e para impedir que o esgotamento do orçamento de um tenant reduza o orçamento disponível de outro (noisy neighbor, testado explicitamente).
Sintoma: um plano de capacidade calcula a taxa média de chegada de documentos ao longo de um período, conclui que o sistema está confortavelmente abaixo da capacidade, e é surpreendido por uma fila crescendo sem limite durante um pico real.
Causa: a Lei de Little (L = λW, número médio de itens no sistema igual à taxa de chegada vezes o tempo médio no sistema) e a fórmula de atraso de fila só valem — e só fazem sentido — para um sistema estável, isto é, com utilização menor que 1. A média de três períodos de demanda pode ser perfeitamente estável mesmo que um deles, isoladamente, não seja: a média nunca "vê" o pico, porque ela já misturou o pico com os períodos calmos antes de qualquer cálculo de fila acontecer.
Resposta: examples/costs/capacity-notes.md reproduz isso com números reais, não com afirmação solta. Com quatro workers e tempo de serviço de 1 segundo (capacidade agrupada de 4 documentos/segundo): o cenário de baixa demanda (0,5 doc/s) fica com utilização 0,125 e atraso de fila de 0,036s; o cenário base (1,5 doc/s) fica com utilização 0,375 e atraso de 0,15s; o cenário de burst (4,5 doc/s) tem utilização 1,125 — instável, sem atraso médio finito definido, fila crescendo sem limite enquanto o burst durar. A média dos três (2,1667 doc/s) resulta em utilização 0,542, perfeitamente estável, com atraso de fila de 0,295s e 2,8 documentos em média no sistema — exatamente o número que uma planilha de capacidade calcularia e aprovaria, sem nunca revelar que um dos três cenários que a compõem está fora da região onde a própria fórmula funciona.
routeDocument() não aceita nenhum campo que pareça uma confiança relatada pelo próprio modelo (confidenceScore, probability e variantes são rejeitados explicitamente por assertNoInventedConfidence) — porque uma pontuação que um modelo relata sobre a própria resposta não é uma probabilidade calibrada, é um número que o modelo produz junto com a resposta, sem garantia estatística nenhuma de que 90% de "confiança" corresponda a 90% de acerto real. A única evidência que a política aceita é uma taxa de sucesso medida, no mesmo formato { overall, n } que o gate de release do capítulo 5 já produz por estrato — e mesmo essa evidência só é usada quando a amostra é grande o bastante (n >= 8 neste capítulo, um piso didático, não calibrado contra histórico real de regressão). Um tipo de documento crítico sem amostra suficiente cai para humano mesmo com uma taxa medida alta e um risco aparentemente baixo — a ausência de evidência suficiente é, por si só, motivo de fallback seguro, independente do que qualquer número isolado diga.
Custo e segurança se encontram na mesma decisão de routing: enviar um documento para o nível mais barato sem evidência suficiente não é só um risco de qualidade, é um risco de vazamento de dado se o schema daquele tipo de documento tiver campos sensíveis que o modelo mais barato erra com mais frequência (subestimando a necessidade de revisão humana justamente onde ela mais importa). O isolamento de tenant no cache (falha 2) é, ao mesmo tempo, uma questão de custo (uma chave errada gera uma resposta errada, que precisa de retry) e de segurança (uma resposta de um tenant vazando para outro é um incidente de confidencialidade, não só um bug de cache). Toda decisão deste capítulo é reversível por desenho: uma reserva de orçamento pode ser liberada (release) sem custo se a chamada nunca aconteceu; uma entrada de cache pode ser invalidada por tenant inteiro (invalidateTenant) sem afetar outros tenants; e uma decisão de routing é recalculada a cada documento, nunca fixada permanentemente — não existe, neste desenho, uma migração de dado irreversível de tenant nem um "modo econômico" que precise ser desligado manualmente para voltar ao estado anterior.
PROJECTION - local arithmetic...) e distingue marginal de alocado.tenantId de quem pede.node --test costs/*.test.mjs passa localmente antes de qualquer mudança de política de custo, cache, orçamento ou routing.Básico — calcular custo por documento aceito com retries. Critério verificável: rodar node costs/calculate.mjs --case cheap-model-more-retries e node costs/calculate.mjs --case pricier-model-fewer-retries; confirmar que costPerAcceptedDocumentUSD do primeiro é maior que o do segundo, e que marginal.breakdown.llmUSD do primeiro (três chamadas) é maior que o do segundo (uma chamada) mesmo com preço por token mais baixo. Solução comentada: os dois comandos imprimem a decomposição completa; a diferença de llmUSD isola exatamente o efeito dos retries, sem misturar com a revisão humana (que é idêntica nos dois casos).
Intermediário — provar invalidação de cache por versão e tenant. Critério verificável: node --test costs/budget.test.mjs reprovaria se buildExactCacheKey parasse de mudar de hash ao variar qualquer um dos seis campos, ou se CacheStore.get parasse de lançar CacheLeakageError numa leitura cross-tenant forçada. Solução comentada: o teste mandatory failure #2 itera os seis campos do contrato um a um, construindo uma chave alterada e comparando o hash contra a chave base; o teste de vazamento escreve uma entrada sob a chave do tenant A e tenta lê-la explicitamente como tenant B, confirmando que a leitura — não só a escrita — impõe o isolamento.
Avançado — simular burst/noisy neighbor, analisar throughput e atraso de fila mantendo qualidade e quota. Critério verificável: estimateQueueDelay() sobre os três cenários de demandScenarios deve reportar stable: true para os dois primeiros e stable: false para o burst; a média dos três deve permanecer stable: true, demonstrando a armadilha da falha 4. Ao mesmo tempo, BudgetLedger com dois tenants deve mostrar que esgotar o orçamento de um (tenant-noisy) não reduz o headroomUSD do outro (tenant-quiet). Solução comentada: os dois testes correspondentes (mandatory failure #4 e noisy neighbor) rodam de forma independente sobre o mesmo arquivo de cenários e o mesmo BudgetLedger, e capacity-notes.md documenta os números completos, incluindo a ressalva de que a aproximação de fila agrupada (pooled M/M/1) subestima o atraso real de um M/M/c exato com poucos servidores — o número aqui é otimista, não conservador.
ρ < 1.L = λW — número médio de itens no sistema igual à taxa de chegada vezes o tempo médio no sistema, válida apenas em regime estável.Este capítulo assumiu que o routing pode, com segurança, escolher entre nível barato, padrão e humano — mas nunca perguntou o que acontece quando um tenant tenta deliberadamente contornar essa política, ou quando uma falha parcial do próprio orçamento (não do modelo) precisa ser tratada como incidente. O capítulo 7 trata de segurança e resiliência: o que muda no RelayOps quando o adversário não é um documento ambíguo, mas um agente — humano ou automatizado — tentando quebrar as garantias de tenancy, orçamento e cache que este capítulo construiu.
Produtos gratuitos e pagos para transformar ideias em uma base que você consegue executar.
13 produtos disponíveisContinue explorando tópicos similares
Um deploy do RelayOps reduz erros HTTP e, na mesma semana, aumenta o número de extrações erradas aceitas como corretas. O capítulo 5 constrói dataset versionado, runners determinísticos, graders,…
Um documento pede, em texto puro, para o próprio agente exportar o arquivo do tenant e se autopromover. A defesa óbvia — instruir o modelo a recusar — roda na camada errada. O capítulo 7 constrói…
Um worker termina a extração e cai antes de confirmar a fila. A entrega é reprocessada e, sem uma chave de idempotência correta, o pipeline cria uma segunda revisão para o mesmo documento. O capítulo…
Checklist de 47 pontos para encontrar bugs, riscos de segurança e problemas de performance antes do lançamento.
Templates testados em produção, usados por desenvolvedores. Economize semanas de setup no seu próximo projeto.