Varias personas leyeron mi artículo sobre cómo la primera versión de GeminiOmni tuvo su clave de API extraída en diecisiete minutos y pidieron la versión completa de la arquitectura del proxy. Este es ese artículo.
Si estás construyendo algo que llama a una API de IA de pago — Gemini, OpenAI, Anthropic, Replicate, cualquier cosa con una clave medida — el patrón a continuación es lo que se interpone entre tú y una sorpresa de martes por la mañana de tus alertas de facturación. No es complicado. Es estructural.
Explicaré la arquitectura, las cuatro cosas concretas que debes hacer bien y los casos extremos que he encontrado ejecutándolo en producción durante un mes.
El patrón principal, dibujado
La forma es la misma independientemente del proveedor de IA que uses:
┌─────────────────┐ ┌──────────────────────┐ ┌──────────────┐
│ │ │ │ │ │
│ Navegador │ │ Tu ruta de API │ │ Proveedor │
│ (no confiable) │ ───→ │ (servidor, tiene │ ───→ │ (Gemini / │
│ │ │ clave) │ │ OpenAI...) │
│ - prompt │ │ - verificación auth │ │ │
│ - subida │ │ - límite de tasa │ └──────────────┘
│ │ │ - verificación cuota│
│ │ │ - adjuntar clave API│
│ │ │ - llamar proveedor │
│ │ │ - devolver resultado│
│ │ ←─── │ │
└─────────────────┘ └──────────────────────┘El punto es que el navegador ve un prompt entrar y un resultado salir. No tiene idea de qué modelo se llamó, cuál es la clave de la API, cómo se ve el proveedor upstream o cuánto costó la llamada. Esta es la cantidad correcta de información que debe tener el navegador.
Cómo se ve "bien", concretamente
Cuatro cosas deben ser ciertas para que esta arquitectura realmente te proteja. Si falta una de ellas, la protección colapsa.
1. La clave de la API vive solo en el servidor
En términos de Next.js: la variable de entorno NO debe comenzar con NEXT_PUBLIC_. En términos de tiempo de ejecución: solo el código que se ejecuta en un manejador de ruta (app/api/.../route.ts), un componente del servidor o una acción del servidor toca la clave.
// app/api/ai/generate/route.ts — solo servidor
const GEMINI_KEY = process.env.GEMINI_API_KEY; // ✅ solo servidor
if (!GEMINI_KEY) throw new Error('GEMINI_API_KEY no establecida');Una comprobación de inicio que bloquee la aplicación si falta GEMINI_API_KEY vale su peso en oro. El peor modo de fallo es que la aplicación se ejecute silenciosamente sin una clave y recurra a algún valor predeterminado que vacíe tus ahorros.
2. El navegador nunca ve las respuestas crudas del proveedor
Cuando la llamada al proveedor regresa, seleccionas qué enviar de vuelta. El navegador NO necesita saber:
- Qué modelo exacto fue llamado (Veo 3.1 Fast vs Veo 3.1 Standard)
- El ID de solicitud del proveedor (útil para soporte, inútil para clientes)
- Cualquier encabezado de límite de tasa del proveedor
- El conteo de tokens o el costo de la llamada
Por qué esto importa: la divulgación de información se acumula. Si tu navegador puede ver "model: gemini-2.5-flash, tokens: 4096", un scraper puede mapear tu front-end a la página de precios de tu proveedor e ingeniería inversa tu economía unitaria. Eso no es catastrófico pero no es bueno. Elimínalo.
// Devuelve solo lo que la UI necesita
return Response.json({
imageUrl: result.generated_images[0].url,
watermarked: !user.isPro,
// No devolver: nombre del modelo, tokens usados, respuesta cruda del proveedor
});3. La verificación de autenticación, límite de tasa y cuota ocurre ANTES de la llamada al proveedor
El proxy no es solo un paso intermedio. Es el lugar donde aplicas las reglas:
// Dentro de la ruta de la API, en orden:
1. Autenticar la solicitud (cookie de sesión, JWT, lo que sea).
Las solicitudes anónimas están permitidas para funciones del nivel gratuito pero se rastrean por IP.
2. Limitar la tasa por ID de usuario (o IP para anónimos).
GeminiOmni: 10 req/min anónimo, 60 req/min Pro, 200 req/min Team.
3. Verificar la cuota del usuario para la operación específica.
Los usuarios gratuitos tienen 5 generaciones de video por mes — verificar la base de datos ANTES
de la llamada costosa, no después.
4. Solo ahora — llamar al proveedor.
5. Incrementar los contadores de uso al tener éxito.El orden importa. Si llamas al proveedor primero y luego verificas la cuota, ya pagaste por la inferencia independientemente de si el usuario tenía derecho o no. Todas las comprobaciones de cuota se realizan por adelantado.
4. El proxy tiene su propia observabilidad
Registra cada llamada al proveedor desde el proxy con: timestamp, ID de usuario, ruta, modelo, tamaño de entrada, tamaño de salida, latencia, éxito/fallo y el costo aproximado en dólares. Este es tu recibo de facturación y tu superficie de depuración.
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',
});Cuando tu alerta de facturación del martes por la mañana se active porque el gasto se duplicó durante la noche, esta tabla es lo que consultas en SQL para descubrir qué usuario, qué ruta, qué modelo. Sin ella, estás adivinando.
Casos extremos que he encontrado ejecutándolo en producción
Un mes ejecutando el proxy en producción ha sacado a la luz algunas cosas no obvias.
Las respuestas de streaming necesitan proxies de streaming. El endpoint streamGenerateContent de Gemini devuelve un stream de generaciones parciales. Si ingenuamente llamas a response.json() en tu proxy, almacenarás en búfer todo el stream y perderás el beneficio de latencia. La solución es canalizar el stream upstream a través de tu proxy como un ReadableStream, lo que significa que tu ruta de API devuelve new Response(stream) en lugar de Response.json(...). Vale la pena hacerlo: la latencia percibida en la generación de video pasa de "esperar 90 segundos" a "ver algo sucediendo inmediatamente".
Las subidas de archivos grandes necesitan subidas directas al almacenamiento. Si un usuario sube un video de 50MB para la generación de imagen a video, no quieres que esos 50MB transiten a través de tu ruta de API de Next.js. El patrón es: el cliente sube directamente a Cloud Storage / R2 a través de una URL prefirmada, luego envía solo la URL a tu proxy. El proxy pasa la URL al parámetro "media URI" de Gemini, y los bytes nunca tocan tu servidor. Esto reduce tu factura de salida en aproximadamente un 95% en las rutas de subida pesadas.
Los errores del upstream necesitan traducción. Gemini devuelve errores como "PROMPT_BLOCKED" o "QUOTA_EXCEEDED" o "INVALID_ARGUMENT". El navegador no necesita ver estas cadenas — son confusas y filtran detalles sobre el proveedor. Traduce cada una a un mensaje orientado al usuario: "Tu prompt fue bloqueado por los filtros de seguridad", "Has usado tus generaciones gratuitas para este mes", "Esa imagen parece estar corrupta." Una tabla de búsqueda de tres líneas.
La estimación de costos es por llamada, no por token. Intenté estimar el costo en dólares de cada llamada contando tokens. Luego noté que la facturación de Veo es por SEGUNDOS de video de salida, Imagen es por IMAGEN, y Nano Banana 2 tiene un precio de cuatro niveles basado en la resolución. La función de costo para el proxy es una declaración switch, no una multiplicación. No hay una forma limpia de evitar esto — cada proveedor factura de manera diferente y debes codificar cada uno.
Cómo se ve esto en el código de GeminiOmni
Cada llamada de modelo en /tools/text-to-video, /tools/image-to-video, /tools/nano-banana-edit, /tools/pdf-chat y el chat en vivo en /chat pasa a través de app/api/ai/generate/route.ts y app/api/ai/query/route.ts. Dos endpoints. Unas 400 líneas de código en total. Las cuatro comprobaciones anteriores se aplican antes de que cualquier llamada al proveedor salga de mi servidor.
Nombro el modelo en la UI orientada al usuario, pero no en la respuesta de la API. La insignia debajo de cada clip generado dice "Veo 3.1 Fast" — eso proviene de una búsqueda del lado del servidor en el momento de renderizado, no de la respuesta de la API. Este es el tipo de cosas que suena tonto hasta que te das cuenta de que cualquier cadena que la API devuelva está a una captura de pantalla de distancia de ser un objetivo.
El resumen honesto
El patrón del proxy no es emocionante. No hay algoritmos inteligentes en él. Es solo el mínimo indispensable requerido para ejecutar una API de IA de pago en la internet pública sin perder dinero frente a personas que no pagaron.
Pero es el mínimo indispensable. Cada creador independiente que conozco que lo lanzó sin él aprendió la lección de la misma manera: una alerta de facturación a una hora incómoda, una revocación de pánico de la clave y una larga noche reescribiendo el flujo de datos. Más barato hacerlo desde el primer día.
Sigue leyendo
- Lo primero que se rompió cuando lancé GeminiOmni
- El contexto de 1M de tokens no es gratis — costo real por página del chat PDF de Gemini
- Precios de GeminiOmni — cómo el proxy da forma a los niveles
— Lena
