OpenAI Chat — Configuración
Módulo de chat listo para integrar en Next.js (App Router) usando el SDK de OpenAI. Cubre chat completion, chat con streaming, conversaciones persistentes con historial de mensajes, gestión de contexto, speech-to-text y text-to-speech.
Es un módulo separado e independiente del módulo OpenAI Text Generation: no importa nada de él ni depende de él de ninguna manera.
Dependencias
openai— SDK oficial de OpenAI
Variables de entorno
Mirá .env.local:
OPENAI_API_KEY— obligatoria.
Estructura de archivos
lib/openai-chat/
├── types.ts # Types, default model/token config, rate limit config
├── store.ts # ChatHistoryStore interface + in-memory reference implementation
├── context.ts # Trims message history to fit a character budget before calling OpenAI
├── service.ts # Internal logic (SDK calls, store/context orchestration, error normalization)
├── actions.ts # "use server" — public API for Server Components
├── client.ts # Public API for Client Components (streaming chat, text-to-speech)
├── stream/
│ ├── handler.ts # Internal — builds the chat ReadableStream
│ └── route.ts # Copy to app/api/openai-chat/stream/route.ts
├── audio/
│ ├── handler.ts # Internal — builds the speech ReadableStream
│ └── route.ts # Copy to app/api/openai-chat/speech/route.ts
└── .env.local
⚠️ Antes de desplegar: reemplazá el store en memoria
store.ts incluye createInMemoryChatStore() solo para desarrollo local.
Las conversaciones y los mensajes se pierden en cada reinicio del servidor y no se
comparten entre instancias serverless. Implementá la interfaz ChatHistoryStore
contra tu propia base de datos y cambiala en getChatStore(); no hace falta
tocar ningún otro archivo.
Pasos de configuración
- Copiá la carpeta
lib/openai-chat/a tu proyecto. - Copiá
lib/openai-chat/stream/route.tsaapp/api/openai-chat/stream/route.ts. - Copiá
lib/openai-chat/audio/route.tsaapp/api/openai-chat/speech/route.ts. - Definí
OPENAI_API_KEYen el.env.localde tu proyecto. - Antes de pasar a producción, reemplazá el store en memoria (mirá arriba).
Uso
Gestión de conversaciones (Server Component / server action)
import {
createConversationAction,
listConversationsAction,
getConversationAction,
renameConversationAction,
deleteConversationAction,
} from "@/lib/openai-chat/actions";
const created = await createConversationAction("Trip planning");
if (created.ok) {
const conversationId = created.data.id;
}
const list = await listConversationsAction();
if (list.ok) {
console.log(list.data); // Conversation[]
}
const details = await getConversationAction(conversationId);
if (details.ok) {
console.log(details.data.conversation, details.data.messages);
}
Enviar un mensaje (sin streaming)
import { sendMessageAction } from "@/lib/openai-chat/actions";
const result = await sendMessageAction({
conversationId,
message: "What's a good 3-day itinerary for Lisbon?",
systemPrompt: "You are a helpful travel assistant.",
});
if (result.ok) {
console.log(result.data.message.content);
} else {
console.error(result.error.message);
}
Respuesta con streaming (Client Component)
"use client";
import { useState } from "react";
import { streamChatMessage } from "@/lib/openai-chat/client";
import type { AIActionError } from "@/lib/openai-chat/types";
export function ChatBox({ conversationId }: { conversationId: string }) {
const [reply, setReply] = useState("");
const [error, setError] = useState<AIActionError | null>(null);
async function handleSend(message: string) {
setReply("");
setError(null);
try {
for await (const chunk of streamChatMessage({ conversationId, message })) {
setReply((prev) => prev + chunk);
}
} catch (err) {
setError(err as AIActionError);
}
}
return (
<div>
{error && <p>{error.message}</p>}
<p>{reply}</p>
</div>
);
}
Speech-to-text (Client Component → Server Action)
"use client";
import { transcribeAudioAction } from "@/lib/openai-chat/actions";
async function handleRecordingStop(blob: Blob) {
const formData = new FormData();
formData.set("audio", blob, "recording.webm");
formData.set("language", "en");
const result = await transcribeAudioAction(formData);
if (result.ok) {
console.log(result.data.text);
}
}
Text-to-speech (Client Component)
"use client";
import { textToSpeech } from "@/lib/openai-chat/client";
async function handlePlay(text: string) {
const blob = await textToSpeech({ text });
const audio = new Audio(URL.createObjectURL(blob));
audio.play();
}
Manejo de errores
Mismo formato normalizado en todo el módulo:
interface AIActionError {
code: "RATE_LIMITED" | "INVALID_REQUEST" | "AUTH_ERROR" | "TIMEOUT" | "NOT_FOUND" | "UNKNOWN";
message: string;
retryable: boolean;
retryAfter?: number; // seconds, only set for RATE_LIMITED
}
- Las funciones de
actions.tsnunca lanzan errores: devuelven{ ok: false, error }. streamChatMessageytextToSpeechdeclient.tslanzan elAIActionError: usá try/catch.NOT_FOUNDse devuelve cuando unconversationIdno existe en el store.
Rate limiting
Mismo enfoque que el módulo Text Generation: chatCompletion y
transcribeAudio reintentan automáticamente ante un 429 con backoff exponencial (mirá
RATE_LIMIT_CONFIG en types.ts). streamChatCompletion y
synthesizeSpeech no reintentan automáticamente: un 429 se devuelve de inmediato como
RATE_LIMITED con un retryAfter en segundos.
Gestión de contexto
context.ts recorta los mensajes más antiguos para que la conversación entre dentro de
DEFAULT_CONTEXT_CHAR_BUDGET caracteres antes de cada request. Es una aproximación
por cantidad de caracteres, no un conteo exacto de tokens: se dejó afuera un tokenizer
real (por ejemplo tiktoken) para mantener el módulo sin dependencias.
Si necesitás un presupuesto exacto de tokens, cambiá la lógica de context.ts.
Limitaciones conocidas
- Store en memoria: mirá la advertencia de arriba; reemplazalo antes de desplegar.
- Fallos a mitad del streaming: si un stream de chat falla después de haber empezado, la conexión simplemente se cierra y la respuesta parcial no se guarda en el historial (un mensaje incompleto del asistente podría confundir el siguiente turno). Tratá un stream inesperadamente corto como un fallo leve en la UI.
- Recorte de contexto: basado en caracteres, no en tokens exactos; mirá arriba.