指南OneMux

遷移 Node.js OpenAI SDK 應用程式:自訂 Base URL、串流與結構化輸出

專為 Node.js 開發者打造的 OpenAI SDK 應用遷移技術指南:涵蓋自訂 Base URL 設定、身分驗證管理、串流結構化輸出處理,以及常見 API 錯誤排查。

文章目錄

前言

遷移使用官方 openai SDK 的既有 Node.js 應用程式時,並不需要重寫商業邏輯。由於許多統整型模型供應商嚴格遵循 OpenAI REST 規範,將現有專案轉移至相容 OpenAI 的路由層(例如 OneMux),通常僅需修改客戶端初始化參數。

然而,正式生產環境往往依賴進階功能:用於資料擷取的嚴格 Schema 規範、逐 Token 串流輸出,以及穩健的錯誤復原機制。本指南將深入探討如何重新設定 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. 路徑後綴:OpenAI SDK 會自動在 baseURL 後方附加 /chat/completions 等路由路徑。除非閘道支援根路由轉發,否則請確保 baseURL 包含 /v1 後綴(例如 https://api.onemux.net/v1)。
  3. SDK 版本:確認專案執行的 openai 版本為 ^4.0.0 或更高,因為舊版 v3 採用不同的設定結構(Configuration 類別)。

若需檢視支援的路由規則並取得 API 憑證,請造訪 OneMux 快速入門指南。


步驟 2:利用嚴格 JSON Schema 進行結構化擷取

結構化輸出可強制模型回傳完全符合指定 JSON Schema 的內容。根據 OpenAI 關於結構化輸出的文件規定,設定 "strict": true 必須符合以下要求:

  • 物件的所有欄位都必須列入 required 陣列中。
  • 所有物件架構必須明確設定 additionalProperties: false。
  • 選擇性欄位必須以包含 null 的聯合型別表示(例如 ["string", "null"])。

標準結構化 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);
}

下游閘道的相容性取決於底層模型端點是否支援帶有 strict: true 的 json_schema 模式。各平台模型功能的詳細資訊,請參考 OneMux 模型目錄。


步驟 3:處理結構化輸出的串流回應

當同時啟用 stream: true 與 response_format 時,模型會串流回傳 JSON 內容的字串片段。與一般純文字對話不同,這些單獨的 chunk 代表不完整的 JSON Token,無法直接使用 JSON.parse() 解析。

累積串流 JSON 片段

若要正確處理串流結構化輸出,應在串流結束前累加 delta 片段,或將傳入的 chunk 導向漸進式 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),請確認下游端點能透過網路代理順暢支援連續的伺服器發送事件(SSE)。


步驟 4:錯誤診斷與排查

將 SDK 請求導向自訂 baseURL 時,錯誤診斷應清楚區分網路代理問題、Schema 設定錯誤、速率限制以及模型拒絕回應。

常見錯誤與解決策略

錯誤代碼 / 類型根本原因診斷與修復方式
401 UnauthorizedAPI 金鑰無效或遺失確認 apiKey 包含目標閘道認可的有效憑證(例如檢查 ONEMUX_API_KEY)。
400 BadRequestErrorSchema 違規(strict: true)檢查錯誤訊息中是否有不支援的 JSON Schema 關鍵字(例如缺少 additionalProperties: false 或欄位未列入 required)。
404 NotFound路徑錯誤或模型 ID 不存在確認 baseURL 是否以 /v1 結尾。確認請求的模型存在於該供應商的有效模型清單中。
429 RateLimitError達到並行上限或消費額度上限實作指數退避重試機制(exponential 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 整合多家供應商

透過 OneMux 路由 OpenAI SDK 請求,無需引入第三方套件或自訂通訊協定轉接器,即可大幅簡化多模型維運。

  • 單一整合端點:維持統一的客戶端介面,使用標準 OpenAI SDK 語法在多種模型間切換路由。
  • 用量花費管理與透明度:集中管理帳單、掌握個別模型的詳細使用量,並制定支出規則,告別管理多家供應商發票的繁瑣痛點。
  • 彈性隨用隨付機制:透過靈活的點數儲值管理帳戶,避免非預期的服務中斷。

若需檢視最新用量費率並比較不同模型的路由選項,請參閱 OneMux 定價指南,並閱讀 OneMux 技術文件 中的技術架構概述。


參考來源

常見問題

使用自訂 Base URL 時,OpenAI SDK 還能搭配 Zod 使用嗎?

可以。只要您的閘道完整實作了 `/chat/completions` 的結構化輸出規格,`client.beta.chat.completions.parse()` 與 `zodResponseFormat()` 等輔助函式便能無縫運作,將 Zod schema 自動轉換為符合規定的嚴格 JSON Schema 內容。

為什麼使用「strict: true」時模型會回傳 400 錯誤?

OpenAI 結構化輸出引擎要求每個物件定義都必須明確宣告 `additionalProperties: false`,且每個已定義的屬性都必須列於 `required` 陣列中。若有任何屬性遺漏於 `required` 之外,或使用了不支援的 JSON Schema 關鍵字,API 將會拒絕請求並回傳 400 驗證錯誤。

變更 baseURL 會影響 SDK 的遙測或逾時設定嗎?

覆寫 `baseURL` 僅會將所有 HTTP 傳輸重導向至新的 URL。用戶端層級的逾時與重試設定(例如 OpenAI 建構函式中的 `maxRetries` 或 `timeout`)功能完全不受影響,運作機制維持一致。

相關文章