Quand vous construisez un projet basé sur l'IA, une question cruciale se pose rapidement : comment appeler les modèles de langage ? Directement via les APIs officielles (OpenAI, Anthropic, Google) ou via un agrégateur comme OpenRouter ?
Ce choix a des implications profondes sur la fiabilité, les coûts, la flexibilité et la maintenabilité de votre code. Dans cet article, on compare les deux approches en détail, avec du code Python concret et un pattern de fallback production-ready.
🎯 Le problème : trop de fournisseurs, pas assez de standards
Le marché des LLM en 2025, c'est un zoo. Chaque fournisseur a :
- Son propre endpoint (api.openai.com, api.anthropic.com, generativelanguage.googleapis.com...)
- Son propre format de requête et de réponse
- Sa propre gestion des erreurs, rate limits, et authentification
- Ses propres modèles avec des noms et versions différents
Pour un développeur, ça signifie : N intégrations à maintenir, N clés API à gérer, N formats à parser.
🔌 Appels directs : la méthode classique
Comment ça marche
Vous créez un compte chez chaque fournisseur, obtenez une clé API, et appelez directement leur endpoint.
Appel direct à OpenAI
import httpx
import os
async def call_openai(prompt: str, model: str = "gpt-4o") -> str:
"""Appel direct a l'API OpenAI."""
async with httpx.AsyncClient() as client:
response = await client.post(
"https://api.openai.com/v1/chat/completions",
headers={
"Authorization": f"Bearer {os.getenv('OPENAI_API_KEY')}",
"Content-Type": "application/json",
},
json={
"model": model,
"messages": [{"role": "user", "content": prompt}],
"max_tokens": 1000,
"temperature": 0.7,
},
timeout=30.0,
)
response.raise_for_status()
return response.json()["choices"][0]["message"]["content"]
Appel direct à Anthropic
async def call_anthropic(prompt: str, model: str = "claude-sonnet-4-20250514") -> str:
"""Appel direct a l'API Anthropic."""
async with httpx.AsyncClient() as client:
response = await client.post(
"https://api.anthropic.com/v1/messages",
headers={
"x-api-key": os.getenv("ANTHROPIC_API_KEY"),
"anthropic-version": "2023-06-01",
"Content-Type": "application/json",
},
json={
"model": model,
"max_tokens": 1000,
"messages": [{"role": "user", "content": prompt}],
},
timeout=30.0,
)
response.raise_for_status()
return response.json()["content"][0]["text"]
Appel direct à Google (Gemini)
async def call_google(prompt: str, model: str = "gemini-2.0-flash") -> str:
"""Appel direct a l'API Google Gemini."""
api_key = os.getenv("GOOGLE_API_KEY")
async with httpx.AsyncClient() as client:
response = await client.post(
f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={api_key}",
json={
"contents": [{"parts": [{"text": prompt}]}],
"generationConfig": {
"maxOutputTokens": 1000,
"temperature": 0.7,
},
},
timeout=30.0,
)
response.raise_for_status()
return response.json()["candidates"][0]["content"]["parts"][0]["text"]
Vous voyez le problème ? Trois fonctions différentes, trois formats de requête, trois formats de réponse, trois systèmes d'authentification. Et on n'a couvert que 3 fournisseurs sur des dizaines.
Avantages des appels directs
| Avantage | Détail |
|---|---|
| Latence minimale | Pas d'intermédiaire, connexion directe |
| Moins de dépendance | Un seul point de défaillance par fournisseur |
| Accès immédiat | Nouveaux modèles disponibles dès la sortie |
| Pas de surcoût | Prix fournisseur brut, sans marge |
| Contrôle total | Accès à tous les paramètres, même expérimentaux |
Inconvénients des appels directs
| Inconvénient | Détail |
|---|---|
| Maintenance lourde | N intégrations = N fois plus de code à maintenir |
| N clés API | Une clé par fournisseur à gérer et sécuriser |
| N factures | Un compte billing par fournisseur |
| Pas de fallback natif | Si un provider tombe, votre code crashe |
| Pas de vue unifiée | Difficile de comparer les coûts et l'usage |
🌐 OpenRouter : l'agrégateur universel
Comment ça marche
OpenRouter est un proxy API qui expose un endpoint unique compatible OpenAI. Vous envoyez vos requêtes à OpenRouter, qui les route vers le bon fournisseur.
Votre code ---> OpenRouter ---> OpenAI / Anthropic / Google / Mistral / ...
Une seule clé API, un seul format, un seul endpoint.
Appel via OpenRouter
async def call_openrouter(
prompt: str,
model: str = "anthropic/claude-sonnet-4-20250514"
) -> str:
"""Appel via OpenRouter, un endpoint pour tous les modeles."""
async with httpx.AsyncClient() as client:
response = await client.post(
"https://openrouter.ai/api/v1/chat/completions",
headers={
"Authorization": f"Bearer {os.getenv('OPENROUTER_API_KEY')}",
"Content-Type": "application/json",
"HTTP-Referer": "https://monsite.com",
"X-Title": "Mon App IA",
},
json={
"model": model,
"messages": [{"role": "user", "content": prompt}],
"max_tokens": 1000,
"temperature": 0.7,
},
timeout=60.0,
)
response.raise_for_status()
return response.json()["choices"][0]["message"]["content"]
Le même code fonctionne pour TOUS les modèles -- il suffit de changer le paramètre model :
# Claude
result = await call_openrouter(prompt, "anthropic/claude-sonnet-4-20250514")
# GPT-4o
result = await call_openrouter(prompt, "openai/gpt-4o")
# Gemini
result = await call_openrouter(prompt, "google/gemini-2.0-flash")
# Mistral
result = await call_openrouter(prompt, "mistralai/mistral-large")
# Modeles GRATUITS
result = await call_openrouter(prompt, "google/gemini-2.0-flash-exp:free")
Tableau comparatif complet
| Critère | Appels directs | OpenRouter |
|---|---|---|
| Endpoint | 1 par fournisseur | 1 unique |
| Clés API | 1 par fournisseur | 1 seule |
| Format | Variable | Unifié (compatible OpenAI) |
| Latence | Minimale | +50-100ms |
| Fallback | À coder soi-même | Intégré (route auto) |
| Billing | N factures | 1 facture |
| Modèles gratuits | Rares | Oui, plusieurs disponibles |
| Nouveaux modèles | Immédiat | Délai de quelques heures/jours |
| Coût | Prix brut | Prix brut + petite marge (~5-15%) |
| Disponibilité | Directe | Dépend d'OpenRouter |
| Dashboard | N dashboards | 1 unifié |
⚡ Les vrais avantages d'OpenRouter
1. Modèles gratuits
OpenRouter propose des modèles gratuits, parfaits pour le prototypage et les tâches simples :
FREE_MODELS = [
"google/gemini-2.0-flash-exp:free",
"meta-llama/llama-3.1-8b-instruct:free",
"mistralai/mistral-7b-instruct:free",
"huggingfaceh4/zephyr-7b-beta:free",
]
2. Fallback automatique
Si un fournisseur est down, OpenRouter peut automatiquement basculer :
# Avec le parametre "route"
response = await client.post(
"https://openrouter.ai/api/v1/chat/completions",
json={
"model": "anthropic/claude-sonnet-4-20250514",
"route": "fallback", # Bascule auto si le modele principal est down
"messages": [{"role": "user", "content": prompt}],
},
)
3. Billing unifié
Une seule facture, un seul dashboard pour voir :
- Combien vous dépensez par modèle
- Quels modèles sont les plus utilisés
- Votre consommation en temps réel
4. Comparaison facile
Tester un nouveau modèle = changer une string. Pas besoin de créer un compte, obtenir une clé API, intégrer un nouveau format.
⚠️ Les vrais inconvénients d'OpenRouter
Soyons honnêtes, tout n'est pas rose.
Pour aller plus loin sur ce sujet, consultez notre guide VPS + IA : le setup complet pour tout auto-héberger.
1. Latence supplémentaire (+50-100ms)
Pour aller plus loin sur ce sujet, consultez notre guide Docker + IA : conteneuriser ses services intelligents.
Chaque requête passe par un intermédiaire. Pour du chat temps réel, ça peut se sentir. Pour du batch processing, c'est négligeable.
Appel direct: Vous ---- 120ms ----> Anthropic
OpenRouter: Vous -- 50ms --> OpenRouter -- 120ms --> Anthropic
Total: ~170ms (+50ms)
2. Point de défaillance unique
Si OpenRouter tombe, tous vos appels échouent. C'est le risque principal de la centralisation.
3. Disponibilité des modèles
Certains modèles ne sont pas disponibles sur OpenRouter, ou avec un délai après leur sortie. Les modèles très récents ou en preview peuvent manquer.
4. Marge sur les prix
OpenRouter ajoute une marge (généralement 5-15%) par rapport aux prix directs. Sur de gros volumes, ça peut compter.
🏗️ Pattern production : ModelManager avec fallback
Voici le pattern que nous utilisons en production. Il combine le meilleur des deux mondes : OpenRouter en principal, appels directs en fallback.
import httpx
import asyncio
import time
import os
from dataclasses import dataclass, field
from typing import Optional
import logging
logger = logging.getLogger(__name__)
@dataclass
class ModelConfig:
"""Configuration d'un modele avec son provider."""
name: str
provider: str # "openrouter", "openai", "anthropic", "google"
model_id: str
max_tokens: int = 2000
temperature: float = 0.7
cost_per_1k_input: float = 0.0
cost_per_1k_output: float = 0.0
@dataclass
class RateLimitState:
"""Etat du rate limiting pour un provider."""
is_limited: bool = False
retry_after: float = 0.0
limited_at: float = 0.0
consecutive_errors: int = 0
class ModelManager:
"""Gestionnaire de modeles avec fallback chain et rate-limit detection."""
def __init__(self):
self.providers: dict[str, RateLimitState] = {}
self.fallback_chains: dict[str, list[ModelConfig]] = {}
self._setup_default_chains()
def _setup_default_chains(self):
"""Configure les chaines de fallback par defaut."""
# Chaine "smart" : meilleurs modeles
self.fallback_chains["smart"] = [
ModelConfig(
name="Claude Sonnet (OpenRouter)",
provider="openrouter",
model_id="anthropic/claude-sonnet-4-20250514",
cost_per_1k_input=0.003,
cost_per_1k_output=0.015,
),
ModelConfig(
name="Claude Sonnet (Direct)",
provider="anthropic",
model_id="claude-sonnet-4-20250514",
cost_per_1k_input=0.003,
cost_per_1k_output=0.015,
),
ModelConfig(
name="GPT-4o (Direct)",
provider="openai",
model_id="gpt-4o",
cost_per_1k_input=0.005,
cost_per_1k_output=0.015,
),
]
# Chaine "fast" : modeles rapides et economiques
self.fallback_chains["fast"] = [
ModelConfig(
name="Gemini Flash (OpenRouter)",
provider="openrouter",
model_id="google/gemini-2.0-flash",
),
ModelConfig(
name="GPT-4o-mini (Direct)",
provider="openai",
model_id="gpt-4o-mini",
cost_per_1k_input=0.00015,
cost_per_1k_output=0.0006,
),
]
# Chaine "free" : modeles gratuits uniquement
self.fallback_chains["free"] = [
ModelConfig(
name="Gemini Flash Free",
provider="openrouter",
model_id="google/gemini-2.0-flash-exp:free",
),
ModelConfig(
name="Llama 3.1 Free",
provider="openrouter",
model_id="meta-llama/llama-3.1-8b-instruct:free",
),
]
def _is_rate_limited(self, provider: str) -> bool:
"""Verifie si un provider est actuellement rate-limite."""
state = self.providers.get(provider)
if not state or not state.is_limited:
return False
if time.time() > state.limited_at + state.retry_after:
state.is_limited = False
state.consecutive_errors = 0
return False
return True
def _mark_rate_limited(self, provider: str, retry_after: float = 60.0):
"""Marque un provider comme rate-limite."""
if provider not in self.providers:
self.providers[provider] = RateLimitState()
state = self.providers[provider]
state.is_limited = True
state.retry_after = retry_after
state.limited_at = time.time()
state.consecutive_errors += 1
logger.warning(
f"Provider {provider} rate-limite pour {retry_after}s "
f"(erreurs consecutives: {state.consecutive_errors})"
)
async def _call_provider(self, config: ModelConfig, messages: list[dict]) -> str:
"""Appelle un provider specifique."""
if config.provider == "openrouter":
return await self._call_openrouter(config, messages)
elif config.provider == "openai":
return await self._call_openai(config, messages)
elif config.provider == "anthropic":
return await self._call_anthropic(config, messages)
else:
raise ValueError(f"Provider inconnu: {config.provider}")
async def _call_openrouter(self, config: ModelConfig, messages: list[dict]) -> str:
async with httpx.AsyncClient() as client:
resp = await client.post(
"https://openrouter.ai/api/v1/chat/completions",
headers={
"Authorization": f"Bearer {os.getenv('OPENROUTER_API_KEY')}",
"Content-Type": "application/json",
},
json={
"model": config.model_id,
"messages": messages,
"max_tokens": config.max_tokens,
"temperature": config.temperature,
},
timeout=60.0,
)
if resp.status_code == 429:
retry = float(resp.headers.get("retry-after", 60))
self._mark_rate_limited("openrouter", retry)
raise Exception("Rate limited")
resp.raise_for_status()
return resp.json()["choices"][0]["message"]["content"]
async def _call_openai(self, config: ModelConfig, messages: list[dict]) -> str:
async with httpx.AsyncClient() as client:
resp = await client.post(
"https://api.openai.com/v1/chat/completions",
headers={"Authorization": f"Bearer {os.getenv('OPENAI_API_KEY')}"},
json={
"model": config.model_id,
"messages": messages,
"max_tokens": config.max_tokens,
"temperature": config.temperature,
},
timeout=30.0,
)
if resp.status_code == 429:
retry = float(resp.headers.get("retry-after", 60))
self._mark_rate_limited("openai", retry)
raise Exception("Rate limited")
resp.raise_for_status()
return resp.json()["choices"][0]["message"]["content"]
async def _call_anthropic(self, config: ModelConfig, messages: list[dict]) -> str:
async with httpx.AsyncClient() as client:
resp = await client.post(
"https://api.anthropic.com/v1/messages",
headers={
"x-api-key": os.getenv("ANTHROPIC_API_KEY"),
"anthropic-version": "2023-06-01",
},
json={
"model": config.model_id,
"max_tokens": config.max_tokens,
"messages": messages,
},
timeout=30.0,
)
if resp.status_code == 429:
retry = float(resp.headers.get("retry-after", 60))
self._mark_rate_limited("anthropic", retry)
raise Exception("Rate limited")
resp.raise_for_status()
return resp.json()["content"][0]["text"]
async def complete(self, messages: list[dict], chain: str = "smart") -> str:
"""Appelle le meilleur modele disponible dans la chaine de fallback."""
models = self.fallback_chains.get(chain, self.fallback_chains["smart"])
last_error = None
for config in models:
if self._is_rate_limited(config.provider):
logger.info(f"Skipping {config.name} (rate-limite)")
continue
try:
logger.info(f"Tentative avec {config.name}...")
result = await self._call_provider(config, messages)
logger.info(f"Succes avec {config.name}")
return result
except Exception as e:
last_error = e
logger.warning(f"Echec avec {config.name}: {e}")
continue
raise Exception(
f"Tous les modeles de la chaine '{chain}' ont echoue. "
f"Derniere erreur: {last_error}"
)
# Utilisation
async def main():
manager = ModelManager()
# Utilisation simple
result = await manager.complete(
messages=[{"role": "user", "content": "Explique le RAG en 3 phrases."}],
chain="smart",
)
print(result)
if __name__ == "__main__":
asyncio.run(main())
Comment ça marche ?
- Chaînes de fallback : chaque chaîne (smart, fast, free) définit une liste ordonnée de modèles
- Rate-limit detection : quand un provider renvoie 429, il est marqué comme limité pour la durée indiquée
- Fallback automatique : si un modèle échoue ou est rate-limité, on passe au suivant
- Providers mixtes : une même chaîne peut mélanger OpenRouter et appels directs
📊 Quand utiliser quoi ?
| Situation | Recommandation | Pourquoi |
|---|---|---|
| Prototypage / side project | OpenRouter seul | Un seul compte, modèles gratuits, rapide à setup |
| Production avec 1 modèle | Appel direct | Moins de latence, moins de dépendance |
| Production multi-modèles | OpenRouter + fallback direct | Meilleur des deux mondes |
| Gros volumes (>100k req/jour) | Appels directs | Économie sur la marge OpenRouter |
| Budget serré | OpenRouter modèles gratuits | Zéro coût pour les modèles free-tier |
| Tests / comparaison modèles | OpenRouter | Changer de modèle = changer une string |
🔧 Configuration dans OpenClaw
Si vous utilisez OpenClaw sur votre VPS, la configuration des providers est simple. OpenClaw utilise nativement OpenRouter :
# Dans votre config OpenClaw
providers:
openrouter:
api_key: ${OPENROUTER_API_KEY}
default_model: anthropic/claude-sonnet-4-20250514
# Fallback direct (optionnel)
anthropic:
api_key: ${ANTHROPIC_API_KEY}
Pour en savoir plus, consultez notre guide Configurer OpenClaw.
🎯 Notre recommandation
Pour la plupart des projets self-hosted :
- Commencez avec OpenRouter -- c'est le plus simple et le plus flexible
- Ajoutez des appels directs en fallback pour vos modèles critiques
- Utilisez le pattern ModelManager pour gérer automatiquement les pannes
- Surveillez vos coûts via le dashboard OpenRouter et ajustez
Le code est dans l'article, le pattern est testé en production. À vous de jouer.
📚 Articles liés
- OpenRouter -- L'agrégateur d'APIs IA que nous recommandons
- Claude Anthropic -- Le modèle IA derrière de nombreux projets
- Qu'est-ce qu'OpenClaw ? -- L'agent IA qui utilise ces APIs
- VPS + IA : le setup complet -- Installer votre serveur pour héberger tout ça
- Configurer OpenClaw -- Brancher les providers dans OpenClaw
- Automatiser sa vie avec l'IA -- Ce que vous ferez avec ces APIs