Skip to slide
Chapter 4 · Model-Provider Abstraction and Multi-Model Strategy
31 / 191

CHAPTER 04 · Model-Provider Abstraction and Multi-Model Strategy · 3 / 8

The adapter pattern

The clean solution is a provider adapter layer: define one neutral interface that the rest of your system speaks, and write a thin adapter per provider that translates to and from the native API. Concretely:

  1. Neutral types. Define your own minimal vocabulary: a message ({role, content}), a tool schema (pick one canonical format, most teams use the widely-documented function-calling shape), a normalized tool call ({id, name, input}), a normalized tool result ({id, content}), and a set of streaming callbacks (onText, onReasoning, onToolCall).
  2. A dispatcher. One function, streamChatWithTools(params), that inspects the requested model id, picks the provider, and calls the right adapter. The rest of your code calls only this.
  3. One adapter per provider. Each implements the same contract: convert neutral input to native, run the streaming tool-call loop, convert native streaming back to your neutral callbacks, and return the accumulated result. The tool-call replay loop (append model turn, append results, repeat) lives inside the adapter, so the provider-specific replay rules are encapsulated.
  4. Schema converters. Small functions that turn your canonical tool schema into each provider's dialect, handling the edge cases (omit empty parameter objects, ensure arrays have item types, etc.).

The payoff: your agent loop calls exactly one function and never branches on provider. Adding a provider is one new adapter file plus registering its model ids. Switching a user from one model to another is a single field change.

← → arrow keys work too