GuidesOneMux

Di chuyển ứng dụng Node.js dùng OpenAI SDK: Base URL, Streaming và Structured Outputs

Hướng dẫn kỹ thuật dành cho lập trình viên Node.js khi di chuyển ứng dụng dùng OpenAI SDK: cấu hình base URL tùy chỉnh, quản lý xác thực, xử lý structured outputs dạng streaming và chẩn đoán lỗi API.

Mục lục

Giới thiệu

Việc di chuyển một ứng dụng Node.js hiện có đang sử dụng SDK chính thức openai không đòi hỏi bạn phải viết lại logic nghiệp vụ. Vì nhiều nhà cung cấp mô hình hợp nhất tuân thủ nghiêm ngặt các đặc tả REST của OpenAI, việc điều chỉnh ứng dụng hiện có để chạy qua một lớp định tuyến tương thích với OpenAI—chẳng hạn như OneMux—chỉ yêu cầu cập nhật các tham số khởi tạo client.

Tuy nhiên, các pipeline trong môi trường production thường phụ thuộc vào các tính năng nâng cao: tính tuân thủ schema nghiêm ngặt khi trích xuất dữ liệu, streaming từng token và khả năng khôi phục lỗi mạnh mẽ. Hướng dẫn này sẽ hướng dẫn cách cấu hình lại Node.js OpenAI SDK, thực thi trích xuất JSON schema có cấu trúc, xử lý phản hồi streaming đáng tin cậy và chẩn đoán lỗi trong quá trình thực thi.


Bước 1: Cấu hình Base URL và xác thực

Node.js SDK chính thức của OpenAI (openai v4+) cho phép ghi đè cấu hình trực tiếp bên trong constructor của lớp OpenAI. Để di chuyển tầng truyền tải sang một endpoint tương thích với OpenAI, hãy định cấu hình thuộc tính baseURL và thiết lập API key của bạn.

Khởi tạo SDK Client

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

Điều kiện tiên quyết trước khi di chuyển

  1. Biến môi trường: Đảm bảo phân tách các khóa API của từng nhà cung cấp trong cấu hình .env của bạn (ví dụ: ONEMUX_API_KEY thay vì OPENAI_API_KEY).
  2. Hậu tố đường dẫn: OpenAI SDK tự động nối thêm các đường dẫn định tuyến như /chat/completions vào baseURL. Luôn đảm bảo rằng baseURL của bạn có chứa hậu tố /v1 (ví dụ: https://api.onemux.net/v1) trừ khi gateway của bạn xử lý định tuyến gốc.
  3. Phiên bản SDK: Xác minh rằng bạn đang chạy phiên bản openai từ ^4.0.0 trở lên, vì các phiên bản v3 cũ hơn sử dụng cấu trúc cấu hình khác (lớp Configuration).

Để xem lại các quy tắc định tuyến được hỗ trợ và bắt đầu với thông tin xác thực API, hãy truy cập OneMux Quickstart.


Bước 2: Trích xuất có cấu trúc với JSON Schema nghiêm ngặt

Structured outputs buộc các mô hình phải trả về kết quả tuân thủ chính xác theo một JSON Schema được cung cấp. Theo tài liệu của OpenAI về structured outputs, việc đặt "strict": true đòi hỏi:

  • Tất cả các trường của object phải được liệt kê trong mảng required.
  • additionalProperties: false phải được xác định rõ ràng trên tất cả schema của object.
  • Các trường tùy chọn phải được mô hình hóa dưới dạng union types với null (ví dụ: ["string", "null"]).

Triển khai Structured Completion tiêu chuẩn

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

Khả năng tương thích với các gateway hạ nguồn phụ thuộc vào việc endpoint của nhà cung cấp cơ bản có hỗ trợ chế độ json_schema với strict: true hay không. Để biết chi tiết về khả năng hỗ trợ mô hình trên các nền tảng, hãy xem Danh mục mô hình OneMux.


Bước 3: Xử lý Streaming với Structured Outputs

Khi kích hoạt stream: true cùng với response_format, mô hình sẽ stream các đoạn chuỗi phân đoạn của payload JSON. Khác với chat dạng văn bản thông thường, các chunk riêng lẻ không thể được parse trực tiếp bằng JSON.parse() vì chúng đại diện cho các token JSON chưa hoàn chỉnh.

Tích lũy các JSON chunk trong luồng Stream

Để stream structured outputs đúng cách, hãy tích lũy các đoạn delta cho đến khi luồng stream hoàn tất, hoặc chuyển tiếp các chunk nhận được qua một bộ parser luồng JSON tăng dần.

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

Nếu pipeline của bạn chỉ sử dụng streaming nhằm mục đích tối ưu hóa thời gian nhận byte đầu tiên (TTFB) trên giao diện người dùng, hãy xác minh rằng endpoint hạ nguồn có hỗ trợ Server-Sent Events (SSE) liên tục thông qua network proxy của bạn.


Bước 4: Chẩn đoán và xử lý sự cố lỗi

Khi di chuyển các lệnh gọi SDK sang một baseURL tùy chỉnh, việc chẩn đoán lỗi cần phân biệt rõ ràng giữa lỗi network proxy, cấu hình schema sai, giới hạn tốc độ (rate limit) và việc mô hình từ chối phản hồi (refusals).

Các lỗi phổ biến và chiến lược xử lý

Mã lỗi / Phân loạiNguyên nhân gốc rễChẩn đoán & Cách khắc phục
401 UnauthorizedAPI key không hợp lệ hoặc bị thiếuXác nhận rằng apiKey của bạn chứa thông tin xác thực hợp lệ được gateway mục tiêu công nhận (ví dụ: kiểm tra ONEMUX_API_KEY).
400 BadRequestErrorVi phạm schema (strict: true)Kiểm tra thông báo lỗi để tìm các từ khóa JSON schema không được hỗ trợ (ví dụ: thiếu additionalProperties: false hoặc thiếu các trường trong required).
404 NotFoundSai đường dẫn hoặc sai Model IDĐảm bảo baseURL kết thúc bằng /v1. Xác nhận mô hình được yêu cầu có tồn tại trong danh sách mô hình đang hoạt động của nhà cung cấp.
429 RateLimitErrorChạm ngưỡng đồng thời hoặc hạn mức chi tiêuTriển khai cơ chế backoff theo cấp số nhân (exponential backoff). Giám sát hạn mức quota thông qua dashboard của nhà cung cấp.
Refusal (Body của Choice)Kích hoạt rào cản an toàn (safety guardrails) của mô hìnhMô hình đã tạo ra phản hồi từ chối rõ ràng. Hãy kiểm tra choice.message.refusal thay vì tìm trong content.

Trình bọc (wrapper) xử lý lỗi có cấu trúc

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

Bước 5: Hợp nhất các nhà cung cấp thông qua OneMux

Định tuyến các yêu cầu OpenAI SDK qua OneMux giúp đơn giản hóa các hoạt động đa mô hình mà không yêu cầu thêm thư viện bên thứ ba hoặc các bộ điều hợp giao thức tùy chỉnh.

  • Endpoint hợp nhất: Duy trì một giao diện client duy nhất trong khi vẫn định tuyến qua nhiều mô hình bằng ngữ nghĩa tiêu chuẩn của OpenAI SDK.
  • Quản lý & Giám sát chi tiêu: Tập trung hóa việc thanh toán, xem xét mức sử dụng theo từng mô hình và thiết lập các quy tắc chi tiêu mà không cần quản lý hóa đơn từ nhiều nhà cung cấp khác nhau.
  • Trả phí linh hoạt (Pay-as-You-Go): Nạp tiền vào tài khoản thông qua các gói tín dụng linh hoạt, giúp ngăn ngừa sự cố gián đoạn dịch vụ ngoài ý muốn.

Để xem bảng giá sử dụng hiện tại và so sánh các tùy chọn định tuyến giữa các mô hình, hãy tham khảo Bảng giá OneMux và đọc tài liệu tổng quan kỹ thuật trong Tài liệu OneMux.


Tài liệu tham khảo

FAQ

Tôi có thể sử dụng Zod với OpenAI SDK khi dùng base URL tùy chỉnh không?

Có. Nếu gateway của bạn triển khai đầy đủ đặc tả structured outputs cho `/chat/completions`, các phương thức tiện ích như `client.beta.chat.completions.parse()` và `zodResponseFormat()` sẽ hoạt động bình thường bằng cách chuyển đổi Zod schema thành payload JSON Schema nghiêm ngặt theo yêu cầu.

Tại sao mô hình trả về lỗi 400 khi sử dụng 'strict: true'?

Cơ chế structured outputs của OpenAI yêu cầu mọi định nghĩa object phải khai báo rõ ràng `additionalProperties: false` và liệt kê tất cả các thuộc tính đã khai báo bên trong mảng `required`. Nếu thiếu bất kỳ khóa nào trong `required`, hoặc nếu xuất hiện các từ khóa JSON Schema không được hỗ trợ, API sẽ từ chối yêu cầu với lỗi xác thực 400.

Việc thay đổi baseURL có ảnh hưởng đến khả năng đo lường (telemetry) hoặc cơ chế timeout của SDK không?

Việc ghi đè `baseURL` sẽ chuyển hướng toàn bộ luồng truyền tải HTTP đến URL mới. Các tùy chọn cấu hình timeout và thử lại (chẳng hạn như `maxRetries` hoặc `timeout` trong hàm khởi tạo của OpenAI) vẫn hoạt động hoàn toàn bình thường ở phía client.

Bài viết liên quan