Módulo 8: Unified AI Client — Proyecto integrador final
Proyecto Final: Unified AI Client Production-Ready
Descripción
Proyecto final integrador de toda la guía. Crearás un Unified AI Client production-ready que abstrae 5 proveedores (OpenAI, OpenRouter, Ollama, LM Studio, Modal).
Tiempo: 2-3 horas
Dificultad: Alta
🎯 Objetivo
Cliente unificado con:
- ✅ Multi-provider support (5 proveedores)
- ✅ Factory pattern (extensible)
- ✅ Fallback chain automático
- ✅ Cost optimization
- ✅ Retry strategies
- ✅ Monitoring completo
- ✅ Testing suite
💻 Arquitectura Completa
#!/usr/bin/env python3
"""
Unified AI Client - Proyecto Final
Abstrae múltiples proveedores LLM
"""
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:
"""Response estandarizado."""
content: str
model: str
provider: str
tokens: int
latency: float
cost: float
# ============================================================================
# ABSTRACT PROVIDER
# ============================================================================
class LLMProvider(ABC):
"""Interface abstracta para proveedores."""
@abstractmethod
def chat(self, messages: List[Dict], **kwargs) -> LLMResponse:
"""Envía chat y retorna response estandarizado."""
pass
@abstractmethod
def get_name(self) -> str:
"""Nombre del proveedor."""
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
# Costo (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:
"""Cliente unificado con multi-provider support."""
def __init__(self, fallback_chain: List[str] = None):
"""
Args:
fallback_chain: Lista de proveedores en orden (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]:
"""Inicializa providers disponibles."""
providers = {}
# OpenAI (si API key existe)
if os.getenv("OPENAI_API_KEY"):
providers["openai"] = OpenAIProvider(
api_key=os.getenv("OPENAI_API_KEY")
)
# OpenRouter (si API key existe)
if os.getenv("OPENROUTER_API_KEY"):
providers["openrouter"] = OpenRouterProvider(
api_key=os.getenv("OPENROUTER_API_KEY")
)
# Ollama (intenta 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:
"""
Envía chat con fallback automático.
Args:
prompt: Mensaje del usuario
**kwargs: Parameters adicionales (temperature, max_tokens, etc.)
Returns:
LLMResponse con resultado
"""
self.metrics["total_requests"] += 1
messages = [{"role": "user", "content": prompt}]
# Intenta cada provider en orden
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:
"""Retorna métricas."""
return self.metrics
def print_stats(self):
"""Imprime métricas."""
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():
"""Demo del unified client."""
print("\n🤖 UNIFIED AI CLIENT\n")
# Initialize con fallback chain
client = UnifiedAIClient(
fallback_chain=["openai", "openrouter", "ollama"]
)
# Test queries
prompts = [
"¿Qué es Python?",
"Explica qué es un servidor",
"Dame un ejemplo de código"
]
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()
🚀 Uso
1. Setup .env:
OPENAI_API_KEY=sk-proj-...
OPENROUTER_API_KEY=sk-or-v1-...
2. Ejecutar:
python unified_client.py
Output esperado:
🤖 UNIFIED AI CLIENT
Trying openai...
✅ Success with openai | 1.5s | $0.000025
Prompt: ¿Qué es Python?
Response: Python es un lenguaje de programación interpretado...
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
============================================================
✅ Rúbrica de Auto-Evaluación
Arquitectura (30 pts):
- (10) Abstract interface (LLMProvider)
- (10) Factory pattern implementation
- (10) Extensible (fácil añadir providers)
Funcionalidad (40 pts):
- (10) Multi-provider support (3+)
- (10) Fallback chain funciona
- (10) Metrics tracking completo
- (10) Error handling robusto
Production-ready (30 pts):
- (10) Logging comprehensivo
- (10) Testing suite (pytest)
- (10) Documentación clara
Total: ___/100
🎯 Extensiones Opcionales
1. Añadir provider nuevo:
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 según 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)
🎉 ¡Felicitaciones!
Completaste la guía completa "LLM Access Strategies"!
Lo que dominaste:
Estratégico:
- ✅ Framework de decisión (5 dimensiones)
- ✅ Matriz de decisión con 7+ casos
- ✅ Trade-offs cuantitativos
Cloud APIs:
- ✅ OpenAI API (production-ready)
- ✅ OpenRouter (100+ modelos)
- ✅ Modal (serverless)
Local LLMs:
- ✅ LM Studio (GUI)
- ✅ Ollama (CLI + Docker)
Integración:
- ✅ Unified client pattern
- ✅ Multi-provider abstraction
- ✅ Fallback strategies
- ✅ Cost optimization
Proyectos completados (8):
- Decision Framework tool
- Chatbot OpenAI API
- Chatbot LM Studio (local)
- Chatbot Ollama + Docker
- Smart Chatbot OpenRouter
- API Modal serverless
- Decision tool comparativo
- Unified AI Client (este)
📚 Recursos Finales
Docs oficiales:
Comunidad:
🚀 Próximos Pasos
1. Profundiza en un proveedor:
- Si cloud: OpenAI/OpenRouter avanzado
- Si local: Ollama clustering
- Si serverless: Modal + Ray
2. Implementa en proyecto real:
- Chatbot producción
- API cliente
- Internal tools
3. Mantente actualizado:
- Pricing cambia (revisa cada 3 meses)
- Nuevos modelos (Llama 3, GPT-5, etc.)
- Nuevos providers
🎓 Certificación Completada
Has completado exitosamente:
✅ 8 Módulos (64 cápsulas)
✅ 8 Proyectos integradores
✅ 5 Proveedores dominados
✅ 15-20 horas de contenido práctico
Estás listo para implementar LLMs en producción con confianza.
¡Enhorabuena por completar el bootcamp completo! 🎉🚀