@azumag/opencode-rate-limit-fallback 1.49.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
  }
@@ -29,7 +29,10 @@ export declare class ConfigReloader {
29
29
  private worktree?;
30
30
  private notifyOnReload;
31
31
  private reloadMetrics;
32
- constructor(config: PluginConfig, configPath: string | null, logger: Logger, validator: ConfigValidator, client: OpenCodeClient, components: ComponentRefs, directory: string, worktree?: string, notifyOnReload?: boolean);
32
+ private minReloadIntervalMs;
33
+ private recentReloadAttempts;
34
+ private maxReloadAttemptsPerMinute;
35
+ constructor(config: PluginConfig, configPath: string | null, logger: Logger, validator: ConfigValidator, client: OpenCodeClient, components: ComponentRefs, directory: string, worktree?: string, notifyOnReload?: boolean, minReloadIntervalMs?: number, maxReloadAttemptsPerMinute?: number);
33
36
  /**
34
37
  * Reload configuration from file
35
38
  */
@@ -50,6 +53,10 @@ export declare class ConfigReloader {
50
53
  * Get reload metrics
51
54
  */
52
55
  getReloadMetrics(): ReloadMetrics;
56
+ /**
57
+ * Check if reload is allowed based on rate limiting
58
+ */
59
+ private checkRateLimit;
53
60
  /**
54
61
  * Show success toast notification
55
62
  */
@@ -17,7 +17,11 @@ export class ConfigReloader {
17
17
  worktree;
18
18
  notifyOnReload;
19
19
  reloadMetrics;
20
- constructor(config, configPath, logger, validator, client, components, directory, worktree, notifyOnReload = true) {
20
+ // Rate limiting for reload operations
21
+ minReloadIntervalMs;
22
+ recentReloadAttempts;
23
+ maxReloadAttemptsPerMinute;
24
+ constructor(config, configPath, logger, validator, client, components, directory, worktree, notifyOnReload = true, minReloadIntervalMs = 1000, maxReloadAttemptsPerMinute = 10) {
21
25
  this.config = config;
22
26
  this.configPath = configPath;
23
27
  this.logger = logger;
@@ -32,6 +36,10 @@ export class ConfigReloader {
32
36
  successfulReloads: 0,
33
37
  failedReloads: 0,
34
38
  };
39
+ // Rate limiting settings
40
+ this.minReloadIntervalMs = minReloadIntervalMs;
41
+ this.maxReloadAttemptsPerMinute = maxReloadAttemptsPerMinute;
42
+ this.recentReloadAttempts = [];
35
43
  }
36
44
  /**
37
45
  * Reload configuration from file
@@ -41,9 +49,17 @@ export class ConfigReloader {
41
49
  success: false,
42
50
  timestamp: Date.now(),
43
51
  };
52
+ // Rate limiting check
53
+ const rateLimitCheck = this.checkRateLimit();
54
+ if (!rateLimitCheck.allowed) {
55
+ result.error = rateLimitCheck.reason || 'Rate limit exceeded';
56
+ this.logger.warn(`Config reload blocked: ${result.error}`);
57
+ return result;
58
+ }
44
59
  // Track reload metrics
45
60
  this.reloadMetrics.totalReloads++;
46
61
  this.reloadMetrics.lastReloadTime = result.timestamp;
62
+ this.recentReloadAttempts.push(result.timestamp);
47
63
  if (!this.configPath) {
48
64
  result.error = 'No config file path available';
49
65
  this.reloadMetrics.failedReloads++;
@@ -173,6 +189,34 @@ export class ConfigReloader {
173
189
  getReloadMetrics() {
174
190
  return { ...this.reloadMetrics };
175
191
  }
192
+ /**
193
+ * Check if reload is allowed based on rate limiting
194
+ */
195
+ checkRateLimit() {
196
+ const now = Date.now();
197
+ const oneMinuteAgo = now - 60000;
198
+ // Clean up old reload attempts
199
+ this.recentReloadAttempts = this.recentReloadAttempts.filter(timestamp => timestamp > oneMinuteAgo);
200
+ // Check minimum interval between reloads
201
+ if (this.reloadMetrics.lastReloadTime) {
202
+ const timeSinceLastReload = now - this.reloadMetrics.lastReloadTime;
203
+ if (timeSinceLastReload < this.minReloadIntervalMs) {
204
+ const waitTime = this.minReloadIntervalMs - timeSinceLastReload;
205
+ return {
206
+ allowed: false,
207
+ reason: `Too soon. Wait ${waitTime}ms before reloading`,
208
+ };
209
+ }
210
+ }
211
+ // Check maximum attempts per minute
212
+ if (this.recentReloadAttempts.length >= this.maxReloadAttemptsPerMinute) {
213
+ return {
214
+ allowed: false,
215
+ reason: `Rate limit exceeded. Maximum ${this.maxReloadAttemptsPerMinute} reloads per minute`,
216
+ };
217
+ }
218
+ return { allowed: true };
219
+ }
176
220
  /**
177
221
  * Show success toast notification
178
222
  */
@@ -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
  /**