Algumas pessoas leram meu post sobre como a primeira versão do GeminiOmni teve sua chave de API roubada em dezessete minutos e pediram a versão completa da arquitetura de proxy. Este é esse post.
Se você está construindo algo que chama uma API de IA paga — Gemini, OpenAI, Anthropic, Replicate, qualquer coisa com uma chave medida — o padrão abaixo é o que está entre você e uma surpresa de terça-feira de manhã vinda dos seus alertas de faturamento. Não é complicado. É essencial.
Vou detalhar a arquitetura, as quatro coisas concretas que você precisa acertar e os casos extremos que enfrentei executando isso em produção por um mês.
O padrão central, ilustrado
A forma é a mesma independentemente de qual provedor de IA você está usando:
┌─────────────────┐ ┌──────────────────────┐ ┌──────────────┐
│ │ │ │ │ │
│ Navegador │ │ Sua rota de API │ │ Provedor │
│ (não confiável) │ ───→ │ (servidor, tem chave)│ ───→ │ (Gemini / │
│ │ │ │ │ OpenAI...) │
│ - prompt │ │ - verificação de auth│ │ │
│ - upload │ │ - limite de taxa │ └──────────────┘
│ │ │ - verificação de cota│
│ │ │ - anexar chave API │
│ │ │ - chamar provedor │
│ │ │ - retornar resultado │
│ │ ←─── │ │
└─────────────────┘ └──────────────────────┘O ponto é que o navegador vê um prompt entrar e um resultado sair. Ele não tem ideia de qual modelo foi chamado, qual é a chave da API, como é o provedor upstream ou quanto a chamada custou. Esta é a quantidade certa de informação para o navegador ter.
Como "certo" parece, concretamente
Quatro coisas precisam ser verdade para que esta arquitetura realmente proteja você. Perder qualquer uma delas e a proteção desmorona.
1. A chave da API vive apenas no servidor
Em termos de Next.js: a variável de ambiente NÃO deve começar com NEXT_PUBLIC_. Em termos de tempo de execução: apenas código que roda em um manipulador de rota (app/api/.../route.ts), um componente de servidor ou uma ação de servidor toca na chave.
// app/api/ai/generate/route.ts — apenas servidor
const GEMINI_KEY = process.env.GEMINI_API_KEY; // ✅ apenas servidor
if (!GEMINI_KEY) throw new Error('GEMINI_API_KEY não definida');Uma verificação de inicialização que quebra o aplicativo se GEMINI_API_KEY estiver faltando vale seu peso em ouro. O pior modo de falha é o aplicativo rodar silenciosamente sem uma chave e cair em algum padrão que esvazia suas economias.
2. O navegador nunca vê respostas brutas do provedor
Quando a chamada do provedor retorna, você seleciona o que enviar de volta. O navegador NÃO precisa saber:
- Qual modelo exato foi chamado (Veo 3.1 Fast vs Veo 3.1 Standard)
- O ID da requisição do provedor (útil para suporte, inútil para clientes)
- Quaisquer cabeçalhos de limite de taxa do provedor
- A contagem de tokens ou o custo da chamada
Por que isso importa: a divulgação de informações se acumula. Se seu navegador pode ver "model: gemini-2.5-flash, tokens: 4096", um scraper pode mapear seu front-end para a página de preços do seu provedor e engenharia reversa sua economia unitária. Isso não é catastrófico, mas não é bom. Remova.
// Retornar apenas o que a UI precisa
return Response.json({
imageUrl: result.generated_images[0].url,
watermarked: !user.isPro,
// Não retornar: nome do modelo, tokens usados, resposta bruta do provedor
});3. Verificações de autenticação, limite de taxa e cota acontecem ANTES da chamada ao provedor
O proxy não é apenas um pass-through. É o lugar onde você aplica as regras:
// Dentro da rota da API, em ordem:
1. Autenticar a requisição (cookie de sessão, JWT, o que for).
Requisições anônimas são permitidas para recursos gratuitos, mas rastreadas por IP.
2. Limitar taxa por ID do usuário (ou IP para anônimos).
GeminiOmni: 10 req/min anônimo, 60 req/min Pro, 200 req/min Equipe.
3. Verificar a cota do usuário para a operação específica.
Usuários gratuitos têm 5 gerações de vídeo por mês — verificar o banco de dados ANTES
da chamada cara, não depois.
4. Só agora — chamar o provedor.
5. Incrementar contadores de uso em caso de sucesso.A ordem importa. Se você chamar o provedor primeiro e depois verificar a cota, você já pagou pela inferência independentemente de o usuário ter direito ou não. Toda verificação de cota acontece antecipadamente.
4. O proxy tem sua própria observabilidade
Registre cada chamada ao provedor a partir do proxy com: timestamp, ID do usuário, rota, modelo, tamanho da entrada, tamanho da saída, latência, sucesso/falha e o custo aproximado em dólar. Este é seu recibo de faturamento e sua superfície de debug.
await logUsage({
userId,
route: '/api/ai/generate',
model: 'gemini-2.5-flash-image',
inputBytes: prompt.length + imageBytes,
outputBytes: result.length,
latencyMs: Date.now() - start,
costUsd: estimateCost(model, inputBytes, outputBytes),
status: 'success',
});Quando seu alerta de faturamento de terça-feira de manhã disparar porque os gastos dobraram da noite para o dia, esta tabela é o que você consulta em SQL para descobrir qual usuário, qual rota, qual modelo. Sem ela, você está chutando.
Casos extremos que enfrentei executando isso em produção
Um mês executando o proxy em produção trouxe à tona algumas coisas não óbvias.
Respostas em streaming precisam de proxies de streaming. O endpoint streamGenerateContent do Gemini retorna um stream de gerações parciais. Se você ingenuamente chamar response.json() em seu proxy, você vai armazenar em buffer todo o stream e perder o benefício da latência. A correção é canalizar o stream upstream através do seu proxy como um ReadableStream, o que significa que sua rota de API retorna new Response(stream) em vez de Response.json(...). Vale a pena fazer — a latência percebida na geração de vídeo cai de "espere 90 segundos" para "veja algo acontecendo imediatamente."
Uploads de arquivos grandes precisam de uploads diretos para armazenamento. Se um usuário fizer upload de um vídeo de 50MB para geração de imagem para vídeo, você não quer que esses 50MB transitem pela sua rota de API do Next.js. O padrão é: cliente faz upload diretamente para Cloud Storage / R2 via uma URL pré-assinada, depois envia apenas a URL para seu proxy. O proxy passa a URL para o parâmetro "media URI" do Gemini, e os bytes nunca tocam seu servidor. Isso reduz sua fatura de egress em ~95% nos caminhos pesados de upload.
Erros do upstream precisam de tradução. O Gemini retorna erros como "PROMPT_BLOCKED" ou "QUOTA_EXCEEDED" ou "INVALID_ARGUMENT". O navegador não precisa ver essas strings — elas são confusas e vazam detalhes sobre o provedor. Traduza cada uma para uma mensagem voltada ao usuário: "Seu prompt foi bloqueado pelos filtros de segurança", "Você já usou suas gerações gratuitas deste mês", "Essa imagem parece corrompida." Tabela de consulta de três linhas.
Estimativa de custo é por chamada, não por token. Tentei estimar o custo em dólar de cada chamada contando tokens. Então notei que o faturamento do Veo é por SEGUNDOS de vídeo de saída, do Imagen é por IMAGEM, e o Nano Banana 2 tem um preço de quatro níveis baseado na resolução. A função de custo para o proxy é uma instrução switch, não uma multiplicação. Não há uma maneira limpa de contornar isso — cada provedor cobra de forma diferente e você tem que codificar cada um.
Como isso se parece no código do GeminiOmni
Toda chamada de modelo em /tools/text-to-video, /tools/image-to-video, /tools/nano-banana-edit, /tools/pdf-chat e no chat ao vivo em /chat passa por app/api/ai/generate/route.ts e app/api/ai/query/route.ts. Dois endpoints. Cerca de 400 linhas de código juntos. As quatro verificações acima são aplicadas antes de qualquer chamada ao provedor sair do meu servidor.
Nomeei o modelo na UI voltada ao usuário, mas não na resposta da API. O selo abaixo de cada clipe gerado diz "Veo 3.1 Fast" — isso vem de uma consulta do lado do servidor no momento da renderização, não da resposta da API. Este é o tipo de coisa que parece boba até você perceber que qualquer string que a API retorna está a um screenshot de distância de ser um alvo.
O resumo honesto
O padrão de proxy não é empolgante. Não há algoritmos inteligentes nele. É apenas o mínimo necessário para executar uma API de IA paga na internet pública sem perder dinheiro para pessoas que não pagaram.
Mas é o mínimo. Todo criador independente que conheço que lançou sem isso aprendeu a lição da mesma forma: um alerta de faturamento em uma hora inconveniente, uma revogação em pânico da chave e uma longa noite reescrevendo o fluxo de dados. Mais barato fazer isso no primeiro dia.
Leia a seguir
- A primeira coisa que quebrou quando lancei o GeminiOmni
- Contexto de 1M de tokens não é grátis — custo real por página do chat PDF do Gemini
- Preços do GeminiOmni — como o proxy molda os níveis
— Lena
