@azumag/opencode-rate-limit-fallback 1.58.0 → 1.63.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.
@@ -1,8 +1,12 @@
1
1
  /**
2
- * Pattern Learning from Rate Limit Errors
2
+ * Pattern Learner for orchestrating error pattern learning
3
3
  */
4
+ import { PatternExtractor } from './PatternExtractor.js';
5
+ import { ConfidenceScorer } from './ConfidenceScorer.js';
6
+ import { PatternStorage } from './PatternStorage.js';
4
7
  /**
5
- * PatternLearner - Orchestrates the learning process from errors
8
+ * Pattern Learner class
9
+ * Orchestrates the learning process
6
10
  */
7
11
  export class PatternLearner {
8
12
  extractor;
@@ -10,253 +14,178 @@ export class PatternLearner {
10
14
  storage;
11
15
  config;
12
16
  logger;
13
- patternTracker = new Map();
14
- learnedPatterns = new Map();
15
- constructor(extractor, scorer, storage, config, logger) {
16
- this.extractor = extractor;
17
- this.scorer = scorer;
18
- this.storage = storage;
17
+ // Track patterns being learned
18
+ patternTracking;
19
+ // Statistics
20
+ stats = {
21
+ totalErrorsProcessed: 0,
22
+ patternsLearned: 0,
23
+ patternsRejected: 0,
24
+ };
25
+ /**
26
+ * Constructor
27
+ */
28
+ constructor(config, logger) {
19
29
  this.config = config;
20
- this.logger = logger;
21
- // Note: Patterns will be loaded asynchronously via loadLearnedPatterns()
22
- }
23
- /**
24
- * Learn from a rate limit error
25
- */
26
- learnFromError(error) {
27
- if (!this.config.enabled) {
28
- return;
29
- }
30
- try {
31
- // Extract pattern candidates
32
- const candidates = this.extractor.extractPatterns(error);
33
- for (const candidate of candidates) {
34
- this.trackPattern(candidate);
35
- }
36
- // Process and potentially save patterns
37
- // Fire-and-forget with proper error handling
38
- this.processPatterns().catch((error) => {
39
- this.logger.error('[PatternLearner] Failed to process patterns', {
40
- error: error instanceof Error ? error.message : String(error),
41
- });
42
- });
43
- }
44
- catch (error) {
45
- this.logger.error('[PatternLearner] Failed to learn from error', {
46
- error: error instanceof Error ? error.message : String(error),
47
- });
48
- }
30
+ this.extractor = new PatternExtractor();
31
+ this.scorer = new ConfidenceScorer(config);
32
+ this.storage = new PatternStorage(config);
33
+ this.patternTracking = new Map();
34
+ this.logger = logger || {
35
+ debug: () => { },
36
+ info: () => { },
37
+ warn: () => { },
38
+ error: () => { },
39
+ };
49
40
  }
50
41
  /**
51
- * Track a pattern candidate for learning
42
+ * Update configuration
52
43
  */
53
- trackPattern(candidate) {
54
- // Generate a key for this pattern
55
- const key = this.generatePatternKey(candidate);
56
- const existing = this.patternTracker.get(key);
57
- if (existing) {
58
- // Update existing tracker
59
- existing.lastSeen = Date.now();
60
- existing.count++;
61
- existing.samples.push(candidate.sourceError);
62
- // Keep only last 10 samples
63
- if (existing.samples.length > 10) {
64
- existing.samples.shift();
65
- }
66
- }
67
- else {
68
- // Create new tracker
69
- this.patternTracker.set(key, {
70
- pattern: candidate,
71
- firstSeen: Date.now(),
72
- lastSeen: Date.now(),
73
- count: 1,
74
- samples: [candidate.sourceError],
75
- });
76
- }
44
+ updateConfig(config) {
45
+ this.config = config;
46
+ this.scorer.updateConfig(config);
47
+ this.storage.updateConfig(config);
77
48
  }
78
49
  /**
79
- * Process tracked patterns and save those meeting criteria
50
+ * Set the config file path for storage
80
51
  */
81
- async processPatterns() {
82
- for (const [key, tracker] of this.patternTracker.entries()) {
83
- // Check if pattern meets frequency threshold
84
- if (tracker.count < this.config.minErrorFrequency) {
85
- continue;
86
- }
87
- // Check if pattern already exists in learned patterns
88
- if (this.learnedPatterns.has(key)) {
89
- continue;
90
- }
91
- // Calculate confidence score
92
- const confidence = this.scorer.calculateScore(tracker.pattern, tracker.count, tracker.firstSeen);
93
- // Check if pattern meets auto-approve threshold
94
- if (!this.scorer.shouldAutoApprove(confidence)) {
95
- this.logger.debug(`[PatternLearner] Pattern confidence ${confidence.toFixed(2)} below threshold ${this.config.autoApproveThreshold}, not auto-approving`);
96
- continue;
97
- }
98
- // Create learned pattern
99
- const learnedPattern = {
100
- name: this.generatePatternName(tracker.pattern),
101
- provider: tracker.pattern.provider,
102
- patterns: tracker.pattern.patterns,
103
- priority: this.calculatePriority(tracker.pattern, confidence),
104
- confidence,
105
- learnedAt: new Date(tracker.firstSeen).toISOString(),
106
- sampleCount: tracker.count,
107
- lastUsed: undefined,
108
- };
109
- // Save to storage
110
- await this.storage.savePattern(learnedPattern);
111
- // Add to learned patterns
112
- this.learnedPatterns.set(key, learnedPattern);
113
- // Remove from tracker
114
- this.patternTracker.delete(key);
115
- this.logger.info(`[PatternLearner] Learned new pattern: ${learnedPattern.name} (confidence: ${confidence.toFixed(2)})`);
116
- }
52
+ setConfigFilePath(path) {
53
+ this.storage.setConfigFilePath(path);
117
54
  }
118
55
  /**
119
- * Merge similar patterns
56
+ * Process an error and learn from it
120
57
  */
121
- mergePatterns(patterns) {
122
- if (patterns.length === 0) {
58
+ async processError(error) {
59
+ if (!this.config.enabled) {
60
+ this.logger.debug('Pattern learning is disabled, skipping');
123
61
  return null;
124
62
  }
125
- if (patterns.length === 1) {
126
- return patterns[0];
63
+ this.stats.totalErrorsProcessed++;
64
+ // Extract pattern from error
65
+ const candidate = this.extractor.extractPattern(error);
66
+ if (!candidate) {
67
+ return null;
127
68
  }
128
- // Merge all patterns into one
129
- const mergedPattern = {
130
- provider: patterns[0].provider,
131
- patterns: patterns.flatMap(p => p.patterns),
132
- sourceError: patterns.map(p => p.sourceError).join('; '),
133
- extractedAt: Math.max(...patterns.map(p => p.extractedAt)),
134
- };
135
- // Deduplicate patterns
136
- mergedPattern.patterns = [...new Set(mergedPattern.patterns)];
137
- return mergedPattern;
138
- }
139
- /**
140
- * Load learned patterns from storage
141
- */
142
- async loadLearnedPatterns() {
143
- try {
144
- const patterns = await this.storage.loadPatterns();
145
- for (const pattern of patterns) {
146
- const key = this.generatePatternKeyFromLearned(pattern);
147
- this.learnedPatterns.set(key, pattern);
148
- }
149
- this.logger.info(`[PatternLearner] Loaded ${patterns.length} learned patterns`);
69
+ // Check if provider is present (required for meaningful patterns)
70
+ if (!candidate.provider) {
71
+ this.logger.debug('No provider found in error, skipping pattern learning');
72
+ return null;
150
73
  }
151
- catch (error) {
152
- this.logger.error('[PatternLearner] Failed to load learned patterns', {
153
- error: error instanceof Error ? error.message : String(error),
154
- });
74
+ // Create a pattern key for tracking
75
+ const patternKey = this.createPatternKey(candidate);
76
+ // Update pattern tracking
77
+ const tracking = this.getOrCreateTracking(candidate, patternKey);
78
+ tracking.frequency++;
79
+ tracking.samples.push(candidate.rawText);
80
+ // Check if we should learn this pattern
81
+ if (tracking.frequency < this.config.minErrorFrequency) {
82
+ return null; // Not enough samples yet
155
83
  }
156
- }
157
- /**
158
- * Get all learned patterns
159
- */
160
- getLearnedPatterns() {
161
- return Array.from(this.learnedPatterns.values());
162
- }
163
- /**
164
- * Get learned patterns for a specific provider
165
- */
166
- getLearnedPatternsForProvider(provider) {
167
- return this.getLearnedPatterns().filter(p => !p.provider || p.provider === provider);
168
- }
169
- /**
170
- * Add a learned pattern manually
171
- */
172
- async addLearnedPattern(pattern) {
173
- const key = this.generatePatternKeyFromLearned(pattern);
174
- this.learnedPatterns.set(key, pattern);
175
- await this.storage.savePattern(pattern);
176
- }
177
- /**
178
- * Remove a learned pattern
179
- */
180
- async removeLearnedPattern(name) {
181
- const pattern = this.getLearnedPatternByName(name);
182
- if (pattern) {
183
- const key = this.generatePatternKeyFromLearned(pattern);
184
- this.learnedPatterns.delete(key);
185
- await this.storage.deletePattern(name);
186
- return true;
84
+ // Calculate confidence
85
+ const confidence = this.scorer.calculateConfidence(tracking.pattern, tracking.frequency, tracking.firstSeen, []);
86
+ // Check if we should learn and save this pattern
87
+ if (!this.scorer.shouldAutoApprove(confidence)) {
88
+ this.stats.patternsRejected++;
89
+ this.logger.debug(`Pattern confidence ${confidence} below threshold ${this.config.autoApproveThreshold}`);
90
+ return null;
187
91
  }
188
- return false;
92
+ // Create learned pattern
93
+ const learnedPattern = this.storage.createLearnedPattern(tracking.pattern, confidence, tracking.frequency);
94
+ // Save to storage
95
+ await this.saveLearnedPattern(learnedPattern);
96
+ // Clear tracking for this pattern
97
+ this.patternTracking.delete(patternKey);
98
+ this.stats.patternsLearned++;
99
+ this.logger.info(`Learned new pattern: ${learnedPattern.name} with confidence ${confidence}`);
100
+ return learnedPattern;
189
101
  }
190
102
  /**
191
- * Get a learned pattern by name
192
- */
193
- getLearnedPatternByName(name) {
194
- return this.getLearnedPatterns().find(p => p.name === name);
195
- }
196
- /**
197
- * Generate a unique key for a pattern candidate
103
+ * Load learned patterns from storage
198
104
  */
199
- generatePatternKey(pattern) {
200
- const provider = pattern.provider || 'generic';
201
- const patternStr = pattern.patterns.map(p => typeof p === 'string' ? p : p.source).join('|');
202
- return `${provider}:${patternStr}`;
105
+ async loadLearnedPatterns() {
106
+ const patterns = await this.storage.loadLearnedPatterns();
107
+ this.logger.debug(`Loaded ${patterns.length} learned patterns`);
108
+ return patterns;
203
109
  }
204
110
  /**
205
- * Generate a unique key for a learned pattern
111
+ * Save learned patterns
206
112
  */
207
- generatePatternKeyFromLearned(pattern) {
208
- const provider = pattern.provider || 'generic';
209
- const patternStr = pattern.patterns.map(p => typeof p === 'string' ? p : p.source).join('|');
210
- return `${provider}:${patternStr}`;
113
+ async saveLearnedPatterns(patterns) {
114
+ const merged = this.storage.mergeSimilarPatterns(patterns);
115
+ const cleaned = this.storage.cleanupPatterns(merged);
116
+ await this.storage.saveLearnedPatterns(cleaned);
117
+ this.logger.debug(`Saved ${cleaned.length} learned patterns`);
211
118
  }
212
119
  /**
213
- * Generate a name for a pattern
120
+ * Get statistics
214
121
  */
215
- generatePatternName(pattern) {
216
- const provider = pattern.provider || 'generic';
217
- const timestamp = Date.now();
218
- return `learned-${provider}-${timestamp}`;
122
+ getStats() {
123
+ return { ...this.stats };
219
124
  }
220
125
  /**
221
- * Calculate priority for a learned pattern
126
+ * Reset statistics
222
127
  */
223
- calculatePriority(pattern, confidence) {
224
- // Base priority on confidence
225
- const basePriority = Math.floor(confidence * 50);
226
- // Add bonus for provider-specific patterns
227
- const providerBonus = pattern.provider ? 10 : 0;
228
- // Add bonus for number of patterns
229
- const patternCountBonus = Math.min(pattern.patterns.length * 2, 10);
230
- return Math.max(1, Math.min(100, basePriority + providerBonus + patternCountBonus));
128
+ resetStats() {
129
+ this.stats = {
130
+ totalErrorsProcessed: 0,
131
+ patternsLearned: 0,
132
+ patternsRejected: 0,
133
+ };
231
134
  }
232
135
  /**
233
- * Merge duplicate patterns in storage
136
+ * Clear all pattern tracking
234
137
  */
235
- async mergeDuplicatePatterns() {
236
- return this.storage.mergeDuplicatePatterns();
138
+ clearTracking() {
139
+ this.patternTracking.clear();
237
140
  }
238
141
  /**
239
- * Cleanup old patterns
142
+ * Create a pattern key for tracking
240
143
  */
241
- async cleanupOldPatterns() {
242
- const maxPatterns = this.config.maxLearnedPatterns;
243
- return this.storage.cleanupOldPatterns(maxPatterns);
144
+ createPatternKey(candidate) {
145
+ const parts = [
146
+ candidate.provider || 'unknown',
147
+ candidate.statusCode || 'no-status',
148
+ ...candidate.phrases.slice(0, 3), // Use first 3 phrases for key
149
+ ].join('|');
150
+ return parts;
244
151
  }
245
152
  /**
246
- * Clear all tracked patterns
153
+ * Get or create pattern tracking
247
154
  */
248
- clearTrackedPatterns() {
249
- this.patternTracker.clear();
155
+ getOrCreateTracking(candidate, patternKey) {
156
+ if (this.patternTracking.has(patternKey)) {
157
+ return this.patternTracking.get(patternKey);
158
+ }
159
+ // Create pattern from candidate
160
+ const allPatterns = [
161
+ ...candidate.phrases,
162
+ ...candidate.errorCodes,
163
+ ...(candidate.statusCode ? [candidate.statusCode] : []),
164
+ ];
165
+ const pattern = {
166
+ name: `learned-${candidate.provider}-${Date.now()}`,
167
+ provider: candidate.provider || undefined,
168
+ patterns: allPatterns,
169
+ priority: 70, // Medium priority for learned patterns
170
+ };
171
+ const tracking = {
172
+ pattern,
173
+ frequency: 0,
174
+ firstSeen: Date.now(),
175
+ samples: [],
176
+ };
177
+ this.patternTracking.set(patternKey, tracking);
178
+ return tracking;
250
179
  }
251
180
  /**
252
- * Get statistics about learning
181
+ * Save a single learned pattern
253
182
  */
254
- getStats() {
255
- return {
256
- trackedPatterns: this.patternTracker.size,
257
- learnedPatterns: this.learnedPatterns.size,
258
- pendingPatterns: Array.from(this.patternTracker.values())
259
- .filter(t => t.count >= this.config.minErrorFrequency).length,
260
- };
183
+ async saveLearnedPattern(pattern) {
184
+ // Load existing patterns
185
+ const existing = await this.storage.loadLearnedPatterns();
186
+ // Add new pattern
187
+ existing.push(pattern);
188
+ // Merge and clean up
189
+ await this.saveLearnedPatterns(existing);
261
190
  }
262
191
  }
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Error Pattern Registry for rate limit error detection
3
3
  */
4
- import type { ErrorPattern, LearningConfig, LearnedPattern } from '../types/index.js';
4
+ import type { ErrorPattern, LearnedPattern, PatternLearningConfig } from '../types/index.js';
5
5
  import { Logger } from '../../logger.js';
6
+ import { PatternLearner } from './PatternLearner.js';
6
7
  /**
7
8
  * Error Pattern Registry class
8
9
  * Manages and matches error patterns for rate limit detection
@@ -10,51 +11,61 @@ import { Logger } from '../../logger.js';
10
11
  export declare class ErrorPatternRegistry {
11
12
  private patterns;
12
13
  private learnedPatterns;
13
- private patternLearner?;
14
- private logger;
15
- private configPath?;
16
- constructor(logger?: Logger, config?: {
17
- learningConfig?: LearningConfig;
18
- configPath?: string;
19
- });
14
+ private patternLearner;
15
+ private learningConfig;
16
+ private _logger;
17
+ constructor(logger?: Logger);
18
+ /**
19
+ * Register default rate limit error patterns
20
+ */
21
+ registerDefaultPatterns(): void;
22
+ /**
23
+ * Register a new error pattern
24
+ */
25
+ register(pattern: ErrorPattern): void;
26
+ /**
27
+ * Register multiple error patterns
28
+ */
29
+ registerMany(patterns: ErrorPattern[]): void;
20
30
  /**
21
31
  * Initialize pattern learning
22
32
  */
23
- private initializeLearning;
33
+ initializePatternLearning(config: PatternLearningConfig, configFilePath: string): void;
24
34
  /**
25
- * Load learned patterns from storage (async)
35
+ * Check if pattern learning is enabled
26
36
  */
27
- loadLearnedPatternsAsync(): Promise<void>;
37
+ isLearningEnabled(): boolean;
28
38
  /**
29
- * Load learned patterns from storage (synchronous - kept for backward compatibility)
39
+ * Get the pattern learner instance
30
40
  */
31
- loadLearnedPatterns(): void;
41
+ getPatternLearner(): PatternLearner | null;
32
42
  /**
33
- * Reload learned patterns (for config hot reload)
43
+ * Add a learned pattern
34
44
  */
35
- reloadLearnedPatterns(): Promise<void>;
45
+ addLearnedPattern(pattern: LearnedPattern): void;
36
46
  /**
37
- * Register default rate limit error patterns
47
+ * Get all learned patterns
38
48
  */
39
- registerDefaultPatterns(): void;
49
+ getLearnedPatterns(): LearnedPattern[];
40
50
  /**
41
- * Register a new error pattern
51
+ * Clear all learned patterns
42
52
  */
43
- register(pattern: ErrorPattern): void;
53
+ clearLearnedPatterns(): void;
44
54
  /**
45
- * Register multiple error patterns
55
+ * Update learned patterns
46
56
  */
47
- registerMany(patterns: ErrorPattern[]): void;
57
+ updateLearnedPatterns(patterns: LearnedPattern[]): void;
48
58
  /**
49
59
  * Check if an error matches any registered rate limit pattern
50
60
  */
51
61
  isRateLimitError(error: unknown): boolean;
52
62
  /**
53
63
  * Get the matched pattern for an error, or null if no match
64
+ * Checks default patterns first, then learned patterns
54
65
  */
55
66
  getMatchedPattern(error: unknown): ErrorPattern | null;
56
67
  /**
57
- * Get all registered patterns
68
+ * Get all registered patterns (including learned patterns)
58
69
  */
59
70
  getAllPatterns(): ErrorPattern[];
60
71
  /**
@@ -77,47 +88,13 @@ export declare class ErrorPatternRegistry {
77
88
  * Reset to default patterns only
78
89
  */
79
90
  resetToDefaults(): void;
80
- /**
81
- * Learn a new pattern from an error
82
- */
83
- addLearnedPattern(error: unknown): void;
84
- /**
85
- * Learn a new pattern from an error (async version)
86
- */
87
- addLearnedPatternAsync(error: unknown): Promise<void>;
88
- /**
89
- * Get all learned patterns
90
- */
91
- getLearnedPatterns(): LearnedPattern[];
92
- /**
93
- * Get a learned pattern by name
94
- */
95
- getLearnedPatternByName(name: string): LearnedPattern | undefined;
96
- /**
97
- * Remove a learned pattern by name
98
- */
99
- removeLearnedPattern(name: string): Promise<boolean>;
100
- /**
101
- * Merge duplicate learned patterns
102
- */
103
- mergeDuplicatePatterns(): Promise<number>;
104
- /**
105
- * Cleanup old learned patterns
106
- */
107
- cleanupOldPatterns(): Promise<number>;
108
- /**
109
- * Get learning statistics
110
- */
111
- getLearningStats(): {
112
- trackedPatterns: number;
113
- learnedPatterns: number;
114
- pendingPatterns: number;
115
- } | null;
116
91
  /**
117
92
  * Get statistics about registered patterns
118
93
  */
119
94
  getStats(): {
120
95
  total: number;
96
+ default: number;
97
+ learned: number;
121
98
  byProvider: Record<string, number>;
122
99
  byPriority: Record<string, number>;
123
100
  };