Migración de aplicaciones Node.js con el SDK de OpenAI: Base URL, Streaming y Structured Outputs
Guía técnica para desarrolladores de Node.js sobre la migración de aplicaciones con el SDK de OpenAI: configuración de base URLs personalizadas, autenticación, streaming de structured outputs y diagnóstico de errores de API.
En esta página
Introducción
Migrar una aplicación Node.js existente que utiliza el SDK oficial openai no requiere reescribir tu lógica de negocio. Dado que muchos proveedores unificados de modelos se adhieren con precisión a las especificaciones REST de OpenAI, adaptar una aplicación existente para que funcione a través de una capa de enrutamiento compatible con OpenAI —como OneMux— solo exige actualizar los parámetros de inicialización del cliente.
Sin embargo, los pipelines en entornos de producción a menudo dependen de funciones avanzadas: cumplimiento estricto de esquemas para la extracción de datos, streaming token por token y recuperación robusta ante errores. Esta guía cubre cómo reconfigurar el SDK de OpenAI para Node.js, ejecutar extracciones estructuradas con esquemas JSON estrictos, manejar respuestas en streaming de forma fiable y diagnosticar errores de ejecución.
Paso 1: Configuración de Base URL y autenticación
El SDK oficial de OpenAI para Node.js (openai v4+) permite anular parámetros de configuración directamente en el constructor de la clase OpenAI. Para migrar tu capa de transporte a un endpoint compatible con OpenAI, configura la propiedad baseURL y define tu clave de API.
Inicialización del cliente SDK
import OpenAI from "openai";
// Initialize the client pointing to an OpenAI-compatible gateway
const client = new OpenAI({
apiKey: process.env.ONEMUX_API_KEY,
baseURL: "https://api.onemux.net/v1",
});
Requisitos previos para la migración
- Variables de entorno: Asegúrate de separar las claves de los proveedores en tu archivo de configuración
.env(por ejemplo,ONEMUX_API_KEYfrente aOPENAI_API_KEY). - Sufijo de ruta: El SDK de OpenAI añade automáticamente rutas como
/chat/completionsa labaseURL. Comprueba siempre que tubaseURLincluya el sufijo/v1(por ejemplo,https://api.onemux.net/v1), a menos que tu gateway gestione el enrutamiento desde la raíz. - Versión del SDK: Verifica que estés ejecutando la versión
openai^4.0.0o superior, ya que las versiones v3 anteriores utilizaban una estructura de configuración distinta (la claseConfiguration).
Para consultar las reglas de enrutamiento admitidas y comenzar con tus credenciales de API, visita la guía de inicio rápido de OneMux.
Paso 2: Extracción estructurada con JSON Schema estricto
Los structured outputs obligan a los modelos a devolver respuestas que se ajusten con exactitud a un JSON Schema proporcionado. Según la documentación de OpenAI sobre structured outputs, establecer "strict": true exige que:
- Todos los campos de los objetos se enumeren en el array
required. - Se defina explícitamente
additionalProperties: falseen todos los esquemas de objetos. - Los campos opcionales se modelen como tipos de unión con
null(por ejemplo,["string", "null"]).
Implementación estándar de Structured Completion
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.ONEMUX_API_KEY,
baseURL: "https://api.onemux.net/v1",
});
// Define strict JSON Schema for invoice extraction
const invoiceExtractionSchema = {
type: "object",
properties: {
vendorName: { type: "string" },
invoiceId: { type: "string" },
totalAmount: { type: "number" },
currency: { type: "string" },
items: {
type: "array",
items: {
type: "object",
properties: {
description: { type: "string" },
quantity: { type: "number" },
unitPrice: { type: "number" },
},
required: ["description", "quantity", "unitPrice"],
additionalProperties: false,
},
},
},
required: ["vendorName", "invoiceId", "totalAmount", "currency", "items"],
additionalProperties: false,
} as const;
async function extractInvoice(rawDocumentText: string) {
const response = await client.chat.completions.create({
model: "gpt-4o",
messages: [
{
role: "system",
content: "Extract normalized invoice records from the provided unstructured input.",
},
{
role: "user",
content: rawDocumentText,
},
],
response_format: {
type: "json_schema",
json_schema: {
name: "invoice_extraction",
strict: true,
schema: invoiceExtractionSchema,
},
},
});
const choice = response.choices[0];
// Check for safety or policy refusals
if (choice.message.refusal) {
throw new Error(`Model refused extraction: ${choice.message.refusal}`);
}
const rawJson = choice.message.content;
if (!rawJson) {
throw new Error("Received empty response payload");
}
return JSON.parse(rawJson);
}
La compatibilidad con gateways downstream depende de si el endpoint del proveedor subyacente admite el modo json_schema con strict: true. Para obtener más detalles sobre las capacidades de los modelos en distintas plataformas, consulta el catálogo de modelos de OneMux.
Paso 3: Manejo de streaming con Structured Outputs
Cuando se activa stream: true junto con response_format, el modelo transmite fragmentos parciales de texto correspondientes a la carga útil JSON. A diferencia del chat de texto estándar, los chunks individuales no se pueden parsear mediante JSON.parse(), ya que representan tokens JSON incompletos.
Acumulación de chunks JSON transmitidos por streaming
Para procesar structured outputs por streaming adecuadamente, acumula los fragmentos delta hasta que la transmisión finalice o canaliza los chunks entrantes a través de un parser incremental de streams JSON.
async function streamInvoiceExtraction(rawDocumentText: string) {
const stream = await client.chat.completions.create({
model: "gpt-4o",
stream: true,
messages: [
{
role: "system",
content: "Extract normalized invoice records from the provided text.",
},
{
role: "user",
content: rawDocumentText,
},
],
response_format: {
type: "json_schema",
json_schema: {
name: "invoice_extraction",
strict: true,
schema: invoiceExtractionSchema,
},
},
});
let accumulatedJson = "";
let refusalReason: string | null = null;
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta;
if (delta?.refusal) {
refusalReason = (refusalReason || "") + delta.refusal;
}
if (delta?.content) {
accumulatedJson += delta.content;
// Emit partial events or log progress if building a reactive UI
}
}
if (refusalReason) {
throw new Error(`Extraction was refused during stream: ${refusalReason}`);
}
try {
return JSON.parse(accumulatedJson);
} catch (err) {
throw new Error(`Failed to parse streamed JSON payload: ${accumulatedJson}`);
}
}
Si tu pipeline utiliza streaming exclusivamente para minimizar el tiempo hasta el primer byte (TTFB) en interfaces de usuario, comprueba que el endpoint downstream soporte Server-Sent Events (SSE) continuos a través de tu proxy de red.
Paso 4: Diagnóstico y resolución de errores
Al migrar llamadas del SDK a una baseURL personalizada, el diagnóstico de errores debe diferenciar entre fallos de proxies de red, desajustes de esquema, límites de tasa y denegaciones (refusals) del modelo.
Errores frecuentes y estrategias de resolución
| Código de error / Tipo | Causa raíz | Diagnóstico y solución |
|---|---|---|
401 Unauthorized | API key no válida o ausente | Confirma que tu apiKey contiene una credencial válida y reconocida por el gateway de destino (por ejemplo, revisa ONEMUX_API_KEY). |
400 BadRequestError | Infracción de esquema (strict: true) | Inspecciona el mensaje de error en busca de palabras clave de JSON schema no admitidas (por ejemplo, la ausencia de additionalProperties: false o campos omitidos en required). |
404 NotFound | Ruta incorrecta o ID de modelo inexistente | Asegúrate de que la baseURL termine en /v1. Confirma que el modelo solicitado exista en la lista de modelos activos del proveedor. |
429 RateLimitError | Se ha alcanzado el límite de concurrencia o gasto | Implementa retroceso exponencial (exponential backoff). Monitorea los límites de cuota a través del panel de control de tu proveedor. |
Refusal (Cuerpo de Choice) | Activación de barreras de seguridad del modelo | El modelo ha generado una denegación explícita. Comprueba choice.message.refusal en lugar de buscar en content. |
Wrapper para el manejo estructurado de errores
import OpenAI from "openai";
async function safeExtractWrapper(fn: () => Promise<any>) {
try {
return await fn();
} catch (error: any) {
if (error instanceof OpenAI.APIError) {
console.error(`OpenAI API Error: status=${error.status}, type=${error.type}`);
console.error(`Message: ${error.message}`);
if (error.status === 400) {
console.error("Verify that your schema complies strictly with JSON Schema constraints.");
} else if (error.status === 401) {
console.error("Authentication failed: Check your bearer token and proxy baseURL.");
} else if (error.status === 429) {
console.warn("Rate limit reached. Queue request for delayed retry.");
}
} else {
console.error("Unexpected application error:", error);
}
throw error;
}
}
Paso 5: Consolidación de proveedores con OneMux
Enrutar las peticiones del SDK de OpenAI a través de OneMux simplifica las operaciones multimodelo sin necesidad de incorporar librerías de terceros o adaptadores de protocolo personalizados.
- Endpoint unificado: Mantén una única interfaz de cliente mientras enrutas tráfico entre modelos utilizando la semántica estándar del SDK de OpenAI.
- Gestión y visibilidad del gasto: Centraliza la facturación, revisa el consumo por modelo y define reglas de presupuesto sin tener que gestionar facturas de múltiples proveedores.
- Modalidad flexible Pay-as-You-Go: Recarga saldo en tu cuenta mediante recargas de crédito flexibles para evitar interrupciones de servicio inesperadas.
Para consultar las tarifas de uso actualizadas y comparar opciones de enrutamiento entre modelos, revisa la guía de precios de OneMux y lee la descripción técnica en la documentación de OneMux.
Fuentes
FAQ
¿Puedo utilizar Zod con el SDK de OpenAI cuando configuro una base URL personalizada?
Sí. Si tu gateway implementa por completo la especificación de structured outputs para `/chat/completions`, métodos auxiliares como `client.beta.chat.completions.parse()` y `zodResponseFormat()` funcionan de forma nativa traduciendo los esquemas de Zod a la carga útil estricta requerida de JSON Schema.
¿Por qué el modelo devuelve un error 400 cuando se usa 'strict: true'?
El motor de structured outputs de OpenAI exige que cada definición de objeto incluya explícitamente `additionalProperties: false` y enumere todas las propiedades declaradas dentro del array `required`. Si falta alguna clave en `required` o se utilizan palabras clave de JSON Schema no compatibles, la API rechaza la solicitud con un error de validación 400.
¿Afecta el cambio de baseURL a la telemetría del SDK o a los tiempos de espera (timeouts)?
Modificar `baseURL` redirige todo el transporte HTTP hacia la nueva URL. Las opciones de timeout y reintentos (como `maxRetries` o `timeout` en el constructor de OpenAI) continúan funcionando exactamente igual en el lado del cliente.
Artículos relacionados
Guides
Migración de una aplicación Node.js con el SDK de OpenAI: Base URL, streaming y triaje con intervención humana
Aprende a migrar una aplicación Node.js existente con el SDK de OpenAI hacia una pasarela compatible como OneMux, gestionar streaming, diagnosticar errores y diseñar un flujo de triaje de soporte con aprobación humana.
Guides
Veo 3.1 Fast frente a Veo 3.1: cómo evaluar el detalle decorativo sin confundirlo con Gemini, Claude u OpenAI
Un marco práctico para evaluar el detalle decorativo en Veo 3.1 Fast Generate Preview frente a Veo 3.1 Generate Preview, integrando modelos de Gemini, Claude y OpenAI en las tareas operativas vinculadas a la generación de video.
Guides
Suscripciones a ChatGPT vs. Claude vs. Gemini vs. Grok: cuándo conviene usar la API de Grok 4.5
Una guía práctica de decisión entre suscripción y API para equipos que evalúan ChatGPT, Claude, Gemini y Grok, con costes de Grok 4.5 y criterios de enrutamiento.
Guides
Cómo usar GPT-5.6 Terra en Claude Code con un proxy de API de IA
Guía práctica de toma de decisiones para desarrolladores que desean ejecutar GPT-5.6 Terra a través de un proxy compatible con Claude Code, manteniendo el acceso a modelos, credenciales y visibilidad del gasto en un solo lugar.