Guide · OpenAI

Migrate OpenAI Python SDK from v0 to v1

Version 1 of the OpenAI Python library replaced module-level calls with a client object, dicts with typed objects, and openai.error with new exception classes. Here is every change, in the order you will hit it.

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_key to an OpenAI() client you create once and reuse.
  • openai.ChatCompletion.create() becomes client.chat.completions.create(), and every other resource follows the same pattern.
  • Responses are Pydantic models. Read response.choices[0].message.content, not response["choices"][0]["message"]["content"].
  • The openai.error exceptions are replaced by top-level classes such as openai.RateLimitError, openai.BadRequestError and openai.APIConnectionError.
  • The async a* methods such as acreate are removed. Use the AsyncOpenAI client instead.
  • Azure OpenAI gets its own AzureOpenAI client. openai.api_type, openai.api_version and the engine= argument are gone.
  • HTTP goes through httpx, so openai.requestssession and openai.aiosession are removed, and failed requests are retried twice by default.

Migration steps

  1. 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.1 while you finish the migration.

    pip install --upgrade openai
  2. Create 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_KEY is 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_base is now the base_url client option, and options such as openai.api_key_path, openai.debug and openai.log were removed.

  3. 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() to client.completions.create()
    • openai.Embedding.create() to client.embeddings.create()
    • openai.Image.create() to client.images.generate(), and Image.create_edit() to client.images.edit()
    • openai.Audio.transcribe() to client.audio.transcriptions.create()
    • openai.Moderation.create() to client.moderations.create()
    • openai.FineTuningJob.* to client.fine_tuning.jobs.*
    • openai.File.download() to client.files.retrieve_content()
  4. 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 manual auto_paging_iter() loops.

  5. 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.APIStatusError and carry status_code and response: 400 is BadRequestError, 401 AuthenticationError, 403 PermissionDeniedError, 404 NotFoundError, 429 RateLimitError, and 500 or above InternalServerError. Network failures raise APIConnectionError, and timeouts raise APITimeoutError. All of them inherit from openai.APIError.

  6. 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.

  7. 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)

    model takes your deployment name, not the underlying model name. AzureOpenAI also accepts azure_deployment, azure_ad_token and azure_ad_token_provider for Microsoft Entra ID auth, and AsyncAzureOpenAI is the async version. Microsoft now also documents a v1 endpoint that no longer needs api_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 python

Check 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.create and 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 NotFoundError or BadRequestError) 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.

Sources