
Cómo integrar la API de OpenAI en un SaaS Next.js sin reventar costos
Respuesta corta (60 segundos): integrar OpenAI en un SaaS sin reventar costos depende de cinco prácticas que se pueden implementar en 3 horas: (1) wrappear el SDK con telemetría desde el día 1, (2) routear cada call al modelo más barato que pueda hacer la tarea (gpt-4o-mini para 80% de los casos), (3) cachear prompts determinísticos en Redis, (4) streamear respuestas para mejor UX, y (5) rate limiting + caps de presupuesto por tenant. Aplicado bien, baja la factura mensual 50-70% sin pérdida de calidad perceptible.
La integración inicial con OpenAI en un SaaS es engañosamente fácil: cuatro líneas de código y ya tenés un chatbot funcionando. El problema empieza el segundo mes, cuando ves la factura.
Este post es la guía que me hubiera gustado encontrar cuando integré IA en mi primer SaaS. No "cómo llamar a la API" — eso está en los docs de OpenAI. Sino cómo integrarla de forma que escale sin reventar el margen.
El error más común que veo en SaaS LATAM
Cobrar USD 49/mes a un usuario que te cuesta USD 80/mes en tokens. Ese caso es real — un cliente que vino a consulting con exactamente ese problema: feature de IA en plan medio, usuarios pesados consumiendo más de lo que pagan, margen negativo por usuario activo.
Las cinco prácticas que siguen evitan ese escenario.
Paso 1 · Wrappear el SDK con telemetría desde el día 1
Antes de hacer la primera llamada a OpenAI desde tu app, centralizá todas las llamadas en un solo módulo. Esto te permite:
- Loggear tokens, modelo, latencia, costo estimado.
- Cambiar de provider o modelo sin tocar 50 archivos.
- Aplicar políticas (caps, rate limits) en un único punto.
~// lib/ai/client.ts import OpenAI from "openai"; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, baseURL: "https://oai.helicone.ai/v1", defaultHeaders: { "Helicone-Auth": `Bearer ${process.env.HELICONE_API_KEY}`, }, }); // Precios por 1M tokens (input / output). Actualizar cuando OpenAI cambie. const PRICING: Record<string, { input: number; output: number }> = { "gpt-4o": { input: 2.50, output: 10.00 }, "gpt-4o-mini": { input: 0.15, output: 0.60 }, "o3-mini": { input: 1.10, output: 4.40 }, }; export async function callLLM(opts: { tenantId: string; model: keyof typeof PRICING; messages: OpenAI.ChatCompletionMessageParam[]; temperature?: number; }) { const start = Date.now(); const res = await openai.chat.completions.create({ model: opts.model, messages: opts.messages, temperature: opts.temperature ?? 0.2, }, { headers: { "Helicone-User-Id": opts.tenantId }, }); const latencyMs = Date.now() - start; const u = res.usage!; const pricing = PRICING[opts.model]; const costUsd = (u.prompt_tokens / 1_000_000) * pricing.input + (u.completion_tokens / 1_000_000) * pricing.output; // Log local — Helicone también guarda, esto es redundancia para el dashboard interno await logAiCall({ tenantId: opts.tenantId, model: opts.model, promptTokens: u.prompt_tokens, completionTokens: u.completion_tokens, costUsd, latencyMs, }); return { content: res.choices[0].message.content, usage: u, costUsd }; }
Toda llamada a IA en la app pasa por callLLM. Si más adelante querés agregar caching, kill-switch, fallback a Anthropic, lo hacés acá.
Paso 2 · Seleccionar modelo dinámicamente
Esta es la práctica que más impacto tiene en la factura.
Regla mental para 2026:
| Tipo de tarea | Modelo | Por qué |
|---|---|---|
| Clasificación, extracción, ruteo | gpt-4o-mini | 16x más barato que gpt-4o, calidad indistinguible para tareas estructuradas. |
| Resumen corto, parafraseo | gpt-4o-mini | Igual. |
| Generación rica de contenido | gpt-4o | Necesitás creatividad/voz. La diferencia se nota. |
| Razonamiento multi-paso | o3-mini | Cadenas de razonamiento explícitas. Más caro pero menos passes necesarios. |
| Embeddings | text-embedding-3-small | $0.02/1M tokens, suficiente para casi todo. |
Implementación:
~// lib/ai/routing.ts type TaskType = "classify" | "extract" | "summarize" | "generate" | "reason"; const MODEL_FOR_TASK: Record<TaskType, "gpt-4o" | "gpt-4o-mini" | "o3-mini"> = { classify: "gpt-4o-mini", extract: "gpt-4o-mini", summarize: "gpt-4o-mini", generate: "gpt-4o", reason: "o3-mini", }; export function modelFor(task: TaskType) { return MODEL_FOR_TASK[task]; }
Después, en cada call:
~await callLLM({ tenantId, model: modelFor("classify"), messages: [...] });
Cambiar la política central de mini → 4o cuando hace falta es una sola línea.
Anti-pattern: elegir el modelo dentro de la lógica de negocio (if (premium) model = 'gpt-4o'). Eso esparce decisiones de costo por todo el código.
Paso 3 · Caching de prompts determinísticos
Si el mismo prompt con los mismos parámetros se repite, no tiene sentido pagarlo dos veces.
~// lib/ai/cache.ts import { Redis } from "@upstash/redis"; import crypto from "node:crypto"; const redis = Redis.fromEnv(); function cacheKey(input: { model: string; messages: any[]; temperature: number }) { const hash = crypto.createHash("sha256").update(JSON.stringify(input)).digest("hex"); return `ai:cache:${hash}`; } export async function cachedCallLLM(opts: Parameters<typeof callLLM>[0]) { // Cachear solo si la respuesta es determinística const isDeterministic = (opts.temperature ?? 0.2) === 0 || opts.temperature === 0.2; if (!isDeterministic) return callLLM(opts); const key = cacheKey({ model: opts.model, messages: opts.messages, temperature: opts.temperature ?? 0.2 }); const cached = await redis.get<string>(key); if (cached) { // Hit — costo = 0, latencia = <10ms return { content: cached, usage: { prompt_tokens: 0, completion_tokens: 0 }, costUsd: 0, cached: true }; } const result = await callLLM(opts); await redis.set(key, result.content, { ex: 60 * 60 * 24 * 7 }); // TTL 7 días return { ...result, cached: false }; }
Cuándo NO cachear:
- Chat conversacional (cada interacción tiene contexto distinto).
- Generación creativa con temperature > 0.5.
- Cualquier flujo donde el usuario espera variación.
Cuándo SÍ cachear:
- Clasificación ("este ticket es de cobranzas? sí/no").
- Extracción ("extraer email, monto, fecha de este texto").
- Traducciones del mismo texto fuente.
- Resúmenes de documentos que no cambian.
Hit rates típicos en producción: 30-60% en flujos de extracción, lo que se traduce en 30-60% menos costo en esos endpoints.
Paso 4 · Streaming de respuestas
Para cualquier respuesta que tarde más de 2 segundos, streaming mejora la UX percibida dramáticamente. Implementación en Next.js App Router:
~// app/api/chat/route.ts import { openai } from "@/lib/ai/client"; export async function POST(req: Request) { const { messages, tenantId } = await req.json(); const stream = await openai.chat.completions.create({ model: "gpt-4o-mini", messages, stream: true, }, { headers: { "Helicone-User-Id": tenantId } }); const encoder = new TextEncoder(); const readable = new ReadableStream({ async start(controller) { for await (const chunk of stream) { const content = chunk.choices[0]?.delta?.content; if (content) controller.enqueue(encoder.encode(content)); } controller.close(); }, }); return new Response(readable, { headers: { "Content-Type": "text/event-stream" }, }); }
En el frontend:
~const res = await fetch("/api/chat", { method: "POST", body: JSON.stringify({ messages, tenantId }) }); const reader = res.body!.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); setOutput((prev) => prev + chunk); }
20 líneas en total, gran impacto en UX.
Paso 5 · Rate limiting por tenant
Sin esto, un usuario abusivo (o un bug en tu propio frontend) te puede generar una factura sorpresa de USD 500 en una noche.
~// lib/ai/rate-limit.ts import { Ratelimit } from "@upstash/ratelimit"; import { Redis } from "@upstash/redis"; const redis = Redis.fromEnv(); export const aiLimiter = new Ratelimit({ redis, limiter: Ratelimit.slidingWindow(60, "1 m"), // 60 requests/min por tenant analytics: true, prefix: "rl:ai", }); export async function assertAiLimit(tenantId: string) { const { success, limit, reset, remaining } = await aiLimiter.limit(tenantId); if (!success) { const error: any = new Error("Rate limit exceeded"); error.status = 429; error.headers = { "X-RateLimit-Limit": String(limit), "X-RateLimit-Remaining": String(remaining), "Retry-After": String(Math.ceil((reset - Date.now()) / 1000)), }; throw error; } }
Usar en cada endpoint de IA antes de hacer la llamada:
~await assertAiLimit(tenantId); const result = await cachedCallLLM({...});
60 requests/min suele ser suficiente para uso humano. Si necesitás bulk processing, exponé un endpoint separado con cola.
Paso 6 · Caps de presupuesto con kill switch
La protección más importante: un cap mensual por tenant que cortes cuando se exceda, no que solo loggees.
~// lib/ai/budget.ts async function tenantMonthSpend(tenantId: string): Promise<number> { const month = new Date().toISOString().slice(0, 7); // YYYY-MM const key = `spend:${tenantId}:${month}`; const v = await redis.get<number>(key); return v ?? 0; } async function addSpend(tenantId: string, usd: number) { const month = new Date().toISOString().slice(0, 7); const key = `spend:${tenantId}:${month}`; await redis.incrbyfloat(key, usd); await redis.expire(key, 60 * 60 * 24 * 40); // expirar a los 40 días } const TENANT_MONTHLY_CAP_USD = 50; // configurable por plan export async function assertBudget(tenantId: string) { const spend = await tenantMonthSpend(tenantId); if (spend >= TENANT_MONTHLY_CAP_USD) { const error: any = new Error("Monthly AI budget exceeded"); error.status = 402; // Payment Required throw error; } } export async function recordSpend(tenantId: string, costUsd: number) { await addSpend(tenantId, costUsd); if ((await tenantMonthSpend(tenantId)) > TENANT_MONTHLY_CAP_USD * 0.8) { // Notificación a slack/email cuando un tenant llega al 80% del cap await notifyApproachingCap(tenantId); } }
Combinarlo en el flujo:
~await assertAiLimit(tenantId); await assertBudget(tenantId); const result = await cachedCallLLM({...}); await recordSpend(tenantId, result.costUsd);
Por qué retornar 402 y no error genérico: le decís al frontend exactamente cuál es el problema ("este tenant excedió su presupuesto") y podés mostrar una UI de upsell ("Querés más capacidad? Pasate a plan Pro").
Lo que vas a aprender en producción
Tres lecciones que solo se aprenden pagando facturas:
-
El 10% de los usuarios consumen el 80% de los tokens. Power user detection es crítico. Una alerta cuando un usuario individual pasa cierto umbral suele revelar un bug en tu UI o un caso de uso que no anticipaste.
-
Los costos crecen non-linealmente con el growth de usuarios. No es 2x usuarios = 2x costo. Tiende a ser 2x usuarios = 3-4x costo porque cada usuario nuevo descubre flujos pesados que el resto ya conocía. Modelá esto en tu pricing.
-
Cambios de modelo "sin breaking changes" rompen cosas. OpenAI deprecó modelos varias veces sin previo aviso suficiente. Tu wrapper de Paso 1 te permite migrar en una semana en vez de un mes.
Hablemos de tu caso
Si estás integrando OpenAI en tu SaaS y querés revisar tu arquitectura antes de que los costos te sorprendan, reservá una llamada de 30 minutos sin costo. 30 minutos suelen alcanzar para identificar dónde está la mayor optimización posible y cuánto te ahorrás.
Leer también:
- RAG paso a paso para tu SaaS — si tu integración con OpenAI incluye recuperación de tu propia data.
- Cuánto cuesta implementar IA en una startup SaaS — para presupuestar el proyecto entero.
- Más artículos sobre IA — tutoriales y comparativas.
- Volver al blog — todos los artículos.
Preguntas frecuentes
¿Cuál es el error más caro que se comete integrando OpenAI por primera vez?
Usar gpt-4o para todo. La diferencia de costo entre gpt-4o ($2.50/1M tokens input) y gpt-4o-mini ($0.15/1M) es 16x. Para clasificación, extracción de datos estructurados y resumen corto, gpt-4o-mini es indistinguible. Pasar 80% del tráfico a mini suele bajar la factura 60-70% sin pérdida perceptible de calidad.
¿Cuándo conviene cachear y cuándo no?
Cachear cuando el prompt + parámetros son determinísticos y la respuesta no necesita variar (clasificación, extracción, traducciones). NO cachear chat conversacional ni generación creativa donde temperature > 0 — ahí el usuario espera variación. Hit rate típico: 30-60% en flujos de extracción, casi 0% en chat libre.
¿Streaming agrega complejidad real o vale la pena?
Vale la pena casi siempre. UX percibida pasa de 'esperando 8 segundos' a 'leyendo en tiempo real'. Implementación: 20 líneas de código en Next.js App Router con Server-Sent Events. Único caveat: tu observability tiene que poder rastrear streams (Helicone lo hace transparentemente).
¿Qué pasa cuando OpenAI tiene un outage?
Pasa más seguido de lo que esperás (5-10 minutos cada 2-3 meses). Tres opciones: (1) retry con backoff exponencial — soluciona la mayoría; (2) fallback a Anthropic Claude para los flujos críticos — el código casi cambia; (3) cola en SQS/Inngest para procesar después si el flujo no necesita respuesta inmediata.
¿Cómo evito que un usuario abusivo me funda?
Tres capas combinadas: (1) auth obligatoria — nunca dejes endpoints de IA sin auth; (2) rate limit per-tenant en Redis sliding window (60 requests/min suele ser suficiente para uso humano); (3) budget cap mensual hard — el endpoint retorna 402 Payment Required cuando el tenant excede su límite, y notifica al equipo.
¿Conviene un proxy como Helicone o construir telemetría in-house?
Para startups, Helicone (o Langsmith, Portkey). Free tier hasta 100K requests/mes y te ahorra 1-2 semanas de trabajo. Cuando llegues a 1M+ requests/mes o tengas requerimientos de compliance estrictos, construí in-house. Migrar después es trivial — solo cambia la baseURL del cliente OpenAI.