OpenAI Text Generation — Configuración
Módulo de generación de texto listo para integrar en Next.js (App Router) usando el SDK de OpenAI. Cubre generación de texto, streaming, prompts de sistema/usuario, configuración de temperatura y salida estructurada (JSON schema).
Dependencias
openai— SDK oficial de OpenAI
Variables de entorno
Mirá .env.local:
OPENAI_API_KEY— obligatoria.
Estructura de archivos
lib/openai/
├── service.ts # Internal logic (SDK calls, error normalization). Do not import directly.
├── actions.ts # "use server" — public API for Server Components (generateText, generateStructuredOutput)
├── client.ts # Public API for Client Components (streamText)
├── types.ts # Types, default model/token config, rate limit config
├── stream/
│ ├── handler.ts # Internal — builds the ReadableStream. Do not import directly.
│ └── route.ts # Copy to app/api/openai/stream/route.ts
└── .env.local
Pasos de configuración
- Copiá la carpeta
lib/openai/a tu proyecto. - Copiá
lib/openai/stream/route.tsaapp/api/openai/stream/route.ts. - Definí
OPENAI_API_KEYen el.env.localde tu proyecto.
Uso
Desde un Server Component / server action (sin streaming)
import { generateTextAction } from "@/lib/openai/actions";
const result = await generateTextAction({
prompt: "Write a haiku about the ocean",
systemPrompt: "You are a concise poet.",
temperature: 0.8,
});
if (result.ok) {
console.log(result.data.text);
} else {
// result.error: { code, message, retryable, retryAfter? }
console.error(result.error.message);
}
Salida estructurada
import { generateStructuredOutputAction } from "@/lib/openai/actions";
const result = await generateStructuredOutputAction<{ title: string; tags: string[] }>({
prompt: "Summarize this article: ...",
schemaName: "article_summary",
schema: {
type: "object",
properties: {
title: { type: "string" },
tags: { type: "array", items: { type: "string" } },
},
required: ["title", "tags"],
additionalProperties: false,
},
});
if (result.ok) {
console.log(result.data.data.title);
}
Desde un Client Component (streaming)
"use client";
import { useState } from "react";
import { streamText } from "@/lib/openai/client";
import type { AIActionError } from "@/lib/openai/types";
export function Chat() {
const [text, setText] = useState("");
const [error, setError] = useState<AIActionError | null>(null);
async function handleGenerate() {
setText("");
setError(null);
try {
for await (const chunk of streamText({ prompt: "Tell me a story" })) {
setText((prev) => prev + chunk);
}
} catch (err) {
setError(err as AIActionError);
}
}
return (
<div>
<button onClick={handleGenerate}>Generate</button>
{error && (
<p>
{error.code === "RATE_LIMITED"
? `Too many requests — try again in ${error.retryAfter}s`
: error.message}
</p>
)}
<p>{text}</p>
</div>
);
}
Manejo de errores
Todas las funciones devuelven o lanzan el mismo formato normalizado en lugar de los errores crudos del SDK de OpenAI:
interface AIActionError {
code: "RATE_LIMITED" | "INVALID_REQUEST" | "AUTH_ERROR" | "TIMEOUT" | "UNKNOWN";
message: string;
retryable: boolean;
retryAfter?: number; // seconds, only set for RATE_LIMITED
}
- Las funciones de
actions.tsnunca lanzan errores: devuelven{ ok: false, error }. streamTextdeclient.tslanza elAIActionError: usá try/catch.
Rate limiting
Se maneja dentro del mismo sistema de errores, no por separado:
generateText/generateStructuredOutputreintentan automáticamente ante un 429 con backoff exponencial (miráRATE_LIMIT_CONFIGentypes.ts), hastamaxRetries. Si se agotan los reintentos, el error se devuelve comoRATE_LIMITEDcon unretryAfter(en segundos) tomado del headerretry-afterde OpenAI cuando está presente.streamTextno reintenta automáticamente (un stream no se puede reanudar de forma segura a mitad de camino): un 429 antes de que empiece el stream se devuelve de inmediato comoRATE_LIMITED.
Limitación conocida
Si un request de streaming falla después de que la respuesta ya empezó (poco común:
normalmente solo ante un fallo de red/API a mitad del stream), la conexión simplemente se
cierra. En ese punto no se puede enviar un error en JSON porque la respuesta HTTP
ya empezó como text/plain. Manejalo en la UI tratando un stream
inesperadamente corto o incompleto como un fallo leve.