Module 8: Unified AI Client — Final integrating project
Final Project: Unified AI Client Production-Ready
Description
Final integrating project for the whole guide. You'll create a production-ready Unified AI Client that abstracts 5 providers (OpenAI, OpenRouter, Ollama, LM Studio, Modal).
Time: 2-3 hours
Difficulty: High
🎯 Objective
Unified client with:
- ✅ Multi-provider support (5 providers)
- ✅ Factory pattern (extensible)
- ✅ Automatic fallback chain
- ✅ Cost optimization
- ✅ Retry strategies
- ✅ Complete monitoring
- ✅ Testing suite
💻 Complete Architecture
#!/usr/bin/env python3
"""
Unified AI Client - Final Project
Abstracts multiple LLM providers
"""
import os
import time
import logging
from abc import ABC, abstractmethod
from typing import List, Dict, Optional
from dataclasses import dataclass
from openai import OpenAI
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# ============================================================================
# DATACLASSES
# ============================================================================
@dataclass
class LLMResponse:
"""Standardized response."""
content: str
model: str
provider: str
tokens: int
latency: float
cost: float
# ============================================================================
# ABSTRACT PROVIDER
# ============================================================================
class LLMProvider(ABC):
"""Abstract interface for providers."""
@abstractmethod
def chat(self, messages: List[Dict], **kwargs) -> LLMResponse:
"""Sends chat and returns a standardized response."""
pass
@abstractmethod
def get_name(self) -> str:
"""Provider name."""
pass
# ============================================================================
# PROVIDERS IMPLEMENTATION
# ============================================================================
class OpenAIProvider(LLMProvider):
"""OpenAI API provider."""
def __init__(self, api_key: str, model: str = "gpt-3.5-turbo"):
self.client = OpenAI(api_key=api_key)
self.model = model
def chat(self, messages: List[Dict], **kwargs) -> LLMResponse:
start = time.time()
response = self.client.chat.completions.create(
model=self.model,
messages=messages,
**kwargs
)
latency = time.time() - start
tokens = response.usage.total_tokens
# Cost (GPT-3.5)
cost = (tokens / 1_000_000) * 0.50
return LLMResponse(
content=response.choices[0].message.content,
model=self.model,
provider="openai",
tokens=tokens,
latency=latency,
cost=cost
)
def get_name(self) -> str:
return "OpenAI"
class OpenRouterProvider(LLMProvider):
"""OpenRouter multi-provider."""
def __init__(self, api_key: str, model: str = "mistralai/mixtral-8x7b-instruct"):
self.client = OpenAI(
base_url="https://openrouter.ai/api/v1",
api_key=api_key
)
self.model = model
def chat(self, messages: List[Dict], **kwargs) -> LLMResponse:
start = time.time()
response = self.client.chat.completions.create(
model=self.model,
messages=messages,
**kwargs
)
latency = time.time() - start
tokens = response.usage.total_tokens
cost = (tokens / 1_000_000) * 0.24 # Mixtral pricing
return LLMResponse(
content=response.choices[0].message.content,
model=self.model,
provider="openrouter",
tokens=tokens,
latency=latency,
cost=cost
)
def get_name(self) -> str:
return "OpenRouter"
class OllamaProvider(LLMProvider):
"""Ollama local provider."""
def __init__(self, model: str = "mistral"):
self.client = OpenAI(
base_url="http://localhost:11434/v1",
api_key="ollama"
)
self.model = model
def chat(self, messages: List[Dict], **kwargs) -> LLMResponse:
start = time.time()
response = self.client.chat.completions.create(
model=self.model,
messages=messages,
**kwargs
)
latency = time.time() - start
tokens = response.usage.total_tokens if hasattr(response.usage, 'total_tokens') else 0
return LLMResponse(
content=response.choices[0].message.content,
model=self.model,
provider="ollama",
tokens=tokens,
latency=latency,
cost=0.0 # Local = $0
)
def get_name(self) -> str:
return "Ollama"
# ============================================================================
# UNIFIED CLIENT
# ============================================================================
class UnifiedAIClient:
"""Unified client with multi-provider support."""
def __init__(self, fallback_chain: List[str] = None):
"""
Args:
fallback_chain: List of providers in order (primary → fallback)
Default: ["openai", "openrouter", "ollama"]
"""
self.fallback_chain = fallback_chain or ["openai", "openrouter", "ollama"]
self.providers = self._init_providers()
self.metrics = {
"total_requests": 0,
"successful_requests": 0,
"failed_requests": 0,
"total_cost": 0.0,
"total_tokens": 0,
"by_provider": {}
}
def _init_providers(self) -> Dict[str, LLMProvider]:
"""Initializes available providers."""
providers = {}
# OpenAI (if API key exists)
if os.getenv("OPENAI_API_KEY"):
providers["openai"] = OpenAIProvider(
api_key=os.getenv("OPENAI_API_KEY")
)
# OpenRouter (if API key exists)
if os.getenv("OPENROUTER_API_KEY"):
providers["openrouter"] = OpenRouterProvider(
api_key=os.getenv("OPENROUTER_API_KEY")
)
# Ollama (try to connect)
try:
test_client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
test_client.models.list()
providers["ollama"] = OllamaProvider()
except:
logger.warning("Ollama not available")
return providers
def chat(self, prompt: str, **kwargs) -> LLMResponse:
"""
Sends chat with automatic fallback.
Args:
prompt: User message
**kwargs: Additional parameters (temperature, max_tokens, etc.)
Returns:
LLMResponse with the result
"""
self.metrics["total_requests"] += 1
messages = [{"role": "user", "content": prompt}]
# Try each provider in order
for provider_name in self.fallback_chain:
provider = self.providers.get(provider_name)
if not provider:
logger.warning(f"Provider {provider_name} not configured, skipping")
continue
try:
logger.info(f"Trying {provider_name}...")
response = provider.chat(messages, **kwargs)
# Track success
self._track_success(provider_name, response)
logger.info(f"✅ Success with {provider_name} | {response.latency:.2f}s | ${response.cost:.6f}")
return response
except Exception as e:
logger.error(f"❌ {provider_name} failed: {e}")
self._track_failure(provider_name)
continue
# All failed
self.metrics["failed_requests"] += 1
raise Exception("All providers exhausted")
def _track_success(self, provider: str, response: LLMResponse):
"""Track successful request."""
self.metrics["successful_requests"] += 1
self.metrics["total_cost"] += response.cost
self.metrics["total_tokens"] += response.tokens
if provider not in self.metrics["by_provider"]:
self.metrics["by_provider"][provider] = {
"requests": 0,
"cost": 0.0,
"tokens": 0,
"latency_total": 0.0
}
self.metrics["by_provider"][provider]["requests"] += 1
self.metrics["by_provider"][provider]["cost"] += response.cost
self.metrics["by_provider"][provider]["tokens"] += response.tokens
self.metrics["by_provider"][provider]["latency_total"] += response.latency
def _track_failure(self, provider: str):
"""Track failed request."""
if provider not in self.metrics["by_provider"]:
self.metrics["by_provider"][provider] = {
"requests": 0,
"failures": 0
}
self.metrics["by_provider"][provider]["failures"] = \
self.metrics["by_provider"][provider].get("failures", 0) + 1
def get_stats(self) -> dict:
"""Returns metrics."""
return self.metrics
def print_stats(self):
"""Prints metrics."""
print("\n" + "="*60)
print("📊 UNIFIED CLIENT STATS")
print("="*60)
print(f"Total requests: {self.metrics['total_requests']}")
print(f"Successful: {self.metrics['successful_requests']}")
print(f"Failed: {self.metrics['failed_requests']}")
print(f"Total cost: ${self.metrics['total_cost']:.6f}")
print(f"Total tokens: {self.metrics['total_tokens']:,}")
print("\nBy provider:")
for provider, stats in self.metrics["by_provider"].items():
if stats.get("requests", 0) > 0:
avg_latency = stats["latency_total"] / stats["requests"]
print(f"\n {provider}:")
print(f" Requests: {stats['requests']}")
print(f" Cost: ${stats['cost']:.6f}")
print(f" Tokens: {stats['tokens']:,}")
print(f" Avg latency: {avg_latency:.2f}s")
print("="*60 + "\n")
# ============================================================================
# MAIN - DEMO
# ============================================================================
def main():
"""Unified client demo."""
print("\n🤖 UNIFIED AI CLIENT\n")
# Initialize with fallback chain
client = UnifiedAIClient(
fallback_chain=["openai", "openrouter", "ollama"]
)
# Test queries
prompts = [
"What is Python?",
"Explain what a server is",
"Give me a code example"
]
for prompt in prompts:
try:
response = client.chat(prompt)
print(f"\nPrompt: {prompt}")
print(f"Response: {response.content[:100]}...")
print(f"Provider: {response.provider} | Model: {response.model}")
print(f"Latency: {response.latency:.2f}s | Cost: ${response.cost:.6f}")
except Exception as e:
print(f"\n❌ Error: {e}")
# Show stats
client.print_stats()
if __name__ == "__main__":
main()
🚀 Usage
1. Setup .env:
OPENAI_API_KEY=sk-proj-...
OPENROUTER_API_KEY=sk-or-v1-...
2. Run:
python unified_client.py
Expected output:
🤖 UNIFIED AI CLIENT
Trying openai...
✅ Success with openai | 1.5s | $0.000025
Prompt: What is Python?
Response: Python is an interpreted programming language...
Provider: openai | Model: gpt-3.5-turbo
Latency: 1.50s | Cost: $0.000025
...
============================================================
📊 UNIFIED CLIENT STATS
============================================================
Total requests: 3
Successful: 3
Failed: 0
Total cost: $0.000075
Total tokens: 150
By provider:
openai:
Requests: 3
Cost: $0.000075
Tokens: 150
Avg latency: 1.50s
============================================================
✅ Self-Assessment Rubric
Architecture (30 pts):
- (10) Abstract interface (LLMProvider)
- (10) Factory pattern implementation
- (10) Extensible (easy to add providers)
Functionality (40 pts):
- (10) Multi-provider support (3+)
- (10) Fallback chain works
- (10) Complete metrics tracking
- (10) Robust error handling
Production-ready (30 pts):
- (10) Comprehensive logging
- (10) Testing suite (pytest)
- (10) Clear documentation
Total: ___/100
🎯 Optional Extensions
1. Add a new provider:
class AnthropicProvider(LLMProvider):
"""Anthropic Claude provider."""
def chat(self, messages, **kwargs):
# Implementation
pass
2. Smart routing:
def smart_route(self, prompt: str) -> str:
"""Auto-select provider based on the query."""
if len(prompt) < 50:
return "ollama" # Cheap
else:
return "openai" # Quality
3. Caching layer:
from functools import lru_cache
@lru_cache(maxsize=100)
def cached_chat(self, prompt: str):
return self.chat(prompt)
🎉 Congratulations!
You completed the entire "LLM Access Strategies" guide!
What you mastered:
Strategic:
- ✅ Decision framework (5 dimensions)
- ✅ Decision matrix with 7+ cases
- ✅ Quantitative trade-offs
Cloud APIs:
- ✅ OpenAI API (production-ready)
- ✅ OpenRouter (100+ models)
- ✅ Modal (serverless)
Local LLMs:
- ✅ LM Studio (GUI)
- ✅ Ollama (CLI + Docker)
Integration:
- ✅ Unified client pattern
- ✅ Multi-provider abstraction
- ✅ Fallback strategies
- ✅ Cost optimization
Completed projects (8):
- Decision Framework tool
- OpenAI API chatbot
- LM Studio chatbot (local)
- Ollama + Docker chatbot
- Smart OpenRouter chatbot
- Modal serverless API
- Comparative decision tool
- Unified AI Client (this one)
📚 Final Resources
Official docs:
Community:
🚀 Next Steps
1. Go deeper on a provider:
- If cloud: advanced OpenAI/OpenRouter
- If local: Ollama clustering
- If serverless: Modal + Ray
2. Implement in a real project:
- Production chatbot
- Client API
- Internal tools
3. Stay up to date:
- Pricing changes (check every 3 months)
- New models (Llama 3, GPT-5, etc.)
- New providers
🎓 Certification Completed
You have successfully completed:
✅ 8 Modules (64 capsules)
✅ 8 Integrating projects
✅ 5 Providers mastered
✅ 15-20 hours of practical content
You're ready to implement LLMs in production with confidence.
Congratulations on completing the entire bootcamp! 🎉🚀