Guide d'intégration
Ce guide vous accompagne dans l'intégration complÚte de l'API YODI dans votre application.
Aperçu de l'intĂ©grationâ
Architecture recommandĂ©eâ
âââââââââââââââââââ âââââââââââââââââââ âââââââââââââââââââ
â Frontend â â Backend â â API YODI â
â (React/Vue) âââââșâ (Express/ âââââșâ â
â â â Django) â â â
âââââââââââââââââââ âââââââââââââââââââ âââââââââââââââââââ
Principes de sĂ©curitĂ©â
Ne jamais faire :
- Exposer la clé API cÎté client
- Stocker la clé API en dur dans le code
- Utiliser la mĂȘme clĂ© pour tous les environnements
Bonnes pratiques :
- Proxy des requĂȘtes via votre backend
- Variables d'environnement pour les clés
- Clés séparées par environnement
- Rotation réguliÚre des clés
IntĂ©gration Backendâ
Node.js/Expressâ
Installationâ
npm install express yodi-sdk dotenv cors helmet
Configuration de baseâ
// server.js
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const { YodiClient } = require('yodi-sdk');
const app = express();
const port = process.env.PORT || 3001;
// Sécurité
app.use(helmet());
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3001'],
credentials: true
}));
app.use(express.json({ limit: '10mb' }));
// Client YODI
const yodiClient = new YodiClient({
apiKey: process.env.YODI_API_KEY,
timeout: 30000
});
// Middleware de validation
const validateRequest = (req, res, next) => {
const { messages, model } = req.body;
if (!messages || !Array.isArray(messages)) {
return res.status(400).json({ error: 'Messages requis et doit ĂȘtre un tableau' });
}
if (!model) {
return res.status(400).json({ error: 'ModĂšle requis' });
}
next();
};
// Routes API
app.post('/api/chat', validateRequest, async (req, res) => {
try {
const { messages, model, ...options } = req.body;
const response = await yodiClient.chat.completions.create({
model,
messages,
...options
});
res.json(response);
} catch (error) {
console.error('Erreur chat:', error);
if (error.status === 429) {
res.status(429).json({ error: 'Limite de taux atteinte' });
} else if (error.status === 401) {
res.status(500).json({ error: 'Erreur d\'authentification' });
} else {
res.status(500).json({ error: 'Erreur interne du serveur' });
}
}
});
app.post('/api/embeddings', async (req, res) => {
try {
const { input, model = 'yodi-embed' } = req.body;
if (!input) {
return res.status(400).json({ error: 'Input requis' });
}
const response = await yodiClient.embeddings.create({
model,
input
});
res.json(response);
} catch (error) {
console.error('Erreur embeddings:', error);
res.status(500).json({ error: 'Erreur lors de la création des embeddings' });
}
});
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'OK', timestamp: new Date().toISOString() });
});
app.listen(port, () => {
console.log(`Serveur démarré sur le port ${port}`);
});
Configuration avancĂ©eâ
// services/yodiService.js
class YodiService {
constructor() {
this.client = new YodiClient({
apiKey: process.env.YODI_API_KEY,
timeout: 30000,
maxRetries: 3
});
this.rateLimiter = new Map(); // Simple rate limiting
}
async checkRateLimit(userId, limit = 10, windowMs = 60000) {
const now = Date.now();
const userRequests = this.rateLimiter.get(userId) || [];
// Nettoyer les requĂȘtes anciennes
const recentRequests = userRequests.filter(time => now - time < windowMs);
if (recentRequests.length >= limit) {
throw new Error('Rate limit exceeded');
}
recentRequests.push(now);
this.rateLimiter.set(userId, recentRequests);
}
async createChatCompletion(params, userId = null) {
if (userId) {
await this.checkRateLimit(userId);
}
return await this.client.chat.completions.create(params);
}
async createEmbeddings(params, userId = null) {
if (userId) {
await this.checkRateLimit(userId, 20); // Limite plus haute pour embeddings
}
return await this.client.embeddings.create(params);
}
}
module.exports = new YodiService();
Python/FastAPIâ
Installationâ
pip install fastapi uvicorn yodi-sdk python-dotenv pydantic
Configuration de baseâ
# main.py
from fastapi import FastAPI, HTTPException, Depends
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel, Field
from typing import List, Optional, Dict, Any
import os
from yodi import Client
app = FastAPI(title="YODI API Proxy", version="1.0.0")
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=os.getenv("ALLOWED_ORIGINS", "http://localhost:3001").split(","),
allow_credentials=True,
allow_methods=["GET", "POST"],
allow_headers=["*"],
)
# Client YODI
yodi_client = Client(api_key=os.getenv("YODI_API_KEY"))
# ModĂšles Pydantic
class Message(BaseModel):
role: str = Field(..., regex="^(system|user|assistant)$")
content: str
class ChatRequest(BaseModel):
messages: List[Message]
model: str = "yodi-1"
temperature: Optional[float] = Field(None, ge=0, le=2)
max_tokens: Optional[int] = Field(None, gt=0)
top_p: Optional[float] = Field(None, ge=0, le=1)
class EmbeddingRequest(BaseModel):
input: str
model: str = "yodi-embed"
# Authentification (optionnelle)
security = HTTPBearer(auto_error=False)
async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
if not credentials:
return None
# Ici, vous pouvez valider le token
# Pour cet exemple, on retourne simplement un ID utilisateur
return {"id": "user_123"}
@app.post("/api/chat")
async def create_chat_completion(
request: ChatRequest,
user = Depends(get_current_user)
):
try:
# Convertir les messages Pydantic en dict
messages = [msg.dict() for msg in request.messages]
response = yodi_client.chat.completions.create(
model=request.model,
messages=messages,
temperature=request.temperature,
max_tokens=request.max_tokens,
top_p=request.top_p
)
return response
except Exception as e:
if "rate_limit" in str(e).lower():
raise HTTPException(status_code=429, detail="Rate limit exceeded")
elif "401" in str(e):
raise HTTPException(status_code=500, detail="Authentication error")
else:
raise HTTPException(status_code=500, detail="Internal server error")
@app.post("/api/embeddings")
async def create_embeddings(
request: EmbeddingRequest,
user = Depends(get_current_user)
):
try:
response = yodi_client.embeddings.create(
model=request.model,
input=request.input
)
return response
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/health")
async def health_check():
return {"status": "OK", "timestamp": "2024-01-01T00:00:00Z"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
Service avec gestion d'erreurs avancĂ©eâ
# services/yodi_service.py
import asyncio
import time
from typing import Dict, List, Optional
from yodi import Client
from yodi.exceptions import YodiAPIError
import logging
logger = logging.getLogger(__name__)
class YodiService:
def __init__(self, api_key: str):
self.client = Client(api_key=api_key)
self.rate_limits: Dict[str, List[float]] = {}
def check_rate_limit(self, user_id: str, limit: int = 10, window: int = 60):
"""Vérifie les limites de taux par utilisateur"""
now = time.time()
user_requests = self.rate_limits.get(user_id, [])
# Nettoyer les requĂȘtes anciennes
recent_requests = [req_time for req_time in user_requests if now - req_time < window]
if len(recent_requests) >= limit:
raise HTTPException(
status_code=429,
detail=f"Rate limit exceeded: {limit} requests per {window} seconds"
)
recent_requests.append(now)
self.rate_limits[user_id] = recent_requests
async def create_chat_completion_with_retry(
self,
messages: List[Dict],
model: str = "yodi-1",
user_id: Optional[str] = None,
**kwargs
):
"""Crée une completion avec retry automatique"""
if user_id:
self.check_rate_limit(user_id)
max_retries = 3
for attempt in range(max_retries):
try:
response = self.client.chat.completions.create(
model=model,
messages=messages,
**kwargs
)
return response
except YodiAPIError as e:
if e.status_code == 429 and attempt < max_retries - 1:
# Backoff exponentiel
await asyncio.sleep(2 ** attempt)
continue
else:
logger.error(f"YODI API Error: {e}")
raise HTTPException(
status_code=e.status_code,
detail=f"YODI API Error: {e.message}"
)
except Exception as e:
logger.error(f"Unexpected error: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
raise HTTPException(status_code=500, detail="Max retries exceeded")
yodi_service = YodiService(os.getenv("YODI_API_KEY"))
IntĂ©gration Frontendâ
Reactâ
Installationâ
npm install axios react-query
Service APIâ
// services/apiService.js
import axios from 'axios';
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:3001';
const apiClient = axios.create({
baseURL: API_BASE_URL,
timeout: 30000,
headers: {
'Content-Type': 'application/json'
}
});
// Intercepteur pour la gestion d'erreurs
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 429) {
throw new Error('Trop de requĂȘtes. Veuillez patienter.');
} else if (error.response?.status >= 500) {
throw new Error('Erreur serveur. Veuillez réessayer.');
}
throw error;
}
);
export const yodiAPI = {
async createChatCompletion(messages, options = {}) {
const response = await apiClient.post('/api/chat', {
messages,
model: 'yodi-1',
...options
});
return response.data;
},
async createEmbeddings(input, model = 'yodi-embed') {
const response = await apiClient.post('/api/embeddings', {
input,
model
});
return response.data;
}
};
Hook personnalisĂ©â
// hooks/useYodiChat.js
import { useState, useCallback } from 'react';
import { useMutation } from 'react-query';
import { yodiAPI } from '../services/apiService';
export const useYodiChat = () => {
const [messages, setMessages] = useState([]);
const [isTyping, setIsTyping] = useState(false);
const chatMutation = useMutation(
({ messages, options }) => yodiAPI.createChatCompletion(messages, options),
{
onMutate: () => {
setIsTyping(true);
},
onSettled: () => {
setIsTyping(false);
}
}
);
const sendMessage = useCallback(async (userMessage, options = {}) => {
const newMessages = [
...messages,
{ role: 'user', content: userMessage }
];
setMessages(newMessages);
try {
const response = await chatMutation.mutateAsync({
messages: newMessages,
options
});
const assistantMessage = response.choices[0].message;
setMessages(prev => [...prev, assistantMessage]);
return assistantMessage;
} catch (error) {
console.error('Erreur lors de l\'envoi du message:', error);
throw error;
}
}, [messages, chatMutation]);
const clearMessages = useCallback(() => {
setMessages([]);
}, []);
return {
messages,
sendMessage,
clearMessages,
isLoading: chatMutation.isLoading,
isTyping,
error: chatMutation.error
};
};
Composant Chatâ
// components/ChatInterface.jsx
import React, { useState } from 'react';
import { useYodiChat } from '../hooks/useYodiChat';
const ChatInterface = () => {
const [input, setInput] = useState('');
const { messages, sendMessage, clearMessages, isLoading, error } = useYodiChat();
const handleSubmit = async (e) => {
e.preventDefault();
if (!input.trim() || isLoading) return;
const userMessage = input.trim();
setInput('');
try {
await sendMessage(userMessage, {
temperature: 0.7,
max_tokens: 500
});
} catch (error) {
alert(`Erreur: ${error.message}`);
}
};
return (
<div className="chat-interface">
<div className="chat-header">
<h2>Chat YODI</h2>
<button onClick={clearMessages} className="clear-btn">
Effacer
</button>
</div>
<div className="chat-messages">
{messages.map((message, index) => (
<div key={index} className={`message ${message.role}`}>
<div className="message-content">
{message.content}
</div>
</div>
))}
{isLoading && (
<div className="message assistant">
<div className="typing-indicator">
YODI écrit...
</div>
</div>
)}
</div>
<form onSubmit={handleSubmit} className="chat-input">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Tapez votre message..."
disabled={isLoading}
className="input-field"
/>
<button type="submit" disabled={isLoading || !input.trim()}>
Envoyer
</button>
</form>
{error && (
<div className="error-message">
Erreur: {error.message}
</div>
)}
</div>
);
};
export default ChatInterface;
Vue.jsâ
Service APIâ
// services/yodiService.js
import axios from 'axios';
class YodiService {
constructor() {
this.client = axios.create({
baseURL: process.env.VUE_APP_API_URL || 'http://localhost:3001',
timeout: 30000
});
this.client.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 429) {
return Promise.reject(new Error('Limite de taux atteinte'));
}
return Promise.reject(error);
}
);
}
async chat(messages, options = {}) {
const response = await this.client.post('/api/chat', {
messages,
model: 'yodi-1',
...options
});
return response.data;
}
async embeddings(input, model = 'yodi-embed') {
const response = await this.client.post('/api/embeddings', {
input,
model
});
return response.data;
}
}
export default new YodiService();
Composableâ
// composables/useYodiChat.js
import { ref, computed } from 'vue';
import yodiService from '../services/yodiService';
export function useYodiChat() {
const messages = ref([]);
const isLoading = ref(false);
const error = ref(null);
const sendMessage = async (userMessage, options = {}) => {
const newMessages = [
...messages.value,
{ role: 'user', content: userMessage }
];
messages.value = newMessages;
isLoading.value = true;
error.value = null;
try {
const response = await yodiService.chat(newMessages, options);
const assistantMessage = response.choices[0].message;
messages.value.push(assistantMessage);
return assistantMessage;
} catch (err) {
error.value = err.message;
throw err;
} finally {
isLoading.value = false;
}
};
const clearMessages = () => {
messages.value = [];
error.value = null;
};
const lastMessage = computed(() => {
return messages.value[messages.value.length - 1];
});
return {
messages: computed(() => messages.value),
isLoading: computed(() => isLoading.value),
error: computed(() => error.value),
lastMessage,
sendMessage,
clearMessages
};
}
Streaming et temps rĂ©elâ
ImplĂ©mentation SSE (Server-Sent Events)â
Backend Node.jsâ
// routes/streaming.js
app.post('/api/chat/stream', async (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*'
});
try {
const { messages, model = 'yodi-1', ...options } = req.body;
const stream = await yodiClient.chat.completions.create({
model,
messages,
stream: true,
...options
});
for await (const chunk of stream) {
if (chunk.choices[0]?.delta?.content) {
const data = JSON.stringify({
content: chunk.choices[0].delta.content
});
res.write(`data: ${data}\n\n`);
}
}
res.write('data: [DONE]\n\n');
res.end();
} catch (error) {
const errorData = JSON.stringify({
error: error.message
});
res.write(`data: ${errorData}\n\n`);
res.end();
}
});
Frontend avec EventSourceâ
// hooks/useStreamingChat.js
import { useState, useCallback } from 'react';
export const useStreamingChat = () => {
const [messages, setMessages] = useState([]);
const [isStreaming, setIsStreaming] = useState(false);
const sendStreamingMessage = useCallback((userMessage, options = {}) => {
return new Promise((resolve, reject) => {
const newMessages = [
...messages,
{ role: 'user', content: userMessage }
];
setMessages(newMessages);
setIsStreaming(true);
const eventSource = new EventSource('/api/chat/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
messages: newMessages,
...options
})
});
let assistantContent = '';
eventSource.onmessage = (event) => {
if (event.data === '[DONE]') {
eventSource.close();
setIsStreaming(false);
resolve(assistantContent);
return;
}
try {
const data = JSON.parse(event.data);
if (data.error) {
eventSource.close();
setIsStreaming(false);
reject(new Error(data.error));
return;
}
if (data.content) {
assistantContent += data.content;
setMessages(prev => {
const lastMessage = prev[prev.length - 1];
if (lastMessage && lastMessage.role === 'assistant') {
// Mettre Ă jour le message existant
return [
...prev.slice(0, -1),
{ ...lastMessage, content: assistantContent }
];
} else {
// Ajouter un nouveau message assistant
return [
...prev,
{ role: 'assistant', content: assistantContent }
];
}
});
}
} catch (error) {
console.error('Erreur parsing SSE:', error);
}
};
eventSource.onerror = (error) => {
eventSource.close();
setIsStreaming(false);
reject(error);
};
});
}, [messages]);
return {
messages,
sendStreamingMessage,
isStreaming
};
};
Configuration de productionâ
Variables d'environnementâ
# .env.production
YODI_API_KEY=prod_your_production_api_key
YODI_BASE_URL=https://api.yodi.tg/v1
ALLOWED_ORIGINS=https://yourapp.com,https://www.yourapp.com
RATE_LIMIT_MAX=100
RATE_LIMIT_WINDOW=3600
LOG_LEVEL=warn
Configuration Dockerâ
# Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3001
CMD ["npm", "start"]
docker-compose.ymlâ
version: '3.8'
services:
yodi-proxy:
build: .
ports:
- "3001:3001"
environment:
- NODE_ENV=production
- YODI_API_KEY=${YODI_API_KEY}
restart: unless-stopped
redis:
image: redis:alpine
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
depends_on:
- yodi-proxy
Monitoring et observabilitĂ©â
MĂ©triques personnalisĂ©esâ
// middleware/metrics.js
const promClient = require('prom-client');
const httpRequestDuration = new promClient.Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status']
});
const yodiApiCalls = new promClient.Counter({
name: 'yodi_api_calls_total',
help: 'Total number of YODI API calls',
labelNames: ['model', 'endpoint', 'status']
});
module.exports = {
httpRequestDuration,
yodiApiCalls,
register: promClient.register
};
Health checksâ
// routes/health.js
app.get('/health', async (req, res) => {
const health = {
status: 'OK',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
checks: {}
};
// Test YODI API
try {
await yodiClient.models.list();
health.checks.yodi_api = 'OK';
} catch (error) {
health.checks.yodi_api = 'ERROR';
health.status = 'DEGRADED';
}
// Test database (si applicable)
try {
// await database.ping();
health.checks.database = 'OK';
} catch (error) {
health.checks.database = 'ERROR';
health.status = 'DEGRADED';
}
const statusCode = health.status === 'OK' ? 200 : 503;
res.status(statusCode).json(health);
});