GuidesOneMux

Migrating Node.js OpenAI SDK Applications: Base URL, Streaming, and Structured Outputs

A technical guide for Node.js developers migrating OpenAI SDK applications: configuring custom base URLs, managing authentication, handling streaming structured outputs, and diagnosing API errors.

On this page

Introduction

Migrating an existing Node.js application that uses the official openai SDK does not require rewriting your business logic. Because many unified model providers adhere strictly to the OpenAI REST specifications, adapting an existing application to run through an OpenAI-compatible routing layer—such as OneMux—requires only updating your client initialization parameters.

However, production pipelines often rely on advanced features: strict schema compliance for data extraction, token-by-token streaming, and robust error recovery. This guide covers how to reconfigure the Node.js OpenAI SDK, execute structured JSON schema extraction, handle streaming responses reliably, and diagnose execution errors.


Step 1: Base URL and Authentication Configuration

The official OpenAI Node.js SDK (openai v4+) accepts configuration overrides directly inside the OpenAI class constructor. To migrate your transport layer to an OpenAI-compatible endpoint, configure the baseURL property and set your API key.

SDK Client Initialization

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

Migration Prerequisites

  1. Environment Variables: Ensure you separate provider keys in your .env configuration (for example, ONEMUX_API_KEY vs. OPENAI_API_KEY).
  2. Path Suffix: OpenAI's SDK automatically appends route paths like /chat/completions to the baseURL. Always ensure your baseURL includes the /v1 suffix (e.g., https://api.onemux.net/v1) unless your gateway handles root routing.
  3. SDK Version: Verify that you are running openai version ^4.0.0 or higher, as older v3 versions used a different configuration structure (Configuration class).

To review supported routing rules and get started with API credentials, visit the OneMux Quickstart.


Step 2: Structured Extraction with Strict JSON Schema

Structured outputs force models to return output conforming exactly to a provided JSON Schema. According to OpenAI's documentation on structured outputs, setting "strict": true requires that:

  • All object fields are listed in the required array.
  • additionalProperties: false is explicitly defined on all object schemas.
  • Optional fields are modeled as union types with null (e.g., ["string", "null"]).

Standard Structured Completion Implementation

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

Compatibility with downstream gateways depends on whether the underlying provider endpoint supports json_schema mode with strict: true. For details on model capabilities across platforms, review the OneMux Models catalog.


Step 3: Handling Streaming with Structured Outputs

When stream: true is enabled alongside response_format, the model streams partial string fragments of the JSON payload. Unlike standard text chat, individual chunks cannot be parsed with JSON.parse() because they represent incomplete JSON tokens.

Accumulating Streamed JSON Chunks

To stream structured outputs properly, accumulate delta fragments until the stream completes, or pipe incoming chunks through an incremental JSON stream parser.

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

If your pipeline uses streaming strictly to minimize time-to-first-byte (TTFB) in user interfaces, verify that the downstream endpoint supports continuous Server-Sent Events (SSE) through your network proxy.


Step 4: Error Diagnosis and Troubleshooting

When migrating SDK calls to a custom baseURL, error diagnosis should distinguish between network proxy failures, schema misconfigurations, rate limits, and model refusals.

Common Errors and Resolution Strategies

Error Code / TypeRoot CauseDiagnosis & Fix
401 UnauthorizedInvalid or missing API keyConfirm that your apiKey contains a valid credential recognized by the target gateway (e.g., check ONEMUX_API_KEY).
400 BadRequestErrorSchema violation (strict: true)Inspect error message for unsupported JSON schema keywords (e.g., missing additionalProperties: false or missing fields in required).
404 NotFoundIncorrect path or model IDEnsure baseURL ends with /v1. Confirm the model requested exists in the provider's active model list.
429 RateLimitErrorConcurrency or spend ceiling hitImplement exponential backoff. Monitor quota limits via your provider dashboard.
Refusal (Choice body)Model safety guardrails triggeredModel generated an explicit refusal. Check choice.message.refusal rather than looking in content.

Structured Error Handling Wrapper

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

Step 5: Consolidating Providers via OneMux

Routing OpenAI SDK requests through OneMux simplifies multi-model operations without requiring third-party libraries or custom protocol adapters.

  • Unified Endpoint: Maintain a single client interface while routing across models using standard OpenAI SDK semantics.
  • Spend Management & Visibility: Centralize billing, review per-model usage, and set spending rules without managing multiple provider invoices.
  • Flexible Pay-as-You-Go: Fund your account through flexible credit top-ups, preventing unexpected service interruptions.

To view current usage rates and compare routing options across models, consult the OneMux Pricing Guide and read the technical overview in the OneMux Documentation.


Sources

FAQ

Can I use Zod with the OpenAI SDK when using a custom base URL?

Yes. If your gateway fully implements the `/chat/completions` structured outputs spec, helper methods like `client.beta.chat.completions.parse()` and `zodResponseFormat()` work seamlessly by translating Zod schemas into the required strict JSON Schema payload.

Why does the model return a 400 error when using 'strict: true'?

The OpenAI structured outputs engine requires that every object definition explicitly includes `additionalProperties: false` and lists every declared property inside the `required` array. If any key is missing from `required`, or if unsupported JSON Schema keywords are present, the API rejects the request with a 400 validation error.

Does changing the baseURL affect SDK telemetry or timeouts?

Overriding `baseURL` redirects all HTTP transport to the new URL. Timeout and retry options (such as `maxRetries` or `timeout` in the OpenAI constructor) continue to function identically on the client side.

Related articles