En mars 2026, TechDocs SaaS — une plateforme de génération automatique de documentation technique pour développeurs — faisait face à une facture OpenAI de €4200/mois en croissance constante. Avec 2500 utilisateurs actifs et 85M tokens traités mensuellement, les coûts API représentaient désormais 28% du chiffre d'affaires, menaçant directement la rentabilité de l'entreprise.
Cette étude de cas documente la migration complète vers Ollama + Llama 3.3 70B, réalisée en 6 jours par un tech lead senior. Résultat final : €109/mois (-97%), latence améliorée de 44%, qualité maintenue à 97% de l'originale. Nous partageons l'architecture complète, les pièges rencontrés, les benchmarks réels, et le code Python production-ready.
Contexte : Pourquoi Migrer d'OpenAI ?
Profil de l'Entreprise
- Produit : SaaS B2B de génération de documentation technique à partir de code source
- Stack initial : Python FastAPI backend, OpenAI GPT-4 Turbo pour génération, PostgreSQL
- Utilisateurs : 2500 développeurs actifs, 450 équipes payantes
- Volume IA : 85M tokens/mois (60M input + 25M output), 180k requêtes/mois
- Use cases IA : Génération docstrings, explications de fonctions complexes, résumés de PRs, traduction documentation EN→FR/DE/ES
Problèmes Rencontrés avec OpenAI API
| Problème | Impact Mensuel | Criticité |
|---|
| Coût API explosif | €4200/mois, +35% sur 6 mois | 🔴 Critique |
| Latence réseau variable | p95 = 5.8s (serveurs EU→US) | 🟡 Moyen |
| Rate limits | 12-18 incidents/mois aux heures de pointe | 🟡 Moyen |
| Conformité RGPD | Code propriétaire clients envoyé à OpenAI US | 🟠 Important |
| Dépendance vendor | Changements de prix unilatéraux (+20% jan 2026) | 🟠 Important |
Point de rupture : En février 2026, OpenAI annonce une hausse tarifaire de 15% effective avril 2026. Projection : €4830/mois, soit €58k/an. L'équipe décide d'évaluer sérieusement les alternatives open-source.
Phase 1 : Évaluation et Sélection de Modèle
Critères de Décision
| Critère | Seuil Minimum | Poids |
|---|
| Qualité output | ≥85% de GPT-4 (éval humaine) | 40% |
| Coût total mensuel | ≤€500/mois (infra + ops) | 30% |
| Latence p95 | ≤4s (amélioration vs 5.8s actuel) | 20% |
| Facilité migration | ≤10 jours dev, API compatible | 10% |
Modèles Évalués (POC 1 Semaine)
# Script d'évaluation comparative (eval_models.py)
import ollama
from openai import OpenAI
import time
import json
# Dataset test : 100 exemples réels anonymisés
test_cases = json.load(open("test_dataset.json"))
def evaluate_model(model_name, api_type="ollama"):
"""Évalue un modèle sur qualité, latence, coût."""
results = {
"model": model_name,
"quality_scores": [],
"latencies": [],
"errors": 0
}
for i, test in enumerate(test_cases[:100]):
start = time.time()
try:
if api_type == "ollama":
response = ollama.chat(
model=model_name,
messages=[
{"role": "system", "content": test["system_prompt"]},
{"role": "user", "content": test["user_prompt"]}
]
)
output = response['message']['content']
cost = 0 # Ollama = gratuit
elif api_type == "openai":
client = OpenAI(api_key="sk-...")
response = client.chat.completions.create(
model=model_name,
messages=[
{"role": "system", "content": test["system_prompt"]},
{"role": "user", "content": test["user_prompt"]}
]
)
output = response.choices[0].message.content
cost = response.usage.total_tokens * 0.00006 # GPT-4 Turbo
latency = time.time() - start
# Évaluation qualité (scoring humain de référence)
quality_score = calculate_quality(output, test["reference_output"])
results["quality_scores"].append(quality_score)
results["latencies"].append(latency)
except Exception as e:
results["errors"] += 1
print(f"Error on test {i}: {e}")
# Métriques agrégées
avg_quality = sum(results["quality_scores"]) / len(results["quality_scores"])
p50_latency = sorted(results["latencies"])[len(results["latencies"]) // 2]
p95_latency = sorted(results["latencies"])[int(len(results["latencies"]) * 0.95)]
return {
"model": model_name,
"avg_quality": f"{avg_quality:.1%}",
"p50_latency": f"{p50_latency:.2f}s",
"p95_latency": f"{p95_latency:.2f}s",
"error_rate": f"{results['errors']}%"
}
# Évaluation sur 5 modèles
models_to_test = [
("gpt-4-turbo", "openai"),
("llama3.3:70b", "ollama"),
("llama3.3:8b", "ollama"),
("mistral:7b", "ollama"),
("qwen2.5:72b", "ollama")
]
print("🔬 Évaluation comparative des modèles...")
for model, api in models_to_test:
result = evaluate_model(model, api)
print(f"{model}: {result}")
# Résultats réels obtenus :
# gpt-4-turbo: {'avg_quality': '92.0%', 'p50_latency': '3.2s', 'p95_latency': '5.8s', 'error_rate': '0%'}
# llama3.3:70b: {'avg_quality': '89.0%', 'p50_latency': '1.8s', 'p95_latency': '3.1s', 'error_rate': '1%'}
# llama3.3:8b: {'avg_quality': '78.0%', 'p50_latency': '0.6s', 'p95_latency': '1.2s', 'error_rate': '3%'}
# mistral:7b: {'avg_quality': '74.0%', 'p50_latency': '0.7s', 'p95_latency': '1.4s', 'error_rate': '2%'}
# qwen2.5:72b: {'avg_quality': '87.0%', 'p50_latency': '2.1s', 'p95_latency': '3.6s', 'error_rate': '1%'}
Décision : Llama 3.3 70B (Quantization Q8)
Justification :
- Qualité : 89% vs 92% GPT-4 = écart de 3% acceptable pour économies de 97%
- Latence : 1.8s p50 vs 3.2s GPT-4 = amélioration de 44%
- Coût : €109/mois (serveur Hetzner AX102) vs €4200/mois OpenAI
- RAM GPU : Q8 = 70GB VRAM (tient sur 2× RTX 4090 48GB)
- Compatibilité : API OpenAI-compatible, migration code minimale
Phase 2 : Architecture Infrastructure
Choix du Serveur GPU
| Option | Specs | Coût/mois | Avantages | Inconvénients |
|---|
| Hetzner AX102 (choisi) | 2× RTX 4090, 128GB RAM, 2TB NVMe | €109 | Prix imbattable, 48GB VRAM total | Disponibilité limitée, EU uniquement |
| GCP g2-standard-48 | 4× NVIDIA L4, 192GB RAM | €720 | Scalabilité cloud, SLA 99.95% | 7× plus cher, latence réseau |
| AWS p4d.24xlarge | 8× A100 40GB, 1.1TB RAM | €28,800 (spot: €8640) | Performance maximale | Overkill, coût prohibitif |
| OVHcloud GPU T1-180 | 3× RTX 3090 Ti, 128GB RAM | €180 | Alternative européenne décente | GPU moins performant que 4090 |
Décision finale : Hetzner AX102. Économies annuelles : €49,092 (€4200 - €109) × 12. Amortissement hardware si achat : RTX 4090 × 2 = €3000, amorti en 0.7 mois.
Architecture Docker Production
# docker-compose.production.yml
version: '3.8'
services:
# Ollama : serveur de modèles avec GPU
ollama:
image: ollama/ollama:latest
container_name: ollama-prod
volumes:
- ollama_models:/root/.ollama
- ./ollama-logs:/var/log/ollama
ports:
- "11434:11434"
environment:
- OLLAMA_HOST=0.0.0.0
- OLLAMA_KEEP_ALIVE=24h # Garde modèle en VRAM
- OLLAMA_NUM_PARALLEL=4 # Max 4 requêtes concurrentes
- OLLAMA_MAX_LOADED_MODELS=2
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:11434/api/tags"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
restart: unless-stopped
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# NGINX : reverse proxy + load balancing
nginx:
image: nginx:alpine
container_name: nginx-proxy
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
- nginx_logs:/var/log/nginx
depends_on:
ollama:
condition: service_healthy
restart: unless-stopped
# Prometheus : monitoring métriques
prometheus:
image: prom/prometheus:latest
container_name: prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--storage.tsdb.retention.time=30d'
restart: unless-stopped
# Grafana : dashboards
grafana:
image: grafana/grafana:latest
container_name: grafana
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
- GF_INSTALL_PLUGINS=grafana-piechart-panel
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/dashboards:/etc/grafana/provisioning/dashboards:ro
- ./grafana/datasources:/etc/grafana/provisioning/datasources:ro
depends_on:
- prometheus
restart: unless-stopped
# Node Exporter : métriques système
node-exporter:
image: prom/node-exporter:latest
container_name: node-exporter
ports:
- "9100:9100"
command:
- '--path.rootfs=/host'
volumes:
- '/:/host:ro,rslave'
restart: unless-stopped
# NVIDIA DCGM Exporter : métriques GPU
dcgm-exporter:
image: nvcr.io/nvidia/k8s/dcgm-exporter:3.1.3-3.1.4-ubuntu20.04
container_name: dcgm-exporter
ports:
- "9400:9400"
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
restart: unless-stopped
volumes:
ollama_models:
prometheus_data:
grafana_data:
nginx_logs:
ollama_logs:
# nginx.conf : configuration reverse proxy
events {
worker_connections 2048;
}
http {
upstream ollama_backend {
least_conn; # Load balancing intelligent
server ollama:11434 max_fails=3 fail_timeout=30s;
keepalive 32;
}
# Rate limiting : 100 req/min par IP
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/m;
server {
listen 80;
server_name api.techdocs.example.com;
# Redirect HTTP → HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name api.techdocs.example.com;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# Logs
access_log /var/log/nginx/ollama-access.log;
error_log /var/log/nginx/ollama-error.log;
location /v1/chat/completions {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://ollama_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Timeouts élevés pour LLM
proxy_connect_timeout 90s;
proxy_send_timeout 180s;
proxy_read_timeout 180s;
# Streaming support
proxy_buffering off;
proxy_cache off;
proxy_http_version 1.1;
proxy_set_header Connection "";
# CORS
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
if ($request_method = 'OPTIONS') {
return 204;
}
}
location /health {
access_log off;
return 200 "OK\n";
add_header Content-Type text/plain;
}
}
}
Déploiement et Initialisation
#!/bin/bash
# deploy-ollama.sh : script de déploiement complet
set -e # Exit on error
echo "🚀 Déploiement Ollama Production..."
# 1. Vérifier prérequis
echo "✓ Vérification GPU..."
nvidia-smi > /dev/null || { echo "❌ GPU NVIDIA non détecté"; exit 1; }
echo "✓ Vérification Docker..."
docker --version > /dev/null || { echo "❌ Docker non installé"; exit 1; }
echo "✓ Vérification nvidia-docker..."
docker run --rm --gpus all nvidia/cuda:12.0-base nvidia-smi > /dev/null || {
echo "❌ nvidia-docker non configuré";
exit 1;
}
# 2. Créer répertoires
mkdir -p ssl grafana/dashboards grafana/datasources ollama-logs
# 3. Générer certificats SSL (Let's Encrypt)
if [ ! -f "ssl/fullchain.pem" ]; then
echo "📜 Génération certificats SSL..."
docker run --rm -v $(pwd)/ssl:/etc/letsencrypt certbot/certbot certonly --standalone -d api.techdocs.example.com --agree-tos -m admin@techdocs.example.com
cp /etc/letsencrypt/live/api.techdocs.example.com/fullchain.pem ssl/
cp /etc/letsencrypt/live/api.techdocs.example.com/privkey.pem ssl/
fi
# 4. Démarrer services
echo "🐳 Démarrage containers..."
docker-compose -f docker-compose.production.yml up -d
# 5. Attendre Ollama ready
echo "⏳ Attente Ollama (60s)..."
sleep 60
# 6. Télécharger modèle Llama 3.3 70B Q8
echo "📥 Téléchargement Llama 3.3 70B Q8 (~70GB, peut prendre 30-60min)..."
docker exec ollama-prod ollama pull llama3.3:70b-q8_0
# 7. Preload modèle en VRAM (évite cold start)
echo "🔥 Preload modèle en VRAM..."
docker exec ollama-prod ollama run llama3.3:70b-q8_0 "Test" > /dev/null
# 8. Vérifier santé
echo "🏥 Healthcheck..."
curl -f http://localhost:11434/api/tags || { echo "❌ Ollama non accessible"; exit 1; }
# 9. Configurer monitoring
echo "📊 Configuration Grafana..."
# Import dashboard GPU (ID 12239)
curl -X POST http://admin:${GRAFANA_PASSWORD}@localhost:3000/api/dashboards/import -H "Content-Type: application/json" -d '{"dashboard": {"id": 12239}, "overwrite": true, "inputs": [{"name": "DS_PROMETHEUS", "type": "datasource", "pluginId": "prometheus", "value": "Prometheus"}]}'
echo "✅ Déploiement terminé !"
echo "📍 Ollama API : https://api.techdocs.example.com/v1/chat/completions"
echo "📍 Grafana : http://localhost:3000 (admin / ${GRAFANA_PASSWORD})"
echo "📍 Prometheus : http://localhost:9090"
# 10. Afficher métriques GPU
echo ""
echo "📊 État GPU actuel :"
nvidia-smi --query-gpu=index,name,memory.used,memory.total,utilization.gpu,temperature.gpu --format=csv
Phase 3 : Migration Code (Couche de Compatibilité)
Wrapper Python avec Fallback Automatique
# llm_client.py : abstraction LLM avec fallback OpenAI
import os
import time
import logging
from typing import Optional, Dict, List
from enum import Enum
import ollama
from openai import OpenAI
logger = logging.getLogger(__name__)
class LLMProvider(Enum):
OLLAMA = "ollama"
OPENAI = "openai"
class LLMClient:
"""
Client LLM unifié avec fallback automatique.
Stratégie :
1. Essaye Ollama d'abord (coût = 0)
2. Si erreur ou qualité < seuil : fallback OpenAI
3. Track métriques (coût, latence, provider utilisé)
"""
def __init__(
self,
ollama_model: str = "llama3.3:70b-q8_0",
ollama_base_url: str = "https://api.techdocs.example.com",
openai_model: str = "gpt-4-turbo",
openai_api_key: Optional[str] = None,
enable_fallback: bool = True,
confidence_threshold: float = 0.75
):
self.ollama_model = ollama_model
self.openai_model = openai_model
self.enable_fallback = enable_fallback
self.confidence_threshold = confidence_threshold
# Clients
self.ollama_client = ollama.Client(host=ollama_base_url)
self.openai_client = OpenAI(api_key=openai_api_key or os.getenv("OPENAI_API_KEY"))
# Métriques
self.stats = {
"ollama_success": 0,
"ollama_fallback": 0,
"openai_only": 0,
"total_cost": 0.0,
"total_latency": 0.0,
"total_requests": 0
}
def chat(
self,
messages: List[Dict[str, str]],
temperature: float = 0.7,
max_tokens: int = 2000,
force_provider: Optional[LLMProvider] = None
) -> Dict:
"""
Génère completion avec stratégie fallback.
Returns:
{
'content': str,
'provider': 'ollama' | 'openai',
'latency': float,
'cost': float,
'confidence': float
}
"""
self.stats["total_requests"] += 1
start = time.time()
# Force provider si spécifié
if force_provider == LLMProvider.OPENAI:
return self._call_openai(messages, temperature, max_tokens, start)
# Tentative 1 : Ollama
try:
response = self._call_ollama(messages, temperature, max_tokens, start)
# Vérifier confiance
if response['confidence'] >= self.confidence_threshold:
self.stats["ollama_success"] += 1
logger.info(f"✅ Ollama success (confidence: {response['confidence']:.2f})")
return response
# Confiance faible : fallback si activé
if not self.enable_fallback:
logger.warning(f"⚠️ Low confidence {response['confidence']:.2f} but fallback disabled")
return response
logger.warning(f"⚠️ Ollama confidence {response['confidence']:.2f} < {self.confidence_threshold}, fallback OpenAI")
self.stats["ollama_fallback"] += 1
except Exception as e:
logger.error(f"❌ Ollama error: {e}")
if not self.enable_fallback:
raise
self.stats["ollama_fallback"] += 1
# Tentative 2 : OpenAI
return self._call_openai(messages, temperature, max_tokens, start)
def _call_ollama(self, messages, temperature, max_tokens, start) -> Dict:
"""Appel Ollama."""
response = self.ollama_client.chat(
model=self.ollama_model,
messages=messages,
options={
"temperature": temperature,
"num_predict": max_tokens
}
)
content = response['message']['content']
latency = time.time() - start
# Estimer confiance (heuristique simple)
confidence = self._estimate_confidence(content, messages)
self.stats["total_latency"] += latency
return {
'content': content,
'provider': 'ollama',
'latency': latency,
'cost': 0.0,
'confidence': confidence,
'model': self.ollama_model
}
def _call_openai(self, messages, temperature, max_tokens, start) -> Dict:
"""Appel OpenAI."""
response = self.openai_client.chat.completions.create(
model=self.openai_model,
messages=messages,
temperature=temperature,
max_tokens=max_tokens
)
content = response.choices[0].message.content
latency = time.time() - start
# Calculer coût
input_tokens = response.usage.prompt_tokens
output_tokens = response.usage.completion_tokens
cost = (input_tokens * 0.00001) + (output_tokens * 0.00003) # GPT-4 Turbo pricing
self.stats["total_cost"] += cost
self.stats["total_latency"] += latency
self.stats["openai_only"] += 1
return {
'content': content,
'provider': 'openai',
'latency': latency,
'cost': cost,
'confidence': 1.0,
'model': self.openai_model
}
def _estimate_confidence(self, content: str, messages: List[Dict]) -> float:
"""
Estime confiance de la réponse (heuristique).
En prod : utiliser modèle de scoring ou feedback utilisateur.
"""
# Heuristiques basiques
if len(content) < 30:
return 0.4 # Trop courte
if any(phrase in content.lower() for phrase in [
"i don't know", "je ne sais pas", "i'm not sure", "je ne suis pas sûr",
"i cannot", "je ne peux pas"
]):
return 0.5 # Incertaine
# Si prompt demande du code et réponse contient code fence
if "```" in messages[-1]['content'] or "code" in messages[-1]['content'].lower():
if "```" in content:
return 0.95 # Code présent = bon signe
else:
return 0.65 # Code attendu mais absent
# Par défaut : confiance élevée
return 0.9
def get_stats(self) -> Dict:
"""Retourne métriques d'utilisation."""
total = self.stats["total_requests"]
if total == 0:
return self.stats
ollama_rate = self.stats["ollama_success"] / total
fallback_rate = self.stats["ollama_fallback"] / total
avg_latency = self.stats["total_latency"] / total
# Estimation économies
avg_cost_if_all_openai = total * 0.04 # ~4 cents/req moyenne
actual_cost = self.stats["total_cost"]
savings = avg_cost_if_all_openai - actual_cost
return {
**self.stats,
"ollama_rate": f"{ollama_rate:.1%}",
"fallback_rate": f"{fallback_rate:.1%}",
"avg_latency": f"{avg_latency:.2f}s",
"estimated_savings": f"€{savings:.2f}",
"cost_reduction": f"{(savings / avg_cost_if_all_openai * 100):.1f}%" if avg_cost_if_all_openai > 0 else "N/A"
}
# Exemple d'utilisation
if __name__ == "__main__":
client = LLMClient(
ollama_base_url="https://api.techdocs.example.com",
enable_fallback=True,
confidence_threshold=0.75
)
# Test 1 : Génération docstring (cas d'usage typique)
response1 = client.chat(
messages=[
{
"role": "system",
"content": "You are a technical documentation expert. Generate clear, concise docstrings."
},
{
"role": "user",
"content": """Generate a Python docstring for this function:
def calculate_similarity(text1: str, text2: str, method: str = "cosine") -> float:
embeddings1 = get_embeddings(text1)
embeddings2 = get_embeddings(text2)
if method == "cosine":
return cosine_similarity(embeddings1, embeddings2)
elif method == "euclidean":
return euclidean_distance(embeddings1, embeddings2)
else:
raise ValueError(f"Unknown method: {method}")"""
}
],
temperature=0.3
)
print(f"Response: {response1['content'][:200]}...")
print(f"Provider: {response1['provider']}, Latency: {response1['latency']:.2f}s, Cost: €{response1['cost']:.4f}")
# Afficher stats après 100 requêtes
# ... (simulate 99 more requests)
print("
📊 Stats après 100 requêtes :")
print(client.get_stats())
Résultat attendu : Sur 100 requêtes réelles, taux de succès Ollama = 87%, fallback OpenAI = 13%, coût moyen = €0.0052/req (vs €0.042/req full OpenAI) = -88% de coûts.
Phase 4 : Rollout Progressif avec A/B Testing
Stratégie de Déploiement (4 Semaines)
| Semaine | % Trafic Ollama | Critères Validation | Actions |
|---|
| S1 | 10% | Taux erreur <2%, latence p95 <4s, qualité ≥85% | Monitoring intensif, collecte feedback utilisateurs internes |
| S2 | 30% | Taux erreur <1.5%, NPS stable (≥4.3/5) | A/B test blind (utilisateurs ne savent pas quel modèle) |
| S3 | 60% | Économies mesurées ≥€2500/mois, fallback <15% | Ajustement seuil confiance, optimisation prompts |
| S4 | 90% | CSAT ≥4.4/5, coût stabilisé <€150/mois | Migration complète, OpenAI = fallback uniquement |
Code Feature Flag (Rollout Progressif)
# feature_flags.py : contrôle rollout progressif
import random
import hashlib
from typing import Optional
class FeatureFlags:
"""Gestion feature flags pour rollout progressif."""
def __init__(self, rollout_percentage: int = 0):
"""
Args:
rollout_percentage: % utilisateurs routés vers Ollama (0-100)
"""
self.rollout_percentage = rollout_percentage
def should_use_ollama(self, user_id: str, override: Optional[bool] = None) -> bool:
"""
Détermine si requête doit utiliser Ollama.
Args:
user_id: ID utilisateur (pour distribution cohérente)
override: Force Ollama (True) ou OpenAI (False)
Returns:
True si Ollama, False si OpenAI
"""
# Override manuel
if override is not None:
return override
# Rollout progressif basé sur hash user_id (distribution uniforme)
user_hash = int(hashlib.md5(user_id.encode()).hexdigest(), 16)
user_bucket = user_hash % 100 # 0-99
return user_bucket < self.rollout_percentage
# Intégration dans API FastAPI
from fastapi import FastAPI, HTTPException, Header
from pydantic import BaseModel
import os
app = FastAPI()
# Configuration rollout (variable d'environnement)
OLLAMA_ROLLOUT_PERCENTAGE = int(os.getenv("OLLAMA_ROLLOUT_PERCENTAGE", "0"))
feature_flags = FeatureFlags(rollout_percentage=OLLAMA_ROLLOUT_PERCENTAGE)
llm_client = LLMClient(
ollama_base_url="https://api.techdocs.example.com",
enable_fallback=True
)
class ChatRequest(BaseModel):
messages: list
temperature: float = 0.7
max_tokens: int = 2000
class ChatResponse(BaseModel):
content: str
provider: str
latency: float
cost: float
@app.post("/v1/chat/completions", response_model=ChatResponse)
async def chat_completions(
request: ChatRequest,
user_id: str = Header(..., alias="X-User-ID"),
force_provider: Optional[str] = Header(None, alias="X-Force-Provider")
):
"""
Endpoint compatible OpenAI avec rollout progressif Ollama.
Headers:
X-User-ID: ID utilisateur (requis)
X-Force-Provider: "ollama" ou "openai" (optionnel, pour tests)
"""
# Déterminer provider
if force_provider:
use_ollama = (force_provider.lower() == "ollama")
else:
use_ollama = feature_flags.should_use_ollama(user_id)
# Appel LLM
try:
if use_ollama:
response = llm_client.chat(
messages=request.messages,
temperature=request.temperature,
max_tokens=request.max_tokens
)
else:
response = llm_client.chat(
messages=request.messages,
temperature=request.temperature,
max_tokens=request.max_tokens,
force_provider=LLMProvider.OPENAI
)
return ChatResponse(**response)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/stats")
async def get_stats():
"""Endpoint métriques LLM."""
return llm_client.get_stats()
@app.get("/health")
async def health_check():
"""Healthcheck."""
return {"status": "ok", "rollout_percentage": OLLAMA_ROLLOUT_PERCENTAGE}
# Déploiement avec rollout progressif
# Semaine 1: OLLAMA_ROLLOUT_PERCENTAGE=10
# Semaine 2: OLLAMA_ROLLOUT_PERCENTAGE=30
# Semaine 3: OLLAMA_ROLLOUT_PERCENTAGE=60
# Semaine 4: OLLAMA_ROLLOUT_PERCENTAGE=90
Résultats Mesurés : Avant/Après (6 Mois)
| Métrique | Avant (OpenAI) | Après (Ollama) | Variation |
|---|
| Coût mensuel | €4200 | €109 (Hetzner) + €43 (OpenAI fallback 13%) | €152 total (-96.4%) ✅ |
| Latence p50 | 3.2s | 1.8s | -44% ✅ |
| Latence p95 | 5.8s | 3.1s | -47% ✅ |
| Latence p99 | 12.4s (rate limits) | 4.2s | -66% ✅ |
| Qualité (éval humaine) | 92% | 89% | -3% ⚠️ |
| NPS utilisateurs | 4.3/5 | 4.5/5 | +0.2 ✅ |
| Taux erreur | 0.3% | 0.8% | +0.5% ⚠️ |
| Incidents rate limit | 14/mois | 0 | -100% ✅ |
| Disponibilité | 99.7% (SLA OpenAI) | 99.92% (self-hosted) | +0.22% ✅ |
| Taux fallback OpenAI | — | 13% | 87% requêtes sur Ollama ✅ |
ROI Financier
# Calcul ROI migration
## Coûts
- Migration (6 jours tech lead @ €800/j) : €4800
- Serveur Hetzner AX102 : €109/mois
- Fallback OpenAI (13% trafic) : ~€43/mois
- **Coût total mensuel** : €152
## Économies
- Avant : €4200/mois
- Après : €152/mois
- **Économies mensuelles** : €4048
- **Économies annuelles** : €48,576
## ROI
- Investissement initial : €4800
- Payback : 4800 / 4048 = **1.2 mois**
- Gains nets année 1 : €48,576 - €4800 = **€43,776**
- Gains nets année 2+ : **€48,576/an**
## Amortissement hardware (alternative achat serveur)
- 2× RTX 4090 : €3000
- Serveur barebones : €1500
- Total hardware : €4500
- Amortissement : 4500 / 4048 = **1.1 mois**
- Après amortissement : coût = €0/mois (électricité ~€30/mois)
Conclusion financière : Migration rentabilisée en 6 semaines. Sur 3 ans : économies totales de €145,728.
Monitoring Production : Dashboards Grafana
Métriques Clés Suivies
# prometheus.yml : configuration scraping
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
# Métriques GPU NVIDIA
- job_name: 'gpu'
static_configs:
- targets: ['dcgm-exporter:9400']
metric_relabel_configs:
- source_labels: [__name__]
regex: 'DCGM.*'
action: keep
# Métriques système (CPU, RAM, disque)
- job_name: 'node'
static_configs:
- targets: ['node-exporter:9100']
# Métriques applicatives (custom)
- job_name: 'ollama-api'
static_configs:
- targets: ['fastapi-app:8000']
metrics_path: '/metrics'
# Alertes critiques
rule_files:
- 'alerts.yml'
# alerts.yml
groups:
- name: ollama_alerts
rules:
# GPU temperature > 85°C
- alert: GPUOverheating
expr: DCGM_FI_DEV_GPU_TEMP > 85
for: 5m
labels:
severity: critical
annotations:
summary: "GPU {{ $labels.gpu }} overheating ({{ $value }}°C)"
description: "Temperature above 85°C for 5 minutes"
# VRAM utilization > 95%
- alert: VRAMSaturation
expr: (DCGM_FI_DEV_FB_USED / DCGM_FI_DEV_FB_FREE) > 0.95
for: 2m
labels:
severity: warning
annotations:
summary: "VRAM near saturation on GPU {{ $labels.gpu }}"
# Latence p95 > 5s
- alert: HighLatency
expr: histogram_quantile(0.95, rate(llm_request_duration_seconds_bucket[5m])) > 5
for: 10m
labels:
severity: warning
annotations:
summary: "High latency p95 > 5s"
# Taux erreur > 2%
- alert: HighErrorRate
expr: rate(llm_errors_total[5m]) / rate(llm_requests_total[5m]) > 0.02
for: 5m
labels:
severity: critical
annotations:
summary: "Error rate > 2%"
# Taux fallback OpenAI > 20%
- alert: HighFallbackRate
expr: rate(llm_fallback_total[1h]) / rate(llm_requests_total[1h]) > 0.20
for: 30m
labels:
severity: warning
annotations:
summary: "Fallback rate > 20% (quality issues?)"
description: "Check Ollama model quality or increase confidence threshold"
Dashboard Grafana : Métriques LLM
Panels principaux :
- Request Rate : req/s par provider (Ollama vs OpenAI)
- Latency Distribution : p50/p95/p99 par provider
- Cost Tracking : coût cumulé mensuel (€ économisés vs OpenAI full)
- Fallback Rate : % requêtes tombées en fallback OpenAI
- GPU Utilization : % GPU, VRAM utilisée, température
- Quality Proxy : taux de retry utilisateurs, feedback négatif
Pièges Rencontrés et Solutions
| Piège | Impact | Solution Appliquée |
|---|
| Cold start 40s première requête | Timeout clients, mauvaise UX | Preload modèle au démarrage (OLLAMA_KEEP_ALIVE=24h) |
| GPU throttling après 2h charge | Latence ×2, temp 92°C | Amélioration refroidissement (ventilation forcée), limiter à 3 req/s |
| VRAM OOM avec 4+ requêtes concurrentes | Crashes Ollama | OLLAMA_NUM_PARALLEL=3 (max 3 requêtes simultanées) |
| Qualité inférieure sur code TypeScript | Feedback négatif +15% | Fallback OpenAI automatique si langage=TypeScript (heuristique) |
| Latence réseau serveur EU→Hetzner DE | +200ms RTT depuis France | Acceptable (toujours -44% vs OpenAI US), sinon : Cloudflare Argo Tunnel |
| Modèle Q4 trop dégradé pour certains cas | Qualité 81% vs 92% GPT-4 | Upgrade vers Q8 (70GB VRAM mais qualité 89%) |
Recommandations pour Reproduire Cette Migration
Checklist Pré-Migration (Phase 0)
- ✅ Auditer volume actuel : tokens/mois, requêtes/mois, coût mensuel exact
- ✅ Identifier cas d'usage : classer par criticité (critique → Ollama difficile, non-critique → Ollama parfait)
- ✅ Évaluer 3-5 modèles open-source : POC 1 semaine sur dataset réel anonymisé (100-200 exemples)
- ✅ Calculer ROI précis : coût infra GPU, temps dev migration, économies projetées, payback
- ✅ Préparer rollback plan : en cas d'échec, retour OpenAI en <5min (feature flag)
- ✅ Définir métriques succès : seuils acceptables qualité, latence, coût, NPS
Étapes Migration (6 Jours Tech Lead)
| Jour | Tâches | Livrables |
|---|
| J1 | Setup serveur GPU, Docker Compose, téléchargement modèle | Ollama opérationnel, modèle chargé, healthcheck OK |
| J2 | Wrapper LLM Python, tests unitaires, API compatible OpenAI | Code LLMClient prêt, tests passent (coverage >80%) |
| J3 | Feature flags, rollout progressif, monitoring Prometheus | Déploiement staging, 10% trafic routé Ollama |
| J4 | A/B testing, évaluation qualité, ajustement seuil confiance | Rapport qualité (89%), décision go/no-go pour 30% |
| J5 | Montée en charge 30→60%, optimisations GPU (throttling) | 60% trafic Ollama stable, latence <4s p95 |
| J6 | Montée 90%, dashboards Grafana, alertes, doc post-mortem | Migration prod complète, runbook ops, rapport final |
Quand Ne PAS Migrer vers Ollama
Scénarios où OpenAI/Claude reste préférable :
- ❌ Tâches ultra-créatives : génération marketing, storytelling, brainstorming → GPT-4/Claude Opus meilleurs
- ❌ Volume <50k tokens/mois : coût API <€50/mois, ROI migration négatif
- ❌ Zero tolérance erreur : domaine médical, légal, financier critique → certifications API propriétaires
- ❌ Équipe <2 devs : pas de bande passante pour ops GPU, monitoring, debugging
- ❌ Besoin multimodalité avancée : vision + texte (GPT-4V), audio (Whisper) → stack Ollama limitée
Ressources Complémentaires
Pour approfondir le déploiement Ollama en production et maîtriser les architectures LLM auto-hébergées, consultez nos ressources :
Questions Fréquentes
La qualité de Llama 3.3 70B est-elle vraiment comparable à GPT-4 ?
Pour 80-85% des cas d'usage production, oui. Llama 3.3 70B atteint 90-93% de la qualité GPT-4 Turbo sur tâches standardisées (support client, résumés, extraction de données). Dans notre cas réel, l'évaluation humaine a mesuré 89% de qualité vs 92% pour GPT-4, avec un NPS utilisateurs identique (4.5/5). Pour raisonnement complexe ou créativité, gardez GPT-4 en fallback (10-15% du volume).
Quel est le coût réel d'infrastructure pour auto-héberger Ollama ?
Trois options : (1) Serveur dédié GPU (Hetzner AX102, 2× RTX 4090) = 89-109€/mois, (2) GPU cloud (NVIDIA L4 sur GCP/AWS) = 150-200€/mois, (3) VPS CPU uniquement (modèles 7B-13B) = 25-50€/mois. Pour remplacer €4000/mois d'API OpenAI, l'option (1) est optimale : ROI en 2-3 mois, puis économies nettes de 97%.
Combien de temps prend une migration complète ?
5-7 jours pour un tech lead expérimenté : Jour 1-2 (setup infra + Docker), Jour 3-4 (migration code + tests A/B), Jour 5-6 (optimisations + monitoring), Jour 7 (mise en prod progressive). L'API d'Ollama étant compatible OpenAI SDK, le code change minimal (5-10 lignes modifiées). Le plus long : évaluation qualité sur vos cas d'usage réels.
Peut-on faire une migration progressive sans risque ?
Oui, stratégie recommandée : Semaine 1 (20% du trafic non-critique sur Ollama, 80% reste sur OpenAI), Semaine 2 (50/50 avec A/B test qualité), Semaine 3 (80% Ollama, 20% OpenAI pour tâches complexes), Semaine 4 (95% Ollama avec fallback automatique GPT-4 si confiance < seuil). Aucune coupure service, rollback immédiat possible.
Quels sont les pièges à éviter lors de la migration ?
5 erreurs fréquentes : (1) Sous-estimer la RAM GPU nécessaire (70B = 48GB minimum avec quantization Q8), (2) Ne pas tester la latence en conditions réelles (cold start = 20-40s), (3) Oublier le monitoring GPU (température, VRAM), (4) Migrer 100% d'un coup sans fallback, (5) Utiliser CPU pour production (10-50× plus lent que GPU). Solution : POC sur 1 use case, mesurer, itérer.