One Door to the Models, Parte 3: A Abstração de Provider e o Streaming
Com stream em true, a contagem de tokens vira estimativa. Abra um WebSocket e o load balancing deixa de existir. A Parte 3 mapeia garantias por transporte.

As garantias do gateway não são propriedades do gateway. São propriedades do transporte que o chamador escolheu, e ficam mais fracas a cada passo em direção ao tempo real. Uma chamada request-response simples é medida a partir do bloco usage que o modelo de fato retornou. A mesma chamada com stream: true tem seus prompt tokens estimados independentemente do que a policy diga, e seus completion tokens estimados também. Uma conexão WebSocket não pode passar por load balancing entre backends de forma alguma, porque, uma vez estabelecida, ela fica fixada um-para-um entre cliente e backend. A Parte 2 construiu a infraestrutura. Esta parte coloca uma API sobre ela, e a versão honesta dessa API é uma que diz a cada chamador de quais garantias ele acabou de abrir mão.
O que um time consumidor deveria precisar saber
Exatamente três coisas: uma base URL, uma client credential e um nome de modelo que signifique algo para o time, não para o Azure. Nada sobre deployments, regiões, providers, ou qual deles mudou na última terça-feira. Esse é o contrato inteiro, e tudo nesta parte existe para mantê-lo verdadeiro enquanto as coisas por trás dele se movem.
A unified model API do API Management foi construída exatamente para isso. Os clientes falam o formato OpenAI Chat Completions, e o gateway traduz para o formato de backend que o modelo de destino realmente usa. Ela suporta dois formatos de API de backend, OpenAI Chat Completions e a Anthropic Messages API, e faz quatro coisas que valem ser nomeadas separadamente: padroniza o formato voltado ao cliente de forma independente dos backends, aplica um único conjunto de policies de governança entre providers, configura failover entre providers, e desacopla os nomes de modelo voltados ao cliente dos nomes de modelo do backend usando aliases.
O alias é a parte que mais importa aqui, e é a continuação direta da Parte 2. Um model deployment fixado em gpt-4.1 versão 2025-04-14 com NoAutoUpgrade só é útil se nenhuma aplicação jamais o nomear. As aplicações chamam chat-default. O alias mapeia chat-default para um deployment, e o dia em que esse mapeamento muda é uma mudança de configuração do gateway revisada em um pull request, não um release coordenado entre cinco times.
Duas ressalvas pertencem a este lugar, não a um ticket de suporte. A unified model API está em preview e em rollout; nos tiers clássicos, o acesso antecipado passa pelo AI Gateway Early release channel, que é uma configuração de atualização por instância. E seus dois formatos de backend suportados não cobrem tudo o que a empresa roda. Deployments de Mistral e Llama no Microsoft Foundry são alcançados pela superfície de chat completions compatível com OpenAI, que a unified API consegue expor como um backend em formato OpenAI, mas esse é um caminho de integração diferente da tradução nativa da Anthropic, e vale registrar quais modelos chegam por qual rota antes de alguém depurar um shape mismatch às 2 da manhã.
Roteamento: grupos de prioridade primeiro, pesos depois
Atrás de um alias fica um backend pool, não um backend. Os pools do API Management suportam distribuição round-robin, ponderada, baseada em prioridade e com session awareness, e o padrão útil para tráfego de modelos combina as duas primeiras: um grupo de prioridade de deployments com Provisioned Throughput que deve absorver tudo o que conseguir, e um grupo de prioridade mais baixa de deployments pay-as-you-go que só vê tráfego quando o primeiro grupo está indisponível. Dentro de um grupo, os pesos dividem a carga.
Os pools são configurados pelo portal, pela REST API, ou por um template ARM ou Bicep. A Parte 2 escolheu Terraform, e este é precisamente o caso que aquela parte descreveu como legítimo para a válvula de escape do azapi: um recurso, um corpo ARM cru, ao lado de recursos azurerm e referenciando-os por ID.
resource "azapi_resource" "chat_pool" {
type = "Microsoft.ApiManagement/service/backends@2023-09-01-preview"
name = "chat-pool"
parent_id = azurerm_api_management.gw.id
# azurerm has no backend-pool schema yet. Revisit when it does.
body = {
properties = {
description = "PTU first, PAYG overflow"
type = "Pool"
pool = {
services = [
{ id = azapi_resource.be_ptu_west.id, priority = "1", weight = "1" },
{ id = azapi_resource.be_payg_west.id, priority = "2", weight = "3" },
{ id = azapi_resource.be_payg_north.id, priority = "2", weight = "1" },
]
}
}
}
}
A prioridade 1 é o deployment PTU: ele já está pago por hora, então cada token que ele consegue servir é um token que não custa nada a mais. A prioridade 2 só recebe tráfego quando a prioridade 1 não consegue absorvê-lo, e o peso de 3 para 1 entre duas regiões reflete quota, não preferência. Isso é uma topologia de custo expressa como roteamento, que é o tipo de coisa invisível em um diagrama de arquitetura e óbvia em uma fatura.
O circuit breaker, e quanto confiar nele
Um membro de pool que está falhando deveria parar de receber tráfego sem um humano decidindo isso. O API Management expõe um circuit breaker como propriedade do backend: as regras definem uma contagem ou porcentagem de falhas dentro de um intervalo e as faixas de status code que contam como falha, e quando o breaker dispara, o gateway para de chamar aquele backend pela duração do trip e retorna 503 Service Unavailable ao cliente. Se o backend enviou um header Retry-After, o breaker pode aceitar esse valor e esperar exatamente esse tempo, o que, para um endpoint de modelo sob rate limit, é muito melhor do que um palpite fixo.
circuitBreaker = {
rules = [{
name = "ptu-5xx"
failureCondition = {
count = 3
interval = "PT1H"
statusCodeRanges = [{ min = 500, max = 599 }]
}
tripDuration = "PT1H"
acceptRetryAfter = true
}]
}
Duas propriedades desse recurso decidem como você tem permissão de raciocinar sobre ele. Primeiro, as regras de trip são aproximadas: o gateway é distribuído, as instâncias não sincronizam o estado do breaker, e cada uma aplica a regra com a informação que tem. Um breaker configurado para disparar após três falhas pode, na prática, deixar passar consideravelmente mais em uma instância escalada horizontalmente. É um mecanismo de proteção, não um contador, e nenhum cálculo de SLO deveria ser construído sobre seu limiar exato. Segundo, ele não está disponível no tier Consumption, que é mais uma razão pela qual a decisão de tier da Parte 2 não era sobre custo.
O 503 merece ser projetado de forma deliberada. Um chamador que recebe 503 do gateway não consegue dizer se o modelo caiu, se o breaker está aberto ou se a instância inteira está doente, e os três merecem comportamentos de cliente diferentes. Adicionar um response header na policy de outbound que nomeia qual backend foi tentado, e logar o estado do breaker, transforma um 503 opaco em algo sobre o qual um time consumidor pode agir sem abrir um ticket.
Streaming, e a contabilidade que ele custa
Todo time consumidor pede streaming, e com razão. Time to first token é o número que um humano percebe; a latência total é o número que um dashboard mostra. Server-sent events sobre o mesmo endpoint HTTP é a resposta padrão, o cliente define stream: true, e nada muda no roteamento do gateway.
O que muda é o medidor. A policy de limite de tokens normalmente trabalha a partir do usage real: com estimate-prompt-tokens="false" ela lê a seção usage da resposta do modelo, o que significa que uma requisição pode exceder o limite e ser detectada depois, bloqueando as requisições seguintes até a janela resetar. Com a estimativa habilitada, ela conta os prompt tokens a partir do schema da definição da API antes da chamada, trocando um pouco de precisão por não queimar quota de backend para descobrir que você passou do limite.
O streaming remove a escolha. Quando stream: true está definido, os prompt tokens são sempre estimados independentemente da configuração da policy, e os completion tokens são estimados também. Não existe configuração que faça uma requisição em streaming ser medida a partir da verdade de fato no gateway. Para modelos que aceitam imagens, o desvio se acumula: com streaming habilitado, ou com estimativa ligada, a policy superconta os tokens de imagem em vez de usar a contagem do backend.
Isso não é uma razão para recusar streaming. É uma razão para ser preciso sobre o que os números significam, porque a Parte 5 os transforma em dinheiro:
- HTTP sem streaming: medido a partir do próprio
usagedo modelo. Exato. - SSE com streaming: prompt e completion tokens estimados no gateway. Bom o bastante para rate limiting, aproximado para cobrança.
- WebSocket: veja abaixo. Efetivamente fora de banda.
A resposta do control plane é reconciliação, não uma estimativa melhor. As métricas do gateway continuam sendo o sinal em tempo real que aplica as quotas, e o job mensal de atribuição que a Parte 5 constrói corrige contra o usage autoritativo que o provider reportar, com o delta rastreado como métrica própria. Um desvio que cresce é um bug; um desvio que fica estável em poucos por cento é o custo do streaming, e deveria ser escrito no documento de chargeback, não descoberto por um product owner.
WebSockets, onde a abstração termina
Tráfego de voz em tempo real e speech-to-speech não cabe em request-response, e o API Management suporta APIs WebSocket. As limitações são afiadas o suficiente para mudarem a arquitetura em vez de decorá-la.
Conexões WebSocket não podem ser distribuídas nem passar por load balancing entre múltiplos backends. Uma vez estabelecida, cada conexão é mantida um-para-um entre cliente e backend. Toda decisão de roteamento deste post, os grupos de prioridade, a topologia PTU primeiro, a divisão regional ponderada, se aplica ao handshake inicial e a nada depois dele. Um backend que degrada no meio da sessão não pode sofrer failover pelo gateway; a sessão tem que terminar e o cliente tem que reconectar. Qualquer cliente nesse caminho precisa de lógica de reconexão com backoff, e qualquer modelo de capacidade precisa tratar uma sessão WebSocket como uma reserva fixada, não como um fluxo de requisições roteáveis de forma independente.
Três restrições menores vêm em seguida. Uma longa lista de policies não pode ser aplicada à operação onHandshake, incluindo caching, CORS, manipulação de body e toda policy validate-*, e policies herdadas de um escopo global ou de produto são puladas em tempo de execução em vez de falharem de forma audível. A policy set-header não pode alterar certos headers bem conhecidos, incluindo Host, em requisições de handshake. E a validação de TLS é mais rígida do que em APIs HTTP: para um backend WebSocket, o gateway valida que o certificado do servidor é confiável e que o subject name bate com o hostname, enquanto uma API HTTP exige apenas a confiança. Um backend com subject de certificado divergente que funcionou por anos sobre HTTP vai falhar no momento em que for exposto como uma API WebSocket.
A consequência prática: o caminho de tempo real ganha sua própria API, seu próprio produto e sua própria quota, e a documentação do consumidor diz com clareza que ele não é coberto pelas garantias de failover que o endpoint de chat tem. Fingir o contrário é como um incident review descobre a diferença.
Session affinity, e por que evitar precisar dela
Os pools suportam session awareness por meio de um cookie, com o cliente armazenando um valor de Set-Cookie e devolvendo-o para que as requisições seguintes alcancem o mesmo backend. Isso existe para APIs stateful do lado do servidor, sendo a Assistants API o exemplo canônico, em que o cliente tem que manter um session ID, extrair um thread ID do body da resposta e enviar o cookie certo na chamada certa.
Funciona, e é uma restrição que vale evitar em tempo de design. Uma chamada com session affinity é uma chamada que não pode ser reroteada, o que a coloca na mesma categoria do caminho WebSocket: sem failover, e com um modelo de capacidade em que a carga de um backend depende de quais sessões caíram nele, não de quantas requisições chegaram. Onde uma API stateful for genuinamente necessária, mantenha-a em um alias separado para que suas garantias mais fracas não sejam herdadas em silêncio pelo tráfego stateless que compõe o resto do volume.
Modos de falha para observar
- O 503 que significa quatro coisas diferentes. Breaker aberto, todos os membros do pool esgotados, gateway doente, ou um backend genuinamente falhando. Sem um header ou uma linha de log nomeando qual, cada um deles vira um ticket.
- Desvio de streaming tratado como bug. É comportamento documentado. A falha não é o desvio, é publicar um número estimado em um lugar que diz fatura.
- Uma funcionalidade em preview no caminho crítico. A unified model API está em preview. Isso é aceitável para a camada de tradução apenas se o fallback, uma API direta por provider atrás do mesmo produto e das mesmas policies, já existir e estiver testado, em vez de ser projetado durante o incidente.
- Policies puladas em silêncio em APIs WebSocket. Policies herdadas que não são suportadas em
onHandshakesão puladas em tempo de execução. Um controle de segurança presumido como global não é global aqui, e nada vai avisar você.
O que a Parte 4 herda
Ao final desta parte, um time consumidor tem um endpoint, um formato e aliases de modelo estáveis, com roteamento e failover por trás deles e uma declaração documentada de quais garantias se aplicam a qual transporte. O que nada disso trata é o trabalho que não precisa de resposta agora: a sumarização noturna de tickets de suporte que, de outra forma, ficaria no mesmo caminho síncrono de um cliente esperando o primeiro token. Isso é uma fila, e ela vem a seguir.
Leia isso a seguir
- Parte 4, Trabalho Assíncrono Fora do Caminho da Requisição, onde o trabalho de batch e de queue sai do caminho síncrono, e a batch API se revela uma segunda porta sem nenhuma policy na frente.
- Respostas em Streaming São uma Decisão de UX, Não de Performance, a outra metade do argumento do streaming: o que ele compra para a pessoa que espera.
- Apps LLM Multi-Tenant: Isolando Clientes em um Modelo Compartilhado, a pergunta de tenancy que este gateway responde com produtos e quotas em vez de stacks separadas.
Para o lado de infraestrutura e plataforma de rodar isso em escala, as notas de campo estão em ercan.cloud, e o hub fica em ercanermis.com.
Referências
Mais de Ercan
Mais dois sites, mesmo autor, terreno diferente.
Cloud, AWS, EKS, Terraform, engenharia de plataforma.
Notas de campo de sistemas em produção. EKS, IAM, Terraform em escala organizacional, observabilidade, otimização de custos.
Visitar ercan.cloud →O hub. Sobre, consultoria, contato.
Hub pessoal para as duas trilhas de escrita. Quem sou eu, como funciona a consultoria, como me contatar.
Visitar ercanermis.com →