@dariomvg/create
openaitext-generationnextjsstreamingintegracion

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

  1. Copiá la carpeta lib/openai/ a tu proyecto.
  2. Copiá lib/openai/stream/route.ts a app/api/openai/stream/route.ts.
  3. Definí OPENAI_API_KEY en el .env.local de 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.ts nunca lanzan errores: devuelven { ok: false, error }.
  • streamText de client.ts lanza el AIActionError: usá try/catch.

Rate limiting

Se maneja dentro del mismo sistema de errores, no por separado:

  • generateText / generateStructuredOutput reintentan automáticamente ante un 429 con backoff exponencial (mirá RATE_LIMIT_CONFIG en types.ts), hasta maxRetries. Si se agotan los reintentos, el error se devuelve como RATE_LIMITED con un retryAfter (en segundos) tomado del header retry-after de OpenAI cuando está presente.
  • streamText no 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 como RATE_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.