Suppression rate limiters inutiles et optimisation polling LLM
Problèmes résolus: - Polling toutes les 3s vers /api/llm/limit (1200 req/h par user) - Rate limiters par IP bloquaient légitimement les traductions - globalLimiter (200/15min) et translationLimiter (10/min) redondants Changements: - Suppression setInterval 3s dans index.html - Mise à jour compteur LLM uniquement après traductions - Suppression globalLimiter et translationLimiter - Garde uniquement checkLLMLimit() (par API key, 20/jour) - Fix affichage: utilise data.remaining de l'API Résultat: système de quotas simple, clair et fonctionnel 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
f2143bb10b
commit
272a05b3fe
@ -884,9 +884,8 @@
|
|||||||
} else {
|
} else {
|
||||||
// User with limited requests
|
// User with limited requests
|
||||||
counter.style.display = 'block';
|
counter.style.display = 'block';
|
||||||
const used = data.used || 0;
|
|
||||||
const limit = data.limit || 20;
|
const limit = data.limit || 20;
|
||||||
const remaining = limit - used;
|
const remaining = data.remaining !== undefined ? data.remaining : (limit - (data.used || 0));
|
||||||
text.textContent = `Requêtes LLM restantes: ${remaining}/${limit}`;
|
text.textContent = `Requêtes LLM restantes: ${remaining}/${limit}`;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -953,23 +952,8 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Refresh LLM limit counter every 3 seconds if logged in
|
// Note: LLM limit counter is updated after each translation
|
||||||
let limitErrorCount = 0;
|
// No need for automatic polling every few seconds
|
||||||
const limitInterval = setInterval(() => {
|
|
||||||
if (getApiKey()) {
|
|
||||||
updateLLMLimit().catch(err => {
|
|
||||||
limitErrorCount++;
|
|
||||||
// Stop polling after 5 consecutive errors
|
|
||||||
if (limitErrorCount >= 5) {
|
|
||||||
console.warn('Too many errors loading LLM limit, stopping auto-refresh');
|
|
||||||
clearInterval(limitInterval);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Reset error count if not logged in
|
|
||||||
limitErrorCount = 0;
|
|
||||||
}
|
|
||||||
}, 3000);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Authenticated fetch wrapper with auto-logout on 401/403
|
// Authenticated fetch wrapper with auto-logout on 401/403
|
||||||
@ -1421,6 +1405,10 @@
|
|||||||
} else {
|
} else {
|
||||||
document.getElementById('layer1-content').innerHTML = `<span class="error">Erreur: ${data.error}</span>`;
|
document.getElementById('layer1-content').innerHTML = `<span class="error">Erreur: ${data.error}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update LLM limit counter after translation
|
||||||
|
updateLLMLimit().catch(err => console.error('Error updating LLM limit:', err));
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
document.getElementById('layer1-content').innerHTML = `<span class="error">Erreur: ${error.message}</span>`;
|
document.getElementById('layer1-content').innerHTML = `<span class="error">Erreur: ${error.message}</span>`;
|
||||||
} finally {
|
} finally {
|
||||||
@ -1533,6 +1521,10 @@
|
|||||||
} else {
|
} else {
|
||||||
document.getElementById('cf2fr-layer1-content').innerHTML = `<span class="error">Erreur LLM: ${llmData.error}</span>`;
|
document.getElementById('cf2fr-layer1-content').innerHTML = `<span class="error">Erreur LLM: ${llmData.error}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update LLM limit counter after translation
|
||||||
|
updateLLMLimit().catch(err => console.error('Error updating LLM limit:', err));
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
document.getElementById('cf2fr-layer1-content').innerHTML = `<span class="error">Erreur: ${error.message}</span>`;
|
document.getElementById('cf2fr-layer1-content').innerHTML = `<span class="error">Erreur: ${error.message}</span>`;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -1,32 +1,8 @@
|
|||||||
const rateLimit = require('express-rate-limit');
|
const rateLimit = require('express-rate-limit');
|
||||||
|
|
||||||
// Rate limiter global par IP
|
|
||||||
const globalLimiter = rateLimit({
|
|
||||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
||||||
max: 200, // max 200 requêtes par IP
|
|
||||||
standardHeaders: true,
|
|
||||||
legacyHeaders: false,
|
|
||||||
skip: (req) => {
|
|
||||||
// Skip pour les endpoints qui doivent être appelés très fréquemment
|
|
||||||
return req.path === '/api/llm/limit';
|
|
||||||
},
|
|
||||||
message: { error: 'Too many requests from this IP, please try again later.' }
|
|
||||||
});
|
|
||||||
|
|
||||||
// Rate limiter pour les traductions (plus strict)
|
|
||||||
const translationLimiter = rateLimit({
|
|
||||||
windowMs: 60 * 1000, // 1 minute
|
|
||||||
max: 10, // max 10 traductions par minute
|
|
||||||
standardHeaders: true,
|
|
||||||
legacyHeaders: false,
|
|
||||||
skip: (req) => {
|
|
||||||
// Skip si l'utilisateur est admin
|
|
||||||
return req.user && req.user.role === 'admin';
|
|
||||||
},
|
|
||||||
message: { error: 'Too many translation requests. Please wait a moment.' }
|
|
||||||
});
|
|
||||||
|
|
||||||
// Rate limiter pour les endpoints sensibles (admin)
|
// Rate limiter pour les endpoints sensibles (admin)
|
||||||
|
// Note: Pour les traductions et requêtes LLM, on utilise checkLLMLimit() dans auth.js
|
||||||
|
// qui gère les limites par API key (plus flexible et précis que les rate limiters par IP)
|
||||||
const adminLimiter = rateLimit({
|
const adminLimiter = rateLimit({
|
||||||
windowMs: 5 * 60 * 1000, // 5 minutes
|
windowMs: 5 * 60 * 1000, // 5 minutes
|
||||||
max: 50,
|
max: 50,
|
||||||
@ -36,7 +12,5 @@ const adminLimiter = rateLimit({
|
|||||||
});
|
});
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
globalLimiter,
|
|
||||||
translationLimiter,
|
|
||||||
adminLimiter
|
adminLimiter
|
||||||
};
|
};
|
||||||
|
|||||||
@ -17,7 +17,7 @@ const { translateConfluentToFrench, translateConfluentDetailed } = require('./co
|
|||||||
|
|
||||||
// Security modules
|
// Security modules
|
||||||
const { authenticate, requireAdmin, createToken, listTokens, disableToken, enableToken, deleteToken, getGlobalStats, trackLLMUsage, checkLLMLimit } = require('./auth');
|
const { authenticate, requireAdmin, createToken, listTokens, disableToken, enableToken, deleteToken, getGlobalStats, trackLLMUsage, checkLLMLimit } = require('./auth');
|
||||||
const { globalLimiter, translationLimiter, adminLimiter } = require('./rateLimiter');
|
const { adminLimiter } = require('./rateLimiter');
|
||||||
const { requestLogger, getLogs, getLogStats } = require('./logger');
|
const { requestLogger, getLogs, getLogStats } = require('./logger');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
@ -26,7 +26,7 @@ const PORT = process.env.PORT || 3000;
|
|||||||
// Middlewares
|
// Middlewares
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(requestLogger); // Log toutes les requêtes
|
app.use(requestLogger); // Log toutes les requêtes
|
||||||
app.use(globalLimiter); // Rate limiting global
|
// Rate limiting: on utilise uniquement checkLLMLimit() par API key, pas de rate limit global par IP
|
||||||
|
|
||||||
// Route protégée pour admin.html (AVANT express.static)
|
// Route protégée pour admin.html (AVANT express.static)
|
||||||
// Vérifie l'auth seulement si API key présente, sinon laisse passer (le JS client vérifiera)
|
// Vérifie l'auth seulement si API key présente, sinon laisse passer (le JS client vérifiera)
|
||||||
@ -378,7 +378,7 @@ app.post('/api/analyze/coverage', authenticate, (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Translation endpoint (NOUVEAU SYSTÈME CONTEXTUEL)
|
// Translation endpoint (NOUVEAU SYSTÈME CONTEXTUEL)
|
||||||
app.post('/translate', authenticate, translationLimiter, async (req, res) => {
|
app.post('/translate', authenticate, async (req, res) => {
|
||||||
const { text, target, provider, model, temperature = 1.0, useLexique = true, customAnthropicKey, customOpenAIKey } = req.body;
|
const { text, target, provider, model, temperature = 1.0, useLexique = true, customAnthropicKey, customOpenAIKey } = req.body;
|
||||||
|
|
||||||
if (!text || !target || !provider || !model) {
|
if (!text || !target || !provider || !model) {
|
||||||
@ -586,7 +586,7 @@ function parseTranslationResponse(response) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Raw translation endpoint (for debugging - returns unprocessed LLM output) - SECURED
|
// Raw translation endpoint (for debugging - returns unprocessed LLM output) - SECURED
|
||||||
app.post('/api/translate/raw', authenticate, translationLimiter, async (req, res) => {
|
app.post('/api/translate/raw', authenticate, async (req, res) => {
|
||||||
const { text, target, provider, model, useLexique = true, customAnthropicKey, customOpenAIKey } = req.body;
|
const { text, target, provider, model, useLexique = true, customAnthropicKey, customOpenAIKey } = req.body;
|
||||||
|
|
||||||
if (!text || !target || !provider || !model) {
|
if (!text || !target || !provider || !model) {
|
||||||
@ -700,7 +700,7 @@ app.post('/api/translate/raw', authenticate, translationLimiter, async (req, res
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Batch translation endpoint - SECURED
|
// Batch translation endpoint - SECURED
|
||||||
app.post('/api/translate/batch', authenticate, translationLimiter, async (req, res) => {
|
app.post('/api/translate/batch', authenticate, async (req, res) => {
|
||||||
const { words, target = 'ancien' } = req.body;
|
const { words, target = 'ancien' } = req.body;
|
||||||
|
|
||||||
if (!words || !Array.isArray(words)) {
|
if (!words || !Array.isArray(words)) {
|
||||||
@ -727,7 +727,7 @@ app.post('/api/translate/batch', authenticate, translationLimiter, async (req, r
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Confluent → French translation endpoint (traduction brute) - SECURED
|
// Confluent → French translation endpoint (traduction brute) - SECURED
|
||||||
app.post('/api/translate/conf2fr', authenticate, translationLimiter, (req, res) => {
|
app.post('/api/translate/conf2fr', authenticate, (req, res) => {
|
||||||
const { text, variant = 'ancien', detailed = false } = req.body;
|
const { text, variant = 'ancien', detailed = false } = req.body;
|
||||||
|
|
||||||
if (!text) {
|
if (!text) {
|
||||||
@ -755,7 +755,7 @@ app.post('/api/translate/conf2fr', authenticate, translationLimiter, (req, res)
|
|||||||
});
|
});
|
||||||
|
|
||||||
// NEW: Confluent → French with LLM refinement
|
// NEW: Confluent → French with LLM refinement
|
||||||
app.post('/api/translate/conf2fr/llm', authenticate, translationLimiter, async (req, res) => {
|
app.post('/api/translate/conf2fr/llm', authenticate, async (req, res) => {
|
||||||
const { text, variant = 'ancien', provider = 'anthropic', model = 'claude-sonnet-4-20250514', customAnthropicKey, customOpenAIKey } = req.body;
|
const { text, variant = 'ancien', provider = 'anthropic', model = 'claude-sonnet-4-20250514', customAnthropicKey, customOpenAIKey } = req.body;
|
||||||
|
|
||||||
if (!text) {
|
if (!text) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user