Módulo 8: Unified AI Client — Proyecto integrador final

Módulo 8: Unified AI Client — Proyecto integrador final

Bienvenido al cierre del path. Hasta acá conoces cinco formas de acceder a un LLM, sabes elegir entre ellas con datos, y tienes el decision tool del Módulo 7 que automatiza la elección. Falta una pieza: un cliente Python que abstrae todos los proveedores detrás de una sola interfaz, así tu código de aplicación no se ata a ninguno.

Eso es lo que vas a construir en este módulo. Y vas a hacerlo a la altura de algo que pones en tu portfolio: arquitectura sólida, fallback automático, configuración declarativa, monitoring, y tests.

Al terminar el módulo vas a tener:

  • Una librería Python tuya, unified_ai_client, que abstrae OpenAI, OpenRouter, Ollama, LM Studio y Modal
  • Fallback automático entre providers cuando uno falla
  • Routing por prioridad (cost-first, quality-first, balanced)
  • Métricas observables (latencia, costo, errores por provider)
  • Tests de contrato y comportamiento que validan cada provider
  • Documentación para que tu equipo (o tu yo futuro) lo use sin reaprenderlo

Es el deliverable que cierra el path y el que llevas a entrevistas como prueba de dominio del topic.


Por qué este módulo importa profesionalmente

Cuatro razones concretas:

1. Es exactamente el patrón que startups serias implementan. Empresas como Vercel AI SDK, LiteLLM, LangChain, Portkey — todas construyen abstracciones equivalentes. Tener una propia (aunque más simple) demuestra que entiendes el problema, no solo el uso.

2. Reduces vendor lock-in en producción. Si tu CTO te pregunta "¿qué tan dependientes somos de OpenAI?", la respuesta correcta es "podemos cambiar a otro provider en una hora". Eso solo lo logras con esta abstracción.

3. Habilita estrategias avanzadas. Fallback automático, A/B testing entre providers, routing por costo en producción — todo se vuelve trivial con un cliente unificado.

4. Es código defendible en code review. A diferencia de un demo que solo te funciona a ti, esto tiene arquitectura, tests, y patterns reconocibles (factory, strategy, adapter). Es código de senior, no de junior.


Modelo mental: una interfaz, varios adapters

Pensemos en cómo van a interactuar las piezas:

                          ┌─────────────────────────────────┐
                          │   Tu código de aplicación       │
                          │   client = UnifiedClient(...)   │
                          │   r = client.chat("...")        │
                          └────────────────┬────────────────┘
                                           │
                                  ┌────────▼────────┐
                                  │  UnifiedClient  │
                                  │   (fachada)     │
                                  └────────┬────────┘
                                           │
                  ┌────────────────────────┼────────────────────────┐
                  │                        │                        │
            ┌─────▼─────┐          ┌──────▼──────┐         ┌───────▼──────┐
            │OpenAIAdapt│          │OllamaAdapter│         │ ModalAdapter │
            └─────┬─────┘          └──────┬──────┘         └───────┬──────┘
                  │                       │                        │
            ┌─────▼─────┐          ┌──────▼──────┐         ┌───────▼──────┐
            │ OpenAI API│          │  Ollama (local)│      │ Modal (cloud)│
            └───────────┘          └────────────────┘      └──────────────┘

Tres patrones de diseño se aplican naturalmente:

PatrónPara qué
AdapterCada provider tiene su propia API; el adapter la traduce a tu interfaz común
FactoryDecides qué adapter usar a partir de configuración (provider="openai")
StrategyFallback, routing por costo/calidad — distintas estrategias detrás de la misma interfaz

Vas a usar los tres. No por academicismo — porque resuelven problemas reales.


Un escenario que ilustra el módulo

Mike trabaja en una startup B2B. Su producto usa OpenAI hoy. El equipo discute:

  • Producto pide bajar costos para escalar (jefe de producto)
  • Compliance exige opción para deployments en EU (cliente enterprise)
  • Engineering quiere reducir lock-in (CTO)
  • DevOps necesita visibilidad de gasto por provider (finance)

Sin abstracción, cada uno de estos requerimientos es un proyecto independiente con refactor importante.

Con el Unified Client que vas a construir, estos problemas se vuelven configuración:

# Producto chat regular: OpenAI con fallback a OpenRouter
client_normal = UnifiedClient(
    primary="openai_gpt-4o-mini",
    fallback=["openrouter_mistral", "ollama_mistral"],
    metrics_enabled=True,
)

# Cliente EU: solo Modal en región EU
client_eu = UnifiedClient(
    primary="modal_eu_mistral",
    fallback=["selfhosted_ollama_eu"],
    metrics_enabled=True,
)

# Same call interface
response = client_normal.chat("¿Cómo funciona X?")
response_eu = client_eu.chat("¿Cómo funciona X?")

Cambios de provider son cambios de config, no rewrite de código. Eso es lo que entrega este módulo.


Conexión con el resto del path

Este módulo es donde todo lo previo se vuelve útil:

  • Módulo 1 te dio el framework para decidir → influye el primary que elijas
  • Módulos 2-6 te dieron experiencia con cada provider → cada uno se vuelve un adapter
  • Módulo 7 te dio benchmarks → informan cuándo cambiar providers
  • Módulo 8 te da la abstracción para hacer esos cambios sin dolor

Al final de este módulo cierras un loop: del análisis (M07) a la implementación (M08) que ejecuta tus decisiones.


Mapa del módulo

CápsulaTemaQué construyes
01Introducción (esta)Modelo mental + objetivos
02Arquitectura y diseñoDiagramas de clase, decisiones de diseño, contratos de la interfaz
03Implementación baseEsqueleto: UnifiedClient + primer adapter (OpenAI) funcionando
04Fallback strategyFailover automático con backoff, circuit breaker simple
05Cost optimizationRouting por prioridad (cost/quality/balanced) basado en config
06Monitoring y métricasTracking de latencia, costo, error rate por provider
07Testing y validationTests de contrato, mocks, contract testing por provider
08Proyecto finalVersión completa con todos los providers + docs + ejemplos

Cada cápsula construye sobre la anterior. Al final de la 08 tienes algo deployable.


Qué NO se cubre en este módulo

Para mantener scope:

  • Streaming (SSE / async iterators). El cliente devuelve respuestas completas. Streaming agrega complejidad que merece su propio módulo.
  • Function calling / tool use multi-turn. Cada provider lo implementa distinto; abstraerlo bien es un proyecto serio. Vemos un hook básico, no soporte completo.
  • Embeddings y otros endpoints. El cliente se enfoca en chat completion. La misma arquitectura aplica para embeddings, pero por scope no la implementamos.
  • Production-grade observability (Datadog, OpenTelemetry). Hacemos métricas simples en memoria. Para producción real, conectas a tu stack de observabilidad existente.
  • Rate limiting / circuit breaker complejo. Implementamos un circuit breaker simple. Para production seria, usa una librería dedicada (tenacity, pybreaker).

Trampas comunes al cursar el módulo

Trampa 1 — "Voy a hacer la abstracción perfecta antes de implementar." No. Hazlo iterativo: adapter OpenAI funcionando primero, después agregas el resto. La abstracción óptima emerge implementando, no diseñando en abstracto.

Trampa 2 — "Mi abstracción soporta todas las features de todos los providers." Imposible y contraproducente. Soporta la intersección de features comunes. Para features exclusivas (e.g., JSON mode estricto de OpenAI), expón un escape hatch (provider_specific_options).

Trampa 3 — "Copio LiteLLM." LiteLLM existe y es excelente. Si lo usaras en producción, probablemente tomarías LiteLLM. Pero construir el tuyo te enseña. Tu meta no es competir con LiteLLM — es entender el problema.

Trampa 4 — "Sin tests porque es proyecto demo." Sin tests, no es portfolio. Es script. La diferencia entre el cliente que enseñas en una entrevista y el que vergüenza enseñar es testing.


Pregunta de auto-evaluación

Antes de pasar a la cápsula 02:

  • ¿Cuál es la diferencia entre los patrones Adapter y Factory? ¿Para qué sirve cada uno?
  • ¿Por qué la abstracción debe soportar la intersección de features y no la unión?
  • Si tu cliente unificado se cae porque OpenAI tuvo un outage, ¿cuál es la diferencia entre fallback y circuit breaker?
Respuestas guía
  • Adapter: traduce una interfaz a otra. Aquí: la API de OpenAI/Anthropic/Ollama se "traduce" a tu interfaz común chat(prompt) -> str. Factory: decide qué objeto crear a partir de configuración. Aquí: dado provider="openai", te devuelve el OpenAIAdapter. Un Factory usa Adapters; no son competidores, son colaboradores.
  • Porque si soportas la unión (todas las features de todos los providers), tu interfaz se vuelve enorme y la mayoría no funciona para la mayoría de providers. La intersección (features comunes) te da una interfaz pequeña que siempre funciona. Para features exclusivas, tienes un escape hatch.
  • Fallback: cuando una request falla, retry con otro provider. Mecanismo por-request. Circuit breaker: si un provider falla repetidamente, deja de intentarlo por un tiempo (evitas pagar timeouts una y otra vez). Mecanismo de estado a través de muchas requests. Los dos son complementarios.

Evidencia de éxito al terminar el módulo

Vas a saber que terminaste bien si:

  • pip install -e . instala tu librería localmente
  • ✅ Puedes hacer client = UnifiedClient(provider="openai") o provider="modal" y la misma llamada chat(...) funciona
  • ✅ Si OpenAI cae, tu cliente automáticamente intenta el siguiente provider configurado
  • client.get_metrics() devuelve tracking real de uso por provider
  • pytest corre tests verdes contra al menos 2 providers (mocks acceptables para los que no tienes API key)
  • ✅ Tu README tiene ejemplos copy-pasteables que un compañero entiende sin tu ayuda

Siguiente cápsula

02 — Arquitectura y diseño. Antes de teclear código, vamos a diseñar la interfaz: qué métodos expone UnifiedClient, qué contrato cumple cada Adapter, cómo se configura. Decisiones de diseño explícitas evitan refactors dolorosos en la cápsula 03 cuando empieces a implementar.


Recursos

  1. LiteLLM — implementación de referencia (más completa que la tuya; útil para inspirarse).
  2. LangChain Chat Models — abstracción equivalente en el ecosistema LangChain.
  3. Vercel AI SDK — versión TypeScript del mismo patrón.
  4. Design Patterns — Adapter, Factory, Strategy — refresher conceptual.
  5. Architectural Decision Records — para documentar tus decisiones de diseño.