GuidesOneMux

Node.js版OpenAI SDKアプリケーションの移行:Base URL、ストリーミング、構造化出力の実装

Node.js開発者向けOpenAI SDK移行ガイド:カスタムBase URLの設定、認証管理、ストリーミング構造化出力の処理、およびAPIエラーの診断方法を解説します。

目次

はじめに

公式の openai SDKを使用している既存のNode.jsアプリケーションを移行する際、ビジネスロジックを書き直す必要はありません。多くの統合モデルプロバイダーはOpenAIのREST仕様に厳密に準拠しているため、OneMux のようなOpenAI互換ルーティングレイヤー経由で動作させるには、クライアントの初期化パラメータを更新するだけで済みます。

しかし、本番環境のパイプラインでは、データ抽出のための厳格なスキーマ準拠、トークン単位のストリーミング、堅牢なエラーリカバリといった高度な機能に依存することが少なくありません。本ガイドでは、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 設定内でプロバイダーごとのキーを分離して管理してください(例: OPENAI_API_KEY と ONEMUX_API_KEY を分ける)。
  2. パス接尾辞: OpenAIのSDKは、baseURL の末尾に /chat/completions などのルートパスを自動的に付加します。ゲートウェイがルートルーティングを処理する場合を除き、baseURL には常に /v1 サフィックス(例: https://api.onemux.net/v1)が含まれていることを確認してください。
  3. SDKバージョン: 旧バージョンであるv3系は異なる設定構造(Configuration クラス)を使用していたため、実行環境が openai バージョン ^4.0.0 以上であることを確認してください。

サポートされているルーティングルールを確認し、APIクレデンシャルを取得して開始するには、OneMuxクイックスタート を参照してください。


ステップ2:厳格なJSON Schemaによる構造化抽出

構造化出力(Structured Outputs)を使用すると、指定された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);
}

ダウンストリームゲートウェイとの互換性は、基盤となるプロバイダーのエンドポイントが strict: true を指定した json_schema モードをサポートしているかどうかに依存します。各プラットフォームにおけるモデル機能の詳細については、OneMuxモデルカタログ を確認してください。


ステップ3:構造化出力でのストリーミング処理

response_format と併せて stream: true を有効にすると、モデルはJSONペイロードの部分的な文字列フラグメントをストリーミングします。通常のテキストチャットとは異なり、個々のチャンクは不完全なJSONトークンを表すため、単体で JSON.parse() を実行することはできません。

ストリーミングされた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}`);
  }
}

パイプラインがUIにおける初期応答時間(TTFB)の最小化のみを目的にストリーミングを使用している場合は、使用しているネットワークプロキシを通過してダウンストリームエンドポイントが継続的なServer-Sent Events(SSE)をサポートしていることを確認してください。


ステップ4:エラー診断とトラブルシューティング

SDKの呼び出し先をカスタムの baseURL に移行する場合、エラー診断ではネットワークプロキシの障害、スキーマ設定ミス、レート制限、モデルの拒否(refusal)を適切に切り分ける必要があります。

代表的なエラーと解決戦略

エラーコード / 種類根本原因診断と修正方法
401 UnauthorizedAPIキーの無効または欠落apiKey にターゲットゲートウェイで認識される有効なクレデンシャルが含まれているか確認します(例: ONEMUX_API_KEY の確認)。
400 BadRequestErrorスキーマ違反(strict: true)エラーメッセージを確認し、サポートされていないJSON Schemaキーワードがないか検証します(例: additionalProperties: false の欠落や、required 内のフィールド不足)。
404 NotFound不正なパスまたはモデルIDbaseURL の末尾が /v1 で終わっているか確認します。要求したモデルがプロバイダーのアクティブなモデルリストに存在することを確認します。
429 RateLimitError同時実行数または利用金額上限への到達エクスポネンシャルバックオフを実装します。プロバイダーのダッシュボードでクォータ制限を監視します。
Refusal(Choice本文)モデルのセーフティガードレールが作動モデルが明示的な拒否を生成しました。content ではなく choice.message.refusal を確認してください。

構造化エラーハンドリングラッパー

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セマンティクスを用いて複数モデル間でルーティングしながら、単一のクライアントインターフェースを維持します。
  • 利用コスト管理と可視化: 複数のプロバイダーからの請求書を個別に管理することなく、請求を一本化し、モデルごとの利用状況を確認して支出ルールを設定できます。
  • 柔軟な従量課金制: 柔軟なクレジットチャージを通じてアカウントへ資金をデポジットし、予期せぬサービス中断を防ぎます。

現在の利用料金を確認し、モデル間のルーティングオプションを比較するには、OneMux料金ガイド を参照し、技術的な概要については OneMuxドキュメント をご覧ください。


参考情報

よくある質問

カスタムBase URLを使用している場合でも、OpenAI SDKでZodを利用できますか?

はい、利用可能です。接続先のゲートウェイが `/chat/completions` の構造化出力仕様を完全に実装している場合、`client.beta.chat.completions.parse()` や `zodResponseFormat()` などのヘルパーメソッドは、Zodスキーマを要求される厳格なJSON Schemaペイロードへ正しく変換するため、問題なくシームレスに機能します。

「strict: true」を使用すると、モデルが400エラーを返すのはなぜですか?

OpenAIの構造化出力エンジンでは、すべてのオブジェクト定義に `additionalProperties: false` を明示的に含め、宣言されたすべてのプロパティを `required` 配列内にリストすることが求められます。`required` から欠落しているキーがある場合や、サポートされていないJSON Schemaキーワードが含まれている場合、APIは400バリデーションエラーを返してリクエストを拒否します。

baseURLを変更すると、SDKのテレメトリやタイムアウトに影響しますか?

`baseURL` の上書きは、すべてのHTTPトランスポートの送信先を新しいURLへリダイレクトします。タイムアウトやリトライオプション(OpenAIコンストラクタ内の `maxRetries` や `timeout` など)は、クライアント側で従来とまったく同様に機能し続けます。

関連記事