SDK Integration Maintenance
Maintaining SDK Integrations Through API Evolution
Every pattern in this unit — service classes, dependency injection, typed models, decorators, custom exceptions, packaging — produces code that works today, against the current version of the OpenAI SDK. None of it, by itself, guarantees the code keeps working as the SDK and the underlying API evolve. This final lesson is about the ongoing engineering discipline needed to keep an integration healthy over months and years, not just at the moment it was written.
Why SDKs and APIs Change
The OpenAI SDK, like any actively developed client library, changes for several distinct reasons: the underlying API adds new capabilities (new endpoints, new parameters), the SDK's own maintainers improve its internal design (renaming methods, restructuring response objects), and occasionally a genuinely breaking change is introduced deliberately, usually accompanied by a new major version number. Distinguishing these matters because they call for different responses: a new optional parameter can usually be adopted at your own pace, while a breaking change to an existing method's behavior requires coordinated, tested work before upgrading.
Pinning Dependency Versions Deliberately
Lesson 8 introduced version pinning for a packaged internal library's own dependencies. The same principle applies to any application that depends directly on the OpenAI SDK: pin to a known-good range, rather than leaving the version unconstrained.
[project]
dependencies = [
"openai>=1.40.0,<1.50.0",
]
An unconstrained dependency ("openai", with no version specifier at all) means the exact version installed depends entirely on when pip install happens to run, and on what else in the dependency graph might force a particular version. Two developers running pip install on different days could end up with different SDK versions installed, silently, with no record of which version either of them is actually running against. A pinned range, by contrast, is itself documentation: it states, in the project's own configuration file, exactly which versions of the SDK this codebase has been built and tested against.
The Practical Difference Between a Lockfile and a Version Range
A version range in pyproject.toml (>=1.40.0,<1.50.0) still allows some flexibility — any version within that range can be installed. A lockfile (produced by tools such as pip-compile, poetry.lock, or uv.lock) goes further and records the exact version actually installed and tested, down to the last dependency in the graph. For applications (as opposed to libraries meant to be depended on by others), committing a lockfile alongside pyproject.toml ensures that every developer, and every deployment, installs the identical set of versions that was actually tested — eliminating an entire class of "it works on my machine" bugs caused by two environments silently running different dependency versions.
Reading the Changelog Before Upgrading
Before bumping a pinned version range to include a new SDK release, reading that release's changelog is the single most effective habit for avoiding surprise breakage. Most well-maintained SDKs (including the OpenAI Python SDK) publish a changelog or release notes listing, per version, what was added, what was deprecated, and — critically — what changed in a backward-incompatible way.
A practical workflow:
- Identify the current pinned version and the target version to upgrade to.
- Read every changelog entry between those two versions, not just the latest one — a multi-version jump can accumulate several unrelated changes.
- Search your own codebase for any usage of methods, parameters, or exception types mentioned in the changelog as changed or removed.
- Only then decide whether the upgrade is a routine bump or requires code changes first.
Note: The exact location and format of the OpenAI Python SDK's changelog (a
CHANGELOG.mdin its repository, GitHub release notes, or a dedicated migration guide) can change over time. Locate the current, authoritative source before relying on it as part of this workflow.
Testing Against a New SDK Version Before Upgrading in Production
Because this course's testing pattern relies on fake, injected clients rather than the real SDK, most of an application's own test suite verifies application logic and does not, by itself, catch a genuine breaking change in the SDK's real behavior — a fake client only behaves the way you told it to, not the way the real SDK actually behaves after an upgrade. A dedicated, small set of integration tests, run against the real SDK (and, ideally, a real or sandboxed API endpoint) as a separate step, is what actually validates an upgrade:
import pytest
from openai import OpenAI
@pytest.mark.integration
def test_responses_create_still_returns_output_text() -> None:
"""A minimal, real call verifying the SDK's basic response shape
has not changed. Requires OPENAI_API_KEY and network access; run
only in a dedicated integration test suite, not on every commit."""
client = OpenAI()
response = client.responses.create(
model="gpt-5.6-terra",
input="Say the word 'test' and nothing else.",
)
assert hasattr(response, "output_text")
assert isinstance(response.output_text, str)
print("PASS: real SDK call still exposes response.output_text as a string")
This test is deliberately marked (@pytest.mark.integration) to separate it from the fast, fake-client unit tests that run on every commit — it makes a real network call, costs money, and should run only when specifically validating an SDK upgrade, not as part of routine development. Its purpose is narrow and specific: confirm that the basic shape your application depends on (response.output_text existing and being a string) still holds after upgrading, before that assumption is relied upon in production.
The Role of the Service-Class Boundary During an Upgrade
This is where the architecture built throughout this unit pays off directly. Because SDK calls are concentrated inside service classes (Lesson 1), and SDK-specific exceptions are translated at that same boundary (Lesson 6), an SDK upgrade that changes a method name or a response field typically requires changes in exactly one place per service class — not throughout the application. Contrast the two scenarios:
Without centralized service classes: an SDK method rename requires searching the entire codebase for every direct client.responses.create(...) call, updating each one, and hoping none were missed.
With centralized service classes: the same rename requires updating SummarizerService.summarize, TicketClassifierService.classify_ticket, and any other service method that made the call directly — a small, enumerable, and testable set of locations, each already covered by its own dependency-injection tests.
This is the concrete payoff of the architectural investment made across Lessons 1 through 9: not that any individual pattern prevents API evolution from happening, but that their combination confines the blast radius of a breaking change to a small, well-tested surface area instead of the entire application.
Maintaining a Migration Log
For a codebase maintained over a long period, keeping a short internal log of SDK upgrades — what version was upgraded to, what changed, what had to be updated in your own code — pays for itself the next time a similar migration is needed, or when diagnosing a regression that appeared after a past upgrade:
## SDK Upgrade Log
### 2026-03-14 — openai 1.42.0 → 1.47.0
- No breaking changes for our usage.
- Adopted new `reasoning_effort` parameter in `TicketClassifierService`.
### 2025-11-02 — openai 1.35.0 → 1.42.0
- `client.responses.create()` response object renamed a field we depended on.
- Updated `SummarizerService.summarize()` and its unit tests accordingly.
- Integration test added to catch this class of change earlier next time.
This log is not the changelog itself (which documents the SDK's own history) — it documents your own application's history of reacting to that changelog, which is exactly the information a future maintainer (including a future version of yourself) needs when something breaks and the question is "did a recent SDK upgrade cause this?"
Common Mistakes
Upgrading dependencies opportunistically, without reading what changed. Bumping a version range purely because a newer version exists, without reviewing its changelog first, turns every upgrade into a gamble rather than a deliberate, informed decision.
Relying solely on fake-client unit tests to validate an SDK upgrade. Unit tests built on fakes verify your own logic against the behavior you assumed the SDK has — they cannot detect that the real SDK's actual behavior changed, which is exactly what a small integration-test suite exists to catch.
Leaving dependency versions completely unconstrained in production applications. This makes every deployment a potential source of nondeterministic behavior, since the exact SDK version running in production may differ from what was tested locally.
Best Practices
Pin SDK version ranges explicitly, and commit a lockfile for applications. This makes the currently supported version range an explicit, reviewable part of the codebase rather than an accident of installation timing.
Read the changelog for every version between your current pin and a prospective upgrade target, not just the latest release notes.
Maintain a small, separate integration test suite that exercises the real SDK, run deliberately during upgrades rather than on every commit, to catch behavior changes that fake-client unit tests structurally cannot detect.
Keep SDK calls concentrated inside service classes, so that when the SDK does change in a breaking way, the required fix is confined to a small, already-tested set of locations instead of scattered across the entire codebase.