Proven
Applications using Large Language Models face vendor lock-in and switching costs when tightly coupled to specific LLM providers. Different providers have varying APIs, capabilities, and pricing models, making it difficult to switch or compare providers. The LLM Adapter Pattern addresses:
The LLM Adapter Pattern creates a common interface for all LLM providers, allowing applications to switch between providers without code changes and enabling capabilities like fallback, cost optimization, and performance comparison.
Core concept:
from abc import ABC, abstractmethod
from typing import List, Tuple, Optional, Dict, Any
class LLMAdapter(ABC):
"""Common interface for all LLM providers"""
@abstractmethod
async def complete(self, prompt: str, **kwargs) -> str:
"""Generate text completion"""
pass
@abstractmethod
async def classify(self, text: str, categories: List[str]) -> Tuple[str, float]:
"""Classify text into categories with confidence"""
pass
@abstractmethod
async def embed(self, text: str) -> List[float]:
"""Generate embeddings for text"""
pass
@abstractmethod
def supports_streaming(self) -> bool:
"""Check if provider supports streaming responses"""
pass
@abstractmethod
async def get_model_info(self) -> Dict[str, Any]:
"""Get model capabilities and limits"""
pass
class ClaudeAdapter(LLMAdapter):
"""Claude-specific implementation"""
def __init__(self, api_key: str, model: str = "claude-3-opus"):
self.client = Anthropic(api_key=api_key)
self.model = model
async def complete(self, prompt: str, **kwargs) -> str:
response = await self.client.messages.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
max_tokens=kwargs.get("max_tokens", 1000),
temperature=kwargs.get("temperature", 0.7)
)
return response.content[0].text
async def classify(self, text: str, categories: List[str]) -> Tuple[str, float]:
prompt = f"""
Classify this text into one of these categories: {', '.join(categories)}
Text: {text}
Return format: category_name:confidence_score
"""
response = await self.complete(prompt)
parts = response.split(':')
return parts[0].strip(), float(parts[1].strip())
async def embed(self, text: str) -> List[float]:
# Claude doesn't provide embeddings, use alternative or raise NotImplementedError
raise NotImplementedError("Claude doesn't support embeddings")
def supports_streaming(self) -> bool:
return True
async def get_model_info(self) -> Dict[str, Any]:
return {
"provider": "anthropic",
"model": self.model,
"max_tokens": 100000,
"supports_streaming": True,
"supports_embeddings": False
}
class OpenAIAdapter(LLMAdapter):
"""OpenAI-specific implementation"""
def __init__(self, api_key: str, model: str = "gpt-4"):
self.client = OpenAI(api_key=api_key)
self.model = model
async def complete(self, prompt: str, **kwargs) -> str:
response = await self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
max_tokens=kwargs.get("max_tokens", 1000),
temperature=kwargs.get("temperature", 0.7)
)
return response.choices[0].message.content
async def classify(self, text: str, categories: List[str]) -> Tuple[str, float]:
# Similar implementation with OpenAI format
pass
async def embed(self, text: str) -> List[float]:
response = await self.client.embeddings.create(
model="text-embedding-ada-002",
input=text
)
return response.data[0].embedding
def supports_streaming(self) -> bool:
return True
async def get_model_info(self) -> Dict[str, Any]:
return {
"provider": "openai",
"model": self.model,
"max_tokens": 8192,
"supports_streaming": True,
"supports_embeddings": True
}
class LLMFactory:
"""Creates appropriate adapter based on configuration"""
_adapters: Dict[str, type] = {
"claude": ClaudeAdapter,
"openai": OpenAIAdapter,
"local": LocalLLMAdapter,
"azure": AzureOpenAIAdapter
}
@classmethod
def create(cls, provider: str, **kwargs) -> LLMAdapter:
"""Create adapter for specified provider"""
if provider not in cls._adapters:
raise ValueError(f"Unknown provider: {provider}. Available: {list(cls._adapters.keys())}")
adapter_class = cls._adapters[provider]
return adapter_class(**kwargs)
@classmethod
def register_adapter(cls, provider: str, adapter_class: type):
"""Register custom adapter"""
cls._adapters[provider] = adapter_class
@classmethod
def list_providers(cls) -> List[str]:
"""List available providers"""
return list(cls._adapters.keys())
class LLMManager:
"""Manages multiple providers with fallback and load balancing"""
def __init__(self, config: Dict[str, Dict]):
self.adapters = {}
self.primary_provider = None
self.fallback_providers = []
for provider, provider_config in config.items():
adapter = LLMFactory.create(provider, **provider_config)
self.adapters[provider] = adapter
if provider_config.get("primary", False):
self.primary_provider = provider
if provider_config.get("fallback", False):
self.fallback_providers.append(provider)
async def complete(self, prompt: str, **kwargs) -> str:
"""Try primary provider, fallback on failure"""
providers_to_try = [self.primary_provider] + self.fallback_providers
for provider in providers_to_try:
try:
adapter = self.adapters[provider]
return await adapter.complete(prompt, **kwargs)
except Exception as e:
logger.warning(f"Provider {provider} failed: {e}")
continue
raise LLMProviderError("All providers failed")
async def compare_providers(self, prompt: str) -> Dict[str, str]:
"""Compare responses from all providers"""
results = {}
for provider, adapter in self.adapters.items():
try:
response = await adapter.complete(prompt)
results[provider] = response
except Exception as e:
results[provider] = f"Error: {e}"
return results
Consolidated from:
pattern-catalog.md#12-adapter-pattern-for-llm-providers - Complete implementation with factory and multi-provider management