@azumag/opencode-rate-limit-fallback 1.49.0 → 1.57.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 +219 -36
- package/dist/src/config/Validator.js +94 -0
- package/dist/src/config/defaults.d.ts +22 -0
- package/dist/src/config/defaults.js +28 -0
- package/dist/src/dynamic/DynamicPrioritizer.d.ts +74 -0
- package/dist/src/dynamic/DynamicPrioritizer.js +225 -0
- package/dist/src/errors/ConfidenceScorer.d.ts +45 -0
- package/dist/src/errors/ConfidenceScorer.js +120 -0
- package/dist/src/errors/PatternExtractor.d.ts +31 -0
- package/dist/src/errors/PatternExtractor.js +157 -0
- package/dist/src/errors/PatternLearner.d.ts +97 -0
- package/dist/src/errors/PatternLearner.js +257 -0
- package/dist/src/errors/PatternRegistry.d.ts +54 -5
- package/dist/src/errors/PatternRegistry.js +171 -8
- package/dist/src/errors/PatternStorage.d.ts +49 -0
- package/dist/src/errors/PatternStorage.js +234 -0
- package/dist/src/fallback/FallbackHandler.d.ts +3 -2
- package/dist/src/fallback/FallbackHandler.js +38 -5
- package/dist/src/fallback/ModelSelector.d.ts +7 -1
- package/dist/src/fallback/ModelSelector.js +14 -1
- package/dist/src/main/ConfigReloader.d.ts +8 -1
- package/dist/src/main/ConfigReloader.js +45 -1
- package/dist/src/metrics/MetricsManager.d.ts +8 -0
- package/dist/src/metrics/MetricsManager.js +45 -0
- package/dist/src/types/index.d.ts +55 -0
- package/dist/src/utils/config.js +49 -6
- package/dist/src/utils/similarity.d.ts +10 -0
- package/dist/src/utils/similarity.js +24 -0
- package/package.json +1 -1
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern Storage for Learned Error Patterns
|
|
3
|
+
*/
|
|
4
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
5
|
+
import { calculateJaccardSimilarity } from '../utils/similarity.js';
|
|
6
|
+
/**
|
|
7
|
+
* PatternStorage - Manages persistence of learned patterns to config file
|
|
8
|
+
*/
|
|
9
|
+
export class PatternStorage {
|
|
10
|
+
configPath;
|
|
11
|
+
logger;
|
|
12
|
+
constructor(configPath, logger) {
|
|
13
|
+
this.configPath = configPath;
|
|
14
|
+
this.logger = logger;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Save a learned pattern to config file
|
|
18
|
+
*/
|
|
19
|
+
async savePattern(pattern) {
|
|
20
|
+
try {
|
|
21
|
+
// Load existing config
|
|
22
|
+
const config = await this.loadConfig();
|
|
23
|
+
const errorPatterns = config.errorPatterns || {};
|
|
24
|
+
// Initialize learnedPatterns array if not exists
|
|
25
|
+
if (!errorPatterns.learnedPatterns) {
|
|
26
|
+
errorPatterns.learnedPatterns = [];
|
|
27
|
+
}
|
|
28
|
+
const learnedPatterns = errorPatterns.learnedPatterns;
|
|
29
|
+
// Check if pattern with same name already exists
|
|
30
|
+
const existingIndex = learnedPatterns.findIndex((p) => p.name === pattern.name);
|
|
31
|
+
if (existingIndex >= 0) {
|
|
32
|
+
// Update existing pattern
|
|
33
|
+
learnedPatterns[existingIndex] = pattern;
|
|
34
|
+
this.logger.debug(`[PatternStorage] Updated learned pattern: ${pattern.name}`);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
// Add new pattern
|
|
38
|
+
learnedPatterns.push(pattern);
|
|
39
|
+
this.logger.info(`[PatternStorage] Saved new learned pattern: ${pattern.name}`);
|
|
40
|
+
}
|
|
41
|
+
// Cleanup old patterns if exceeding limit
|
|
42
|
+
const maxPatterns = config.errorPatterns?.maxLearnedPatterns || 20;
|
|
43
|
+
if (learnedPatterns.length > maxPatterns) {
|
|
44
|
+
await this.cleanupOldPatterns(maxPatterns, learnedPatterns);
|
|
45
|
+
}
|
|
46
|
+
// Save config
|
|
47
|
+
await this.saveConfig(config);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
this.logger.error('[PatternStorage] Failed to save pattern', {
|
|
51
|
+
error: error instanceof Error ? error.message : String(error),
|
|
52
|
+
});
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Load all learned patterns from config file
|
|
58
|
+
*/
|
|
59
|
+
async loadPatterns() {
|
|
60
|
+
try {
|
|
61
|
+
const config = await this.loadConfig();
|
|
62
|
+
const learnedPatterns = config.errorPatterns?.learnedPatterns || [];
|
|
63
|
+
// Validate patterns
|
|
64
|
+
const validPatterns = learnedPatterns.filter((p) => this.isValidPattern(p));
|
|
65
|
+
if (validPatterns.length !== learnedPatterns.length) {
|
|
66
|
+
this.logger.warn(`[PatternStorage] Filtered out ${learnedPatterns.length - validPatterns.length} invalid patterns`);
|
|
67
|
+
}
|
|
68
|
+
return validPatterns;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
this.logger.error('[PatternStorage] Failed to load patterns', {
|
|
72
|
+
error: error instanceof Error ? error.message : String(error),
|
|
73
|
+
});
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Delete a pattern by name from config file
|
|
79
|
+
*/
|
|
80
|
+
async deletePattern(name) {
|
|
81
|
+
try {
|
|
82
|
+
const config = await this.loadConfig();
|
|
83
|
+
const errorPatterns = config.errorPatterns || {};
|
|
84
|
+
const learnedPatterns = errorPatterns.learnedPatterns || [];
|
|
85
|
+
const index = learnedPatterns.findIndex((p) => p.name === name);
|
|
86
|
+
if (index >= 0) {
|
|
87
|
+
learnedPatterns.splice(index, 1);
|
|
88
|
+
errorPatterns.learnedPatterns = learnedPatterns;
|
|
89
|
+
config.errorPatterns = errorPatterns;
|
|
90
|
+
await this.saveConfig(config);
|
|
91
|
+
this.logger.info(`[PatternStorage] Deleted learned pattern: ${name}`);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
this.logger.error('[PatternStorage] Failed to delete pattern', {
|
|
98
|
+
error: error instanceof Error ? error.message : String(error),
|
|
99
|
+
});
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Merge duplicate patterns with high similarity
|
|
105
|
+
*/
|
|
106
|
+
async mergeDuplicatePatterns() {
|
|
107
|
+
try {
|
|
108
|
+
const config = await this.loadConfig();
|
|
109
|
+
const errorPatterns = config.errorPatterns || {};
|
|
110
|
+
let learnedPatterns = errorPatterns.learnedPatterns || [];
|
|
111
|
+
const mergeThreshold = 0.8;
|
|
112
|
+
let mergedCount = 0;
|
|
113
|
+
// Find and merge similar patterns
|
|
114
|
+
const patternsToMerge = [];
|
|
115
|
+
for (let i = 0; i < learnedPatterns.length; i++) {
|
|
116
|
+
if (patternsToMerge.includes(i))
|
|
117
|
+
continue;
|
|
118
|
+
for (let j = i + 1; j < learnedPatterns.length; j++) {
|
|
119
|
+
if (patternsToMerge.includes(j))
|
|
120
|
+
continue;
|
|
121
|
+
const pattern1 = learnedPatterns[i];
|
|
122
|
+
const pattern2 = learnedPatterns[j];
|
|
123
|
+
// Only merge patterns from same provider
|
|
124
|
+
if (pattern1.provider !== pattern2.provider)
|
|
125
|
+
continue;
|
|
126
|
+
// Calculate similarity
|
|
127
|
+
const similarity = this.calculatePatternSimilarity(pattern1, pattern2);
|
|
128
|
+
if (similarity >= mergeThreshold) {
|
|
129
|
+
// Merge pattern2 into pattern1
|
|
130
|
+
pattern1.patterns = [...new Set([...pattern1.patterns, ...pattern2.patterns])];
|
|
131
|
+
pattern1.sampleCount += pattern2.sampleCount;
|
|
132
|
+
pattern1.confidence = Math.max(pattern1.confidence, pattern2.confidence);
|
|
133
|
+
patternsToMerge.push(j);
|
|
134
|
+
mergedCount++;
|
|
135
|
+
this.logger.info(`[PatternStorage] Merged pattern ${pattern2.name} into ${pattern1.name}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Remove merged patterns (in reverse order to preserve indices)
|
|
140
|
+
patternsToMerge.sort((a, b) => b - a);
|
|
141
|
+
for (const index of patternsToMerge) {
|
|
142
|
+
learnedPatterns.splice(index, 1);
|
|
143
|
+
}
|
|
144
|
+
if (mergedCount > 0) {
|
|
145
|
+
errorPatterns.learnedPatterns = learnedPatterns;
|
|
146
|
+
config.errorPatterns = errorPatterns;
|
|
147
|
+
await this.saveConfig(config);
|
|
148
|
+
}
|
|
149
|
+
return mergedCount;
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
this.logger.error('[PatternStorage] Failed to merge patterns', {
|
|
153
|
+
error: error instanceof Error ? error.message : String(error),
|
|
154
|
+
});
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Cleanup old patterns, keeping only the most confident ones
|
|
160
|
+
*/
|
|
161
|
+
async cleanupOldPatterns(maxCount, patterns) {
|
|
162
|
+
try {
|
|
163
|
+
const config = await this.loadConfig();
|
|
164
|
+
const errorPatterns = config.errorPatterns || {};
|
|
165
|
+
let learnedPatterns = patterns || errorPatterns.learnedPatterns || [];
|
|
166
|
+
if (learnedPatterns.length <= maxCount) {
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
// Sort by confidence (descending), then by sample count (descending), then by learnedAt (descending)
|
|
170
|
+
learnedPatterns.sort((a, b) => {
|
|
171
|
+
if (b.confidence !== a.confidence) {
|
|
172
|
+
return b.confidence - a.confidence;
|
|
173
|
+
}
|
|
174
|
+
if (b.sampleCount !== a.sampleCount) {
|
|
175
|
+
return b.sampleCount - a.sampleCount;
|
|
176
|
+
}
|
|
177
|
+
return new Date(b.learnedAt).getTime() - new Date(a.learnedAt).getTime();
|
|
178
|
+
});
|
|
179
|
+
const removedCount = learnedPatterns.length - maxCount;
|
|
180
|
+
learnedPatterns = learnedPatterns.slice(0, maxCount);
|
|
181
|
+
errorPatterns.learnedPatterns = learnedPatterns;
|
|
182
|
+
config.errorPatterns = errorPatterns;
|
|
183
|
+
await this.saveConfig(config);
|
|
184
|
+
this.logger.info(`[PatternStorage] Cleaned up ${removedCount} old patterns`);
|
|
185
|
+
return removedCount;
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
this.logger.error('[PatternStorage] Failed to cleanup patterns', {
|
|
189
|
+
error: error instanceof Error ? error.message : String(error),
|
|
190
|
+
});
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Load config file
|
|
196
|
+
*/
|
|
197
|
+
async loadConfig() {
|
|
198
|
+
try {
|
|
199
|
+
const content = await readFile(this.configPath, 'utf-8');
|
|
200
|
+
return JSON.parse(content);
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
// If file doesn't exist or is invalid, return empty config
|
|
204
|
+
return {};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Save config file
|
|
209
|
+
*/
|
|
210
|
+
async saveConfig(config) {
|
|
211
|
+
await writeFile(this.configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Validate pattern structure
|
|
215
|
+
*/
|
|
216
|
+
isValidPattern(pattern) {
|
|
217
|
+
return (pattern &&
|
|
218
|
+
typeof pattern === 'object' &&
|
|
219
|
+
typeof pattern.name === 'string' &&
|
|
220
|
+
Array.isArray(pattern.patterns) &&
|
|
221
|
+
typeof pattern.confidence === 'number' &&
|
|
222
|
+
typeof pattern.learnedAt === 'string' &&
|
|
223
|
+
typeof pattern.sampleCount === 'number' &&
|
|
224
|
+
typeof pattern.priority === 'number');
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Calculate similarity between two patterns
|
|
228
|
+
*/
|
|
229
|
+
calculatePatternSimilarity(pattern1, pattern2) {
|
|
230
|
+
const text1 = pattern1.patterns.join(' ').toLowerCase();
|
|
231
|
+
const text2 = pattern2.patterns.join(' ').toLowerCase();
|
|
232
|
+
return calculateJaccardSimilarity(text1, text2);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -25,6 +25,7 @@ export declare class FallbackHandler {
|
|
|
25
25
|
private retryManager;
|
|
26
26
|
private circuitBreaker?;
|
|
27
27
|
private healthTracker?;
|
|
28
|
+
private dynamicPrioritizer?;
|
|
28
29
|
constructor(config: PluginConfig, client: OpenCodeClient, logger: Logger, metricsManager: MetricsManager, subagentTracker: SubagentTracker, healthTracker?: HealthTracker);
|
|
29
30
|
/**
|
|
30
31
|
* Check and mark fallback in progress for deduplication
|
|
@@ -46,8 +47,8 @@ export declare class FallbackHandler {
|
|
|
46
47
|
*/
|
|
47
48
|
private abortSession;
|
|
48
49
|
/**
|
|
49
|
-
* Queue
|
|
50
|
-
* promptAsync FIRST queues pending work so
|
|
50
|
+
* Queue prompt asynchronously (non-blocking), then abort retry loop.
|
|
51
|
+
* promptAsync FIRST queues pending work so that server doesn't dispose on idle.
|
|
51
52
|
* abort SECOND cancels the retry loop; the server sees the queued prompt and processes it.
|
|
52
53
|
*/
|
|
53
54
|
retryWithModel(targetSessionID: string, model: FallbackModel, parts: MessagePart[], hierarchy: SessionHierarchy | null): Promise<void>;
|
|
@@ -6,6 +6,8 @@ import { ModelSelector } from './ModelSelector.js';
|
|
|
6
6
|
import { CircuitBreaker } from '../circuitbreaker/CircuitBreaker.js';
|
|
7
7
|
import { extractMessageParts, convertPartsToSDKFormat, safeShowToast, getStateKey, getModelKey, DEDUP_WINDOW_MS, STATE_TIMEOUT_MS } from '../utils/helpers.js';
|
|
8
8
|
import { RetryManager } from '../retry/RetryManager.js';
|
|
9
|
+
import { DynamicPrioritizer } from '../dynamic/DynamicPrioritizer.js';
|
|
10
|
+
import { DEFAULT_DYNAMIC_PRIORITIZATION_CONFIG } from '../config/defaults.js';
|
|
9
11
|
/**
|
|
10
12
|
* Fallback Handler class for orchestrating the fallback retry flow
|
|
11
13
|
*/
|
|
@@ -30,6 +32,8 @@ export class FallbackHandler {
|
|
|
30
32
|
circuitBreaker;
|
|
31
33
|
// Health tracker reference
|
|
32
34
|
healthTracker;
|
|
35
|
+
// Dynamic prioritizer reference
|
|
36
|
+
dynamicPrioritizer;
|
|
33
37
|
constructor(config, client, logger, metricsManager, subagentTracker, healthTracker) {
|
|
34
38
|
this.config = config;
|
|
35
39
|
this.client = client;
|
|
@@ -41,7 +45,12 @@ export class FallbackHandler {
|
|
|
41
45
|
if (config.circuitBreaker?.enabled) {
|
|
42
46
|
this.circuitBreaker = new CircuitBreaker(config.circuitBreaker, logger, metricsManager, client);
|
|
43
47
|
}
|
|
44
|
-
|
|
48
|
+
// Initialize dynamic prioritizer if enabled and health tracker is available
|
|
49
|
+
if (healthTracker && config.dynamicPrioritization?.enabled) {
|
|
50
|
+
const dynamicConfig = { ...DEFAULT_DYNAMIC_PRIORITIZATION_CONFIG, ...config.dynamicPrioritization };
|
|
51
|
+
this.dynamicPrioritizer = new DynamicPrioritizer(dynamicConfig, healthTracker, logger, metricsManager);
|
|
52
|
+
}
|
|
53
|
+
this.modelSelector = new ModelSelector(config, client, this.circuitBreaker, healthTracker, this.dynamicPrioritizer);
|
|
45
54
|
this.currentSessionModel = new Map();
|
|
46
55
|
this.modelRequestStartTimes = new Map();
|
|
47
56
|
this.retryState = new Map();
|
|
@@ -95,18 +104,22 @@ export class FallbackHandler {
|
|
|
95
104
|
}
|
|
96
105
|
}
|
|
97
106
|
/**
|
|
98
|
-
* Queue
|
|
99
|
-
* promptAsync FIRST queues pending work so
|
|
107
|
+
* Queue prompt asynchronously (non-blocking), then abort retry loop.
|
|
108
|
+
* promptAsync FIRST queues pending work so that server doesn't dispose on idle.
|
|
100
109
|
* abort SECOND cancels the retry loop; the server sees the queued prompt and processes it.
|
|
101
110
|
*/
|
|
102
111
|
async retryWithModel(targetSessionID, model, parts, hierarchy) {
|
|
103
|
-
//
|
|
112
|
+
// Record model usage for dynamic prioritization
|
|
113
|
+
if (this.dynamicPrioritizer) {
|
|
114
|
+
this.dynamicPrioritizer.recordUsage(model.providerID, model.modelID);
|
|
115
|
+
}
|
|
116
|
+
// Track new model for this session
|
|
104
117
|
this.currentSessionModel.set(targetSessionID, {
|
|
105
118
|
providerID: model.providerID,
|
|
106
119
|
modelID: model.modelID,
|
|
107
120
|
lastUpdated: Date.now(),
|
|
108
121
|
});
|
|
109
|
-
// If this is a root session with subagents, propagate
|
|
122
|
+
// If this is a root session with subagents, propagate model to all subagents
|
|
110
123
|
if (hierarchy) {
|
|
111
124
|
if (hierarchy.rootSessionID === targetSessionID) {
|
|
112
125
|
hierarchy.sharedFallbackState = "completed";
|
|
@@ -464,6 +477,26 @@ export class FallbackHandler {
|
|
|
464
477
|
this.modelSelector.setCircuitBreaker(undefined);
|
|
465
478
|
}
|
|
466
479
|
}
|
|
480
|
+
// Handle dynamic prioritizer configuration changes
|
|
481
|
+
const oldDynamicPrioritizerEnabled = this.dynamicPrioritizer !== undefined;
|
|
482
|
+
if (newConfig.dynamicPrioritization?.enabled !== oldDynamicPrioritizerEnabled) {
|
|
483
|
+
if (newConfig.dynamicPrioritization?.enabled && this.healthTracker) {
|
|
484
|
+
// Create new dynamic prioritizer
|
|
485
|
+
const dynamicConfig = { ...DEFAULT_DYNAMIC_PRIORITIZATION_CONFIG, ...newConfig.dynamicPrioritization };
|
|
486
|
+
this.dynamicPrioritizer = new DynamicPrioritizer(dynamicConfig, this.healthTracker, this.logger, this.metricsManager);
|
|
487
|
+
this.modelSelector.setDynamicPrioritizer(this.dynamicPrioritizer);
|
|
488
|
+
}
|
|
489
|
+
else if (!newConfig.dynamicPrioritization?.enabled) {
|
|
490
|
+
// Disable dynamic prioritizer
|
|
491
|
+
this.dynamicPrioritizer = undefined;
|
|
492
|
+
this.modelSelector.setDynamicPrioritizer(undefined);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
else if (this.dynamicPrioritizer && newConfig.dynamicPrioritization) {
|
|
496
|
+
// Update existing dynamic prioritizer config
|
|
497
|
+
const dynamicConfig = { ...DEFAULT_DYNAMIC_PRIORITIZATION_CONFIG, ...newConfig.dynamicPrioritization };
|
|
498
|
+
this.dynamicPrioritizer.updateConfig(dynamicConfig);
|
|
499
|
+
}
|
|
467
500
|
this.logger.debug('FallbackHandler configuration updated');
|
|
468
501
|
}
|
|
469
502
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import type { FallbackModel, PluginConfig, OpenCodeClient } from '../types/index.js';
|
|
5
5
|
import type { CircuitBreaker } from '../circuitbreaker/index.js';
|
|
6
6
|
import type { HealthTracker } from '../health/HealthTracker.js';
|
|
7
|
+
import type { DynamicPrioritizer } from '../dynamic/DynamicPrioritizer.js';
|
|
7
8
|
/**
|
|
8
9
|
* Model Selector class for handling model selection strategies
|
|
9
10
|
*/
|
|
@@ -13,7 +14,8 @@ export declare class ModelSelector {
|
|
|
13
14
|
private client;
|
|
14
15
|
private circuitBreaker?;
|
|
15
16
|
private healthTracker?;
|
|
16
|
-
|
|
17
|
+
private dynamicPrioritizer?;
|
|
18
|
+
constructor(config: PluginConfig, client: OpenCodeClient, circuitBreaker?: CircuitBreaker, healthTracker?: HealthTracker, dynamicPrioritizer?: DynamicPrioritizer);
|
|
17
19
|
/**
|
|
18
20
|
* Check if a model is currently rate limited
|
|
19
21
|
*/
|
|
@@ -50,4 +52,8 @@ export declare class ModelSelector {
|
|
|
50
52
|
* Set circuit breaker (for hot reload)
|
|
51
53
|
*/
|
|
52
54
|
setCircuitBreaker(circuitBreaker: CircuitBreaker | undefined): void;
|
|
55
|
+
/**
|
|
56
|
+
* Set dynamic prioritizer (for hot reload)
|
|
57
|
+
*/
|
|
58
|
+
setDynamicPrioritizer(dynamicPrioritizer: DynamicPrioritizer | undefined): void;
|
|
53
59
|
}
|
|
@@ -12,11 +12,13 @@ export class ModelSelector {
|
|
|
12
12
|
client;
|
|
13
13
|
circuitBreaker;
|
|
14
14
|
healthTracker;
|
|
15
|
-
|
|
15
|
+
dynamicPrioritizer;
|
|
16
|
+
constructor(config, client, circuitBreaker, healthTracker, dynamicPrioritizer) {
|
|
16
17
|
this.config = config;
|
|
17
18
|
this.client = client;
|
|
18
19
|
this.circuitBreaker = circuitBreaker;
|
|
19
20
|
this.healthTracker = healthTracker;
|
|
21
|
+
this.dynamicPrioritizer = dynamicPrioritizer;
|
|
20
22
|
this.rateLimitedModels = new Map();
|
|
21
23
|
}
|
|
22
24
|
/**
|
|
@@ -57,6 +59,11 @@ export class ModelSelector {
|
|
|
57
59
|
candidates.push(model);
|
|
58
60
|
}
|
|
59
61
|
}
|
|
62
|
+
// Apply dynamic prioritization if enabled
|
|
63
|
+
if (this.dynamicPrioritizer && this.dynamicPrioritizer.isEnabled() && this.dynamicPrioritizer.shouldUseDynamicOrdering()) {
|
|
64
|
+
const prioritizedCandidates = this.dynamicPrioritizer.getPrioritizedModels(candidates);
|
|
65
|
+
return prioritizedCandidates[0] || null;
|
|
66
|
+
}
|
|
60
67
|
// Sort by health score if health tracker is enabled
|
|
61
68
|
if (this.healthTracker && this.config.enableHealthBasedSelection) {
|
|
62
69
|
const healthiest = this.healthTracker.getHealthiestModels(candidates);
|
|
@@ -172,4 +179,10 @@ export class ModelSelector {
|
|
|
172
179
|
setCircuitBreaker(circuitBreaker) {
|
|
173
180
|
this.circuitBreaker = circuitBreaker;
|
|
174
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Set dynamic prioritizer (for hot reload)
|
|
184
|
+
*/
|
|
185
|
+
setDynamicPrioritizer(dynamicPrioritizer) {
|
|
186
|
+
this.dynamicPrioritizer = dynamicPrioritizer;
|
|
187
|
+
}
|
|
175
188
|
}
|
|
@@ -29,7 +29,10 @@ export declare class ConfigReloader {
|
|
|
29
29
|
private worktree?;
|
|
30
30
|
private notifyOnReload;
|
|
31
31
|
private reloadMetrics;
|
|
32
|
-
|
|
32
|
+
private minReloadIntervalMs;
|
|
33
|
+
private recentReloadAttempts;
|
|
34
|
+
private maxReloadAttemptsPerMinute;
|
|
35
|
+
constructor(config: PluginConfig, configPath: string | null, logger: Logger, validator: ConfigValidator, client: OpenCodeClient, components: ComponentRefs, directory: string, worktree?: string, notifyOnReload?: boolean, minReloadIntervalMs?: number, maxReloadAttemptsPerMinute?: number);
|
|
33
36
|
/**
|
|
34
37
|
* Reload configuration from file
|
|
35
38
|
*/
|
|
@@ -50,6 +53,10 @@ export declare class ConfigReloader {
|
|
|
50
53
|
* Get reload metrics
|
|
51
54
|
*/
|
|
52
55
|
getReloadMetrics(): ReloadMetrics;
|
|
56
|
+
/**
|
|
57
|
+
* Check if reload is allowed based on rate limiting
|
|
58
|
+
*/
|
|
59
|
+
private checkRateLimit;
|
|
53
60
|
/**
|
|
54
61
|
* Show success toast notification
|
|
55
62
|
*/
|
|
@@ -17,7 +17,11 @@ export class ConfigReloader {
|
|
|
17
17
|
worktree;
|
|
18
18
|
notifyOnReload;
|
|
19
19
|
reloadMetrics;
|
|
20
|
-
|
|
20
|
+
// Rate limiting for reload operations
|
|
21
|
+
minReloadIntervalMs;
|
|
22
|
+
recentReloadAttempts;
|
|
23
|
+
maxReloadAttemptsPerMinute;
|
|
24
|
+
constructor(config, configPath, logger, validator, client, components, directory, worktree, notifyOnReload = true, minReloadIntervalMs = 1000, maxReloadAttemptsPerMinute = 10) {
|
|
21
25
|
this.config = config;
|
|
22
26
|
this.configPath = configPath;
|
|
23
27
|
this.logger = logger;
|
|
@@ -32,6 +36,10 @@ export class ConfigReloader {
|
|
|
32
36
|
successfulReloads: 0,
|
|
33
37
|
failedReloads: 0,
|
|
34
38
|
};
|
|
39
|
+
// Rate limiting settings
|
|
40
|
+
this.minReloadIntervalMs = minReloadIntervalMs;
|
|
41
|
+
this.maxReloadAttemptsPerMinute = maxReloadAttemptsPerMinute;
|
|
42
|
+
this.recentReloadAttempts = [];
|
|
35
43
|
}
|
|
36
44
|
/**
|
|
37
45
|
* Reload configuration from file
|
|
@@ -41,9 +49,17 @@ export class ConfigReloader {
|
|
|
41
49
|
success: false,
|
|
42
50
|
timestamp: Date.now(),
|
|
43
51
|
};
|
|
52
|
+
// Rate limiting check
|
|
53
|
+
const rateLimitCheck = this.checkRateLimit();
|
|
54
|
+
if (!rateLimitCheck.allowed) {
|
|
55
|
+
result.error = rateLimitCheck.reason || 'Rate limit exceeded';
|
|
56
|
+
this.logger.warn(`Config reload blocked: ${result.error}`);
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
44
59
|
// Track reload metrics
|
|
45
60
|
this.reloadMetrics.totalReloads++;
|
|
46
61
|
this.reloadMetrics.lastReloadTime = result.timestamp;
|
|
62
|
+
this.recentReloadAttempts.push(result.timestamp);
|
|
47
63
|
if (!this.configPath) {
|
|
48
64
|
result.error = 'No config file path available';
|
|
49
65
|
this.reloadMetrics.failedReloads++;
|
|
@@ -173,6 +189,34 @@ export class ConfigReloader {
|
|
|
173
189
|
getReloadMetrics() {
|
|
174
190
|
return { ...this.reloadMetrics };
|
|
175
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Check if reload is allowed based on rate limiting
|
|
194
|
+
*/
|
|
195
|
+
checkRateLimit() {
|
|
196
|
+
const now = Date.now();
|
|
197
|
+
const oneMinuteAgo = now - 60000;
|
|
198
|
+
// Clean up old reload attempts
|
|
199
|
+
this.recentReloadAttempts = this.recentReloadAttempts.filter(timestamp => timestamp > oneMinuteAgo);
|
|
200
|
+
// Check minimum interval between reloads
|
|
201
|
+
if (this.reloadMetrics.lastReloadTime) {
|
|
202
|
+
const timeSinceLastReload = now - this.reloadMetrics.lastReloadTime;
|
|
203
|
+
if (timeSinceLastReload < this.minReloadIntervalMs) {
|
|
204
|
+
const waitTime = this.minReloadIntervalMs - timeSinceLastReload;
|
|
205
|
+
return {
|
|
206
|
+
allowed: false,
|
|
207
|
+
reason: `Too soon. Wait ${waitTime}ms before reloading`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// Check maximum attempts per minute
|
|
212
|
+
if (this.recentReloadAttempts.length >= this.maxReloadAttemptsPerMinute) {
|
|
213
|
+
return {
|
|
214
|
+
allowed: false,
|
|
215
|
+
reason: `Rate limit exceeded. Maximum ${this.maxReloadAttemptsPerMinute} reloads per minute`,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
return { allowed: true };
|
|
219
|
+
}
|
|
176
220
|
/**
|
|
177
221
|
* Show success toast notification
|
|
178
222
|
*/
|
|
@@ -65,6 +65,14 @@ export declare class MetricsManager {
|
|
|
65
65
|
* Record a circuit breaker state transition
|
|
66
66
|
*/
|
|
67
67
|
recordCircuitBreakerStateTransition(modelKey: string, oldState: CircuitBreakerStateType, newState: CircuitBreakerStateType): void;
|
|
68
|
+
/**
|
|
69
|
+
* Update dynamic prioritization metrics
|
|
70
|
+
*/
|
|
71
|
+
updateDynamicPrioritizationMetrics(enabled: boolean, reorders: number, modelsWithDynamicScores: number): void;
|
|
72
|
+
/**
|
|
73
|
+
* Record a model reorder event
|
|
74
|
+
*/
|
|
75
|
+
recordDynamicPrioritizationReorder(): void;
|
|
68
76
|
/**
|
|
69
77
|
* Helper method to update circuit breaker state counts
|
|
70
78
|
* @private
|
|
@@ -43,6 +43,11 @@ export class MetricsManager {
|
|
|
43
43
|
},
|
|
44
44
|
byModel: new Map(),
|
|
45
45
|
},
|
|
46
|
+
dynamicPrioritization: {
|
|
47
|
+
enabled: false,
|
|
48
|
+
reorders: 0,
|
|
49
|
+
modelsWithDynamicScores: 0,
|
|
50
|
+
},
|
|
46
51
|
startedAt: Date.now(),
|
|
47
52
|
generatedAt: Date.now(),
|
|
48
53
|
};
|
|
@@ -95,6 +100,11 @@ export class MetricsManager {
|
|
|
95
100
|
},
|
|
96
101
|
byModel: new Map(),
|
|
97
102
|
},
|
|
103
|
+
dynamicPrioritization: {
|
|
104
|
+
enabled: false,
|
|
105
|
+
reorders: 0,
|
|
106
|
+
modelsWithDynamicScores: 0,
|
|
107
|
+
},
|
|
98
108
|
startedAt: Date.now(),
|
|
99
109
|
generatedAt: Date.now(),
|
|
100
110
|
};
|
|
@@ -280,6 +290,24 @@ export class MetricsManager {
|
|
|
280
290
|
this.updateCircuitBreakerStateCounts(modelMetrics, oldState, newState);
|
|
281
291
|
this.metrics.circuitBreaker.byModel.set(modelKey, modelMetrics);
|
|
282
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* Update dynamic prioritization metrics
|
|
295
|
+
*/
|
|
296
|
+
updateDynamicPrioritizationMetrics(enabled, reorders, modelsWithDynamicScores) {
|
|
297
|
+
if (!this.config.enabled)
|
|
298
|
+
return;
|
|
299
|
+
this.metrics.dynamicPrioritization.enabled = enabled;
|
|
300
|
+
this.metrics.dynamicPrioritization.reorders = reorders;
|
|
301
|
+
this.metrics.dynamicPrioritization.modelsWithDynamicScores = modelsWithDynamicScores;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Record a model reorder event
|
|
305
|
+
*/
|
|
306
|
+
recordDynamicPrioritizationReorder() {
|
|
307
|
+
if (!this.config.enabled)
|
|
308
|
+
return;
|
|
309
|
+
this.metrics.dynamicPrioritization.reorders++;
|
|
310
|
+
}
|
|
283
311
|
/**
|
|
284
312
|
* Helper method to update circuit breaker state counts
|
|
285
313
|
* @private
|
|
@@ -350,6 +378,7 @@ export class MetricsManager {
|
|
|
350
378
|
...metrics.circuitBreaker,
|
|
351
379
|
byModel: Object.fromEntries(Array.from(metrics.circuitBreaker.byModel.entries()).map(([k, v]) => [k, v])),
|
|
352
380
|
},
|
|
381
|
+
dynamicPrioritization: metrics.dynamicPrioritization,
|
|
353
382
|
startedAt: metrics.startedAt,
|
|
354
383
|
generatedAt: metrics.generatedAt,
|
|
355
384
|
};
|
|
@@ -472,6 +501,13 @@ export class MetricsManager {
|
|
|
472
501
|
}
|
|
473
502
|
}
|
|
474
503
|
}
|
|
504
|
+
lines.push("");
|
|
505
|
+
// Dynamic Prioritization
|
|
506
|
+
lines.push("Dynamic Prioritization:");
|
|
507
|
+
lines.push("-".repeat(40));
|
|
508
|
+
lines.push(` Enabled: ${metrics.dynamicPrioritization.enabled ? 'Yes' : 'No'}`);
|
|
509
|
+
lines.push(` Reorders: ${metrics.dynamicPrioritization.reorders}`);
|
|
510
|
+
lines.push(` Models with dynamic scores: ${metrics.dynamicPrioritization.modelsWithDynamicScores}`);
|
|
475
511
|
return lines.join("\n");
|
|
476
512
|
}
|
|
477
513
|
/**
|
|
@@ -579,6 +615,15 @@ export class MetricsManager {
|
|
|
579
615
|
successRate,
|
|
580
616
|
].join(","));
|
|
581
617
|
}
|
|
618
|
+
lines.push("");
|
|
619
|
+
// Dynamic Prioritization CSV
|
|
620
|
+
lines.push("=== DYNAMIC_PRIORITIZATION ===");
|
|
621
|
+
lines.push("enabled,reorders,models_with_dynamic_scores");
|
|
622
|
+
lines.push([
|
|
623
|
+
metrics.dynamicPrioritization.enabled ? 'Yes' : 'No',
|
|
624
|
+
metrics.dynamicPrioritization.reorders,
|
|
625
|
+
metrics.dynamicPrioritization.modelsWithDynamicScores,
|
|
626
|
+
].join(","));
|
|
582
627
|
return lines.join("\n");
|
|
583
628
|
}
|
|
584
629
|
/**
|