ADR 0014 — Superfície Anthropic-compatible no gateway (POST /v1/messages)
Status: Accepted (2026-08-30, horário da Flórida)
Contexto: plan 0360, epic garra agents setup
Supersedes: nada. Superseded by: nada.
Contexto
O comando garra agents setup configura quatro agentes com um mesmo par
provedor+modelo. Três deles (GarraIA, OpenClaw, Hermes) aceitam um provedor
OpenAI-compatible e um modelo arbitrário. O Claude Code não: ele fala
exclusivamente o wire da Anthropic Messages API, e tem um único campo de
modelo (ANTHROPIC_MODEL), sem slot de fallback.
Havia três caminhos:
- Deixar o Claude Code fora da configuração unificada, na autenticação Anthropic nativa.
- Apontar
ANTHROPIC_BASE_URLdireto para a OpenRouter. - Apontar
ANTHROPIC_BASE_URLpara o gateway do GarraIA, e o gateway traduz.
Decisão
Opção 3. O gateway ganha POST /v1/messages e
POST /v1/messages/count_tokens, traduzindo o wire da Anthropic para o
LlmRequest interno e de volta.
Isso dá ao Claude Code duas coisas que ele não tem sozinho: qualquer provedor
que o GarraIA saiba falar, e failover primário→backup, porque o gateway já
implementa complete_with_fallback / stream_complete_with_fallback com
circuit breaker (GAR-210).
Consequências
Aceitas
- A instalação do Claude Code deixa de usar a autenticação de assinatura
Anthropic. O uso passa a ser cobrado por token no provedor configurado. Para
quem tem Pro/Max isso é uma troca de tarifa plana por consumo, e precisa estar
dito no wizard, no painel e aqui.
agents setupavisa;--skip claude-codee a página Providers deixam reverter. - Prompt caching não é repassado. O
cache_controlque o Claude Code manda nos blocos desystemé tolerado e descartado. Em conversa longa isso multiplica o custo de entrada. - O gateway vira dependência de runtime do Claude Code. Com ele fora do ar, aquele Claude Code não responde.
- Superfície local de proxy de credencial. Um
/v1/messagesem 127.0.0.1 que faz proxy de uma chave paga é escalada de privilégio para qualquer processo local. Mitigação: o gateway já tem camada de auth em/v1/*, e o bind é loopback por padrão. Antes de qualquer bind fora do loopback, exigir token — mesma regra que vale para as rotas A2A.
Invariantes de implementação
- É proxy, não agente. O caminho não passa por
AgentRuntime::process_message_*, que injeta as tools do próprio GarraIA e as executa. O Claude Code precisa dos blocostool_usecrus de volta para rodar o próprio Read/Edit/Bash; passar pelo runtime significaria que ele nunca conseguiria editar arquivo nenhum. - Sem hidratação de sessão. A API da Anthropic é stateless e o cliente reenvia a conversa inteira a cada turno. Reusar o histórico do gateway duplicaria contexto.
stop_reason: "tool_use"quando há blocotool_use. Decidido pela presença do bloco, não só pelofinish_reasondo provedor — os modelos da OpenRouter são inconsistentes nesse campo. Errar aqui faz o Claude Code imprimir as tools em vez de executá-las.- Eventos SSE nomeados. O stream OpenAI-compatible do gateway usa
Event::default().data(...)sem nome; clientes Anthropic despacham pelo nome do evento. usagenunca zero. Omessage_startalimenta o medidor de contexto e o gatilho de auto-compact do cliente. Zero reportado é tratado como "não sei" e substituído por estimativa (chars/4); repassar zero faria o transcript crescer sem limite até o provedor recusar por contexto.- Streaming sintetizado quando o provedor não streama. O
OllamaProvidernão implementastream_completeno trait — e Ollama é justamente o backup padrão. O envelope SSE é montado sobrecomplete()nesse caso. - Nunca registrar
GET /v1/modelsaqui.build_openai_routerjá registra essa rota e o Axum entra em pânico no boot com método+path duplicados.
Alternativas rejeitadas
- (1) Deixar o Claude Code de fora — preserva assinatura e caching, mas contradiz o pedido de "configurar todos simultaneamente" e deixa o agente mais usado fora do painel.
- (2) Apontar direto para a OpenRouter — funciona, mas dá ao Claude Code
zero failover (campo de modelo único) e coloca a chave da OpenRouter dentro de
~/.claude/settings.json, somando mais uma cópia do segredo.