Ao fim desta lição você sabe por que TODA chamada de modelo do motor passa por ModelAdapter.run(), o que a espinha runWithGuards garante em cada tentativa, quais são as três pistas (gateway, local $0, CLIs agênticas) — e como cada token vira centavos auditáveis.
packages/adapters/src/adapter-core.ts — a espinha que todos os adapters compartilham
adapter-core.ts (guards), cliproxyapi.ts (o gateway default), local-openai.ts (a pista $0), cli-presets.ts (as CLIs) e cost.ts (a contabilidade) — esta lição destila os cinco.
run() nunca lança” e quem o garante.computeCostUsd na mão.O motor conversa com muitos cérebros — gateway multi-provedor, servidor local, CLIs de assinatura, fakes offline. Se cada chamador falasse com cada backend do seu jeito, teríamos N×M integrações e N×M jeitos de falhar. A solução é a cintura estreita: uma única interface, ModelAdapter, com um único método que importa: run(input): Promise<ModelRunResult>.
O contrato tem um superpoder (lição 02): run() NUNCA lança. Rede caiu, quota estourou, bug no adapter — tudo volta como ModelRunFailure com error.retryable tipado (model.ts:131-135). Quem chama nunca escreve try/catch; ramifica em result.ok. E como todos os adapters constroem o run pela MESMA espinha (runWithGuards), todos falham do mesmo jeito, com breaker e retry de graça.
Pense como… a cabine única de pedágio de uma ponte: não importa se você chega de caminhão (council), moto (CLI) ou ônibus (funil) — todo mundo passa pela mesma cancela, que confere o veículo, mede o peso e emite recibo ATÉ QUANDO nega a passagem. Onde quebra: um pedágio real deixa você dar ré; aqui a resposta negativa já vem com a instrução de nova tentativa (retryable) embutida.
A interface vive nos contracts (packages/contracts/src/model.ts:140-151): id, capabilities?, healthCheck?() (retorna false, nunca lança), run() e stream?() opcional. A espinha comum vive em packages/adapters/src/adapter-core.ts e o comentário de topo é explícito (linhas 23-36): validação de fronteira via Zod, invariante nunca-lança via failureFromThrown, gate opcional de CircuitBreaker com feedback correto, e withRetry guiado pelo flag retryable de cada resultado — “everything cross-cutting lives here so all six adapters behave identically”.
O roteamento (quem atende cada tier) é o pickAdapter/AdapterRegistry de registry.ts+router.ts deste pacote, alimentado pelo MODEL_REGISTRY dos contracts — o leilão da lição 02 acontece aqui na prática.
Um adapter concreto implementa só o miolo: attempt(input) — uma tentativa de rede/subprocesso. TODO o resto é a espinha compartilhada, na ordem: ① valida o input com modelRunInputSchema (inválido ⇒ falha CLIENT_ERROR, nem tenta); ② consulta o circuit breaker (canExecute() falso ⇒ falha CIRCUIT_OPEN, retryable, espera o cooldown); ③ executa a tentativa com try/catch de defesa (throw vira failureFromThrown); ④ realimenta o breaker (sucesso/falha) e deixa o withRetry decidir backoff pelo flag retryable.
guardedAttempt o converte (adapter-core.ts:97-101). E se o próprio withRetry rejeitar, um catch final preserva o invariante (adapter-core.ts:142-146). Não existe caminho em que uma exceção escape de run().| Pista | Adapter · endpoint | Quando usar |
|---|---|---|
| Gateway | cliproxyapi · http://127.0.0.1:8317 (cliproxyapi.ts:21) | o DEFAULT pago: fala /v1/chat/completions OpenAI-compatible com um proxy local que abre leque para os provedores (cliproxyapi.ts:12-18) |
| Local $0 | local · http://127.0.0.1:11434 (local-openai.ts:19-20) | a pista LOCAL/T0: LM Studio, llama.cpp, shim do Ollama, vLLM — mesmo wire format, preço $0 no registry. É por aqui que o deepseek-v4-flash (ds4, ALEMBIC_LOCAL_URL) serve o motor de graça (registry.ts:161-174) |
| CLIs agênticas | codex-cli · kimi-cli · grok-cli (cli-presets.ts:160-164) | assinaturas flat dirigidas headless como modelo: argv PROVADO na fronteira real (cli-presets.ts:20-27). T1/$0 marginal no registry — mas o leilão nunca cai nelas sozinho: alcance-as PINANDO o modelId (registry.ts:82-85) |
| Offline (fake) | offline.ts | testes herméticos e dry-runs: respostas canônicas, $0, zero rede — é o que mantém a suíte de 1.834 testes independente de qualquer serviço |
modelId roteado é o próprio id do preset (ex.: codex-cli), a flag de modelo é omitida e a CLI usa o modelo default dela — o motor não fabrica nomes de modelo upstream que a CLI poderia rejeitar (cli-presets.ts:84-92). Um modelId explícito diferente passa verbatim.ModelAdapter.run(). O que o chamador recebe?model.ts:131-135): QUALQUER erro — rede, timeout, quota, bug — vira ModelRunFailure com error.code/message/retryable. O chamador ramifica em result.ok; o withRetry da espinha já usa o retryable para decidir backoff sozinho (adapter-core.ts:102-111).Todo sucesso pode carregar usage (tokens) e costUsd. A conta é pura e auditável: computeCostUsd multiplica tokens/1000 pelas tarifas do registry e arredonda a 6 casas (roundUsd) para não acumular ruído binário em ledgers. Três honestidades embutidas:
costForModel devolve undefined quando o modelo não está no registry — o chamador omite costUsd em vez de reportar um zero enganoso (cost.ts:36-48).
parseUsage valida com tokenUsageSchema e devolve undefined em qualquer shape estranho — um bloco de usage ruim nunca lança nem inventa contagem (cost.ts:55-71).
accountFor(modelId, rawUsage) junta as duas: monta o par {usage?, costUsd?} do resultado — custo só aparece quando usage parseou E o preço existe (cost.ts:77-85). É essa dupla que o BudgetGuard do funil consome na lição 05.
A conta que transforma tokens em dólares — pura, sem IO, testável de cabeça:
export const computeCostUsd = (usage: TokenUsage, pricing: Pricing): number => { const inputCost = (usage.inputTokens / 1000) * pricing.costPer1kInputUsd; const outputCost = (usage.outputTokens / 1000) * pricing.costPer1kOutputUsd; return roundUsd(inputCost + outputCost); }; /** Round a USD amount to 6 decimal places (sub-cent precision). */ export const roundUsd = (amount: number): number => Math.round(amount * 1_000_000) / 1_000_000;
Conta de exemplo com as tarifas reais do registry: uma chamada T2 no gemini-3.5-flash (0.0001 in / 0.0004 out por 1k — registry.ts:179-180) com 2.000 tokens de entrada e 500 de saída custa (2000/1000)×0.0001 + (500/1000)×0.0004 = US$ 0,0004. Confira os testes de borda em packages/adapters/src/cost.test.ts — é lá que a regra “cheapest mudou? atualize o teste” do CLAUDE.md aterrissa.
# valida o catálogo + coerência dos adapters, offline, $0, SEM rede alembic doctor --client-stack # preflight REAL do gateway + smoke por modelo (fail-closed; precisa do proxy vivo) alembic doctor --online # o gateway default e sua sonda de vida (o que healthCheck faz por você) curl -s http://127.0.0.1:8317/v1/models -H "Authorization: Bearer $ALEMBIC_CLIPROXY_TOKEN" | head -c 200 # troubleshooting canônico (CLAUDE.md): "fetch failed" = gateway fora ou token ausente # → use --offline, ou suba o proxy e exporte ALEMBIC_CLIPROXY_TOKEN (fallback CLIPROXYAPI_API_KEY)
Quatro cartas para fixar a cintura estreita.
failureFromThrown, chamado no catch de guardedAttempt (adapter-core.ts:97-101) e no catch final de runWithGuards (142-146). Duas redes de segurança.cliproxyapi: 127.0.0.1:8317 (cliproxyapi.ts:21). Local $0: 127.0.0.1:11434 (local-openai.ts:20), com o ds4 servindo via ALEMBIC_LOCAL_URL.local-extract, e no empate o pickCheapestForTier mantém o 1º declarado (registry.ts:82-85) — a pista CLI só é alcançada pinando o modelId.costForModel devolve undefined e o resultado sai sem costUsd (cost.ts:36-48) — zero seria mentira contábil; ausência é honestidade.glm-5.2 (0.0002/0.0006 — registry.ts:190-191). Confira: US$ 0,0032. Teaser da lição 05: com contratos, ETL e adapters na mão, o harness monta o funil T0→T3 inteiro — com DOIS orçamentos independentes e o run que produziu os primeiros 1.022 learnings da história do motor.