/v1/chat/completions compatible con OpenAI, acepta tus propios tokens bearer, reenvía las peticiones a Venice, admite respuestas en streaming y emite spans y métricas útiles de OpenTelemetry.
¿Te interesa la implementación completa del código? Echa un vistazo a el repositorio de GitHub.
Requisitos previos
- Rust 1.92+
- Docker y Docker Compose
- Una clave de API de Venice
curl- Familiaridad básica con servicios web en Rust
Qué vamos a construir
La implementación de referencia es un pequeño servicio en Rust con varias partes claras:| Parte | Qué hace |
|---|---|
| Router de Axum | Sirve /healthz y /v1/chat/completions |
| Extractor de auth | Valida los tokens bearer del gateway contra claves hasheadas en Postgres |
| Limitador de tasa | Usa ventanas fijas almacenadas en Postgres |
| Cliente Venice | Hace de proxy para peticiones de chat completions con y sin streaming |
| Telemetría | Registra spans GenAI, uso de tokens, coste de facturación de Venice, latencia y tiempos de streaming |
| Docker Compose | Ejecuta Postgres y el gateway localmente |

Crear el servicio en Rust
Empieza con un nuevo proyecto binario en Rust:Cargo.toml, con explicaciones en el fragmento de código:
Cargar la configuración
El gateway lee todo desde variables de entorno. Para código de infraestructura como este, las variables de entorno son un buen valor por defecto porque el mismo binario puede ejecutarse localmente, en Docker Compose o en un entorno gestionado sin necesidad de un formato de archivo de configuración aparte. Además, muchos proveedores te permiten almacenar tus propias variables de entorno como secretos en su propio runtime de contenedores. A menudo esto es mucho más seguro que intentar usar algo comodotenv (o dotenvy en Rust, ya que el crate original dotenv está mayormente descontinuado).
Crea src/config.rs:
- La URL de la base de datos
- Tu clave de API de Venice
VENICE_BASE_URL apunta a https://api.venice.ai/api/v1, y CAPTURE_GENAI_CONTENT por defecto es false para que el contenido de los prompts no se registre a menos que lo habilites intencionadamente.
Ese segundo valor por defecto es el más importante. Un gateway puede ver todos los prompts y respuestas que pasan por él, pero la observabilidad no debería convertirse automáticamente en captura de contenido. En la mayoría de los sistemas en producción, los conteos de tokens, la latencia, los nombres de los modelos, los códigos de estado y los metadatos de facturación son suficientes para operaciones. En términos generales, registrar prompts y conversaciones en producción no solo puede ser un riesgo de privacidad, sino también un riesgo de almacenamiento. Añadirlos supone crear spans y trazas con un nivel extremadamente alto de cardinalidad (es decir, la unicidad de los datos dentro de un dataset). Esto puede encarecer mucho la búsqueda en tus datos de observabilidad, además de perjudicar potencialmente el rendimiento al buscar en los datos.
Crear el esquema de la base de datos
A continuación, creamigrations/0001_api_keys.sql.
Solo almacenaremos los primeros 12 caracteres de cada clave de API del gateway como prefijo de búsqueda, más el hash SHA-256 de la clave completa. Eso permite que el gateway encuentre rápidamente una fila candidata sin almacenar las credenciales en texto plano.
El prefijo no es secreto. Existe para indexar. El hash es lo que demuestra que quien llama presentó la clave completa. Esta es la misma forma básica que usan muchos sistemas de claves de API: mostrar la clave en texto plano una vez, almacenar una representación no reversible y mantener un prefijo corto para búsquedas y flujos de soporte.
- Las claves de API nunca se almacenan en texto plano.
- Los ajustes de rate limit deben ser positivos.
- Las claves revocadas no pueden permanecer activas.
- Una ventana de rate limit se identifica de forma única por clave, hora de inicio y longitud de ventana.
Construir el cliente de Venice
A continuación, crearemossrc/venice.rs. El cliente solo necesita conocer la URL upstream de chat completions, la clave de API de Venice y cuántas veces reintentar ante fallos transitorios.
Mantener este wrapper pequeño es intencionado: el gateway no debería reimplementar toda la API de Venice. A un nivel básico, el trabajo del gateway es adjuntar la credencial del lado del servidor, aplicar un timeout, reintentar las peticiones que sea seguro reintentar y devolver la respuesta upstream en una forma que el router pueda reenviar.
EventSource a partir de la misma petición:
serde_json::Value. Es una elección deliberada de compatibilidad. Si modelamos en Rust cada campo posible de chat completion, tenemos que mantener el gateway actualizado cada vez que la API upstream añada una opción útil. Al parsear solo lo que necesitamos en otras partes, permitimos que los parámetros más nuevos de Venice pasen sin necesidad de una nueva release del gateway.
Compartir el estado de la aplicación
Creasrc/state.rs:
PgPool ya es un handle a un pool compartido, y Arc<Config> mantiene también barata la configuración.
Esto da a cada handler acceso a las mismas tres cosas: configuración inmutable, conexiones a la base de datos en pool y el cliente de Venice. Mantenerlas en un único AppState también hace más sencillo el testing más adelante, porque los handlers reciben sus dependencias a través del estado de Axum en lugar de leer variables globales.
Autenticar claves de API del gateway
El cliente envía su clave del gateway así:src/auth.rs e implementa un extractor de Axum. El extractor permite que los handlers protegidos declaren que requieren una clave autenticada:
- Parsear el token bearer.
- Tomar los primeros 12 bytes como prefijo de la clave.
- Hashear el token candidato completo con SHA-256.
- Cargar la fila de la clave activa por prefijo.
- Comparar en tiempo constante el hash almacenado y el hash candidato.
AuthenticatedApiKey no puede saltarse accidentalmente la auth dentro del cuerpo de la función; Axum tiene que construir ese valor antes de que el handler se ejecute. Eso hace que el camino protegido sea fácil de auditar.
Añadir rate limits de ventana fija
Creasrc/rate_limit.rs. El limitador de tasa usa una única sentencia SQL para insertar una nueva ventana o incrementar la existente:
WHERE api_key_rate_limit_windows.request_count < $3 es la parte importante. Cuando la ventana ya está llena, Postgres no actualiza la fila y RETURNING no produce ninguna fila. El handler puede convertir eso en una respuesta 429 Too Many Requests con una cabecera Retry-After.
Una ventana fija no es el limitador de tasa más sofisticado, pero es fácil de explicar, fácil de inspeccionar y suficientemente bueno para un tutorial de gateway. La contrapartida es que el tráfico puede acumularse alrededor de los límites de la ventana. Si necesitas un comportamiento más suave a escala, un token bucket o un limitador de ventana deslizante respaldado por Redis es un siguiente paso natural.
Devolver errores estilo OpenAI
Creasrc/error.rs y haz que los errores de la aplicación implementen IntoResponse:
Construir el router
Ahora podemos cablear las rutas HTTP ensrc/router.rs:
AuthenticatedApiKey. Si la autenticación falla, Axum nunca entra en el cuerpo del handler:
model, messages y stream. Todo lo demás en el cuerpo JSON pasa a Venice. Eso mantiene al gateway compatible con las funcionalidades del proveedor que quizá quieras usar más adelante.
El handler también hace explícitos los dos modos de respuesta. Las peticiones sin streaming esperan a que Venice devuelva una respuesta JSON completa y luego registran los metadatos de la respuesta antes de enviar los bytes al cliente. Las peticiones en streaming devuelven inmediatamente un cuerpo text/event-stream respaldado por un stream asíncrono. Esa división mantiene sencillo el camino sin streaming y da al camino en streaming el control suficiente para observar los chunks mientras pasan.
Soportar respuestas en streaming
Los chat completions en streaming usan server-sent events. Venice envía datos SSE, y el gateway retransmite esos datos al cliente. El gateway debería evitar hacer buffering de todo el stream, porque eso desvirtuaría el propósito del streaming. A los usuarios les importa el tiempo hasta el primer token, no solo el tiempo hasta el último token. Al reenviar cada evento upstream a medida que llega, los clientes pueden renderizar salida parcial mientras el modelo aún está generando. Creasrc/sse.rs:
data: ..., y el stream termina con data: [DONE].
El observador del stream es también el lugar donde podemos recolectar metadatos sin cambiar lo que ve el cliente. Cada chunk se reenvía en formato SSE, pero el gateway todavía puede vigilar los IDs de respuesta, los finish reasons, el uso de tokens, los campos de coste y la información de tiempos a medida que esos chunks pasan.
Registrar telemetría GenAI
Los gateways son útiles porque cada petición pasa por un único lugar. Eso los convierte en un gran sitio para registrar modelo, latencia, uso de tokens, finish reasons, coste de facturación y tiempos de streaming. Creasrc/telemetry.rs y empieza parseando la petición:
id de la respuesta de Venice:
Arrancar el servidor
Ahora conecta todo ensrc/main.rs:
- Lee la configuración.
- Inicializa la telemetría.
- Se conecta a Postgres.
- Ejecuta las migraciones de SQLx.
- Construye el estado compartido de la app.
- Arranca el servidor de Axum.
docker compose up puede llevar toda la pila a un estado funcional. En un despliegue de producción más grande, quizá prefieras ejecutar las migraciones como un paso de release separado para que los cambios de esquema se revisen y apliquen antes de que arranquen nuevas instancias del gateway.
Sembrar una clave local del gateway
Para desarrollo local, creascripts/seed_api_key.sh. El script inserta una clave de API del gateway en Postgres almacenando su prefijo y su hash SHA-256:
Ejecutar localmente
Para ejecutar localmente, usaremos Docker Compose para arrancar tanto el gateway como Postgres. Esto mantiene el tutorial reproducible: los lectores no necesitan una base de datos configurada manualmente, y el gateway puede usar la misma forma deDATABASE_URL que usaría en un despliegue en contenedores.
-d si quieres usar tu terminal para otras cosas después (y luego usar docker compose down para eliminarlo).
En otra terminal, siembra la clave de desarrollo del gateway:
5432, quita el mapeo de puerto del host para el servicio Postgres de Compose. El gateway solo necesita alcanzar Postgres en la red interna de Docker.
Lo importante a tener en cuenta es que la clave de API de Venice solo debe estar en el entorno del gateway. Las peticiones de los clientes deberían usar la clave del gateway sembrada. Esa separación es todo el propósito de poner un gateway delante del proveedor del modelo.
Probar el gateway
Primero, comprueba la salud:Extender este gateway
Este gateway es intencionadamente pequeño, pero te da una base sólida. Buenos siguientes pasos incluyen:- Añadir presupuestos por sujeto y límites de gasto mensuales.
- Soportar múltiples proveedores upstream detrás de la misma API compatible con OpenAI.
- Almacenar metadatos de las peticiones para logs de auditoría manteniendo el registro de prompts desactivado por defecto.
- Añadir una API de administración para crear, revocar y rotar claves del gateway.
- Añadir listas de modelos permitidos por clave de API.
- Añadir Redis u otro almacén compartido si necesitas un rate limiting con menor latencia entre muchas instancias del gateway.