@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.
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Pattern Learning from Rate Limit Errors
3
+ */
4
+ /**
5
+ * PatternLearner - Orchestrates the learning process from errors
6
+ */
7
+ export class PatternLearner {
8
+ extractor;
9
+ scorer;
10
+ storage;
11
+ config;
12
+ 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;
19
+ 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
+ }
49
+ }
50
+ /**
51
+ * Track a pattern candidate for learning
52
+ */
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
+ }
77
+ }
78
+ /**
79
+ * Process tracked patterns and save those meeting criteria
80
+ */
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
+ }
117
+ }
118
+ /**
119
+ * Merge similar patterns
120
+ */
121
+ mergePatterns(patterns) {
122
+ if (patterns.length === 0) {
123
+ return null;
124
+ }
125
+ if (patterns.length === 1) {
126
+ return patterns[0];
127
+ }
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`);
150
+ }
151
+ catch (error) {
152
+ this.logger.error('[PatternLearner] Failed to load learned patterns', {
153
+ error: error instanceof Error ? error.message : String(error),
154
+ });
155
+ }
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;
187
+ }
188
+ return false;
189
+ }
190
+ /**
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
198
+ */
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}`;
203
+ }
204
+ /**
205
+ * Generate a unique key for a learned pattern
206
+ */
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}`;
211
+ }
212
+ /**
213
+ * Generate a name for a pattern
214
+ */
215
+ generatePatternName(pattern) {
216
+ const provider = pattern.provider || 'generic';
217
+ const timestamp = Date.now();
218
+ return `learned-${provider}-${timestamp}`;
219
+ }
220
+ /**
221
+ * Calculate priority for a learned pattern
222
+ */
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));
231
+ }
232
+ /**
233
+ * Merge duplicate patterns in storage
234
+ */
235
+ async mergeDuplicatePatterns() {
236
+ return this.storage.mergeDuplicatePatterns();
237
+ }
238
+ /**
239
+ * Cleanup old patterns
240
+ */
241
+ async cleanupOldPatterns() {
242
+ const maxPatterns = this.config.maxLearnedPatterns;
243
+ return this.storage.cleanupOldPatterns(maxPatterns);
244
+ }
245
+ /**
246
+ * Clear all tracked patterns
247
+ */
248
+ clearTrackedPatterns() {
249
+ this.patternTracker.clear();
250
+ }
251
+ /**
252
+ * Get statistics about learning
253
+ */
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
+ };
261
+ }
262
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Error Pattern Registry for rate limit error detection
3
3
  */
4
- import type { ErrorPattern } from '../types/index.js';
4
+ import type { ErrorPattern, LearningConfig, LearnedPattern } from '../types/index.js';
5
5
  import { Logger } from '../../logger.js';
6
6
  /**
7
7
  * Error Pattern Registry class
@@ -9,8 +9,30 @@ import { Logger } from '../../logger.js';
9
9
  */
10
10
  export declare class ErrorPatternRegistry {
11
11
  private patterns;
12
+ private learnedPatterns;
13
+ private patternLearner?;
12
14
  private logger;
13
- constructor(logger?: Logger);
15
+ private configPath?;
16
+ constructor(logger?: Logger, config?: {
17
+ learningConfig?: LearningConfig;
18
+ configPath?: string;
19
+ });
20
+ /**
21
+ * Initialize pattern learning
22
+ */
23
+ private initializeLearning;
24
+ /**
25
+ * Load learned patterns from storage (async)
26
+ */
27
+ loadLearnedPatternsAsync(): Promise<void>;
28
+ /**
29
+ * Load learned patterns from storage (synchronous - kept for backward compatibility)
30
+ */
31
+ loadLearnedPatterns(): void;
32
+ /**
33
+ * Reload learned patterns (for config hot reload)
34
+ */
35
+ reloadLearnedPatterns(): Promise<void>;
14
36
  /**
15
37
  * Register default rate limit error patterns
16
38
  */
@@ -56,10 +78,41 @@ export declare class ErrorPatternRegistry {
56
78
  */
57
79
  resetToDefaults(): void;
58
80
  /**
59
- * Learn a new pattern from an error (for future ML-based learning)
60
- * Currently disabled - patterns must be manually registered
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
61
110
  */
62
- addLearnedPattern(_error: unknown): void;
111
+ getLearningStats(): {
112
+ trackedPatterns: number;
113
+ learnedPatterns: number;
114
+ pendingPatterns: number;
115
+ } | null;
63
116
  /**
64
117
  * Get statistics about registered patterns
65
118
  */
@@ -1,14 +1,21 @@
1
1
  /**
2
2
  * Error Pattern Registry for rate limit error detection
3
3
  */
4
+ import { PatternExtractor } from './PatternExtractor.js';
5
+ import { ConfidenceScorer } from './ConfidenceScorer.js';
6
+ import { PatternStorage } from './PatternStorage.js';
7
+ import { PatternLearner } from './PatternLearner.js';
4
8
  /**
5
9
  * Error Pattern Registry class
6
10
  * Manages and matches error patterns for rate limit detection
7
11
  */
8
12
  export class ErrorPatternRegistry {
9
13
  patterns = [];
14
+ learnedPatterns = new Map();
15
+ patternLearner;
10
16
  logger;
11
- constructor(logger) {
17
+ configPath;
18
+ constructor(logger, config) {
12
19
  // Initialize logger
13
20
  this.logger = logger || {
14
21
  debug: () => { },
@@ -16,7 +23,89 @@ export class ErrorPatternRegistry {
16
23
  warn: () => { },
17
24
  error: () => { },
18
25
  };
26
+ this.configPath = config?.configPath;
19
27
  this.registerDefaultPatterns();
28
+ // Initialize pattern learning if enabled
29
+ if (config?.learningConfig && config.learningConfig.enabled) {
30
+ this.initializeLearning(config.learningConfig);
31
+ }
32
+ }
33
+ /**
34
+ * Initialize pattern learning
35
+ */
36
+ initializeLearning(learningConfig) {
37
+ if (!this.configPath) {
38
+ this.logger.warn('[ErrorPatternRegistry] Config path not provided, pattern learning disabled');
39
+ return;
40
+ }
41
+ try {
42
+ const extractor = new PatternExtractor();
43
+ const scorer = new ConfidenceScorer(learningConfig, this.patterns);
44
+ const storage = new PatternStorage(this.configPath, this.logger);
45
+ this.patternLearner = new PatternLearner(extractor, scorer, storage, learningConfig, this.logger);
46
+ // Note: Patterns will be loaded asynchronously via loadLearnedPatternsAsync()
47
+ this.logger.info('[ErrorPatternRegistry] Pattern learning enabled');
48
+ }
49
+ catch (error) {
50
+ this.logger.error('[ErrorPatternRegistry] Failed to initialize pattern learning', {
51
+ error: error instanceof Error ? error.message : String(error),
52
+ });
53
+ }
54
+ }
55
+ /**
56
+ * Load learned patterns from storage (async)
57
+ */
58
+ async loadLearnedPatternsAsync() {
59
+ if (!this.patternLearner) {
60
+ return;
61
+ }
62
+ try {
63
+ await this.patternLearner.loadLearnedPatterns();
64
+ const learnedPatterns = this.patternLearner.getLearnedPatterns();
65
+ for (const pattern of learnedPatterns) {
66
+ this.learnedPatterns.set(pattern.name, pattern);
67
+ this.register(pattern);
68
+ }
69
+ this.logger.info(`[ErrorPatternRegistry] Loaded ${learnedPatterns.length} learned patterns`);
70
+ }
71
+ catch (error) {
72
+ this.logger.error('[ErrorPatternRegistry] Failed to load learned patterns', {
73
+ error: error instanceof Error ? error.message : String(error),
74
+ });
75
+ }
76
+ }
77
+ /**
78
+ * Load learned patterns from storage (synchronous - kept for backward compatibility)
79
+ */
80
+ loadLearnedPatterns() {
81
+ if (!this.patternLearner) {
82
+ return;
83
+ }
84
+ try {
85
+ // Load patterns without awaiting (sync fallback)
86
+ this.patternLearner.loadLearnedPatterns().catch((error) => {
87
+ this.logger.error('[ErrorPatternRegistry] Failed to load learned patterns asynchronously', {
88
+ error: error instanceof Error ? error.message : String(error),
89
+ });
90
+ });
91
+ const learnedPatterns = this.patternLearner.getLearnedPatterns();
92
+ for (const pattern of learnedPatterns) {
93
+ this.learnedPatterns.set(pattern.name, pattern);
94
+ this.register(pattern);
95
+ }
96
+ }
97
+ catch (error) {
98
+ this.logger.error('[ErrorPatternRegistry] Failed to load learned patterns', {
99
+ error: error instanceof Error ? error.message : String(error),
100
+ });
101
+ }
102
+ }
103
+ /**
104
+ * Reload learned patterns (for config hot reload)
105
+ */
106
+ async reloadLearnedPatterns() {
107
+ this.learnedPatterns.clear();
108
+ await this.loadLearnedPatternsAsync();
20
109
  }
21
110
  /**
22
111
  * Register default rate limit error patterns
@@ -108,7 +197,13 @@ export class ErrorPatternRegistry {
108
197
  * Check if an error matches any registered rate limit pattern
109
198
  */
110
199
  isRateLimitError(error) {
111
- return this.getMatchedPattern(error) !== null;
200
+ // Check if this is a rate limit error
201
+ const isRateLimit = this.getMatchedPattern(error) !== null;
202
+ // If enabled, learn from this error
203
+ if (isRateLimit && this.patternLearner) {
204
+ this.patternLearner.learnFromError(error);
205
+ }
206
+ return isRateLimit;
112
207
  }
113
208
  /**
114
209
  * Get the matched pattern for an error, or null if no match
@@ -191,13 +286,92 @@ export class ErrorPatternRegistry {
191
286
  this.registerDefaultPatterns();
192
287
  }
193
288
  /**
194
- * Learn a new pattern from an error (for future ML-based learning)
195
- * Currently disabled - patterns must be manually registered
289
+ * Learn a new pattern from an error
290
+ */
291
+ addLearnedPattern(error) {
292
+ if (this.patternLearner) {
293
+ this.patternLearner.learnFromError(error);
294
+ }
295
+ else {
296
+ this.logger.warn('[ErrorPatternRegistry] Pattern learning is not enabled. Patterns must be manually registered via configuration.');
297
+ }
298
+ }
299
+ /**
300
+ * Learn a new pattern from an error (async version)
196
301
  */
197
- addLearnedPattern(_error) {
198
- // Placeholder for future ML-based pattern learning
199
- // For now, patterns must be manually registered via config
200
- this.logger.warn('[ErrorPatternRegistry] Automatic pattern learning is not enabled. Patterns must be manually registered via configuration.');
302
+ async addLearnedPatternAsync(error) {
303
+ if (this.patternLearner) {
304
+ this.patternLearner.learnFromError(error);
305
+ }
306
+ else {
307
+ this.logger.warn('[ErrorPatternRegistry] Pattern learning is not enabled. Patterns must be manually registered via configuration.');
308
+ }
309
+ }
310
+ /**
311
+ * Get all learned patterns
312
+ */
313
+ getLearnedPatterns() {
314
+ if (!this.patternLearner) {
315
+ return [];
316
+ }
317
+ return this.patternLearner.getLearnedPatterns();
318
+ }
319
+ /**
320
+ * Get a learned pattern by name
321
+ */
322
+ getLearnedPatternByName(name) {
323
+ if (!this.patternLearner) {
324
+ return undefined;
325
+ }
326
+ return this.patternLearner.getLearnedPatternByName(name);
327
+ }
328
+ /**
329
+ * Remove a learned pattern by name
330
+ */
331
+ async removeLearnedPattern(name) {
332
+ if (!this.patternLearner) {
333
+ return false;
334
+ }
335
+ const removed = await this.patternLearner.removeLearnedPattern(name);
336
+ if (removed) {
337
+ this.removePattern(name);
338
+ }
339
+ return removed;
340
+ }
341
+ /**
342
+ * Merge duplicate learned patterns
343
+ */
344
+ async mergeDuplicatePatterns() {
345
+ if (!this.patternLearner) {
346
+ return 0;
347
+ }
348
+ const mergedCount = await this.patternLearner.mergeDuplicatePatterns();
349
+ if (mergedCount > 0) {
350
+ this.reloadLearnedPatterns();
351
+ }
352
+ return mergedCount;
353
+ }
354
+ /**
355
+ * Cleanup old learned patterns
356
+ */
357
+ async cleanupOldPatterns() {
358
+ if (!this.patternLearner) {
359
+ return 0;
360
+ }
361
+ const removedCount = await this.patternLearner.cleanupOldPatterns();
362
+ if (removedCount > 0) {
363
+ this.reloadLearnedPatterns();
364
+ }
365
+ return removedCount;
366
+ }
367
+ /**
368
+ * Get learning statistics
369
+ */
370
+ getLearningStats() {
371
+ if (!this.patternLearner) {
372
+ return null;
373
+ }
374
+ return this.patternLearner.getStats();
201
375
  }
202
376
  /**
203
377
  * Get statistics about registered patterns
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Pattern Storage for Learned Error Patterns
3
+ */
4
+ import type { LearnedPattern } from '../types/index.js';
5
+ import type { Logger } from '../../logger.js';
6
+ /**
7
+ * PatternStorage - Manages persistence of learned patterns to config file
8
+ */
9
+ export declare class PatternStorage {
10
+ private configPath;
11
+ private logger;
12
+ constructor(configPath: string, logger: Logger);
13
+ /**
14
+ * Save a learned pattern to config file
15
+ */
16
+ savePattern(pattern: LearnedPattern): Promise<void>;
17
+ /**
18
+ * Load all learned patterns from config file
19
+ */
20
+ loadPatterns(): Promise<LearnedPattern[]>;
21
+ /**
22
+ * Delete a pattern by name from config file
23
+ */
24
+ deletePattern(name: string): Promise<boolean>;
25
+ /**
26
+ * Merge duplicate patterns with high similarity
27
+ */
28
+ mergeDuplicatePatterns(): Promise<number>;
29
+ /**
30
+ * Cleanup old patterns, keeping only the most confident ones
31
+ */
32
+ cleanupOldPatterns(maxCount: number, patterns?: LearnedPattern[]): Promise<number>;
33
+ /**
34
+ * Load config file
35
+ */
36
+ private loadConfig;
37
+ /**
38
+ * Save config file
39
+ */
40
+ private saveConfig;
41
+ /**
42
+ * Validate pattern structure
43
+ */
44
+ private isValidPattern;
45
+ /**
46
+ * Calculate similarity between two patterns
47
+ */
48
+ private calculatePatternSimilarity;
49
+ }