Vue d'ensemble
API IA centralisée pour toute l'infrastructure Bakeli SI. Un seul endpoint pour tous les projets — Django, Laravel, React, mobile.
Architecture
Tes apps → https://ai.bakeli.tech → NPM → ai-RedTeam:8000 ┬→ Groq API (défaut chat)
├→ Gemini API (défaut fichiers + fallback)
└→ Ollama (fallback final)openai/gpt-oss-20b (Groq) par défaut pour /v1/chat. Gemini (gemini-3-flash-preview) est le backend par défaut pour /v1/chat/file (meilleur pour vision + documents) et sert de fallback secondaire si Groq est en rate limit. Ollama est le fallback final.Endpoints
| Méthode | Endpoint | Description | Auth |
|---|---|---|---|
POST | /v1/chat | Chat texte + streaming (Groq → Gemini → Ollama) | ✅ |
POST | /v1/chat/file | Chat + fichier joint (Gemini par défaut) | ✅ |
POST | /v1/chat/url | Chat + fichier via URL publique (Gemini par défaut) | ✅ |
POST | /v1/extract | Extraction structurée (JSON) depuis fichiers | ✅ |
GET | /v1/models | Liste des modèles (Ollama + Groq) | ✅ |
GET | /v1/health | Healthcheck | — |
Démarrage rapide
Ton premier appel en moins de 2 minutes.
API_KEY à l'équipe infra.curl -X POST https://ai.bakeli.tech/v1/chat \ -H "Authorization: Bearer <ta-cle-api>" \ -H "Content-Type: application/json" \ -d '{"model":"openai/gpt-oss-20b","messages":[{"role":"user","content":"Bonjour !"}]}'
Réponse attendue
{ "model": "openai/gpt-oss-20b", "message": { "role": "assistant", "content": "Bonjour !" }, "done": true }messages pour maintenir le contexte entre les tours.Authentification
Tous les endpoints sauf /v1/health requièrent une clé API.
Header requis
Authorization: Bearer <API_KEY>{ "error": "Unauthorized" }Stocker la clé
BAKELI_AI_URL=https://ai.bakeli.tech BAKELI_AI_KEY=ta-cle-api-secrete
POST /v1/chat
Chat texte avec historique. Supporte le streaming ndjson.
| Champ | Type | Défaut | Description |
|---|---|---|---|
model | string | openai/gpt-oss-20b | Modèle à utiliser (Groq par défaut, fallback Ollama) |
messages | array | requis | Tableau {role, content} |
stream | boolean | false | Active le streaming |
openai/, llama-, mixtral-, gemma-, qwen-, deepseek- sont envoyés à Groq. Si Groq est indisponible, le système bascule automatiquement sur llama3.2 via Ollama. Les autres modèles vont directement vers Ollama.Exemples
curl -X POST https://ai.bakeli.tech/v1/chat \ -H "Authorization: Bearer <API_KEY>" \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-oss-20b", "messages": [ {"role": "system", "content": "Tu es un assistant Bakeli SI."}, {"role": "user", "content": "Explique Docker en 3 lignes."} ] }'
import requests resp = requests.post( "https://ai.bakeli.tech/v1/chat", headers={"Authorization": "Bearer <API_KEY>"}, json={ "model": "openai/gpt-oss-20b", "messages": [ {"role": "system", "content": "Tu es un assistant Bakeli SI."}, {"role": "user", "content": "Explique Docker en 3 lignes."}, ], }, timeout=120, ) print(resp.json()["message"]["content"])
$ch = curl_init('https://ai.bakeli.tech/v1/chat'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer <API_KEY>', 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'model' => 'openai/gpt-oss-20b', 'messages' => [ ['role' => 'system', 'content' => 'Tu es un assistant Bakeli SI.'], ['role' => 'user', 'content' => 'Explique Docker en 3 lignes.'], ], ]), CURLOPT_TIMEOUT => 120, ]); $r = json_decode(curl_exec($ch), true); echo $r['message']['content'];
const res = await fetch("https://ai.bakeli.tech/v1/chat", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": "Bearer <API_KEY>", }, body: JSON.stringify({ model: "openai/gpt-oss-20b", messages: [ { role: "system", content: "Tu es un assistant Bakeli SI." }, { role: "user", content: "Explique Docker en 3 lignes." }, ], }), }); const data = await res.json(); console.log(data.message.content);
# Il suffit de choisir un modèle Groq — le routage est automatique import requests resp = requests.post( "https://ai.bakeli.tech/v1/chat", headers={"Authorization": "Bearer <API_KEY>"}, json={ "model": "llama-3.3-70b-versatile", # ← préfixe "llama-" → Groq "messages": [ {"role": "user", "content": "Explique Docker en 3 lignes."}, ], }, timeout=60, ) print(resp.json()["message"]["content"])
llama-3.3-70b-versatile, llama-3.1-8b-instant, mixtral-8x7b-32768, gemma-7b-it, deepseek-r1-distill-llama-70b. Voir la page Groq pour la liste complète.Réponse — cascade de fallback
Le gateway utilise une cascade de fallback automatique : Groq → Gemini-3 → Gemini-2.5 → Ollama. Quand un backend est indisponible, la réponse inclut deux champs supplémentaires :
{
"model": "gemini-3-flash-preview",
"message": { "role": "assistant", "content": "Docker est..." },
"done": true,
"fallback": true,
"fallback_reason": "Groq rate limit"
}{
"model": "gemini-2.5-flash",
"message": { "role": "assistant", "content": "Docker est..." },
"done": true,
"fallback": true,
"fallback_reason": "Groq rate limit → gemini-3-flash-preview quota épuisé"
}{
"model": "llama3.2",
"message": { "role": "assistant", "content": "Docker est..." },
"done": true,
"fallback": true,
"fallback_reason": "Groq rate limit → tous modèles Gemini épuisés"
}"fallback": true te permet de détecter côté client quel backend a répondu. La cascade complète est : Groq (défaut) → Gemini-3 → Gemini-2.5 → Ollama (llama3.2).POST /v1/chat/file
Envoie un fichier avec un prompt. Le gateway utilise Gemini par défaut (meilleur pour vision + documents), détecte automatiquement le type et extrait le contenu.
Content-Type : multipart/form-data
| Champ | Type | Description |
|---|---|---|
prompt | string | Question sur le fichier |
model | string | Modèle (défaut : gemini-3-flash-preview) |
file | file | Fichier à analyser |
Logique de traitement automatique
| Type de fichier | Backend par défaut | Ce que fait le gateway |
|---|---|---|
image/* (jpg, png, webp…) |
Gemini-3 | Vision multimodale native via SDK Google. Si échec → llava-phi3. Cascade : gemini-3 → gemini-2.5 → llava-phi3 |
application/pdf | Gemini-3 | Lecture PDF multimodale native via SDK Google. Si échec (ex: PDF corrompu) → extraction PyMuPDF. Cascade : gemini-3 → gemini-2.5 → extraction + llama3.2 |
.xlsx / .xls | Gemini-3 | Lecture Excel multimodale native via SDK Google. Si échec → extraction openpyxl/xlrd. Cascade : gemini-3 → gemini-2.5 → extraction + llama3.2 |
.docx | Gemini-3 | Lecture Word multimodale native via SDK Google. Si échec → extraction python-docx. Cascade : gemini-3 → gemini-2.5 → extraction + llama3.2 |
.csv / .tsv | Gemini-3 | Lecture CSV multimodale native via SDK Google. Si échec → extraction csv. Cascade : gemini-3 → gemini-2.5 → extraction + llama3.2 |
text/*, .json, .sql, code… | Gemini-3 | Lecture texte multimodale native via SDK Google. Si échec → injection texte brut. Cascade : gemini-3 → gemini-2.5 → llama3.2 |
/v1/chat/file — il supporte le multimodal natif via le SDK Google : images, PDF, Excel, Word, CSV sont envoyés directement. En cas d'échec (fichier corrompu, format non supporté), le gateway bascule automatiquement sur l'extraction de texte.llava-phi3 pour images, llama3.2 pour documents).Exemples
curl -X POST https://ai.bakeli.tech/v1/chat/file \ -H "Authorization: Bearer <API_KEY>" \ -F "prompt=Résume en 5 points clés" \ -F "file=@/chemin/document.pdf"
curl -X POST https://ai.bakeli.tech/v1/chat/file \ -H "Authorization: Bearer <API_KEY>" \ -F "prompt=Décris cette image en détail" \ -F "file=@/chemin/image.jpg"
curl -X POST https://ai.bakeli.tech/v1/chat/file \ -H "Authorization: Bearer <API_KEY>" \ -F "prompt=Décris cette image" \ -F "model=llava-phi3" \ -F "file=@/chemin/image.jpg"
import requests with open("rapport.pdf", "rb") as f: resp = requests.post( "https://ai.bakeli.tech/v1/chat/file", headers={"Authorization": "Bearer <API_KEY>"}, data={"prompt": "Résume en 5 points"}, # Gemini par défaut files={"file": f}, timeout=180, ) print(resp.json()["message"]["content"])
with open("image.jpg", "rb") as f: resp = requests.post( "https://ai.bakeli.tech/v1/chat/file", headers={"Authorization": "Bearer <API_KEY>"}, data={"prompt": "Décris cette image en détail"}, # Gemini par défaut files={"file": ("image.jpg", f, "image/jpeg")}, timeout=180, )
with open("image.jpg", "rb") as f: resp = requests.post( "https://ai.bakeli.tech/v1/chat/file", headers={"Authorization": "Bearer <API_KEY>"}, data={"prompt": "Décris cette image", "model": "llava-phi3"}, files={"file": ("image.jpg", f, "image/jpeg")}, timeout=180, )
$ch = curl_init('https://ai.bakeli.tech/v1/chat/file'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer <API_KEY>'], CURLOPT_POSTFIELDS => [ 'prompt' => 'Résume en 5 points', // pas de 'model' → gemini-3-flash-preview par défaut 'file' => new CURLFile('/chemin/document.pdf'), ], CURLOPT_TIMEOUT => 180, ]); $r = json_decode(curl_exec($ch), true);
POST /v1/chat/url
Chat avec un fichier ou une image accessible via URL publique. Le gateway télécharge le fichier et le traite comme /v1/chat/file. Gemini par défaut avec fallback automatique vers Ollama.
Content-Type : application/json
| Champ | Type | Description |
|---|---|---|
url | string | URL publique du fichier (requis, doit commencer par http:// ou https://) |
prompt | string | Question sur le fichier (optionnel, défaut : "Analyse ce fichier et résume son contenu.") |
model | string | Modèle (optionnel, défaut : gemini-3-flash-preview) |
/v1/chat/file.Cascade de fallback automatique
Le système essaie 2 modèles Gemini (3 tentatives chacun) avant de basculer vers Ollama :
Tentative 1-3 :
gemini-3-flash-preview → gemini-2.5-flash
↓ (si tous échouent, attente 120s entre tentatives)
Fallback final :
Ollama (llama3.2-vision pour images, llama3.2 pour autres fichiers)
Taux de succès : ~99.9% (7 essais au total)"fallback": true et "fallback_reason".Types de fichiers supportés
| Type | Extensions | Backend |
|---|---|---|
| Images | jpg, png, gif, webp | Gemini-3 → llama3.2-vision |
| Documents | pdf, docx, doc | Gemini-3 → llama3.2 |
| Tableurs | xlsx, xls, csv | Gemini-3 → llama3.2 |
| Texte | txt, json, sql, code | Gemini-3 → llama3.2 |
Exemples
curl -X POST https://ai.bakeli.tech/v1/chat/url \ -H "Authorization: Bearer <API_KEY>" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/image.jpg", "prompt": "Décris cette image en détail" }'
curl -X POST https://ai.bakeli.tech/v1/chat/url \ -H "Authorization: Bearer <API_KEY>" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/document.pdf", "prompt": "Résume ce document en 5 points" }'
curl -X POST https://ai.bakeli.tech/v1/chat/url \ -H "Authorization: Bearer <API_KEY>" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/image.jpg", "prompt": "Décris cette image", "model": "gemini-2.5-flash" }'
import requests resp = requests.post( "https://ai.bakeli.tech/v1/chat/url", headers={"Authorization": "Bearer <API_KEY>"}, json={ "url": "https://example.com/image.jpg", "prompt": "Décris cette image en détail" }, timeout=180, ) print(resp.json()["message"]["content"])
import requests resp = requests.post( "https://ai.bakeli.tech/v1/chat/url", headers={"Authorization": "Bearer <API_KEY>"}, json={ "url": "https://example.com/document.pdf", "prompt": "Résume ce document en 5 points" }, timeout=180, ) print(resp.json()["message"]["content"])
$ch = curl_init('https://ai.bakeli.tech/v1/chat/url'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer <API_KEY>', 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'url' => 'https://example.com/image.jpg', 'prompt' => 'Décris cette image', ]), CURLOPT_TIMEOUT => 180, ]); $r = json_decode(curl_exec($ch), true); echo $r['message']['content'];
const res = await fetch("https://ai.bakeli.tech/v1/chat/url", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": "Bearer <API_KEY>", }, body: JSON.stringify({ url: "https://example.com/image.jpg", prompt: "Décris cette image", }), }); const data = await res.json(); console.log(data.message.content);
Réponse — succès avec Gemini
{
"message": {
"role": "assistant",
"content": "Cette image montre..."
},
"done": true
}Réponse — fallback Ollama
Si tous les modèles Gemini échouent, le système bascule automatiquement vers Ollama :
{
"model": "llama3.2-vision",
"message": {
"role": "assistant",
"content": "Cette image montre..."
},
"done": true,
"fallback": true,
"fallback_reason": "Tous les modèles Gemini indisponibles après 3 tentatives"
}"fallback": true indique que le fallback a été utilisé. Le champ "fallback_reason" explique pourquoi (quotas épuisés, clés invalides, erreurs réseau, etc.).Gestion d'erreurs
| Code | Cause | Solution |
|---|---|---|
| 400 | URL manquante ou invalide | Vérifier que l'URL commence par http:// ou https:// |
| 400 | Fichier vide (0 bytes) | Vérifier que l'URL pointe vers un fichier valide |
| 400 | Erreur de téléchargement (404, timeout) | Vérifier que l'URL est accessible publiquement |
| 408 | Timeout (30s) | Le fichier est trop volumineux ou le serveur est lent |
Cas d'usage
- Analyser une image depuis un lien public : Partage d'image sur Slack, Discord, etc.
- Résumer un PDF hébergé en ligne : Documentation, rapports, articles
- Extraire des données d'un fichier Excel partagé : Google Sheets, Dropbox, etc.
- Discuter avec un document sans l'uploader : Fichiers volumineux, liens temporaires
Limitations
- URL publique uniquement : Le fichier doit être accessible sans authentification
- Timeout de 30 secondes : Pour les fichiers volumineux, préférer
/v1/chat/file - Taille maximale : Dépend de la configuration du serveur (généralement 50 MB)
POST /v1/extract
Extraction de données structurées (JSON) à partir de fichiers. Idéal pour OCR de factures, extraction de tableaux, parsing de documents structurés.
Content-Type : multipart/form-data
| Champ | Type | Défaut | Description |
|---|---|---|---|
file | file | requis | Fichier à analyser (image, PDF, Excel, etc.) |
user_prompt | string | requis | Instructions d'extraction (ex: "Extraire les données de facture en JSON") |
system_prompt | string | — | Instructions système optionnelles (ex: "Tu es un expert en OCR") |
model | string | gemini-3-flash-preview | Modèle à utiliser |
temperature | float | 0.2 | Créativité (0.0-1.0, plus bas = plus déterministe) |
format | string | json | Format de sortie : json ou text |
Cas d'usage
| Cas d'usage | Exemple de prompt | Type de fichier |
|---|---|---|
| OCR de factures | "Extraire les données de facture en JSON avec numéro, date, montant, articles" | Image, PDF |
| Extraction de tableaux | "Convertir ce tableau en JSON avec colonnes nom, prénom, email" | Excel, PDF, Image |
| Parsing de CV | "Extraire nom, compétences, expériences, formation en JSON" | PDF, Word, Image |
| Analyse de reçus | "Extraire montant total, date, commerçant, articles en JSON" | Image, PDF |
| Extraction de formulaires | "Extraire tous les champs du formulaire en JSON" | Image, PDF |
Exemples
curl -X POST https://ai.bakeli.tech/v1/extract \ -H "Authorization: Bearer <API_KEY>" \ -F "file=@facture.jpg" \ -F "user_prompt=Extraire les données de facture en JSON avec les champs: numero_facture, date, montant_total, articles (nom, quantite, prix_unitaire)" \ -F "system_prompt=Tu es un expert en OCR de factures" \ -F "temperature=0.2"
curl -X POST https://ai.bakeli.tech/v1/extract \ -H "Authorization: Bearer <API_KEY>" \ -F "file=@donnees.xlsx" \ -F "user_prompt=Convertir ce tableau en JSON avec colonnes: nom, prenom, email, telephone"
import requests import json with open("facture.jpg", "rb") as f: resp = requests.post( "https://ai.bakeli.tech/v1/extract", headers={"Authorization": "Bearer <API_KEY>"}, data={ "user_prompt": "Extraire les données de facture en JSON avec: numero_facture, date, montant_total, articles", "system_prompt": "Tu es un expert en OCR de factures", "temperature": "0.2", "format": "json", }, files={"file": f}, timeout=180, ) result = resp.json() if result.get("data"): # JSON parsé automatiquement facture = result["data"] print(f"Facture n°{facture['numero_facture']}") print(f"Montant: {facture['montant_total']}") else: # Fallback sur raw_text si parsing JSON échoue print(result["raw_text"])
def extract_structured_data( filepath: str, user_prompt: str, system_prompt: str = "", temperature: float = 0.2, ) -> dict: """Extrait des données structurées depuis un fichier.""" with open(filepath, "rb") as f: resp = requests.post( "https://ai.bakeli.tech/v1/extract", headers={"Authorization": f"Bearer {settings.BAKELI_AI_KEY}"}, data={ "user_prompt": user_prompt, "system_prompt": system_prompt, "temperature": str(temperature), "format": "json", }, files={"file": f}, timeout=180, ) resp.raise_for_status() result = resp.json() return result.get("data") or result.get("raw_text") # Utilisation facture = extract_structured_data( "facture.jpg", "Extraire numero_facture, date, montant_total, articles en JSON", "Tu es un expert en OCR de factures", )
$ch = curl_init('https://ai.bakeli.tech/v1/extract'); $file = new CURLFile('facture.jpg', 'image/jpeg', 'facture.jpg'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer <API_KEY>'], CURLOPT_POSTFIELDS => [ 'file' => $file, 'user_prompt' => 'Extraire numero_facture, date, montant_total, articles en JSON', 'system_prompt' => 'Tu es un expert en OCR de factures', 'temperature' => '0.2', 'format' => 'json', ], CURLOPT_TIMEOUT => 180, ]); $response = json_decode(curl_exec($ch), true); if ($response['data']) { // JSON parsé automatiquement $facture = $response['data']; echo "Facture n°" . $facture['numero_facture']; } else { // Fallback sur raw_text echo $response['raw_text']; }
Réponse — Format JSON
Quand format=json (défaut), le gateway tente de parser le JSON automatiquement :
{
"model": "gemini-3-flash-preview",
"data": {
"numero_facture": "FAC-2024-001",
"date": "2024-03-15",
"montant_total": 1250.00,
"articles": [
{ "nom": "Ordinateur portable", "quantite": 1, "prix_unitaire": 1000.00 },
{ "nom": "Souris sans fil", "quantite": 2, "prix_unitaire": 125.00 }
]
},
"raw_text": "```json\n{...}\n```",
"done": true
}{
"model": "gemini-3-flash-preview",
"data": null,
"raw_text": "Voici les données extraites: ...",
"done": true,
"warning": "Réponse non-JSON, voir raw_text"
}data contient le JSON parsé si possible, sinon null. Le champ raw_text contient toujours la réponse brute du modèle (utile pour debug).Réponse — Format texte
Quand format=text, la réponse est retournée telle quelle :
{
"model": "gemini-3-flash-preview",
"text": "Le document contient les informations suivantes:\n- Numéro: FAC-2024-001\n- Date: 15/03/2024\n...",
"done": true
}Cascade de fallback
Comme pour /v1/chat/file, l'extraction utilise une cascade automatique : Gemini-3 → Gemini-2.5 → Ollama.
{
"model": "gemini-2.5-flash",
"data": { /* ... */ },
"done": true,
"fallback": true,
"fallback_reason": "gemini-3-flash-preview quota épuisé"
}{
"model": "llama3.2",
"data": { /* ... */ },
"done": true,
"fallback": true,
"fallback_reason": "Gemini indisponible"
}Bonnes pratiques
| Pratique | Description |
|---|---|
| Prompt précis | Spécifie exactement les champs JSON attendus : "Extraire en JSON avec champs: nom, prenom, email" |
| System prompt | Utilise system_prompt pour définir le rôle : "Tu es un expert en OCR de factures" |
| Température basse | Garde temperature=0.2 (ou moins) pour des résultats reproductibles |
| Validation JSON | Vérifie toujours data != null avant d'utiliser le JSON parsé |
| Fallback raw_text | Si data est null, parse manuellement raw_text |
| Timeout généreux | Utilise timeout=180 (3 min) pour les gros fichiers |
"Extraire en JSON format: {\"nom\": \"...\", \"prenom\": \"...\", \"email\": \"...\"}"GET /v1/models
Liste les modèles disponibles — Ollama (local) et Groq (cloud) si la clé est configurée.
curl https://ai.bakeli.tech/v1/models -H "Authorization: Bearer <API_KEY>"
Réponse
{
"models": [
{ "name": "llama3.2:latest", "size": 2019383552 },
{ "name": "mistral:latest", "size": 4109854720 },
{ "name": "llama-3.3-70b-versatile", "provider": "groq" },
{ "name": "mixtral-8x7b-32768", "provider": "groq" }
]
}GROQ_API_KEY est définie. Ils ont un champ "provider": "groq".GET /v1/health
Healthcheck sans authentification. Utilisé par NPM et les outils de monitoring.
curl https://ai.bakeli.tech/v1/health{ "RedTeam": "ok", "ollama": "ok" }{ "RedTeam": "ok", "ollama": "unreachable" }Django / Python
Module client réutilisable à copier dans chaque app Django de l'infrastructure Bakeli.
1 — Variables d'environnement
BAKELI_AI_URL=https://ai.bakeli.tech BAKELI_AI_KEY=ta-cle-api
BAKELI_AI_URL = env("BAKELI_AI_URL", default="https://ai.bakeli.tech") BAKELI_AI_KEY = env("BAKELI_AI_KEY")
2 — Module client
import requests from django.conf import settings def ask(messages: list, model: str = "openai/gpt-oss-20b") -> str: """Envoie une conversation, retourne le texte de réponse.""" resp = requests.post( f"{settings.BAKELI_AI_URL}/v1/chat", headers={"Authorization": f"Bearer {settings.BAKELI_AI_KEY}"}, json={"model": model, "messages": messages}, timeout=120, ) resp.raise_for_status() return resp.json()["message"]["content"] def ask_with_file(prompt: str, filepath: str, model: str = "openai/gpt-oss-20b") -> str: """Envoie un fichier avec un prompt.""" with open(filepath, "rb") as f: resp = requests.post( f"{settings.BAKELI_AI_URL}/v1/chat/file", headers={"Authorization": f"Bearer {settings.BAKELI_AI_KEY}"}, data={"prompt": prompt, "model": model}, files={"file": f}, timeout=180, ) return resp.json()["message"]["content"]
3 — Utilisation dans une view
from utils.ai_client import ask def assistant(request): historique = request.session.get("chat_history", []) historique.append({"role": "user", "content": request.POST.get("message")}) reponse = ask(historique) historique.append({"role": "assistant", "content": reponse}) request.session["chat_history"] = historique return JsonResponse({"reponse": reponse})
Laravel / PHP
Service injectable pour une intégration élégante dans l'architecture Laravel.
1 — Config
BAKELI_AI_URL=https://ai.bakeli.tech BAKELI_AI_KEY=ta-cle-api
<?php return [ 'url' => env('BAKELI_AI_URL', 'https://ai.bakeli.tech'), 'key' => env('BAKELI_AI_KEY'), 'model' => env('BAKELI_AI_MODEL', 'openai/gpt-oss-20b'), 'timeout' => 120, ];
2 — AiService
<?php namespace App\Services; use Illuminate\Support\Facades\Http; class AiService { private string $url, $key, $model; public function __construct() { $this->url = config('ai.url'); $this->key = config('ai.key'); $this->model = config('ai.model'); } public function ask(array $messages, string $model = null): string { return Http::withToken($this->key) ->timeout(config('ai.timeout')) ->post($this->url . '/v1/chat', [ 'model' => $model ?? $this->model, 'messages' => $messages, ])->throw()->json('message.content'); } public function askWithFile(string $prompt, string $path, string $model = null): string { return Http::withToken($this->key) ->timeout(180) ->attach('file', file_get_contents($path), basename($path)) ->post($this->url . '/v1/chat/file', [ 'prompt' => $prompt, 'model' => $model ?? $this->model, ])->throw()->json('message.content'); } }
3 — Controller
class AssistantController extends Controller { public function __construct(private AiService $ai) {} public function chat(Request $request) { $history = session('chat_history', []); $history[] = ['role' => 'user', 'content' => $request->input('message')]; $reply = $this->ai->ask($history); $history[] = ['role' => 'assistant', 'content' => $reply]; session(['chat_history' => $history]); return response()->json(['reply' => $reply]); } }
React / Next.js
API Route sécurisée côté serveur + hook React réutilisable côté client.
ai.bakeli.tech directement depuis le browser. Utilise une API Route Next.js comme proxy.1 — Variables d'environnement
# Côté serveur uniquement (sans NEXT_PUBLIC_)
BAKELI_AI_URL=https://ai.bakeli.tech
BAKELI_AI_KEY=ta-cle-api2 — API Route (proxy sécurisé)
import { NextRequest, NextResponse } from 'next/server'; export async function POST(req: NextRequest) { const body = await req.json(); const resp = await fetch(`${process.env.BAKELI_AI_URL}/v1/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${process.env.BAKELI_AI_KEY}`, }, body: JSON.stringify(body), }); return NextResponse.json(await resp.json()); }
3 — Hook React
import { useState, useCallback } from 'react'; type Message = { role: 'user' | 'assistant'; content: string }; export function useAI(model = 'openai/gpt-oss-20b') { const [loading, setLoading] = useState(false); const [messages, setMessages] = useState<Message[]>([]); const ask = useCallback(async (userMessage: string) => { const updated = [...messages, { role: 'user' as const, content: userMessage }]; setMessages(updated); setLoading(true); try { const res = await fetch('/api/ai', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model, messages: updated }), }); const data = await res.json(); const reply = data.message.content; setMessages(prev => [...prev, { role: 'assistant', content: reply }]); return reply; } finally { setLoading(false); } }, [messages, model]); return { ask, loading, messages, reset: () => setMessages([]) }; }
4 — Composant Chat
import { useState } from 'react'; import { useAI } from '@/hooks/useAI'; export default function ChatWidget() { const { ask, loading, messages } = useAI(); const [input, setInput] = useState(''); return ( <div> {messages.map((m, i) => <div key={i}>{m.content}</div>)} <input value={input} onChange={e => setInput(e.target.value)} /> <button onClick={() => { ask(input); setInput(''); }} disabled={loading}> {loading ? '...' : 'Envoyer'} </button> </div> ); }
Migration depuis Gemini
Remplace l'API Gemini native par le gateway Bakeli AI. Le format messages est identique, et Gemini est maintenant intégré comme backend du gateway.
ai.bakeli.tech, avec en bonus le fallback automatique vers Ollama si Gemini est down.Avant → Après
import google.generativeai as genai genai.configure(api_key="AIza...") model = genai.GenerativeModel("gemini-pro") text = model.generate_content("Explique Docker").text
from utils.ai_client import ask # Option 1 : Utiliser Groq par défaut (+ fallback Gemini automatique) text = ask([{"role": "user", "content": "Explique Docker"}]) # Option 2 : Forcer Gemini directement text = ask([{"role": "user", "content": "Explique Docker"}], model="gemini-3-flash-preview")
Avantages du gateway vs Gemini natif
| Fonctionnalité | Gemini natif | Gateway Bakeli AI |
|---|---|---|
| Accès à Gemini | ✅ Direct | ✅ Via gateway (même API) |
| Fallback automatique | ❌ Aucun | ✅ Gemini → Ollama |
| Multi-backends | ❌ Gemini uniquement | ✅ Groq + Gemini + Ollama |
| Clé API unique | ❌ Clé Gemini | ✅ Une clé pour tout |
| Hébergement local | ❌ Cloud Google | ✅ Ollama en fallback |
| Coût | Quota gratuit limité | Gratuit (Ollama) + quota Gemini |
Checklist de migration
| # | Action |
|---|---|
| 1 | Ajouter BAKELI_AI_URL et BAKELI_AI_KEY dans le .env |
| 2 | Ajouter les variables dans settings.py / config/ai.php |
| 3 | Copier le module client dans le projet |
| 4 | Remplacer les appels genai.* par le module client |
| 5 | (Optionnel) Garder GEMINI_API_KEY dans le .env du serveur pour activer Gemini côté gateway |
| 6 | Supprimer google-generativeai de requirements.txt |
model="gemini-3-flash-preview" dans tes appels. Le gateway routera vers Gemini avec cascade automatique (gemini-3 → gemini-2.5 → Ollama).Modèles disponibles
Modèles LLM installés sur le serveur Ollama. GET /v1/models pour la liste en temps réel.
Modèles Ollama (local)
| Identifiant | Taille | RAM min | Usage |
|---|---|---|---|
llama3.2 | 2.0 GB | 4 GB | Usage général, rapide ⚡ |
mistral | 4.1 GB | 8 GB | Raisonnement, texte long |
gemma3:4b | 3.3 GB | 6 GB | Multilingue, proche Gemini |
llava-phi3 | 2.9 GB | 6 GB | Vision — analyse d'images 🖼️ |
phi3 | 2.3 GB | 4 GB | Code, logique |
llama3.2 est le modèle de fallback automatique quand Groq est indisponible. Le modèle principal par défaut est openai/gpt-oss-20b (Groq). Pour les modèles Groq cloud, voir la page Groq.Groq — Modèles cloud
Modèles hébergés sur Groq Cloud, accessibles via le même endpoint /v1/chat. Le routage est automatique selon le préfixe du modèle.
GROQ_API_KEY soit définie dans les variables d'environnement du serveur.Configuration
GROQ_API_KEY=gsk_xxxxxxxxxxxxxxxxxxxx
GROQ_BASE_URL=https://api.groq.com/openai/v1 # optionnelPréfixes routés vers Groq
| Préfixe | Exemple |
|---|---|
openai/ | openai/gpt-oss-20b ⭐ défaut |
llama- | llama-3.3-70b-versatile |
mixtral- | mixtral-8x7b-32768 |
gemma- | gemma-7b-it |
qwen- | qwen-qwq-32b |
deepseek- | deepseek-r1-distill-llama-70b |
whisper- | whisper-large-v3 |
Modèles Groq recommandés
| Identifiant | Contexte | Usage |
|---|---|---|
openai/gpt-oss-20b | — | ⭐ Modèle par défaut du gateway |
llama-3.3-70b-versatile | 128k | Usage général, haute qualité |
llama-3.1-8b-instant | 128k | Rapide, faible latence ⚡ |
mixtral-8x7b-32768 | 32k | Raisonnement, texte long |
gemma-7b-it | 8k | Léger, multilingue |
deepseek-r1-distill-llama-70b | 128k | Raisonnement avancé 🧠 |
qwen-qwq-32b | 128k | Raisonnement, code |
Exemple d'appel
curl -X POST https://ai.bakeli.tech/v1/chat \ -H "Authorization: Bearer <API_KEY>" \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-oss-20b", "messages": [{"role": "user", "content": "Explique les LLM en 3 lignes."}] }'
{"model": "...", "message": {"role": "assistant", "content": "..."}, "done": true}llama3.2 via Ollama. La réponse contiendra "fallback": true.Erreurs spécifiques Groq
| Code | Cause | Solution |
|---|---|---|
| 503 | GROQ_API_KEY non configurée → fallback Ollama | Définir la variable d'environnement |
| 401 | Clé Groq invalide ou expirée → fallback Ollama | Vérifier la clé sur console.groq.com |
| 429 | Rate limit Groq dépassé → fallback Ollama | Réduire la fréquence ou changer de modèle |
Gemini — Google AI
Modèles Gemini de Google, accessibles via une API OpenAI-compatible. Gemini est le backend par défaut pour /v1/chat/file (meilleur pour vision + documents) et sert de fallback secondaire après Groq pour /v1/chat.
Configuration
GEMINI_API_KEY=AIzaSyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx GEMINI_BASE_URL=https://generativelanguage.googleapis.com/v1beta/openai/ GEMINI_MODEL=gemini-3-flash-preview # chat classique GEMINI_FILE_MODEL=gemini-3-flash-preview # défaut pour /chat/file GEMINI_FALLBACK_MODEL=gemini-2.5-flash # fallback si quota épuisé
API OpenAI-compatible
Le gateway utilise deux APIs de Gemini selon le contexte :
- Chat texte (
/v1/chat) : API OpenAI-compatible (v1beta/openai/) pour la compatibilité avec le client OpenAI Python/JS - Fichiers (
/v1/chat/file) : SDK natif Google (google-genai) pour le multimodal natif (images, PDF, Excel, Word, CSV)
from openai import OpenAI client = OpenAI( api_key="AIzaSy...", base_url="https://generativelanguage.googleapis.com/v1beta/openai/" ) response = client.chat.completions.create( model="gemini-3-flash-preview", messages=[{"role": "user", "content": "Bonjour !"}] ) print(response.choices[0].message.content)
from google import genai client = genai.Client(api_key="AIzaSy...") with open("document.pdf", "rb") as f: file_part = genai.types.Part.from_bytes( data=f.read(), mime_type="application/pdf" ) response = client.models.generate_content( model="gemini-3-flash-preview", contents=["Résume ce document", file_part] ) print(response.text)
Modèles Gemini disponibles
| Identifiant | Contexte | Capacités | Usage |
|---|---|---|---|
gemini-3-flash-preview | 1M tokens | Texte, Vision, Audio | ⭐ Défaut — rapide, multimodal, dernière génération |
gemini-2.5-flash | 1M tokens | Texte, Vision, Audio | 🔄 Fallback — si quota gemini-3 épuisé |
Quand Gemini est utilisé
| Endpoint | Condition | Comportement |
|---|---|---|
/v1/chat/file |
Toujours (par défaut) | Gemini traite tous les fichiers en multimodal natif via le SDK Google (pas d'extraction). Cascade : gemini-3-flash-preview → gemini-2.5-flash → Ollama (avec extraction : llava-phi3 pour images, llama3.2 pour documents) |
/v1/chat |
Si Groq rate limit (429) | Cascade : Groq → gemini-3-flash-preview → gemini-2.5-flash → Ollama (llama3.2). La réponse contient "fallback": true |
/v1/chat |
Si Groq down | Cascade : Groq → gemini-3-flash-preview → gemini-2.5-flash → Ollama |
/v1/chat |
Si model=gemini-* |
Appel direct vers Gemini avec cascade interne si quota épuisé |
Exemples d'utilisation
Chat classique (fallback automatique)
curl -X POST https://ai.bakeli.tech/v1/chat \ -H "Authorization: Bearer <API_KEY>" \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-oss-20b", "messages": [{"role": "user", "content": "Explique Docker"}] }' # Si Groq est en rate limit, la réponse sera : { "model": "gemini-3-flash-preview", "message": { "role": "assistant", "content": "Docker est..." }, "done": true, "fallback": true, "fallback_reason": "Groq rate limit" } # Si gemini-3-flash-preview est aussi épuisé : { "model": "gemini-2.5-flash", "message": { "role": "assistant", "content": "Docker est..." }, "done": true, "fallback": true, "fallback_reason": "Groq rate limit → gemini-3-flash-preview quota épuisé" }
Forcer Gemini directement
import requests resp = requests.post( "https://ai.bakeli.tech/v1/chat", headers={"Authorization": "Bearer <API_KEY>"}, json={ "model": "gemini-3-flash-preview", # ← force Gemini "messages": [ {"role": "user", "content": "Explique les LLM en 3 lignes"} ] }, timeout=60 ) print(resp.json()["message"]["content"]) # Si gemini-3-flash-preview est épuisé, bascule automatiquement sur gemini-2.5-flash
Fichiers (Gemini par défaut)
curl -X POST https://ai.bakeli.tech/v1/chat/file \ -H "Authorization: Bearer <API_KEY>" \ -F "prompt=Décris cette image en détail" \ -F "file=@image.jpg" # Pas besoin de spécifier model= → Gemini par défaut
import requests with open("rapport.pdf", "rb") as f: resp = requests.post( "https://ai.bakeli.tech/v1/chat/file", headers={"Authorization": "Bearer <API_KEY>"}, data={"prompt": "Résume ce document en 5 points"}, files={"file": f}, timeout=180 ) print(resp.json()["message"]["content"])
Avantages de Gemini
| Capacité | Description |
|---|---|
| 🖼️ Vision native | Analyse d'images en multimodal natif via SDK Google |
| 📄 Multimodal natif | Lit PDF, Excel, Word, CSV directement sans extraction de texte |
| 📊 Documents longs | Contexte jusqu'à 1M tokens |
| 🌍 Multilingue | Excellent sur le français, l'arabe, le wolof |
| ⚡ Rapide | Gemini 3 Flash : latence ultra-faible |
| 💰 Quota gratuit | 1500 requêtes/jour gratuites |
| 🔄 Fallback intelligent | Cascade interne : gemini-3 → gemini-2.5 → Ollama |
Erreurs spécifiques Gemini
| Code | Cause | Solution |
|---|---|---|
| 503 | GEMINI_API_KEY non configurée → fallback Ollama | Définir la variable d'environnement |
| 401 | Clé Gemini invalide → fallback Ollama | Vérifier la clé sur AI Studio |
| 429 | Quota Gemini dépassé → fallback Ollama | Attendre le reset quotidien ou upgrader vers API payante |
| 400 | Fichier trop volumineux (>20 MB) | Compresser l'image ou découper le PDF |
llava-phi3 pour images, llama3.2 pour texte). La réponse contiendra "fallback": true.Cascade complète de fallback
# /v1/chat (texte) Groq (openai/gpt-oss-20b) → Gemini-3 → Gemini-2.5 → Ollama (llama3.2) # /v1/chat/file (fichiers) Gemini-3 (gemini-3-flash-preview) → Gemini-2.5 (gemini-2.5-flash) → Ollama ├─ Images : llava-phi3 └─ Documents : llama3.2
Codes d'erreur
Erreurs possibles, leurs causes et solutions.
| Code | Cause | Solution |
|---|---|---|
| 400 | JSON mal formé | Vérifier le body |
| 401 | Clé API invalide | Vérifier Authorization: Bearer ... |
| 503 | Tous les backends down (Groq + Gemini + Ollama) | Vérifier la config et docker start ollama |
| 502 | ai-RedTeam arrêté | docker-compose up -d ai-RedTeam |
| 504 | Timeout | Utiliser llama-3.1-8b-instant (Groq) ou llama3.2 (Ollama) |
| 429 | Rate limit Groq → fallback Gemini automatique | Normal, Gemini prend le relais |
Cascade de fallback
Le gateway utilise une cascade intelligente pour garantir la disponibilité :
# Chat texte (/v1/chat) 1. Groq (openai/gpt-oss-20b) — défaut 2. Gemini-3 (gemini-3-flash-preview) — si Groq 429 ou down 3. Gemini-2.5 (gemini-2.5-flash) — si Gemini-3 429 ou down 4. Ollama (llama3.2) — si tous les quotas épuisés # Fichiers (/v1/chat/file) 1. Gemini-3 (gemini-3-flash-preview) — défaut 2. Gemini-2.5 (gemini-2.5-flash) — si Gemini-3 429 ou down 3. Ollama — si tous les quotas épuisés ├─ llava-phi3 (images) └─ llama3.2 (documents)
"fallback": true et "fallback_reason": "..." pour tracer le backend utilisé.Diagnostic
curl https://ai.bakeli.tech/v1/health docker logs -f ai-RedTeam docker ps | grep -E "ollama|ai-RedTeam"