@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.
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Pattern Storage for Learned Error Patterns
3
+ */
4
+ import { readFile, writeFile } from 'fs/promises';
5
+ import { calculateJaccardSimilarity } from '../utils/similarity.js';
6
+ /**
7
+ * PatternStorage - Manages persistence of learned patterns to config file
8
+ */
9
+ export class PatternStorage {
10
+ configPath;
11
+ logger;
12
+ constructor(configPath, logger) {
13
+ this.configPath = configPath;
14
+ this.logger = logger;
15
+ }
16
+ /**
17
+ * Save a learned pattern to config file
18
+ */
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
+ }
55
+ }
56
+ /**
57
+ * Load all learned patterns from config file
58
+ */
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
+ }
76
+ }
77
+ /**
78
+ * Delete a pattern by name from config file
79
+ */
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
+ }
102
+ }
103
+ /**
104
+ * Merge duplicate patterns with high similarity
105
+ */
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;
113
+ // Find and merge similar patterns
114
+ const patternsToMerge = [];
115
+ for (let i = 0; i < learnedPatterns.length; i++) {
116
+ if (patternsToMerge.includes(i))
117
+ 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}`);
136
+ }
137
+ }
138
+ }
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;
150
+ }
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;
156
+ }
157
+ }
158
+ /**
159
+ * Cleanup old patterns, keeping only the most confident ones
160
+ */
161
+ async cleanupOldPatterns(maxCount, patterns) {
162
+ 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;
168
+ }
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;
186
+ }
187
+ catch (error) {
188
+ this.logger.error('[PatternStorage] Failed to cleanup patterns', {
189
+ error: error instanceof Error ? error.message : String(error),
190
+ });
191
+ throw error;
192
+ }
193
+ }
194
+ /**
195
+ * Load config file
196
+ */
197
+ async loadConfig() {
198
+ try {
199
+ const content = await readFile(this.configPath, 'utf-8');
200
+ return JSON.parse(content);
201
+ }
202
+ catch (error) {
203
+ // If file doesn't exist or is invalid, return empty config
204
+ return {};
205
+ }
206
+ }
207
+ /**
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
215
+ */
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');
225
+ }
226
+ /**
227
+ * Calculate similarity between two patterns
228
+ */
229
+ calculatePatternSimilarity(pattern1, pattern2) {
230
+ const text1 = pattern1.patterns.join(' ').toLowerCase();
231
+ const text2 = pattern2.patterns.join(' ').toLowerCase();
232
+ return calculateJaccardSimilarity(text1, text2);
233
+ }
234
+ }
@@ -25,6 +25,7 @@ export declare class FallbackHandler {
25
25
  private retryManager;
26
26
  private circuitBreaker?;
27
27
  private healthTracker?;
28
+ private dynamicPrioritizer?;
28
29
  constructor(config: PluginConfig, client: OpenCodeClient, logger: Logger, metricsManager: MetricsManager, subagentTracker: SubagentTracker, healthTracker?: HealthTracker);
29
30
  /**
30
31
  * Check and mark fallback in progress for deduplication
@@ -46,8 +47,8 @@ export declare class FallbackHandler {
46
47
  */
47
48
  private abortSession;
48
49
  /**
49
- * Queue the prompt asynchronously (non-blocking), then abort the retry loop.
50
- * promptAsync FIRST queues pending work so the server doesn't dispose on idle.
50
+ * Queue prompt asynchronously (non-blocking), then abort retry loop.
51
+ * promptAsync FIRST queues pending work so that server doesn't dispose on idle.
51
52
  * abort SECOND cancels the retry loop; the server sees the queued prompt and processes it.
52
53
  */
53
54
  retryWithModel(targetSessionID: string, model: FallbackModel, parts: MessagePart[], hierarchy: SessionHierarchy | null): Promise<void>;
@@ -6,6 +6,8 @@ import { ModelSelector } from './ModelSelector.js';
6
6
  import { CircuitBreaker } from '../circuitbreaker/CircuitBreaker.js';
7
7
  import { extractMessageParts, convertPartsToSDKFormat, safeShowToast, getStateKey, getModelKey, DEDUP_WINDOW_MS, STATE_TIMEOUT_MS } from '../utils/helpers.js';
8
8
  import { RetryManager } from '../retry/RetryManager.js';
9
+ import { DynamicPrioritizer } from '../dynamic/DynamicPrioritizer.js';
10
+ import { DEFAULT_DYNAMIC_PRIORITIZATION_CONFIG } from '../config/defaults.js';
9
11
  /**
10
12
  * Fallback Handler class for orchestrating the fallback retry flow
11
13
  */
@@ -30,6 +32,8 @@ export class FallbackHandler {
30
32
  circuitBreaker;
31
33
  // Health tracker reference
32
34
  healthTracker;
35
+ // Dynamic prioritizer reference
36
+ dynamicPrioritizer;
33
37
  constructor(config, client, logger, metricsManager, subagentTracker, healthTracker) {
34
38
  this.config = config;
35
39
  this.client = client;
@@ -41,7 +45,12 @@ export class FallbackHandler {
41
45
  if (config.circuitBreaker?.enabled) {
42
46
  this.circuitBreaker = new CircuitBreaker(config.circuitBreaker, logger, metricsManager, client);
43
47
  }
44
- this.modelSelector = new ModelSelector(config, client, this.circuitBreaker, healthTracker);
48
+ // Initialize dynamic prioritizer if enabled and health tracker is available
49
+ if (healthTracker && config.dynamicPrioritization?.enabled) {
50
+ const dynamicConfig = { ...DEFAULT_DYNAMIC_PRIORITIZATION_CONFIG, ...config.dynamicPrioritization };
51
+ this.dynamicPrioritizer = new DynamicPrioritizer(dynamicConfig, healthTracker, logger, metricsManager);
52
+ }
53
+ this.modelSelector = new ModelSelector(config, client, this.circuitBreaker, healthTracker, this.dynamicPrioritizer);
45
54
  this.currentSessionModel = new Map();
46
55
  this.modelRequestStartTimes = new Map();
47
56
  this.retryState = new Map();
@@ -95,18 +104,22 @@ export class FallbackHandler {
95
104
  }
96
105
  }
97
106
  /**
98
- * Queue the prompt asynchronously (non-blocking), then abort the retry loop.
99
- * promptAsync FIRST queues pending work so the server doesn't dispose on idle.
107
+ * Queue prompt asynchronously (non-blocking), then abort retry loop.
108
+ * promptAsync FIRST queues pending work so that server doesn't dispose on idle.
100
109
  * abort SECOND cancels the retry loop; the server sees the queued prompt and processes it.
101
110
  */
102
111
  async retryWithModel(targetSessionID, model, parts, hierarchy) {
103
- // Track the new model for this session
112
+ // Record model usage for dynamic prioritization
113
+ if (this.dynamicPrioritizer) {
114
+ this.dynamicPrioritizer.recordUsage(model.providerID, model.modelID);
115
+ }
116
+ // Track new model for this session
104
117
  this.currentSessionModel.set(targetSessionID, {
105
118
  providerID: model.providerID,
106
119
  modelID: model.modelID,
107
120
  lastUpdated: Date.now(),
108
121
  });
109
- // If this is a root session with subagents, propagate the model to all subagents
122
+ // If this is a root session with subagents, propagate model to all subagents
110
123
  if (hierarchy) {
111
124
  if (hierarchy.rootSessionID === targetSessionID) {
112
125
  hierarchy.sharedFallbackState = "completed";
@@ -464,6 +477,26 @@ export class FallbackHandler {
464
477
  this.modelSelector.setCircuitBreaker(undefined);
465
478
  }
466
479
  }
480
+ // Handle dynamic prioritizer configuration changes
481
+ const oldDynamicPrioritizerEnabled = this.dynamicPrioritizer !== undefined;
482
+ if (newConfig.dynamicPrioritization?.enabled !== oldDynamicPrioritizerEnabled) {
483
+ if (newConfig.dynamicPrioritization?.enabled && this.healthTracker) {
484
+ // Create new dynamic prioritizer
485
+ const dynamicConfig = { ...DEFAULT_DYNAMIC_PRIORITIZATION_CONFIG, ...newConfig.dynamicPrioritization };
486
+ this.dynamicPrioritizer = new DynamicPrioritizer(dynamicConfig, this.healthTracker, this.logger, this.metricsManager);
487
+ this.modelSelector.setDynamicPrioritizer(this.dynamicPrioritizer);
488
+ }
489
+ else if (!newConfig.dynamicPrioritization?.enabled) {
490
+ // Disable dynamic prioritizer
491
+ this.dynamicPrioritizer = undefined;
492
+ this.modelSelector.setDynamicPrioritizer(undefined);
493
+ }
494
+ }
495
+ else if (this.dynamicPrioritizer && newConfig.dynamicPrioritization) {
496
+ // Update existing dynamic prioritizer config
497
+ const dynamicConfig = { ...DEFAULT_DYNAMIC_PRIORITIZATION_CONFIG, ...newConfig.dynamicPrioritization };
498
+ this.dynamicPrioritizer.updateConfig(dynamicConfig);
499
+ }
467
500
  this.logger.debug('FallbackHandler configuration updated');
468
501
  }
469
502
  }
@@ -4,6 +4,7 @@
4
4
  import type { FallbackModel, PluginConfig, OpenCodeClient } from '../types/index.js';
5
5
  import type { CircuitBreaker } from '../circuitbreaker/index.js';
6
6
  import type { HealthTracker } from '../health/HealthTracker.js';
7
+ import type { DynamicPrioritizer } from '../dynamic/DynamicPrioritizer.js';
7
8
  /**
8
9
  * Model Selector class for handling model selection strategies
9
10
  */
@@ -13,7 +14,8 @@ export declare class ModelSelector {
13
14
  private client;
14
15
  private circuitBreaker?;
15
16
  private healthTracker?;
16
- constructor(config: PluginConfig, client: OpenCodeClient, circuitBreaker?: CircuitBreaker, healthTracker?: HealthTracker);
17
+ private dynamicPrioritizer?;
18
+ constructor(config: PluginConfig, client: OpenCodeClient, circuitBreaker?: CircuitBreaker, healthTracker?: HealthTracker, dynamicPrioritizer?: DynamicPrioritizer);
17
19
  /**
18
20
  * Check if a model is currently rate limited
19
21
  */
@@ -50,4 +52,8 @@ export declare class ModelSelector {
50
52
  * Set circuit breaker (for hot reload)
51
53
  */
52
54
  setCircuitBreaker(circuitBreaker: CircuitBreaker | undefined): void;
55
+ /**
56
+ * Set dynamic prioritizer (for hot reload)
57
+ */
58
+ setDynamicPrioritizer(dynamicPrioritizer: DynamicPrioritizer | undefined): void;
53
59
  }
@@ -12,11 +12,13 @@ export class ModelSelector {
12
12
  client;
13
13
  circuitBreaker;
14
14
  healthTracker;
15
- constructor(config, client, circuitBreaker, healthTracker) {
15
+ dynamicPrioritizer;
16
+ constructor(config, client, circuitBreaker, healthTracker, dynamicPrioritizer) {
16
17
  this.config = config;
17
18
  this.client = client;
18
19
  this.circuitBreaker = circuitBreaker;
19
20
  this.healthTracker = healthTracker;
21
+ this.dynamicPrioritizer = dynamicPrioritizer;
20
22
  this.rateLimitedModels = new Map();
21
23
  }
22
24
  /**
@@ -57,6 +59,11 @@ export class ModelSelector {
57
59
  candidates.push(model);
58
60
  }
59
61
  }
62
+ // Apply dynamic prioritization if enabled
63
+ if (this.dynamicPrioritizer && this.dynamicPrioritizer.isEnabled() && this.dynamicPrioritizer.shouldUseDynamicOrdering()) {
64
+ const prioritizedCandidates = this.dynamicPrioritizer.getPrioritizedModels(candidates);
65
+ return prioritizedCandidates[0] || null;
66
+ }
60
67
  // Sort by health score if health tracker is enabled
61
68
  if (this.healthTracker && this.config.enableHealthBasedSelection) {
62
69
  const healthiest = this.healthTracker.getHealthiestModels(candidates);
@@ -172,4 +179,10 @@ export class ModelSelector {
172
179
  setCircuitBreaker(circuitBreaker) {
173
180
  this.circuitBreaker = circuitBreaker;
174
181
  }
182
+ /**
183
+ * Set dynamic prioritizer (for hot reload)
184
+ */
185
+ setDynamicPrioritizer(dynamicPrioritizer) {
186
+ this.dynamicPrioritizer = dynamicPrioritizer;
187
+ }
175
188
  }
@@ -65,6 +65,14 @@ export declare class MetricsManager {
65
65
  * Record a circuit breaker state transition
66
66
  */
67
67
  recordCircuitBreakerStateTransition(modelKey: string, oldState: CircuitBreakerStateType, newState: CircuitBreakerStateType): void;
68
+ /**
69
+ * Update dynamic prioritization metrics
70
+ */
71
+ updateDynamicPrioritizationMetrics(enabled: boolean, reorders: number, modelsWithDynamicScores: number): void;
72
+ /**
73
+ * Record a model reorder event
74
+ */
75
+ recordDynamicPrioritizationReorder(): void;
68
76
  /**
69
77
  * Helper method to update circuit breaker state counts
70
78
  * @private
@@ -43,6 +43,11 @@ export class MetricsManager {
43
43
  },
44
44
  byModel: new Map(),
45
45
  },
46
+ dynamicPrioritization: {
47
+ enabled: false,
48
+ reorders: 0,
49
+ modelsWithDynamicScores: 0,
50
+ },
46
51
  startedAt: Date.now(),
47
52
  generatedAt: Date.now(),
48
53
  };
@@ -95,6 +100,11 @@ export class MetricsManager {
95
100
  },
96
101
  byModel: new Map(),
97
102
  },
103
+ dynamicPrioritization: {
104
+ enabled: false,
105
+ reorders: 0,
106
+ modelsWithDynamicScores: 0,
107
+ },
98
108
  startedAt: Date.now(),
99
109
  generatedAt: Date.now(),
100
110
  };
@@ -280,6 +290,24 @@ export class MetricsManager {
280
290
  this.updateCircuitBreakerStateCounts(modelMetrics, oldState, newState);
281
291
  this.metrics.circuitBreaker.byModel.set(modelKey, modelMetrics);
282
292
  }
293
+ /**
294
+ * Update dynamic prioritization metrics
295
+ */
296
+ updateDynamicPrioritizationMetrics(enabled, reorders, modelsWithDynamicScores) {
297
+ if (!this.config.enabled)
298
+ return;
299
+ this.metrics.dynamicPrioritization.enabled = enabled;
300
+ this.metrics.dynamicPrioritization.reorders = reorders;
301
+ this.metrics.dynamicPrioritization.modelsWithDynamicScores = modelsWithDynamicScores;
302
+ }
303
+ /**
304
+ * Record a model reorder event
305
+ */
306
+ recordDynamicPrioritizationReorder() {
307
+ if (!this.config.enabled)
308
+ return;
309
+ this.metrics.dynamicPrioritization.reorders++;
310
+ }
283
311
  /**
284
312
  * Helper method to update circuit breaker state counts
285
313
  * @private
@@ -350,6 +378,7 @@ export class MetricsManager {
350
378
  ...metrics.circuitBreaker,
351
379
  byModel: Object.fromEntries(Array.from(metrics.circuitBreaker.byModel.entries()).map(([k, v]) => [k, v])),
352
380
  },
381
+ dynamicPrioritization: metrics.dynamicPrioritization,
353
382
  startedAt: metrics.startedAt,
354
383
  generatedAt: metrics.generatedAt,
355
384
  };
@@ -472,6 +501,13 @@ export class MetricsManager {
472
501
  }
473
502
  }
474
503
  }
504
+ lines.push("");
505
+ // Dynamic Prioritization
506
+ lines.push("Dynamic Prioritization:");
507
+ lines.push("-".repeat(40));
508
+ lines.push(` Enabled: ${metrics.dynamicPrioritization.enabled ? 'Yes' : 'No'}`);
509
+ lines.push(` Reorders: ${metrics.dynamicPrioritization.reorders}`);
510
+ lines.push(` Models with dynamic scores: ${metrics.dynamicPrioritization.modelsWithDynamicScores}`);
475
511
  return lines.join("\n");
476
512
  }
477
513
  /**
@@ -579,6 +615,15 @@ export class MetricsManager {
579
615
  successRate,
580
616
  ].join(","));
581
617
  }
618
+ lines.push("");
619
+ // Dynamic Prioritization CSV
620
+ lines.push("=== DYNAMIC_PRIORITIZATION ===");
621
+ lines.push("enabled,reorders,models_with_dynamic_scores");
622
+ lines.push([
623
+ metrics.dynamicPrioritization.enabled ? 'Yes' : 'No',
624
+ metrics.dynamicPrioritization.reorders,
625
+ metrics.dynamicPrioritization.modelsWithDynamicScores,
626
+ ].join(","));
582
627
  return lines.join("\n");
583
628
  }
584
629
  /**
@@ -113,6 +113,26 @@ export interface HealthTrackerConfig {
113
113
  * Use this for backward compatibility.
114
114
  */
115
115
  export type HealthPersistenceConfig = HealthTrackerConfig;
116
+ /**
117
+ * Dynamic prioritization configuration
118
+ */
119
+ export interface DynamicPrioritizationConfig {
120
+ enabled: boolean;
121
+ updateInterval: number;
122
+ successRateWeight: number;
123
+ responseTimeWeight: number;
124
+ recentUsageWeight: number;
125
+ minSamples: number;
126
+ maxHistorySize: number;
127
+ }
128
+ /**
129
+ * Dynamic prioritization metrics
130
+ */
131
+ export interface DynamicPrioritizationMetrics {
132
+ enabled: boolean;
133
+ reorders: number;
134
+ modelsWithDynamicScores: number;
135
+ }
116
136
  /**
117
137
  * Health metrics for a model
118
138
  */
@@ -139,12 +159,45 @@ export interface ErrorPattern {
139
159
  patterns: (string | RegExp)[];
140
160
  priority: number;
141
161
  }
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
173
+ */
174
+ export interface LearnedPattern extends ErrorPattern {
175
+ confidence: number;
176
+ learnedAt: string;
177
+ sampleCount: number;
178
+ lastUsed?: number;
179
+ }
180
+ /**
181
+ * Learning configuration
182
+ */
183
+ export interface LearningConfig {
184
+ enabled: boolean;
185
+ autoApproveThreshold: number;
186
+ maxLearnedPatterns: number;
187
+ minErrorFrequency: number;
188
+ learningWindowMs: number;
189
+ }
142
190
  /**
143
191
  * Error pattern configuration
144
192
  */
145
193
  export interface ErrorPatternsConfig {
146
194
  custom?: ErrorPattern[];
147
195
  enableLearning?: boolean;
196
+ learnedPatterns?: LearnedPattern[];
197
+ autoApproveThreshold?: number;
198
+ maxLearnedPatterns?: number;
199
+ minErrorFrequency?: number;
200
+ learningWindowMs?: number;
148
201
  }
149
202
  /**
150
203
  * Configuration hot reload settings
@@ -193,6 +246,7 @@ export interface PluginConfig {
193
246
  verbose?: boolean;
194
247
  errorPatterns?: ErrorPatternsConfig;
195
248
  configReload?: ConfigReloadConfig;
249
+ dynamicPrioritization?: DynamicPrioritizationConfig;
196
250
  }
197
251
  /**
198
252
  * Fallback state for tracking progress
@@ -357,6 +411,7 @@ export interface MetricsData {
357
411
  total: CircuitBreakerMetrics;
358
412
  byModel: Map<string, CircuitBreakerMetrics>;
359
413
  };
414
+ dynamicPrioritization: DynamicPrioritizationMetrics;
360
415
  startedAt: number;
361
416
  generatedAt: number;
362
417
  }
@@ -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, } 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_PATTERN_LEARNING_CONFIG, } from '../config/defaults.js';
8
8
  /**
9
9
  * Default plugin configuration
10
10
  */
@@ -19,6 +19,8 @@ export const DEFAULT_CONFIG = {
19
19
  log: DEFAULT_LOG_CONFIG,
20
20
  metrics: DEFAULT_METRICS_CONFIG,
21
21
  configReload: DEFAULT_CONFIG_RELOAD_CONFIG,
22
+ dynamicPrioritization: DEFAULT_DYNAMIC_PRIORITIZATION_CONFIG,
23
+ errorPatterns: DEFAULT_ERROR_PATTERN_LEARNING_CONFIG,
22
24
  };
23
25
  /**
24
26
  * Validate that a path does not contain directory traversal attempts
@@ -85,6 +87,14 @@ export function validateConfig(config) {
85
87
  ...DEFAULT_CONFIG.configReload,
86
88
  ...config.configReload,
87
89
  } : DEFAULT_CONFIG.configReload,
90
+ dynamicPrioritization: config.dynamicPrioritization ? {
91
+ ...DEFAULT_DYNAMIC_PRIORITIZATION_CONFIG,
92
+ ...config.dynamicPrioritization,
93
+ } : DEFAULT_DYNAMIC_PRIORITIZATION_CONFIG,
94
+ errorPatterns: config.errorPatterns ? {
95
+ ...DEFAULT_ERROR_PATTERN_LEARNING_CONFIG,
96
+ ...config.errorPatterns,
97
+ } : DEFAULT_ERROR_PATTERN_LEARNING_CONFIG,
88
98
  };
89
99
  }
90
100
  /**
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Similarity utility functions
3
+ */
4
+ /**
5
+ * Calculate Jaccard similarity between two strings
6
+ * @param str1 - First string
7
+ * @param str2 - Second string
8
+ * @returns Similarity score between 0 and 1
9
+ */
10
+ export declare function calculateJaccardSimilarity(str1: string, str2: string): number;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Similarity utility functions
3
+ */
4
+ /**
5
+ * Calculate Jaccard similarity between two strings
6
+ * @param str1 - First string
7
+ * @param str2 - Second string
8
+ * @returns Similarity score between 0 and 1
9
+ */
10
+ export function calculateJaccardSimilarity(str1, str2) {
11
+ // Tokenize strings
12
+ const tokens1 = new Set(str1.split(/\s+/).filter(t => t.length > 0));
13
+ const tokens2 = new Set(str2.split(/\s+/).filter(t => t.length > 0));
14
+ if (tokens1.size === 0 && tokens2.size === 0) {
15
+ return 1;
16
+ }
17
+ if (tokens1.size === 0 || tokens2.size === 0) {
18
+ return 0;
19
+ }
20
+ // Calculate intersection and union
21
+ const intersection = new Set([...tokens1].filter(x => tokens2.has(x)));
22
+ const union = new Set([...tokens1, ...tokens2]);
23
+ return intersection.size / union.size;
24
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azumag/opencode-rate-limit-fallback",
3
- "version": "1.50.0",
3
+ "version": "1.57.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",