Skip to content

Fixing Breaking Changes After an Automatic SDK Dependency Bump

10 min read · updated August 11, 2026

Nothing in the repository changed. The build ran, the deploy went out, and the first request raised an attribute error on a call that has been in production for a year. This is what that looks like, and the order the two separate problems get solved in.

The error strings

These are the messages that put people on this page. Each one is a major-version rewrite of a client library meeting code written against the previous major.

  • module 'openai' has no attribute 'ChatCompletion' — often printed with the follow-on hint that the method was removed in the 1.0.0 release of the Python library. Your code calls openai.ChatCompletion.create(...); the installed library has a client-object interface instead.
  • module 'openai' has no attribute 'error' — the same rewrite. Exception classes moved out of an openai.error module, so every except openai.error.RateLimitError clause raises while handling the error it was meant to handle.
  • You tried to access openai.ChatCompletion, but this is no longer supported in openai>=1.0.0 — the deliberately explicit variant. If you see this, the diagnosis is finished before it started.
  • TypeError: streamText is not a function — a JavaScript equivalent: a named export that moved, was renamed, or now lives behind a different entry point after a major of a TypeScript AI toolkit.
  • AI_APICallError — the Vercel AI SDK’s error name for a failed provider call. It surfaces after an upgrade when the request the toolkit now builds is rejected by the provider, so the useful part is not the class name but the HTTP status and provider message it carries. Print the whole error object, not error.message.
  • Unsupported parameter: 'max_tokens' is not supported with this model. Use 'max_completion_tokens' instead. — a server-side rejection rather than a client crash, and it appears after an upgrade when a newer client stops silently translating, or when a model string changed at the same time. It belongs on this page because it looks like an SDK break and is not one.

Confirm it is a version change

Before touching anything, establish that the library version differs between the last known-good artefact and the current one. This takes under a minute and it prevents a wrong fix.

# what the broken deploy actually has
docker run --rm your-image:current pip freeze | grep -i '^openai'

# what the last good one had
docker run --rm your-image:last-good pip freeze | grep -i '^openai'

# node, same question
docker run --rm your-image:current npm ls ai openai --depth=0

If the two numbers differ and no manifest change appears in your git log, you have confirmed an unpinned resolution. If they are identical, stop — the break is something else and this page will send you the wrong way.

Restore service first

There are two problems here and they have different urgencies. The urgent one is that production is down; the important one is that your dependency resolution is non-deterministic. Solve them in that order, and do not attempt the code migration during the incident.

  1. Redeploy the last known-good image if you still have it. This is the fastest restore available and it needs no reasoning about libraries.
  2. If you cannot redeploy an image, pin the manifest to the exact version the good artefact had — the full version, not a range, not a caret — and rebuild.
  3. Commit the lockfile in the same change. A pinned manifest with an uncommitted lockfile leaves every transitive dependency free to do the same thing next week.
  4. Verify by inspecting the built artefact, not the manifest. Run the same pip freeze or npm ls against the new image and check the number is what you intended.

You are now on old code with a hard pin. That is a stable place to stand, and it is explicitly not the end state: a pin held indefinitely is deferred reading, as the argument about what pinning protects sets out.

How an unpinned major got in

Worth understanding, because the mechanism decides which guard to add.

  • No lockfile in the image build. The most common cause by a distance. A Dockerfile running pip install -r requirements.txt where the requirements file says openai with no specifier resolves to the newest release on the day the layer cache missed. Nothing in your repository changed; the registry did.
  • A lockfile that CI does not install from. npm install may update the lockfile to satisfy the manifest; npm ci installs the lockfile exactly and fails if the two disagree. Using the former in CI means the lockfile is a suggestion.
  • An open specifier. openai>=1.0 or a bare package name permits any future major. A caret in npm does not cross a major for versions at or above 1.0.0, but it does for 0.x, where ^0.3.1 allows 0.3.9 and a 0.x minor is where publishers put breaking changes.
  • An auto-merged bot pull request. Automated dependency updates are good; auto-merging major bumps on a green build is how a green build becomes the last thing anybody looked at. Configure the bot to require review for majors specifically.
  • A transitive bump. You pinned the client and a framework that wraps it did not. Your lockfile is the only artefact that sees this one.

Then migrate deliberately

Once service is restored and the pin holds, the upgrade becomes an ordinary planned change. The mechanical part of a client-rewrite major is usually smaller than it looks: the calls move from module-level functions to methods on a client object, and the response stops being a dictionary and becomes a typed object.

# pre-1.x shape
import openai
openai.api_key = KEY
resp = openai.ChatCompletion.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "hello"}],
)
text = resp["choices"][0]["message"]["content"]

# 1.x shape
from openai import OpenAI
client = OpenAI(api_key=KEY)
resp = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "hello"}],
)
text = resp.choices[0].message.content

Three things reliably get missed in that rewrite, and none of them is the call itself. First, subscript access: any code treating the response as a mapping — resp["usage"]["total_tokens"] — needs attribute access instead, and those lines are often in logging and metrics code that no test covers. Second, exception handling: the except clauses must name the new classes, and a clause that fails to match turns a retryable rate-limit into an unhandled crash under load. Third, client construction: creating a client per request rather than once per process discards connection reuse and shows up as a latency regression that looks like the provider’s fault.

Do the migration in a branch, with the changelog read properly for the whole range you are crossing, and ship it as its own release with nothing else in it. Then set the manifest to a range that cannot cross a major, keep the lockfile committed, and let the bot open minor-bump pull requests you actually review.

The error strings quoted above are current-generation library messages and vendors reword them. Match on the shape — a missing attribute on a module you did not change — rather than the exact sentence, and confirm against the installed version rather than the message.