@azumag/opencode-rate-limit-fallback 1.59.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,234 +1,179 @@
1
1
  /**
2
- * Pattern Storage for Learned Error Patterns
2
+ * Pattern Storage for persisting learned patterns
3
3
  */
4
- import { readFile, writeFile } from 'fs/promises';
5
4
  import { calculateJaccardSimilarity } from '../utils/similarity.js';
5
+ import * as fs from 'fs/promises';
6
6
  /**
7
- * PatternStorage - Manages persistence of learned patterns to config file
7
+ * Pattern Storage class
8
+ * Manages persistence of learned patterns
8
9
  */
9
10
  export class PatternStorage {
10
- configPath;
11
- logger;
12
- constructor(configPath, logger) {
13
- this.configPath = configPath;
14
- this.logger = logger;
15
- }
11
+ config;
12
+ configFilePath = null;
16
13
  /**
17
- * Save a learned pattern to config file
14
+ * Constructor
18
15
  */
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
- }
16
+ constructor(config) {
17
+ this.config = config;
55
18
  }
56
19
  /**
57
- * Load all learned patterns from config file
20
+ * Update configuration
58
21
  */
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
- }
22
+ updateConfig(config) {
23
+ this.config = config;
76
24
  }
77
25
  /**
78
- * Delete a pattern by name from config file
26
+ * Set the config file path
79
27
  */
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
- }
28
+ setConfigFilePath(path) {
29
+ this.configFilePath = path;
102
30
  }
103
31
  /**
104
- * Merge duplicate patterns with high similarity
32
+ * Merge similar patterns (Jaccard similarity > 0.8)
105
33
  */
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;
34
+ mergeSimilarPatterns(patterns) {
35
+ if (patterns.length === 0) {
36
+ return patterns;
37
+ }
38
+ const merged = [];
39
+ const usedIndices = new Set();
40
+ for (let i = 0; i < patterns.length; i++) {
41
+ if (usedIndices.has(i)) {
42
+ continue;
43
+ }
44
+ let currentPattern = patterns[i];
45
+ let combinedSampleCount = currentPattern.sampleCount;
46
+ let combinedPhrases = new Set();
47
+ // Collect all phrases from the current pattern
48
+ for (const p of currentPattern.patterns) {
49
+ combinedPhrases.add(String(p));
50
+ }
113
51
  // Find and merge similar patterns
114
- const patternsToMerge = [];
115
- for (let i = 0; i < learnedPatterns.length; i++) {
116
- if (patternsToMerge.includes(i))
52
+ for (let j = i + 1; j < patterns.length; j++) {
53
+ if (usedIndices.has(j)) {
117
54
  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}`);
55
+ }
56
+ const otherPattern = patterns[j];
57
+ const currentStr = currentPattern.patterns.map(p => String(p)).join(' ');
58
+ const otherStr = otherPattern.patterns.map(p => String(p)).join(' ');
59
+ const similarity = calculateJaccardSimilarity(currentStr, otherStr);
60
+ if (similarity > 0.8) {
61
+ // Merge patterns
62
+ usedIndices.add(j);
63
+ combinedSampleCount += otherPattern.sampleCount;
64
+ // Add phrases from the other pattern
65
+ for (const p of otherPattern.patterns) {
66
+ combinedPhrases.add(String(p));
136
67
  }
68
+ // Use the maximum confidence
69
+ currentPattern = {
70
+ ...currentPattern,
71
+ confidence: Math.max(currentPattern.confidence, otherPattern.confidence),
72
+ };
137
73
  }
138
74
  }
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;
75
+ // Create merged pattern
76
+ const mergedPattern = {
77
+ ...currentPattern,
78
+ patterns: Array.from(combinedPhrases),
79
+ sampleCount: combinedSampleCount,
80
+ };
81
+ merged.push(mergedPattern);
150
82
  }
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;
83
+ return merged;
84
+ }
85
+ /**
86
+ * Clean up old patterns when exceeding limit
87
+ */
88
+ cleanupPatterns(patterns) {
89
+ if (patterns.length <= this.config.maxLearnedPatterns) {
90
+ return patterns;
156
91
  }
92
+ // Sort by confidence and sampleCount (descending)
93
+ const sorted = [...patterns].sort((a, b) => {
94
+ if (a.confidence !== b.confidence) {
95
+ return b.confidence - a.confidence;
96
+ }
97
+ return b.sampleCount - a.sampleCount;
98
+ });
99
+ // Trim to max limit
100
+ return sorted.slice(0, this.config.maxLearnedPatterns);
157
101
  }
158
102
  /**
159
- * Cleanup old patterns, keeping only the most confident ones
103
+ * Save learned patterns to config file
160
104
  */
161
- async cleanupOldPatterns(maxCount, patterns) {
105
+ async saveLearnedPatterns(patterns) {
106
+ if (!this.configFilePath) {
107
+ return; // No config file set, skip saving
108
+ }
162
109
  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;
110
+ // Read the existing config
111
+ const configData = JSON.parse(await fs.readFile(this.configFilePath, 'utf-8'));
112
+ // Update the learned patterns
113
+ if (!configData.errorPatterns) {
114
+ configData.errorPatterns = {};
168
115
  }
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;
116
+ configData.errorPatterns.learnedPatterns = patterns;
117
+ // Write back to file
118
+ await fs.writeFile(this.configFilePath, JSON.stringify(configData, null, 2), 'utf-8');
186
119
  }
187
120
  catch (error) {
188
- this.logger.error('[PatternStorage] Failed to cleanup patterns', {
189
- error: error instanceof Error ? error.message : String(error),
190
- });
191
- throw error;
121
+ // Silently handle save errors - pattern learning is a best-effort feature
122
+ // Errors will be logged by the caller if needed
192
123
  }
193
124
  }
194
125
  /**
195
- * Load config file
126
+ * Validate and load learned patterns from config
196
127
  */
197
- async loadConfig() {
128
+ async loadLearnedPatterns() {
129
+ if (!this.configFilePath) {
130
+ return [];
131
+ }
198
132
  try {
199
- const content = await readFile(this.configPath, 'utf-8');
200
- return JSON.parse(content);
133
+ const configData = JSON.parse(await fs.readFile(this.configFilePath, 'utf-8'));
134
+ const learnedPatterns = configData.errorPatterns?.learnedPatterns;
135
+ if (!Array.isArray(learnedPatterns)) {
136
+ return [];
137
+ }
138
+ // Validate each pattern
139
+ const validPatterns = [];
140
+ for (const pattern of learnedPatterns) {
141
+ if (this.isValidLearnedPattern(pattern)) {
142
+ validPatterns.push(pattern);
143
+ }
144
+ }
145
+ return validPatterns;
201
146
  }
202
- catch (error) {
203
- // If file doesn't exist or is invalid, return empty config
204
- return {};
147
+ catch {
148
+ // File doesn't exist or is invalid
149
+ return [];
205
150
  }
206
151
  }
207
152
  /**
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
153
+ * Validate a learned pattern object
215
154
  */
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');
155
+ isValidLearnedPattern(pattern) {
156
+ if (!pattern || typeof pattern !== 'object') {
157
+ return false;
158
+ }
159
+ const p = pattern;
160
+ return (typeof p.name === 'string' &&
161
+ typeof p.confidence === 'number' &&
162
+ typeof p.learnedAt === 'string' &&
163
+ typeof p.sampleCount === 'number' &&
164
+ typeof p.priority === 'number' &&
165
+ Array.isArray(p.patterns) &&
166
+ p.patterns.every((pt) => typeof pt === 'string' || pt instanceof RegExp));
225
167
  }
226
168
  /**
227
- * Calculate similarity between two patterns
169
+ * Create a learned pattern from an error pattern
228
170
  */
229
- calculatePatternSimilarity(pattern1, pattern2) {
230
- const text1 = pattern1.patterns.join(' ').toLowerCase();
231
- const text2 = pattern2.patterns.join(' ').toLowerCase();
232
- return calculateJaccardSimilarity(text1, text2);
171
+ createLearnedPattern(basePattern, confidence, sampleCount) {
172
+ return {
173
+ ...basePattern,
174
+ confidence,
175
+ learnedAt: new Date().toISOString(),
176
+ sampleCount,
177
+ };
233
178
  }
234
179
  }
@@ -14,6 +14,12 @@ export interface ComponentRefs {
14
14
  metricsManager?: {
15
15
  updateConfig: (newConfig: PluginConfig) => void;
16
16
  };
17
+ errorPatternRegistry?: {
18
+ updateLearnedPatterns: (patterns: any[]) => void;
19
+ getPatternLearner: () => {
20
+ updateConfig: (config: any) => void;
21
+ } | null;
22
+ };
17
23
  }
18
24
  /**
19
25
  * ConfigReloader class - handles configuration reload logic
@@ -125,6 +125,23 @@ export class ConfigReloader {
125
125
  if (this.components.metricsManager) {
126
126
  this.components.metricsManager.updateConfig(newConfig);
127
127
  }
128
+ // Reload learned patterns if errorPatterns config changed
129
+ if (this.components.errorPatternRegistry && newConfig.errorPatterns?.learnedPatterns) {
130
+ this.components.errorPatternRegistry.updateLearnedPatterns(newConfig.errorPatterns.learnedPatterns);
131
+ this.logger.info(`Reloaded ${newConfig.errorPatterns.learnedPatterns.length} learned patterns`);
132
+ // Update pattern learner config if it exists
133
+ const patternLearner = this.components.errorPatternRegistry.getPatternLearner();
134
+ if (patternLearner && newConfig.errorPatterns) {
135
+ const patternLearningConfig = {
136
+ enabled: newConfig.errorPatterns.enableLearning ?? false,
137
+ autoApproveThreshold: newConfig.errorPatterns.autoApproveThreshold ?? 0.8,
138
+ maxLearnedPatterns: newConfig.errorPatterns.maxLearnedPatterns ?? 20,
139
+ minErrorFrequency: newConfig.errorPatterns.minErrorFrequency ?? 3,
140
+ learningWindowMs: newConfig.errorPatterns.learningWindowMs ?? 86400000,
141
+ };
142
+ patternLearner.updateConfig(patternLearningConfig);
143
+ }
144
+ }
128
145
  // Log configuration changes
129
146
  const changedSettings = this.getChangedSettings(oldConfig, newConfig);
130
147
  if (changedSettings.length > 0) {
@@ -160,27 +160,21 @@ export interface ErrorPattern {
160
160
  priority: number;
161
161
  }
162
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
163
+ * Error pattern configuration
173
164
  */
174
- export interface LearnedPattern extends ErrorPattern {
175
- confidence: number;
176
- learnedAt: string;
177
- sampleCount: number;
178
- lastUsed?: number;
165
+ export interface ErrorPatternsConfig {
166
+ custom?: ErrorPattern[];
167
+ enableLearning?: boolean;
168
+ learnedPatterns?: LearnedPattern[];
169
+ autoApproveThreshold?: number;
170
+ maxLearnedPatterns?: number;
171
+ minErrorFrequency?: number;
172
+ learningWindowMs?: number;
179
173
  }
180
174
  /**
181
- * Learning configuration
175
+ * Pattern learning configuration
182
176
  */
183
- export interface LearningConfig {
177
+ export interface PatternLearningConfig {
184
178
  enabled: boolean;
185
179
  autoApproveThreshold: number;
186
180
  maxLearnedPatterns: number;
@@ -188,16 +182,22 @@ export interface LearningConfig {
188
182
  learningWindowMs: number;
189
183
  }
190
184
  /**
191
- * Error pattern configuration
185
+ * Learned pattern with confidence metadata
192
186
  */
193
- export interface ErrorPatternsConfig {
194
- custom?: ErrorPattern[];
195
- enableLearning?: boolean;
196
- learnedPatterns?: LearnedPattern[];
197
- autoApproveThreshold?: number;
198
- maxLearnedPatterns?: number;
199
- minErrorFrequency?: number;
200
- learningWindowMs?: number;
187
+ export interface LearnedPattern extends ErrorPattern {
188
+ confidence: number;
189
+ learnedAt: string;
190
+ sampleCount: number;
191
+ }
192
+ /**
193
+ * Extracted pattern from an error
194
+ */
195
+ export interface PatternCandidate {
196
+ provider: string | null;
197
+ statusCode: string | null;
198
+ phrases: string[];
199
+ errorCodes: string[];
200
+ rawText: string;
201
201
  }
202
202
  /**
203
203
  * Configuration hot reload settings
@@ -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, DEFAULT_DYNAMIC_PRIORITIZATION_CONFIG, DEFAULT_ERROR_PATTERN_LEARNING_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_PATTERNS_CONFIG, DEFAULT_PATTERN_LEARNING_CONFIG, } from '../config/defaults.js';
8
8
  /**
9
9
  * Default plugin configuration
10
10
  */
@@ -20,7 +20,7 @@ export const DEFAULT_CONFIG = {
20
20
  metrics: DEFAULT_METRICS_CONFIG,
21
21
  configReload: DEFAULT_CONFIG_RELOAD_CONFIG,
22
22
  dynamicPrioritization: DEFAULT_DYNAMIC_PRIORITIZATION_CONFIG,
23
- errorPatterns: DEFAULT_ERROR_PATTERN_LEARNING_CONFIG,
23
+ errorPatterns: DEFAULT_ERROR_PATTERNS_CONFIG,
24
24
  };
25
25
  /**
26
26
  * Validate that a path does not contain directory traversal attempts
@@ -92,9 +92,14 @@ export function validateConfig(config) {
92
92
  ...config.dynamicPrioritization,
93
93
  } : DEFAULT_DYNAMIC_PRIORITIZATION_CONFIG,
94
94
  errorPatterns: config.errorPatterns ? {
95
- ...DEFAULT_ERROR_PATTERN_LEARNING_CONFIG,
95
+ ...DEFAULT_ERROR_PATTERNS_CONFIG,
96
96
  ...config.errorPatterns,
97
- } : DEFAULT_ERROR_PATTERN_LEARNING_CONFIG,
97
+ enableLearning: config.errorPatterns.enableLearning ?? DEFAULT_PATTERN_LEARNING_CONFIG.enabled,
98
+ autoApproveThreshold: config.errorPatterns.autoApproveThreshold ?? DEFAULT_PATTERN_LEARNING_CONFIG.autoApproveThreshold,
99
+ maxLearnedPatterns: config.errorPatterns.maxLearnedPatterns ?? DEFAULT_PATTERN_LEARNING_CONFIG.maxLearnedPatterns,
100
+ minErrorFrequency: config.errorPatterns.minErrorFrequency ?? DEFAULT_PATTERN_LEARNING_CONFIG.minErrorFrequency,
101
+ learningWindowMs: config.errorPatterns.learningWindowMs ?? DEFAULT_PATTERN_LEARNING_CONFIG.learningWindowMs,
102
+ } : DEFAULT_ERROR_PATTERNS_CONFIG,
98
103
  };
99
104
  }
100
105
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azumag/opencode-rate-limit-fallback",
3
- "version": "1.59.0",
3
+ "version": "1.63.0",
4
4
  "description": "OpenCode plugin that automatically switches to fallback models when rate limited",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",