Lição 04 · Fundamentos · Alembic × Hermes v3 · @alembic/adapters
Alembic × Hermes — O Curso de Fusão v3 · Visual Course

@alembic/adapters — a cintura estreita

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.

Leia primeiro (fonte primária)
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.

Você vai conseguir
  • Explicar o invariante “run() nunca lança” e quem o garante.
  • Narrar a esteira validate → breaker → attempt → retry de cor.
  • Escolher a pista certa: gateway 8317, local 11434 ou CLI de assinatura.
  • Calcular o custo de uma chamada com computeCostUsd na mão.
Leia a versão simples, ou abra a camada técnica em qualquer seção.
1

A grande ideia


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.

Por baixo do capô

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.

2

Em uma imagem


fusao · 14-the-narrow-waist · s0
ORIGENS (quem chama) CLI council swarm funil (harness) hermes/employee run(input): ModelRunResult NUNCA lança · Zod na entrada · ok discrimina cliproxyapi :8317 gateway default (pago) local :11434 · $0 LM Studio / Ollama / ds4 CLIs: codex·kimi·grok assinatura, $0 marginal offline (fake) testes herméticos $0
A ampulheta: cinco origens em cima, UMA cintura no meio, quatro famílias de backend embaixo. Trocar de cérebro nunca muda o chamador.
3

runWithGuards — a esteira de proteções


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.

fusao · 14-the-narrow-waist · s1
① validate (Zod) inválido ⇒ CLIENT_ERROR (sem tentativa, sem gasto) ② breaker.canExecute aberto ⇒ CIRCUIT_OPEN (retryable: espera cooldown) ③ attempt(input) throw? ⇒ failureFromThrown (defesa em profundidade) ④ withRetry backoff se retryable senão devolve já nova tentativa (jitter injetável p/ testes) sucesso ⇒ breaker.recordSuccess() · falha ⇒ breaker.recordFailure()
adapter-core.ts: validate (50-66), guardedAttempt com breaker + catch (82-111), runWithGuards amarrando tudo (118-147). O adapter concreto escreve SÓ a estação ③.
Defesa em três camadas contra o throw
O attempt “não deveria” lançar; se lançar, o catch de 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().
4

As três pistas (e a fake)


PistaAdapter · endpointQuando usar
Gatewaycliproxyapi · 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 $0local · 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ênticascodex-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.tstestes 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
Detalhe de honestidade nos presets: quando 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.
A rede cai no meio de uma chamada ModelAdapter.run(). O que o chamador recebe?
O invariante do contrato (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).
5

Contabilidade de custo (cost.ts)


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:

modelo desconhecido ⇒ custo OMITIDO

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).

usage malformado ⇒ NUNCA fabricado

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.

6

No código


A conta que transforma tokens em dólares — pura, sem IO, testável de cabeça:

packages/adapters/src/cost.ts:26-34 (trecho real, não editado)
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;

Acesse você mesmo

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.

Camada técnica — comandos desta seção
# 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)
7

Experimente


Quatro cartas para fixar a cintura estreita.

invariante
Quem converte um throw perdido em falha tipada?
clique para virar
failureFromThrown, chamado no catch de guardedAttempt (adapter-core.ts:97-101) e no catch final de runWithGuards (142-146). Duas redes de segurança.
pistas
Qual é a porta do gateway? E a da pista local?
clique para virar
Gateway 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.
presets
Por que o leilão T1 nunca cai nas CLIs sozinho?
clique para virar
Elas custam $0 como o 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.
custo
Modelo fora do registry: custo zero ou omitido?
clique para virar
Omitido. costForModel devolve undefined e o resultado sai sem costUsd (cost.ts:36-48) — zero seria mentira contábil; ausência é honestidade.
Exercício de mesa: calcule o custo de 10.000 tokens in + 2.000 out no 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.