/v1/chat/completions compatível com OpenAI, aceita seus próprios bearer tokens, encaminha requisições para a Venice, suporta respostas em streaming e emite spans e métricas úteis do OpenTelemetry.
Interessado na implementação completa do código? Confira o repositório no GitHub.
Pré-requisitos
- Rust 1.92+
- Docker e Docker Compose
- Uma chave de API da Venice
curl- Familiaridade básica com serviços web em Rust
O que vamos construir
A implementação de referência é um pequeno serviço em Rust com algumas partes bem definidas:| Parte | O que faz |
|---|---|
| Router Axum | Serve /healthz e /v1/chat/completions |
| Extractor de autenticação | Valida bearer tokens do gateway contra chaves com hash no Postgres |
| Rate limiter | Usa janelas fixas armazenadas no Postgres |
| Cliente Venice | Faz proxy de requisições de chat completion streaming e não-streaming |
| Telemetria | Registra spans GenAI, uso de tokens, custo de cobrança da Venice, latência e tempos de streaming |
| Docker Compose | Roda o Postgres e o gateway localmente |

Criando o serviço em Rust
Comece com um novo projeto de binário Rust:Cargo.toml — explicações adicionadas no snippet de código:
Carregando a configuração
O gateway lê tudo a partir de variáveis de ambiente. Para código de infraestrutura como este, variáveis de ambiente são um bom padrão porque o mesmo binário pode rodar localmente, no Docker Compose ou em um ambiente hospedado sem um formato de arquivo de configuração separado. Além disso, muitos provedores permitem que você armazene suas próprias variáveis de ambiente como segredos no runtime de contêiner deles. Isso costuma ser muito mais seguro do que tentar usar algo comodotenv (ou dotenvy em Rust, já que a crate original dotenv está em grande parte descontinuada).
Crie src/config.rs:
- A URL do banco de dados
- Sua chave de API da Venice
VENICE_BASE_URL aponta para https://api.venice.ai/api/v1, e CAPTURE_GENAI_CONTENT tem padrão false para que o conteúdo dos prompts não seja registrado a menos que você intencionalmente o habilite.
Esse segundo padrão é o mais importante. Um gateway pode ver cada prompt e resposta fluindo por ele, mas a observabilidade não deve automaticamente virar captura de conteúdo. Na maioria dos sistemas de produção, contagens de tokens, latência, nomes de modelos, códigos de status e metadados de cobrança são suficientes para operações. De modo geral, registrar prompts e conversas em produção pode não ser apenas um risco de privacidade — também pode ser um risco de armazenamento. Adicioná-los significa criar spans e traces com um nível extremamente alto de cardinalidade (ou seja, a unicidade dos dados dentro de um conjunto de dados). Isso pode tornar a busca através dos seus dados de observabilidade muito cara, além de potencialmente prejudicar o desempenho ao fazer buscas nos dados.
Criando o schema do banco de dados
Em seguida, criemigrations/0001_api_keys.sql.
Vamos armazenar apenas os primeiros 12 caracteres de cada chave de API do gateway como um prefixo de lookup, mais o hash SHA-256 da chave completa. Isso permite que o gateway encontre uma linha candidata rapidamente sem armazenar credenciais brutas.
O prefixo não é secreto. Ele existe para indexação. O hash é o que prova que o chamador apresentou a chave completa. Essa é a mesma forma básica usada por muitos sistemas de chaves de API: mostre a chave bruta uma vez, armazene uma representação não reversível e mantenha um prefixo curto por perto para lookup e fluxos de suporte.
- Chaves de API nunca são armazenadas em texto puro.
- Configurações de rate limit precisam ser positivas.
- Chaves revogadas não podem permanecer ativas.
- Uma janela de rate limit é identificada unicamente por chave, tempo de início e duração da janela.
Construindo o cliente Venice
Em seguida, vamos criarsrc/venice.rs. O cliente só precisa saber a URL upstream de chat completions, a chave de API da Venice e quantas vezes tentar novamente falhas transitórias.
Manter esse wrapper pequeno é intencional — o gateway não deve reimplementar toda a API da Venice. Em um nível básico, o trabalho do gateway é anexar a credencial do lado do servidor, aplicar um timeout, tentar novamente requisições que são seguras para retentativa e retornar a resposta upstream em uma forma que o router possa encaminhar.
EventSource a partir da mesma requisição:
serde_json::Value. Essa é uma escolha deliberada de compatibilidade. Se modelássemos todos os campos possíveis de chat-completion em Rust, teríamos que manter o gateway atualizado toda vez que a API upstream adiciona uma opção útil. Ao fazer parse apenas do que precisamos em outros lugares, deixamos parâmetros mais novos da Venice passarem sem uma nova versão do gateway.
Compartilhando estado da aplicação
Criesrc/state.rs:
PgPool já é um handle de pool compartilhado, e Arc<Config> também mantém a configuração barata.
Isso dá a cada handler acesso às mesmas três coisas: configuração imutável, conexões de banco de dados em pool e o cliente Venice. Mantê-los em um único AppState também torna os testes mais simples depois, porque handlers recebem suas dependências através do state do Axum em vez de ler globais.
Autenticando chaves de API do gateway
O cliente envia sua chave do gateway assim:src/auth.rs e implemente um extractor do Axum. O extractor permite que handlers protegidos declarem que exigem uma chave autenticada:
- Fazer o parse do bearer token.
- Pegar os primeiros 12 bytes como o prefixo da chave.
- Fazer hash do token candidato completo com SHA-256.
- Carregar a linha da chave ativa pelo prefixo.
- Comparar o hash armazenado e o hash candidato em tempo constante.
AuthenticatedApiKey não pode acidentalmente pular a autenticação dentro do corpo da função; o Axum precisa construir esse valor antes que o handler rode. Isso mantém o caminho protegido fácil de auditar.
Adicionando rate limits de janela fixa
Criesrc/rate_limit.rs. O rate limiter usa uma única instrução SQL para inserir uma nova janela ou incrementar a existente:
WHERE api_key_rate_limit_windows.request_count < $3 é a parte importante. Quando a janela já está cheia, o Postgres não atualiza a linha e RETURNING não produz nenhuma linha. O handler pode transformar isso em uma resposta 429 Too Many Requests com um header Retry-After.
Uma janela fixa não é o rate limiter mais sofisticado, mas é fácil de explicar, fácil de inspecionar e bom o suficiente para um tutorial de gateway. O trade-off é que o tráfego pode se acumular ao redor dos limites das janelas. Se você precisar de comportamento mais suave em escala, um token bucket ou um limitador de janela deslizante apoiado por Redis é um próximo passo natural.
Retornando erros no estilo OpenAI
Criesrc/error.rs e faça os erros da aplicação implementarem IntoResponse:
Construindo o router
Agora podemos conectar as rotas HTTP emsrc/router.rs:
AuthenticatedApiKey. Se a autenticação falhar, o Axum nunca entra no corpo do handler:
model, messages e stream. Todo o resto no corpo JSON passa direto para a Venice. Isso mantém o gateway compatível com recursos do provedor que você pode querer usar mais tarde.
O handler também torna os dois modos de resposta explícitos. Requisições não-streaming aguardam a Venice retornar uma resposta JSON completa e, então, registram metadados de resposta antes de enviar os bytes para o cliente. Requisições em streaming retornam imediatamente com um corpo text/event-stream apoiado por um stream assíncrono. Essa divisão mantém o caminho não-streaming simples enquanto dá ao caminho de streaming controle suficiente para observar chunks conforme eles passam.
Suportando respostas em streaming
Chat completions em streaming usam server-sent events. A Venice envia dados SSE, e o gateway repassa esses dados de volta para o cliente. O gateway deve evitar bufferizar o stream inteiro porque isso derrotaria o propósito do streaming. Usuários se importam com o tempo até o primeiro token, não apenas com o tempo até o token final. Ao encaminhar cada evento upstream conforme ele chega, os clientes podem renderizar saída parcial enquanto o modelo ainda está gerando. Criesrc/sse.rs:
data: ..., e o stream termina com data: [DONE].
O observador de stream também é onde podemos coletar metadados sem mudar o que o cliente vê. Cada chunk é encaminhado no formato SSE, mas o gateway ainda pode observar IDs de resposta, motivos de encerramento, uso de tokens, campos de custo e informações de tempo conforme esses chunks passam.
Registrando telemetria GenAI
Gateways são úteis porque cada requisição passa por um único lugar. Isso os torna um ótimo ponto para registrar modelo, latência, uso de tokens, motivos de encerramento, custo de cobrança e tempos de streaming. Criesrc/telemetry.rs e comece fazendo o parse da requisição:
id da resposta da Venice:
Iniciando o servidor
Agora conecte tudo emsrc/main.rs:
- Lê a configuração.
- Inicializa a telemetria.
- Conecta ao Postgres.
- Roda as migrações do SQLx.
- Constrói o estado compartilhado da aplicação.
- Inicia o servidor Axum.
docker compose up pode levar a stack inteira a um estado funcional. Em uma implantação de produção maior, você pode preferir rodar migrações como um passo separado de release, para que mudanças de schema sejam revisadas e aplicadas antes que novas instâncias do gateway subam.
Semeando uma chave de gateway local
Para desenvolvimento local, criescripts/seed_api_key.sh. O script insere uma chave de API do gateway no Postgres armazenando seu prefixo e o hash SHA-256:
Rodando localmente
Para rodar localmente, vamos usar o Docker Compose para iniciar tanto o gateway quanto o Postgres. Isso mantém o tutorial reproduzível: os leitores não precisam de um banco de dados configurado manualmente, e o gateway pode usar o mesmo formato deDATABASE_URL que usaria em uma implantação containerizada.
-d se quiser usar seu terminal para outras coisas depois (e então usar docker compose down para removê-lo).
Em outro terminal, semeie a chave do gateway de desenvolvimento:
5432, remova o mapeamento de porta do host para o serviço Postgres do Compose. O gateway só precisa alcançar o Postgres na rede interna do Docker.
O importante é ter em mente que a chave de API da Venice pertence somente ao ambiente do gateway. Requisições dos clientes devem usar a chave de gateway semeada. Essa separação é toda a ideia de colocar um gateway na frente do provedor do modelo.
Testando o gateway
Primeiro, verifique a saúde:Estendendo este gateway
Este gateway é intencionalmente pequeno, mas te dá uma base sólida. Bons próximos passos incluem:- Adicionar orçamentos por sujeito e limites mensais de gasto.
- Suportar múltiplos provedores upstream por trás da mesma API compatível com OpenAI.
- Armazenar metadados de requisição para logs de auditoria enquanto mantém o logging de prompts desabilitado por padrão.
- Adicionar uma API de administração para criar, revogar e rotacionar chaves do gateway.
- Adicionar allowlists de modelo por chave de API.
- Adicionar Redis ou outro armazenamento compartilhado se você precisar de rate limiting com menor latência através de muitas instâncias do gateway.