@azumag/opencode-rate-limit-fallback 1.35.0 → 1.36.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/dist/index.d.ts +1 -0
- package/dist/index.js +50 -5
- package/dist/src/circuitbreaker/CircuitBreaker.d.ts +8 -1
- package/dist/src/circuitbreaker/CircuitBreaker.js +11 -1
- package/dist/src/config/Validator.d.ts +64 -0
- package/dist/src/config/Validator.js +618 -0
- package/dist/src/diagnostics/Reporter.d.ts +128 -0
- package/dist/src/diagnostics/Reporter.js +285 -0
- package/dist/src/errors/PatternRegistry.d.ts +75 -0
- package/dist/src/errors/PatternRegistry.js +234 -0
- package/dist/src/fallback/FallbackHandler.d.ts +3 -1
- package/dist/src/fallback/FallbackHandler.js +16 -2
- package/dist/src/fallback/ModelSelector.d.ts +3 -1
- package/dist/src/fallback/ModelSelector.js +17 -1
- package/dist/src/health/HealthTracker.d.ts +96 -0
- package/dist/src/health/HealthTracker.js +353 -0
- package/dist/src/types/index.d.ts +52 -0
- package/package.json +1 -1
- package/dist/src/utils/errorDetection.d.ts +0 -7
- package/dist/src/utils/errorDetection.js +0 -34
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration validation and diagnostics
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, readFileSync } from 'fs';
|
|
5
|
+
/**
|
|
6
|
+
* Configuration Validator class
|
|
7
|
+
*/
|
|
8
|
+
export class ConfigValidator {
|
|
9
|
+
logger;
|
|
10
|
+
constructor(logger) {
|
|
11
|
+
this.logger = logger;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Validate a configuration object
|
|
15
|
+
*/
|
|
16
|
+
validate(config, options) {
|
|
17
|
+
const errors = [];
|
|
18
|
+
const warnings = [];
|
|
19
|
+
const { strict = false, logWarnings = true } = options || {};
|
|
20
|
+
// Validate fallbackModels
|
|
21
|
+
if (config.fallbackModels) {
|
|
22
|
+
if (!Array.isArray(config.fallbackModels)) {
|
|
23
|
+
errors.push({
|
|
24
|
+
path: 'fallbackModels',
|
|
25
|
+
message: 'fallbackModels must be an array',
|
|
26
|
+
severity: 'error',
|
|
27
|
+
value: config.fallbackModels,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
for (let i = 0; i < config.fallbackModels.length; i++) {
|
|
32
|
+
const model = config.fallbackModels[i];
|
|
33
|
+
const modelPath = `fallbackModels[${i}]`;
|
|
34
|
+
if (!model || typeof model !== 'object') {
|
|
35
|
+
errors.push({
|
|
36
|
+
path: modelPath,
|
|
37
|
+
message: 'Fallback model must be an object',
|
|
38
|
+
severity: 'error',
|
|
39
|
+
value: model,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
if (!model.providerID || typeof model.providerID !== 'string') {
|
|
44
|
+
errors.push({
|
|
45
|
+
path: `${modelPath}.providerID`,
|
|
46
|
+
message: 'providerID is required and must be a string',
|
|
47
|
+
severity: 'error',
|
|
48
|
+
value: model.providerID,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (!model.modelID || typeof model.modelID !== 'string') {
|
|
52
|
+
errors.push({
|
|
53
|
+
path: `${modelPath}.modelID`,
|
|
54
|
+
message: 'modelID is required and must be a string',
|
|
55
|
+
severity: 'error',
|
|
56
|
+
value: model.modelID,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Warning if fallbackModels is empty
|
|
62
|
+
if (config.fallbackModels.length === 0) {
|
|
63
|
+
warnings.push({
|
|
64
|
+
path: 'fallbackModels',
|
|
65
|
+
message: 'fallbackModels is empty - no fallback models available',
|
|
66
|
+
severity: 'warning',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Validate cooldownMs
|
|
72
|
+
if (config.cooldownMs !== undefined) {
|
|
73
|
+
if (typeof config.cooldownMs !== 'number' || config.cooldownMs < 0) {
|
|
74
|
+
errors.push({
|
|
75
|
+
path: 'cooldownMs',
|
|
76
|
+
message: 'cooldownMs must be a non-negative number',
|
|
77
|
+
severity: 'error',
|
|
78
|
+
value: config.cooldownMs,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
else if (config.cooldownMs < 1000) {
|
|
82
|
+
warnings.push({
|
|
83
|
+
path: 'cooldownMs',
|
|
84
|
+
message: 'cooldownMs is very low (< 1000ms), may cause frequent retries',
|
|
85
|
+
severity: 'warning',
|
|
86
|
+
value: config.cooldownMs,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
else if (config.cooldownMs > 300000) {
|
|
90
|
+
warnings.push({
|
|
91
|
+
path: 'cooldownMs',
|
|
92
|
+
message: 'cooldownMs is very high (> 5min), fallback will be slow',
|
|
93
|
+
severity: 'warning',
|
|
94
|
+
value: config.cooldownMs,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Validate enabled
|
|
99
|
+
if (config.enabled !== undefined && typeof config.enabled !== 'boolean') {
|
|
100
|
+
errors.push({
|
|
101
|
+
path: 'enabled',
|
|
102
|
+
message: 'enabled must be a boolean',
|
|
103
|
+
severity: 'error',
|
|
104
|
+
value: config.enabled,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
// Validate fallbackMode
|
|
108
|
+
if (config.fallbackMode && config.fallbackMode !== 'cycle' && config.fallbackMode !== 'stop' && config.fallbackMode !== 'retry-last') {
|
|
109
|
+
errors.push({
|
|
110
|
+
path: 'fallbackMode',
|
|
111
|
+
message: 'fallbackMode must be one of: cycle, stop, retry-last',
|
|
112
|
+
severity: 'error',
|
|
113
|
+
value: config.fallbackMode,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
// Validate retryPolicy
|
|
117
|
+
if (config.retryPolicy) {
|
|
118
|
+
if (typeof config.retryPolicy !== 'object') {
|
|
119
|
+
errors.push({
|
|
120
|
+
path: 'retryPolicy',
|
|
121
|
+
message: 'retryPolicy must be an object',
|
|
122
|
+
severity: 'error',
|
|
123
|
+
value: config.retryPolicy,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
if (config.retryPolicy.maxRetries !== undefined) {
|
|
128
|
+
if (typeof config.retryPolicy.maxRetries !== 'number' || config.retryPolicy.maxRetries < 0) {
|
|
129
|
+
errors.push({
|
|
130
|
+
path: 'retryPolicy.maxRetries',
|
|
131
|
+
message: 'maxRetries must be a non-negative number',
|
|
132
|
+
severity: 'error',
|
|
133
|
+
value: config.retryPolicy.maxRetries,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
else if (config.retryPolicy.maxRetries > 10) {
|
|
137
|
+
warnings.push({
|
|
138
|
+
path: 'retryPolicy.maxRetries',
|
|
139
|
+
message: 'maxRetries is very high (> 10), may cause excessive retries',
|
|
140
|
+
severity: 'warning',
|
|
141
|
+
value: config.retryPolicy.maxRetries,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (config.retryPolicy.strategy !== undefined) {
|
|
146
|
+
const validStrategies = ['immediate', 'exponential', 'linear', 'custom'];
|
|
147
|
+
if (!validStrategies.includes(config.retryPolicy.strategy)) {
|
|
148
|
+
errors.push({
|
|
149
|
+
path: 'retryPolicy.strategy',
|
|
150
|
+
message: `strategy must be one of: ${validStrategies.join(', ')}`,
|
|
151
|
+
severity: 'error',
|
|
152
|
+
value: config.retryPolicy.strategy,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (config.retryPolicy.baseDelayMs !== undefined) {
|
|
157
|
+
if (typeof config.retryPolicy.baseDelayMs !== 'number' || config.retryPolicy.baseDelayMs < 0) {
|
|
158
|
+
errors.push({
|
|
159
|
+
path: 'retryPolicy.baseDelayMs',
|
|
160
|
+
message: 'baseDelayMs must be a non-negative number',
|
|
161
|
+
severity: 'error',
|
|
162
|
+
value: config.retryPolicy.baseDelayMs,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (config.retryPolicy.maxDelayMs !== undefined) {
|
|
167
|
+
if (typeof config.retryPolicy.maxDelayMs !== 'number' || config.retryPolicy.maxDelayMs < 0) {
|
|
168
|
+
errors.push({
|
|
169
|
+
path: 'retryPolicy.maxDelayMs',
|
|
170
|
+
message: 'maxDelayMs must be a non-negative number',
|
|
171
|
+
severity: 'error',
|
|
172
|
+
value: config.retryPolicy.maxDelayMs,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (config.retryPolicy.jitterEnabled !== undefined && typeof config.retryPolicy.jitterEnabled !== 'boolean') {
|
|
177
|
+
errors.push({
|
|
178
|
+
path: 'retryPolicy.jitterEnabled',
|
|
179
|
+
message: 'jitterEnabled must be a boolean',
|
|
180
|
+
severity: 'error',
|
|
181
|
+
value: config.retryPolicy.jitterEnabled,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
if (config.retryPolicy.jitterFactor !== undefined) {
|
|
185
|
+
if (typeof config.retryPolicy.jitterFactor !== 'number' || config.retryPolicy.jitterFactor < 0 || config.retryPolicy.jitterFactor > 1) {
|
|
186
|
+
errors.push({
|
|
187
|
+
path: 'retryPolicy.jitterFactor',
|
|
188
|
+
message: 'jitterFactor must be a number between 0 and 1',
|
|
189
|
+
severity: 'error',
|
|
190
|
+
value: config.retryPolicy.jitterFactor,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (config.retryPolicy.timeoutMs !== undefined) {
|
|
195
|
+
if (typeof config.retryPolicy.timeoutMs !== 'number' || config.retryPolicy.timeoutMs < 0) {
|
|
196
|
+
errors.push({
|
|
197
|
+
path: 'retryPolicy.timeoutMs',
|
|
198
|
+
message: 'timeoutMs must be a non-negative number',
|
|
199
|
+
severity: 'error',
|
|
200
|
+
value: config.retryPolicy.timeoutMs,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Validate circuitBreaker
|
|
207
|
+
if (config.circuitBreaker) {
|
|
208
|
+
if (typeof config.circuitBreaker !== 'object') {
|
|
209
|
+
errors.push({
|
|
210
|
+
path: 'circuitBreaker',
|
|
211
|
+
message: 'circuitBreaker must be an object',
|
|
212
|
+
severity: 'error',
|
|
213
|
+
value: config.circuitBreaker,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
if (config.circuitBreaker.enabled !== undefined && typeof config.circuitBreaker.enabled !== 'boolean') {
|
|
218
|
+
errors.push({
|
|
219
|
+
path: 'circuitBreaker.enabled',
|
|
220
|
+
message: 'enabled must be a boolean',
|
|
221
|
+
severity: 'error',
|
|
222
|
+
value: config.circuitBreaker.enabled,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
if (config.circuitBreaker.failureThreshold !== undefined) {
|
|
226
|
+
if (typeof config.circuitBreaker.failureThreshold !== 'number' || config.circuitBreaker.failureThreshold < 1) {
|
|
227
|
+
errors.push({
|
|
228
|
+
path: 'circuitBreaker.failureThreshold',
|
|
229
|
+
message: 'failureThreshold must be a positive number',
|
|
230
|
+
severity: 'error',
|
|
231
|
+
value: config.circuitBreaker.failureThreshold,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (config.circuitBreaker.recoveryTimeoutMs !== undefined) {
|
|
236
|
+
if (typeof config.circuitBreaker.recoveryTimeoutMs !== 'number' || config.circuitBreaker.recoveryTimeoutMs < 0) {
|
|
237
|
+
errors.push({
|
|
238
|
+
path: 'circuitBreaker.recoveryTimeoutMs',
|
|
239
|
+
message: 'recoveryTimeoutMs must be a non-negative number',
|
|
240
|
+
severity: 'error',
|
|
241
|
+
value: config.circuitBreaker.recoveryTimeoutMs,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (config.circuitBreaker.halfOpenMaxCalls !== undefined) {
|
|
246
|
+
if (typeof config.circuitBreaker.halfOpenMaxCalls !== 'number' || config.circuitBreaker.halfOpenMaxCalls < 1) {
|
|
247
|
+
errors.push({
|
|
248
|
+
path: 'circuitBreaker.halfOpenMaxCalls',
|
|
249
|
+
message: 'halfOpenMaxCalls must be a positive number',
|
|
250
|
+
severity: 'error',
|
|
251
|
+
value: config.circuitBreaker.halfOpenMaxCalls,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (config.circuitBreaker.successThreshold !== undefined) {
|
|
256
|
+
if (typeof config.circuitBreaker.successThreshold !== 'number' || config.circuitBreaker.successThreshold < 1) {
|
|
257
|
+
errors.push({
|
|
258
|
+
path: 'circuitBreaker.successThreshold',
|
|
259
|
+
message: 'successThreshold must be a positive number',
|
|
260
|
+
severity: 'error',
|
|
261
|
+
value: config.circuitBreaker.successThreshold,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Validate log
|
|
268
|
+
if (config.log) {
|
|
269
|
+
if (typeof config.log !== 'object') {
|
|
270
|
+
errors.push({
|
|
271
|
+
path: 'log',
|
|
272
|
+
message: 'log must be an object',
|
|
273
|
+
severity: 'error',
|
|
274
|
+
value: config.log,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
if (config.log.level !== undefined) {
|
|
279
|
+
const validLevels = ['error', 'warn', 'info', 'debug'];
|
|
280
|
+
if (!validLevels.includes(config.log.level)) {
|
|
281
|
+
errors.push({
|
|
282
|
+
path: 'log.level',
|
|
283
|
+
message: `level must be one of: ${validLevels.join(', ')}`,
|
|
284
|
+
severity: 'error',
|
|
285
|
+
value: config.log.level,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (config.log.format !== undefined) {
|
|
290
|
+
const validFormats = ['simple', 'json'];
|
|
291
|
+
if (!validFormats.includes(config.log.format)) {
|
|
292
|
+
errors.push({
|
|
293
|
+
path: 'log.format',
|
|
294
|
+
message: `format must be one of: ${validFormats.join(', ')}`,
|
|
295
|
+
severity: 'error',
|
|
296
|
+
value: config.log.format,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (config.log.enableTimestamp !== undefined && typeof config.log.enableTimestamp !== 'boolean') {
|
|
301
|
+
errors.push({
|
|
302
|
+
path: 'log.enableTimestamp',
|
|
303
|
+
message: 'enableTimestamp must be a boolean',
|
|
304
|
+
severity: 'error',
|
|
305
|
+
value: config.log.enableTimestamp,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// Validate metrics
|
|
311
|
+
if (config.metrics) {
|
|
312
|
+
if (typeof config.metrics !== 'object') {
|
|
313
|
+
errors.push({
|
|
314
|
+
path: 'metrics',
|
|
315
|
+
message: 'metrics must be an object',
|
|
316
|
+
severity: 'error',
|
|
317
|
+
value: config.metrics,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
if (config.metrics.enabled !== undefined && typeof config.metrics.enabled !== 'boolean') {
|
|
322
|
+
errors.push({
|
|
323
|
+
path: 'metrics.enabled',
|
|
324
|
+
message: 'enabled must be a boolean',
|
|
325
|
+
severity: 'error',
|
|
326
|
+
value: config.metrics.enabled,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
if (config.metrics.output !== undefined) {
|
|
330
|
+
if (typeof config.metrics.output !== 'object') {
|
|
331
|
+
errors.push({
|
|
332
|
+
path: 'metrics.output',
|
|
333
|
+
message: 'output must be an object',
|
|
334
|
+
severity: 'error',
|
|
335
|
+
value: config.metrics.output,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
if (config.metrics.output.console !== undefined && typeof config.metrics.output.console !== 'boolean') {
|
|
340
|
+
errors.push({
|
|
341
|
+
path: 'metrics.output.console',
|
|
342
|
+
message: 'console must be a boolean',
|
|
343
|
+
severity: 'error',
|
|
344
|
+
value: config.metrics.output.console,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
if (config.metrics.output.format !== undefined) {
|
|
348
|
+
const validFormats = ['pretty', 'json', 'csv'];
|
|
349
|
+
if (!validFormats.includes(config.metrics.output.format)) {
|
|
350
|
+
errors.push({
|
|
351
|
+
path: 'metrics.output.format',
|
|
352
|
+
message: `format must be one of: ${validFormats.join(', ')}`,
|
|
353
|
+
severity: 'error',
|
|
354
|
+
value: config.metrics.output.format,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (config.metrics.output.file !== undefined && typeof config.metrics.output.file !== 'string') {
|
|
359
|
+
errors.push({
|
|
360
|
+
path: 'metrics.output.file',
|
|
361
|
+
message: 'file must be a string',
|
|
362
|
+
severity: 'error',
|
|
363
|
+
value: config.metrics.output.file,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (config.metrics.resetInterval !== undefined) {
|
|
369
|
+
const validIntervals = ['hourly', 'daily', 'weekly'];
|
|
370
|
+
if (!validIntervals.includes(config.metrics.resetInterval)) {
|
|
371
|
+
errors.push({
|
|
372
|
+
path: 'metrics.resetInterval',
|
|
373
|
+
message: `resetInterval must be one of: ${validIntervals.join(', ')}`,
|
|
374
|
+
severity: 'error',
|
|
375
|
+
value: config.metrics.resetInterval,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// Validate new configuration options (for v1.36.0)
|
|
382
|
+
// Validate enableHealthBasedSelection
|
|
383
|
+
if (config.enableHealthBasedSelection !== undefined && typeof config.enableHealthBasedSelection !== 'boolean') {
|
|
384
|
+
errors.push({
|
|
385
|
+
path: 'enableHealthBasedSelection',
|
|
386
|
+
message: 'enableHealthBasedSelection must be a boolean',
|
|
387
|
+
severity: 'error',
|
|
388
|
+
value: config.enableHealthBasedSelection,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
// Validate healthPersistence
|
|
392
|
+
if (config.healthPersistence) {
|
|
393
|
+
if (typeof config.healthPersistence !== 'object') {
|
|
394
|
+
errors.push({
|
|
395
|
+
path: 'healthPersistence',
|
|
396
|
+
message: 'healthPersistence must be an object',
|
|
397
|
+
severity: 'error',
|
|
398
|
+
value: config.healthPersistence,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
if (config.healthPersistence.enabled !== undefined && typeof config.healthPersistence.enabled !== 'boolean') {
|
|
403
|
+
errors.push({
|
|
404
|
+
path: 'healthPersistence.enabled',
|
|
405
|
+
message: 'enabled must be a boolean',
|
|
406
|
+
severity: 'error',
|
|
407
|
+
value: config.healthPersistence.enabled,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
if (config.healthPersistence.path !== undefined && typeof config.healthPersistence.path !== 'string') {
|
|
411
|
+
errors.push({
|
|
412
|
+
path: 'healthPersistence.path',
|
|
413
|
+
message: 'path must be a string',
|
|
414
|
+
severity: 'error',
|
|
415
|
+
value: config.healthPersistence.path,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
else if (config.healthPersistence.path) {
|
|
419
|
+
// Check for potential path traversal
|
|
420
|
+
if (config.healthPersistence.path.includes('..')) {
|
|
421
|
+
errors.push({
|
|
422
|
+
path: 'healthPersistence.path',
|
|
423
|
+
message: 'path must not contain ".." for security reasons',
|
|
424
|
+
severity: 'error',
|
|
425
|
+
value: config.healthPersistence.path,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
// Validate verbose
|
|
432
|
+
if (config.verbose !== undefined && typeof config.verbose !== 'boolean') {
|
|
433
|
+
errors.push({
|
|
434
|
+
path: 'verbose',
|
|
435
|
+
message: 'verbose must be a boolean',
|
|
436
|
+
severity: 'error',
|
|
437
|
+
value: config.verbose,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
// Validate errorPatterns
|
|
441
|
+
if (config.errorPatterns) {
|
|
442
|
+
if (typeof config.errorPatterns !== 'object') {
|
|
443
|
+
errors.push({
|
|
444
|
+
path: 'errorPatterns',
|
|
445
|
+
message: 'errorPatterns must be an object',
|
|
446
|
+
severity: 'error',
|
|
447
|
+
value: config.errorPatterns,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
if (config.errorPatterns.custom && !Array.isArray(config.errorPatterns.custom)) {
|
|
452
|
+
errors.push({
|
|
453
|
+
path: 'errorPatterns.custom',
|
|
454
|
+
message: 'custom must be an array',
|
|
455
|
+
severity: 'error',
|
|
456
|
+
value: config.errorPatterns.custom,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
if (config.errorPatterns.enableLearning !== undefined && typeof config.errorPatterns.enableLearning !== 'boolean') {
|
|
460
|
+
errors.push({
|
|
461
|
+
path: 'errorPatterns.enableLearning',
|
|
462
|
+
message: 'enableLearning must be a boolean',
|
|
463
|
+
severity: 'error',
|
|
464
|
+
value: config.errorPatterns.enableLearning,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// Log warnings if enabled
|
|
470
|
+
if (logWarnings && warnings.length > 0 && this.logger) {
|
|
471
|
+
for (const warning of warnings) {
|
|
472
|
+
this.logger.warn(`Config warning at ${warning.path}: ${warning.message}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
// Log errors if present
|
|
476
|
+
if (errors.length > 0 && this.logger) {
|
|
477
|
+
for (const error of errors) {
|
|
478
|
+
this.logger.error(`Config error at ${error.path}: ${error.message}`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
isValid: strict ? errors.length === 0 : true,
|
|
483
|
+
errors,
|
|
484
|
+
warnings,
|
|
485
|
+
config: config,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Validate a configuration file
|
|
490
|
+
*/
|
|
491
|
+
validateFile(filePath, options) {
|
|
492
|
+
if (!existsSync(filePath)) {
|
|
493
|
+
return {
|
|
494
|
+
isValid: false,
|
|
495
|
+
errors: [{
|
|
496
|
+
path: 'file',
|
|
497
|
+
message: `Config file not found: ${filePath}`,
|
|
498
|
+
severity: 'error',
|
|
499
|
+
}],
|
|
500
|
+
warnings: [],
|
|
501
|
+
config: {},
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
try {
|
|
505
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
506
|
+
const config = JSON.parse(content);
|
|
507
|
+
return this.validate(config, options);
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
511
|
+
return {
|
|
512
|
+
isValid: false,
|
|
513
|
+
errors: [{
|
|
514
|
+
path: 'file',
|
|
515
|
+
message: `Failed to parse config file: ${errorMessage}`,
|
|
516
|
+
severity: 'error',
|
|
517
|
+
value: error,
|
|
518
|
+
}],
|
|
519
|
+
warnings: [],
|
|
520
|
+
config: {},
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Get diagnostics information for the current configuration
|
|
526
|
+
*/
|
|
527
|
+
getDiagnostics(config, configSource, defaultsApplied = []) {
|
|
528
|
+
const validation = this.validate(config, { logWarnings: false });
|
|
529
|
+
return {
|
|
530
|
+
configSource,
|
|
531
|
+
config,
|
|
532
|
+
validation,
|
|
533
|
+
defaultsApplied,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Format diagnostics as human-readable text
|
|
538
|
+
*/
|
|
539
|
+
formatDiagnostics(diagnostics) {
|
|
540
|
+
const lines = [];
|
|
541
|
+
lines.push('='.repeat(60));
|
|
542
|
+
lines.push('Rate Limit Fallback - Configuration Diagnostics');
|
|
543
|
+
lines.push('='.repeat(60));
|
|
544
|
+
lines.push('');
|
|
545
|
+
// Config source
|
|
546
|
+
lines.push(`Config Source: ${diagnostics.configSource || 'Default (no file found)'}`);
|
|
547
|
+
lines.push('');
|
|
548
|
+
// Validation summary
|
|
549
|
+
const { isValid, errors, warnings } = diagnostics.validation;
|
|
550
|
+
lines.push(`Validation Status: ${isValid ? 'VALID' : 'INVALID'}`);
|
|
551
|
+
lines.push(`Errors: ${errors.length}, Warnings: ${warnings.length}`);
|
|
552
|
+
lines.push('');
|
|
553
|
+
// Errors
|
|
554
|
+
if (errors.length > 0) {
|
|
555
|
+
lines.push('ERRORS:');
|
|
556
|
+
for (const error of errors) {
|
|
557
|
+
lines.push(` - ${error.path}: ${error.message}`);
|
|
558
|
+
}
|
|
559
|
+
lines.push('');
|
|
560
|
+
}
|
|
561
|
+
// Warnings
|
|
562
|
+
if (warnings.length > 0) {
|
|
563
|
+
lines.push('WARNINGS:');
|
|
564
|
+
for (const warning of warnings) {
|
|
565
|
+
lines.push(` - ${warning.path}: ${warning.message}`);
|
|
566
|
+
}
|
|
567
|
+
lines.push('');
|
|
568
|
+
}
|
|
569
|
+
// Defaults applied
|
|
570
|
+
if (diagnostics.defaultsApplied.length > 0) {
|
|
571
|
+
lines.push('DEFAULTS APPLIED:');
|
|
572
|
+
for (const defaultApplied of diagnostics.defaultsApplied) {
|
|
573
|
+
lines.push(` - ${defaultApplied}`);
|
|
574
|
+
}
|
|
575
|
+
lines.push('');
|
|
576
|
+
}
|
|
577
|
+
// Current configuration
|
|
578
|
+
lines.push('CURRENT CONFIGURATION:');
|
|
579
|
+
lines.push(` Fallback Models: ${JSON.stringify(diagnostics.config.fallbackModels.map(m => `${m.providerID}/${m.modelID}`))}`);
|
|
580
|
+
lines.push(` Cooldown: ${diagnostics.config.cooldownMs}ms`);
|
|
581
|
+
lines.push(` Enabled: ${diagnostics.config.enabled}`);
|
|
582
|
+
lines.push(` Fallback Mode: ${diagnostics.config.fallbackMode}`);
|
|
583
|
+
lines.push(` Health-Based Selection: ${diagnostics.config.enableHealthBasedSelection ?? false}`);
|
|
584
|
+
lines.push(` Verbose: ${diagnostics.config.verbose ?? false}`);
|
|
585
|
+
lines.push('');
|
|
586
|
+
// Retry policy
|
|
587
|
+
if (diagnostics.config.retryPolicy) {
|
|
588
|
+
lines.push('RETRY POLICY:');
|
|
589
|
+
lines.push(` Max Retries: ${diagnostics.config.retryPolicy.maxRetries}`);
|
|
590
|
+
lines.push(` Strategy: ${diagnostics.config.retryPolicy.strategy}`);
|
|
591
|
+
lines.push(` Base Delay: ${diagnostics.config.retryPolicy.baseDelayMs}ms`);
|
|
592
|
+
lines.push(` Max Delay: ${diagnostics.config.retryPolicy.maxDelayMs}ms`);
|
|
593
|
+
lines.push(` Jitter Enabled: ${diagnostics.config.retryPolicy.jitterEnabled}`);
|
|
594
|
+
lines.push('');
|
|
595
|
+
}
|
|
596
|
+
// Circuit breaker
|
|
597
|
+
if (diagnostics.config.circuitBreaker) {
|
|
598
|
+
lines.push('CIRCUIT BREAKER:');
|
|
599
|
+
lines.push(` Enabled: ${diagnostics.config.circuitBreaker.enabled}`);
|
|
600
|
+
lines.push(` Failure Threshold: ${diagnostics.config.circuitBreaker.failureThreshold}`);
|
|
601
|
+
lines.push(` Recovery Timeout: ${diagnostics.config.circuitBreaker.recoveryTimeoutMs}ms`);
|
|
602
|
+
lines.push(` Half-Open Max Calls: ${diagnostics.config.circuitBreaker.halfOpenMaxCalls}`);
|
|
603
|
+
lines.push(` Success Threshold: ${diagnostics.config.circuitBreaker.successThreshold}`);
|
|
604
|
+
lines.push('');
|
|
605
|
+
}
|
|
606
|
+
// Metrics
|
|
607
|
+
if (diagnostics.config.metrics) {
|
|
608
|
+
lines.push('METRICS:');
|
|
609
|
+
lines.push(` Enabled: ${diagnostics.config.metrics.enabled}`);
|
|
610
|
+
lines.push(` Output: ${diagnostics.config.metrics.output.console ? 'console' : diagnostics.config.metrics.output.file || 'none'}`);
|
|
611
|
+
lines.push(` Format: ${diagnostics.config.metrics.output.format}`);
|
|
612
|
+
lines.push(` Reset Interval: ${diagnostics.config.metrics.resetInterval}`);
|
|
613
|
+
lines.push('');
|
|
614
|
+
}
|
|
615
|
+
lines.push('='.repeat(60));
|
|
616
|
+
return lines.join('\n');
|
|
617
|
+
}
|
|
618
|
+
}
|