The Adapter pattern abstracts the LLM behind a stream() interface yielding typed chunks. This lets you swap between API-key auth, Codex OAuth, or even different providers.
Narrow boundary, rich downstream
The temptation when designing an Adapter ABC is to expose every provider feature: chat(), stream(), embed(), transcribe(), generate_image(). Resist it. A narrow ABC with one method (stream(messages, tools) -> AsyncIterator[Event]) keeps every provider on equal footing and forces provider-specific bits to specialize per-implementation.
cwkPippa's backend/adapters/base.py is exactly this shape: one ABC, one streaming method, an Event protocol the consumer parses. The Codex / Gemini / Ollama vessels live under backend/variants/ and specialize that contract; they do not push provider differences upward into routes, store, or frontend. Rule 2 of the architecture: cost is absorbed downstream, not pushed upstream.