Aller au contenu principal

Guide d'intégration

Ce guide vous accompagne dans l'intégration complète de l'API YODI dans votre application.

Vue d'ensemble 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);
});

Prochaines étapes