vibecodingmachine-core 1.0.1 → 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.
- package/.babelrc +13 -13
- package/README.md +28 -28
- package/__tests__/applescript-manager-claude-fix.test.js +286 -286
- package/__tests__/requirement-2-auto-start-looping.test.js +69 -69
- package/__tests__/requirement-3-auto-start-looping.test.js +69 -69
- package/__tests__/requirement-4-auto-start-looping.test.js +69 -69
- package/__tests__/requirement-6-auto-start-looping.test.js +73 -73
- package/__tests__/requirement-7-status-tracking.test.js +332 -332
- package/jest.config.js +18 -18
- package/jest.setup.js +12 -12
- package/package.json +48 -48
- package/src/auth/access-denied.html +119 -119
- package/src/auth/shared-auth-storage.js +230 -230
- package/src/autonomous-mode/feature-implementer.cjs +70 -70
- package/src/autonomous-mode/feature-implementer.js +425 -425
- package/src/chat-management/chat-manager.cjs +71 -71
- package/src/chat-management/chat-manager.js +342 -342
- package/src/ide-integration/__tests__/applescript-manager-thread-closure.test.js +227 -227
- package/src/ide-integration/aider-cli-manager.cjs +850 -850
- package/src/ide-integration/applescript-manager.cjs +1088 -1088
- package/src/ide-integration/applescript-manager.js +2802 -2802
- package/src/ide-integration/applescript-utils.js +306 -306
- package/src/ide-integration/cdp-manager.cjs +221 -221
- package/src/ide-integration/cdp-manager.js +321 -321
- package/src/ide-integration/claude-code-cli-manager.cjs +301 -301
- package/src/ide-integration/cline-cli-manager.cjs +2252 -2252
- package/src/ide-integration/continue-cli-manager.js +431 -431
- package/src/ide-integration/provider-manager.cjs +354 -354
- package/src/ide-integration/quota-detector.cjs +34 -34
- package/src/ide-integration/quota-detector.js +349 -349
- package/src/ide-integration/windows-automation-manager.js +262 -262
- package/src/index.cjs +47 -43
- package/src/index.js +17 -17
- package/src/llm/direct-llm-manager.cjs +609 -609
- package/src/ui/ButtonComponents.js +247 -247
- package/src/ui/ChatInterface.js +499 -499
- package/src/ui/StateManager.js +259 -259
- package/src/utils/audit-logger.cjs +116 -116
- package/src/utils/config-helpers.cjs +94 -94
- package/src/utils/config-helpers.js +94 -94
- package/src/utils/electron-update-checker.js +113 -85
- package/src/utils/gcloud-auth.cjs +394 -394
- package/src/utils/logger.cjs +193 -193
- package/src/utils/logger.js +191 -191
- package/src/utils/repo-helpers.cjs +120 -120
- package/src/utils/repo-helpers.js +120 -120
- package/src/utils/requirement-helpers.js +432 -432
- package/src/utils/update-checker.js +227 -167
- 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;
|