Vazamento de Formato do Provedor (Provider Format Leak) é um smell de IA, ou seja, um sinal de problema em sistemas com LLM, do grupo Acopladores. O formato de mensagem de um provedor específico se espalhou pelo código de negócio.
Uma automação de back-office concilia pagamentos e emite notas. O time quer testar outro modelo, de outro provedor, numa das etapas. Uma busca no código mostra o tamanho do problema (anonimizado):
$ grep -rl "tool_use\|stop_reason\|content\[0\]" src/ | wc -l31src/billing/reconcile.py: if resp.stop_reason == "tool_use":src/billing/reconcile.py: block = resp.content[0]src/invoices/emit.py: args = msg.content[1].input["cnpj"]src/reports/daily.py: history.append({"role": "assistant", "content": resp.content})src/crm/sync.py: if part["type"] == "tool_result" and part["is_error"]:...
O formato de mensagem de um provedor específico (tipos de bloco, nomes de campo, motivos de parada) aparece em 31 arquivos de regra de negócio. Trocar o modelo de uma etapa exige mexer em todos.
No emissor de NF-e isso dói na hora: o módulo que monta o XML da nota lê resp.content[0].text, o de conciliação lê choices[0].message, e o job noturno grava o JSON cru da API no Postgres. Três jeitos de dizer “o modelo respondeu”, nenhum deles seu.
Sinais de Vazamento de Formato do Provedor:
Código de negócio lê campos específicos da resposta de um SDK (content[0], stop_reason, choices[0].message).
Ferramentas devolvem resultados no formato de bloco de um provedor.
Históricos de conversa são guardados no banco no formato de um provedor.
Testes dependem do formato da resposta bruta.
Trocar de modelo ou provedor é estimado em semanas.
flowchart LR
P(["1. SDK do provedor"]):::llm --> B1["2. reconcile.py"]:::tool
P --> B2["2. emit.py"]:::tool
P --> B3["2. sync.py"]:::tool
P --> DB[("3. Histórico no formato do provedor")]:::store
Um único SDK, com seu formato de mensagem.
Cada módulo de negócio lê esse formato diretamente.
Até os dados guardados ficam presos a ele.
Por que acontece
Começa com um provedor só. Usar o formato do SDK direto é o mais rápido. O primeiro tutorial pega choices[0].message.content e segue. Seis meses depois, aquilo está no faturamento, no CRM e no job de NF-e.
Copiar exemplos da documentação. O código de exemplo lê os campos do SDK no lugar onde a resposta é usada. Você cola o exemplo dentro de emit.py em vez de atrás de um cliente.
Sem camada de adaptação. Ninguém criou um ponto único que converte o formato externo no formato do sistema. Cada módulo fez a sua leitura “só desta vez”.
O banco virou arquivo morto do SDK. Gravar o JSON da API parece ótimo para debug. Um ano de conversas fica ilegível para outro provedor, e um campo renomeado na v3 da API quebra o replay dos testes.
A armadilha: adotar uma biblioteca que fala com vários provedores e, na primeira diferença, ler o raw_response. A abstração existe no package.json e morreu no if.
Quanto custa ignorar
Preso a um provedor. Comparar ou trocar modelos vira projeto. O eval de um modelo novo não é “apontar outra chave”: é uma refatoração.
Fragilidade a atualizações do SDK. Uma mudança de formato quebra vários módulos ao mesmo tempo. O changelog do provedor vira incidente interno.
Sem plano B. Quando o provedor fica fora do ar, não há como desviar para outro rapidamente. Veja Refém de um Provedor. Os dois smells andam juntos: aquele é não ter alternativa, este é o formato que impede a alternativa.
Testes difíceis. Simular respostas exige reproduzir o formato completo do provedor, com blocos, índices e flags. Um teste de conciliação não deveria saber o que é stop_reason.
OpenAI usa choices[0].message e finish_reason. Anthropic usa lista de blocos em content e stop_reason. Google tem outro envelope. Nenhum desses nomes deveria aparecer em invoices/emit.py.
O vazamento está na fronteira com modelos e ferramentas. Uma camada fina converte respostas e resultados para tipos do seu domínio, e só ela conhece o SDK.
As ferramentas estão escritas no formato de um provedor. Expostas por MCP (Model Context Protocol), elas funcionam com qualquer cliente que fale o protocolo. Destino: Adaptador MCP.
O cliente único devolve tipos seus: TextTurn, ToolCall, ToolResult, Stop.completed | Stop.tool | Stop.filtered. Histórico no banco guarda esses tipos. Cada adapter (Anthropic, OpenAI, um terceiro) traduz nas bordas.
function complete(req: DomainRequest) -> DomainResponse: # só este módulo importa o SDK raw = provider.sdk.messages(to_provider(req)) return from_provider(raw)
O resto do back-office chama complete. Trocar o modelo da conciliação é configuração mais um adapter, não 31 arquivos.
Isso não apaga recursos exclusivos. Se você usa um modo que só um provedor tem, isole esse modo num adapter específico e deixe o resto do domínio alheio. Não espalhe o campo exclusivo em cinco serviços “porque já está na resposta”.
O que você ganha
O SDK aparece em um módulo, não em trinta.
Testar outro modelo numa etapa vira uma mudança de configuração.
Dados guardados num formato seu, que sobrevive à troca de provedor.
Testes que simulam respostas no formato do domínio, mais simples.
Quando ignorar
Um script pequeno, com um provedor e sem plano de trocar. Uma camada de adaptação é peso morto. Se o script crescer para um serviço, a camada entra no primeiro refactor.
Recursos exclusivos de um provedor. Se você depende de um recurso que só um provedor tem, aceite o acoplamento nesse ponto e isole-o, em vez de esconder num formato genérico que não o representa.
É esse smell?
Perguntas rápidas
O que é Vazamento de Formato do Provedor?
Vazamento de Formato do Provedor (Provider Format Leak) é um smell de IA, ou seja, um sinal de problema em sistemas com LLM, do grupo Acopladores. O formato de mensagem de um provedor específico se espalhou pelo código de negócio.