Migrating 11 OpenAI call sites without touching 11 callers

How a single adapter layer moved a 20,000-line chat controller to the Responses API while keeping rollback a one-line change.

Ramya Lakhani2 min read

The real estate AI assistant I worked on lives inside a chat controller of roughly 20,000 lines. When it was time to move to the OpenAI Responses API, the controller had 11 separate places that called the model, and each one built its request a little differently.

The obvious approach is to patch all eleven. I didn't, for three reasons.

Why patching every caller is the wrong move

  1. Eleven patches means eleven chances to get it subtly wrong. Tool definitions, message roles and streaming all changed shape between the APIs.
  2. Rollback becomes a revert of eleven diffs. On a production assistant that's a bad place to be at 2 a.m.
  3. The next migration repeats the work. APIs keep evolving. The callers shouldn't have to care.

The adapter

Instead, every call site now talks to one internal interface. The adapter translates that interface into whichever API is active:

interface LlmClient {
    public function respond(array $messages, array $tools = [], array $options = []): LlmResult;
}

Callers describe what they want: messages, tools, persona. The adapter owns how it's sent: request shape, tool-call parsing, error mapping. Switching APIs is one binding in the container, and switching back is the same line.

Moving one caller at a time

Each of the 11 call sites was moved behind the adapter in its own change, while the adapter still targeted the old API. Behaviour stayed identical, so every step could be verified on its own. Only once all eleven went through the adapter did the adapter itself switch to the Responses API.

What I'd tell anyone doing this

  • Write the reference first. I documented the whole assistant in a 999-line reference before changing it. Most of the risk turned up while writing that document, not while writing code.
  • Keep the model's output contract structured. Our tag protocol ([PLACE:poi_id:X]) let the model drive the map UI, and it survived the migration untouched because the adapter never interpreted content, only transport.
  • Make rollback boring. If going back takes more than a config change, the migration isn't finished.