Since November 6, 2023, pip install openai installs version 1.x of the library. Going from 0.28.1 to 1.x is a breaking change: code that calls openai.ChatCompletion.create fails on the first call, and code that reads response["choices"] fails even after you fix the call. The steps below cover the changes that affect most codebases. They follow OpenAI's v1.0.0 migration guide and Microsoft's Azure supplement, linked at the end.
What changed
- Configuration moves from module globals like
openai.api_keyto anOpenAI()client you create once and reuse. openai.ChatCompletion.create()becomesclient.chat.completions.create(), and every other resource follows the same pattern.- Responses are Pydantic models. Read
response.choices[0].message.content, notresponse["choices"][0]["message"]["content"]. - The
openai.errorexceptions are replaced by top-level classes such asopenai.RateLimitError,openai.BadRequestErrorandopenai.APIConnectionError. - The async
a*methods such asacreateare removed. Use theAsyncOpenAIclient instead. - Azure OpenAI gets its own
AzureOpenAIclient.openai.api_type,openai.api_versionand theengine=argument are gone. - HTTP goes through httpx, so
openai.requestssessionandopenai.aiosessionare removed, and failed requests are retried twice by default.
Migration steps
Upgrade on a branch
Upgrade in a branch and keep the old pin handy. If something breaks in production you can roll back with
pip install openai==0.28.1while you finish the migration.pip install --upgrade openaiCreate a client instead of setting globals
Before
import os import openai openai.api_key = os.environ["OPENAI_API_KEY"]After
import os from openai import OpenAI client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])If
OPENAI_API_KEYis set in the environment,OpenAI()with no arguments picks it up. Create the client once (at module level or in your dependency container) and pass it to the code that needs it. That also makes it easy to swap in a fake client in tests.openai.api_baseis now thebase_urlclient option, and options such asopenai.api_key_path,openai.debugandopenai.logwere removed.Rename the calls
Before
response = openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=[{"role": "user", "content": "Summarize this ticket"}], )After
response = client.chat.completions.create( model="gpt-3.5-turbo", messages=[{"role": "user", "content": "Summarize this ticket"}], )The arguments stay the same. The other resources move the same way:
openai.Completion.create()toclient.completions.create()openai.Embedding.create()toclient.embeddings.create()openai.Image.create()toclient.images.generate(), andImage.create_edit()toclient.images.edit()openai.Audio.transcribe()toclient.audio.transcriptions.create()openai.Moderation.create()toclient.moderations.create()openai.FineTuningJob.*toclient.fine_tuning.jobs.*openai.File.download()toclient.files.retrieve_content()
Read responses as objects
This is the change that survives a quick find-and-replace. Every response is now a typed object, so subscript access fails at runtime.
Before
text = response["choices"][0]["message"]["content"] tokens = response["usage"]["total_tokens"] log.info(json.dumps(response))After
text = response.choices[0].message.content tokens = response.usage.total_tokens log.info(response.model_dump_json())Streaming works the same way: each chunk is an object too.
Before
for chunk in openai.ChatCompletion.create(model=model, messages=messages, stream=True): print(chunk["choices"][0]["delta"].get("content", ""), end="")After
stream = client.chat.completions.create(model=model, messages=messages, stream=True) for chunk in stream: print(chunk.choices[0].delta.content or "", end="")List endpoints paginate automatically when you iterate them, so
for job in client.fine_tuning.jobs.list(limit=1):replaces manualauto_paging_iter()loops.Update exception handling
Before
try: response = openai.ChatCompletion.create(model=model, messages=messages) except openai.error.RateLimitError: retry_later() except openai.error.InvalidRequestError as e: log.warning("bad request: %s", e)After
try: response = client.chat.completions.create(model=model, messages=messages) except openai.RateLimitError: retry_later() except openai.BadRequestError as e: log.warning("bad request: %s (HTTP %s)", e, e.status_code)Errors with an HTTP response are subclasses of
openai.APIStatusErrorand carrystatus_codeandresponse: 400 isBadRequestError, 401AuthenticationError, 403PermissionDeniedError, 404NotFoundError, 429RateLimitError, and 500 or aboveInternalServerError. Network failures raiseAPIConnectionError, and timeouts raiseAPITimeoutError. All of them inherit fromopenai.APIError.Move async calls to AsyncOpenAI
Before
response = await openai.ChatCompletion.acreate(model=model, messages=messages)After
from openai import AsyncOpenAI client = AsyncOpenAI() response = await client.chat.completions.create(model=model, messages=messages)The method names match the sync client. Only the client class changes, plus
await.Switch Azure OpenAI code to AzureOpenAI
Before
openai.api_type = "azure" openai.api_base = os.getenv("AZURE_OPENAI_ENDPOINT") openai.api_key = os.getenv("AZURE_OPENAI_API_KEY") openai.api_version = "2024-02-01" response = openai.ChatCompletion.create(engine="my-deployment", messages=messages)After
from openai import AzureOpenAI client = AzureOpenAI( azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"), api_key=os.getenv("AZURE_OPENAI_API_KEY"), api_version="2024-02-01", ) response = client.chat.completions.create(model="my-deployment", messages=messages)modeltakes your deployment name, not the underlying model name.AzureOpenAIalso acceptsazure_deployment,azure_ad_tokenandazure_ad_token_providerfor Microsoft Entra ID auth, andAsyncAzureOpenAIis the async version. Microsoft now also documents a v1 endpoint that no longer needsapi_version; check their current Python guide before choosing.
How to find every affected call
Text search misses calls split across lines and aliased imports. A structural search with ast-grep matches the syntax tree instead. $$$ARGS matches any number of arguments, and $RESOURCE captures a single name:
sg run --pattern 'openai.ChatCompletion.create($$$ARGS)' --lang python
sg run --pattern 'openai.$RESOURCE.create($$$ARGS)' --lang python
sg run --pattern 'openai.$RESOURCE.acreate($$$ARGS)' --lang python
sg run --pattern 'openai.error.$ERROR' --lang python
sg run --pattern 'openai.api_key = $KEY' --lang python
sg run --pattern '$RESPONSE["choices"]' --lang pythonCheck imports first with sg run --pattern 'import openai as $ALIAS' --lang python and from openai import $$$NAMES. If the module is imported under another name, repeat the searches with that name. The last pattern finds dict-style reads on responses, which is where most leftover bugs hide. On Linux, sg may be a system command; ast-grep run is the same tool.
Testing the change
- Run your type checker. 1.x ships type hints, so mypy or pyright flags
response["choices"]and removed attributes before you run anything. - Fix mocks. Tests that patch
openai.ChatCompletion.createand return dicts will pass against code that no longer exists. Inject a fake client, or mock at the HTTP layer: 1.x uses httpx, so httpx mocking tools such as respx work. - Run one real request per endpoint you use in a staging environment, including a streamed one if you stream.
- Force an error path (an invalid model name gives a
NotFoundErrororBadRequestError) and confirm your handlers still catch it.
Automating it
OpenAI ships an automatic migration: openai migrate runs Grit locally with AST-based transforms. Start from a clean working tree and review the result with git add --patch. It needs bash, so on Windows you run it inside WSL, and Microsoft notes it is not supported for Azure OpenAI code. It rewrites syntax; it does not run your tests.
Versyka takes a different route. It finds call sites deterministically with ast-grep patterns like the ones above, rewrites them along with the code around them (dict reads, exception handlers, mocks), then runs your repository's own install, type check and tests in a disposable sandbox. It opens a pull request only if those checks 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.