@azumag/opencode-rate-limit-fallback 1.50.0 → 1.58.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 +262 -0
- package/dist/src/errors/PatternRegistry.d.ts +58 -5
- package/dist/src/errors/PatternRegistry.js +182 -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/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 +11 -1
- 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
|
}
|
|
@@ -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
|
/**
|
|
@@ -113,6 +113,26 @@ export interface HealthTrackerConfig {
|
|
|
113
113
|
* Use this for backward compatibility.
|
|
114
114
|
*/
|
|
115
115
|
export type HealthPersistenceConfig = HealthTrackerConfig;
|
|
116
|
+
/**
|
|
117
|
+
* Dynamic prioritization configuration
|
|
118
|
+
*/
|
|
119
|
+
export interface DynamicPrioritizationConfig {
|
|
120
|
+
enabled: boolean;
|
|
121
|
+
updateInterval: number;
|
|
122
|
+
successRateWeight: number;
|
|
123
|
+
responseTimeWeight: number;
|
|
124
|
+
recentUsageWeight: number;
|
|
125
|
+
minSamples: number;
|
|
126
|
+
maxHistorySize: number;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Dynamic prioritization metrics
|
|
130
|
+
*/
|
|
131
|
+
export interface DynamicPrioritizationMetrics {
|
|
132
|
+
enabled: boolean;
|
|
133
|
+
reorders: number;
|
|
134
|
+
modelsWithDynamicScores: number;
|
|
135
|
+
}
|
|
116
136
|
/**
|
|
117
137
|
* Health metrics for a model
|
|
118
138
|
*/
|
|
@@ -139,12 +159,45 @@ export interface ErrorPattern {
|
|
|
139
159
|
patterns: (string | RegExp)[];
|
|
140
160
|
priority: number;
|
|
141
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Pattern candidate extracted from an error
|
|
164
|
+
*/
|
|
165
|
+
export interface PatternCandidate {
|
|
166
|
+
provider?: string;
|
|
167
|
+
patterns: (string | RegExp)[];
|
|
168
|
+
sourceError: string;
|
|
169
|
+
extractedAt: number;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Learned pattern with metadata
|
|
173
|
+
*/
|
|
174
|
+
export interface LearnedPattern extends ErrorPattern {
|
|
175
|
+
confidence: number;
|
|
176
|
+
learnedAt: string;
|
|
177
|
+
sampleCount: number;
|
|
178
|
+
lastUsed?: number;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Learning configuration
|
|
182
|
+
*/
|
|
183
|
+
export interface LearningConfig {
|
|
184
|
+
enabled: boolean;
|
|
185
|
+
autoApproveThreshold: number;
|
|
186
|
+
maxLearnedPatterns: number;
|
|
187
|
+
minErrorFrequency: number;
|
|
188
|
+
learningWindowMs: number;
|
|
189
|
+
}
|
|
142
190
|
/**
|
|
143
191
|
* Error pattern configuration
|
|
144
192
|
*/
|
|
145
193
|
export interface ErrorPatternsConfig {
|
|
146
194
|
custom?: ErrorPattern[];
|
|
147
195
|
enableLearning?: boolean;
|
|
196
|
+
learnedPatterns?: LearnedPattern[];
|
|
197
|
+
autoApproveThreshold?: number;
|
|
198
|
+
maxLearnedPatterns?: number;
|
|
199
|
+
minErrorFrequency?: number;
|
|
200
|
+
learningWindowMs?: number;
|
|
148
201
|
}
|
|
149
202
|
/**
|
|
150
203
|
* Configuration hot reload settings
|
|
@@ -193,6 +246,7 @@ export interface PluginConfig {
|
|
|
193
246
|
verbose?: boolean;
|
|
194
247
|
errorPatterns?: ErrorPatternsConfig;
|
|
195
248
|
configReload?: ConfigReloadConfig;
|
|
249
|
+
dynamicPrioritization?: DynamicPrioritizationConfig;
|
|
196
250
|
}
|
|
197
251
|
/**
|
|
198
252
|
* Fallback state for tracking progress
|
|
@@ -357,6 +411,7 @@ export interface MetricsData {
|
|
|
357
411
|
total: CircuitBreakerMetrics;
|
|
358
412
|
byModel: Map<string, CircuitBreakerMetrics>;
|
|
359
413
|
};
|
|
414
|
+
dynamicPrioritization: DynamicPrioritizationMetrics;
|
|
360
415
|
startedAt: number;
|
|
361
416
|
generatedAt: number;
|
|
362
417
|
}
|
package/dist/src/utils/config.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { existsSync, readFileSync } from "fs";
|
|
5
5
|
import { join, resolve, normalize, relative } 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, DEFAULT_CONFIG_RELOAD_CONFIG, } from '../config/defaults.js';
|
|
7
|
+
import { DEFAULT_HEALTH_TRACKER_CONFIG, DEFAULT_COOLDOWN_MS, DEFAULT_FALLBACK_MODE, DEFAULT_LOG_CONFIG, DEFAULT_METRICS_CONFIG, DEFAULT_CONFIG_RELOAD_CONFIG, DEFAULT_DYNAMIC_PRIORITIZATION_CONFIG, DEFAULT_ERROR_PATTERN_LEARNING_CONFIG, } from '../config/defaults.js';
|
|
8
8
|
/**
|
|
9
9
|
* Default plugin configuration
|
|
10
10
|
*/
|
|
@@ -19,6 +19,8 @@ export const DEFAULT_CONFIG = {
|
|
|
19
19
|
log: DEFAULT_LOG_CONFIG,
|
|
20
20
|
metrics: DEFAULT_METRICS_CONFIG,
|
|
21
21
|
configReload: DEFAULT_CONFIG_RELOAD_CONFIG,
|
|
22
|
+
dynamicPrioritization: DEFAULT_DYNAMIC_PRIORITIZATION_CONFIG,
|
|
23
|
+
errorPatterns: DEFAULT_ERROR_PATTERN_LEARNING_CONFIG,
|
|
22
24
|
};
|
|
23
25
|
/**
|
|
24
26
|
* Validate that a path does not contain directory traversal attempts
|
|
@@ -85,6 +87,14 @@ export function validateConfig(config) {
|
|
|
85
87
|
...DEFAULT_CONFIG.configReload,
|
|
86
88
|
...config.configReload,
|
|
87
89
|
} : DEFAULT_CONFIG.configReload,
|
|
90
|
+
dynamicPrioritization: config.dynamicPrioritization ? {
|
|
91
|
+
...DEFAULT_DYNAMIC_PRIORITIZATION_CONFIG,
|
|
92
|
+
...config.dynamicPrioritization,
|
|
93
|
+
} : DEFAULT_DYNAMIC_PRIORITIZATION_CONFIG,
|
|
94
|
+
errorPatterns: config.errorPatterns ? {
|
|
95
|
+
...DEFAULT_ERROR_PATTERN_LEARNING_CONFIG,
|
|
96
|
+
...config.errorPatterns,
|
|
97
|
+
} : DEFAULT_ERROR_PATTERN_LEARNING_CONFIG,
|
|
88
98
|
};
|
|
89
99
|
}
|
|
90
100
|
/**
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Similarity utility functions
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Calculate Jaccard similarity between two strings
|
|
6
|
+
* @param str1 - First string
|
|
7
|
+
* @param str2 - Second string
|
|
8
|
+
* @returns Similarity score between 0 and 1
|
|
9
|
+
*/
|
|
10
|
+
export declare function calculateJaccardSimilarity(str1: string, str2: string): number;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Similarity utility functions
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Calculate Jaccard similarity between two strings
|
|
6
|
+
* @param str1 - First string
|
|
7
|
+
* @param str2 - Second string
|
|
8
|
+
* @returns Similarity score between 0 and 1
|
|
9
|
+
*/
|
|
10
|
+
export function calculateJaccardSimilarity(str1, str2) {
|
|
11
|
+
// Tokenize strings
|
|
12
|
+
const tokens1 = new Set(str1.split(/\s+/).filter(t => t.length > 0));
|
|
13
|
+
const tokens2 = new Set(str2.split(/\s+/).filter(t => t.length > 0));
|
|
14
|
+
if (tokens1.size === 0 && tokens2.size === 0) {
|
|
15
|
+
return 1;
|
|
16
|
+
}
|
|
17
|
+
if (tokens1.size === 0 || tokens2.size === 0) {
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
// Calculate intersection and union
|
|
21
|
+
const intersection = new Set([...tokens1].filter(x => tokens2.has(x)));
|
|
22
|
+
const union = new Set([...tokens1, ...tokens2]);
|
|
23
|
+
return intersection.size / union.size;
|
|
24
|
+
}
|
package/package.json
CHANGED