@azumag/opencode-rate-limit-fallback 1.50.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/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,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic Prioritizer
|
|
3
|
+
* Dynamically prioritizes fallback models based on performance metrics
|
|
4
|
+
*/
|
|
5
|
+
import { getModelKey } from '../utils/helpers.js';
|
|
6
|
+
/**
|
|
7
|
+
* Dynamic Prioritizer class for calculating dynamic model scores
|
|
8
|
+
*/
|
|
9
|
+
export class DynamicPrioritizer {
|
|
10
|
+
config;
|
|
11
|
+
healthTracker;
|
|
12
|
+
logger;
|
|
13
|
+
metricsManager;
|
|
14
|
+
modelScores;
|
|
15
|
+
modelUsageHistory;
|
|
16
|
+
requestCount;
|
|
17
|
+
constructor(config, healthTracker, logger, metricsManager) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
this.healthTracker = healthTracker;
|
|
20
|
+
this.logger = logger;
|
|
21
|
+
this.metricsManager = metricsManager;
|
|
22
|
+
this.modelScores = new Map();
|
|
23
|
+
this.modelUsageHistory = new Map();
|
|
24
|
+
this.requestCount = 0;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Record usage of a model for tracking recent activity
|
|
28
|
+
*/
|
|
29
|
+
recordUsage(providerID, modelID) {
|
|
30
|
+
if (!this.config.enabled) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const key = getModelKey(providerID, modelID);
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
let history = this.modelUsageHistory.get(key);
|
|
36
|
+
if (!history) {
|
|
37
|
+
history = [];
|
|
38
|
+
this.modelUsageHistory.set(key, history);
|
|
39
|
+
}
|
|
40
|
+
history.push(now);
|
|
41
|
+
// Trim history to max size
|
|
42
|
+
if (history.length > this.config.maxHistorySize) {
|
|
43
|
+
history.shift();
|
|
44
|
+
}
|
|
45
|
+
this.requestCount++;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Calculate dynamic score for a model
|
|
49
|
+
* Score is 0-1, higher is better
|
|
50
|
+
*/
|
|
51
|
+
calculateScore(providerID, modelID) {
|
|
52
|
+
if (!this.config.enabled) {
|
|
53
|
+
return 0.5; // Neutral score when disabled
|
|
54
|
+
}
|
|
55
|
+
const key = getModelKey(providerID, modelID);
|
|
56
|
+
const health = this.healthTracker.getModelHealth(providerID, modelID);
|
|
57
|
+
// Default values if no health data
|
|
58
|
+
let healthScore = 100;
|
|
59
|
+
let avgResponseTime = 0;
|
|
60
|
+
let recentUsageScore = 0;
|
|
61
|
+
if (health) {
|
|
62
|
+
healthScore = health.healthScore;
|
|
63
|
+
avgResponseTime = health.avgResponseTime;
|
|
64
|
+
}
|
|
65
|
+
// Normalize health score (0-100 -> 0-1)
|
|
66
|
+
const normalizedHealthScore = healthScore / 100;
|
|
67
|
+
// Normalize response time (inverse - faster is better)
|
|
68
|
+
const normalizedResponseTime = this.normalizeResponseTime(avgResponseTime);
|
|
69
|
+
// Calculate recent usage score
|
|
70
|
+
recentUsageScore = this.calculateRecentUsageScore(key);
|
|
71
|
+
// Calculate weighted score
|
|
72
|
+
const score = normalizedHealthScore * this.config.successRateWeight +
|
|
73
|
+
normalizedResponseTime * this.config.responseTimeWeight +
|
|
74
|
+
recentUsageScore * this.config.recentUsageWeight;
|
|
75
|
+
this.modelScores.set(key, score);
|
|
76
|
+
return score;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Get prioritized models based on dynamic scores
|
|
80
|
+
* Returns models sorted by score (highest first)
|
|
81
|
+
*/
|
|
82
|
+
getPrioritizedModels(candidates) {
|
|
83
|
+
if (!this.config.enabled) {
|
|
84
|
+
return candidates; // Return original order when disabled
|
|
85
|
+
}
|
|
86
|
+
// Check if we have enough samples for reliable ordering
|
|
87
|
+
if (!this.shouldUseDynamicOrdering()) {
|
|
88
|
+
return candidates;
|
|
89
|
+
}
|
|
90
|
+
// Map candidates with their scores
|
|
91
|
+
const scored = candidates.map(model => ({
|
|
92
|
+
model,
|
|
93
|
+
score: this.calculateScore(model.providerID, model.modelID),
|
|
94
|
+
}));
|
|
95
|
+
// Sort by score (descending)
|
|
96
|
+
scored.sort((a, b) => b.score - a.score);
|
|
97
|
+
// Check if the order actually changed (reorder occurred)
|
|
98
|
+
const reordered = candidates.map(m => getModelKey(m.providerID, m.modelID));
|
|
99
|
+
const sorted = scored.map(s => getModelKey(s.model.providerID, s.model.modelID));
|
|
100
|
+
const isReordered = JSON.stringify(reordered) !== JSON.stringify(sorted);
|
|
101
|
+
if (isReordered) {
|
|
102
|
+
// Record reorder event
|
|
103
|
+
if (this.metricsManager) {
|
|
104
|
+
this.metricsManager.recordDynamicPrioritizationReorder();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Return sorted models
|
|
108
|
+
return scored.map(item => item.model);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Check if dynamic ordering should be used
|
|
112
|
+
* Returns true if dynamic prioritization is enabled and we have enough data for reliable ordering
|
|
113
|
+
*/
|
|
114
|
+
shouldUseDynamicOrdering() {
|
|
115
|
+
if (!this.config.enabled) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
// Get health data for all tracked models
|
|
119
|
+
const healthData = this.healthTracker.getAllHealthData();
|
|
120
|
+
// Check if we have at least one model with enough samples to make dynamic ordering useful
|
|
121
|
+
const modelsWithEnoughSamples = healthData.filter(h => h.totalRequests >= this.config.minSamples);
|
|
122
|
+
// Use dynamic ordering if at least minSamples models have sufficient data
|
|
123
|
+
return modelsWithEnoughSamples.length >= this.config.minSamples;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Update configuration
|
|
127
|
+
*/
|
|
128
|
+
updateConfig(newConfig) {
|
|
129
|
+
this.config = newConfig;
|
|
130
|
+
this.logger.debug('DynamicPrioritizer configuration updated', {
|
|
131
|
+
enabled: newConfig.enabled,
|
|
132
|
+
updateInterval: newConfig.updateInterval,
|
|
133
|
+
weights: {
|
|
134
|
+
successRate: newConfig.successRateWeight,
|
|
135
|
+
responseTime: newConfig.responseTimeWeight,
|
|
136
|
+
recentUsage: newConfig.recentUsageWeight,
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
// Update metrics when config changes
|
|
140
|
+
if (this.metricsManager) {
|
|
141
|
+
this.metricsManager.updateDynamicPrioritizationMetrics(newConfig.enabled, 0, // Reorders are tracked separately
|
|
142
|
+
this.modelScores.size);
|
|
143
|
+
}
|
|
144
|
+
// Clear scores when config changes
|
|
145
|
+
if (!newConfig.enabled) {
|
|
146
|
+
this.modelScores.clear();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Get current scores for all tracked models
|
|
151
|
+
*/
|
|
152
|
+
getAllScores() {
|
|
153
|
+
return new Map(this.modelScores);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Check if dynamic prioritization is enabled
|
|
157
|
+
*/
|
|
158
|
+
isEnabled() {
|
|
159
|
+
return this.config.enabled;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Get number of models with calculated scores
|
|
163
|
+
*/
|
|
164
|
+
getModelsWithDynamicScores() {
|
|
165
|
+
return this.modelScores.size;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Update metrics with current dynamic prioritization state
|
|
169
|
+
*/
|
|
170
|
+
updateMetrics() {
|
|
171
|
+
if (!this.metricsManager) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
this.metricsManager.updateDynamicPrioritizationMetrics(this.config.enabled, 0, // Reorder count is cumulative, tracked separately
|
|
175
|
+
this.modelScores.size);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Reset all scores and usage history
|
|
179
|
+
*/
|
|
180
|
+
reset() {
|
|
181
|
+
this.modelScores.clear();
|
|
182
|
+
this.modelUsageHistory.clear();
|
|
183
|
+
this.requestCount = 0;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Normalize response time (inverse - faster is better)
|
|
187
|
+
* Returns 0-1, higher is better
|
|
188
|
+
*/
|
|
189
|
+
normalizeResponseTime(avgResponseTime) {
|
|
190
|
+
// Thresholds for normalization (in milliseconds)
|
|
191
|
+
const FAST_THRESHOLD = 500; // Below this is considered "fast"
|
|
192
|
+
const SLOW_THRESHOLD = 5000; // Above this is considered "slow"
|
|
193
|
+
if (avgResponseTime <= FAST_THRESHOLD) {
|
|
194
|
+
return 1.0; // Excellent
|
|
195
|
+
}
|
|
196
|
+
else if (avgResponseTime >= SLOW_THRESHOLD) {
|
|
197
|
+
return 0.1; // Poor (but not zero to allow for recovery)
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
// Linear interpolation between thresholds
|
|
201
|
+
const ratio = (avgResponseTime - FAST_THRESHOLD) / (SLOW_THRESHOLD - FAST_THRESHOLD);
|
|
202
|
+
return 1.0 - (ratio * 0.9); // Scale from 1.0 to 0.1
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Calculate recent usage score
|
|
207
|
+
* Returns 0-1, higher for more recent usage
|
|
208
|
+
*/
|
|
209
|
+
calculateRecentUsageScore(key) {
|
|
210
|
+
const history = this.modelUsageHistory.get(key);
|
|
211
|
+
if (!history || history.length === 0) {
|
|
212
|
+
return 0.0;
|
|
213
|
+
}
|
|
214
|
+
const now = Date.now();
|
|
215
|
+
const lastUsage = history[history.length - 1];
|
|
216
|
+
// Time since last usage (in hours)
|
|
217
|
+
const timeSinceLastUsage = (now - lastUsage) / (1000 * 60 * 60);
|
|
218
|
+
// Decay score over time (24 hour window)
|
|
219
|
+
const decay = Math.max(0, 1 - (timeSinceLastUsage / 24));
|
|
220
|
+
// Bonus for frequent usage (more history entries = higher score)
|
|
221
|
+
const frequencyBonus = Math.min(1, history.length / 10);
|
|
222
|
+
// Combine decay and frequency (weighted towards recent activity)
|
|
223
|
+
return (decay * 0.7) + (frequencyBonus * 0.3);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Confidence Scoring for Learned Patterns
|
|
3
|
+
*/
|
|
4
|
+
import type { PatternCandidate, ErrorPattern, LearningConfig } from '../types/index.js';
|
|
5
|
+
/**
|
|
6
|
+
* ConfidenceScorer - Calculates confidence scores for learned patterns
|
|
7
|
+
*/
|
|
8
|
+
export declare class ConfidenceScorer {
|
|
9
|
+
private config;
|
|
10
|
+
private knownPatterns;
|
|
11
|
+
constructor(config: LearningConfig, knownPatterns: ErrorPattern[]);
|
|
12
|
+
/**
|
|
13
|
+
* Calculate overall confidence score for a pattern
|
|
14
|
+
* @param pattern - The pattern candidate to score
|
|
15
|
+
* @param sampleCount - Number of times this pattern was seen
|
|
16
|
+
* @param learnedAt - Timestamp when pattern was first learned
|
|
17
|
+
* @returns Confidence score between 0 and 1
|
|
18
|
+
*/
|
|
19
|
+
calculateScore(pattern: PatternCandidate, sampleCount: number, learnedAt?: number): number;
|
|
20
|
+
/**
|
|
21
|
+
* Calculate frequency score based on how often the pattern occurs
|
|
22
|
+
* @param count - Number of times pattern was seen
|
|
23
|
+
* @param window - Learning window size
|
|
24
|
+
* @returns Score between 0 and 1
|
|
25
|
+
*/
|
|
26
|
+
calculateFrequencyScore(count: number, minFrequency: number): number;
|
|
27
|
+
/**
|
|
28
|
+
* Calculate similarity score based on how well pattern matches known patterns
|
|
29
|
+
* @param pattern - Pattern candidate to evaluate
|
|
30
|
+
* @returns Score between 0 and 1
|
|
31
|
+
*/
|
|
32
|
+
calculateSimilarityScore(pattern: PatternCandidate): number;
|
|
33
|
+
/**
|
|
34
|
+
* Calculate recency score based on when pattern was first learned
|
|
35
|
+
* @param learnedAt - Timestamp when pattern was learned
|
|
36
|
+
* @returns Score between 0 and 1
|
|
37
|
+
*/
|
|
38
|
+
calculateRecencyScore(learnedAt: number): number;
|
|
39
|
+
/**
|
|
40
|
+
* Check if pattern meets auto-approve threshold
|
|
41
|
+
* @param confidence - Confidence score
|
|
42
|
+
* @returns True if pattern should be auto-approved
|
|
43
|
+
*/
|
|
44
|
+
shouldAutoApprove(confidence: number): boolean;
|
|
45
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Confidence Scoring for Learned Patterns
|
|
3
|
+
*/
|
|
4
|
+
import { calculateJaccardSimilarity } from '../utils/similarity.js';
|
|
5
|
+
/**
|
|
6
|
+
* Weight for each confidence component
|
|
7
|
+
*/
|
|
8
|
+
const FREQUENCY_WEIGHT = 0.5;
|
|
9
|
+
const SIMILARITY_WEIGHT = 0.3;
|
|
10
|
+
const RECENCY_WEIGHT = 0.2;
|
|
11
|
+
/**
|
|
12
|
+
* Common rate limit words for similarity calculation
|
|
13
|
+
*/
|
|
14
|
+
const RATE_LIMIT_KEYWORDS = [
|
|
15
|
+
'rate', 'limit', 'quota', 'exceeded', 'too', 'many', 'requests',
|
|
16
|
+
'429', '429', 'exhausted', 'resource', 'daily', 'monthly', 'maximum',
|
|
17
|
+
'insufficient', 'per', 'minute', 'second', 'request',
|
|
18
|
+
];
|
|
19
|
+
/**
|
|
20
|
+
* ConfidenceScorer - Calculates confidence scores for learned patterns
|
|
21
|
+
*/
|
|
22
|
+
export class ConfidenceScorer {
|
|
23
|
+
config;
|
|
24
|
+
knownPatterns;
|
|
25
|
+
constructor(config, knownPatterns) {
|
|
26
|
+
this.config = config;
|
|
27
|
+
this.knownPatterns = knownPatterns;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Calculate overall confidence score for a pattern
|
|
31
|
+
* @param pattern - The pattern candidate to score
|
|
32
|
+
* @param sampleCount - Number of times this pattern was seen
|
|
33
|
+
* @param learnedAt - Timestamp when pattern was first learned
|
|
34
|
+
* @returns Confidence score between 0 and 1
|
|
35
|
+
*/
|
|
36
|
+
calculateScore(pattern, sampleCount, learnedAt) {
|
|
37
|
+
const frequencyScore = this.calculateFrequencyScore(sampleCount, this.config.minErrorFrequency);
|
|
38
|
+
const similarityScore = this.calculateSimilarityScore(pattern);
|
|
39
|
+
const recencyScore = learnedAt ? this.calculateRecencyScore(learnedAt) : 0.5;
|
|
40
|
+
// Weighted average
|
|
41
|
+
const confidence = (frequencyScore * FREQUENCY_WEIGHT) +
|
|
42
|
+
(similarityScore * SIMILARITY_WEIGHT) +
|
|
43
|
+
(recencyScore * RECENCY_WEIGHT);
|
|
44
|
+
// Clamp to [0, 1]
|
|
45
|
+
return Math.max(0, Math.min(1, confidence));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Calculate frequency score based on how often the pattern occurs
|
|
49
|
+
* @param count - Number of times pattern was seen
|
|
50
|
+
* @param window - Learning window size
|
|
51
|
+
* @returns Score between 0 and 1
|
|
52
|
+
*/
|
|
53
|
+
calculateFrequencyScore(count, minFrequency) {
|
|
54
|
+
if (count <= 0) {
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
// Normalize against minimum frequency threshold
|
|
58
|
+
// Patterns seen at least minFrequency times get baseline score
|
|
59
|
+
// More frequent patterns get higher scores
|
|
60
|
+
const normalized = Math.min(count / (minFrequency * 2), 1);
|
|
61
|
+
// Boost score for patterns seen at least minFrequency times
|
|
62
|
+
const baseline = count >= minFrequency ? 0.5 : 0;
|
|
63
|
+
return Math.max(0, Math.min(1, baseline + normalized * 0.5));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Calculate similarity score based on how well pattern matches known patterns
|
|
67
|
+
* @param pattern - Pattern candidate to evaluate
|
|
68
|
+
* @returns Score between 0 and 1
|
|
69
|
+
*/
|
|
70
|
+
calculateSimilarityScore(pattern) {
|
|
71
|
+
if (pattern.patterns.length === 0) {
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
// Calculate keyword overlap with known rate limit patterns
|
|
75
|
+
const patternText = pattern.patterns.join(' ').toLowerCase();
|
|
76
|
+
const keywordsFound = RATE_LIMIT_KEYWORDS.filter(keyword => patternText.includes(keyword));
|
|
77
|
+
// Base score from keyword matching
|
|
78
|
+
const keywordScore = keywordsFound.length / RATE_LIMIT_KEYWORDS.length;
|
|
79
|
+
// Bonus for patterns that match known patterns
|
|
80
|
+
let knownPatternBonus = 0;
|
|
81
|
+
if (this.knownPatterns.length > 0) {
|
|
82
|
+
for (const known of this.knownPatterns) {
|
|
83
|
+
for (const knownPatternStr of known.patterns) {
|
|
84
|
+
const knownText = typeof knownPatternStr === 'string' ? knownPatternStr : knownPatternStr.source;
|
|
85
|
+
const similarity = calculateJaccardSimilarity(patternText, knownText.toLowerCase());
|
|
86
|
+
knownPatternBonus = Math.max(knownPatternBonus, similarity);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Combine scores
|
|
91
|
+
return Math.max(0, Math.min(1, (keywordScore * 0.5) + (knownPatternBonus * 0.5)));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Calculate recency score based on when pattern was first learned
|
|
95
|
+
* @param learnedAt - Timestamp when pattern was learned
|
|
96
|
+
* @returns Score between 0 and 1
|
|
97
|
+
*/
|
|
98
|
+
calculateRecencyScore(learnedAt) {
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
const age = now - learnedAt;
|
|
101
|
+
// Learning window in milliseconds
|
|
102
|
+
const window = this.config.learningWindowMs;
|
|
103
|
+
// Recent patterns (within window) get higher scores
|
|
104
|
+
if (age <= window) {
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
// Older patterns get decaying score
|
|
108
|
+
// Score decays over time, but never goes to 0
|
|
109
|
+
const decayFactor = Math.exp(-age / (window * 10));
|
|
110
|
+
return Math.max(0.3, decayFactor);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Check if pattern meets auto-approve threshold
|
|
114
|
+
* @param confidence - Confidence score
|
|
115
|
+
* @returns True if pattern should be auto-approved
|
|
116
|
+
*/
|
|
117
|
+
shouldAutoApprove(confidence) {
|
|
118
|
+
return confidence >= this.config.autoApproveThreshold;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern Extraction from Rate Limit Errors
|
|
3
|
+
*/
|
|
4
|
+
import type { PatternCandidate } from '../types/index.js';
|
|
5
|
+
/**
|
|
6
|
+
* PatternExtractor - Extracts pattern candidates from error messages
|
|
7
|
+
*/
|
|
8
|
+
export declare class PatternExtractor {
|
|
9
|
+
/**
|
|
10
|
+
* Extract candidate patterns from an error
|
|
11
|
+
*/
|
|
12
|
+
extractPatterns(error: unknown): PatternCandidate[];
|
|
13
|
+
/**
|
|
14
|
+
* Extract provider ID from error
|
|
15
|
+
*/
|
|
16
|
+
extractProvider(error: unknown): string | null;
|
|
17
|
+
/**
|
|
18
|
+
* Extract HTTP status code from error
|
|
19
|
+
*/
|
|
20
|
+
extractStatusCode(error: unknown): number | null;
|
|
21
|
+
/**
|
|
22
|
+
* Extract common rate limit phrases from error
|
|
23
|
+
*/
|
|
24
|
+
extractPhrases(error: unknown): string[];
|
|
25
|
+
/**
|
|
26
|
+
* Validate if error is a valid object type
|
|
27
|
+
* @param error - The error to validate
|
|
28
|
+
* @returns True if error is a valid object
|
|
29
|
+
*/
|
|
30
|
+
private isValidErrorObject;
|
|
31
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern Extraction from Rate Limit Errors
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Common rate limit phrases to extract from errors
|
|
6
|
+
*/
|
|
7
|
+
const RATE_LIMIT_PHRASES = [
|
|
8
|
+
'rate limit',
|
|
9
|
+
'rate_limit',
|
|
10
|
+
'ratelimit',
|
|
11
|
+
'too many requests',
|
|
12
|
+
'quota exceeded',
|
|
13
|
+
'quota_exceeded',
|
|
14
|
+
'insufficient_quota',
|
|
15
|
+
'resource exhausted',
|
|
16
|
+
'resource_exhausted',
|
|
17
|
+
'daily limit exceeded',
|
|
18
|
+
'monthly limit exceeded',
|
|
19
|
+
'maximum requests',
|
|
20
|
+
'requests per minute',
|
|
21
|
+
'requests per second',
|
|
22
|
+
'request limit',
|
|
23
|
+
'request_limit',
|
|
24
|
+
'limit exceeded',
|
|
25
|
+
'limit_exceeded',
|
|
26
|
+
];
|
|
27
|
+
/**
|
|
28
|
+
* Known provider identifiers
|
|
29
|
+
*/
|
|
30
|
+
const KNOWN_PROVIDERS = ['anthropic', 'google', 'openai', 'azure', 'cohere', 'mistral', 'meta', 'huggingface', 'together'];
|
|
31
|
+
/**
|
|
32
|
+
* Rate limit status code regex patterns (pre-defined to avoid regex injection)
|
|
33
|
+
*/
|
|
34
|
+
const RATE_LIMIT_STATUS_REGEX = {
|
|
35
|
+
429: /\b429\b/gi,
|
|
36
|
+
503: /\b503\b/gi,
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* PatternExtractor - Extracts pattern candidates from error messages
|
|
40
|
+
*/
|
|
41
|
+
export class PatternExtractor {
|
|
42
|
+
/**
|
|
43
|
+
* Extract candidate patterns from an error
|
|
44
|
+
*/
|
|
45
|
+
extractPatterns(error) {
|
|
46
|
+
if (!this.isValidErrorObject(error)) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
const err = error;
|
|
50
|
+
// Extract error text
|
|
51
|
+
const responseBody = String(err.data?.responseBody || '');
|
|
52
|
+
const message = String(err.data?.message || err.message || '');
|
|
53
|
+
const name = String(err.name || '');
|
|
54
|
+
const statusCode = err.data?.statusCode?.toString() || '';
|
|
55
|
+
const errorCode = err.data?.code?.toString() || err.data?.type?.toString() || '';
|
|
56
|
+
// Combine all text sources for original text
|
|
57
|
+
const originalText = [responseBody, message, name, statusCode, errorCode].join(' ');
|
|
58
|
+
// Extract patterns
|
|
59
|
+
const patterns = [];
|
|
60
|
+
// Extract HTTP status code
|
|
61
|
+
const extractedStatusCode = this.extractStatusCode(error);
|
|
62
|
+
if (extractedStatusCode) {
|
|
63
|
+
// Use pre-defined regex pattern instead of constructing one
|
|
64
|
+
if (extractedStatusCode in RATE_LIMIT_STATUS_REGEX) {
|
|
65
|
+
patterns.push(RATE_LIMIT_STATUS_REGEX[extractedStatusCode]);
|
|
66
|
+
}
|
|
67
|
+
patterns.push(String(extractedStatusCode));
|
|
68
|
+
}
|
|
69
|
+
// Extract provider
|
|
70
|
+
const provider = this.extractProvider(error);
|
|
71
|
+
// Extract common phrases
|
|
72
|
+
const extractedPhrases = this.extractPhrases(error);
|
|
73
|
+
patterns.push(...extractedPhrases);
|
|
74
|
+
// Extract error codes
|
|
75
|
+
if (errorCode) {
|
|
76
|
+
patterns.push(errorCode.toLowerCase());
|
|
77
|
+
}
|
|
78
|
+
// Filter out empty patterns
|
|
79
|
+
const validPatterns = patterns.filter(p => p && p !== '');
|
|
80
|
+
if (validPatterns.length === 0) {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
return [{
|
|
84
|
+
provider: provider || undefined,
|
|
85
|
+
patterns: validPatterns,
|
|
86
|
+
sourceError: originalText,
|
|
87
|
+
extractedAt: Date.now(),
|
|
88
|
+
}];
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Extract provider ID from error
|
|
92
|
+
*/
|
|
93
|
+
extractProvider(error) {
|
|
94
|
+
if (!this.isValidErrorObject(error)) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const err = error;
|
|
98
|
+
const allText = [
|
|
99
|
+
String(err.name || ''),
|
|
100
|
+
String(err.message || ''),
|
|
101
|
+
String(err.data?.message || ''),
|
|
102
|
+
String(err.data?.responseBody || ''),
|
|
103
|
+
].join(' ').toLowerCase();
|
|
104
|
+
// Check for known provider names
|
|
105
|
+
for (const provider of KNOWN_PROVIDERS) {
|
|
106
|
+
if (allText.includes(provider)) {
|
|
107
|
+
return provider;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Extract HTTP status code from error
|
|
114
|
+
*/
|
|
115
|
+
extractStatusCode(error) {
|
|
116
|
+
if (!this.isValidErrorObject(error)) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const err = error;
|
|
120
|
+
const statusCode = err.data?.statusCode;
|
|
121
|
+
if (statusCode && typeof statusCode === 'number') {
|
|
122
|
+
return statusCode;
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Extract common rate limit phrases from error
|
|
128
|
+
*/
|
|
129
|
+
extractPhrases(error) {
|
|
130
|
+
if (!this.isValidErrorObject(error)) {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
const err = error;
|
|
134
|
+
const allText = [
|
|
135
|
+
String(err.name || ''),
|
|
136
|
+
String(err.message || ''),
|
|
137
|
+
String(err.data?.message || ''),
|
|
138
|
+
String(err.data?.responseBody || ''),
|
|
139
|
+
].join(' ').toLowerCase();
|
|
140
|
+
// Find matching phrases
|
|
141
|
+
const foundPhrases = [];
|
|
142
|
+
for (const phrase of RATE_LIMIT_PHRASES) {
|
|
143
|
+
if (allText.includes(phrase)) {
|
|
144
|
+
foundPhrases.push(phrase);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return foundPhrases;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Validate if error is a valid object type
|
|
151
|
+
* @param error - The error to validate
|
|
152
|
+
* @returns True if error is a valid object
|
|
153
|
+
*/
|
|
154
|
+
isValidErrorObject(error) {
|
|
155
|
+
return error !== null && typeof error === 'object';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern Learning from Rate Limit Errors
|
|
3
|
+
*/
|
|
4
|
+
import type { PatternCandidate, LearnedPattern, LearningConfig } from '../types/index.js';
|
|
5
|
+
import type { Logger } from '../../logger.js';
|
|
6
|
+
import { PatternExtractor } from './PatternExtractor.js';
|
|
7
|
+
import { ConfidenceScorer } from './ConfidenceScorer.js';
|
|
8
|
+
import { PatternStorage } from './PatternStorage.js';
|
|
9
|
+
/**
|
|
10
|
+
* PatternLearner - Orchestrates the learning process from errors
|
|
11
|
+
*/
|
|
12
|
+
export declare class PatternLearner {
|
|
13
|
+
private extractor;
|
|
14
|
+
private scorer;
|
|
15
|
+
private storage;
|
|
16
|
+
private config;
|
|
17
|
+
private logger;
|
|
18
|
+
private patternTracker;
|
|
19
|
+
private learnedPatterns;
|
|
20
|
+
constructor(extractor: PatternExtractor, scorer: ConfidenceScorer, storage: PatternStorage, config: LearningConfig, logger: Logger);
|
|
21
|
+
/**
|
|
22
|
+
* Learn from a rate limit error
|
|
23
|
+
*/
|
|
24
|
+
learnFromError(error: unknown): void;
|
|
25
|
+
/**
|
|
26
|
+
* Track a pattern candidate for learning
|
|
27
|
+
*/
|
|
28
|
+
private trackPattern;
|
|
29
|
+
/**
|
|
30
|
+
* Process tracked patterns and save those meeting criteria
|
|
31
|
+
*/
|
|
32
|
+
private processPatterns;
|
|
33
|
+
/**
|
|
34
|
+
* Merge similar patterns
|
|
35
|
+
*/
|
|
36
|
+
mergePatterns(patterns: PatternCandidate[]): PatternCandidate | null;
|
|
37
|
+
/**
|
|
38
|
+
* Load learned patterns from storage
|
|
39
|
+
*/
|
|
40
|
+
loadLearnedPatterns(): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Get all learned patterns
|
|
43
|
+
*/
|
|
44
|
+
getLearnedPatterns(): LearnedPattern[];
|
|
45
|
+
/**
|
|
46
|
+
* Get learned patterns for a specific provider
|
|
47
|
+
*/
|
|
48
|
+
getLearnedPatternsForProvider(provider: string): LearnedPattern[];
|
|
49
|
+
/**
|
|
50
|
+
* Add a learned pattern manually
|
|
51
|
+
*/
|
|
52
|
+
addLearnedPattern(pattern: LearnedPattern): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Remove a learned pattern
|
|
55
|
+
*/
|
|
56
|
+
removeLearnedPattern(name: string): Promise<boolean>;
|
|
57
|
+
/**
|
|
58
|
+
* Get a learned pattern by name
|
|
59
|
+
*/
|
|
60
|
+
getLearnedPatternByName(name: string): LearnedPattern | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Generate a unique key for a pattern candidate
|
|
63
|
+
*/
|
|
64
|
+
private generatePatternKey;
|
|
65
|
+
/**
|
|
66
|
+
* Generate a unique key for a learned pattern
|
|
67
|
+
*/
|
|
68
|
+
private generatePatternKeyFromLearned;
|
|
69
|
+
/**
|
|
70
|
+
* Generate a name for a pattern
|
|
71
|
+
*/
|
|
72
|
+
private generatePatternName;
|
|
73
|
+
/**
|
|
74
|
+
* Calculate priority for a learned pattern
|
|
75
|
+
*/
|
|
76
|
+
private calculatePriority;
|
|
77
|
+
/**
|
|
78
|
+
* Merge duplicate patterns in storage
|
|
79
|
+
*/
|
|
80
|
+
mergeDuplicatePatterns(): Promise<number>;
|
|
81
|
+
/**
|
|
82
|
+
* Cleanup old patterns
|
|
83
|
+
*/
|
|
84
|
+
cleanupOldPatterns(): Promise<number>;
|
|
85
|
+
/**
|
|
86
|
+
* Clear all tracked patterns
|
|
87
|
+
*/
|
|
88
|
+
clearTrackedPatterns(): void;
|
|
89
|
+
/**
|
|
90
|
+
* Get statistics about learning
|
|
91
|
+
*/
|
|
92
|
+
getStats(): {
|
|
93
|
+
trackedPatterns: number;
|
|
94
|
+
learnedPatterns: number;
|
|
95
|
+
pendingPatterns: number;
|
|
96
|
+
};
|
|
97
|
+
}
|