Provider Abstraction Pattern¶
A minimal, self-contained example of the provider abstraction pattern for LLM APIs.
This pattern lets you switch between LLM providers (OpenRouter, OpenAI, Anthropic, etc.) without changing your application code. It is the simplest form of what Chapter 4 calls the "AI Gateway" -- focused purely on provider interchangeability.
Related: Chapter 4 -- Infrastructure for AI-First Companies
Why Provider Abstraction Matters¶
Three problems that hit every AI-first company eventually:
-
Vendor lock-in. If your application code calls the OpenAI SDK directly, switching to Anthropic means rewriting every call site. A provider layer isolates the change to one file.
-
Cost optimization. You want to route simple queries to cheap models and complex ones to expensive models. With an abstraction layer, routing logic lives in one place -- not scattered across your codebase.
-
Fallback and resilience. When a provider goes down, you need to fail over to another. An abstract interface makes fallback logic trivial to implement.
The Pattern¶
Application Code
|
v
LLMProvider (abstract interface)
^
|
+-----------+-----------+
| | |
OpenRouter OpenAI (future)
Three layers:
| Layer | File | Purpose |
|---|---|---|
| Interface | providers/base.py |
Abstract LLMProvider class with complete() and name |
| Implementations | providers/openrouter.py, providers/openai_direct.py |
Concrete providers that talk to specific APIs |
| Factory | factory.py |
Creates providers by name, reads config from env vars |
Application code (like demo.py) never imports a concrete provider. It asks the factory for a provider by name and uses the abstract interface.
Quick Start¶
# 1. Install dependencies
pip install -r requirements.txt
# 2. Configure API keys
cp .env.example .env
# Edit .env with your actual keys
# 3. Run with default provider (OpenRouter)
python demo.py
# 4. Run with a specific provider
python demo.py --provider openai
# 5. Compare providers side-by-side
python demo.py --compare
# 6. Custom prompt
python demo.py --prompt "What is RAG?" --compare
# 7. Verbose mode (see HTTP request details)
python demo.py --compare -v
How to Add a New Provider¶
Adding a provider (e.g. Anthropic) takes three steps:
Step 1: Implement the interface¶
Create providers/anthropic.py:
from .base import LLMProvider, Message, Response
import httpx
class AnthropicProvider(LLMProvider):
def __init__(self, api_key=None, model="claude-sonnet-4-20250514"):
self._api_key = api_key or os.getenv("ANTHROPIC_API_KEY")
self._model = model
@property
def name(self) -> str:
return "anthropic"
async def complete(self, messages: list[Message], **kwargs) -> Response:
# Anthropic uses a different message format -- the provider
# handles that translation internally
...
Step 2: Register in the factory¶
In factory.py, add one line:
from providers.anthropic import AnthropicProvider
PROVIDERS = {
"openrouter": OpenRouterProvider,
"openai": OpenAIDirectProvider,
"anthropic": AnthropicProvider, # <-- add this
}
Step 3: Update .env¶
That is it. Every existing call to get_provider() can now use "anthropic" without any other code changes.
File Structure¶
provider-abstraction/
providers/
__init__.py # Re-exports for clean imports
base.py # Abstract LLMProvider interface
openrouter.py # OpenRouter implementation
openai_direct.py # Direct OpenAI implementation
factory.py # Provider factory (single entry point)
demo.py # Interactive demo script
.env.example # Template for API keys
requirements.txt # Python dependencies
README.md # This file
When to Use This vs. the Full AI Gateway¶
| Feature | This Pattern | Full AI Gateway (Chapter 4) |
|---|---|---|
| Provider switching | Yes | Yes |
| Cost-based routing | No | Yes |
| Request caching | No | Yes |
| Rate limiting | No | Yes |
| Observability/logging | Basic | Full tracing |
| Streaming support | No | Yes |
| Fallback chains | No | Yes |
| Semantic caching | No | Yes |
Use this pattern when: - You are early stage and need to move fast - You have 1-2 providers and want the option to switch - You want a clean architecture without over-engineering
Graduate to a full AI Gateway when: - You are routing thousands of requests per minute - You need caching, rate limiting, or cost controls - You have 3+ providers with complex routing rules - You need production observability (traces, cost tracking)
Design Decisions¶
Why httpx instead of provider SDKs? Both OpenRouter and OpenAI use the same REST API format. Using httpx directly keeps the dependency count low and makes the HTTP communication transparent -- important for a learning example.
Why async? LLM API calls are I/O-bound and often take 1-5 seconds. Async lets you run multiple provider calls concurrently (as --compare mode does implicitly). Real applications will want this for parallel tool calls and streaming.
Why dataclasses for Message and Response? They are simple, built into Python, and do not require any additional dependencies. In production you might graduate to Pydantic models for validation.