écrits/tutorial/2026/07
Tutorial21 juil. 2026·28 min

Héberger sa propre passerelle IA avec LiteLLM Proxy : routage, budgets et observabilité

Déployez votre propre passerelle IA avec LiteLLM Proxy. Installation Docker Compose avec Postgres et Redis, routage et fallbacks entre modèles, clés virtuelles avec budgets par équipe, cache Redis et connexion depuis TypeScript avec le SDK OpenAI et le Vercel AI SDK.

Toute équipe qui livre plus d'une fonctionnalité IA finit par se heurter au même mur. Un service appelle Anthropic directement, un autre appelle OpenAI, un troisième utilise une instance Ollama locale. Les clés API sont dispersées dans quatre fichiers .env. Personne ne peut répondre à la question « combien avons-nous dépensé en IA le mois dernier, par équipe ? » sans exporter trois CSV de facturation. Et lorsqu'un fournisseur tombe en panne, chaque service échoue de son côté, faute de fallback partagé.

Une passerelle IA résout ce problème en plaçant un seul point d'entrée compatible OpenAI devant tous vos modèles. Vos applications parlent à la passerelle ; la passerelle parle aux fournisseurs. Routage, réessais, budgets, cache et journalisation vivent au même endroit.

LiteLLM Proxy est l'option open source de cette catégorie. Contrairement aux passerelles hébergées, elle tourne sur votre propre infrastructure — un conteneur, une base Postgres et une instance Redis — ce qui compte lorsque vous traitez des données clients soumises à une exigence de résidence, ou lorsque vous ne souhaitez simplement pas d'intermédiaire tiers entre vous et chaque appel de modèle.

Ce tutoriel déploie une passerelle LiteLLM en configuration proche de la production, puis y connecte une application TypeScript.

Une passerelle, pas un framework. LiteLLM Proxy ne remplace pas votre framework d'agents. Vous continuez d'utiliser le Vercel AI SDK, LangGraph ou un simple fetch. La passerelle ne change que l'URL de base et la clé utilisées par ces bibliothèques — c'est pourquoi l'adoption peut être progressive, un service à la fois.

Prérequis

Avant de commencer, assurez-vous de disposer de :

  • Docker et Docker Compose installés et opérationnels
  • Node.js 20+ pour le client TypeScript
  • Au moins une clé API fournisseur (Anthropic, OpenAI, Google ou un serveur Ollama local)
  • Des bases en variables d'environnement et YAML
  • Un terminal à l'aise avec curl

Aucune connaissance de Python n'est requise. LiteLLM est écrit en Python, mais nous l'exécutons entièrement en conteneur et interagissons avec via HTTP.

Ce que vous allez construire

Une passerelle qui :

  1. Expose un point d'entrée unique compatible OpenAI sur http://localhost:4000
  2. Route les requêtes entre Claude, GPT et un modèle local derrière des alias comme smart et cheap
  3. Bascule automatiquement en cas d'erreur ou de timeout d'un fournisseur
  4. Émet des clés virtuelles par équipe, chacune avec son budget mensuel et ses limites de débit
  5. Met en cache dans Redis les requêtes identiques pour réduire les coûts
  6. Journalise chaque appel — modèle, jetons, coût, latence — dans Postgres, avec une interface d'administration intégrée

Nous la consommerons ensuite depuis TypeScript de deux manières : le SDK OpenAI brut, et le Vercel AI SDK pour le streaming.

Étape 1 : structure du projet

Créez un répertoire dédié à la passerelle. Il reste séparé du code applicatif : la passerelle est une infrastructure, pas un morceau d'une application donnée.

mkdir ai-gateway && cd ai-gateway
touch docker-compose.yml config.yaml .env

Ajoutez les secrets dans .env. Ne versionnez jamais ce fichier.

# .env
LITELLM_MASTER_KEY=sk-master-a-remplacer-par-une-longue-chaine-aleatoire
LITELLM_SALT_KEY=autre-longue-chaine-aleatoire-pour-chiffrer-les-cles-fournisseurs
 
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-proj-...
GEMINI_API_KEY=...
 
POSTGRES_PASSWORD=un-mot-de-passe-solide

Deux clés méritent une explication. LITELLM_MASTER_KEY est l'identifiant administrateur — il peut créer et révoquer toutes les autres clés, il ne doit donc jamais apparaître dans une application. LITELLM_SALT_KEY chiffre les identifiants fournisseurs stockés en base ; si vous la modifiez plus tard, les clés déjà enregistrées deviennent illisibles. Définissez-la une fois et sauvegardez-la.

Étape 2 : Docker Compose avec Postgres et Redis

Le proxy fonctionne sans base de données, mais vous perdez tout ce qui est intéressant : clés virtuelles, budgets et journaux de dépenses exigent Postgres. Redis alimente le cache et rend les limites de débit exactes lorsque plusieurs répliques du proxy tournent en parallèle.

# docker-compose.yml
services:
  litellm:
    image: ghcr.io/berriai/litellm:main-stable
    ports:
      - "4000:4000"
    volumes:
      - ./config.yaml:/app/config.yaml
    command: ["--config", "/app/config.yaml", "--port", "4000"]
    environment:
      LITELLM_MASTER_KEY: ${LITELLM_MASTER_KEY}
      LITELLM_SALT_KEY: ${LITELLM_SALT_KEY}
      DATABASE_URL: postgresql://litellm:${POSTGRES_PASSWORD}@postgres:5432/litellm
      REDIS_HOST: redis
      REDIS_PORT: "6379"
      ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
      OPENAI_API_KEY: ${OPENAI_API_KEY}
      GEMINI_API_KEY: ${GEMINI_API_KEY}
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started
    restart: unless-stopped
 
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: litellm
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: litellm
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U litellm"]
      interval: 5s
      timeout: 5s
      retries: 10
    restart: unless-stopped
 
  redis:
    image: redis:7-alpine
    command: ["redis-server", "--maxmemory", "512mb", "--maxmemory-policy", "allkeys-lru"]
    restart: unless-stopped
 
volumes:
  pgdata:

La politique d'éviction Redis a son importance. allkeys-lru signifie que lorsque le cache est plein, les entrées les moins récemment utilisées sont supprimées plutôt que le serveur ne refuse les écritures — un cache qui cesse d'accepter des entrées transforme silencieusement votre passerelle en simple relais coûteux.

Étape 3 : le fichier de configuration — modèles, alias et fallbacks

config.yaml est là où la passerelle prend toute sa valeur. L'idée centrale : vous définissez des noms de modèles publics utilisés par vos applications, et vous associez chacun à un ou plusieurs déploiements fournisseurs réels.

# config.yaml
model_list:
  # Un palier "smart" adossé à deux fournisseurs
  - model_name: smart
    litellm_params:
      model: anthropic/claude-opus-4-8
      api_key: os.environ/ANTHROPIC_API_KEY
      rpm: 500
  - model_name: smart
    litellm_params:
      model: openai/gpt-5.6
      api_key: os.environ/OPENAI_API_KEY
      rpm: 500
 
  # Un palier "cheap" pour la classification et le résumé
  - model_name: cheap
    litellm_params:
      model: anthropic/claude-haiku-4-5-20251001
      api_key: os.environ/ANTHROPIC_API_KEY
 
  # Un modèle auto-hébergé pour ce qui ne doit pas quitter le réseau
  - model_name: private
    litellm_params:
      model: ollama/qwen3:14b
      api_base: http://host.docker.internal:11434
 
  # Les embeddings, pour que les pipelines vectoriels passent aussi par la passerelle
  - model_name: embed
    litellm_params:
      model: openai/text-embedding-3-large
      api_key: os.environ/OPENAI_API_KEY
 
router_settings:
  routing_strategy: usage-based-routing-v2
  redis_host: os.environ/REDIS_HOST
  redis_port: os.environ/REDIS_PORT
  num_retries: 2
  timeout: 60
  fallbacks:
    - smart: ["cheap"]
  context_window_fallbacks:
    - smart: ["smart"]
 
litellm_settings:
  drop_params: true
  set_verbose: false
 
general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY
  database_url: os.environ/DATABASE_URL

Plusieurs décisions méritent d'être détaillées.

Deux entrées portent le nom smart. C'est volontaire : LiteLLM traite les entrées homonymes comme un pool de répartition de charge. Avec usage-based-routing-v2, le trafic est réparti selon la marge de débit restante de chaque déploiement, suivie dans Redis pour que toutes les répliques restent d'accord.

Les alias découplent les applications des fournisseurs. Votre application demande smart. Dans six mois, vous changez le modèle sous-jacent dans un seul fichier YAML, vous redémarrez le conteneur, et tous les services sont mis à niveau. Pas de redéploiement, pas de changement de code, pas de réunion de coordination.

drop_params: true supprime silencieusement les paramètres qu'un fournisseur donné ne prend pas en charge au lieu de renvoyer une erreur 400. C'est ce qui rend le principe « même code, modèle différent » réellement praticable.

Les fallbacks se déclenchent sur les erreurs, pas sur la qualité. Si tous les déploiements smart échouent, la requête est réessayée sur cheap. La réponse est dégradée, mais une réponse dégradée vaut mieux qu'une erreur 500 pour la plupart des fonctionnalités visibles par l'utilisateur. Décidez route par route si ce compromis est acceptable.

Lancez l'ensemble :

docker compose up -d
docker compose logs -f litellm

Attendez la ligne de log indiquant que le serveur écoute sur le port 4000, puis vérifiez :

curl http://localhost:4000/health/liveliness

Étape 4 : votre première requête

Testez d'abord avec la clé maître, simplement pour confirmer que le routage fonctionne de bout en bout :

curl http://localhost:4000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY" \
  -d '{
    "model": "smart",
    "messages": [{"role": "user", "content": "Réponds exactement : passerelle ok"}]
  }'

La réponse est un JSON de complétion OpenAI standard, quel que soit le fournisseur qui l'a réellement servie. Cette compatibilité est tout l'intérêt : n'importe quel SDK, outil ou plugin d'IDE qui parle l'API OpenAI parle désormais à tous vos modèles configurés.

Ne livrez jamais la clé maître. Elle dispose de tous les pouvoirs administratifs. À partir d'ici, les applications reçoivent des clés virtuelles, et la clé maître ne vit que dans votre gestionnaire de secrets et votre propre terminal.

Étape 5 : clés virtuelles, équipes et budgets

C'est la fonctionnalité qui justifie l'auto-hébergement. Une clé virtuelle est un identifiant que vous générez via l'API, limité à certains modèles, avec un plafond de dépenses et des limites de débit associés.

Créez d'abord une équipe, puis une clé qui lui appartient :

# Créer une équipe avec un plafond mensuel
curl http://localhost:4000/team/new \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "team_alias": "web-frontend",
    "max_budget": 200,
    "budget_duration": "30d",
    "models": ["smart", "cheap", "embed"]
  }'

La réponse contient un team_id. Utilisez-le pour générer une clé :

curl http://localhost:4000/key/generate \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "team_id": "COLLER_LE_TEAM_ID_ICI",
    "key_alias": "web-frontend-production",
    "max_budget": 50,
    "budget_duration": "30d",
    "rpm_limit": 120,
    "tpm_limit": 200000,
    "metadata": {"service": "noqta-web", "env": "production"}
  }'

Vous récupérez une clé sk-.... Trois limites s'appliquent désormais simultanément : le budget propre de la clé (50 USD), celui de l'équipe (200 USD), et les plafonds de requêtes et de jetons par minute. Dès que l'une est dépassée, la passerelle renvoie un 429 en nommant la limite déclenchée — bien plus facile à diagnostiquer qu'un rejet générique de fournisseur.

Une pratique à adopter : générer une clé par service et par environnement, jamais une clé par développeur. En cas de fuite, vous révoquez l'identifiant d'un seul service et le remplacez à un seul endroit :

curl http://localhost:4000/key/delete \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"keys": ["sk-la-cle-compromise"]}'

Consultez les dépenses à tout moment :

curl "http://localhost:4000/key/info?key=sk-votre-cle" \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY"

Étape 6 : connexion depuis TypeScript

La passerelle étant compatible OpenAI, le SDK officiel OpenAI fonctionne sans modification. Seuls baseURL et apiKey changent.

npm install openai
// lib/gateway.ts
import OpenAI from "openai";
 
export const gateway = new OpenAI({
  baseURL: process.env.LITELLM_BASE_URL ?? "http://localhost:4000/v1",
  apiKey: process.env.LITELLM_API_KEY!, // la clé virtuelle, pas la clé maître
});
 
export async function resumer(texte: string) {
  const res = await gateway.chat.completions.create({
    model: "cheap", // un alias, pas un identifiant de modèle fournisseur
    messages: [
      { role: "system", content: "Résume en trois points." },
      { role: "user", content: texte },
    ],
  });
 
  return res.choices[0]?.message?.content ?? "";
}

Remarquez qu'aucune ligne de ce fichier ne nomme un fournisseur. C'est précisément la propriété que vous achetez : du code applicatif qui n'a aucune opinion sur le laboratoire ayant entraîné le modèle derrière lui.

Streaming avec le Vercel AI SDK

Pour les applications Next.js, pointez le provider compatible OpenAI du AI SDK vers la passerelle :

npm install ai @ai-sdk/openai-compatible
// app/api/chat/route.ts
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import { streamText, convertToModelMessages } from "ai";
 
const gateway = createOpenAICompatible({
  name: "litellm",
  baseURL: process.env.LITELLM_BASE_URL!,
  apiKey: process.env.LITELLM_API_KEY!,
});
 
export async function POST(req: Request) {
  const { messages } = await req.json();
 
  const result = streamText({
    model: gateway.chatModel("smart"),
    messages: convertToModelMessages(messages),
    // Ces métadonnées atteignent la passerelle et se retrouvent dans les journaux de dépenses
    headers: {
      "x-litellm-tags": "feature:support-chat,tier:pro",
    },
  });
 
  return result.toUIMessageStreamResponse();
}

Ces tags sont plus utiles qu'il n'y paraît. L'attribution cesse d'être « l'application web a dépensé 340 USD » pour devenir « le chat support a dépensé 210 USD, l'onboarding 90 USD » — la granularité nécessaire pour décider quoi optimiser.

Étape 7 : le cache

Les prompts identiques sont plus fréquents que les équipes ne l'imaginent : réessais, pages rechargées, traitements par lots relancés sur des lignes inchangées. Le cache Redis les transforme en réponses gratuites et instantanées.

Ajoutez dans config.yaml :

litellm_settings:
  drop_params: true
  cache: true
  cache_params:
    type: redis
    host: os.environ/REDIS_HOST
    port: os.environ/REDIS_PORT
    ttl: 3600
    supported_call_types: ["acompletion", "atext_completion", "aembedding"]

Redémarrez avec docker compose restart litellm. Envoyez deux fois la même requête et comparez : la seconde réponse revient en quelques millisecondes et enregistre un coût nul.

Désactivez le cache par requête lorsqu'un appel doit toujours être frais :

const res = await gateway.chat.completions.create(
  { model: "cheap", messages },
  { headers: { "x-litellm-no-cache": "true" } },
);

C'est sur les embeddings que le cache est le plus rentable. Réindexer un corpus revient généralement à recalculer les vecteurs de textes inchangés ; avec un cache en amont, seuls les fragments réellement nouveaux atteignent le fournisseur.

Prudence avec les prompts personnalisés. Si votre gabarit de prompt interpole le nom d'un utilisateur ou des données de compte, deux utilisateurs différents ne produiront jamais un prompt identique et le cache ne servira jamais. En revanche, si votre gabarit n'interpole rien de spécifique à l'utilisateur alors que les réponses devraient différer, le cache servira la réponse d'un utilisateur à un autre. Auditez la construction de vos prompts avant une activation généralisée.

Étape 8 : observabilité

Chaque requête atterrit déjà dans Postgres. L'interface intégrée sur http://localhost:4000/ui — connectez-vous avec la clé maître — affiche les dépenses par clé, par équipe et par modèle, ainsi que les taux d'erreur et les percentiles de latence.

Pour un traçage plus fin, ajoutez des callbacks qui transmettent les métadonnées à une plateforme d'observabilité :

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

Fournissez LANGFUSE_PUBLIC_KEY et LANGFUSE_SECRET_KEY comme variables d'environnement dans le fichier compose. Chaque appel via la passerelle produit alors une trace avec prompt, complétion, jetons, coût et latence — sans une seule ligne d'instrumentation dans votre application. Si vous utilisez déjà Langfuse, notre tutoriel sur l'observabilité LLM avec Langfuse couvre la partie tableau de bord.

Pour les alertes, la passerelle peut publier les notifications de budget et d'erreur sur Slack :

general_settings:
  alerting: ["slack"]
  alerting_threshold: 300  # secondes ; signale les requêtes qui traînent

Tester votre implémentation

Déroulez ces vérifications avant d'envoyer du trafic de production vers la passerelle.

Routage. Appelez smart vingt fois et vérifiez dans l'interface que les requêtes se répartissent entre les deux déploiements plutôt que de rester bloquées sur un seul.

Fallback. Placez temporairement une api_key invalide sur l'entrée Anthropic, redémarrez, puis envoyez une requête. Elle doit aboutir via OpenAI, et les journaux doivent montrer un réessai.

Application des budgets. Générez une clé avec "max_budget": 0.01, envoyez des requêtes jusqu'au déclenchement, et confirmez que vous recevez un 429 nommant le budget.

Limites de débit. Générez une clé avec "rpm_limit": 2 et envoyez cinq requêtes en boucle :

for i in $(seq 1 5); do
  curl -s -o /dev/null -w "%{http_code}\n" http://localhost:4000/v1/chat/completions \
    -H "Authorization: Bearer $TEST_KEY" \
    -H "Content-Type: application/json" \
    -d '{"model":"cheap","messages":[{"role":"user","content":"salut"}]}'
done

Vous devriez voir deux réponses 200 suivies de 429.

Cache. Envoyez deux fois une requête identique et comparez la latence et le coût enregistré.

Dépannage

Le conteneur démarre puis s'arrête. C'est presque toujours la base de données. Consultez docker compose logs postgres et vérifiez que DATABASE_URL correspond exactement aux identifiants Postgres — un mot de passe incohérent produit une erreur de démarrage déroutante plutôt qu'un échec d'authentification explicite.

Invalid model name passed in. Le nom de modèle de votre requête doit correspondre à un model_name de config.yaml, pas à l'identifiant du fournisseur. Demander claude-opus-4-8 échoue si vous avez défini l'alias smart. Listez ce qui est disponible avec curl http://localhost:4000/v1/models.

Ollama est injoignable depuis le conteneur. Dans Docker, localhost désigne le conteneur lui-même. Utilisez http://host.docker.internal:11434 sur macOS et Windows ; sur Linux, ajoutez extra_hosts: ["host.docker.internal:host-gateway"] au service.

Les budgets ne se réinitialisent jamais. budget_duration est une fenêtre glissante depuis la création de la clé, pas un mois calendaire. Une clé créée le 20 se réinitialise le 20.

Les dépenses restent à zéro pour un modèle personnalisé. LiteLLM tarifie les requêtes à partir d'une table de coûts intégrée. Les modèles auto-hébergés ou atypiques nécessitent une tarification explicite :

  - model_name: private
    litellm_params:
      model: ollama/qwen3:14b
      api_base: http://host.docker.internal:11434
    model_info:
      input_cost_per_token: 0.0000001
      output_cost_per_token: 0.0000002

Le streaming se bloque derrière un reverse proxy. Nginx met les réponses en tampon par défaut, ce qui casse les server-sent events. Ajoutez proxy_buffering off; sur le bloc de localisation de la passerelle.

Passage en production

Quelques changements séparent l'installation locale ci-dessus d'un système auquel confier du trafic réel :

  1. Terminez le TLS. Placez le proxy derrière Caddy, Nginx ou un load balancer. N'exposez jamais le port 4000 directement.
  2. Faites tourner plusieurs répliques. Le proxy est sans état ; Redis garde les limites de débit et le cache cohérents entre instances.
  3. Utilisez un Postgres managé. Le registre des dépenses est votre document de facturation : il mérite de vraies sauvegardes.
  4. Épinglez le tag de l'image. main-stable évolue. Fixez une version précise et mettez à jour délibérément.
  5. Faites tourner la clé maître régulièrement et gardez-la hors de tout environnement applicatif.
  6. Configurez des alertes sur le budget, pas seulement sur les erreurs. Les dérives de coût sont généralement silencieuses.

Étapes suivantes

  • Faites transiter vos agents existants par la passerelle — les tutoriels Vercel AI SDK et Mastra utilisent tous deux des URL de base configurables
  • Ajoutez Langfuse pour le versionnage des prompts en complément du traçage au niveau passerelle
  • Comparez avec l'approche managée dans notre tutoriel Vercel AI Gateway
  • Activez des plugins de garde-fous pour masquer les données personnelles avant que les requêtes n'atteignent des fournisseurs externes

Conclusion

Le motif de la passerelle relève d'une infrastructure peu spectaculaire qui élimine discrètement toute une catégorie de problèmes. Les clés fournisseurs cessent de se disséminer dans votre base de code. Le choix du modèle devient un changement de configuration plutôt qu'un déploiement. Les dépenses deviennent imputables à la fonctionnalité qui les provoque. Les pannes dégradent au lieu d'interrompre.

Auto-héberger LiteLLM ajoute une heure d'installation et un conteneur à maintenir. En échange, vos prompts et vos complétions ne traversent jamais une infrastructure que vous ne contrôlez pas — ce qui, pour les équipes traitant des données clients sous contrainte de résidence ou de confidentialité, n'est pas une préférence mais une exigence.

Commencez par un seul service. Pointez-le vers la passerelle, observez les journaux pendant une semaine, puis migrez le suivant.