One narrow boundary, many implementations
The way to handle multiple providers without rewriting your stack: define an adapter interface that captures the streaming-call surface, then implement it per provider. The rest of your code (routes, store, frontend) calls the adapter, never the provider SDK directly.
What the adapter covers
- Streaming call (system prompt, messages, tools, tool_choice, sampler params, max_tokens, optional reasoning budget).
- Event yield: token deltas, tool-call deltas, finish reason, usage data.
- Error normalization (429s, 5xx, content-policy refusals).
What the adapter doesn't cover
- Application logic (routing, history, RAG).
- Storage.
- UI.
Keep the adapter narrow. The temptation is to abstract everything; resist. The cwkPippa codebase uses exactly this pattern — see backend/adapters/base.py.