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