Um gateway de API compatível com OpenAI que fica entre suas aplicações e os modelos Gemini do Google. Ele fornece controle de custos através de cache de respostas, resiliência através de fallback automático de modelos e observabilidade através de métricas Prometheus e logging estruturado -- problemas que se tornam inegociáveis quando você executa workloads de LLM em produção.
Chamar provedores de LLM diretamente do código da aplicação cria diversas dores de cabeça operacionais:
- Surpresas de custo. Prompts idênticos atingem a API repetidamente. Uma camada de cache com chaves baseadas em hash elimina chamadas redundantes e oferece um único controle (TTL) para gerenciar o trade-off entre custo e atualização.
- Falhas em cascata. Quando um modelo está sobrecarregado ou degradado, todos os chamadores falham. Uma cadeia de fallback entre modelos significa uma experiência degradada ao invés de uma quebrada.
- Sem visão centralizada. Sem métricas centralizadas, você fica correlacionando logs entre serviços para responder "quantas requisições estamos fazendo?" ou "qual é nossa latência P95?". O gateway exporta contadores e histogramas Prometheus prontos para uso.
- Dispersão de autenticação. Distribuir chaves de API brutas do provedor para cada serviço é um risco de segurança. O gateway aceita suas próprias chaves de API e faz proxy das chamadas com a chave do provedor no lado do servidor.
Client
|
v
POST /v1/chat/completions
|
v
+------------------+
| Auth Middleware | Validate API key (constant-time comparison)
+------------------+
|
v
+------------------+
| Rate Limiter | Sliding-window, per-key, in-memory store
+------------------+
|
v
+------------------+
| Cache Check | SHA-256 hash of (model + messages + params)
+------------------+
| hit --> return cached response
| miss
v
+------------------+
| Provider Registry| Try primary model, then each fallback in order
+------------------+
|
v
+------------------+
| LLM Provider | Google Gemini via ADK
+------------------+
|
v
+------------------+
| Cache Store | Write response with TTL
+------------------+
|
v
+------------------+
| Prometheus | Increment counters, observe latency histogram
+------------------+
|
v
Response (OpenAI-compatible JSON)
graph LR
Client -->|POST /v1/chat/completions| Gateway
Gateway --> Auth[Auth Middleware]
Auth --> RL[Rate Limiter]
RL --> Cache[Redis Cache]
Cache -->|hit| Client
Cache -->|miss| Registry[Provider Registry]
Registry --> Primary[Primary Model]
Primary -->|fail| Fallback[Fallback Models]
Registry --> Cache
Gateway --> Metrics[Prometheus Metrics]
Formato de API compatível com OpenAI. Os endpoints /v1/chat/completions e /v1/models espelham o schema de request/response da OpenAI. Isso significa que qualquer biblioteca cliente ou ferramenta que fala o protocolo OpenAI (LangChain, LiteLLM, Cursor, Continue) funciona sem modificação. A tradução do schema OpenAI para Gemini acontece inteiramente dentro do gateway.
Rate limiting com janela deslizante. Uma abordagem de janela fixa tem um problema de burst nas fronteiras da janela (um cliente pode disparar 2x o limite cronometrando requisições na transição da janela). A janela deslizante rastreia timestamps individuais de requisições e remove os expirados a cada verificação, oferecendo enforcement suave ao custo de armazenamento O(n) por chave por janela. Para a escala esperada (dezenas de chaves, sub-100 RPM cada), isso é desprezível.
Chaves de cache baseadas em hash SHA-256. As chaves de cache são derivadas de uma serialização JSON determinística de (model, messages, temperature, max_tokens). SHA-256 mantém as chaves em 64 caracteres hex fixos independente do tamanho do prompt, evita limites de tamanho de chave no Redis e torna colisões astronomicamente improváveis. O trade-off é que prompts semanticamente idênticos mas sintaticamente diferentes (espaços extras, chaves reordenadas) produzem chaves de cache diferentes -- aceitável dado que a alternativa é uma verificação de similaridade baseada em embeddings muito mais cara.
Fallback em memória para Redis. A camada de cache tenta Redis primeiro e degrada silenciosamente para um dict em memória em caso de falha de conexão. Isso significa que ambientes de desenvolvimento local e CI funcionam sem Redis rodando, enquanto produção obtém os benefícios de durabilidade e estado compartilhado do Redis. O fallback é por processo, então entradas em cache não são compartilhadas entre workers -- um trade-off intencional de simplicidade.
-
Rate limiting em memória não escala horizontalmente. Cada processo mantém sua própria janela deslizante. Atrás de um load balancer com N workers, um atacante poderia obter N vezes o limite de taxa pretendido. Um deploy em produção deveria trocar o store em memória por um backed pelo Redis (a interface
RateLimiteré projetada para essa troca). Foi mantido em memória para manter a implementação inicial leve em dependências e fácil de testar. -
Invalidação de cache é apenas por TTL. Não há como invalidar seletivamente respostas em cache quando um modelo é atualizado ou fine-tuned. A variável de ambiente
CACHE_TTL(padrão 1 hora) é o único controle. Para a maioria dos casos de uso isso é suficiente; para aplicações sensíveis à latência que precisam de saída fresca do modelo, defina TTL como 0 para desabilitar o cache. -
Sem suporte a streaming ainda. O gateway faz buffer de toda a resposta antes de retorná-la. Suporte a streaming (SSE) exigiria um fluxo fundamentalmente diferente através da camada de cache (não é possível fazer cache de um stream que ainda não foi totalmente recebido) e está planejado mas não implementado.
-
Contagem de tokens simplificada. O uso de tokens é estimado dividindo por espaços em branco ao invés de usar um tokenizador adequado. Isso é intencional -- evita adicionar uma dependência de tokenizador e é preciso o suficiente para rastreamento de custos no nível do gateway. Contagens precisas vêm dos metadados de uso do provedor quando disponíveis.
-
Padrões de fallback são enganosamente simples. O caminho feliz (tentar modelo A, em caso de falha tentar modelo B) é direto. A parte difícil é definir "falha" -- timeouts, respostas 5xx, saída malformada e rate limits do provedor todos precisam de tratamento diferente. A implementação atual trata qualquer exceção como gatilho para fallback, o que é pragmático mas grosseiro.
-
Design de chave de cache requer pensar sobre equivalência. Dois prompts que significam a mesma coisa mas diferem por uma quebra de linha no final produzem chaves de cache diferentes. Considerei normalizar espaços em branco mas decidi contra -- o risco de acidentalmente conflitar prompts genuinamente diferentes supera a melhoria marginal de cache hit.
-
Composição de middleware no FastAPI. A injeção de dependências do FastAPI torna natural compor middleware como dependências injetáveis ao invés de middleware ASGI. Auth, rate limiting e resolução de provedor são todos callables
Depends(), o que significa que são testáveis isoladamente e substituíveis por rota. O middleware de logging é a exceção -- ele usa oBaseHTTPMiddlewaredo Starlette porque precisa envolver todo o ciclo de vida da requisição incluindo handlers de erro.
# Install dependencies
uv sync --dev
# Copy and configure environment
cp .env.example .env
# Edit .env with your Google API key
# Run the server
uv run llm-gateway
# Or run directly
uv run uvicorn src.cli:app --reload# Health check
curl http://localhost:8000/health
# List models
curl http://localhost:8000/v1/models
# Chat completion
curl -X POST http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "gemini-2.0-flash",
"messages": [{"role": "user", "content": "Hello!"}]
}'docker compose up -d# Lint and format
uv run ruff check --fix .
uv run ruff format .
# Type check
uv run mypy src/
# Run tests
uv run pytest --cov=src -v| Variável | Padrão | Descrição |
|---|---|---|
GOOGLE_API_KEY |
(obrigatório) | Chave de API do Google para modelos Gemini |
DEFAULT_MODEL |
gemini-2.0-flash |
Modelo padrão para completions |
REDIS_URL |
redis://localhost:6379 |
URL de conexão Redis |
RATE_LIMIT_RPM |
60 |
Máximo de requisições por minuto por chave de API |
API_KEYS |
(vazio) | Chaves de API válidas separadas por vírgula |
FALLBACK_MODELS |
(vazio) | Lista de modelos de fallback separados por vírgula |
CACHE_TTL |
3600 |
Tempo de vida do cache em segundos |
MIT