@azumag/opencode-rate-limit-fallback 1.59.0 → 1.64.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/dist/index.js CHANGED
@@ -163,6 +163,18 @@ export const RateLimitFallback = async ({ client, directory, worktree }) => {
163
163
  if (config.errorPatterns?.custom) {
164
164
  errorPatternRegistry.registerMany(config.errorPatterns.custom);
165
165
  }
166
+ // Initialize pattern learning if enabled
167
+ if (config.errorPatterns?.enableLearning && configSource) {
168
+ const patternLearningConfig = {
169
+ enabled: config.errorPatterns.enableLearning,
170
+ autoApproveThreshold: config.errorPatterns.autoApproveThreshold ?? 0.8,
171
+ maxLearnedPatterns: config.errorPatterns.maxLearnedPatterns ?? 20,
172
+ minErrorFrequency: config.errorPatterns.minErrorFrequency ?? 3,
173
+ learningWindowMs: config.errorPatterns.learningWindowMs ?? 86400000,
174
+ };
175
+ errorPatternRegistry.initializePatternLearning(patternLearningConfig, configSource);
176
+ logger.info('Pattern learning enabled');
177
+ }
166
178
  // Initialize health tracker
167
179
  let healthTracker;
168
180
  if (config.enableHealthBasedSelection) {
@@ -186,6 +198,7 @@ export const RateLimitFallback = async ({ client, directory, worktree }) => {
186
198
  const componentRefs = {
187
199
  fallbackHandler,
188
200
  metricsManager,
201
+ errorPatternRegistry,
189
202
  };
190
203
  const configReloader = new ConfigReloader(config, configSource, logger, validator, client, componentRefs, directory, worktree, config.configReload?.notifyOnReload ?? true);
191
204
  configWatcher = new ConfigWatcher(configSource || '', logger, async () => { await configReloader.reloadConfig(); }, {
@@ -214,6 +227,13 @@ export const RateLimitFallback = async ({ client, directory, worktree }) => {
214
227
  if (isSessionErrorEvent(event)) {
215
228
  const { sessionID, error } = event.properties;
216
229
  if (sessionID && error && errorPatternRegistry.isRateLimitError(error)) {
230
+ // Learn from this error if pattern learning is enabled
231
+ const patternLearner = errorPatternRegistry.getPatternLearner();
232
+ if (patternLearner) {
233
+ patternLearner.processError(error).catch((err) => {
234
+ logger.debug('Pattern learning failed', { error: err });
235
+ });
236
+ }
217
237
  await fallbackHandler.handleRateLimitFallback(sessionID, "", "");
218
238
  }
219
239
  }
@@ -221,6 +241,13 @@ export const RateLimitFallback = async ({ client, directory, worktree }) => {
221
241
  if (isMessageUpdatedEvent(event)) {
222
242
  const info = event.properties.info;
223
243
  if (info?.error && errorPatternRegistry.isRateLimitError(info.error)) {
244
+ // Learn from this error if pattern learning is enabled
245
+ const patternLearner = errorPatternRegistry.getPatternLearner();
246
+ if (patternLearner) {
247
+ patternLearner.processError(info.error).catch((err) => {
248
+ logger.debug('Pattern learning failed', { error: err });
249
+ });
250
+ }
224
251
  await fallbackHandler.handleRateLimitFallback(info.sessionID, info.providerID || "", info.modelID || "");
225
252
  }
226
253
  else if (info?.status === "completed" && !info?.error && info?.id) {
@@ -456,14 +456,6 @@ export class ConfigValidator {
456
456
  value: config.errorPatterns.custom,
457
457
  });
458
458
  }
459
- if (config.errorPatterns.enableLearning !== undefined && typeof config.errorPatterns.enableLearning !== 'boolean') {
460
- errors.push({
461
- path: 'errorPatterns.enableLearning',
462
- message: 'enableLearning must be a boolean',
463
- severity: 'error',
464
- value: config.errorPatterns.enableLearning,
465
- });
466
- }
467
459
  }
468
460
  }
469
461
  // Validate dynamicPrioritization
@@ -91,12 +91,23 @@ export declare const DEFAULT_DYNAMIC_PRIORITIZATION_CONFIG: {
91
91
  readonly maxHistorySize: 100;
92
92
  };
93
93
  /**
94
- * Default error pattern learning configuration
94
+ * Default pattern learning configuration
95
95
  */
96
- export declare const DEFAULT_ERROR_PATTERN_LEARNING_CONFIG: {
96
+ export declare const DEFAULT_PATTERN_LEARNING_CONFIG: {
97
+ readonly enabled: false;
98
+ readonly autoApproveThreshold: 0.8;
99
+ readonly maxLearnedPatterns: 20;
100
+ readonly minErrorFrequency: 3;
101
+ readonly learningWindowMs: 86400000;
102
+ };
103
+ /**
104
+ * Default error pattern configuration
105
+ */
106
+ export declare const DEFAULT_ERROR_PATTERNS_CONFIG: {
107
+ readonly custom: undefined;
97
108
  readonly enableLearning: false;
98
109
  readonly autoApproveThreshold: 0.8;
99
110
  readonly maxLearnedPatterns: 20;
100
111
  readonly minErrorFrequency: 3;
101
- readonly learningWindowMs: number;
112
+ readonly learningWindowMs: 86400000;
102
113
  };
@@ -117,15 +117,26 @@ export const DEFAULT_DYNAMIC_PRIORITIZATION_CONFIG = {
117
117
  maxHistorySize: 100,
118
118
  };
119
119
  // ============================================================================
120
- // Error Pattern Learning Defaults
120
+ // Error Pattern Configuration Defaults
121
121
  // ============================================================================
122
122
  /**
123
- * Default error pattern learning configuration
123
+ * Default pattern learning configuration
124
124
  */
125
- export const DEFAULT_ERROR_PATTERN_LEARNING_CONFIG = {
125
+ export const DEFAULT_PATTERN_LEARNING_CONFIG = {
126
+ enabled: false,
127
+ autoApproveThreshold: 0.8,
128
+ maxLearnedPatterns: 20,
129
+ minErrorFrequency: 3,
130
+ learningWindowMs: 86400000, // 24 hours
131
+ };
132
+ /**
133
+ * Default error pattern configuration
134
+ */
135
+ export const DEFAULT_ERROR_PATTERNS_CONFIG = {
136
+ custom: undefined,
126
137
  enableLearning: false,
127
138
  autoApproveThreshold: 0.8,
128
139
  maxLearnedPatterns: 20,
129
140
  minErrorFrequency: 3,
130
- learningWindowMs: 24 * 60 * 60 * 1000, // 24 hours
141
+ learningWindowMs: 86400000,
131
142
  };
@@ -3,7 +3,7 @@
3
3
  * Generates and reports diagnostic information about the plugin
4
4
  */
5
5
  import { Logger } from '../../logger.js';
6
- import type { PluginConfig, ModelHealth } from '../types/index.js';
6
+ import type { PluginConfig, ModelHealth, LearnedPattern } from '../types/index.js';
7
7
  import type { HealthTracker } from '../health/HealthTracker.js';
8
8
  import type { CircuitBreaker } from '../circuitbreaker/index.js';
9
9
  import { ErrorPatternRegistry } from '../errors/PatternRegistry.js';
@@ -55,9 +55,12 @@ export interface DiagnosticReport {
55
55
  errorPatterns: {
56
56
  stats: {
57
57
  total: number;
58
+ default: number;
59
+ learned: number;
58
60
  byProvider: Record<string, number>;
59
61
  byPriority: Record<string, number>;
60
62
  };
63
+ learnedPatterns: LearnedPattern[];
61
64
  };
62
65
  circuitBreaker: {
63
66
  enabled: boolean;
@@ -76,6 +76,7 @@ export class DiagnosticReporter {
76
76
  generateErrorPatternsReport() {
77
77
  return {
78
78
  stats: this.errorPatternRegistry.getStats(),
79
+ learnedPatterns: this.errorPatternRegistry.getLearnedPatterns(),
79
80
  };
80
81
  }
81
82
  /**
@@ -208,6 +209,8 @@ export class DiagnosticReporter {
208
209
  lines.push('-'.repeat(70));
209
210
  const patternStats = report.errorPatterns.stats;
210
211
  lines.push(`Total Patterns: ${patternStats.total}`);
212
+ lines.push(`Default Patterns: ${patternStats.default || 0}`);
213
+ lines.push(`Learned Patterns: ${patternStats.learned || 0}`);
211
214
  lines.push('');
212
215
  if (Object.keys(patternStats.byProvider).length > 0) {
213
216
  lines.push('By Provider:');
@@ -223,6 +226,19 @@ export class DiagnosticReporter {
223
226
  }
224
227
  lines.push('');
225
228
  }
229
+ // Display learned patterns details
230
+ if (report.errorPatterns.learnedPatterns.length > 0) {
231
+ lines.push('LEARNED PATTERNS:');
232
+ for (const pattern of report.errorPatterns.learnedPatterns.sort((a, b) => b.confidence - a.confidence)) {
233
+ lines.push(` Name: ${pattern.name}`);
234
+ lines.push(` Provider: ${pattern.provider || 'generic'}`);
235
+ lines.push(` Confidence: ${(pattern.confidence * 100).toFixed(1)}%`);
236
+ lines.push(` Sample Count: ${pattern.sampleCount}`);
237
+ lines.push(` Learned At: ${pattern.learnedAt}`);
238
+ lines.push(` Patterns: ${pattern.patterns.map(p => typeof p === 'string' ? `"${p}"` : p.toString()).join(', ')}`);
239
+ lines.push('');
240
+ }
241
+ }
226
242
  // Circuit breaker section
227
243
  lines.push('-'.repeat(70));
228
244
  lines.push('CIRCUIT BREAKER');
@@ -1,45 +1,55 @@
1
1
  /**
2
- * Confidence Scoring for Learned Patterns
2
+ * Confidence Scorer for calculating confidence scores for learned patterns
3
3
  */
4
- import type { PatternCandidate, ErrorPattern, LearningConfig } from '../types/index.js';
4
+ import type { ErrorPattern, LearnedPattern, PatternLearningConfig } from '../types/index.js';
5
5
  /**
6
- * ConfidenceScorer - Calculates confidence scores for learned patterns
6
+ * Confidence Scorer class
7
+ * Calculates confidence scores for learned patterns
7
8
  */
8
9
  export declare class ConfidenceScorer {
9
10
  private config;
10
- private knownPatterns;
11
- constructor(config: LearningConfig, knownPatterns: ErrorPattern[]);
12
11
  /**
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
12
+ * Constructor
18
13
  */
19
- calculateScore(pattern: PatternCandidate, sampleCount: number, learnedAt?: number): number;
14
+ constructor(config: PatternLearningConfig);
20
15
  /**
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
16
+ * Update configuration
25
17
  */
26
- calculateFrequencyScore(count: number, minFrequency: number): number;
18
+ updateConfig(config: PatternLearningConfig): void;
27
19
  /**
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
20
+ * Calculate frequency score (50% weight)
31
21
  */
32
- calculateSimilarityScore(pattern: PatternCandidate): number;
22
+ private calculateFrequencyScore;
33
23
  /**
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
24
+ * Calculate similarity score (30% weight)
37
25
  */
38
- calculateRecencyScore(learnedAt: number): number;
26
+ private calculateSimilarityScore;
39
27
  /**
40
- * Check if pattern meets auto-approve threshold
41
- * @param confidence - Confidence score
42
- * @returns True if pattern should be auto-approved
28
+ * Calculate recency score (20% weight)
29
+ */
30
+ private calculateRecencyScore;
31
+ /**
32
+ * Calculate overall confidence score
33
+ */
34
+ calculateConfidence(pattern: ErrorPattern, frequency: number, firstSeen: number, existingPatterns?: ErrorPattern[]): number;
35
+ /**
36
+ * Check if a pattern should be auto-approved
43
37
  */
44
38
  shouldAutoApprove(confidence: number): boolean;
39
+ /**
40
+ * Get confidence level category
41
+ */
42
+ getConfidenceLevel(confidence: number): 'high' | 'medium' | 'low';
43
+ /**
44
+ * Calculate pattern statistics
45
+ */
46
+ calculatePatternStats(patterns: LearnedPattern[]): {
47
+ totalPatterns: number;
48
+ avgConfidence: number;
49
+ confidenceDistribution: {
50
+ high: number;
51
+ medium: number;
52
+ low: number;
53
+ };
54
+ };
45
55
  }
@@ -1,120 +1,110 @@
1
1
  /**
2
- * Confidence Scoring for Learned Patterns
2
+ * Confidence Scorer for calculating confidence scores for learned patterns
3
3
  */
4
4
  import { calculateJaccardSimilarity } from '../utils/similarity.js';
5
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
6
+ * Confidence Scorer class
7
+ * Calculates confidence scores for learned patterns
21
8
  */
22
9
  export class ConfidenceScorer {
23
10
  config;
24
- knownPatterns;
25
- constructor(config, knownPatterns) {
11
+ /**
12
+ * Constructor
13
+ */
14
+ constructor(config) {
26
15
  this.config = config;
27
- this.knownPatterns = knownPatterns;
28
16
  }
29
17
  /**
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
18
+ * Update configuration
35
19
  */
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));
20
+ updateConfig(config) {
21
+ this.config = config;
46
22
  }
47
23
  /**
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
24
+ * Calculate frequency score (50% weight)
52
25
  */
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));
26
+ calculateFrequencyScore(frequency) {
27
+ return Math.min(1, frequency / this.config.minErrorFrequency);
64
28
  }
65
29
  /**
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
30
+ * Calculate similarity score (30% weight)
69
31
  */
70
- calculateSimilarityScore(pattern) {
71
- if (pattern.patterns.length === 0) {
72
- return 0;
32
+ calculateSimilarityScore(pattern, existingPatterns) {
33
+ if (existingPatterns.length === 0) {
34
+ return 1; // No existing patterns, so new pattern is novel
73
35
  }
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
- }
36
+ // Find the maximum similarity to any existing pattern
37
+ let maxSimilarity = 0;
38
+ for (const existing of existingPatterns) {
39
+ // Compare patterns
40
+ const patternStr = pattern.patterns.map(p => String(p)).join(' ');
41
+ const existingStr = existing.patterns.map(p => String(p)).join(' ');
42
+ const similarity = calculateJaccardSimilarity(patternStr, existingStr);
43
+ if (similarity > maxSimilarity) {
44
+ maxSimilarity = similarity;
88
45
  }
89
46
  }
90
- // Combine scores
91
- return Math.max(0, Math.min(1, (keywordScore * 0.5) + (knownPatternBonus * 0.5)));
47
+ // Return 1 - maxSimilarity (lower similarity to existing patterns = higher score)
48
+ return 1 - maxSimilarity;
92
49
  }
93
50
  /**
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
51
+ * Calculate recency score (20% weight)
97
52
  */
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);
53
+ calculateRecencyScore(firstSeen) {
54
+ const timeSinceFirst = Date.now() - firstSeen;
55
+ return 1 - Math.min(1, timeSinceFirst / this.config.learningWindowMs);
56
+ }
57
+ /**
58
+ * Calculate overall confidence score
59
+ */
60
+ calculateConfidence(pattern, frequency, firstSeen, existingPatterns = []) {
61
+ const frequencyScore = this.calculateFrequencyScore(frequency);
62
+ const similarityScore = this.calculateSimilarityScore(pattern, existingPatterns);
63
+ const recencyScore = this.calculateRecencyScore(firstSeen);
64
+ // Weighted combination
65
+ const confidence = frequencyScore * 0.5 +
66
+ similarityScore * 0.3 +
67
+ recencyScore * 0.2;
68
+ return Math.round(confidence * 100) / 100; // Round to 2 decimal places
111
69
  }
112
70
  /**
113
- * Check if pattern meets auto-approve threshold
114
- * @param confidence - Confidence score
115
- * @returns True if pattern should be auto-approved
71
+ * Check if a pattern should be auto-approved
116
72
  */
117
73
  shouldAutoApprove(confidence) {
118
74
  return confidence >= this.config.autoApproveThreshold;
119
75
  }
76
+ /**
77
+ * Get confidence level category
78
+ */
79
+ getConfidenceLevel(confidence) {
80
+ if (confidence >= 0.8)
81
+ return 'high';
82
+ if (confidence >= 0.5)
83
+ return 'medium';
84
+ return 'low';
85
+ }
86
+ /**
87
+ * Calculate pattern statistics
88
+ */
89
+ calculatePatternStats(patterns) {
90
+ if (patterns.length === 0) {
91
+ return {
92
+ totalPatterns: 0,
93
+ avgConfidence: 0,
94
+ confidenceDistribution: { high: 0, medium: 0, low: 0 },
95
+ };
96
+ }
97
+ let totalConfidence = 0;
98
+ const distribution = { high: 0, medium: 0, low: 0 };
99
+ for (const pattern of patterns) {
100
+ totalConfidence += pattern.confidence;
101
+ const level = this.getConfidenceLevel(pattern.confidence);
102
+ distribution[level]++;
103
+ }
104
+ return {
105
+ totalPatterns: patterns.length,
106
+ avgConfidence: Math.round((totalConfidence / patterns.length) * 100) / 100,
107
+ confidenceDistribution: distribution,
108
+ };
109
+ }
120
110
  }
@@ -1,31 +1,38 @@
1
1
  /**
2
- * Pattern Extraction from Rate Limit Errors
2
+ * Pattern Extractor for extracting patterns from error messages
3
3
  */
4
4
  import type { PatternCandidate } from '../types/index.js';
5
5
  /**
6
- * PatternExtractor - Extracts pattern candidates from error messages
6
+ * Pattern Extractor class
7
+ * Extracts pattern candidates from error messages
7
8
  */
8
9
  export declare class PatternExtractor {
9
10
  /**
10
- * Extract candidate patterns from an error
11
+ * Check if an object is a valid error object
11
12
  */
12
- extractPatterns(error: unknown): PatternCandidate[];
13
+ isValidErrorObject(error: unknown): boolean;
13
14
  /**
14
- * Extract provider ID from error
15
+ * Extract error text from various error fields
15
16
  */
16
- extractProvider(error: unknown): string | null;
17
+ private extractErrorText;
17
18
  /**
18
- * Extract HTTP status code from error
19
+ * Extract provider ID from error text
19
20
  */
20
- extractStatusCode(error: unknown): number | null;
21
+ private extractProvider;
21
22
  /**
22
- * Extract common rate limit phrases from error
23
+ * Extract HTTP status codes from error text
23
24
  */
24
- extractPhrases(error: unknown): string[];
25
+ private extractStatusCodes;
25
26
  /**
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
27
+ * Extract rate limit phrases from error text
29
28
  */
30
- private isValidErrorObject;
29
+ private extractPhrases;
30
+ /**
31
+ * Extract API error codes from error text
32
+ */
33
+ private extractErrorCodes;
34
+ /**
35
+ * Extract pattern candidates from an error
36
+ */
37
+ extractPattern(error: unknown): PatternCandidate | null;
31
38
  }