Tiers, custo & orçamento
Um motor que distribui uma requisição a muitos modelos precisa de dois botões independentes: autonomia (quanta supervisão humana o trabalho exige) e custo (quanto dinheiro uma chamada pode gastar). O Alembic codifica o primeiro como a escada de Tiers T0→T4, o segundo como um registro de modelos precificado por 1k tokens mais um BudgetGuard fail-closed. Esta lição conecta os dois: como um tier roteia para o modelo qualificado mais barato, como toda chamada paga é medida, e por que um orçamento não-positivo significa "só free tier" — nunca "ilimitado".
- Distinguir os dois botões: Tier (autonomia/supervisão) vs custo (preço por modelo) — relacionados, mas mecanismos separados.
- Ler a escada
T0→T4e o marcador ortogonalLOCAL, e por queDEFAULT_TIER = T4é fail-closed. - Aplicar
pickCheapestForTier: o fold puro que escolhe a entrada mais barata de um tier por custoin+outcombinado. - Explicar o golpe fail-closed do
BudgetGuard:cap = Math.max(0, capUsd)⇒ teto não-positivo vira 0 = só free tier.
01 · Dois botões ortogonais: autonomia e custo
Antes de qualquer código, fixe a ideia central — porque quase toda confusão desta lição vem de fundi-la. Tier e custo são dois botões que giram de forma independente. Um diz quanta supervisão humana o trabalho precisa; o outro diz quanto dinheiro uma chamada pode gastar. Eles se cruzam no roteamento, mas nunca são a mesma coisa.
02 · A escada de autonomia: T0 → T4
O enum Tier tem exatamente cinco degraus, autonomia-mais-barata primeiro. Eles são sobre supervisão humana, não sobre dinheiro. Esta é a primeira tabela comparativa — leia cada degrau como "quem precisa aprovar":
| Tier | Significado | Autônomo? |
|---|---|---|
| T0 | silencioso / totalmente autônomo, sem humano no loop | sim (o caminho silencioso) |
| T1 | autônomo com logging leve | sim |
| T2 | autônomo, um revisor notificado | sim |
| T3 | autônomo, revisão de council exigida | sim |
| T4 | PARK — retido da execução autônoma; precisa de council + humano | não |
Dois fatos tornam isto fail-closed: DEFAULT_TIER = T4 (trabalho não classificado estaciona, em vez de rodar sozinho — Lição 26) e isAutonomous retorna true só para T1–T3 (tier.ts:59). Veja a escada como degraus, com a fronteira da autonomia desenhada entre T3 e T4:
03 · O marcador LOCAL não é um sexto tier
Erro clássico: contar "T0, T1, T2, T3, T4, LOCAL — seis tiers". Não. Tier é exatamente T0–T4. LOCAL é um marcador ortogonal que marca trabalho que deve ficar em modelos local/$0 "independente do tier" (caminhos sensíveis a privacidade ou custo, tier.ts:36). Por ser ortogonal, ele se combina com qualquer degrau: uma unidade pode ser T2 e LOCAL ao mesmo tempo — autônoma com um revisor, mas presa a modelos locais.
04 · O registro de modelos: custo por 1k tokens
Cada modelo roteável é uma entrada de registro com um adapterId, um tier e dois preços — costPer1kInputUsd e costPer1kOutputUsd. Há 11 entradas espalhadas pelos tiers. A forma é o contrato (validado por Zod); ids e preços são dados do registro e evoluem:
// packages/contracts/src/registry.ts:8-13 — a FORMA (o contrato) export const modelRegistryEntrySchema = z.object({ modelId: z.string().min(1), adapterId: z.string().min(1), tier: tierSchema, costPer1kInputUsd: z.number().nonnegative(), costPer1kOutputUsd: z.number().nonnegative(), });
// packages/contracts/src/registry.ts:57-173 — entradas representativas (in / out por 1k) { modelId: 'local-default', adapterId: 'local', tier: T0, in: 0, out: 0 } // $0 hermético { modelId: 'local-extract', adapterId: 'local', tier: T1, in: 0, out: 0 } { modelId: 'qwen3.7-plus', adapterId: 'cliproxyapi', tier: T1, in: 0.00015,out: 0.0005 } // + barato T1 { modelId: 'glm-5.2', adapterId: 'cliproxyapi', tier: T2, in: 0.0002, out: 0.0006 } // DEFAULT_MODEL_ID { modelId: 'gpt-5.5-xhigh', adapterId: 'cliproxyapi', tier: T3, in: 0.015, out: 0.045 } { modelId: 'claude-opus-4-8-max', adapterId: 'cliproxyapi', tier: T3, in: 0.03, out: 0.15 }
A divisão completa pelos tiers (do bloco-doc do registro, linhas 39–53): T0 = local-default; T1 = local-extract, kimi-k2.7-code-highspeed, grok-composer-2.5-fast, qwen3.7-plus; T2 = deepseek-v4-pro, gemini-3.5-flash, glm-5.2, qwen3.7-max; T3 = gpt-5.5-xhigh, claude-opus-4-8-max. O adapter local mantém os $0 para CI hermético. (Preços são aproximados/relativos no código e devem ser refinados contra faturas reais — comentário do próprio arquivo, linha 54.)
Comparativo: custo combinado (in+out) por tier
Esta é a segunda tabela comparativa — os números reais de registry.ts, somados (in + out) por 1k tokens, que é exatamente o escalar que o roteador compara. Verde = o mais barato do tier (o que pickCheapestForTier escolhe); vermelho = a faixa frontier.
| modelId | Tier | in / 1k | out / 1k | in + out / 1k |
|---|---|---|---|---|
local-default | T0 | 0 | 0 | 0 |
local-extract | T1 | 0 | 0 | 0 |
qwen3.7-plus | T1 | 0.00015 | 0.0005 | 0.00065 ← + barato T1* |
kimi-k2.7-code-highspeed | T1 | 0.0002 | 0.0006 | 0.0008 |
grok-composer-2.5-fast | T1 | 0.00025 | 0.00075 | 0.001 |
deepseek-v4-pro | T2 | 0.0001 | 0.0004 | 0.0005 ← + barato T2 |
gemini-3.5-flash | T2 | 0.0001 | 0.0004 | 0.0005 (empate) |
glm-5.2 DEFAULT | T2 | 0.0002 | 0.0006 | 0.0008 |
qwen3.7-max | T2 | 0.00025 | 0.0007 | 0.00095 |
gpt-5.5-xhigh | T3 | 0.015 | 0.045 | 0.06 ← + barato T3 |
claude-opus-4-8-max | T3 | 0.03 | 0.15 | 0.18 |
* Entre os modelos T1, local-extract (0) é o mais barato em absoluto; entre os pagos T1, qwen3.7-plus (0.00065) lidera. O empate em T2 (0.0005) entre deepseek-v4-pro e gemini-3.5-flash é resolvido pela ordem de iteração — ver §05.
O salto de preço entre os tiers baratos e o frontier é brutal — e é ele que justifica tanto o roteamento quanto o teto. Veja as barras em escala log (o eixo dobra a cada passo); o T3 fica numa ordem de grandeza inteiramente diferente:
05 · Roteamento: pickCheapestForTier
Quando um tier é escolhido mas nenhum modelId específico está fixado, pickCheapestForTier seleciona a entrada mais barata daquele tier pelo custo por-1k combinado. É pura sobre o registro — um fold determinístico, sem efeitos colaterais, sem Date/random:
// packages/contracts/src/registry.ts:187-200 export const pickCheapestForTier = ( tier: Tier, registry = MODEL_REGISTRY, ): ModelRegistryEntry | undefined => { const candidates = Object.values(registry).filter((entry) => entry.tier === tier); if (candidates.length === 0) return undefined; return candidates.reduce((cheapest, entry) => { const entryCost = entry.costPer1kInputUsd + entry.costPer1kOutputUsd; // in + out const bestCost = cheapest.costPer1kInputUsd + cheapest.costPer1kOutputUsd; return entryCost < bestCost ? entry : cheapest; // strict < ⇒ mantém o 1º em empate }); };
< estrito: num empate (como os dois T2 a 0.0005), o reduce mantém o candidato já visto — ou seja, a ordem de iteração do registro decide. Por isso o resultado é determinístico: o mesmo MODEL_REGISTRY sempre produz a mesma escolha, o que importa para replay (Lição 28).< ESTRITO mantém o primeiro visto (deepseek vs gemini, ambos 0.0005)Compare lado a lado os caminhos do fold em três tiers — o vencedor é o menor in+out de cada coluna:
06 · Calculadora de custo ao vivo
Hora de sentir os números. Escolha um tier, mexa os tokens de input e output — a calculadora aplica pickCheapestForTier de verdade (preços reais de registry.ts), mostra qual modelo ganha, recomputa o custo em USD e redesenha a barra. Verde = cabe no teto; vermelho = o BudgetGuard bloquearia (próxima seção).
Escala do eixo: 0 → $10 · a linha vermelha é o teto. Em T3 (frontier) basta um punhado de milhares de tokens para a barra cruzar a linha — é exatamente o que o BudgetGuard checa antes de a chamada rodar.
gpt-5.5-xhigh: 0.015 in / 0.045 out por 1k). Quanto custa essa única chamada? E ela passa num teto de $0.50?
(10000/1000)×0.015 + (10000/1000)×0.045 = 0.15 + 0.45 = $0.60. Com teto $0.50: 0 + 0.60 > 0.50 → BLOQUEIA (budget_exceeded), a chamada nunca roda. A mesma chamada em T2 com glm-5.2 (0.0002/0.0006) custaria 0.002 + 0.006 = $0.008 — 75× mais barata. É o salto de preço do frontier que o teto existe para conter.07 · O BudgetGuard fail-closed
O roteamento decide qual modelo; o BudgetGuard decide se a chamada pode acontecer. Ele é criado com um teto rígido em USD, e seus defaults são deliberadamente paranoicos. Esta é a peça central da lição:
// packages/etl/src/budget.ts:120-149 (condensado) export const createBudgetGuard = (capUsd) => { const cap = Math.max(0, capUsd); // teto não-positivo → 0 = só free tier let spent = 0; return { check(estimate) { const projectedUsd = priceEstimate(estimate); if (projectedUsd === 0) return { ok: true, projectedUsd: 0, … }; // chamada grátis SEMPRE passa if (roundUsd(spent + projectedUsd) > cap) return { ok: false, reason: 'budget_exceeded', … }; // estouraria ⇒ BLOQUEIA return { ok: true, projectedUsd, remainingUsd: remaining() }; }, record(spend) { spent = roundUsd(spent + costOf(spend)); return spent; }, }; };
cap = Math.max(0, capUsd)Um teto não-positivo não significa "sem limite" — ele vira 0, que significa só free tier: qualquer chamada com projectedUsd > 0 é bloqueada, porque 0 + qualquer > 0. Então o default seguro (sem orçamento definido) é a postura mais barata possível, nunca um gasto descontrolado. Chamadas free-tier (projectedUsd === 0) sempre passam — é por isso que o funil inteiro pode rodar $0 e hermético no adapter local (Lição 15).
Math.max(0, capUsd) escolhe diante de um teto não definidoSimples vs Técnico: o que "fail-closed" quer dizer aqui
Math.max(0, capUsd) elimina tetos negativos e o "não definido" (0). check tem duas portas: projectedUsd === 0 passa imediatamente; senão, compara roundUsd(spent + projectedUsd) > cap e devolve { ok:false, reason:'budget_exceeded' } se estouraria. O record só soma via costOf, que mede modelos free-tier como 0 (budget.ts:110) — a medição é a fonte da verdade, não o estimador.O preço é sempre aplicado na medição
O mapa declara um invariante sutil: o preço é sempre aplicado. Olhe o helper costOf do record — mesmo quando um ModelRunResult carrega um costUsd, um modelo free-tier é medido como 0 (isFreeTierModel(spend.modelId), budget.ts:110), e um modelo pago recorre a costForModel(modelId, usage) se nenhum custo explícito estiver presente. O gasto nunca é adivinhado e nunca pulado: chamadas T2/T3 são medidas ao preço do registro, então o total corrente spent é autoritativo.
record decide o que somar ao spent — o free-tier é zerado ANTES do campo08 · Decisão: "essa chamada paga roda?" (fluxograma)
Junte tudo num único fluxograma. Diante de qualquer chamada, o check segue este caminho — cada losango é uma pergunta que escolhe a rota até "roda" ou "bloqueia". Siga as setas com o caso do teto-zero destacado:
09 · Meça uma chamada na mão (passo a passo → agora você)
Você já viu a calculadora girar. Agora faça a conta na mão, devagar — recuperar o procedimento (não só ver o número) é o que fixa. Caso de referência: uma chamada T2 com o modelo default.
modelId fixado. pickCheapestForTier(T2) percorre os candidatos e retorna o de menor in+out: deepseek-v4-pro (0.0005). Mas suponha que a unidade fixou o default glm-5.2 (0.0002 in / 0.0006 out).priceEstimate/costForModel): (in/1000)×inUsd + (out/1000)×outUsd.(4000/1000)×0.0002 + (3000/1000)×0.0006 = 0.0008 + 0.0018 = $0.0026 de custo projetado.spent = 0 e teto $1: 0 + 0.0026 = 0.0026 ≤ 1 → ok ✓, a chamada roda, sobram $0.9974.gpt-5.5-xhigh (0.015 in / 0.045 out). Quanto custa? Passa num teto de $0.10? Faça antes de revelar.
(4000/1000)×0.015 + (3000/1000)×0.045 = 0.06 + 0.135 = $0.195. Com teto $0.10: 0 + 0.195 > 0.10 → BLOQUEIA (budget_exceeded). Repare: a mesma chamada saltou de $0.0026 (T2) para $0.195 (T3) — ~75× — porque o frontier é caro. O procedimento é sempre o mesmo; só trocam os preços do modelo.Confusões comuns: Tier ≠ custo
Tier é exatamente T0–T4. LOCAL é um marcador ortogonal que fixa trabalho em modelos $0 independente do seu tier. Uma unidade pode ser T2 e LOCAL ao mesmo tempo: autônoma-com-um-revisor, mas mantida em modelos locais por privacidade ou custo.Como isso se encaixa
Os dois botões desta lição não vivem isolados — eles formam a cintura econômica da máquina inteira. Toda chamada de modelo, venha de onde vier (council, swarm, funil, gates), passa por dois pontos de estrangulamento: o roteador escolhe qual modelo pelo tier (pickCheapestForTier), e o BudgetGuard decide se ela roda pelo custo projetado. Veja o fluxo de cima para baixo — o que alimenta tiers+custo (upstream), a peça desta lição (destacada, no centro), e o que ela alimenta (downstream):
Onde você está na metodologia. Tiers+custo são a camada de economia que recobre a cintura estreita (Lição 14): a cintura padroniza uma forma de chamada (usage/costUsd); esta lição decide qual modelo a atende e se ela cabe no orçamento. Nada roda fora desse par. Para ver a peça no mapa interativo completo — escopo → council → proof → validator → publish, com o funil e o loop de aprendizado — abra o mapa da metodologia.
Conecta com
- Lição 14 · A cintura estreita — porque conecta: a cintura é a forma única de toda chamada (
usage/costUsd); oBudgetGuard.recordmede exatamente essecostUsde o roteamento escolhe qual modelo passa pela cintura. - Lição 18 · Council & Verifier — porque conecta: o tier
T3é o que arma o painel com os modelos frontier (gpt-5.5-xhigh/claude-opus-4-8-max); o salto de preço do T3 é justamente o que o teto existe para conter. - Lição 15 · O funil — porque conecta: o funil é ETL tiered (T0→T3); o adapter
localde custo zero é o que deixa T0 rodar$0e hermético, e os degraus caros só são gastos quando o sinal sobe. - Lição 17 · A pipeline de gates — porque conecta: os gates rodam
unit.proof[]e chamadas de validação; cada uma é uma chamada precificada que oBudgetGuardtem de aprovar antes de acontecer. - Lição 28 · Determinismo & replay — porque conecta:
pickCheapestForTieré um fold puro (semDate/random) e a medição é a fonte da verdade — por isso o mesmo registro reproduz a mesma escolha e o mesmo custo no replay.
Na prática
Você não precisa fazer uma chamada paga para inspecionar esta camada. O comando alembic doctor --client-stack valida o registro inteiro offline e $0 (sem rede): confere a forma de cada entrada contra o schema Zod e a coerência de cada adapterId com um adapter que o sistema sabe construir. É a verificação direta da peça que você acabou de estudar.
# valida a FORMA do MODEL_REGISTRY + coerência de adapters · offline, $0, SEM rede $ alembic doctor --client-stack # saída esperada (offline — só a forma; passe --online p/ o smoke real no gateway): # doctor (client-stack): # [OK] model-registry: 11 model(s) validated; 0 invalid, 0 with unknown adapter # [OK] client-stack: offline mode: validated registry shape only; pass --online for live model smoke ($0 without it) # summary: 2 ok, 0 warn, 0 fail
11 model(s) validated = as 11 entradas do registro passaram no modelRegistryEntrySchema. 0 invalid = nenhuma quebrou a forma (os 5 campos obrigatórios). 0 with unknown adapter = todo adapterId (local, cliproxyapi) é um que o sistema sabe construir — um adapter desconhecido viraria um [WARN], não falha. O comando é fail-closed: qualquer [FAIL] faz o processo sair com código diferente de zero (o CI enxerga). Acrescente --online só quando o gateway cliproxyapi (127.0.0.1:8317) estiver de pé — aí ele roda um smoke de completação por modelo. [uncertain] os números exatos (11, summary: 2 ok) acompanham o estado de registry.ts no seu checkout; a forma da saída é a que o código produz.A fonte desses números é um único arquivo — os preços que o roteador e o guard usam vivem em packages/contracts/src/registry.ts (a forma é o contrato; ids e preços são dados que evoluem contra faturas reais, comentário da linha 54). Se quiser ver o fold e o guard exercitados por testes, sem nenhuma chamada de rede:
# o fold de roteamento (pickCheapestForTier) é coberto pelos testes de custo dos adapters $ pnpm --filter @alembic/adapters test # o BudgetGuard fail-closed (cap=Math.max(0,…), check/record) é coberto pelos testes da ETL $ pnpm --filter @alembic/etl test
cd alembic (a raiz do monorepo, onde fica o package.json de workspace).pnpm -r build — assim o CLI enxerga os .d.ts de @alembic/contracts e @alembic/etl.alembic doctor --client-stack. Procure na saída a linha model-registry: N model(s) validated e o summary: … 0 fail — é o sinal de que a forma do registro está sã, sem tocar a rede.packages/contracts/src/registry.ts, ache os dois modelos T3 (gpt-5.5-xhigh 0.06 e claude-opus-4-8-max 0.18, combinados) e confirme à mão: pickCheapestForTier(T3) tem de devolver gpt-5.5-xhigh. Confira contra a calculadora ao vivo (§06): escolha T3 e veja qual modelo a linha pickCheapestForTier(T3) reporta.cliproxyapi estiver de pé em 127.0.0.1:8317, rode alembic doctor --client-stack --online para o smoke de completação por modelo. Sem o gateway, o comando offline acima já prova a peça — e custa $0.Fixe os conceitos (flashcards)
Clique pra virar. Tente lembrar a resposta antes de virar — recuperação ativa fixa mais que reler.
DEFAULT_TIER = T4 (PARK) — trabalho não classificado estaciona, fail-closed.costPer1kInputUsd + costPer1kOutputUsd do tier. Fold puro, < estrito (mantém o 1º em empate) ⇒ determinístico.cap = Math.max(0, capUsd) → vira 0 = só free tier. Nunca "ilimitado". Toda chamada paga bloqueia (0 + qualquer > 0).costOf checa isFreeTierModel primeiro (budget.ts:110), antes de ler o campo. Medição é autoritativa.Revisão cumulativa — recupere de memória
Antes de clicar: responda de cabeça. As opções têm tamanho parecido de propósito — sem pista pela forma.
createBudgetGuard(0) (ou um teto negativo). O que pode rodar?cap = Math.max(0, capUsd) vira 0; check passa uma chamada só se projectedUsd === 0 (porta 1) ou se ela cabe no teto. Com teto 0, só grátis passa — fail-closed. a erra na porta 1: chamadas gratuitas sempre passam, mesmo com teto 0. b é o erro perigoso (fail-open) que o Math.max existe justamente para evitar — zero nunca vira ilimitado. d confunde os botões: o bloqueio é por preço (projectedUsd), não por tier — um T0 grátis e um LOCAL pago passam/bloqueiam pelo custo, não pelo degrau.0.0005 e 0.0009 por 1k. Sem modelId fixado, o que pickCheapestForTier(T2) retorna, e a escolha é determinística?reduce mantém a entrada com o menor costPer1kInputUsd + costPer1kOutputUsd. É puro sobre MODEL_REGISTRY (sem Date/random), então o mesmo registro sempre dá a mesma escolha — o que importa para replay (Lição 28). a inverte tudo: ele escolhe o menor, e nada ali é aleatório. c inventa uma medição de latência que a função não faz — ela só lê preços do registro. d confunde com unicidade: dois (ou mais) candidatos são o caso normal — o fold é justamente o que os compara.ModelRunResult de um modelo free-tier carrega um costUsd: 0.01 perdido. Como record o mede?costOf (budget.ts:110) retorna 0 para um modelo free-tier antes de ler costUsd. O preço é sempre aplicado a partir do tier/registro do modelo, então um campo perdido não pode inflar o total de gasto. a trataria a medição como consultiva — mas ela é autoritativa. b contradiz a regra fail-closed (o código não lança em biblioteca; devolve Result); ele simplesmente normaliza para 0. c também erra: a chamada não é ignorada — ela é medida, só que como 0.< estrito e não <= no fold?", "Como o piso de profundidade da ADR-0006 muda a escolha?", "O que acontece se eu fixar um modelId de outro tier?". É só dizer.