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
ConfigurationandOpenAIApiare replaced by one class:new OpenAI({ apiKey }).- Flat methods move under namespaces:
createChatCompletionbecomeschat.completions.create,createEmbeddingbecomesembeddings.create. - Responses are no longer wrapped:
completion.data.choicesbecomescompletion.choices. - Errors are
OpenAI.APIErrorinstances withstatus,codeandtype, instead oferror.response.statusanderror.response.data. - Streaming is supported directly with
stream: trueandfor await. - Methods that take files, such as transcription, take a single params object with a
filefield. createAnswer,createClassification,createSearch,listEnginesandretrieveEngineare removed.
Migration steps
Upgrade the package
npm install openai@^4Replace 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 asOpenAI.Chat.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:
createCompletiontocompletions.createcreateEmbeddingtoembeddings.createcreateImagetoimages.generate,createImageEdittoimages.editcreateTranscriptiontoaudio.transcriptions.createcreateModerationtomoderations.createcreateFiletofiles.create,listFilestofiles.list,deleteFiletofiles.dellistModelstomodels.list
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; } }Replace streaming workarounds
v3 did not support streaming. If your code passed
stream: truewith 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 ?? ""); }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"), });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 });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 tsRepeat 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.datareads and old type imports show up as type errors in TypeScript files. - Update mocks. Tests that stub
OpenAIApi.prototype.createChatCompletionto 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.APIErrorbranch with a numericstatus. - 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.