@azumag/opencode-rate-limit-fallback 1.35.0 → 1.36.1

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,128 @@
1
+ /**
2
+ * Diagnostic Reporter
3
+ * Generates and reports diagnostic information about the plugin
4
+ */
5
+ import { Logger } from '../../logger.js';
6
+ import type { PluginConfig, ModelHealth } from '../types/index.js';
7
+ import type { HealthTracker } from '../health/HealthTracker.js';
8
+ import type { CircuitBreaker } from '../circuitbreaker/index.js';
9
+ import { ErrorPatternRegistry } from '../errors/PatternRegistry.js';
10
+ /**
11
+ * Active fallback information
12
+ */
13
+ export interface ActiveFallbackInfo {
14
+ sessionID: string;
15
+ currentProviderID: string;
16
+ currentModelID: string;
17
+ targetProviderID: string;
18
+ targetModelID: string;
19
+ startTime: number;
20
+ }
21
+ /**
22
+ * Circuit breaker status for a model
23
+ */
24
+ export interface CircuitBreakerStatus {
25
+ modelKey: string;
26
+ providerID: string;
27
+ modelID: string;
28
+ state: 'CLOSED' | 'OPEN' | 'HALF_OPEN';
29
+ failureCount: number;
30
+ successCount: number;
31
+ lastFailureTime?: number;
32
+ lastSuccessTime?: number;
33
+ }
34
+ /**
35
+ * Complete diagnostic report
36
+ */
37
+ export interface DiagnosticReport {
38
+ timestamp: number;
39
+ config: {
40
+ source: string;
41
+ data: PluginConfig;
42
+ };
43
+ health: {
44
+ enabled: boolean;
45
+ stats: {
46
+ totalTracked: number;
47
+ totalRequests: number;
48
+ totalSuccesses: number;
49
+ totalFailures: number;
50
+ avgHealthScore: number;
51
+ modelsWithReliableData: number;
52
+ };
53
+ models: ModelHealth[];
54
+ };
55
+ errorPatterns: {
56
+ stats: {
57
+ total: number;
58
+ byProvider: Record<string, number>;
59
+ byPriority: Record<string, number>;
60
+ };
61
+ };
62
+ circuitBreaker: {
63
+ enabled: boolean;
64
+ models: CircuitBreakerStatus[];
65
+ };
66
+ activeFallbacks: ActiveFallbackInfo[];
67
+ }
68
+ /**
69
+ * Report format type
70
+ */
71
+ export type ReportFormat = 'text' | 'json';
72
+ /**
73
+ * Diagnostic Reporter class
74
+ */
75
+ export declare class DiagnosticReporter {
76
+ private config;
77
+ private configSource;
78
+ private healthTracker?;
79
+ private circuitBreaker?;
80
+ private errorPatternRegistry;
81
+ private activeFallbacks;
82
+ private logger;
83
+ constructor(config: PluginConfig, configSource: string, healthTracker?: HealthTracker, circuitBreaker?: CircuitBreaker, errorPatternRegistry?: ErrorPatternRegistry, logger?: Logger);
84
+ /**
85
+ * Generate a complete diagnostic report
86
+ */
87
+ generateReport(): DiagnosticReport;
88
+ /**
89
+ * Generate health tracking report
90
+ */
91
+ private generateHealthReport;
92
+ /**
93
+ * Generate error pattern registry report
94
+ */
95
+ private generateErrorPatternsReport;
96
+ /**
97
+ * Generate circuit breaker report
98
+ */
99
+ private generateCircuitBreakerReport;
100
+ /**
101
+ * Get circuit breaker statuses for all tracked models
102
+ */
103
+ private getCircuitBreakerStatuses;
104
+ /**
105
+ * Format a report as text or JSON
106
+ */
107
+ formatReport(report: DiagnosticReport, format?: ReportFormat): string;
108
+ /**
109
+ * Format report as human-readable text
110
+ */
111
+ private formatReportAsText;
112
+ /**
113
+ * Log current configuration to console
114
+ */
115
+ logCurrentConfig(): void;
116
+ /**
117
+ * Register an active fallback
118
+ */
119
+ registerActiveFallback(info: ActiveFallbackInfo): void;
120
+ /**
121
+ * Unregister an active fallback
122
+ */
123
+ unregisterActiveFallback(sessionID: string): void;
124
+ /**
125
+ * Get active fallbacks count
126
+ */
127
+ getActiveFallbacksCount(): number;
128
+ }
@@ -0,0 +1,285 @@
1
+ /**
2
+ * Diagnostic Reporter
3
+ * Generates and reports diagnostic information about the plugin
4
+ */
5
+ import { ErrorPatternRegistry } from '../errors/PatternRegistry.js';
6
+ /**
7
+ * Diagnostic Reporter class
8
+ */
9
+ export class DiagnosticReporter {
10
+ config;
11
+ configSource;
12
+ healthTracker;
13
+ circuitBreaker;
14
+ errorPatternRegistry;
15
+ activeFallbacks;
16
+ logger;
17
+ constructor(config, configSource, healthTracker, circuitBreaker, errorPatternRegistry, logger) {
18
+ this.config = config;
19
+ this.configSource = configSource;
20
+ this.healthTracker = healthTracker;
21
+ this.circuitBreaker = circuitBreaker;
22
+ this.errorPatternRegistry = errorPatternRegistry || new ErrorPatternRegistry();
23
+ this.activeFallbacks = new Map();
24
+ // Initialize logger
25
+ this.logger = logger || {
26
+ debug: () => { },
27
+ info: () => { },
28
+ warn: () => { },
29
+ error: () => { },
30
+ };
31
+ }
32
+ /**
33
+ * Generate a complete diagnostic report
34
+ */
35
+ generateReport() {
36
+ return {
37
+ timestamp: Date.now(),
38
+ config: {
39
+ source: this.configSource,
40
+ data: this.config,
41
+ },
42
+ health: this.generateHealthReport(),
43
+ errorPatterns: this.generateErrorPatternsReport(),
44
+ circuitBreaker: this.generateCircuitBreakerReport(),
45
+ activeFallbacks: Array.from(this.activeFallbacks.values()),
46
+ };
47
+ }
48
+ /**
49
+ * Generate health tracking report
50
+ */
51
+ generateHealthReport() {
52
+ if (!this.healthTracker) {
53
+ return {
54
+ enabled: false,
55
+ stats: {
56
+ totalTracked: 0,
57
+ totalRequests: 0,
58
+ totalSuccesses: 0,
59
+ totalFailures: 0,
60
+ avgHealthScore: 100,
61
+ modelsWithReliableData: 0,
62
+ },
63
+ models: [],
64
+ };
65
+ }
66
+ const stats = this.healthTracker.getStats();
67
+ return {
68
+ enabled: this.healthTracker.isEnabled(),
69
+ stats,
70
+ models: this.healthTracker.getAllHealthData(),
71
+ };
72
+ }
73
+ /**
74
+ * Generate error pattern registry report
75
+ */
76
+ generateErrorPatternsReport() {
77
+ return {
78
+ stats: this.errorPatternRegistry.getStats(),
79
+ };
80
+ }
81
+ /**
82
+ * Generate circuit breaker report
83
+ */
84
+ generateCircuitBreakerReport() {
85
+ if (!this.circuitBreaker) {
86
+ return {
87
+ enabled: false,
88
+ models: [],
89
+ };
90
+ }
91
+ return {
92
+ enabled: this.config.circuitBreaker?.enabled || false,
93
+ models: this.getCircuitBreakerStatuses(),
94
+ };
95
+ }
96
+ /**
97
+ * Get circuit breaker statuses for all tracked models
98
+ */
99
+ getCircuitBreakerStatuses() {
100
+ if (!this.circuitBreaker) {
101
+ return [];
102
+ }
103
+ const allStates = this.circuitBreaker.getAllStates();
104
+ return allStates.map(({ modelKey, state }) => {
105
+ const [providerID, modelID] = modelKey.split('/');
106
+ return {
107
+ modelKey,
108
+ providerID,
109
+ modelID,
110
+ state: state.state,
111
+ failureCount: state.failureCount,
112
+ successCount: state.successCount,
113
+ lastFailureTime: state.lastFailureTime || undefined,
114
+ lastSuccessTime: state.lastSuccessTime || undefined,
115
+ };
116
+ });
117
+ }
118
+ /**
119
+ * Format a report as text or JSON
120
+ */
121
+ formatReport(report, format = 'text') {
122
+ if (format === 'json') {
123
+ return JSON.stringify(report, null, 2);
124
+ }
125
+ return this.formatReportAsText(report);
126
+ }
127
+ /**
128
+ * Format report as human-readable text
129
+ */
130
+ formatReportAsText(report) {
131
+ const lines = [];
132
+ lines.push('='.repeat(70));
133
+ lines.push('Rate Limit Fallback - Diagnostic Report');
134
+ lines.push('='.repeat(70));
135
+ lines.push(`Generated: ${new Date(report.timestamp).toISOString()}`);
136
+ lines.push('');
137
+ // Configuration section
138
+ lines.push('-'.repeat(70));
139
+ lines.push('CONFIGURATION');
140
+ lines.push('-'.repeat(70));
141
+ lines.push(`Source: ${report.config.source || 'Default (no file found)'}`);
142
+ lines.push(`Enabled: ${report.config.data.enabled}`);
143
+ lines.push(`Fallback Mode: ${report.config.data.fallbackMode}`);
144
+ lines.push(`Cooldown: ${report.config.data.cooldownMs}ms`);
145
+ lines.push(`Health-Based Selection: ${report.config.data.enableHealthBasedSelection ?? false}`);
146
+ lines.push(`Verbose Mode: ${report.config.data.verbose ?? false}`);
147
+ lines.push('');
148
+ lines.push('Fallback Models:');
149
+ for (const model of report.config.data.fallbackModels) {
150
+ lines.push(` - ${model.providerID}/${model.modelID}`);
151
+ }
152
+ lines.push('');
153
+ // Retry policy
154
+ if (report.config.data.retryPolicy) {
155
+ lines.push('Retry Policy:');
156
+ lines.push(` Max Retries: ${report.config.data.retryPolicy.maxRetries}`);
157
+ lines.push(` Strategy: ${report.config.data.retryPolicy.strategy}`);
158
+ lines.push(` Base Delay: ${report.config.data.retryPolicy.baseDelayMs}ms`);
159
+ lines.push(` Max Delay: ${report.config.data.retryPolicy.maxDelayMs}ms`);
160
+ lines.push(` Jitter: ${report.config.data.retryPolicy.jitterEnabled ? 'enabled' : 'disabled'}`);
161
+ lines.push('');
162
+ }
163
+ // Circuit breaker
164
+ if (report.config.data.circuitBreaker) {
165
+ lines.push('Circuit Breaker:');
166
+ lines.push(` Enabled: ${report.config.data.circuitBreaker.enabled}`);
167
+ if (report.config.data.circuitBreaker.enabled) {
168
+ lines.push(` Failure Threshold: ${report.config.data.circuitBreaker.failureThreshold}`);
169
+ lines.push(` Recovery Timeout: ${report.config.data.circuitBreaker.recoveryTimeoutMs}ms`);
170
+ lines.push(` Success Threshold: ${report.config.data.circuitBreaker.successThreshold}`);
171
+ }
172
+ lines.push('');
173
+ }
174
+ // Health tracking section
175
+ lines.push('-'.repeat(70));
176
+ lines.push('HEALTH TRACKING');
177
+ lines.push('-'.repeat(70));
178
+ lines.push(`Enabled: ${report.health.enabled}`);
179
+ if (report.health.enabled) {
180
+ const stats = report.health.stats;
181
+ lines.push(`Total Models Tracked: ${stats.totalTracked}`);
182
+ lines.push(`Total Requests: ${stats.totalRequests}`);
183
+ lines.push(`Total Successes: ${stats.totalSuccesses}`);
184
+ lines.push(`Total Failures: ${stats.totalFailures}`);
185
+ lines.push(`Average Health Score: ${stats.avgHealthScore}/100`);
186
+ lines.push(`Models with Reliable Data: ${stats.modelsWithReliableData}/${stats.totalTracked}`);
187
+ lines.push('');
188
+ if (report.health.models.length > 0) {
189
+ lines.push('Model Health Details:');
190
+ for (const health of report.health.models.sort((a, b) => b.healthScore - a.healthScore)) {
191
+ const successRate = health.totalRequests > 0
192
+ ? Math.round((health.successfulRequests / health.totalRequests) * 100)
193
+ : 0;
194
+ lines.push(` ${health.providerID}/${health.modelID}:`);
195
+ lines.push(` Score: ${health.healthScore}/100`);
196
+ lines.push(` Requests: ${health.totalRequests} (${successRate}% success)`);
197
+ lines.push(` Avg Response: ${health.avgResponseTime}ms`);
198
+ lines.push(` Consecutive Failures: ${health.consecutiveFailures}`);
199
+ lines.push(` Last Used: ${new Date(health.lastUsed).toISOString()}`);
200
+ }
201
+ lines.push('');
202
+ }
203
+ }
204
+ lines.push('');
205
+ // Error patterns section
206
+ lines.push('-'.repeat(70));
207
+ lines.push('ERROR PATTERN REGISTRY');
208
+ lines.push('-'.repeat(70));
209
+ const patternStats = report.errorPatterns.stats;
210
+ lines.push(`Total Patterns: ${patternStats.total}`);
211
+ lines.push('');
212
+ if (Object.keys(patternStats.byProvider).length > 0) {
213
+ lines.push('By Provider:');
214
+ for (const [provider, count] of Object.entries(patternStats.byProvider).sort((a, b) => b[1] - a[1])) {
215
+ lines.push(` ${provider}: ${count} patterns`);
216
+ }
217
+ lines.push('');
218
+ }
219
+ if (Object.keys(patternStats.byPriority).length > 0) {
220
+ lines.push('By Priority:');
221
+ for (const [priority, count] of Object.entries(patternStats.byPriority).sort((a, b) => b[1] - a[1])) {
222
+ lines.push(` ${priority}: ${count} patterns`);
223
+ }
224
+ lines.push('');
225
+ }
226
+ // Circuit breaker section
227
+ lines.push('-'.repeat(70));
228
+ lines.push('CIRCUIT BREAKER');
229
+ lines.push('-'.repeat(70));
230
+ lines.push(`Enabled: ${report.circuitBreaker.enabled}`);
231
+ if (report.circuitBreaker.enabled && report.circuitBreaker.models.length > 0) {
232
+ for (const status of report.circuitBreaker.models) {
233
+ lines.push(` ${status.providerID}/${status.modelID}:`);
234
+ lines.push(` State: ${status.state}`);
235
+ lines.push(` Failures: ${status.failureCount}, Successes: ${status.successCount}`);
236
+ }
237
+ }
238
+ lines.push('');
239
+ // Active fallbacks section
240
+ lines.push('-'.repeat(70));
241
+ lines.push('ACTIVE FALLBACKS');
242
+ lines.push('-'.repeat(70));
243
+ lines.push(`Count: ${report.activeFallbacks.length}`);
244
+ if (report.activeFallbacks.length > 0) {
245
+ for (const fallback of report.activeFallbacks) {
246
+ const duration = Date.now() - fallback.startTime;
247
+ lines.push(` Session ${fallback.sessionID}:`);
248
+ lines.push(` From: ${fallback.currentProviderID}/${fallback.currentModelID}`);
249
+ lines.push(` To: ${fallback.targetProviderID}/${fallback.targetModelID}`);
250
+ lines.push(` Duration: ${duration}ms`);
251
+ }
252
+ }
253
+ lines.push('');
254
+ lines.push('='.repeat(70));
255
+ lines.push('End of Report');
256
+ lines.push('='.repeat(70));
257
+ return lines.join('\n');
258
+ }
259
+ /**
260
+ * Log current configuration to console
261
+ */
262
+ logCurrentConfig() {
263
+ const report = this.generateReport();
264
+ const formatted = this.formatReport(report, 'text');
265
+ this.logger.info(formatted);
266
+ }
267
+ /**
268
+ * Register an active fallback
269
+ */
270
+ registerActiveFallback(info) {
271
+ this.activeFallbacks.set(info.sessionID, info);
272
+ }
273
+ /**
274
+ * Unregister an active fallback
275
+ */
276
+ unregisterActiveFallback(sessionID) {
277
+ this.activeFallbacks.delete(sessionID);
278
+ }
279
+ /**
280
+ * Get active fallbacks count
281
+ */
282
+ getActiveFallbacksCount() {
283
+ return this.activeFallbacks.size;
284
+ }
285
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Error Pattern Registry for rate limit error detection
3
+ */
4
+ import type { ErrorPattern } from '../types/index.js';
5
+ import { Logger } from '../../logger.js';
6
+ /**
7
+ * Error Pattern Registry class
8
+ * Manages and matches error patterns for rate limit detection
9
+ */
10
+ export declare class ErrorPatternRegistry {
11
+ private patterns;
12
+ private logger;
13
+ constructor(logger?: Logger);
14
+ /**
15
+ * Register default rate limit error patterns
16
+ */
17
+ registerDefaultPatterns(): void;
18
+ /**
19
+ * Register a new error pattern
20
+ */
21
+ register(pattern: ErrorPattern): void;
22
+ /**
23
+ * Register multiple error patterns
24
+ */
25
+ registerMany(patterns: ErrorPattern[]): void;
26
+ /**
27
+ * Check if an error matches any registered rate limit pattern
28
+ */
29
+ isRateLimitError(error: unknown): boolean;
30
+ /**
31
+ * Get the matched pattern for an error, or null if no match
32
+ */
33
+ getMatchedPattern(error: unknown): ErrorPattern | null;
34
+ /**
35
+ * Get all registered patterns
36
+ */
37
+ getAllPatterns(): ErrorPattern[];
38
+ /**
39
+ * Get patterns for a specific provider
40
+ */
41
+ getPatternsForProvider(provider: string): ErrorPattern[];
42
+ /**
43
+ * Get patterns by name
44
+ */
45
+ getPatternByName(name: string): ErrorPattern | undefined;
46
+ /**
47
+ * Remove a pattern by name
48
+ */
49
+ removePattern(name: string): boolean;
50
+ /**
51
+ * Clear all patterns (including default ones)
52
+ */
53
+ clearAllPatterns(): void;
54
+ /**
55
+ * Reset to default patterns only
56
+ */
57
+ resetToDefaults(): void;
58
+ /**
59
+ * Learn a new pattern from an error (for future ML-based learning)
60
+ * Currently disabled - patterns must be manually registered
61
+ */
62
+ addLearnedPattern(_error: unknown): void;
63
+ /**
64
+ * Get statistics about registered patterns
65
+ */
66
+ getStats(): {
67
+ total: number;
68
+ byProvider: Record<string, number>;
69
+ byPriority: Record<string, number>;
70
+ };
71
+ /**
72
+ * Get a readable priority range string
73
+ */
74
+ private getPriorityRange;
75
+ }