Saltar a contenido

LiteLLM: gateway unificado para LLMs

Empiezas con OpenAI. Luego el equipo de datos quiere Claude. Marketing pide Gemini. Y tú, que ya montaste Ollama para lo que no puede salir de la red interna, acabas con cuatro SDKs distintos, cuatro formatos de error y cero idea de cuánto está gastando cada equipo.

LiteLLM resuelve exactamente eso: una sola API OpenAI-compatible por delante de 100+ proveedores. Tu código habla siempre /v1/chat/completions; el gateway decide si eso acaba en Azure, en Bedrock, en un vLLM de tu cluster o en el Ollama del portátil de alguien.

🎯 Qué problema resuelve

Sin gateway Con LiteLLM
Un SDK por proveedor Un solo cliente OpenAI
Claves de proveedor repartidas por el código Claves virtuales revocables
Coste real desconocido hasta la factura Cost tracking por token, equipo y usuario
Caída de proveedor = caída de tu app Fallbacks automáticos entre proveedores
Rate limits gestionados a mano Load balancing entre despliegues
flowchart LR
    A["Apps / Agentes<br/>(cliente OpenAI)"] --> G["LiteLLM Proxy<br/>:4000"]
    G --> K[("Postgres<br/>keys · spend")]
    G --> R[("Redis<br/>cache · estado router")]
    G --> C1["OpenAI"]
    G --> C2["Azure OpenAI"]
    G --> C3["AWS Bedrock"]
    G --> L1["Ollama local"]
    G --> L2["vLLM en K8s"]
    G -.logs.-> O["Langfuse / Prometheus"]

🐍 SDK Python vs Proxy Server

LiteLLM son dos cosas que se suelen confundir:

  • SDK Python (pip install litellm): una librería que normaliza llamadas dentro de tu proceso. Ideal para un script o un servicio único.
  • Proxy Server (litellm[proxy]): un servidor HTTP centralizado con claves virtuales, presupuestos, logging y UI. Es lo que quieres cuando hay más de un consumidor.
# SDK: mismo código, distinto proveedor
from litellm import completion

resp = completion(
    model="anthropic/claude-sonnet-4-5",
    messages=[{"role": "user", "content": "Explica Kubernetes en 3 líneas"}],
)
print(resp.choices[0].message.content)

Regla práctica

Si necesitas responder "¿cuánto gastó el equipo de datos este mes?", necesitas el Proxy. Si no, el SDK sobra y basta.

⚙️ Configuración con config.yaml

El corazón del proxy es config.yaml. Su bloque principal es model_list, que mapea un nombre virtual (model_name, lo que piden tus apps) a los parámetros reales (litellm_params, lo que se envía al proveedor).

model_list:
  - model_name: gpt-4o                      # nombre que usan tus apps
    litellm_params:
      model: azure/gpt-4o-eu                # modelo real enviado al proveedor
      api_base: https://my-endpoint-europe.openai.azure.com/
      api_key: "os.environ/AZURE_API_KEY_EU"
      rpm: 6                                # rate limit de este despliegue

  - model_name: anthropic-claude
    litellm_params:
      model: bedrock/anthropic.claude-instant-v1
      aws_region_name: us-east-1

  - model_name: local-llama                 # tu Ollama, misma API
    litellm_params:
      model: ollama/llama3
      api_base: http://ollama:11434

  - model_name: vllm-models
    litellm_params:
      model: openai/facebook/opt-125m       # el prefijo openai/ = API compatible
      api_base: http://0.0.0.0:8000/v1
      api_key: none

litellm_settings:
  drop_params: True                         # ignora params no soportados por el proveedor
  success_callback: ["langfuse"]

general_settings:
  master_key: sk-1234                       # exige Authorization: Bearer en toda llamada
  alerting: ["slack"]

Arráncalo y consúmelo como si fuera OpenAI:

litellm --config config.yaml --port 4000
curl http://0.0.0.0:4000/v1/chat/completions \
  -H "Authorization: Bearer sk-1234" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "local-llama",
    "messages": [{"role": "user", "content": "Hola"}]
  }'
# Cualquier cliente OpenAI vale: solo cambia base_url
from openai import OpenAI

client = OpenAI(api_key="sk-1234", base_url="http://0.0.0.0:4000")
resp = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Resume este incidente"}],
)

El comodín

Una entrada model_name: "*" con model: "*" deja pasar cualquier modelo del proveedor usando las credenciales del entorno. Cómodo en desarrollo, mala idea en producción: pierdes el control de qué modelos se pueden invocar.

🔑 Claves virtuales y presupuestos

Tus apps nunca deberían ver una clave de proveedor. En su lugar, el proxy emite claves virtuales contra /key/generate, autenticándote con el master_key.

curl -X POST 'http://0.0.0.0:4000/key/generate' \
  -H 'Authorization: Bearer sk-1234' \
  -H 'Content-Type: application/json' \
  -d '{
    "models": ["gpt-4o", "local-llama"],
    "max_budget": 50,
    "duration": "30d",
    "team_id": "equipo-datos"
  }'

El spend se atribuye de forma jerárquica y los presupuestos se heredan hacia abajo:

Organization Spend
    ├── Team 1 Spend
    │   ├── User A Spend
    │   │   ├── Key 1 Spend
    │   │   └── Key 2 Spend
    │   └── Service Account Spend
    └── Team 2 Spend

Se puede poner budget en cualquier nivel, con una regla estricta: el presupuesto de un equipo no puede exceder el de su organización, ni el de un usuario el de su equipo. Si algún nivel de la jerarquía se pasa, la petición se bloquea en tiempo real.

Requiere base de datos

Claves virtuales, equipos y spend persistente necesitan Postgres (database_url en general_settings). Sin BD, el proxy funciona pero es efímero: no hay gobernanza ni histórico.

💰 Cost tracking

Cada petición genera una entrada en LiteLLM_SpendLogs con la atribución completa:

{
  "api_key": "fe6b0cab4ff5a5a8df823196cc8a450*****",
  "user": "default_user",
  "team_id": "e8d1460f-846c-45d7-9b43-55f3cc52ac32",
  "request_tags": ["jobID:214590dsff09fds", "taskName:run_page_classification"],
  "end_user": "palantir",
  "model_group": "llama3",
  "api_base": "https://api.groq.com/openai/v1/",
  "spend": 0.000002,
  "total_tokens": 100,
  "completion_tokens": 80,
  "prompt_tokens": 20
}

Con request_tags puedes responder preguntas del tipo "¿cuánto nos costó el batch nocturno de clasificación?" sin instrumentar nada en la aplicación: basta con etiquetar la petición.

🔁 Fallbacks, reintentos y load balancing

Aquí es donde el gateway deja de ser comodidad y pasa a ser disponibilidad. Todo vive en router_settings.

Load balancing

Repite el mismo model_name en varias entradas: LiteLLM las trata como despliegues intercambiables del mismo grupo.

model_list:
  - model_name: gpt-3.5-turbo
    litellm_params:
      model: azure/gpt-turbo-small-ca
      api_base: https://my-endpoint-canada.openai.azure.com/
      api_key: os.environ/AZURE_API_KEY_CA
      rpm: 6
  - model_name: gpt-3.5-turbo
    litellm_params:
      model: azure/gpt-turbo-large
      api_base: https://openai-france-1234.openai.azure.com/
      api_key: os.environ/AZURE_API_KEY_FR
      rpm: 1440

router_settings:
  routing_strategy: simple-shuffle   # simple-shuffle | least-busy | usage-based-routing | latency-based-routing
  num_retries: 2
  timeout: 30                        # segundos, para la llamada completa
  redis_host: os.environ/REDIS_HOST  # obligatorio con varias réplicas del proxy
  redis_port: os.environ/REDIS_PORT
  redis_password: os.environ/REDIS_PASSWORD
routing_strategy Cuándo usarla
simple-shuffle Por defecto. Reparto aleatorio ponderado por rpm/tpm
least-busy Despliegues con latencias muy dispares bajo carga
usage-based-routing Exprimir cuotas TPM/RPM sin tocar los límites
latency-based-routing Prioridad a la respuesta más rápida observada

Redis no es opcional en HA

El estado del router (uso, latencias, budgets) es local a cada réplica. Con más de un pod y sin Redis, cada instancia contará por su cuenta y los rate limits y presupuestos se te irán de las manos.

Fallbacks entre proveedores

router_settings:
  fallbacks: [{"gpt-4": ["azure/gpt-4", "anthropic-claude"]}]
  num_retries: 2

Si el grupo gpt-4 agota sus reintentos, la petición salta al siguiente modelo de la lista de forma transparente para el cliente. Un patrón muy útil: cloud primero, modelo local como último recurso para degradar en vez de caer.

⚡ Caching

Respuestas idénticas no deberían pagarse dos veces. LiteLLM cachea en Redis desde litellm_settings:

litellm_settings:
  cache: true
  cache_params:
    type: redis
    host: os.environ/REDIS_HOST
    port: 6379
    ttl: 600            # segundos en Redis
  enable_redis_auth_cache: true   # comparte auth de claves entre workers y réplicas

general_settings:
  user_api_key_cache_ttl: 300     # opcional, en segundos

enable_redis_auth_cache es el detalle que suele faltar: sin él, cada worker valida las claves virtuales contra Postgres y la BD se convierte en el cuello de botella.

📈 Logging y observabilidad

litellm_settings:
  success_callback: ["langfuse", "prometheus"]
  failure_callback: ["langfuse"]

El endpoint /metrics expone métricas Prometheus listas para alertar, entre ellas:

  • litellm_api_key_max_budget_metric — presupuesto asignado a la clave
  • litellm_remaining_api_key_budget_metric — saldo restante
  • litellm_api_key_budget_remaining_hours_metric — horas hasta el reset

Alerta antes del corte

Alerta sobre litellm_remaining_api_key_budget_metric cuando baje del 20 %. Enterarte de que un equipo se quedó sin presupuesto por los tickets de "la IA no funciona" es un mal día.

🐳 Despliegue

Docker

docker run --rm \
  --name litellm-proxy \
  -p 4000:4000 \
  -e OPENAI_API_KEY=$OPENAI_API_KEY \
  -e DATABASE_URL=$DATABASE_URL \
  -v $(pwd)/config.yaml:/app/config.yaml \
  docker.litellm.ai/berriai/litellm:latest \
  --config /app/config.yaml

La UI de administración (claves, equipos, spend) queda en http://localhost:4000/ui.

Kubernetes

apiVersion: apps/v1
kind: Deployment
metadata:
  name: litellm-proxy
spec:
  replicas: 3
  selector:
    matchLabels:
      app: litellm
  template:
    metadata:
      labels:
        app: litellm
    spec:
      containers:
        - name: litellm
          image: docker.litellm.ai/berriai/litellm:latest
          args: ["--config", "/app/config.yaml", "--port", "4000"]
          ports:
            - containerPort: 4000
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef: { name: litellm-secrets, key: database-url }
            - name: REDIS_HOST
              value: redis.default.svc.cluster.local
            - name: LITELLM_MASTER_KEY
              valueFrom:
                secretKeyRef: { name: litellm-secrets, key: master-key }
          volumeMounts:
            - name: config
              mountPath: /app/config.yaml
              subPath: config.yaml
      volumes:
        - name: config
          configMap:
            name: litellm-config

Existe además un chart oficial de Helm que resuelve Postgres, Redis (incluido modo cluster) y secretos sin escribir estos manifiestos a mano.

El master key es la llave del reino

master_key permite crear claves, cambiar presupuestos y leer el spend de todos. Va en un Secret o en un gestor externo, nunca en el config.yaml versionado en Git.

🔗 Relacionados