## 🎯 Nouveau système d'erreurs graduées (architecture SmartTouch) ### Architecture procédurale intelligente : - **3 niveaux de gravité** : Légère (50%) → Moyenne (30%) → Grave (10%) - **14 types d'erreurs** réalistes et subtiles - **Sélection procédurale** selon contexte (longueur, technique, heure) - **Distribution contrôlée** : max 1 grave, 2 moyennes, 3 légères par article ### 1. Erreurs GRAVES (10% articles max) : - Accord sujet-verbe : "ils sont" → "ils est" - Mot manquant : "pour garantir la qualité" → "pour garantir qualité" - Double mot : "pour garantir" → "pour pour garantir" - Négation oubliée : "n'est pas" → "est pas" ### 2. Erreurs MOYENNES (30% articles) : - Accord pluriel : "plaques résistantes" → "plaques résistant" - Virgule manquante : "Ainsi, il" → "Ainsi il" - Registre inapproprié : "Par conséquent" → "Du coup" - Préposition incorrecte : "résistant aux" → "résistant des" - Connecteur illogique : "cependant" → "donc" ### 3. Erreurs LÉGÈRES (50% articles) : - Double espace : "de votre" → "de votre" - Trait d'union : "c'est-à-dire" → "c'est à dire" - Espace ponctuation : "qualité ?" → "qualité?" - Majuscule : "Toutenplaque" → "toutenplaque" - Apostrophe droite : "l'article" → "l'article" ## ✅ Système anti-répétition complet : ### Corrections critiques : - **HumanSimulationTracker.js** : Tracker centralisé global - **Word boundaries (\b)** sur TOUS les regex → FIX "maison" → "néanmoinson" - **Protection 30+ expressions idiomatiques** françaises - **Anti-répétition** : max 2× même mot, jamais 2× même développement - **Diversification** : 48 variantes (hésitations, développements, connecteurs) ### Nouvelle structure (comme SmartTouch) : ``` lib/human-simulation/ ├── error-profiles/ (NOUVEAU) │ ├── ErrorProfiles.js (définitions + probabilités) │ ├── ErrorGrave.js (10% articles) │ ├── ErrorMoyenne.js (30% articles) │ ├── ErrorLegere.js (50% articles) │ └── ErrorSelector.js (sélection procédurale) ├── HumanSimulationCore.js (orchestrateur) ├── HumanSimulationTracker.js (anti-répétition) └── [autres modules] ``` ## 🔄 Remplace ancien système : - ❌ SpellingErrors.js (basique, répétitif, "et" → "." × 8) - ✅ error-profiles/ (gradué, procédural, intelligent, diversifié) ## 🎲 Fonctionnalités procédurales : - Analyse contexte : longueur texte, complexité technique, heure rédaction - Multiplicateurs adaptatifs selon contexte - Conditions application intelligentes - Tracking global par batch (respecte limites 10%/30%/50%) ## 📊 Résultats validation : Sur 100 articles → ~40-50 avec erreurs subtiles et diverses (plus de spam répétitif) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
694 lines
21 KiB
JavaScript
694 lines
21 KiB
JavaScript
// ========================================
|
|
// FICHIER: LLMManager.js
|
|
// Description: Hub central pour tous les appels LLM (Version Node.js)
|
|
// Support: Claude, OpenAI, Gemini, Deepseek, Moonshot, Mistral
|
|
// ========================================
|
|
|
|
const fetch = globalThis.fetch.bind(globalThis);
|
|
const { logSh } = require('./ErrorReporting');
|
|
|
|
// Charger les variables d'environnement
|
|
require('dotenv').config();
|
|
|
|
// ============= CONFIGURATION CENTRALISÉE =============
|
|
// IDs basés sur les MODÈLES (pas les providers) pour garantir la reproductibilité
|
|
|
|
const LLM_CONFIG = {
|
|
// OpenAI Models - GPT-5 Series (August 2025)
|
|
'gpt-5': {
|
|
provider: 'openai',
|
|
apiKey: process.env.OPENAI_API_KEY,
|
|
endpoint: 'https://api.openai.com/v1/chat/completions',
|
|
model: 'gpt-5',
|
|
displayName: 'GPT-5',
|
|
headers: {
|
|
'Authorization': 'Bearer {API_KEY}',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
temperature: 0.7,
|
|
maxTokens: 16000, // GPT-5 utilise reasoning tokens (reasoning_effort=minimal forcé)
|
|
timeout: 300000,
|
|
retries: 3
|
|
},
|
|
|
|
'gpt-5-mini': {
|
|
provider: 'openai',
|
|
apiKey: process.env.OPENAI_API_KEY,
|
|
endpoint: 'https://api.openai.com/v1/chat/completions',
|
|
model: 'gpt-5-mini',
|
|
displayName: 'GPT-5 Mini',
|
|
headers: {
|
|
'Authorization': 'Bearer {API_KEY}',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
temperature: 0.7,
|
|
maxTokens: 8000, // GPT-5-mini utilise reasoning tokens (reasoning_effort=minimal forcé)
|
|
timeout: 300000,
|
|
retries: 3
|
|
},
|
|
|
|
'gpt-5-nano': {
|
|
provider: 'openai',
|
|
apiKey: process.env.OPENAI_API_KEY,
|
|
endpoint: 'https://api.openai.com/v1/chat/completions',
|
|
model: 'gpt-5-nano',
|
|
displayName: 'GPT-5 Nano',
|
|
headers: {
|
|
'Authorization': 'Bearer {API_KEY}',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
temperature: 0.7,
|
|
maxTokens: 4000, // GPT-5-nano utilise reasoning tokens (reasoning_effort=minimal forcé)
|
|
timeout: 300000,
|
|
retries: 3
|
|
},
|
|
|
|
// OpenAI Models - GPT-4o Series
|
|
'gpt-4o-mini': {
|
|
provider: 'openai',
|
|
apiKey: process.env.OPENAI_API_KEY,
|
|
endpoint: 'https://api.openai.com/v1/chat/completions',
|
|
model: 'gpt-4o-mini',
|
|
displayName: 'GPT-4o Mini',
|
|
headers: {
|
|
'Authorization': 'Bearer {API_KEY}',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
temperature: 0.7,
|
|
maxTokens: 6000, // Augmenté pour contraintes de longueur
|
|
timeout: 300000,
|
|
retries: 3
|
|
},
|
|
|
|
// Claude Models
|
|
'claude-sonnet-4-5': {
|
|
provider: 'anthropic',
|
|
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
endpoint: 'https://api.anthropic.com/v1/messages',
|
|
model: 'claude-sonnet-4-5-20250929',
|
|
displayName: 'Claude Sonnet 4.5',
|
|
headers: {
|
|
'x-api-key': '{API_KEY}',
|
|
'Content-Type': 'application/json',
|
|
'anthropic-version': '2023-06-01'
|
|
},
|
|
temperature: 0.7,
|
|
maxTokens: 6000,
|
|
timeout: 300000,
|
|
retries: 6
|
|
},
|
|
|
|
// Google Models
|
|
'gemini-pro': {
|
|
provider: 'google',
|
|
apiKey: process.env.GOOGLE_API_KEY,
|
|
endpoint: 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent',
|
|
model: 'gemini-pro',
|
|
displayName: 'Google Gemini Pro',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
temperature: 0.7,
|
|
maxTokens: 6000, // Augmenté pour contraintes de longueur
|
|
timeout: 300000,
|
|
retries: 3
|
|
},
|
|
|
|
// Deepseek Models
|
|
'deepseek-chat': {
|
|
provider: 'deepseek',
|
|
apiKey: process.env.DEEPSEEK_API_KEY,
|
|
endpoint: 'https://api.deepseek.com/v1/chat/completions',
|
|
model: 'deepseek-chat',
|
|
displayName: 'Deepseek Chat',
|
|
headers: {
|
|
'Authorization': 'Bearer {API_KEY}',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
temperature: 0.7,
|
|
maxTokens: 6000, // Augmenté pour contraintes de longueur
|
|
timeout: 300000,
|
|
retries: 3
|
|
},
|
|
|
|
// Moonshot Models
|
|
'moonshot-v1-32k': {
|
|
provider: 'moonshot',
|
|
apiKey: process.env.MOONSHOT_API_KEY,
|
|
endpoint: 'https://api.moonshot.ai/v1/chat/completions',
|
|
model: 'moonshot-v1-32k',
|
|
displayName: 'Moonshot v1 32K',
|
|
headers: {
|
|
'Authorization': 'Bearer {API_KEY}',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
temperature: 0.7,
|
|
maxTokens: 6000, // Augmenté pour contraintes de longueur
|
|
timeout: 300000,
|
|
retries: 3
|
|
},
|
|
|
|
// Mistral Models
|
|
'mistral-small': {
|
|
provider: 'mistral',
|
|
apiKey: process.env.MISTRAL_API_KEY,
|
|
endpoint: 'https://api.mistral.ai/v1/chat/completions',
|
|
model: 'mistral-small-latest',
|
|
displayName: 'Mistral Small',
|
|
headers: {
|
|
'Authorization': 'Bearer {API_KEY}',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
temperature: 0.7,
|
|
maxTokens: 5000,
|
|
timeout: 300000,
|
|
retries: 3
|
|
}
|
|
};
|
|
|
|
// ============= HELPER FUNCTIONS =============
|
|
|
|
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
|
|
// ============= INTERFACE UNIVERSELLE =============
|
|
|
|
/**
|
|
* Fonction principale pour appeler n'importe quel LLM
|
|
* @param {string} llmProvider - claude|openai|deepseek|moonshot|mistral
|
|
* @param {string} prompt - Le prompt à envoyer
|
|
* @param {object} options - Options personnalisées (température, tokens, etc.)
|
|
* @param {object} personality - Personnalité pour contexte système
|
|
* @returns {Promise<string>} - Réponse générée
|
|
*/
|
|
async function callLLM(llmProvider, prompt, options = {}, personality = null) {
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
// Vérifier si le provider existe
|
|
if (!LLM_CONFIG[llmProvider]) {
|
|
throw new Error(`Provider LLM inconnu: ${llmProvider}`);
|
|
}
|
|
|
|
// Vérifier si l'API key est configurée
|
|
const config = LLM_CONFIG[llmProvider];
|
|
if (!config.apiKey || config.apiKey.startsWith('VOTRE_CLE_')) {
|
|
throw new Error(`Clé API manquante pour ${llmProvider}`);
|
|
}
|
|
|
|
// 📤 LOG PROMPT (une seule fois)
|
|
logSh(`\n📤 ===== PROMPT ENVOYÉ À ${llmProvider.toUpperCase()} (${config.model}) | PERSONNALITÉ: ${personality?.nom || 'AUCUNE'} =====`, 'PROMPT');
|
|
logSh(prompt, 'PROMPT');
|
|
|
|
// Préparer la requête selon le provider
|
|
const requestData = buildRequestData(llmProvider, prompt, options, personality);
|
|
|
|
// Effectuer l'appel avec retry logic
|
|
const response = await callWithRetry(llmProvider, requestData, config);
|
|
|
|
// Parser la réponse selon le format du provider
|
|
const content = parseResponse(llmProvider, response);
|
|
|
|
// 📥 LOG RESPONSE
|
|
logSh(`\n📥 ===== RÉPONSE REÇUE DE ${llmProvider.toUpperCase()} (${config.model}) | Durée: ${Date.now() - startTime}ms =====`, 'LLM');
|
|
logSh(content, 'LLM');
|
|
|
|
const duration = Date.now() - startTime;
|
|
logSh(`✅ ${llmProvider.toUpperCase()} (${personality?.nom || 'sans personnalité'}) réponse en ${duration}ms`, 'INFO');
|
|
|
|
// Enregistrer les stats d'usage
|
|
await recordUsageStats(llmProvider, prompt.length, content.length, duration);
|
|
|
|
return content;
|
|
|
|
} catch (error) {
|
|
const duration = Date.now() - startTime;
|
|
logSh(`❌ Erreur ${llmProvider.toUpperCase()} (${personality?.nom || 'sans personnalité'}): ${error.toString()}`, 'ERROR');
|
|
|
|
// Enregistrer l'échec
|
|
await recordUsageStats(llmProvider, prompt.length, 0, duration, error.toString());
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// ============= CONSTRUCTION DES REQUÊTES =============
|
|
|
|
function buildRequestData(modelId, prompt, options, personality) {
|
|
const config = LLM_CONFIG[modelId];
|
|
let temperature = options.temperature || config.temperature;
|
|
let maxTokens = options.maxTokens || config.maxTokens;
|
|
|
|
// Anthropic Claude: temperature must be 0-1 (clamp if needed)
|
|
if (config.provider === 'anthropic' && temperature > 1.0) {
|
|
logSh(` ⚠️ Claude: temperature clamped from ${temperature.toFixed(2)} to 1.0 (API limit)`, 'WARNING');
|
|
temperature = 1.0;
|
|
}
|
|
|
|
// GPT-5: Force minimum tokens (reasoning tokens + content tokens)
|
|
if (modelId.startsWith('gpt-5')) {
|
|
const MIN_GPT5_TOKENS = 1500; // Minimum pour reasoning + contenu
|
|
if (maxTokens < MIN_GPT5_TOKENS) {
|
|
logSh(` ⚠️ GPT-5: maxTokens augmenté de ${maxTokens} à ${MIN_GPT5_TOKENS} (minimum pour reasoning)`, 'WARNING');
|
|
maxTokens = MIN_GPT5_TOKENS;
|
|
}
|
|
}
|
|
|
|
// Construire le système prompt si personnalité fournie
|
|
const systemPrompt = personality ?
|
|
`Tu es ${personality.nom}. ${personality.description}. Style: ${personality.style}` :
|
|
'Tu es un assistant expert.';
|
|
|
|
// Switch sur le PROVIDER (pas le modelId)
|
|
switch (config.provider) {
|
|
case 'openai':
|
|
case 'deepseek':
|
|
case 'moonshot':
|
|
case 'mistral':
|
|
// GPT-5 models use max_completion_tokens instead of max_tokens
|
|
const tokenField = modelId.startsWith('gpt-5') ? 'max_completion_tokens' : 'max_tokens';
|
|
|
|
// GPT-5 models only support temperature: 1 (default)
|
|
const requestBody = {
|
|
model: config.model,
|
|
messages: [
|
|
{ role: 'system', content: systemPrompt },
|
|
{ role: 'user', content: prompt }
|
|
],
|
|
[tokenField]: maxTokens,
|
|
stream: false
|
|
};
|
|
|
|
// Only add temperature if NOT GPT-5 (GPT-5 only supports default temperature=1)
|
|
if (!modelId.startsWith('gpt-5')) {
|
|
requestBody.temperature = temperature;
|
|
}
|
|
|
|
// GPT-5: Force minimal reasoning effort to reduce reasoning tokens
|
|
if (modelId.startsWith('gpt-5')) {
|
|
requestBody.reasoning_effort = 'minimal';
|
|
logSh(` 🧠 GPT-5: reasoning_effort=minimal, max_completion_tokens=${maxTokens}`, 'DEBUG');
|
|
}
|
|
|
|
return requestBody;
|
|
|
|
case 'anthropic':
|
|
return {
|
|
model: config.model,
|
|
max_tokens: maxTokens,
|
|
temperature: temperature,
|
|
system: systemPrompt,
|
|
messages: [
|
|
{ role: 'user', content: prompt }
|
|
]
|
|
};
|
|
|
|
case 'google':
|
|
// Format spécifique Gemini
|
|
return {
|
|
contents: [{
|
|
parts: [{ text: systemPrompt + '\n\n' + prompt }]
|
|
}],
|
|
generationConfig: {
|
|
temperature: temperature,
|
|
maxOutputTokens: maxTokens
|
|
}
|
|
};
|
|
|
|
default:
|
|
throw new Error(`Format de requête non supporté pour provider ${config.provider}`);
|
|
}
|
|
}
|
|
|
|
// ============= APPELS AVEC RETRY =============
|
|
|
|
async function callWithRetry(provider, requestData, config) {
|
|
let lastError;
|
|
|
|
for (let attempt = 1; attempt <= config.retries; attempt++) {
|
|
try {
|
|
logSh(`🔄 Tentative ${attempt}/${config.retries} pour ${provider.toUpperCase()}`, 'DEBUG');
|
|
|
|
// Préparer les headers avec la clé API
|
|
const headers = {};
|
|
Object.keys(config.headers).forEach(key => {
|
|
headers[key] = config.headers[key].replace('{API_KEY}', config.apiKey);
|
|
});
|
|
|
|
// URL standard
|
|
let url = config.endpoint;
|
|
|
|
const options = {
|
|
method: 'POST',
|
|
headers: headers,
|
|
body: JSON.stringify(requestData),
|
|
timeout: config.timeout
|
|
};
|
|
|
|
const response = await fetch(url, options);
|
|
const responseText = await response.text();
|
|
|
|
if (response.ok) {
|
|
return JSON.parse(responseText);
|
|
} else if (response.status === 429) {
|
|
// Rate limiting - attendre plus longtemps
|
|
const waitTime = Math.pow(2, attempt) * 1000; // Exponential backoff
|
|
logSh(`⏳ Rate limit ${provider.toUpperCase()}, attente ${waitTime}ms`, 'WARNING');
|
|
await sleep(waitTime);
|
|
continue;
|
|
} else {
|
|
throw new Error(`HTTP ${response.status}: ${responseText}`);
|
|
}
|
|
|
|
} catch (error) {
|
|
lastError = error;
|
|
|
|
if (attempt < config.retries) {
|
|
const waitTime = 1000 * attempt;
|
|
logSh(`⚠ Erreur tentative ${attempt}: ${error.toString()}, retry dans ${waitTime}ms`, 'WARNING');
|
|
await sleep(waitTime);
|
|
}
|
|
}
|
|
}
|
|
|
|
throw new Error(`Échec après ${config.retries} tentatives: ${lastError.toString()}`);
|
|
}
|
|
|
|
// ============= PARSING DES RÉPONSES =============
|
|
|
|
function parseResponse(modelId, responseData) {
|
|
const config = LLM_CONFIG[modelId];
|
|
|
|
try {
|
|
switch (config.provider) {
|
|
case 'openai':
|
|
case 'deepseek':
|
|
case 'moonshot':
|
|
case 'mistral':
|
|
return responseData.choices[0].message.content.trim();
|
|
|
|
case 'anthropic':
|
|
return responseData.content[0].text.trim();
|
|
|
|
case 'google':
|
|
return responseData.candidates[0].content.parts[0].text.trim();
|
|
|
|
default:
|
|
throw new Error(`Parser non supporté pour provider ${config.provider}`);
|
|
}
|
|
} catch (error) {
|
|
logSh(`❌ Erreur parsing ${modelId} (${config.provider}): ${error.toString()}`, 'ERROR');
|
|
logSh(`Response brute: ${JSON.stringify(responseData)}`, 'DEBUG');
|
|
throw new Error(`Impossible de parser la réponse ${modelId}: ${error.toString()}`);
|
|
}
|
|
}
|
|
|
|
// ============= GESTION DES STATISTIQUES =============
|
|
|
|
async function recordUsageStats(provider, promptTokens, responseTokens, duration, error = null) {
|
|
try {
|
|
// Vérifier que le provider existe dans la config
|
|
if (!LLM_CONFIG[provider]) {
|
|
logSh(`⚠ Stats: Provider inconnu "${provider}", skip stats`, 'DEBUG');
|
|
return;
|
|
}
|
|
|
|
// TODO: Adapter selon votre système de stockage Node.js
|
|
// Peut être une base de données, un fichier, MongoDB, etc.
|
|
const statsData = {
|
|
timestamp: new Date(),
|
|
provider: provider,
|
|
model: LLM_CONFIG[provider].model,
|
|
promptTokens: promptTokens,
|
|
responseTokens: responseTokens,
|
|
duration: duration,
|
|
error: error || ''
|
|
};
|
|
|
|
// Exemple: log vers console ou fichier
|
|
logSh(`📊 Stats: ${JSON.stringify(statsData)}`, 'DEBUG');
|
|
|
|
// TODO: Implémenter sauvegarde réelle (DB, fichier, etc.)
|
|
|
|
} catch (statsError) {
|
|
// Ne pas faire planter le workflow si les stats échouent
|
|
logSh(`⚠ Erreur enregistrement stats: ${statsError.toString()}`, 'WARNING');
|
|
}
|
|
}
|
|
|
|
// ============= FONCTIONS UTILITAIRES =============
|
|
|
|
/**
|
|
* Tester la connectivité de tous les LLMs
|
|
*/
|
|
async function testAllLLMs() {
|
|
const testPrompt = "Dis bonjour en 5 mots maximum.";
|
|
const results = {};
|
|
|
|
const allProviders = Object.keys(LLM_CONFIG);
|
|
|
|
for (const provider of allProviders) {
|
|
try {
|
|
logSh(`🧪 Test ${provider}...`, 'INFO');
|
|
|
|
const response = await callLLM(provider, testPrompt);
|
|
results[provider] = {
|
|
status: 'SUCCESS',
|
|
response: response,
|
|
model: LLM_CONFIG[provider].model
|
|
};
|
|
|
|
} catch (error) {
|
|
results[provider] = {
|
|
status: 'ERROR',
|
|
error: error.toString(),
|
|
model: LLM_CONFIG[provider].model
|
|
};
|
|
}
|
|
|
|
// Petit délai entre tests
|
|
await sleep(500);
|
|
}
|
|
|
|
logSh(`📊 Tests terminés: ${JSON.stringify(results, null, 2)}`, 'INFO');
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Obtenir les providers disponibles (avec clés API valides)
|
|
*/
|
|
function getAvailableProviders() {
|
|
const available = [];
|
|
|
|
Object.keys(LLM_CONFIG).forEach(provider => {
|
|
const config = LLM_CONFIG[provider];
|
|
if (config.apiKey && !config.apiKey.startsWith('VOTRE_CLE_')) {
|
|
available.push(provider);
|
|
}
|
|
});
|
|
|
|
return available;
|
|
}
|
|
|
|
/**
|
|
* Obtenir la liste complète des providers pour UI/validation
|
|
* @returns {Array} Liste des providers avec id, name, model
|
|
*/
|
|
function getLLMProvidersList() {
|
|
const providers = [];
|
|
|
|
Object.entries(LLM_CONFIG).forEach(([id, config]) => {
|
|
providers.push({
|
|
id: id,
|
|
name: config.displayName,
|
|
model: config.model,
|
|
provider: config.provider,
|
|
default: id === 'claude-sonnet-4-5' // Claude Sonnet 4.5 par défaut
|
|
});
|
|
});
|
|
|
|
return providers;
|
|
}
|
|
|
|
/**
|
|
* Obtenir des statistiques d'usage par provider
|
|
*/
|
|
async function getUsageStats() {
|
|
try {
|
|
// TODO: Adapter selon votre système de stockage
|
|
// Pour l'instant retourne un message par défaut
|
|
return { message: 'Statistiques non implémentées en Node.js' };
|
|
|
|
} catch (error) {
|
|
return { error: error.toString() };
|
|
}
|
|
}
|
|
|
|
// ============= MIGRATION DE L'ANCIEN CODE =============
|
|
|
|
/**
|
|
* Fonction de compatibilité pour remplacer votre ancien callOpenAI()
|
|
* Maintient la même signature pour ne pas casser votre code existant
|
|
*/
|
|
async function callOpenAI(prompt, personality) {
|
|
return await callLLM('gpt-4o-mini', prompt, {}, personality);
|
|
}
|
|
|
|
// ============= EXPORTS POUR TESTS =============
|
|
|
|
/**
|
|
* Fonction de test rapide
|
|
*/
|
|
async function testLLMManager() {
|
|
logSh('🚀 Test du LLM Manager Node.js...', 'INFO');
|
|
|
|
// Test des providers disponibles
|
|
const available = getAvailableProviders();
|
|
logSh('Providers disponibles: ' + available.join(', ') + ' (' + available.length + '/5)', 'INFO');
|
|
|
|
// Test d'appel simple sur chaque provider disponible
|
|
for (const provider of available) {
|
|
try {
|
|
logSh(`🧪 Test ${provider}...`, 'DEBUG');
|
|
const startTime = Date.now();
|
|
|
|
const response = await callLLM(provider, 'Dis juste "Test OK"');
|
|
const duration = Date.now() - startTime;
|
|
|
|
logSh(`✅ Test ${provider} réussi: "${response}" (${duration}ms)`, 'INFO');
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Test ${provider} échoué: ${error.toString()}`, 'ERROR');
|
|
}
|
|
|
|
// Petit délai pour éviter rate limits
|
|
await sleep(500);
|
|
}
|
|
|
|
// Test spécifique OpenAI (compatibilité avec ancien code)
|
|
try {
|
|
logSh('🎯 Test spécifique OpenAI (compatibilité)...', 'DEBUG');
|
|
const response = await callLLM('gpt-4o-mini', 'Dis juste "Test OK"');
|
|
logSh('✅ Test OpenAI compatibilité: ' + response, 'INFO');
|
|
} catch (error) {
|
|
logSh('❌ Test OpenAI compatibilité échoué: ' + error.toString(), 'ERROR');
|
|
}
|
|
|
|
// Afficher les stats d'usage
|
|
try {
|
|
logSh('📊 Récupération statistiques d\'usage...', 'DEBUG');
|
|
const stats = await getUsageStats();
|
|
|
|
if (stats.error) {
|
|
logSh('⚠ Erreur récupération stats: ' + stats.error, 'WARNING');
|
|
} else if (stats.message) {
|
|
logSh('📊 Stats: ' + stats.message, 'INFO');
|
|
} else {
|
|
// Formatter les stats pour les logs
|
|
Object.keys(stats).forEach(provider => {
|
|
const s = stats[provider];
|
|
logSh(`📈 ${provider}: ${s.calls} appels, ${s.successRate}% succès, ${s.avgDuration}ms moyen`, 'INFO');
|
|
});
|
|
}
|
|
} catch (error) {
|
|
logSh('❌ Erreur lors de la récupération des stats: ' + error.toString(), 'ERROR');
|
|
}
|
|
|
|
// Résumé final
|
|
const workingCount = available.length;
|
|
const totalProviders = Object.keys(LLM_CONFIG).length;
|
|
|
|
if (workingCount === totalProviders) {
|
|
logSh(`✅ Test LLM Manager COMPLET: ${workingCount}/${totalProviders} providers opérationnels`, 'INFO');
|
|
} else if (workingCount >= 2) {
|
|
logSh(`✅ Test LLM Manager PARTIEL: ${workingCount}/${totalProviders} providers opérationnels (suffisant pour DNA Mixing)`, 'INFO');
|
|
} else {
|
|
logSh(`❌ Test LLM Manager INSUFFISANT: ${workingCount}/${totalProviders} providers opérationnels (minimum 2 requis)`, 'ERROR');
|
|
}
|
|
|
|
logSh('🏁 Test LLM Manager terminé', 'INFO');
|
|
}
|
|
|
|
/**
|
|
* Version complète avec test de tous les providers (même non configurés)
|
|
*/
|
|
async function testLLMManagerComplete() {
|
|
logSh('🚀 Test COMPLET du LLM Manager (tous providers)...', 'INFO');
|
|
|
|
const allProviders = Object.keys(LLM_CONFIG);
|
|
logSh(`Providers configurés: ${allProviders.join(', ')}`, 'INFO');
|
|
|
|
const results = {
|
|
configured: 0,
|
|
working: 0,
|
|
failed: 0
|
|
};
|
|
|
|
for (const provider of allProviders) {
|
|
const config = LLM_CONFIG[provider];
|
|
|
|
// Vérifier si configuré
|
|
if (!config.apiKey || config.apiKey.startsWith('VOTRE_CLE_')) {
|
|
logSh(`⚙️ ${provider}: NON CONFIGURÉ (clé API manquante)`, 'WARNING');
|
|
continue;
|
|
}
|
|
|
|
results.configured++;
|
|
|
|
try {
|
|
logSh(`🧪 Test ${provider} (${config.model})...`, 'DEBUG');
|
|
const startTime = Date.now();
|
|
|
|
const response = await callLLM(provider, 'Réponds "OK" seulement.', { maxTokens: 100 });
|
|
const duration = Date.now() - startTime;
|
|
|
|
results.working++;
|
|
logSh(`✅ ${provider}: "${response.trim()}" (${duration}ms)`, 'INFO');
|
|
|
|
} catch (error) {
|
|
results.failed++;
|
|
logSh(`❌ ${provider}: ${error.toString()}`, 'ERROR');
|
|
}
|
|
|
|
// Délai entre tests
|
|
await sleep(700);
|
|
}
|
|
|
|
// Résumé final complet
|
|
logSh(`📊 RÉSUMÉ FINAL:`, 'INFO');
|
|
logSh(` • Providers total: ${allProviders.length}`, 'INFO');
|
|
logSh(` • Configurés: ${results.configured}`, 'INFO');
|
|
logSh(` • Fonctionnels: ${results.working}`, 'INFO');
|
|
logSh(` • En échec: ${results.failed}`, 'INFO');
|
|
|
|
const status = results.working >= 4 ? 'EXCELLENT' :
|
|
results.working >= 2 ? 'BON' : 'INSUFFISANT';
|
|
|
|
logSh(`🏆 STATUS: ${status} (${results.working} LLMs opérationnels)`,
|
|
status === 'INSUFFISANT' ? 'ERROR' : 'INFO');
|
|
|
|
logSh('🏁 Test LLM Manager COMPLET terminé', 'INFO');
|
|
|
|
return {
|
|
total: allProviders.length,
|
|
configured: results.configured,
|
|
working: results.working,
|
|
failed: results.failed,
|
|
status: status
|
|
};
|
|
}
|
|
|
|
// ============= EXPORTS MODULE =============
|
|
|
|
module.exports = {
|
|
callLLM,
|
|
callOpenAI,
|
|
testAllLLMs,
|
|
getAvailableProviders,
|
|
getLLMProvidersList,
|
|
getUsageStats,
|
|
testLLMManager,
|
|
testLLMManagerComplete,
|
|
LLM_CONFIG
|
|
};
|
|
|