Guide · OpenAI

Migrate OpenAI Node SDK from v3 to v4

Version 4 of the openai npm package replaced the generated OpenAIApi class with a single OpenAI client, moved every method under a namespace, and dropped the .data wrapper on responses.

v3 of the Node library was a thin generated wrapper: you built a Configuration, passed it to OpenAIApi, called flat methods like createChatCompletion, and read results from response.data. v4 is a rewrite. Almost every line that touches the SDK changes, but the changes are regular, so the migration is mostly mechanical once you know the rules. This guide follows OpenAI's v4 migration guide, linked at the end. If you are going past 4.x, do this step first, then read the changelog for each later major.

What changed

  • Configuration and OpenAIApi are replaced by one class: new OpenAI({ apiKey }).
  • Flat methods move under namespaces: createChatCompletion becomes chat.completions.create, createEmbedding becomes embeddings.create.
  • Responses are no longer wrapped: completion.data.choices becomes completion.choices.
  • Errors are OpenAI.APIError instances with status, code and type, instead of error.response.status and error.response.data.
  • Streaming is supported directly with stream: true and for await.
  • Methods that take files, such as transcription, take a single params object with a file field.
  • createAnswer, createClassification, createSearch, listEngines and retrieveEngine are removed.

Migration steps

  1. Upgrade the package

    npm install openai@^4
  2. Replace Configuration and OpenAIApi

    Before

    import { Configuration, OpenAIApi } from "openai";
    
    const configuration = new Configuration({ apiKey: process.env.OPENAI_API_KEY });
    const openai = new OpenAIApi(configuration);

    After

    import OpenAI from "openai";
    
    const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

    The import is now a default export. If you import v3 types such as ChatCompletionRequestMessage, expect those imports to change as well: v4 organizes its types under namespaces such as OpenAI.Chat.

  3. Rename methods and drop .data

    Before

    const completion = await openai.createChatCompletion({
      model: "gpt-3.5-turbo",
      messages: [{ role: "user", content: "Hello!" }],
    });
    console.log(completion.data.choices[0].message);

    After

    const completion = await openai.chat.completions.create({
      model: "gpt-3.5-turbo",
      messages: [{ role: "user", content: "Hello!" }],
    });
    console.log(completion.choices[0].message);

    The full rename table is in OpenAI's guide. The ones most codebases use:

    • createCompletion to completions.create
    • createEmbedding to embeddings.create
    • createImage to images.generate, createImageEdit to images.edit
    • createTranscription to audio.transcriptions.create
    • createModeration to moderations.create
    • createFile to files.create, listFiles to files.list, deleteFile to files.del
    • listModels to models.list
  4. Update error handling

    Before

    try {
      await openai.createChatCompletion(params);
    } catch (error) {
      if (error.response) {
        console.error(error.response.status);
        console.error(error.response.data.message);
      } else {
        console.error(error.message);
      }
    }

    After

    try {
      await openai.chat.completions.create(params);
    } catch (error) {
      if (error instanceof OpenAI.APIError) {
        console.error(error.status);
        console.error(error.message);
        console.error(error.code, error.type);
      } else {
        throw error;
      }
    }
  5. Replace streaming workarounds

    v3 did not support streaming. If your code passed stream: true with a raw-response option and parsed server-sent events by hand, delete that code and iterate the stream instead:

    const stream = await openai.chat.completions.create({
      model: "gpt-3.5-turbo",
      messages: [{ role: "user", content: "Say this is a test" }],
      stream: true,
    });
    for await (const part of stream) {
      process.stdout.write(part.choices[0]?.delta?.content ?? "");
    }
  6. Pass files in a params object

    Before

    const transcript = await openai.createTranscription(
      fs.createReadStream("audio.mp3"),
      "whisper-1",
    );

    After

    const transcript = await openai.audio.transcriptions.create({
      model: "whisper-1",
      file: fs.createReadStream("audio.mp3"),
    });
  7. Review retries and timeouts

    The client retries connection errors, 408, 409, 429 and 5xx responses twice by default with exponential backoff, and its README documents a default timeout of 10 minutes. If you wrapped v3 calls in your own retry helper, you now retry twice as often as you think. Remove the wrapper, or turn the built-in retries off:

    const openai = new OpenAI({ maxRetries: 0, timeout: 20 * 1000 });
    
    // or per request, as a second argument
    await openai.chat.completions.create(params, { maxRetries: 5, timeout: 5 * 1000 });
  8. Read headers with withResponse()

    v3 returned the full HTTP response, so headers were next to .data. In v4, call .withResponse() when you need them:

    const { data: completion, response } = await openai.chat.completions
      .create(params)
      .withResponse();
    console.log(response.headers.get("x-ratelimit-remaining-requests"));

How to find every affected call

Use ast-grep to match the syntax tree rather than text, so calls split across lines are found too. $CLIENT matches any receiver, whatever you named the instance:

sg run --pattern 'new OpenAIApi($$$ARGS)' --lang ts
sg run --pattern 'new Configuration($$$ARGS)' --lang ts
sg run --pattern '$CLIENT.createChatCompletion($$$ARGS)' --lang ts
sg run --pattern '$CLIENT.createEmbedding($$$ARGS)' --lang ts
sg run --pattern '$RES.data.choices' --lang ts
sg run --pattern '$ERR.response.status' --lang ts

Repeat with --lang tsx and --lang js for those files. A metavariable matches a whole node, not part of a name, so $CLIENT.create$NAME does not work; search each v3 method you use, or write a YAML rule with a regex constraint on the method name. new Configuration can also match other libraries' classes, so check the import before changing it.

Testing the change

  • Run tsc --noEmit. Removed .data reads and old type imports show up as type errors in TypeScript files.
  • Update mocks. Tests that stub OpenAIApi.prototype.createChatCompletion to return { data: {...} } need to stub the new method and return the unwrapped shape, or they will keep passing while production breaks.
  • Exercise one error path: a request with an invalid model should land in your OpenAI.APIError branch with a numeric status.
  • If you stream, run one real streamed request in staging and check that the client sees partial output, not one block at the end.

Automating it

OpenAI provides npm exec openai migrate, which runs Grit locally with AST-based transforms. Commit or stash first and review the result with git add --patch. The command needs bash; for Windows a maintainer suggested npx -y @getgrit/launcher apply openai_v4. It is a good first pass for the renames. It does not run your build or tests.

Versyka finds the same call sites deterministically with ast-grep, rewrites them together with the code that depends on them (the .data reads, error branches, mocks), then runs your repository's install, typecheck, build and tests in a disposable sandbox. A pull request is opened only when those pass.

Was this guide helpful?

Check your own repository

Paste your package.json, requirements.txt or pyproject.toml into the free upgrade check to see which API SDKs are behind, with no account. Or let Versyka run this migration on one repository and open a pull request that has already passed your tests.

Sources