locai 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +928 -0
- package/dist/cli.js +718 -0
- package/dist/commands/review.js +357 -0
- package/dist/commands/translator.js +29 -0
- package/dist/config/index.js +52 -0
- package/dist/core/adapters/base-adapter.js +1 -0
- package/dist/core/adapters/factory.js +44 -0
- package/dist/core/adapters/json-adapter.js +20 -0
- package/dist/core/adapters/po-adapter.js +50 -0
- package/dist/core/adapters/properties-adapter.js +31 -0
- package/dist/core/adapters/yaml-adapter.js +24 -0
- package/dist/core/context-processor.js +247 -0
- package/dist/core/detection/framework-detector.js +56 -0
- package/dist/core/detection/structure-detector.js +30 -0
- package/dist/core/fallback-provider.js +442 -0
- package/dist/core/orchestrator.js +438 -0
- package/dist/core/pipeline/Pipeline.js +48 -0
- package/dist/core/pipeline/PipelineStep.js +13 -0
- package/dist/core/pipeline/context.js +21 -0
- package/dist/core/pipeline/steps/CacheReadStep.js +80 -0
- package/dist/core/pipeline/steps/CacheWriteStep.js +32 -0
- package/dist/core/pipeline/steps/ConfidenceCheckStep.js +67 -0
- package/dist/core/pipeline/steps/GlossaryPostStep.js +17 -0
- package/dist/core/pipeline/steps/GlossaryPreStep.js +20 -0
- package/dist/core/pipeline/steps/InputValidationStep.js +26 -0
- package/dist/core/pipeline/steps/QualityCheckStep.js +48 -0
- package/dist/core/pipeline/steps/ToneCheckStep.js +58 -0
- package/dist/core/pipeline/steps/TranslationStep.js +65 -0
- package/dist/core/pipeline/steps/VectorCacheReadStep.js +84 -0
- package/dist/core/pipeline/steps/VectorCacheWriteStep.js +42 -0
- package/dist/core/pipeline/steps/__tests__/ToneCheckStep.test.js +100 -0
- package/dist/core/provider-factory.js +139 -0
- package/dist/providers/base-provider.js +230 -0
- package/dist/providers/dashscope.js +128 -0
- package/dist/providers/deepseek.js +111 -0
- package/dist/providers/gemini.js +135 -0
- package/dist/providers/openai.js +203 -0
- package/dist/providers/xai.js +111 -0
- package/dist/services/embedding-provider.js +50 -0
- package/dist/services/translation-service.js +609 -0
- package/dist/services/vector-store.js +79 -0
- package/dist/utils/ai-context-analyzer.js +460 -0
- package/dist/utils/confidence-scorer.js +277 -0
- package/dist/utils/error-helper.js +386 -0
- package/dist/utils/file-manager.js +357 -0
- package/dist/utils/glossary-manager.js +194 -0
- package/dist/utils/graceful-shutdown.js +118 -0
- package/dist/utils/input-validator.js +663 -0
- package/dist/utils/logger.js +374 -0
- package/dist/utils/object-transformer.js +177 -0
- package/dist/utils/progress-tracker.js +277 -0
- package/dist/utils/prompt-templates.js +415 -0
- package/dist/utils/quality/__tests__/tone-verifier.test.js +91 -0
- package/dist/utils/quality/base-checker.js +73 -0
- package/dist/utils/quality/code-block-checker.js +214 -0
- package/dist/utils/quality/html-tag-checker.js +64 -0
- package/dist/utils/quality/index.js +220 -0
- package/dist/utils/quality/length-checker.js +140 -0
- package/dist/utils/quality/markdown-checker.js +220 -0
- package/dist/utils/quality/placeholder-checker.js +142 -0
- package/dist/utils/quality/punctuation-checker.js +50 -0
- package/dist/utils/quality/quote-balance-checker.js +143 -0
- package/dist/utils/quality/special-characters-checker.js +229 -0
- package/dist/utils/quality/style-guide-checker.js +192 -0
- package/dist/utils/quality/text-sanitizer.js +120 -0
- package/dist/utils/quality/tone-verifier.js +91 -0
- package/dist/utils/rate-limiter.js +398 -0
- package/dist/utils/retry-helper.js +166 -0
- package/dist/utils/state-manager.js +162 -0
- package/dist/utils/statistics-manager.js +51 -0
- package/dist/utils/ui-manager.js +98 -0
- package/package.json +80 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { performance } from "perf_hooks";
|
|
2
|
+
/**
|
|
3
|
+
* Handles API rate limiting and request throttling.
|
|
4
|
+
* Supports provider-specific limits and adaptive throttling.
|
|
5
|
+
*/
|
|
6
|
+
class RateLimiter {
|
|
7
|
+
queues;
|
|
8
|
+
processing;
|
|
9
|
+
config;
|
|
10
|
+
metrics;
|
|
11
|
+
providers;
|
|
12
|
+
_processingLocks;
|
|
13
|
+
/**
|
|
14
|
+
* Create a new RateLimiter.
|
|
15
|
+
* @param config - Rate limiter configuration.
|
|
16
|
+
*/
|
|
17
|
+
constructor(config = {}) {
|
|
18
|
+
this.queues = {};
|
|
19
|
+
this.processing = {};
|
|
20
|
+
this.config = {
|
|
21
|
+
queueStrategy: config.queueStrategy || "fifo",
|
|
22
|
+
queueTimeout: config.queueTimeout || 15000,
|
|
23
|
+
adaptiveThrottling: config.adaptiveThrottling !== false,
|
|
24
|
+
};
|
|
25
|
+
this.metrics = {
|
|
26
|
+
responseTimes: {},
|
|
27
|
+
errorRates: {},
|
|
28
|
+
adjustments: {},
|
|
29
|
+
lastMetricsReset: {},
|
|
30
|
+
};
|
|
31
|
+
this.providers = {
|
|
32
|
+
openai: {
|
|
33
|
+
requestsPerMinute: 300,
|
|
34
|
+
currentRequests: 0,
|
|
35
|
+
lastReset: performance.now(),
|
|
36
|
+
maxConcurrent: 5,
|
|
37
|
+
},
|
|
38
|
+
deepseek: {
|
|
39
|
+
requestsPerMinute: 30,
|
|
40
|
+
currentRequests: 0,
|
|
41
|
+
lastReset: performance.now(),
|
|
42
|
+
maxConcurrent: 2,
|
|
43
|
+
},
|
|
44
|
+
gemini: {
|
|
45
|
+
requestsPerMinute: 300,
|
|
46
|
+
currentRequests: 0,
|
|
47
|
+
lastReset: performance.now(),
|
|
48
|
+
maxConcurrent: 5,
|
|
49
|
+
},
|
|
50
|
+
dashscope: {
|
|
51
|
+
requestsPerMinute: 80,
|
|
52
|
+
currentRequests: 0,
|
|
53
|
+
lastReset: performance.now(),
|
|
54
|
+
maxConcurrent: 6,
|
|
55
|
+
},
|
|
56
|
+
xai: {
|
|
57
|
+
requestsPerMinute: 80,
|
|
58
|
+
currentRequests: 0,
|
|
59
|
+
lastReset: performance.now(),
|
|
60
|
+
maxConcurrent: 8,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
this._processingLocks = {};
|
|
64
|
+
Object.keys(this.providers).forEach((provider) => {
|
|
65
|
+
this.queues[provider] = [];
|
|
66
|
+
this.processing[provider] = 0;
|
|
67
|
+
this.metrics.responseTimes[provider] = [];
|
|
68
|
+
this.metrics.errorRates[provider] = [];
|
|
69
|
+
this.metrics.adjustments[provider] = { rpm: 0, concurrency: 0 };
|
|
70
|
+
this.metrics.lastMetricsReset[provider] = Date.now();
|
|
71
|
+
});
|
|
72
|
+
setInterval(() => this._resetCounters(), 60000);
|
|
73
|
+
setInterval(() => this._cleanupMetrics(), 2 * 60 * 1000);
|
|
74
|
+
if (this.config.adaptiveThrottling) {
|
|
75
|
+
setInterval(() => this._adjustThrottling(), 5 * 60 * 1000);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Update rate limiter configuration.
|
|
80
|
+
* @param config - Configuration updates.
|
|
81
|
+
*/
|
|
82
|
+
updateConfig(config) {
|
|
83
|
+
if (!config)
|
|
84
|
+
return;
|
|
85
|
+
if (config.queueStrategy) {
|
|
86
|
+
this.config.queueStrategy = config.queueStrategy;
|
|
87
|
+
}
|
|
88
|
+
if (config.queueTimeout) {
|
|
89
|
+
this.config.queueTimeout = config.queueTimeout;
|
|
90
|
+
}
|
|
91
|
+
if (config.adaptiveThrottling !== undefined) {
|
|
92
|
+
this.config.adaptiveThrottling = config.adaptiveThrottling;
|
|
93
|
+
}
|
|
94
|
+
if (config.providerLimits && typeof config.providerLimits === "object") {
|
|
95
|
+
Object.keys(config.providerLimits).forEach((providerName) => {
|
|
96
|
+
if (this.providers[providerName]) {
|
|
97
|
+
const limits = config.providerLimits[providerName];
|
|
98
|
+
if (limits.rpm && typeof limits.rpm === "number") {
|
|
99
|
+
this.providers[providerName].requestsPerMinute = limits.rpm;
|
|
100
|
+
}
|
|
101
|
+
if (limits.concurrency && typeof limits.concurrency === "number") {
|
|
102
|
+
this.providers[providerName].maxConcurrent = limits.concurrency;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
// Only log in debug mode to avoid cluttering output
|
|
107
|
+
if (process.env.DEBUG) {
|
|
108
|
+
console.log("Provider limits updated from config");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Enqueue a task for execution with rate limiting.
|
|
114
|
+
* @param provider - Provider name.
|
|
115
|
+
* @param task - Async task to execute.
|
|
116
|
+
* @param priority - Task priority (higher is better).
|
|
117
|
+
* @returns Task result.
|
|
118
|
+
*/
|
|
119
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
120
|
+
async enqueue(provider, task, priority = 0) {
|
|
121
|
+
provider = provider.toLowerCase();
|
|
122
|
+
if (!this.providers[provider]) {
|
|
123
|
+
throw new Error(`Unknown provider: ${provider}`);
|
|
124
|
+
}
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
const queueItem = {
|
|
127
|
+
task,
|
|
128
|
+
resolve,
|
|
129
|
+
reject,
|
|
130
|
+
timestamp: Date.now(),
|
|
131
|
+
priority,
|
|
132
|
+
};
|
|
133
|
+
if (this.config.queueStrategy === "priority") {
|
|
134
|
+
const index = this.queues[provider].findIndex((item) => item.priority < priority);
|
|
135
|
+
if (index === -1) {
|
|
136
|
+
this.queues[provider].push(queueItem);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
this.queues[provider].splice(index, 0, queueItem);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
this.queues[provider].push(queueItem);
|
|
144
|
+
}
|
|
145
|
+
this._processQueue(provider);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Process the queue for a specific provider.
|
|
150
|
+
* @param provider - Provider name.
|
|
151
|
+
*/
|
|
152
|
+
async _processQueue(provider) {
|
|
153
|
+
if (!provider || typeof provider !== "string") {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const queue = this.queues[provider];
|
|
157
|
+
const limits = this.providers[provider];
|
|
158
|
+
if (!queue || !Array.isArray(queue) || !limits) {
|
|
159
|
+
console.warn(`Invalid queue or limits for provider: ${provider}`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const currentProcessing = this.processing[provider] || 0;
|
|
163
|
+
if (queue.length === 0 || currentProcessing >= limits.maxConcurrent) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (this._processingLocks[provider]) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
this._processingLocks[provider] = true;
|
|
170
|
+
try {
|
|
171
|
+
if (queue.length === 0 || this.processing[provider] >= limits.maxConcurrent) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const now = Date.now();
|
|
175
|
+
const timedOutItems = [];
|
|
176
|
+
let i = 0;
|
|
177
|
+
while (i < queue.length &&
|
|
178
|
+
queue[i] &&
|
|
179
|
+
now - queue[i].timestamp > this.config.queueTimeout) {
|
|
180
|
+
timedOutItems.push(queue[i]);
|
|
181
|
+
i++;
|
|
182
|
+
}
|
|
183
|
+
if (timedOutItems.length > 0) {
|
|
184
|
+
queue.splice(0, timedOutItems.length);
|
|
185
|
+
setImmediate(() => {
|
|
186
|
+
timedOutItems.forEach((item) => {
|
|
187
|
+
try {
|
|
188
|
+
item.reject(new Error(`Request timed out in queue after ${this.config.queueTimeout}ms`));
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
console.error(`Failed to reject timed out request: ${error.message}`);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
if (queue.length === 0) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (this._shouldThrottle(provider)) {
|
|
200
|
+
const waitTime = this._calculateWaitTime(provider);
|
|
201
|
+
setTimeout(() => this._processQueue(provider), waitTime);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const queueItem = queue.shift();
|
|
205
|
+
if (!queueItem) {
|
|
206
|
+
return; // Queue was emptied by another process or contains invalid item
|
|
207
|
+
}
|
|
208
|
+
const { task, resolve, reject, timestamp } = queueItem;
|
|
209
|
+
const startTime = performance.now();
|
|
210
|
+
const queueTime = Date.now() - timestamp;
|
|
211
|
+
if (queueTime > 30000) {
|
|
212
|
+
console.warn(`Task for ${provider} waited ${queueTime}ms in queue`);
|
|
213
|
+
}
|
|
214
|
+
this.processing[provider]++;
|
|
215
|
+
this.providers[provider].currentRequests++;
|
|
216
|
+
try {
|
|
217
|
+
const result = await task();
|
|
218
|
+
const responseTime = performance.now() - startTime;
|
|
219
|
+
this._trackResponseTime(provider, responseTime);
|
|
220
|
+
this._trackErrorRate(provider, false);
|
|
221
|
+
resolve(result);
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
this._trackErrorRate(provider, true);
|
|
225
|
+
console.error(`Provider ${provider} task failed:`, {
|
|
226
|
+
error: error.message,
|
|
227
|
+
queueTime: queueTime,
|
|
228
|
+
processingTime: performance.now() - startTime,
|
|
229
|
+
queueSize: queue.length,
|
|
230
|
+
});
|
|
231
|
+
reject(error);
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
this.processing[provider]--;
|
|
235
|
+
setImmediate(() => this._processQueue(provider));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
this._processingLocks[provider] = false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
_trackResponseTime(provider, time) {
|
|
243
|
+
const times = this.metrics.responseTimes[provider];
|
|
244
|
+
const MAX_RESPONSE_TIMES = 50;
|
|
245
|
+
times.push(time);
|
|
246
|
+
if (times.length > MAX_RESPONSE_TIMES) {
|
|
247
|
+
times.splice(0, times.length - MAX_RESPONSE_TIMES);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
_trackErrorRate(provider, isError) {
|
|
251
|
+
const errorRates = this.metrics.errorRates[provider];
|
|
252
|
+
const MAX_ERROR_RATES = 50;
|
|
253
|
+
errorRates.push(isError ? 1 : 0);
|
|
254
|
+
if (errorRates.length > MAX_ERROR_RATES) {
|
|
255
|
+
errorRates.splice(0, errorRates.length - MAX_ERROR_RATES);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
_adjustThrottling() {
|
|
259
|
+
if (!this.config.adaptiveThrottling)
|
|
260
|
+
return;
|
|
261
|
+
Object.keys(this.providers).forEach((provider) => {
|
|
262
|
+
const errorRates = this.metrics.errorRates[provider];
|
|
263
|
+
const errorRate = errorRates.length === 0
|
|
264
|
+
? 0
|
|
265
|
+
: errorRates.reduce((a, b) => a + b, 0) / errorRates.length;
|
|
266
|
+
const respTimes = this.metrics.responseTimes[provider];
|
|
267
|
+
const avgResponseTime = respTimes.length === 0
|
|
268
|
+
? 0
|
|
269
|
+
: respTimes.reduce((a, b) => a + b, 0) / respTimes.length;
|
|
270
|
+
let concurrencyAdjustment = 0;
|
|
271
|
+
let rpmAdjustment = 0;
|
|
272
|
+
if (errorRate > 0.15) {
|
|
273
|
+
concurrencyAdjustment = -1;
|
|
274
|
+
rpmAdjustment = -5;
|
|
275
|
+
}
|
|
276
|
+
else if (errorRate < 0.05 &&
|
|
277
|
+
avgResponseTime < 3000 &&
|
|
278
|
+
this.queues[provider].length > 0) {
|
|
279
|
+
concurrencyAdjustment = 1;
|
|
280
|
+
rpmAdjustment = 5;
|
|
281
|
+
}
|
|
282
|
+
if (concurrencyAdjustment !== 0) {
|
|
283
|
+
const newConcurrency = Math.max(1, this.providers[provider].maxConcurrent + concurrencyAdjustment);
|
|
284
|
+
this.providers[provider].maxConcurrent = newConcurrency;
|
|
285
|
+
this.metrics.adjustments[provider].concurrency += concurrencyAdjustment;
|
|
286
|
+
}
|
|
287
|
+
if (rpmAdjustment !== 0) {
|
|
288
|
+
const newRPM = Math.max(10, this.providers[provider].requestsPerMinute + rpmAdjustment);
|
|
289
|
+
this.providers[provider].requestsPerMinute = newRPM;
|
|
290
|
+
this.metrics.adjustments[provider].rpm += rpmAdjustment;
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
_shouldThrottle(provider) {
|
|
295
|
+
const limits = this.providers[provider];
|
|
296
|
+
const now = performance.now();
|
|
297
|
+
if (!limits) {
|
|
298
|
+
throw new Error(`Unknown provider: ${provider}`);
|
|
299
|
+
}
|
|
300
|
+
if (now - limits.lastReset >= 60000) {
|
|
301
|
+
limits.currentRequests = 0;
|
|
302
|
+
limits.lastReset = now;
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
return limits.currentRequests >= limits.requestsPerMinute;
|
|
306
|
+
}
|
|
307
|
+
_calculateWaitTime(provider) {
|
|
308
|
+
const limits = this.providers[provider];
|
|
309
|
+
if (!limits) {
|
|
310
|
+
throw new Error(`Unknown provider: ${provider}`);
|
|
311
|
+
}
|
|
312
|
+
return Math.max(0, 60000 - (performance.now() - limits.lastReset));
|
|
313
|
+
}
|
|
314
|
+
_resetCounters() {
|
|
315
|
+
const now = performance.now();
|
|
316
|
+
Object.values(this.providers).forEach((provider) => {
|
|
317
|
+
provider.currentRequests = 0;
|
|
318
|
+
provider.lastReset = now;
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Get current queue size for a provider.
|
|
323
|
+
* @param provider - Provider name.
|
|
324
|
+
* @returns Queue size.
|
|
325
|
+
*/
|
|
326
|
+
getQueueSize(provider) {
|
|
327
|
+
provider = provider.toLowerCase();
|
|
328
|
+
return this.queues[provider]?.length || 0;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Get complete rate limiter status.
|
|
332
|
+
* @returns Status object including active metrics.
|
|
333
|
+
*/
|
|
334
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
335
|
+
getStatus() {
|
|
336
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
337
|
+
const status = {};
|
|
338
|
+
Object.entries(this.providers).forEach(([name, config]) => {
|
|
339
|
+
const errorRates = this.metrics.errorRates[name];
|
|
340
|
+
const errorRate = errorRates.length === 0
|
|
341
|
+
? 0
|
|
342
|
+
: errorRates.reduce((a, b) => a + b, 0) / errorRates.length;
|
|
343
|
+
status[name] = {
|
|
344
|
+
queueSize: this.queues[name]?.length || 0,
|
|
345
|
+
processing: this.processing[name] || 0,
|
|
346
|
+
requestsUsed: config.currentRequests,
|
|
347
|
+
requestsLimit: config.requestsPerMinute,
|
|
348
|
+
resetIn: Math.max(0, 60000 - (performance.now() - config.lastReset)) / 1000,
|
|
349
|
+
errorRate: Math.round(errorRate * 100) / 100,
|
|
350
|
+
adjustments: this.metrics.adjustments[name],
|
|
351
|
+
config: {
|
|
352
|
+
maxConcurrent: config.maxConcurrent,
|
|
353
|
+
rpm: config.requestsPerMinute,
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
});
|
|
357
|
+
return status;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Get current configuration.
|
|
361
|
+
* @returns Configuration object.
|
|
362
|
+
*/
|
|
363
|
+
getConfig() {
|
|
364
|
+
return { ...this.config };
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Clear all pending queues.
|
|
368
|
+
*/
|
|
369
|
+
clearAllQueues() {
|
|
370
|
+
for (const provider in this.queues) {
|
|
371
|
+
if (this.queues[provider] && this.queues[provider].length > 0) {
|
|
372
|
+
console.log(`Clearing ${this.queues[provider].length} pending operations for ${provider}`);
|
|
373
|
+
this.queues[provider].forEach((item) => {
|
|
374
|
+
if (item.reject) {
|
|
375
|
+
item.reject(new Error("Operation cancelled due to shutdown"));
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
this.queues[provider] = [];
|
|
379
|
+
}
|
|
380
|
+
this.processing[provider] = 0;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
_cleanupMetrics() {
|
|
384
|
+
const now = Date.now();
|
|
385
|
+
Object.keys(this.providers).forEach((providerName) => {
|
|
386
|
+
this.metrics.responseTimes[providerName].length = 0;
|
|
387
|
+
this.metrics.errorRates[providerName].length = 0;
|
|
388
|
+
this.metrics.adjustments[providerName] = { rpm: 0, concurrency: 0 };
|
|
389
|
+
this.metrics.lastMetricsReset[providerName] = now;
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const config = {
|
|
394
|
+
queueStrategy: process.env.QUEUE_STRATEGY || "priority",
|
|
395
|
+
queueTimeout: parseInt(process.env.QUEUE_TIMEOUT || "30000"),
|
|
396
|
+
adaptiveThrottling: process.env.ADAPTIVE_THROTTLING !== "false",
|
|
397
|
+
};
|
|
398
|
+
export default new RateLimiter(config);
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry helper for API operations with exponential backoff
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Retry helper for API operations with exponential backoff.
|
|
6
|
+
*/
|
|
7
|
+
class RetryHelper {
|
|
8
|
+
/**
|
|
9
|
+
* Retry an operation with configurable backoff strategy.
|
|
10
|
+
* Supports per-provider retry configuration.
|
|
11
|
+
*/
|
|
12
|
+
static async withRetry(operation, options = {}) {
|
|
13
|
+
const providerName = options.provider?.toLowerCase();
|
|
14
|
+
const perProviderRetry = options.perProviderRetry || {};
|
|
15
|
+
const retryableErrors = options.retryableErrors || [
|
|
16
|
+
"rate_limit",
|
|
17
|
+
"timeout",
|
|
18
|
+
"network",
|
|
19
|
+
"server",
|
|
20
|
+
"unknown",
|
|
21
|
+
];
|
|
22
|
+
// Get provider-specific retry settings or use defaults
|
|
23
|
+
let maxRetries = options.maxRetries ?? 2;
|
|
24
|
+
let initialDelay = options.initialDelay ?? 1000;
|
|
25
|
+
let maxDelay = options.maxDelay ?? 10000;
|
|
26
|
+
// Override with provider-specific settings if available
|
|
27
|
+
if (providerName && perProviderRetry[providerName]) {
|
|
28
|
+
const providerConfig = perProviderRetry[providerName];
|
|
29
|
+
maxRetries = providerConfig.maxRetries ?? maxRetries;
|
|
30
|
+
initialDelay = providerConfig.initialDelay ?? initialDelay;
|
|
31
|
+
maxDelay = providerConfig.maxDelay ?? maxDelay;
|
|
32
|
+
}
|
|
33
|
+
const context = options.context ?? "Operation";
|
|
34
|
+
const logContext = options.logContext ?? {};
|
|
35
|
+
const retryCondition = options.retryCondition ??
|
|
36
|
+
((error) => this.defaultRetryCondition(error, retryableErrors));
|
|
37
|
+
let lastError = null;
|
|
38
|
+
let attempts = 0;
|
|
39
|
+
const startTime = Date.now();
|
|
40
|
+
const retryStats = { attempts: 0, totalTime: 0, provider: providerName };
|
|
41
|
+
while (attempts <= maxRetries) {
|
|
42
|
+
try {
|
|
43
|
+
retryStats.attempts = attempts;
|
|
44
|
+
if (attempts > 0) {
|
|
45
|
+
const delay = this.calculateBackoff(attempts, initialDelay, maxDelay);
|
|
46
|
+
if (process.env.DEBUG) {
|
|
47
|
+
const providerInfo = providerName ? ` [${providerName}]` : "";
|
|
48
|
+
console.log(`${context}${providerInfo}: Retrying attempt ${attempts}/${maxRetries} after ${delay}ms delay`);
|
|
49
|
+
}
|
|
50
|
+
await this.delay(delay);
|
|
51
|
+
}
|
|
52
|
+
const result = await operation();
|
|
53
|
+
// Track time if this was a retry
|
|
54
|
+
if (attempts > 0) {
|
|
55
|
+
retryStats.totalTime = Date.now() - startTime;
|
|
56
|
+
retryStats.success = true;
|
|
57
|
+
if (process.env.DEBUG) {
|
|
58
|
+
console.log(`${context}: Succeeded after ${attempts} retries (${retryStats.totalTime}ms)`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
attempts++;
|
|
65
|
+
lastError = error;
|
|
66
|
+
if (!error.retryInfo) {
|
|
67
|
+
error.retryInfo = {
|
|
68
|
+
attemptNumber: attempts,
|
|
69
|
+
maxRetries,
|
|
70
|
+
willRetry: attempts <= maxRetries,
|
|
71
|
+
context: logContext,
|
|
72
|
+
provider: providerName,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const shouldRetry = attempts <= maxRetries && (await retryCondition(error, attempts, maxRetries));
|
|
76
|
+
if (process.env.DEBUG) {
|
|
77
|
+
const providerInfo = providerName ? ` [${providerName}]` : "";
|
|
78
|
+
console.warn(`Warning: ${context}${providerInfo}: Error on attempt ${attempts}/${maxRetries + 1}: ${error.message}` +
|
|
79
|
+
(error.code ? ` [${error.code}]` : ""));
|
|
80
|
+
}
|
|
81
|
+
if (!shouldRetry) {
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
retryStats.totalTime = Date.now() - startTime;
|
|
87
|
+
retryStats.success = false;
|
|
88
|
+
if (lastError) {
|
|
89
|
+
lastError.retryStats = retryStats;
|
|
90
|
+
lastError.message = `${lastError.message} (after ${attempts} attempts over ${retryStats.totalTime}ms)`;
|
|
91
|
+
}
|
|
92
|
+
throw lastError;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Calculate exponential backoff with jitter.
|
|
96
|
+
*/
|
|
97
|
+
static calculateBackoff(attempt, initialDelay, maxDelay) {
|
|
98
|
+
const expBackoff = Math.min(maxDelay, initialDelay * Math.pow(2, attempt - 1));
|
|
99
|
+
return Math.floor(Math.random() * expBackoff);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Default retry condition based on error type.
|
|
103
|
+
* Checks against retryableErrors list.
|
|
104
|
+
*/
|
|
105
|
+
static defaultRetryCondition(error, retryableErrors = []) {
|
|
106
|
+
// Check for rate_limit errors
|
|
107
|
+
if (retryableErrors.includes("rate_limit") &&
|
|
108
|
+
(error.status === 429 || error.code === "ERR_RATE_LIMIT")) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
// Check for timeout errors
|
|
112
|
+
if (retryableErrors.includes("timeout") &&
|
|
113
|
+
(error.code === "ETIMEDOUT" ||
|
|
114
|
+
error.code === "ECONNABORTED" ||
|
|
115
|
+
error.message?.includes("timeout"))) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
// Check for network errors
|
|
119
|
+
if (retryableErrors.includes("network") &&
|
|
120
|
+
(error.code === "ECONNRESET" ||
|
|
121
|
+
error.code === "ECONNREFUSED" ||
|
|
122
|
+
error.code === "ENOTFOUND" ||
|
|
123
|
+
error.code === "ERR_NETWORK" ||
|
|
124
|
+
error.message?.includes("network") ||
|
|
125
|
+
error.message?.includes("connection"))) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
// Check for server errors (5xx)
|
|
129
|
+
if (retryableErrors.includes("server") && error.status >= 500 && error.status < 600) {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
// Check for unknown/general errors
|
|
133
|
+
if (retryableErrors.includes("unknown") && !error.status) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
// Legacy fallback for backward compatibility
|
|
137
|
+
if (retryableErrors.length === 0) {
|
|
138
|
+
// Old behavior when no retryableErrors specified
|
|
139
|
+
if (error.status && error.status >= 400 && error.status < 500) {
|
|
140
|
+
if (error.status === 429)
|
|
141
|
+
return true;
|
|
142
|
+
if ([408, 425, 449].includes(error.status))
|
|
143
|
+
return true;
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
if (error.code === "ECONNRESET" ||
|
|
147
|
+
error.code === "ETIMEDOUT" ||
|
|
148
|
+
error.code === "ECONNREFUSED" ||
|
|
149
|
+
error.code === "ENOTFOUND" ||
|
|
150
|
+
error.message?.includes("network") ||
|
|
151
|
+
error.message?.includes("timeout") ||
|
|
152
|
+
error.message?.includes("connection")) {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
return error.status >= 500 || !error.status;
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Promise-based delay.
|
|
161
|
+
*/
|
|
162
|
+
static delay(ms) {
|
|
163
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
export default RetryHelper;
|