vibecodingmachine-core 1.0.2 → 2025.11.2-7.1239

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.
Files changed (49) hide show
  1. package/.babelrc +13 -13
  2. package/README.md +28 -28
  3. package/__tests__/applescript-manager-claude-fix.test.js +286 -286
  4. package/__tests__/requirement-2-auto-start-looping.test.js +69 -69
  5. package/__tests__/requirement-3-auto-start-looping.test.js +69 -69
  6. package/__tests__/requirement-4-auto-start-looping.test.js +69 -69
  7. package/__tests__/requirement-6-auto-start-looping.test.js +73 -73
  8. package/__tests__/requirement-7-status-tracking.test.js +332 -332
  9. package/jest.config.js +18 -18
  10. package/jest.setup.js +12 -12
  11. package/package.json +48 -48
  12. package/src/auth/access-denied.html +119 -119
  13. package/src/auth/shared-auth-storage.js +230 -230
  14. package/src/autonomous-mode/feature-implementer.cjs +70 -70
  15. package/src/autonomous-mode/feature-implementer.js +425 -425
  16. package/src/chat-management/chat-manager.cjs +71 -71
  17. package/src/chat-management/chat-manager.js +342 -342
  18. package/src/ide-integration/__tests__/applescript-manager-thread-closure.test.js +227 -227
  19. package/src/ide-integration/aider-cli-manager.cjs +850 -850
  20. package/src/ide-integration/applescript-manager.cjs +1088 -1088
  21. package/src/ide-integration/applescript-manager.js +2802 -2802
  22. package/src/ide-integration/applescript-utils.js +306 -306
  23. package/src/ide-integration/cdp-manager.cjs +221 -221
  24. package/src/ide-integration/cdp-manager.js +321 -321
  25. package/src/ide-integration/claude-code-cli-manager.cjs +301 -301
  26. package/src/ide-integration/cline-cli-manager.cjs +2252 -2252
  27. package/src/ide-integration/continue-cli-manager.js +431 -431
  28. package/src/ide-integration/provider-manager.cjs +354 -354
  29. package/src/ide-integration/quota-detector.cjs +34 -34
  30. package/src/ide-integration/quota-detector.js +349 -349
  31. package/src/ide-integration/windows-automation-manager.js +262 -262
  32. package/src/index.cjs +47 -43
  33. package/src/index.js +17 -17
  34. package/src/llm/direct-llm-manager.cjs +609 -609
  35. package/src/ui/ButtonComponents.js +247 -247
  36. package/src/ui/ChatInterface.js +499 -499
  37. package/src/ui/StateManager.js +259 -259
  38. package/src/utils/audit-logger.cjs +116 -116
  39. package/src/utils/config-helpers.cjs +94 -94
  40. package/src/utils/config-helpers.js +94 -94
  41. package/src/utils/electron-update-checker.js +113 -85
  42. package/src/utils/gcloud-auth.cjs +394 -394
  43. package/src/utils/logger.cjs +193 -193
  44. package/src/utils/logger.js +191 -191
  45. package/src/utils/repo-helpers.cjs +120 -120
  46. package/src/utils/repo-helpers.js +120 -120
  47. package/src/utils/requirement-helpers.js +432 -432
  48. package/src/utils/update-checker.js +227 -167
  49. package/src/utils/version-checker.js +169 -0
@@ -1,354 +1,354 @@
1
- /**
2
- * Provider Manager - Handles LLM provider rotation and rate limit tracking
3
- */
4
-
5
- const fs = require('fs');
6
- const path = require('path');
7
- const os = require('os');
8
-
9
- class ProviderManager {
10
- constructor() {
11
- this.rateLimitFile = path.join(os.homedir(), '.config', 'allnightai', 'rate-limits.json');
12
- this.performanceFile = path.join(os.homedir(), '.config', 'allnightai', 'provider-performance.json');
13
- this.rateLimits = this.loadRateLimits();
14
- this.performance = this.loadPerformance();
15
- }
16
-
17
- /**
18
- * Load rate limit tracking data from file
19
- * Auto-cleanup expired rate limits
20
- */
21
- loadRateLimits() {
22
- try {
23
- if (fs.existsSync(this.rateLimitFile)) {
24
- const data = fs.readFileSync(this.rateLimitFile, 'utf8');
25
- const limits = JSON.parse(data);
26
-
27
- // Auto-cleanup expired rate limits
28
- const now = Date.now();
29
- let cleaned = false;
30
- for (const key in limits) {
31
- if (limits[key].resetTime && limits[key].resetTime <= now) {
32
- delete limits[key];
33
- cleaned = true;
34
- }
35
- }
36
-
37
- // Save cleaned data if any expired limits were removed
38
- if (cleaned) {
39
- this.saveRateLimitsInternal(limits);
40
- }
41
-
42
- return limits;
43
- }
44
- } catch (err) {
45
- console.error('Error loading rate limits:', err.message);
46
- }
47
- return {};
48
- }
49
-
50
- /**
51
- * Internal save without reload (to prevent infinite loop during cleanup)
52
- */
53
- saveRateLimitsInternal(limits) {
54
- try {
55
- const dir = path.dirname(this.rateLimitFile);
56
- if (!fs.existsSync(dir)) {
57
- fs.mkdirSync(dir, { recursive: true });
58
- }
59
- fs.writeFileSync(this.rateLimitFile, JSON.stringify(limits, null, 2));
60
- } catch (err) {
61
- console.error('Error saving rate limits:', err.message);
62
- }
63
- }
64
-
65
- /**
66
- * Save rate limit tracking data to file
67
- */
68
- saveRateLimits() {
69
- try {
70
- const dir = path.dirname(this.rateLimitFile);
71
- if (!fs.existsSync(dir)) {
72
- fs.mkdirSync(dir, { recursive: true });
73
- }
74
- fs.writeFileSync(this.rateLimitFile, JSON.stringify(this.rateLimits, null, 2));
75
- } catch (err) {
76
- console.error('Error saving rate limits:', err.message);
77
- }
78
- }
79
-
80
- /**
81
- * Parse rate limit duration from error message
82
- * Examples:
83
- * "Please try again in 15m5.472s" -> 905472 ms
84
- * "Please try again in 13m21.792s" -> 801792 ms
85
- * "Please try again in 1h30m" -> 5400000 ms
86
- * "Session limit reached ∙ resets 12pm" -> ms until 12pm today/tomorrow
87
- */
88
- parseRateLimitDuration(errorMessage) {
89
- // Check for Claude Code session limit format: "Session limit reached ∙ resets 12pm"
90
- const sessionMatch = errorMessage.match(/resets?\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?/i);
91
- if (sessionMatch) {
92
- let hour = parseInt(sessionMatch[1]);
93
- const minute = sessionMatch[2] ? parseInt(sessionMatch[2]) : 0;
94
- const meridiem = sessionMatch[3] ? sessionMatch[3].toLowerCase() : null;
95
-
96
- // Convert to 24-hour format
97
- if (meridiem === 'pm' && hour !== 12) {
98
- hour += 12;
99
- } else if (meridiem === 'am' && hour === 12) {
100
- hour = 0;
101
- }
102
-
103
- // Calculate reset time
104
- const now = new Date();
105
- const resetTime = new Date(now);
106
- resetTime.setHours(hour, minute, 0, 0);
107
-
108
- // If reset time is in the past, it's tomorrow
109
- if (resetTime <= now) {
110
- resetTime.setDate(resetTime.getDate() + 1);
111
- }
112
-
113
- return resetTime.getTime() - now.getTime();
114
- }
115
-
116
- // Match patterns like "15m5.472s" or "1h30m" or "45s"
117
- const timeMatch = errorMessage.match(/try again in ([\d.]+h)?\s?([\d.]+m)?\s?([\d.]+s)?/i);
118
- if (!timeMatch) return null;
119
-
120
- let totalMs = 0;
121
-
122
- // Hours
123
- if (timeMatch[1]) {
124
- const hours = parseFloat(timeMatch[1].replace('h', ''));
125
- totalMs += hours * 60 * 60 * 1000;
126
- }
127
-
128
- // Minutes
129
- if (timeMatch[2]) {
130
- const minutes = parseFloat(timeMatch[2].replace('m', ''));
131
- totalMs += minutes * 60 * 1000;
132
- }
133
-
134
- // Seconds
135
- if (timeMatch[3]) {
136
- const seconds = parseFloat(timeMatch[3].replace('s', ''));
137
- totalMs += seconds * 1000;
138
- }
139
-
140
- return totalMs;
141
- }
142
-
143
- /**
144
- * Mark a provider as rate limited
145
- * @param {string} provider - Provider name (e.g., "groq", "anthropic")
146
- * @param {string} model - Model name (e.g., "llama-3.3-70b-versatile")
147
- * @param {string} errorMessage - Full error message containing duration
148
- */
149
- markRateLimited(provider, model, errorMessage) {
150
- let duration = this.parseRateLimitDuration(errorMessage);
151
- if (!duration) {
152
- console.warn(`Could not parse rate limit duration from: ${errorMessage}`);
153
- // Default to 15 minutes if we can't parse
154
- duration = 15 * 60 * 1000;
155
- }
156
-
157
- const resetTime = Date.now() + duration;
158
- const key = `${provider}:${model}`;
159
-
160
- this.rateLimits[key] = {
161
- provider,
162
- model,
163
- resetTime,
164
- resetDate: new Date(resetTime).toISOString(),
165
- markedAt: new Date().toISOString()
166
- };
167
-
168
- this.saveRateLimits();
169
-
170
- const chalk = require('chalk');
171
- const resetMinutes = Math.ceil(duration / 60000);
172
- console.log(chalk.yellow(`📊 Provider ${provider}/${model} rate limited until ${new Date(resetTime).toLocaleTimeString()} (~${resetMinutes}m)`));
173
- }
174
-
175
- /**
176
- * Check if a provider is currently rate limited
177
- * @param {string} provider - Provider name
178
- * @param {string} model - Model name
179
- * @returns {boolean}
180
- */
181
- isRateLimited(provider, model) {
182
- const key = `${provider}:${model}`;
183
- const limit = this.rateLimits[key];
184
-
185
- if (!limit) return false;
186
-
187
- // Check if rate limit has expired
188
- if (Date.now() >= limit.resetTime) {
189
- // Clean up expired rate limit
190
- delete this.rateLimits[key];
191
- this.saveRateLimits();
192
- return false;
193
- }
194
-
195
- return true;
196
- }
197
-
198
- /**
199
- * Get available provider (not rate limited)
200
- * @param {Array} providers - Array of {provider, model, apiKey} objects
201
- * @returns {Object|null} - Available provider object or null
202
- */
203
- getAvailableProvider(providers) {
204
- for (const providerConfig of providers) {
205
- if (!this.isRateLimited(providerConfig.provider, providerConfig.model)) {
206
- return providerConfig;
207
- }
208
- }
209
- return null;
210
- }
211
-
212
- /**
213
- * Get time remaining until provider is available again
214
- * @param {string} provider
215
- * @param {string} model
216
- * @returns {number|null} - Milliseconds until reset, or null if not rate limited
217
- */
218
- getTimeUntilReset(provider, model) {
219
- const key = `${provider}:${model}`;
220
- const limit = this.rateLimits[key];
221
-
222
- if (!limit) return null;
223
-
224
- const remaining = limit.resetTime - Date.now();
225
- return remaining > 0 ? remaining : null;
226
- }
227
-
228
- /**
229
- * Get status of all providers
230
- * @param {Array} providers - Array of provider configs
231
- * @returns {Array} - Array of provider status objects
232
- */
233
- getProviderStatus(providers) {
234
- return providers.map(p => {
235
- const isLimited = this.isRateLimited(p.provider, p.model);
236
- const timeUntilReset = this.getTimeUntilReset(p.provider, p.model);
237
-
238
- return {
239
- provider: p.provider,
240
- model: p.model,
241
- available: !isLimited,
242
- rateLimited: isLimited,
243
- resetIn: timeUntilReset ? Math.ceil(timeUntilReset / 60000) + 'm' : null
244
- };
245
- });
246
- }
247
-
248
- /**
249
- * Clear all rate limits (useful for testing)
250
- */
251
- clearAllRateLimits() {
252
- this.rateLimits = {};
253
- this.saveRateLimits();
254
- }
255
-
256
- /**
257
- * Load performance tracking data
258
- */
259
- loadPerformance() {
260
- try {
261
- if (fs.existsSync(this.performanceFile)) {
262
- return JSON.parse(fs.readFileSync(this.performanceFile, 'utf8'));
263
- }
264
- } catch (err) {
265
- // Ignore errors
266
- }
267
- return {};
268
- }
269
-
270
- /**
271
- * Save performance tracking data
272
- */
273
- savePerformance() {
274
- try {
275
- const dir = path.dirname(this.performanceFile);
276
- if (!fs.existsSync(dir)) {
277
- fs.mkdirSync(dir, { recursive: true });
278
- }
279
- fs.writeFileSync(this.performanceFile, JSON.stringify(this.performance, null, 2));
280
- } catch (err) {
281
- // Ignore errors
282
- }
283
- }
284
-
285
- /**
286
- * Record performance metric for a provider
287
- * @param {string} provider - Provider name
288
- * @param {string} model - Model name
289
- * @param {number} durationMs - Duration in milliseconds
290
- */
291
- recordPerformance(provider, model, durationMs) {
292
- const key = `${provider}:${model}`;
293
-
294
- if (!this.performance[key]) {
295
- this.performance[key] = {
296
- provider,
297
- model,
298
- samples: [],
299
- avgSpeed: 0
300
- };
301
- }
302
-
303
- const perf = this.performance[key];
304
- perf.samples.push(durationMs);
305
-
306
- // Keep only last 10 samples
307
- if (perf.samples.length > 10) {
308
- perf.samples.shift();
309
- }
310
-
311
- // Calculate average
312
- perf.avgSpeed = perf.samples.reduce((a, b) => a + b, 0) / perf.samples.length;
313
- perf.lastUsed = Date.now();
314
-
315
- this.savePerformance();
316
- }
317
-
318
- /**
319
- * Get fastest available provider from a list
320
- * @param {Array} providers - Array of {provider, model, ...} objects
321
- * @returns {Object|null} - Fastest available provider or null
322
- */
323
- getFastestAvailable(providers) {
324
- const available = providers.filter(p => !this.isRateLimited(p.provider, p.model));
325
-
326
- if (available.length === 0) return null;
327
- if (available.length === 1) return available[0];
328
-
329
- // Sort by average speed (fastest first)
330
- available.sort((a, b) => {
331
- const aKey = `${a.provider}:${a.model}`;
332
- const bKey = `${b.provider}:${b.model}`;
333
- const aAvg = this.performance[aKey]?.avgSpeed;
334
- const bAvg = this.performance[bKey]?.avgSpeed;
335
- const aSpeed = (aAvg ?? a.estimatedSpeed ?? Infinity);
336
- const bSpeed = (bAvg ?? b.estimatedSpeed ?? Infinity);
337
- return aSpeed - bSpeed;
338
- });
339
-
340
- return available[0];
341
- }
342
-
343
- /**
344
- * Get provider rankings by speed
345
- * @returns {Array} - Sorted array of {provider, model, avgSpeed}
346
- */
347
- getProviderRankings() {
348
- return Object.values(this.performance)
349
- .filter(p => p.avgSpeed > 0)
350
- .sort((a, b) => a.avgSpeed - b.avgSpeed);
351
- }
352
- }
353
-
354
- module.exports = ProviderManager;
1
+ /**
2
+ * Provider Manager - Handles LLM provider rotation and rate limit tracking
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+
9
+ class ProviderManager {
10
+ constructor() {
11
+ this.rateLimitFile = path.join(os.homedir(), '.config', 'allnightai', 'rate-limits.json');
12
+ this.performanceFile = path.join(os.homedir(), '.config', 'allnightai', 'provider-performance.json');
13
+ this.rateLimits = this.loadRateLimits();
14
+ this.performance = this.loadPerformance();
15
+ }
16
+
17
+ /**
18
+ * Load rate limit tracking data from file
19
+ * Auto-cleanup expired rate limits
20
+ */
21
+ loadRateLimits() {
22
+ try {
23
+ if (fs.existsSync(this.rateLimitFile)) {
24
+ const data = fs.readFileSync(this.rateLimitFile, 'utf8');
25
+ const limits = JSON.parse(data);
26
+
27
+ // Auto-cleanup expired rate limits
28
+ const now = Date.now();
29
+ let cleaned = false;
30
+ for (const key in limits) {
31
+ if (limits[key].resetTime && limits[key].resetTime <= now) {
32
+ delete limits[key];
33
+ cleaned = true;
34
+ }
35
+ }
36
+
37
+ // Save cleaned data if any expired limits were removed
38
+ if (cleaned) {
39
+ this.saveRateLimitsInternal(limits);
40
+ }
41
+
42
+ return limits;
43
+ }
44
+ } catch (err) {
45
+ console.error('Error loading rate limits:', err.message);
46
+ }
47
+ return {};
48
+ }
49
+
50
+ /**
51
+ * Internal save without reload (to prevent infinite loop during cleanup)
52
+ */
53
+ saveRateLimitsInternal(limits) {
54
+ try {
55
+ const dir = path.dirname(this.rateLimitFile);
56
+ if (!fs.existsSync(dir)) {
57
+ fs.mkdirSync(dir, { recursive: true });
58
+ }
59
+ fs.writeFileSync(this.rateLimitFile, JSON.stringify(limits, null, 2));
60
+ } catch (err) {
61
+ console.error('Error saving rate limits:', err.message);
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Save rate limit tracking data to file
67
+ */
68
+ saveRateLimits() {
69
+ try {
70
+ const dir = path.dirname(this.rateLimitFile);
71
+ if (!fs.existsSync(dir)) {
72
+ fs.mkdirSync(dir, { recursive: true });
73
+ }
74
+ fs.writeFileSync(this.rateLimitFile, JSON.stringify(this.rateLimits, null, 2));
75
+ } catch (err) {
76
+ console.error('Error saving rate limits:', err.message);
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Parse rate limit duration from error message
82
+ * Examples:
83
+ * "Please try again in 15m5.472s" -> 905472 ms
84
+ * "Please try again in 13m21.792s" -> 801792 ms
85
+ * "Please try again in 1h30m" -> 5400000 ms
86
+ * "Session limit reached ∙ resets 12pm" -> ms until 12pm today/tomorrow
87
+ */
88
+ parseRateLimitDuration(errorMessage) {
89
+ // Check for Claude Code session limit format: "Session limit reached ∙ resets 12pm"
90
+ const sessionMatch = errorMessage.match(/resets?\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?/i);
91
+ if (sessionMatch) {
92
+ let hour = parseInt(sessionMatch[1]);
93
+ const minute = sessionMatch[2] ? parseInt(sessionMatch[2]) : 0;
94
+ const meridiem = sessionMatch[3] ? sessionMatch[3].toLowerCase() : null;
95
+
96
+ // Convert to 24-hour format
97
+ if (meridiem === 'pm' && hour !== 12) {
98
+ hour += 12;
99
+ } else if (meridiem === 'am' && hour === 12) {
100
+ hour = 0;
101
+ }
102
+
103
+ // Calculate reset time
104
+ const now = new Date();
105
+ const resetTime = new Date(now);
106
+ resetTime.setHours(hour, minute, 0, 0);
107
+
108
+ // If reset time is in the past, it's tomorrow
109
+ if (resetTime <= now) {
110
+ resetTime.setDate(resetTime.getDate() + 1);
111
+ }
112
+
113
+ return resetTime.getTime() - now.getTime();
114
+ }
115
+
116
+ // Match patterns like "15m5.472s" or "1h30m" or "45s"
117
+ const timeMatch = errorMessage.match(/try again in ([\d.]+h)?\s?([\d.]+m)?\s?([\d.]+s)?/i);
118
+ if (!timeMatch) return null;
119
+
120
+ let totalMs = 0;
121
+
122
+ // Hours
123
+ if (timeMatch[1]) {
124
+ const hours = parseFloat(timeMatch[1].replace('h', ''));
125
+ totalMs += hours * 60 * 60 * 1000;
126
+ }
127
+
128
+ // Minutes
129
+ if (timeMatch[2]) {
130
+ const minutes = parseFloat(timeMatch[2].replace('m', ''));
131
+ totalMs += minutes * 60 * 1000;
132
+ }
133
+
134
+ // Seconds
135
+ if (timeMatch[3]) {
136
+ const seconds = parseFloat(timeMatch[3].replace('s', ''));
137
+ totalMs += seconds * 1000;
138
+ }
139
+
140
+ return totalMs;
141
+ }
142
+
143
+ /**
144
+ * Mark a provider as rate limited
145
+ * @param {string} provider - Provider name (e.g., "groq", "anthropic")
146
+ * @param {string} model - Model name (e.g., "llama-3.3-70b-versatile")
147
+ * @param {string} errorMessage - Full error message containing duration
148
+ */
149
+ markRateLimited(provider, model, errorMessage) {
150
+ let duration = this.parseRateLimitDuration(errorMessage);
151
+ if (!duration) {
152
+ console.warn(`Could not parse rate limit duration from: ${errorMessage}`);
153
+ // Default to 15 minutes if we can't parse
154
+ duration = 15 * 60 * 1000;
155
+ }
156
+
157
+ const resetTime = Date.now() + duration;
158
+ const key = `${provider}:${model}`;
159
+
160
+ this.rateLimits[key] = {
161
+ provider,
162
+ model,
163
+ resetTime,
164
+ resetDate: new Date(resetTime).toISOString(),
165
+ markedAt: new Date().toISOString()
166
+ };
167
+
168
+ this.saveRateLimits();
169
+
170
+ const chalk = require('chalk');
171
+ const resetMinutes = Math.ceil(duration / 60000);
172
+ console.log(chalk.yellow(`📊 Provider ${provider}/${model} rate limited until ${new Date(resetTime).toLocaleTimeString()} (~${resetMinutes}m)`));
173
+ }
174
+
175
+ /**
176
+ * Check if a provider is currently rate limited
177
+ * @param {string} provider - Provider name
178
+ * @param {string} model - Model name
179
+ * @returns {boolean}
180
+ */
181
+ isRateLimited(provider, model) {
182
+ const key = `${provider}:${model}`;
183
+ const limit = this.rateLimits[key];
184
+
185
+ if (!limit) return false;
186
+
187
+ // Check if rate limit has expired
188
+ if (Date.now() >= limit.resetTime) {
189
+ // Clean up expired rate limit
190
+ delete this.rateLimits[key];
191
+ this.saveRateLimits();
192
+ return false;
193
+ }
194
+
195
+ return true;
196
+ }
197
+
198
+ /**
199
+ * Get available provider (not rate limited)
200
+ * @param {Array} providers - Array of {provider, model, apiKey} objects
201
+ * @returns {Object|null} - Available provider object or null
202
+ */
203
+ getAvailableProvider(providers) {
204
+ for (const providerConfig of providers) {
205
+ if (!this.isRateLimited(providerConfig.provider, providerConfig.model)) {
206
+ return providerConfig;
207
+ }
208
+ }
209
+ return null;
210
+ }
211
+
212
+ /**
213
+ * Get time remaining until provider is available again
214
+ * @param {string} provider
215
+ * @param {string} model
216
+ * @returns {number|null} - Milliseconds until reset, or null if not rate limited
217
+ */
218
+ getTimeUntilReset(provider, model) {
219
+ const key = `${provider}:${model}`;
220
+ const limit = this.rateLimits[key];
221
+
222
+ if (!limit) return null;
223
+
224
+ const remaining = limit.resetTime - Date.now();
225
+ return remaining > 0 ? remaining : null;
226
+ }
227
+
228
+ /**
229
+ * Get status of all providers
230
+ * @param {Array} providers - Array of provider configs
231
+ * @returns {Array} - Array of provider status objects
232
+ */
233
+ getProviderStatus(providers) {
234
+ return providers.map(p => {
235
+ const isLimited = this.isRateLimited(p.provider, p.model);
236
+ const timeUntilReset = this.getTimeUntilReset(p.provider, p.model);
237
+
238
+ return {
239
+ provider: p.provider,
240
+ model: p.model,
241
+ available: !isLimited,
242
+ rateLimited: isLimited,
243
+ resetIn: timeUntilReset ? Math.ceil(timeUntilReset / 60000) + 'm' : null
244
+ };
245
+ });
246
+ }
247
+
248
+ /**
249
+ * Clear all rate limits (useful for testing)
250
+ */
251
+ clearAllRateLimits() {
252
+ this.rateLimits = {};
253
+ this.saveRateLimits();
254
+ }
255
+
256
+ /**
257
+ * Load performance tracking data
258
+ */
259
+ loadPerformance() {
260
+ try {
261
+ if (fs.existsSync(this.performanceFile)) {
262
+ return JSON.parse(fs.readFileSync(this.performanceFile, 'utf8'));
263
+ }
264
+ } catch (err) {
265
+ // Ignore errors
266
+ }
267
+ return {};
268
+ }
269
+
270
+ /**
271
+ * Save performance tracking data
272
+ */
273
+ savePerformance() {
274
+ try {
275
+ const dir = path.dirname(this.performanceFile);
276
+ if (!fs.existsSync(dir)) {
277
+ fs.mkdirSync(dir, { recursive: true });
278
+ }
279
+ fs.writeFileSync(this.performanceFile, JSON.stringify(this.performance, null, 2));
280
+ } catch (err) {
281
+ // Ignore errors
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Record performance metric for a provider
287
+ * @param {string} provider - Provider name
288
+ * @param {string} model - Model name
289
+ * @param {number} durationMs - Duration in milliseconds
290
+ */
291
+ recordPerformance(provider, model, durationMs) {
292
+ const key = `${provider}:${model}`;
293
+
294
+ if (!this.performance[key]) {
295
+ this.performance[key] = {
296
+ provider,
297
+ model,
298
+ samples: [],
299
+ avgSpeed: 0
300
+ };
301
+ }
302
+
303
+ const perf = this.performance[key];
304
+ perf.samples.push(durationMs);
305
+
306
+ // Keep only last 10 samples
307
+ if (perf.samples.length > 10) {
308
+ perf.samples.shift();
309
+ }
310
+
311
+ // Calculate average
312
+ perf.avgSpeed = perf.samples.reduce((a, b) => a + b, 0) / perf.samples.length;
313
+ perf.lastUsed = Date.now();
314
+
315
+ this.savePerformance();
316
+ }
317
+
318
+ /**
319
+ * Get fastest available provider from a list
320
+ * @param {Array} providers - Array of {provider, model, ...} objects
321
+ * @returns {Object|null} - Fastest available provider or null
322
+ */
323
+ getFastestAvailable(providers) {
324
+ const available = providers.filter(p => !this.isRateLimited(p.provider, p.model));
325
+
326
+ if (available.length === 0) return null;
327
+ if (available.length === 1) return available[0];
328
+
329
+ // Sort by average speed (fastest first)
330
+ available.sort((a, b) => {
331
+ const aKey = `${a.provider}:${a.model}`;
332
+ const bKey = `${b.provider}:${b.model}`;
333
+ const aAvg = this.performance[aKey]?.avgSpeed;
334
+ const bAvg = this.performance[bKey]?.avgSpeed;
335
+ const aSpeed = (aAvg ?? a.estimatedSpeed ?? Infinity);
336
+ const bSpeed = (bAvg ?? b.estimatedSpeed ?? Infinity);
337
+ return aSpeed - bSpeed;
338
+ });
339
+
340
+ return available[0];
341
+ }
342
+
343
+ /**
344
+ * Get provider rankings by speed
345
+ * @returns {Array} - Sorted array of {provider, model, avgSpeed}
346
+ */
347
+ getProviderRankings() {
348
+ return Object.values(this.performance)
349
+ .filter(p => p.avgSpeed > 0)
350
+ .sort((a, b) => a.avgSpeed - b.avgSpeed);
351
+ }
352
+ }
353
+
354
+ module.exports = ProviderManager;