GuidesOneMux

Миграция приложений на Node.js OpenAI SDK: Base URL, стриминг и структурированный вывод

Техническое руководство для Node.js-разработчиков по миграции приложений с OpenAI SDK: настройка кастомного base URL, аутентификация, стриминг структурированного вывода и диагностика ошибок API.

В этой статье

Введение

Миграция существующего Node.js-приложения, использующего официальный SDK openai, не требует переписывания бизнес-логики. Поскольку многие унифицированные провайдеры строго следуют спецификации OpenAI REST, для перевода приложения на OpenAI-совместимый уровень маршрутизации — такой как OneMux — достаточно лишь скорректировать параметры инициализации клиента.

Тем не менее, продакшен-пайплайны часто задействуют продвинутые возможности: строгое соблюдение схем при извлечении данных, посимвольный стриминг и надежное восстановление после сбоев. В этом руководстве подробно рассмотрены перенастройка Node.js OpenAI SDK, извлечение структурированных данных с помощью JSON Schema, надежная обработка потоковых ответов и диагностика ошибок во время выполнения.


Шаг 1: Настройка Base URL и аутентификации

Официальный OpenAI Node.js SDK (openai v4+) поддерживает переопределение параметров конфигурации прямо в конструкторе класса OpenAI. Чтобы переключить транспортный уровень на OpenAI-совместимый эндпоинт, настройте свойство baseURL и передайте нужный API-ключ.

Инициализация клиента 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",
});

Предварительные требования для миграции

  1. Переменные окружения: Убедитесь, что ключи разных провайдеров разделены в конфигурации .env (например, ONEMUX_API_KEY и OPENAI_API_KEY).
  2. Суффикс пути: SDK OpenAI автоматически добавляет пути маршрутов, такие как /chat/completions, к значению baseURL. Убедитесь, что ваш baseURL содержит суффикс /v1 (например, https://api.onemux.net/v1), если только используемый шлюз не обрабатывает маршрутизацию от корня.
  3. Версия SDK: Проверьте, что в проекте используется openai версии ^4.0.0 или выше, поскольку устаревшие версии v3 использовали другую структуру конфигурации (класс Configuration).

Чтобы ознакомиться с поддерживаемыми правилами маршрутизации и получить учетные данные API, посетите раздел OneMux Quickstart.


Шаг 2: Извлечение структурированных данных со строгой схемой JSON Schema

Структурированный вывод заставляет модели возвращать ответы, в точности соответствующие заданной JSON Schema. Согласно документации OpenAI по структурированному выводу, указание "strict": true накладывает следующие требования:

  • Все поля объекта должны быть перечислены в массиве required.
  • Свойство additionalProperties: false должно быть явно задано во всех схемах объектов.
  • Необязательные поля должны моделироваться как типы-объединения с null (например, ["string", "null"]).

Стандартная реализация структурированного вывода

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);
}

Совместимость со сторонними шлюзами зависит от того, поддерживает ли конечный эндпоинт провайдера режим json_schema со значением strict: true. Подробную информацию о возможностях моделей на различных платформах можно найти в каталоге OneMux Models.


Шаг 3: Обработка стриминга со структурированным выводом

Когда флаг stream: true используется совместно с response_format, модель передает фрагменты JSON-строки по частям. В отличие от стандартного текстового чата, отдельные чанки нельзя разобрать с помощью JSON.parse(), так как они представляют собой неполные токены JSON.

Накопление потоковых JSON-чанков

Для корректного стриминга структурированного вывода необходимо накапливать дельта-фрагменты до полного завершения потока либо передавать входящие чанки через инкрементальный парсер потокового 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}`);
  }
}

Если стриминг в вашем пайплайне используется исключительно для снижения времени до первого байта (TTFB) в интерфейсах, убедитесь, что целевой эндпоинт поддерживает непрерывные Server-Sent Events (SSE) через ваш сетевой прокси.


Шаг 4: Диагностика ошибок и устранение неполадок

При переводе вызовов SDK на кастомный baseURL в процессе диагностики важно четко разделять сбои сетевого прокси, ошибки в схемах данных, превышение лимитов запросов и отказы самой модели.

Типичные ошибки и стратегии их устранения

Код / тип ошибкиПервопричинаДиагностика и исправление
401 UnauthorizedНедействительный или отсутствующий API-ключУбедитесь, что в apiKey переданы корректные учетные данные, принимаемые целевым шлюзом (например, проверьте ONEMUX_API_KEY).
400 BadRequestErrorНарушение требований схемы (strict: true)Проверьте сообщение об ошибке на наличие неподдерживаемых ключевых слов JSON Schema (например, отсутствует additionalProperties: false или не все поля указаны в required).
404 NotFoundНеверный путь или идентификатор моделиУбедитесь, что baseURL заканчивается на /v1. Проверьте, присутствует ли запрошенная модель в списке активных моделей провайдера.
429 RateLimitErrorПревышен лимит параллельных запросов или бюджетРеализуйте алгоритм экспоненциальной задержки (backoff). Отслеживайте лимиты квот в панели управления провайдера.
Refusal (Тело choice)Сработали защитные механизмы моделиМодель сформировала явный отказ. Проверяйте свойство choice.message.refusal, а не content.

Обертка для структурированной обработки ошибок

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;
  }
}

Шаг 5: Консолидация провайдеров с помощью OneMux

Маршрутизация запросов OpenAI SDK через OneMux упрощает работу с несколькими моделями без привлечения сторонних библиотек или разработки собственных адаптеров протокола.

  • Единый эндпоинт: сохраняйте привычный интерфейс клиента, обращаясь к разным моделям через стандартную семантику OpenAI SDK.
  • Контроль и прозрачность расходов: централизуйте биллинг, анализируйте расход токенов по каждой модели и устанавливайте лимиты затрат без необходимости администрировать счета от десятка провайдеров.
  • Гибкая оплата по факту (Pay-as-You-Go): пополняйте баланс гибкими платежами, избегая неожиданных остановок сервиса.

Актуальные тарифы и варианты маршрутизации между моделями можно найти в OneMux Pricing Guide, а технический обзор сервиса — в OneMux Documentation.


Источники

Вопросы

Можно ли использовать Zod с OpenAI SDK при работе через кастомный base URL?

Да. Если ваш шлюз полностью реализует спецификацию структурированного вывода для `/chat/completions`, вспомогательные методы вроде `client.beta.chat.completions.parse()` и `zodResponseFormat()` работают без изменений, автоматически преобразуя Zod-схемы в требуемый формат strict JSON Schema.

Почему модель возвращает ошибку 400 при использовании 'strict: true'?

Движок структурированного вывода OpenAI требует, чтобы в каждом определении объекта явно присутствовало поле `additionalProperties: false`, а все объявленные свойства были перечислены в массиве `required`. Если какой-либо ключ не указан в `required` или используются неподдерживаемые ключевые слова JSON Schema, API отклоняет запрос с ошибкой валидации 400.

Влияет ли изменение baseURL на телеметрию SDK или таймауты?

Переопределение `baseURL` перенаправляет весь HTTP-трафик на новый адрес. Параметры таймаутов и повторных попыток (такие как `maxRetries` или `timeout` в конструкторе OpenAI) продолжают работать на стороне клиента точно так же.

Похожие статьи

Guides

Миграция приложения на Node.js OpenAI SDK: Base URL, стриминг и триаж с участием человека

Узнайте, как перенести существующее Node.js-приложение на базе OpenAI SDK на OpenAI-совместимый шлюз вроде OneMux, настроить стриминг, диагностировать ошибки и реализовать триаж тикетов техподдержки с подтверждением действий оператором.

Guides

GPT-5.6 Luna API: цены, задержка и компромиссы надёжности

Изучите цены, задержку, доступность и пропускную способность GPT-5.6 Luna API. Узнайте, как получить доступ к модели через единый AI-шлюз OneMux и когда выбрать Luna вместо премиальных моделей.

Guides

GPT-5.6 Sol для клиентской поддержки: как выбрать подходящую модель OpenAI API

Узнайте, как GPT-5.6 Sol подходит для автоматизации клиентской поддержки, сколько стоит ее использование и как получить к ней доступ через унифицированный OpenAI-совместимый API OneMux.

Guides

GPT-5.5 vs 5.6 Luna vs Terra vs Sol: какая модель лучше для задач с длинным контекстом?

Изучите семейство GPT-5.6 для задач с длинным контекстом. Сравните цены и производительность Luna, Terra и Sol и узнайте, как OneMux упрощает доступ.