@azumag/opencode-rate-limit-fallback 1.37.0 → 1.39.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/src/circuitbreaker/CircuitBreaker.d.ts +5 -0
- package/dist/src/circuitbreaker/CircuitBreaker.js +52 -46
- package/dist/src/config/defaults.d.ts +71 -0
- package/dist/src/config/defaults.js +91 -0
- package/dist/src/health/HealthTracker.d.ts +1 -0
- package/dist/src/health/HealthTracker.js +17 -24
- package/dist/src/retry/RetryManager.d.ts +8 -0
- package/dist/src/retry/RetryManager.js +62 -0
- package/dist/src/types/index.d.ts +28 -3
- package/dist/src/types/index.js +3 -1
- package/dist/src/utils/config.js +12 -19
- package/package.json +1 -1
|
@@ -37,6 +37,11 @@ export declare class CircuitBreaker {
|
|
|
37
37
|
* @returns The current circuit state
|
|
38
38
|
*/
|
|
39
39
|
getState(modelKey: string): CircuitBreakerState;
|
|
40
|
+
/**
|
|
41
|
+
* Show toast notification for circuit state transition
|
|
42
|
+
* @private
|
|
43
|
+
*/
|
|
44
|
+
private showStateTransitionToast;
|
|
40
45
|
/**
|
|
41
46
|
* Clean up stale entries from the circuits map
|
|
42
47
|
*/
|
|
@@ -44,17 +44,8 @@ export class CircuitBreaker {
|
|
|
44
44
|
oldState: oldStateType,
|
|
45
45
|
newState: newStateType,
|
|
46
46
|
});
|
|
47
|
-
// Show toast notification for
|
|
48
|
-
|
|
49
|
-
safeShowToast(this.client, {
|
|
50
|
-
body: {
|
|
51
|
-
title: "Circuit Recovery Attempt",
|
|
52
|
-
message: `Attempting recovery for ${modelKey} after ${this.config.recoveryTimeoutMs}ms`,
|
|
53
|
-
variant: "info",
|
|
54
|
-
duration: 3000,
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
}
|
|
47
|
+
// Show toast notification for state transition
|
|
48
|
+
this.showStateTransitionToast(modelKey, oldStateType, newStateType);
|
|
58
49
|
// Record metrics
|
|
59
50
|
if (this.metricsManager) {
|
|
60
51
|
this.metricsManager.recordCircuitBreakerStateTransition(modelKey, oldStateType, newStateType);
|
|
@@ -74,23 +65,14 @@ export class CircuitBreaker {
|
|
|
74
65
|
const oldState = circuit.getState().state;
|
|
75
66
|
circuit.onSuccess();
|
|
76
67
|
const newState = circuit.getState().state;
|
|
77
|
-
// Log state transition
|
|
68
|
+
// Log state transition and show toast
|
|
78
69
|
if (oldState !== newState) {
|
|
79
70
|
this.logger.info(`Circuit breaker state changed for ${modelKey}`, {
|
|
80
71
|
oldState,
|
|
81
72
|
newState,
|
|
82
73
|
});
|
|
83
|
-
// Show toast notification for
|
|
84
|
-
|
|
85
|
-
safeShowToast(this.client, {
|
|
86
|
-
body: {
|
|
87
|
-
title: "Circuit Closed",
|
|
88
|
-
message: `Circuit breaker closed for ${modelKey} - service recovered`,
|
|
89
|
-
variant: "success",
|
|
90
|
-
duration: 3000,
|
|
91
|
-
},
|
|
92
|
-
});
|
|
93
|
-
}
|
|
74
|
+
// Show toast notification for state transition
|
|
75
|
+
this.showStateTransitionToast(modelKey, oldState, newState);
|
|
94
76
|
// Record metrics
|
|
95
77
|
if (this.metricsManager) {
|
|
96
78
|
this.metricsManager.recordCircuitBreakerStateTransition(modelKey, oldState, newState);
|
|
@@ -115,35 +97,15 @@ export class CircuitBreaker {
|
|
|
115
97
|
const oldState = circuit.getState().state;
|
|
116
98
|
circuit.onFailure();
|
|
117
99
|
const newState = circuit.getState().state;
|
|
118
|
-
// Log state transition
|
|
100
|
+
// Log state transition and show toast
|
|
119
101
|
if (oldState !== newState) {
|
|
120
102
|
this.logger.warn(`Circuit breaker state changed for ${modelKey}`, {
|
|
121
103
|
oldState,
|
|
122
104
|
newState,
|
|
123
105
|
failureCount: circuit.getState().failureCount,
|
|
124
106
|
});
|
|
125
|
-
// Show toast notification for
|
|
126
|
-
|
|
127
|
-
safeShowToast(this.client, {
|
|
128
|
-
body: {
|
|
129
|
-
title: "Circuit Opened",
|
|
130
|
-
message: `Circuit breaker opened for ${modelKey} after failure threshold`,
|
|
131
|
-
variant: "warning",
|
|
132
|
-
duration: 5000,
|
|
133
|
-
},
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
// Show toast notification for circuit close
|
|
137
|
-
if (newState === 'CLOSED' && oldState !== 'CLOSED' && this.client) {
|
|
138
|
-
safeShowToast(this.client, {
|
|
139
|
-
body: {
|
|
140
|
-
title: "Circuit Closed",
|
|
141
|
-
message: `Circuit breaker closed for ${modelKey} - service recovered`,
|
|
142
|
-
variant: "success",
|
|
143
|
-
duration: 3000,
|
|
144
|
-
},
|
|
145
|
-
});
|
|
146
|
-
}
|
|
107
|
+
// Show toast notification for state transition
|
|
108
|
+
this.showStateTransitionToast(modelKey, oldState, newState);
|
|
147
109
|
// Record metrics
|
|
148
110
|
if (this.metricsManager) {
|
|
149
111
|
this.metricsManager.recordCircuitBreakerStateTransition(modelKey, oldState, newState);
|
|
@@ -169,6 +131,50 @@ export class CircuitBreaker {
|
|
|
169
131
|
}
|
|
170
132
|
return circuit.getState();
|
|
171
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Show toast notification for circuit state transition
|
|
136
|
+
* @private
|
|
137
|
+
*/
|
|
138
|
+
showStateTransitionToast(modelKey, oldState, newState) {
|
|
139
|
+
if (!this.client) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
switch (newState) {
|
|
143
|
+
case 'OPEN':
|
|
144
|
+
safeShowToast(this.client, {
|
|
145
|
+
body: {
|
|
146
|
+
title: "Circuit Opened",
|
|
147
|
+
message: `Circuit breaker opened for ${modelKey} after failure threshold`,
|
|
148
|
+
variant: "warning",
|
|
149
|
+
duration: 5000,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
break;
|
|
153
|
+
case 'HALF_OPEN':
|
|
154
|
+
safeShowToast(this.client, {
|
|
155
|
+
body: {
|
|
156
|
+
title: "Circuit Recovery Attempt",
|
|
157
|
+
message: `Attempting recovery for ${modelKey} after ${this.config.recoveryTimeoutMs}ms`,
|
|
158
|
+
variant: "info",
|
|
159
|
+
duration: 3000,
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
break;
|
|
163
|
+
case 'CLOSED':
|
|
164
|
+
// Only show toast for circuit close when transitioning from non-CLOSED state
|
|
165
|
+
if (oldState !== 'CLOSED') {
|
|
166
|
+
safeShowToast(this.client, {
|
|
167
|
+
body: {
|
|
168
|
+
title: "Circuit Closed",
|
|
169
|
+
message: `Circuit breaker closed for ${modelKey} - service recovered`,
|
|
170
|
+
variant: "success",
|
|
171
|
+
duration: 3000,
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
172
178
|
/**
|
|
173
179
|
* Clean up stale entries from the circuits map
|
|
174
180
|
*/
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default configuration constants
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Default health persistence path
|
|
6
|
+
*/
|
|
7
|
+
export declare const DEFAULT_HEALTH_PERSISTENCE_PATH: string;
|
|
8
|
+
/**
|
|
9
|
+
* Default health tracker configuration
|
|
10
|
+
*/
|
|
11
|
+
export declare const DEFAULT_HEALTH_TRACKER_CONFIG: {
|
|
12
|
+
readonly enabled: true;
|
|
13
|
+
readonly path: string;
|
|
14
|
+
readonly responseTimeThreshold: 2000;
|
|
15
|
+
readonly responseTimePenaltyDivisor: 200;
|
|
16
|
+
readonly failurePenaltyMultiplier: 15;
|
|
17
|
+
readonly minRequestsForReliableScore: 3;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Default retry policy configuration
|
|
21
|
+
*/
|
|
22
|
+
export declare const DEFAULT_RETRY_POLICY_CONFIG: {
|
|
23
|
+
readonly maxRetries: 3;
|
|
24
|
+
readonly strategy: "immediate";
|
|
25
|
+
readonly baseDelayMs: 1000;
|
|
26
|
+
readonly maxDelayMs: 30000;
|
|
27
|
+
readonly jitterEnabled: false;
|
|
28
|
+
readonly jitterFactor: 0.1;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Default polynomial retry parameters
|
|
32
|
+
*/
|
|
33
|
+
export declare const DEFAULT_POLYNOMIAL_BASE = 1.5;
|
|
34
|
+
export declare const DEFAULT_POLYNOMIAL_EXPONENT = 2;
|
|
35
|
+
/**
|
|
36
|
+
* Default circuit breaker configuration
|
|
37
|
+
*/
|
|
38
|
+
export declare const DEFAULT_CIRCUIT_BREAKER_CONFIG: {
|
|
39
|
+
readonly enabled: false;
|
|
40
|
+
readonly failureThreshold: 5;
|
|
41
|
+
readonly recoveryTimeoutMs: 60000;
|
|
42
|
+
readonly halfOpenMaxCalls: 1;
|
|
43
|
+
readonly successThreshold: 2;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Default cooldown period (ms)
|
|
47
|
+
*/
|
|
48
|
+
export declare const DEFAULT_COOLDOWN_MS: number;
|
|
49
|
+
/**
|
|
50
|
+
* Default fallback mode
|
|
51
|
+
*/
|
|
52
|
+
export declare const DEFAULT_FALLBACK_MODE: "cycle";
|
|
53
|
+
/**
|
|
54
|
+
* Default log configuration
|
|
55
|
+
*/
|
|
56
|
+
export declare const DEFAULT_LOG_CONFIG: {
|
|
57
|
+
readonly level: "warn";
|
|
58
|
+
readonly format: "simple";
|
|
59
|
+
readonly enableTimestamp: true;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Default metrics configuration
|
|
63
|
+
*/
|
|
64
|
+
export declare const DEFAULT_METRICS_CONFIG: {
|
|
65
|
+
readonly enabled: false;
|
|
66
|
+
readonly output: {
|
|
67
|
+
readonly console: true;
|
|
68
|
+
readonly format: "pretty";
|
|
69
|
+
};
|
|
70
|
+
readonly resetInterval: "daily";
|
|
71
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default configuration constants
|
|
3
|
+
*/
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Health Tracker Defaults
|
|
8
|
+
// ============================================================================
|
|
9
|
+
/**
|
|
10
|
+
* Default health persistence path
|
|
11
|
+
*/
|
|
12
|
+
export const DEFAULT_HEALTH_PERSISTENCE_PATH = join(homedir(), '.opencode', 'rate-limit-fallback-health.json');
|
|
13
|
+
/**
|
|
14
|
+
* Default health tracker configuration
|
|
15
|
+
*/
|
|
16
|
+
export const DEFAULT_HEALTH_TRACKER_CONFIG = {
|
|
17
|
+
enabled: true,
|
|
18
|
+
path: DEFAULT_HEALTH_PERSISTENCE_PATH,
|
|
19
|
+
responseTimeThreshold: 2000, // ms - threshold for response time penalty
|
|
20
|
+
responseTimePenaltyDivisor: 200, // divisor for response time penalty calculation
|
|
21
|
+
failurePenaltyMultiplier: 15, // penalty per consecutive failure
|
|
22
|
+
minRequestsForReliableScore: 3, // min requests before score is reliable
|
|
23
|
+
};
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Retry Defaults
|
|
26
|
+
// ============================================================================
|
|
27
|
+
/**
|
|
28
|
+
* Default retry policy configuration
|
|
29
|
+
*/
|
|
30
|
+
export const DEFAULT_RETRY_POLICY_CONFIG = {
|
|
31
|
+
maxRetries: 3,
|
|
32
|
+
strategy: "immediate",
|
|
33
|
+
baseDelayMs: 1000,
|
|
34
|
+
maxDelayMs: 30000,
|
|
35
|
+
jitterEnabled: false,
|
|
36
|
+
jitterFactor: 0.1,
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Default polynomial retry parameters
|
|
40
|
+
*/
|
|
41
|
+
export const DEFAULT_POLYNOMIAL_BASE = 1.5;
|
|
42
|
+
export const DEFAULT_POLYNOMIAL_EXPONENT = 2;
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// Circuit Breaker Defaults
|
|
45
|
+
// ============================================================================
|
|
46
|
+
/**
|
|
47
|
+
* Default circuit breaker configuration
|
|
48
|
+
*/
|
|
49
|
+
export const DEFAULT_CIRCUIT_BREAKER_CONFIG = {
|
|
50
|
+
enabled: false,
|
|
51
|
+
failureThreshold: 5,
|
|
52
|
+
recoveryTimeoutMs: 60000,
|
|
53
|
+
halfOpenMaxCalls: 1,
|
|
54
|
+
successThreshold: 2,
|
|
55
|
+
};
|
|
56
|
+
// ============================================================================
|
|
57
|
+
// Plugin Defaults
|
|
58
|
+
// ============================================================================
|
|
59
|
+
/**
|
|
60
|
+
* Default cooldown period (ms)
|
|
61
|
+
*/
|
|
62
|
+
export const DEFAULT_COOLDOWN_MS = 60 * 1000;
|
|
63
|
+
/**
|
|
64
|
+
* Default fallback mode
|
|
65
|
+
*/
|
|
66
|
+
export const DEFAULT_FALLBACK_MODE = "cycle";
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// Logging Defaults
|
|
69
|
+
// ============================================================================
|
|
70
|
+
/**
|
|
71
|
+
* Default log configuration
|
|
72
|
+
*/
|
|
73
|
+
export const DEFAULT_LOG_CONFIG = {
|
|
74
|
+
level: "warn",
|
|
75
|
+
format: "simple",
|
|
76
|
+
enableTimestamp: true,
|
|
77
|
+
};
|
|
78
|
+
// ============================================================================
|
|
79
|
+
// Metrics Defaults
|
|
80
|
+
// ============================================================================
|
|
81
|
+
/**
|
|
82
|
+
* Default metrics configuration
|
|
83
|
+
*/
|
|
84
|
+
export const DEFAULT_METRICS_CONFIG = {
|
|
85
|
+
enabled: false,
|
|
86
|
+
output: {
|
|
87
|
+
console: true,
|
|
88
|
+
format: "pretty",
|
|
89
|
+
},
|
|
90
|
+
resetInterval: "daily",
|
|
91
|
+
};
|
|
@@ -18,6 +18,7 @@ export declare class HealthTracker {
|
|
|
18
18
|
private responseTimeThreshold;
|
|
19
19
|
private responseTimePenaltyDivisor;
|
|
20
20
|
private failurePenaltyMultiplier;
|
|
21
|
+
private minRequestsForReliableScore;
|
|
21
22
|
private persistenceDebounceMs;
|
|
22
23
|
constructor(config: PluginConfig, logger: Logger);
|
|
23
24
|
/**
|
|
@@ -4,23 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { getModelKey } from '../utils/helpers.js';
|
|
6
6
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { dirname } from 'path';
|
|
8
|
+
import { DEFAULT_HEALTH_TRACKER_CONFIG } from '../config/defaults.js';
|
|
9
9
|
/**
|
|
10
|
-
* Default health
|
|
10
|
+
* Default health tracker configuration
|
|
11
11
|
*/
|
|
12
|
-
const
|
|
13
|
-
/**
|
|
14
|
-
* Minimum requests before health score is considered reliable
|
|
15
|
-
*/
|
|
16
|
-
const MIN_REQUESTS_FOR_RELIABLE_SCORE = 3;
|
|
17
|
-
/**
|
|
18
|
-
* Default health configuration
|
|
19
|
-
*/
|
|
20
|
-
const DEFAULT_HEALTH_CONFIG = {
|
|
21
|
-
enabled: true,
|
|
22
|
-
path: DEFAULT_HEALTH_PERSISTENCE_PATH,
|
|
23
|
-
};
|
|
12
|
+
const DEFAULT_HEALTH_CONFIG = DEFAULT_HEALTH_TRACKER_CONFIG;
|
|
24
13
|
/**
|
|
25
14
|
* Model Health Tracker class
|
|
26
15
|
*/
|
|
@@ -36,22 +25,26 @@ export class HealthTracker {
|
|
|
36
25
|
responseTimeThreshold;
|
|
37
26
|
responseTimePenaltyDivisor;
|
|
38
27
|
failurePenaltyMultiplier;
|
|
28
|
+
minRequestsForReliableScore;
|
|
39
29
|
persistenceDebounceMs;
|
|
40
30
|
constructor(config, logger) {
|
|
41
31
|
this.healthData = new Map();
|
|
42
32
|
// Parse health persistence config
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
33
|
+
const healthConfig = config.healthPersistence
|
|
34
|
+
? { ...DEFAULT_HEALTH_CONFIG, ...config.healthPersistence }
|
|
35
|
+
: DEFAULT_HEALTH_CONFIG;
|
|
36
|
+
this.persistenceEnabled = healthConfig.enabled !== false;
|
|
37
|
+
this.persistencePath = healthConfig.path || DEFAULT_HEALTH_CONFIG.path;
|
|
46
38
|
this.healthBasedSelectionEnabled = config.enableHealthBasedSelection || false;
|
|
47
39
|
// Initialize logger
|
|
48
40
|
this.logger = logger;
|
|
49
41
|
// Initialize save state
|
|
50
42
|
this.savePending = false;
|
|
51
|
-
// Initialize configurable thresholds
|
|
52
|
-
this.responseTimeThreshold =
|
|
53
|
-
this.responseTimePenaltyDivisor =
|
|
54
|
-
this.failurePenaltyMultiplier =
|
|
43
|
+
// Initialize configurable thresholds from config
|
|
44
|
+
this.responseTimeThreshold = healthConfig.responseTimeThreshold ?? DEFAULT_HEALTH_CONFIG.responseTimeThreshold;
|
|
45
|
+
this.responseTimePenaltyDivisor = healthConfig.responseTimePenaltyDivisor ?? DEFAULT_HEALTH_CONFIG.responseTimePenaltyDivisor;
|
|
46
|
+
this.failurePenaltyMultiplier = healthConfig.failurePenaltyMultiplier ?? DEFAULT_HEALTH_CONFIG.failurePenaltyMultiplier;
|
|
47
|
+
this.minRequestsForReliableScore = healthConfig.minRequestsForReliableScore ?? DEFAULT_HEALTH_CONFIG.minRequestsForReliableScore;
|
|
55
48
|
this.persistenceDebounceMs = 30000; // 30 seconds debounce for persistence
|
|
56
49
|
// Load existing state
|
|
57
50
|
if (this.persistenceEnabled) {
|
|
@@ -189,7 +182,7 @@ export class HealthTracker {
|
|
|
189
182
|
calculateHealthScore(health) {
|
|
190
183
|
let score = 100;
|
|
191
184
|
// Penalize based on success rate
|
|
192
|
-
if (health.totalRequests >=
|
|
185
|
+
if (health.totalRequests >= this.minRequestsForReliableScore) {
|
|
193
186
|
const successRate = health.successfulRequests / health.totalRequests;
|
|
194
187
|
score = Math.round(score * successRate);
|
|
195
188
|
}
|
|
@@ -310,7 +303,7 @@ export class HealthTracker {
|
|
|
310
303
|
const avgHealthScore = models.length > 0
|
|
311
304
|
? Math.round(models.reduce((sum, h) => sum + h.healthScore, 0) / models.length)
|
|
312
305
|
: 100;
|
|
313
|
-
const modelsWithReliableData = models.filter(h => h.totalRequests >=
|
|
306
|
+
const modelsWithReliableData = models.filter(h => h.totalRequests >= this.minRequestsForReliableScore).length;
|
|
314
307
|
return {
|
|
315
308
|
totalTracked: models.length,
|
|
316
309
|
totalRequests,
|
|
@@ -36,6 +36,14 @@ export declare class RetryManager {
|
|
|
36
36
|
* Calculate linear backoff delay
|
|
37
37
|
*/
|
|
38
38
|
private calculateLinearDelay;
|
|
39
|
+
/**
|
|
40
|
+
* Calculate polynomial backoff delay
|
|
41
|
+
*/
|
|
42
|
+
private calculatePolynomialDelay;
|
|
43
|
+
/**
|
|
44
|
+
* Calculate custom backoff delay
|
|
45
|
+
*/
|
|
46
|
+
private calculateCustomDelay;
|
|
39
47
|
/**
|
|
40
48
|
* Apply jitter to delay
|
|
41
49
|
*/
|
|
@@ -26,6 +26,10 @@ export class RetryManager {
|
|
|
26
26
|
this.logger.warn('Invalid strategy, using default', { strategy: this.config.strategy });
|
|
27
27
|
this.config.strategy = DEFAULT_RETRY_POLICY.strategy;
|
|
28
28
|
}
|
|
29
|
+
if (this.config.strategy === 'custom' && typeof this.config.customStrategy !== 'function') {
|
|
30
|
+
this.logger.warn('Custom strategy selected but customStrategy is not a function, using immediate');
|
|
31
|
+
this.config.strategy = 'immediate';
|
|
32
|
+
}
|
|
29
33
|
if (this.config.maxRetries < 0) {
|
|
30
34
|
this.logger.warn('Invalid maxRetries, using default', { maxRetries: this.config.maxRetries });
|
|
31
35
|
this.config.maxRetries = DEFAULT_RETRY_POLICY.maxRetries;
|
|
@@ -46,6 +50,14 @@ export class RetryManager {
|
|
|
46
50
|
this.logger.warn('Invalid jitterFactor, using default', { jitterFactor: this.config.jitterFactor });
|
|
47
51
|
this.config.jitterFactor = DEFAULT_RETRY_POLICY.jitterFactor;
|
|
48
52
|
}
|
|
53
|
+
if (this.config.polynomialBase !== undefined && this.config.polynomialBase <= 0) {
|
|
54
|
+
this.logger.warn('Invalid polynomialBase, using default', { polynomialBase: this.config.polynomialBase });
|
|
55
|
+
this.config.polynomialBase = DEFAULT_RETRY_POLICY.polynomialBase;
|
|
56
|
+
}
|
|
57
|
+
if (this.config.polynomialExponent !== undefined && this.config.polynomialExponent <= 0) {
|
|
58
|
+
this.logger.warn('Invalid polynomialExponent, using default', { polynomialExponent: this.config.polynomialExponent });
|
|
59
|
+
this.config.polynomialExponent = DEFAULT_RETRY_POLICY.polynomialExponent;
|
|
60
|
+
}
|
|
49
61
|
if (this.config.timeoutMs !== undefined && this.config.timeoutMs < 0) {
|
|
50
62
|
this.logger.warn('Invalid timeoutMs, ignoring', { timeoutMs: this.config.timeoutMs });
|
|
51
63
|
this.config.timeoutMs = undefined;
|
|
@@ -96,6 +108,12 @@ export class RetryManager {
|
|
|
96
108
|
case "linear":
|
|
97
109
|
delay = this.calculateLinearDelay(attempt.attemptCount);
|
|
98
110
|
break;
|
|
111
|
+
case "polynomial":
|
|
112
|
+
delay = this.calculatePolynomialDelay(attempt.attemptCount);
|
|
113
|
+
break;
|
|
114
|
+
case "custom":
|
|
115
|
+
delay = this.calculateCustomDelay(attempt.attemptCount);
|
|
116
|
+
break;
|
|
99
117
|
case "immediate":
|
|
100
118
|
default:
|
|
101
119
|
delay = 0;
|
|
@@ -121,6 +139,50 @@ export class RetryManager {
|
|
|
121
139
|
const linearDelay = this.config.baseDelayMs * (attemptCount + 1);
|
|
122
140
|
return Math.min(linearDelay, this.config.maxDelayMs);
|
|
123
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Calculate polynomial backoff delay
|
|
144
|
+
*/
|
|
145
|
+
calculatePolynomialDelay(attemptCount) {
|
|
146
|
+
const base = this.config.polynomialBase || 1.5;
|
|
147
|
+
const exponent = this.config.polynomialExponent || 2;
|
|
148
|
+
const polynomialDelay = this.config.baseDelayMs * Math.pow(base, attemptCount * exponent);
|
|
149
|
+
return Math.min(polynomialDelay, this.config.maxDelayMs);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Calculate custom backoff delay
|
|
153
|
+
*/
|
|
154
|
+
calculateCustomDelay(attemptCount) {
|
|
155
|
+
if (this.config.customStrategy) {
|
|
156
|
+
try {
|
|
157
|
+
const rawDelay = this.config.customStrategy(attemptCount);
|
|
158
|
+
// Validate and clamp the delay to valid range
|
|
159
|
+
const clampedDelay = Math.max(0, Math.min(rawDelay, this.config.maxDelayMs));
|
|
160
|
+
// Log warnings if value was clamped
|
|
161
|
+
if (rawDelay < 0) {
|
|
162
|
+
this.logger.warn('Custom strategy returned negative delay, clamping to 0', {
|
|
163
|
+
rawDelay,
|
|
164
|
+
attemptCount,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
else if (rawDelay > this.config.maxDelayMs) {
|
|
168
|
+
this.logger.warn('Custom strategy returned delay exceeding maxDelayMs, clamping', {
|
|
169
|
+
rawDelay,
|
|
170
|
+
maxDelayMs: this.config.maxDelayMs,
|
|
171
|
+
attemptCount,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return clampedDelay;
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
this.logger.error('Custom strategy function threw error, using immediate', { error, attemptCount });
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
this.logger.warn('Custom strategy selected but no customStrategy function provided, using immediate');
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
124
186
|
/**
|
|
125
187
|
* Apply jitter to delay
|
|
126
188
|
*/
|
|
@@ -19,8 +19,21 @@ export interface FallbackModel {
|
|
|
19
19
|
export type FallbackMode = "cycle" | "stop" | "retry-last";
|
|
20
20
|
/**
|
|
21
21
|
* Retry strategy type
|
|
22
|
+
* - "immediate": Retry immediately without delay
|
|
23
|
+
* - "exponential": Exponential backoff (baseDelayMs * 2^attempt)
|
|
24
|
+
* - "linear": Linear backoff (baseDelayMs * (attempt + 1))
|
|
25
|
+
* - "polynomial": Polynomial backoff (polynomialBase ^ polynomialExponent * attempt * baseDelayMs)
|
|
26
|
+
* - "custom": Use custom function (TypeScript/JS configuration only, not JSON)
|
|
22
27
|
*/
|
|
23
|
-
export type RetryStrategy = "immediate" | "exponential" | "linear" | "custom";
|
|
28
|
+
export type RetryStrategy = "immediate" | "exponential" | "linear" | "polynomial" | "custom";
|
|
29
|
+
/**
|
|
30
|
+
* Custom retry strategy function (for TypeScript/JavaScript configuration only)
|
|
31
|
+
* Note: This only works when configured programmatically in TypeScript/JS, not in JSON files.
|
|
32
|
+
* For JSON configuration, use named strategies like "polynomial".
|
|
33
|
+
* @param attemptCount - The current attempt count (0-indexed)
|
|
34
|
+
* @returns The delay in milliseconds before the next retry
|
|
35
|
+
*/
|
|
36
|
+
export type CustomRetryStrategyFn = (attemptCount: number) => number;
|
|
24
37
|
/**
|
|
25
38
|
* Retry policy configuration
|
|
26
39
|
*/
|
|
@@ -32,6 +45,9 @@ export interface RetryPolicy {
|
|
|
32
45
|
jitterEnabled: boolean;
|
|
33
46
|
jitterFactor: number;
|
|
34
47
|
timeoutMs?: number;
|
|
48
|
+
polynomialBase?: number;
|
|
49
|
+
polynomialExponent?: number;
|
|
50
|
+
customStrategy?: CustomRetryStrategyFn;
|
|
35
51
|
}
|
|
36
52
|
/**
|
|
37
53
|
* Circuit breaker state
|
|
@@ -82,12 +98,21 @@ export interface ConfigValidationOptions {
|
|
|
82
98
|
logWarnings?: boolean;
|
|
83
99
|
}
|
|
84
100
|
/**
|
|
85
|
-
* Health
|
|
101
|
+
* Health tracker configuration
|
|
86
102
|
*/
|
|
87
|
-
export interface
|
|
103
|
+
export interface HealthTrackerConfig {
|
|
88
104
|
enabled: boolean;
|
|
89
105
|
path?: string;
|
|
106
|
+
responseTimeThreshold?: number;
|
|
107
|
+
responseTimePenaltyDivisor?: number;
|
|
108
|
+
failurePenaltyMultiplier?: number;
|
|
109
|
+
minRequestsForReliableScore?: number;
|
|
90
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Health persistence configuration (alias for HealthTrackerConfig)
|
|
113
|
+
* Use this for backward compatibility.
|
|
114
|
+
*/
|
|
115
|
+
export type HealthPersistenceConfig = HealthTrackerConfig;
|
|
91
116
|
/**
|
|
92
117
|
* Health metrics for a model
|
|
93
118
|
*/
|
package/dist/src/types/index.js
CHANGED
|
@@ -22,6 +22,8 @@ export const DEFAULT_RETRY_POLICY = {
|
|
|
22
22
|
maxDelayMs: 30000,
|
|
23
23
|
jitterEnabled: false,
|
|
24
24
|
jitterFactor: 0.1,
|
|
25
|
+
polynomialBase: 1.5,
|
|
26
|
+
polynomialExponent: 2,
|
|
25
27
|
};
|
|
26
28
|
/**
|
|
27
29
|
* Default circuit breaker configuration
|
|
@@ -40,7 +42,7 @@ export const VALID_FALLBACK_MODES = ["cycle", "stop", "retry-last"];
|
|
|
40
42
|
/**
|
|
41
43
|
* Valid retry strategies
|
|
42
44
|
*/
|
|
43
|
-
export const VALID_RETRY_STRATEGIES = ["immediate", "exponential", "linear", "custom"];
|
|
45
|
+
export const VALID_RETRY_STRATEGIES = ["immediate", "exponential", "linear", "polynomial", "custom"];
|
|
44
46
|
/**
|
|
45
47
|
* Valid reset intervals
|
|
46
48
|
*/
|
package/dist/src/utils/config.js
CHANGED
|
@@ -4,29 +4,20 @@
|
|
|
4
4
|
import { existsSync, readFileSync } from "fs";
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
import { DEFAULT_FALLBACK_MODELS, VALID_FALLBACK_MODES, VALID_RESET_INTERVALS, DEFAULT_RETRY_POLICY, VALID_RETRY_STRATEGIES, DEFAULT_CIRCUIT_BREAKER_CONFIG, } from '../types/index.js';
|
|
7
|
+
import { DEFAULT_HEALTH_TRACKER_CONFIG, DEFAULT_COOLDOWN_MS, DEFAULT_FALLBACK_MODE, DEFAULT_LOG_CONFIG, DEFAULT_METRICS_CONFIG, } from '../config/defaults.js';
|
|
7
8
|
/**
|
|
8
9
|
* Default plugin configuration
|
|
9
10
|
*/
|
|
10
11
|
export const DEFAULT_CONFIG = {
|
|
11
12
|
fallbackModels: DEFAULT_FALLBACK_MODELS,
|
|
12
|
-
cooldownMs:
|
|
13
|
+
cooldownMs: DEFAULT_COOLDOWN_MS,
|
|
13
14
|
enabled: true,
|
|
14
|
-
fallbackMode:
|
|
15
|
+
fallbackMode: DEFAULT_FALLBACK_MODE,
|
|
15
16
|
retryPolicy: DEFAULT_RETRY_POLICY,
|
|
16
17
|
circuitBreaker: DEFAULT_CIRCUIT_BREAKER_CONFIG,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
enableTimestamp: true,
|
|
21
|
-
},
|
|
22
|
-
metrics: {
|
|
23
|
-
enabled: false,
|
|
24
|
-
output: {
|
|
25
|
-
console: true,
|
|
26
|
-
format: "pretty",
|
|
27
|
-
},
|
|
28
|
-
resetInterval: "daily",
|
|
29
|
-
},
|
|
18
|
+
healthPersistence: DEFAULT_HEALTH_TRACKER_CONFIG,
|
|
19
|
+
log: DEFAULT_LOG_CONFIG,
|
|
20
|
+
metrics: DEFAULT_METRICS_CONFIG,
|
|
30
21
|
};
|
|
31
22
|
/**
|
|
32
23
|
* Validate configuration values
|
|
@@ -49,6 +40,10 @@ export function validateConfig(config) {
|
|
|
49
40
|
...DEFAULT_CONFIG.circuitBreaker,
|
|
50
41
|
...config.circuitBreaker,
|
|
51
42
|
} : DEFAULT_CONFIG.circuitBreaker,
|
|
43
|
+
healthPersistence: config.healthPersistence ? {
|
|
44
|
+
...DEFAULT_CONFIG.healthPersistence,
|
|
45
|
+
...config.healthPersistence,
|
|
46
|
+
} : DEFAULT_CONFIG.healthPersistence,
|
|
52
47
|
log: config.log ? { ...DEFAULT_CONFIG.log, ...config.log } : DEFAULT_CONFIG.log,
|
|
53
48
|
metrics: config.metrics ? {
|
|
54
49
|
...DEFAULT_CONFIG.metrics,
|
|
@@ -89,10 +84,8 @@ export function loadConfig(directory, worktree) {
|
|
|
89
84
|
const userConfig = JSON.parse(content);
|
|
90
85
|
return { config: validateConfig(userConfig), source: configPath };
|
|
91
86
|
}
|
|
92
|
-
catch
|
|
93
|
-
//
|
|
94
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
95
|
-
console.error(`[RateLimitFallback] Failed to load config from ${configPath}:`, errorMessage);
|
|
87
|
+
catch {
|
|
88
|
+
// Skip invalid config files silently - caller will log via structured logger
|
|
96
89
|
}
|
|
97
90
|
}
|
|
98
91
|
}
|
package/package.json
CHANGED