vibecodingmachine-core 2026.3.9-907 → 2026.3.10-1548
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/package.json +1 -1
- package/src/auth/access-denied.html +119 -119
- package/src/auth/shared-auth-storage.js +267 -267
- package/src/autonomous-mode/feature-implementer.cjs +70 -70
- package/src/autonomous-mode/feature-implementer.js +425 -425
- package/src/beta-request.js +160 -160
- package/src/chat-management/chat-manager.cjs +71 -71
- package/src/chat-management/chat-manager.js +342 -342
- package/src/compliance/compliance-prompt.js +183 -183
- package/src/ide-integration/aider-cli-manager.cjs +850 -850
- package/src/ide-integration/applescript-manager.cjs +3215 -3215
- package/src/ide-integration/applescript-utils.js +314 -314
- package/src/ide-integration/cdp-manager.cjs +221 -221
- package/src/ide-integration/claude-code-cli-manager.cjs +456 -456
- 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 +595 -595
- package/src/ide-integration/quota-detector.cjs +399 -399
- package/src/ide-integration/windows-automation-manager.js +532 -4
- package/src/ide-integration/windows-ide-manager.js +12 -3
- package/src/index.cjs +142 -142
- package/src/llm/direct-llm-manager.cjs +1299 -1299
- package/src/localization/index.js +147 -147
- package/src/quota-management/index.js +108 -108
- package/src/requirement-numbering.js +164 -164
- package/src/sync/aws-setup.js +445 -445
- 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/env-helpers.js +54 -54
- package/src/utils/error-reporter.js +117 -117
- package/src/utils/gcloud-auth.cjs +394 -394
- package/src/utils/git-branch-manager.js +278 -278
- 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/update-checker.js +246 -246
- package/src/utils/version-checker.js +170 -170
|
@@ -1,595 +1,595 @@
|
|
|
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
|
-
clearRateLimit(provider, model) {
|
|
18
|
-
if (!provider) return false;
|
|
19
|
-
const key = `${provider}:${model || provider}`;
|
|
20
|
-
if (this.rateLimits[key]) {
|
|
21
|
-
delete this.rateLimits[key];
|
|
22
|
-
this.saveRateLimits();
|
|
23
|
-
return true;
|
|
24
|
-
}
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
clearProviderRateLimits(provider) {
|
|
29
|
-
if (!provider) return 0;
|
|
30
|
-
const providerPrefix = `${provider}:`;
|
|
31
|
-
let cleared = 0;
|
|
32
|
-
for (const key of Object.keys(this.rateLimits)) {
|
|
33
|
-
if (key.startsWith(providerPrefix)) {
|
|
34
|
-
delete this.rateLimits[key];
|
|
35
|
-
cleared += 1;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
if (cleared > 0) {
|
|
39
|
-
this.saveRateLimits();
|
|
40
|
-
}
|
|
41
|
-
return cleared;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Load rate limit tracking data from file
|
|
46
|
-
* Auto-cleanup expired rate limits
|
|
47
|
-
*/
|
|
48
|
-
loadRateLimits() {
|
|
49
|
-
try {
|
|
50
|
-
if (fs.existsSync(this.rateLimitFile)) {
|
|
51
|
-
const data = fs.readFileSync(this.rateLimitFile, 'utf8');
|
|
52
|
-
const limits = JSON.parse(data);
|
|
53
|
-
|
|
54
|
-
// Auto-cleanup expired rate limits
|
|
55
|
-
const now = Date.now();
|
|
56
|
-
let cleaned = false;
|
|
57
|
-
for (const key in limits) {
|
|
58
|
-
if (limits[key].resetTime && limits[key].resetTime <= now) {
|
|
59
|
-
delete limits[key];
|
|
60
|
-
cleaned = true;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Save cleaned data if any expired limits were removed
|
|
65
|
-
if (cleaned) {
|
|
66
|
-
this.saveRateLimitsInternal(limits);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return limits;
|
|
70
|
-
}
|
|
71
|
-
} catch (err) {
|
|
72
|
-
console.error('Error loading rate limits:', err.message);
|
|
73
|
-
}
|
|
74
|
-
return {};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Internal save without reload (to prevent infinite loop during cleanup)
|
|
79
|
-
*/
|
|
80
|
-
saveRateLimitsInternal(limits) {
|
|
81
|
-
try {
|
|
82
|
-
const dir = path.dirname(this.rateLimitFile);
|
|
83
|
-
if (!fs.existsSync(dir)) {
|
|
84
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
85
|
-
}
|
|
86
|
-
fs.writeFileSync(this.rateLimitFile, JSON.stringify(limits, null, 2));
|
|
87
|
-
} catch (err) {
|
|
88
|
-
console.error('Error saving rate limits:', err.message);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Save rate limit tracking data to file
|
|
94
|
-
*/
|
|
95
|
-
saveRateLimits() {
|
|
96
|
-
try {
|
|
97
|
-
const dir = path.dirname(this.rateLimitFile);
|
|
98
|
-
if (!fs.existsSync(dir)) {
|
|
99
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
100
|
-
}
|
|
101
|
-
fs.writeFileSync(this.rateLimitFile, JSON.stringify(this.rateLimits, null, 2));
|
|
102
|
-
} catch (err) {
|
|
103
|
-
console.error('Error saving rate limits:', err.message);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Parse rate limit duration from error message
|
|
109
|
-
* Examples:
|
|
110
|
-
* "Please try again in 15m5.472s" -> 905472 ms
|
|
111
|
-
* "Please try again in 13m21.792s" -> 801792 ms
|
|
112
|
-
* "Please try again in 1h30m" -> 5400000 ms
|
|
113
|
-
* "Session limit reached ∙ resets 12pm" -> ms until 12pm today/tomorrow
|
|
114
|
-
*/
|
|
115
|
-
parseRateLimitDuration(errorMessage) {
|
|
116
|
-
// Gemini / Google AI style: "You can resume using this model at 1/12/2026, 4:07:27 PM"
|
|
117
|
-
// Also seen as: "resume using this model at <date>, <time>"
|
|
118
|
-
const resumeAtMatch = errorMessage.match(/resume\s+(?:using\s+this\s+model\s+)?at\s+(\d{1,2}\/\d{1,2}\/\d{4})\s*,?\s*(\d{1,2}:\d{2}(?::\d{2})?)\s*(am|pm)/i);
|
|
119
|
-
if (resumeAtMatch) {
|
|
120
|
-
try {
|
|
121
|
-
const datePart = resumeAtMatch[1];
|
|
122
|
-
const timePart = resumeAtMatch[2];
|
|
123
|
-
const meridiem = resumeAtMatch[3];
|
|
124
|
-
const parsed = new Date(`${datePart}, ${timePart} ${meridiem.toUpperCase()}`);
|
|
125
|
-
if (!Number.isNaN(parsed.getTime())) {
|
|
126
|
-
const ms = parsed.getTime() - Date.now();
|
|
127
|
-
return ms > 0 ? ms : 0;
|
|
128
|
-
}
|
|
129
|
-
} catch (_) { }
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Check for "resets [Month] [Day] at [Time]" format (e.g. "Spending cap reached resets Jan 17 at 12pm")
|
|
133
|
-
const dateMatch = errorMessage.match(/resets\s+([a-zA-Z]+)\s+(\d{1,2})\s+at\s+(\d{1,2}(?::\d{2})?)\s*(am|pm)?/i);
|
|
134
|
-
if (dateMatch) {
|
|
135
|
-
try {
|
|
136
|
-
const monthStr = dateMatch[1]; // Jan, February, etc.
|
|
137
|
-
const day = parseInt(dateMatch[2]);
|
|
138
|
-
const timeStr = dateMatch[3];
|
|
139
|
-
const meridiem = dateMatch[4] ? dateMatch[4].toLowerCase() : null;
|
|
140
|
-
|
|
141
|
-
// Parse time
|
|
142
|
-
let [hours, minutes] = timeStr.split(':').map(n => parseInt(n));
|
|
143
|
-
if (!minutes || Number.isNaN(minutes)) minutes = 0;
|
|
144
|
-
|
|
145
|
-
if (meridiem === 'pm' && hours !== 12) hours += 12;
|
|
146
|
-
if (meridiem === 'am' && hours === 12) hours = 0;
|
|
147
|
-
|
|
148
|
-
// Parse month
|
|
149
|
-
const months = {
|
|
150
|
-
jan: 0, january: 0,
|
|
151
|
-
feb: 1, february: 1,
|
|
152
|
-
mar: 2, march: 2,
|
|
153
|
-
apr: 3, april: 3,
|
|
154
|
-
may: 4,
|
|
155
|
-
jun: 5, june: 5,
|
|
156
|
-
jul: 6, july: 6,
|
|
157
|
-
aug: 7, august: 7,
|
|
158
|
-
sep: 8, sept: 8, september: 8,
|
|
159
|
-
oct: 9, october: 9,
|
|
160
|
-
nov: 10, november: 10,
|
|
161
|
-
dec: 11, december: 11
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
const monthIndex = months[monthStr.toLowerCase().substring(0, 3)] ?? months[monthStr.toLowerCase()];
|
|
165
|
-
|
|
166
|
-
if (monthIndex !== undefined) {
|
|
167
|
-
const currentYear = new Date().getFullYear();
|
|
168
|
-
let parsed = new Date(currentYear, monthIndex, day, hours, minutes);
|
|
169
|
-
const now = Date.now();
|
|
170
|
-
|
|
171
|
-
// Logic to handle year crossing (reset in Jan, currently Dec)
|
|
172
|
-
if (parsed.getTime() < now) {
|
|
173
|
-
if (!Number.isNaN(parsed.getTime())) {
|
|
174
|
-
if (parsed.getTime() < now) {
|
|
175
|
-
const nextYearDate = new Date(parsed);
|
|
176
|
-
nextYearDate.setFullYear(nextYearDate.getFullYear() + 1);
|
|
177
|
-
if (nextYearDate.getTime() > now) {
|
|
178
|
-
parsed = nextYearDate;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const ms = parsed.getTime() - now;
|
|
185
|
-
return ms > 0 ? ms : 0;
|
|
186
|
-
}
|
|
187
|
-
} catch (_) { }
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Check for Claude Code session limit format: "Session limit reached ∙ resets 12pm"
|
|
191
|
-
const sessionMatch = errorMessage.match(/resets?\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?/i);
|
|
192
|
-
if (sessionMatch) {
|
|
193
|
-
let hour = parseInt(sessionMatch[1]);
|
|
194
|
-
const minute = sessionMatch[2] ? parseInt(sessionMatch[2]) : 0;
|
|
195
|
-
const meridiem = sessionMatch[3] ? sessionMatch[3].toLowerCase() : null;
|
|
196
|
-
|
|
197
|
-
// Convert to 24-hour format
|
|
198
|
-
if (meridiem === 'pm' && hour !== 12) {
|
|
199
|
-
hour += 12;
|
|
200
|
-
} else if (meridiem === 'am' && hour === 12) {
|
|
201
|
-
hour = 0;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Calculate reset time
|
|
205
|
-
const now = new Date();
|
|
206
|
-
const resetTime = new Date(now);
|
|
207
|
-
resetTime.setHours(hour, minute, 0, 0);
|
|
208
|
-
|
|
209
|
-
// If reset time is in the past, it's tomorrow
|
|
210
|
-
if (resetTime <= now) {
|
|
211
|
-
resetTime.setDate(resetTime.getDate() + 1);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return resetTime.getTime() - now.getTime();
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Match patterns like "15m5.472s" or "1h30m" or "45s"
|
|
218
|
-
const timeMatch = errorMessage.match(/try again in ([\d.]+h)?\s?([\d.]+m)?\s?([\d.]+s)?/i);
|
|
219
|
-
if (!timeMatch) return null;
|
|
220
|
-
|
|
221
|
-
let totalMs = 0;
|
|
222
|
-
|
|
223
|
-
// Hours
|
|
224
|
-
if (timeMatch[1]) {
|
|
225
|
-
const hours = parseFloat(timeMatch[1].replace('h', ''));
|
|
226
|
-
totalMs += hours * 60 * 60 * 1000;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Minutes
|
|
230
|
-
if (timeMatch[2]) {
|
|
231
|
-
const minutes = parseFloat(timeMatch[2].replace('m', ''));
|
|
232
|
-
totalMs += minutes * 60 * 1000;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Seconds
|
|
236
|
-
if (timeMatch[3]) {
|
|
237
|
-
const seconds = parseFloat(timeMatch[3].replace('s', ''));
|
|
238
|
-
totalMs += seconds * 1000;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
return totalMs;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Mark a provider as rate limited
|
|
246
|
-
* @param {string} provider - Provider name (e.g., "groq", "anthropic")
|
|
247
|
-
* @param {string} model - Model name (e.g., "llama-3.3-70b-versatile")
|
|
248
|
-
* @param {string} errorMessage - Full error message containing duration
|
|
249
|
-
*/
|
|
250
|
-
markRateLimited(provider, model, errorMessage) {
|
|
251
|
-
// Normalize model: if model is missing/undefined, assume it's the provider name.
|
|
252
|
-
model = model || provider;
|
|
253
|
-
|
|
254
|
-
let duration = this.parseRateLimitDuration(errorMessage);
|
|
255
|
-
if (duration === null || duration === undefined) {
|
|
256
|
-
console.warn(`Could not parse rate limit duration from: ${errorMessage}`);
|
|
257
|
-
// Don't guess the duration - just mark as rate limited without a specific reset time
|
|
258
|
-
duration = null;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const resetTime = duration ? Date.now() + duration : null;
|
|
262
|
-
const key = `${provider}:${model}`;
|
|
263
|
-
|
|
264
|
-
this.rateLimits[key] = {
|
|
265
|
-
provider,
|
|
266
|
-
model,
|
|
267
|
-
resetTime,
|
|
268
|
-
resetDate: resetTime ? new Date(resetTime).toISOString() : null,
|
|
269
|
-
reason: errorMessage,
|
|
270
|
-
markedAt: new Date().toISOString()
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
this.saveRateLimits();
|
|
274
|
-
|
|
275
|
-
if (resetTime) {
|
|
276
|
-
const chalk = require('chalk');
|
|
277
|
-
const resetMinutes = Math.ceil(duration / 60000);
|
|
278
|
-
const resetDateTime = new Date(resetTime);
|
|
279
|
-
const timeString = resetDateTime.toLocaleTimeString('en-US', {
|
|
280
|
-
timeZone: 'America/Denver',
|
|
281
|
-
hour: 'numeric',
|
|
282
|
-
minute: '2-digit',
|
|
283
|
-
hour12: true
|
|
284
|
-
});
|
|
285
|
-
const dateString = resetDateTime.toLocaleDateString('en-US', {
|
|
286
|
-
timeZone: 'America/Denver',
|
|
287
|
-
weekday: 'short',
|
|
288
|
-
month: 'short',
|
|
289
|
-
day: 'numeric'
|
|
290
|
-
});
|
|
291
|
-
console.log(chalk.yellow(`📊 Provider ${provider}/${model} rate limited until ${timeString} MST on ${dateString} (~${resetMinutes}m)`));
|
|
292
|
-
} else {
|
|
293
|
-
const chalk = require('chalk');
|
|
294
|
-
console.log(chalk.yellow(`📊 Provider ${provider}/${model} rate limited (reset time unknown)`));
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Check if a provider is currently rate limited
|
|
300
|
-
* @param {string} provider - Provider name
|
|
301
|
-
* @param {string} model - Model name (optional, will check all models if not provided)
|
|
302
|
-
* @returns {boolean}
|
|
303
|
-
*/
|
|
304
|
-
isRateLimited(provider, model) {
|
|
305
|
-
if (!model) {
|
|
306
|
-
// Check if ANY model for this provider is rate limited
|
|
307
|
-
const providerPrefix = `${provider}:`;
|
|
308
|
-
for (const key in this.rateLimits) {
|
|
309
|
-
if (key.startsWith(providerPrefix)) {
|
|
310
|
-
const limit = this.rateLimits[key];
|
|
311
|
-
if (limit && Date.now() < limit.resetTime) {
|
|
312
|
-
return true;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
return false;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const key = `${provider}:${model}`;
|
|
320
|
-
const limit = this.rateLimits[key];
|
|
321
|
-
|
|
322
|
-
if (limit) {
|
|
323
|
-
// Check if rate limit has expired
|
|
324
|
-
if (Date.now() >= limit.resetTime) {
|
|
325
|
-
// Clean up expired rate limit
|
|
326
|
-
delete this.rateLimits[key];
|
|
327
|
-
this.saveRateLimits();
|
|
328
|
-
return false;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
return true;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Fall back to any provider-level entries (handles older or model-agnostic keys)
|
|
335
|
-
const providerPrefix = `${provider}:`;
|
|
336
|
-
for (const k in this.rateLimits) {
|
|
337
|
-
if (k.startsWith(providerPrefix)) {
|
|
338
|
-
const l = this.rateLimits[k];
|
|
339
|
-
if (l && Date.now() < l.resetTime) return true;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
return false;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* Get rate limit information for a provider
|
|
348
|
-
* @param {string} provider - Provider name
|
|
349
|
-
* @param {string} model - Model name (optional)
|
|
350
|
-
* @returns {Object} - {isRateLimited: boolean, resetTime: number, reason: string}
|
|
351
|
-
*/
|
|
352
|
-
getRateLimitInfo(provider, model) {
|
|
353
|
-
// If model not specified, check all models for this provider
|
|
354
|
-
if (!model) {
|
|
355
|
-
const providerPrefix = `${provider}:`;
|
|
356
|
-
let earliestReset = null;
|
|
357
|
-
let reason = null;
|
|
358
|
-
|
|
359
|
-
for (const key in this.rateLimits) {
|
|
360
|
-
if (key.startsWith(providerPrefix)) {
|
|
361
|
-
const limit = this.rateLimits[key];
|
|
362
|
-
if (limit && Date.now() < limit.resetTime) {
|
|
363
|
-
if (!earliestReset || limit.resetTime < earliestReset) {
|
|
364
|
-
earliestReset = limit.resetTime;
|
|
365
|
-
reason = limit.reason;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
if (earliestReset) {
|
|
372
|
-
return {
|
|
373
|
-
isRateLimited: true,
|
|
374
|
-
resetTime: earliestReset,
|
|
375
|
-
reason: reason
|
|
376
|
-
};
|
|
377
|
-
}
|
|
378
|
-
} else {
|
|
379
|
-
const key = `${provider}:${model}`;
|
|
380
|
-
const limit = this.rateLimits[key];
|
|
381
|
-
|
|
382
|
-
if (limit && Date.now() < limit.resetTime) {
|
|
383
|
-
return {
|
|
384
|
-
isRateLimited: true,
|
|
385
|
-
resetTime: limit.resetTime,
|
|
386
|
-
reason: limit.reason
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Fallback: check other keys for this provider and return earliest reset
|
|
391
|
-
const providerPrefix = `${provider}:`;
|
|
392
|
-
let earliestReset = null;
|
|
393
|
-
let reason = null;
|
|
394
|
-
|
|
395
|
-
for (const k in this.rateLimits) {
|
|
396
|
-
if (k.startsWith(providerPrefix)) {
|
|
397
|
-
const l = this.rateLimits[k];
|
|
398
|
-
if (l && Date.now() < l.resetTime) {
|
|
399
|
-
if (!earliestReset || l.resetTime < earliestReset) {
|
|
400
|
-
earliestReset = l.resetTime;
|
|
401
|
-
reason = l.reason;
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
if (earliestReset) {
|
|
408
|
-
return {
|
|
409
|
-
isRateLimited: true,
|
|
410
|
-
resetTime: earliestReset,
|
|
411
|
-
reason
|
|
412
|
-
};
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
return {
|
|
417
|
-
isRateLimited: false,
|
|
418
|
-
resetTime: null,
|
|
419
|
-
reason: null
|
|
420
|
-
};
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/**
|
|
424
|
-
* Get available provider (not rate limited)
|
|
425
|
-
* @param {Array} providers - Array of {provider, model, apiKey} objects
|
|
426
|
-
* @returns {Object|null} - Available provider object or null
|
|
427
|
-
*/
|
|
428
|
-
getAvailableProvider(providers) {
|
|
429
|
-
for (const providerConfig of providers) {
|
|
430
|
-
if (!this.isRateLimited(providerConfig.provider, providerConfig.model)) {
|
|
431
|
-
return providerConfig;
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
return null;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
* Get time remaining until provider is available again
|
|
439
|
-
* @param {string} provider
|
|
440
|
-
* @param {string} model
|
|
441
|
-
* @returns {number|null} - Milliseconds until reset, or null if not rate limited
|
|
442
|
-
*/
|
|
443
|
-
getTimeUntilReset(provider, model) {
|
|
444
|
-
const key = `${provider}:${model}`;
|
|
445
|
-
const limit = this.rateLimits[key];
|
|
446
|
-
|
|
447
|
-
if (limit && Date.now() < limit.resetTime) {
|
|
448
|
-
const remaining = limit.resetTime - Date.now();
|
|
449
|
-
return remaining > 0 ? remaining : null;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
// Fallback: check other keys for this provider (handles legacy or model-agnostic entries)
|
|
453
|
-
const providerPrefix = `${provider}:`;
|
|
454
|
-
let earliest = null;
|
|
455
|
-
for (const k in this.rateLimits) {
|
|
456
|
-
if (k.startsWith(providerPrefix)) {
|
|
457
|
-
const l = this.rateLimits[k];
|
|
458
|
-
if (l && Date.now() < l.resetTime) {
|
|
459
|
-
if (!earliest || l.resetTime < earliest) earliest = l.resetTime;
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
if (!earliest) return null;
|
|
465
|
-
const remaining = earliest - Date.now();
|
|
466
|
-
return remaining > 0 ? remaining : null;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
/**
|
|
470
|
-
* Get status of all providers
|
|
471
|
-
* @param {Array} providers - Array of provider configs
|
|
472
|
-
* @returns {Array} - Array of provider status objects
|
|
473
|
-
*/
|
|
474
|
-
getProviderStatus(providers) {
|
|
475
|
-
return providers.map(p => {
|
|
476
|
-
const isLimited = this.isRateLimited(p.provider, p.model);
|
|
477
|
-
const timeUntilReset = this.getTimeUntilReset(p.provider, p.model);
|
|
478
|
-
|
|
479
|
-
return {
|
|
480
|
-
provider: p.provider,
|
|
481
|
-
model: p.model,
|
|
482
|
-
available: !isLimited,
|
|
483
|
-
rateLimited: isLimited,
|
|
484
|
-
resetIn: timeUntilReset ? Math.ceil(timeUntilReset / 60000) + 'm' : null
|
|
485
|
-
};
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
/**
|
|
490
|
-
* Clear all rate limits (useful for testing)
|
|
491
|
-
*/
|
|
492
|
-
clearAllRateLimits() {
|
|
493
|
-
this.rateLimits = {};
|
|
494
|
-
this.saveRateLimits();
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
/**
|
|
498
|
-
* Load performance tracking data
|
|
499
|
-
*/
|
|
500
|
-
loadPerformance() {
|
|
501
|
-
try {
|
|
502
|
-
if (fs.existsSync(this.performanceFile)) {
|
|
503
|
-
return JSON.parse(fs.readFileSync(this.performanceFile, 'utf8'));
|
|
504
|
-
}
|
|
505
|
-
} catch (err) {
|
|
506
|
-
// Ignore errors
|
|
507
|
-
}
|
|
508
|
-
return {};
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
/**
|
|
512
|
-
* Save performance tracking data
|
|
513
|
-
*/
|
|
514
|
-
savePerformance() {
|
|
515
|
-
try {
|
|
516
|
-
const dir = path.dirname(this.performanceFile);
|
|
517
|
-
if (!fs.existsSync(dir)) {
|
|
518
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
519
|
-
}
|
|
520
|
-
fs.writeFileSync(this.performanceFile, JSON.stringify(this.performance, null, 2));
|
|
521
|
-
} catch (err) {
|
|
522
|
-
// Ignore errors
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/**
|
|
527
|
-
* Record performance metric for a provider
|
|
528
|
-
* @param {string} provider - Provider name
|
|
529
|
-
* @param {string} model - Model name
|
|
530
|
-
* @param {number} durationMs - Duration in milliseconds
|
|
531
|
-
*/
|
|
532
|
-
recordPerformance(provider, model, durationMs) {
|
|
533
|
-
const key = `${provider}:${model}`;
|
|
534
|
-
|
|
535
|
-
if (!this.performance[key]) {
|
|
536
|
-
this.performance[key] = {
|
|
537
|
-
provider,
|
|
538
|
-
model,
|
|
539
|
-
samples: [],
|
|
540
|
-
avgSpeed: 0
|
|
541
|
-
};
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
const perf = this.performance[key];
|
|
545
|
-
perf.samples.push(durationMs);
|
|
546
|
-
|
|
547
|
-
// Keep only last 10 samples
|
|
548
|
-
if (perf.samples.length > 10) {
|
|
549
|
-
perf.samples.shift();
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// Calculate average
|
|
553
|
-
perf.avgSpeed = perf.samples.reduce((a, b) => a + b, 0) / perf.samples.length;
|
|
554
|
-
perf.lastUsed = Date.now();
|
|
555
|
-
|
|
556
|
-
this.savePerformance();
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
/**
|
|
560
|
-
* Get fastest available provider from a list
|
|
561
|
-
* @param {Array} providers - Array of {provider, model, ...} objects
|
|
562
|
-
* @returns {Object|null} - Fastest available provider or null
|
|
563
|
-
*/
|
|
564
|
-
getFastestAvailable(providers) {
|
|
565
|
-
const available = providers.filter(p => !this.isRateLimited(p.provider, p.model));
|
|
566
|
-
|
|
567
|
-
if (available.length === 0) return null;
|
|
568
|
-
if (available.length === 1) return available[0];
|
|
569
|
-
|
|
570
|
-
// Sort by average speed (fastest first)
|
|
571
|
-
available.sort((a, b) => {
|
|
572
|
-
const aKey = `${a.provider}:${a.model}`;
|
|
573
|
-
const bKey = `${b.provider}:${b.model}`;
|
|
574
|
-
const aAvg = this.performance[aKey]?.avgSpeed;
|
|
575
|
-
const bAvg = this.performance[bKey]?.avgSpeed;
|
|
576
|
-
const aSpeed = (aAvg ?? a.estimatedSpeed ?? Infinity);
|
|
577
|
-
const bSpeed = (bAvg ?? b.estimatedSpeed ?? Infinity);
|
|
578
|
-
return aSpeed - bSpeed;
|
|
579
|
-
});
|
|
580
|
-
|
|
581
|
-
return available[0];
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
/**
|
|
585
|
-
* Get provider rankings by speed
|
|
586
|
-
* @returns {Array} - Sorted array of {provider, model, avgSpeed}
|
|
587
|
-
*/
|
|
588
|
-
getProviderRankings() {
|
|
589
|
-
return Object.values(this.performance)
|
|
590
|
-
.filter(p => p.avgSpeed > 0)
|
|
591
|
-
.sort((a, b) => a.avgSpeed - b.avgSpeed);
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
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
|
+
clearRateLimit(provider, model) {
|
|
18
|
+
if (!provider) return false;
|
|
19
|
+
const key = `${provider}:${model || provider}`;
|
|
20
|
+
if (this.rateLimits[key]) {
|
|
21
|
+
delete this.rateLimits[key];
|
|
22
|
+
this.saveRateLimits();
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
clearProviderRateLimits(provider) {
|
|
29
|
+
if (!provider) return 0;
|
|
30
|
+
const providerPrefix = `${provider}:`;
|
|
31
|
+
let cleared = 0;
|
|
32
|
+
for (const key of Object.keys(this.rateLimits)) {
|
|
33
|
+
if (key.startsWith(providerPrefix)) {
|
|
34
|
+
delete this.rateLimits[key];
|
|
35
|
+
cleared += 1;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (cleared > 0) {
|
|
39
|
+
this.saveRateLimits();
|
|
40
|
+
}
|
|
41
|
+
return cleared;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Load rate limit tracking data from file
|
|
46
|
+
* Auto-cleanup expired rate limits
|
|
47
|
+
*/
|
|
48
|
+
loadRateLimits() {
|
|
49
|
+
try {
|
|
50
|
+
if (fs.existsSync(this.rateLimitFile)) {
|
|
51
|
+
const data = fs.readFileSync(this.rateLimitFile, 'utf8');
|
|
52
|
+
const limits = JSON.parse(data);
|
|
53
|
+
|
|
54
|
+
// Auto-cleanup expired rate limits
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
let cleaned = false;
|
|
57
|
+
for (const key in limits) {
|
|
58
|
+
if (limits[key].resetTime && limits[key].resetTime <= now) {
|
|
59
|
+
delete limits[key];
|
|
60
|
+
cleaned = true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Save cleaned data if any expired limits were removed
|
|
65
|
+
if (cleaned) {
|
|
66
|
+
this.saveRateLimitsInternal(limits);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return limits;
|
|
70
|
+
}
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.error('Error loading rate limits:', err.message);
|
|
73
|
+
}
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Internal save without reload (to prevent infinite loop during cleanup)
|
|
79
|
+
*/
|
|
80
|
+
saveRateLimitsInternal(limits) {
|
|
81
|
+
try {
|
|
82
|
+
const dir = path.dirname(this.rateLimitFile);
|
|
83
|
+
if (!fs.existsSync(dir)) {
|
|
84
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
85
|
+
}
|
|
86
|
+
fs.writeFileSync(this.rateLimitFile, JSON.stringify(limits, null, 2));
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.error('Error saving rate limits:', err.message);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Save rate limit tracking data to file
|
|
94
|
+
*/
|
|
95
|
+
saveRateLimits() {
|
|
96
|
+
try {
|
|
97
|
+
const dir = path.dirname(this.rateLimitFile);
|
|
98
|
+
if (!fs.existsSync(dir)) {
|
|
99
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
100
|
+
}
|
|
101
|
+
fs.writeFileSync(this.rateLimitFile, JSON.stringify(this.rateLimits, null, 2));
|
|
102
|
+
} catch (err) {
|
|
103
|
+
console.error('Error saving rate limits:', err.message);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Parse rate limit duration from error message
|
|
109
|
+
* Examples:
|
|
110
|
+
* "Please try again in 15m5.472s" -> 905472 ms
|
|
111
|
+
* "Please try again in 13m21.792s" -> 801792 ms
|
|
112
|
+
* "Please try again in 1h30m" -> 5400000 ms
|
|
113
|
+
* "Session limit reached ∙ resets 12pm" -> ms until 12pm today/tomorrow
|
|
114
|
+
*/
|
|
115
|
+
parseRateLimitDuration(errorMessage) {
|
|
116
|
+
// Gemini / Google AI style: "You can resume using this model at 1/12/2026, 4:07:27 PM"
|
|
117
|
+
// Also seen as: "resume using this model at <date>, <time>"
|
|
118
|
+
const resumeAtMatch = errorMessage.match(/resume\s+(?:using\s+this\s+model\s+)?at\s+(\d{1,2}\/\d{1,2}\/\d{4})\s*,?\s*(\d{1,2}:\d{2}(?::\d{2})?)\s*(am|pm)/i);
|
|
119
|
+
if (resumeAtMatch) {
|
|
120
|
+
try {
|
|
121
|
+
const datePart = resumeAtMatch[1];
|
|
122
|
+
const timePart = resumeAtMatch[2];
|
|
123
|
+
const meridiem = resumeAtMatch[3];
|
|
124
|
+
const parsed = new Date(`${datePart}, ${timePart} ${meridiem.toUpperCase()}`);
|
|
125
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
126
|
+
const ms = parsed.getTime() - Date.now();
|
|
127
|
+
return ms > 0 ? ms : 0;
|
|
128
|
+
}
|
|
129
|
+
} catch (_) { }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Check for "resets [Month] [Day] at [Time]" format (e.g. "Spending cap reached resets Jan 17 at 12pm")
|
|
133
|
+
const dateMatch = errorMessage.match(/resets\s+([a-zA-Z]+)\s+(\d{1,2})\s+at\s+(\d{1,2}(?::\d{2})?)\s*(am|pm)?/i);
|
|
134
|
+
if (dateMatch) {
|
|
135
|
+
try {
|
|
136
|
+
const monthStr = dateMatch[1]; // Jan, February, etc.
|
|
137
|
+
const day = parseInt(dateMatch[2]);
|
|
138
|
+
const timeStr = dateMatch[3];
|
|
139
|
+
const meridiem = dateMatch[4] ? dateMatch[4].toLowerCase() : null;
|
|
140
|
+
|
|
141
|
+
// Parse time
|
|
142
|
+
let [hours, minutes] = timeStr.split(':').map(n => parseInt(n));
|
|
143
|
+
if (!minutes || Number.isNaN(minutes)) minutes = 0;
|
|
144
|
+
|
|
145
|
+
if (meridiem === 'pm' && hours !== 12) hours += 12;
|
|
146
|
+
if (meridiem === 'am' && hours === 12) hours = 0;
|
|
147
|
+
|
|
148
|
+
// Parse month
|
|
149
|
+
const months = {
|
|
150
|
+
jan: 0, january: 0,
|
|
151
|
+
feb: 1, february: 1,
|
|
152
|
+
mar: 2, march: 2,
|
|
153
|
+
apr: 3, april: 3,
|
|
154
|
+
may: 4,
|
|
155
|
+
jun: 5, june: 5,
|
|
156
|
+
jul: 6, july: 6,
|
|
157
|
+
aug: 7, august: 7,
|
|
158
|
+
sep: 8, sept: 8, september: 8,
|
|
159
|
+
oct: 9, october: 9,
|
|
160
|
+
nov: 10, november: 10,
|
|
161
|
+
dec: 11, december: 11
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const monthIndex = months[monthStr.toLowerCase().substring(0, 3)] ?? months[monthStr.toLowerCase()];
|
|
165
|
+
|
|
166
|
+
if (monthIndex !== undefined) {
|
|
167
|
+
const currentYear = new Date().getFullYear();
|
|
168
|
+
let parsed = new Date(currentYear, monthIndex, day, hours, minutes);
|
|
169
|
+
const now = Date.now();
|
|
170
|
+
|
|
171
|
+
// Logic to handle year crossing (reset in Jan, currently Dec)
|
|
172
|
+
if (parsed.getTime() < now) {
|
|
173
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
174
|
+
if (parsed.getTime() < now) {
|
|
175
|
+
const nextYearDate = new Date(parsed);
|
|
176
|
+
nextYearDate.setFullYear(nextYearDate.getFullYear() + 1);
|
|
177
|
+
if (nextYearDate.getTime() > now) {
|
|
178
|
+
parsed = nextYearDate;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const ms = parsed.getTime() - now;
|
|
185
|
+
return ms > 0 ? ms : 0;
|
|
186
|
+
}
|
|
187
|
+
} catch (_) { }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check for Claude Code session limit format: "Session limit reached ∙ resets 12pm"
|
|
191
|
+
const sessionMatch = errorMessage.match(/resets?\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?/i);
|
|
192
|
+
if (sessionMatch) {
|
|
193
|
+
let hour = parseInt(sessionMatch[1]);
|
|
194
|
+
const minute = sessionMatch[2] ? parseInt(sessionMatch[2]) : 0;
|
|
195
|
+
const meridiem = sessionMatch[3] ? sessionMatch[3].toLowerCase() : null;
|
|
196
|
+
|
|
197
|
+
// Convert to 24-hour format
|
|
198
|
+
if (meridiem === 'pm' && hour !== 12) {
|
|
199
|
+
hour += 12;
|
|
200
|
+
} else if (meridiem === 'am' && hour === 12) {
|
|
201
|
+
hour = 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Calculate reset time
|
|
205
|
+
const now = new Date();
|
|
206
|
+
const resetTime = new Date(now);
|
|
207
|
+
resetTime.setHours(hour, minute, 0, 0);
|
|
208
|
+
|
|
209
|
+
// If reset time is in the past, it's tomorrow
|
|
210
|
+
if (resetTime <= now) {
|
|
211
|
+
resetTime.setDate(resetTime.getDate() + 1);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return resetTime.getTime() - now.getTime();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Match patterns like "15m5.472s" or "1h30m" or "45s"
|
|
218
|
+
const timeMatch = errorMessage.match(/try again in ([\d.]+h)?\s?([\d.]+m)?\s?([\d.]+s)?/i);
|
|
219
|
+
if (!timeMatch) return null;
|
|
220
|
+
|
|
221
|
+
let totalMs = 0;
|
|
222
|
+
|
|
223
|
+
// Hours
|
|
224
|
+
if (timeMatch[1]) {
|
|
225
|
+
const hours = parseFloat(timeMatch[1].replace('h', ''));
|
|
226
|
+
totalMs += hours * 60 * 60 * 1000;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Minutes
|
|
230
|
+
if (timeMatch[2]) {
|
|
231
|
+
const minutes = parseFloat(timeMatch[2].replace('m', ''));
|
|
232
|
+
totalMs += minutes * 60 * 1000;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Seconds
|
|
236
|
+
if (timeMatch[3]) {
|
|
237
|
+
const seconds = parseFloat(timeMatch[3].replace('s', ''));
|
|
238
|
+
totalMs += seconds * 1000;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return totalMs;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Mark a provider as rate limited
|
|
246
|
+
* @param {string} provider - Provider name (e.g., "groq", "anthropic")
|
|
247
|
+
* @param {string} model - Model name (e.g., "llama-3.3-70b-versatile")
|
|
248
|
+
* @param {string} errorMessage - Full error message containing duration
|
|
249
|
+
*/
|
|
250
|
+
markRateLimited(provider, model, errorMessage) {
|
|
251
|
+
// Normalize model: if model is missing/undefined, assume it's the provider name.
|
|
252
|
+
model = model || provider;
|
|
253
|
+
|
|
254
|
+
let duration = this.parseRateLimitDuration(errorMessage);
|
|
255
|
+
if (duration === null || duration === undefined) {
|
|
256
|
+
console.warn(`Could not parse rate limit duration from: ${errorMessage}`);
|
|
257
|
+
// Don't guess the duration - just mark as rate limited without a specific reset time
|
|
258
|
+
duration = null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const resetTime = duration ? Date.now() + duration : null;
|
|
262
|
+
const key = `${provider}:${model}`;
|
|
263
|
+
|
|
264
|
+
this.rateLimits[key] = {
|
|
265
|
+
provider,
|
|
266
|
+
model,
|
|
267
|
+
resetTime,
|
|
268
|
+
resetDate: resetTime ? new Date(resetTime).toISOString() : null,
|
|
269
|
+
reason: errorMessage,
|
|
270
|
+
markedAt: new Date().toISOString()
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
this.saveRateLimits();
|
|
274
|
+
|
|
275
|
+
if (resetTime) {
|
|
276
|
+
const chalk = require('chalk');
|
|
277
|
+
const resetMinutes = Math.ceil(duration / 60000);
|
|
278
|
+
const resetDateTime = new Date(resetTime);
|
|
279
|
+
const timeString = resetDateTime.toLocaleTimeString('en-US', {
|
|
280
|
+
timeZone: 'America/Denver',
|
|
281
|
+
hour: 'numeric',
|
|
282
|
+
minute: '2-digit',
|
|
283
|
+
hour12: true
|
|
284
|
+
});
|
|
285
|
+
const dateString = resetDateTime.toLocaleDateString('en-US', {
|
|
286
|
+
timeZone: 'America/Denver',
|
|
287
|
+
weekday: 'short',
|
|
288
|
+
month: 'short',
|
|
289
|
+
day: 'numeric'
|
|
290
|
+
});
|
|
291
|
+
console.log(chalk.yellow(`📊 Provider ${provider}/${model} rate limited until ${timeString} MST on ${dateString} (~${resetMinutes}m)`));
|
|
292
|
+
} else {
|
|
293
|
+
const chalk = require('chalk');
|
|
294
|
+
console.log(chalk.yellow(`📊 Provider ${provider}/${model} rate limited (reset time unknown)`));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Check if a provider is currently rate limited
|
|
300
|
+
* @param {string} provider - Provider name
|
|
301
|
+
* @param {string} model - Model name (optional, will check all models if not provided)
|
|
302
|
+
* @returns {boolean}
|
|
303
|
+
*/
|
|
304
|
+
isRateLimited(provider, model) {
|
|
305
|
+
if (!model) {
|
|
306
|
+
// Check if ANY model for this provider is rate limited
|
|
307
|
+
const providerPrefix = `${provider}:`;
|
|
308
|
+
for (const key in this.rateLimits) {
|
|
309
|
+
if (key.startsWith(providerPrefix)) {
|
|
310
|
+
const limit = this.rateLimits[key];
|
|
311
|
+
if (limit && Date.now() < limit.resetTime) {
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const key = `${provider}:${model}`;
|
|
320
|
+
const limit = this.rateLimits[key];
|
|
321
|
+
|
|
322
|
+
if (limit) {
|
|
323
|
+
// Check if rate limit has expired
|
|
324
|
+
if (Date.now() >= limit.resetTime) {
|
|
325
|
+
// Clean up expired rate limit
|
|
326
|
+
delete this.rateLimits[key];
|
|
327
|
+
this.saveRateLimits();
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Fall back to any provider-level entries (handles older or model-agnostic keys)
|
|
335
|
+
const providerPrefix = `${provider}:`;
|
|
336
|
+
for (const k in this.rateLimits) {
|
|
337
|
+
if (k.startsWith(providerPrefix)) {
|
|
338
|
+
const l = this.rateLimits[k];
|
|
339
|
+
if (l && Date.now() < l.resetTime) return true;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Get rate limit information for a provider
|
|
348
|
+
* @param {string} provider - Provider name
|
|
349
|
+
* @param {string} model - Model name (optional)
|
|
350
|
+
* @returns {Object} - {isRateLimited: boolean, resetTime: number, reason: string}
|
|
351
|
+
*/
|
|
352
|
+
getRateLimitInfo(provider, model) {
|
|
353
|
+
// If model not specified, check all models for this provider
|
|
354
|
+
if (!model) {
|
|
355
|
+
const providerPrefix = `${provider}:`;
|
|
356
|
+
let earliestReset = null;
|
|
357
|
+
let reason = null;
|
|
358
|
+
|
|
359
|
+
for (const key in this.rateLimits) {
|
|
360
|
+
if (key.startsWith(providerPrefix)) {
|
|
361
|
+
const limit = this.rateLimits[key];
|
|
362
|
+
if (limit && Date.now() < limit.resetTime) {
|
|
363
|
+
if (!earliestReset || limit.resetTime < earliestReset) {
|
|
364
|
+
earliestReset = limit.resetTime;
|
|
365
|
+
reason = limit.reason;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (earliestReset) {
|
|
372
|
+
return {
|
|
373
|
+
isRateLimited: true,
|
|
374
|
+
resetTime: earliestReset,
|
|
375
|
+
reason: reason
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
} else {
|
|
379
|
+
const key = `${provider}:${model}`;
|
|
380
|
+
const limit = this.rateLimits[key];
|
|
381
|
+
|
|
382
|
+
if (limit && Date.now() < limit.resetTime) {
|
|
383
|
+
return {
|
|
384
|
+
isRateLimited: true,
|
|
385
|
+
resetTime: limit.resetTime,
|
|
386
|
+
reason: limit.reason
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Fallback: check other keys for this provider and return earliest reset
|
|
391
|
+
const providerPrefix = `${provider}:`;
|
|
392
|
+
let earliestReset = null;
|
|
393
|
+
let reason = null;
|
|
394
|
+
|
|
395
|
+
for (const k in this.rateLimits) {
|
|
396
|
+
if (k.startsWith(providerPrefix)) {
|
|
397
|
+
const l = this.rateLimits[k];
|
|
398
|
+
if (l && Date.now() < l.resetTime) {
|
|
399
|
+
if (!earliestReset || l.resetTime < earliestReset) {
|
|
400
|
+
earliestReset = l.resetTime;
|
|
401
|
+
reason = l.reason;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (earliestReset) {
|
|
408
|
+
return {
|
|
409
|
+
isRateLimited: true,
|
|
410
|
+
resetTime: earliestReset,
|
|
411
|
+
reason
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
isRateLimited: false,
|
|
418
|
+
resetTime: null,
|
|
419
|
+
reason: null
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Get available provider (not rate limited)
|
|
425
|
+
* @param {Array} providers - Array of {provider, model, apiKey} objects
|
|
426
|
+
* @returns {Object|null} - Available provider object or null
|
|
427
|
+
*/
|
|
428
|
+
getAvailableProvider(providers) {
|
|
429
|
+
for (const providerConfig of providers) {
|
|
430
|
+
if (!this.isRateLimited(providerConfig.provider, providerConfig.model)) {
|
|
431
|
+
return providerConfig;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Get time remaining until provider is available again
|
|
439
|
+
* @param {string} provider
|
|
440
|
+
* @param {string} model
|
|
441
|
+
* @returns {number|null} - Milliseconds until reset, or null if not rate limited
|
|
442
|
+
*/
|
|
443
|
+
getTimeUntilReset(provider, model) {
|
|
444
|
+
const key = `${provider}:${model}`;
|
|
445
|
+
const limit = this.rateLimits[key];
|
|
446
|
+
|
|
447
|
+
if (limit && Date.now() < limit.resetTime) {
|
|
448
|
+
const remaining = limit.resetTime - Date.now();
|
|
449
|
+
return remaining > 0 ? remaining : null;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Fallback: check other keys for this provider (handles legacy or model-agnostic entries)
|
|
453
|
+
const providerPrefix = `${provider}:`;
|
|
454
|
+
let earliest = null;
|
|
455
|
+
for (const k in this.rateLimits) {
|
|
456
|
+
if (k.startsWith(providerPrefix)) {
|
|
457
|
+
const l = this.rateLimits[k];
|
|
458
|
+
if (l && Date.now() < l.resetTime) {
|
|
459
|
+
if (!earliest || l.resetTime < earliest) earliest = l.resetTime;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (!earliest) return null;
|
|
465
|
+
const remaining = earliest - Date.now();
|
|
466
|
+
return remaining > 0 ? remaining : null;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Get status of all providers
|
|
471
|
+
* @param {Array} providers - Array of provider configs
|
|
472
|
+
* @returns {Array} - Array of provider status objects
|
|
473
|
+
*/
|
|
474
|
+
getProviderStatus(providers) {
|
|
475
|
+
return providers.map(p => {
|
|
476
|
+
const isLimited = this.isRateLimited(p.provider, p.model);
|
|
477
|
+
const timeUntilReset = this.getTimeUntilReset(p.provider, p.model);
|
|
478
|
+
|
|
479
|
+
return {
|
|
480
|
+
provider: p.provider,
|
|
481
|
+
model: p.model,
|
|
482
|
+
available: !isLimited,
|
|
483
|
+
rateLimited: isLimited,
|
|
484
|
+
resetIn: timeUntilReset ? Math.ceil(timeUntilReset / 60000) + 'm' : null
|
|
485
|
+
};
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Clear all rate limits (useful for testing)
|
|
491
|
+
*/
|
|
492
|
+
clearAllRateLimits() {
|
|
493
|
+
this.rateLimits = {};
|
|
494
|
+
this.saveRateLimits();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Load performance tracking data
|
|
499
|
+
*/
|
|
500
|
+
loadPerformance() {
|
|
501
|
+
try {
|
|
502
|
+
if (fs.existsSync(this.performanceFile)) {
|
|
503
|
+
return JSON.parse(fs.readFileSync(this.performanceFile, 'utf8'));
|
|
504
|
+
}
|
|
505
|
+
} catch (err) {
|
|
506
|
+
// Ignore errors
|
|
507
|
+
}
|
|
508
|
+
return {};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Save performance tracking data
|
|
513
|
+
*/
|
|
514
|
+
savePerformance() {
|
|
515
|
+
try {
|
|
516
|
+
const dir = path.dirname(this.performanceFile);
|
|
517
|
+
if (!fs.existsSync(dir)) {
|
|
518
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
519
|
+
}
|
|
520
|
+
fs.writeFileSync(this.performanceFile, JSON.stringify(this.performance, null, 2));
|
|
521
|
+
} catch (err) {
|
|
522
|
+
// Ignore errors
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Record performance metric for a provider
|
|
528
|
+
* @param {string} provider - Provider name
|
|
529
|
+
* @param {string} model - Model name
|
|
530
|
+
* @param {number} durationMs - Duration in milliseconds
|
|
531
|
+
*/
|
|
532
|
+
recordPerformance(provider, model, durationMs) {
|
|
533
|
+
const key = `${provider}:${model}`;
|
|
534
|
+
|
|
535
|
+
if (!this.performance[key]) {
|
|
536
|
+
this.performance[key] = {
|
|
537
|
+
provider,
|
|
538
|
+
model,
|
|
539
|
+
samples: [],
|
|
540
|
+
avgSpeed: 0
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const perf = this.performance[key];
|
|
545
|
+
perf.samples.push(durationMs);
|
|
546
|
+
|
|
547
|
+
// Keep only last 10 samples
|
|
548
|
+
if (perf.samples.length > 10) {
|
|
549
|
+
perf.samples.shift();
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Calculate average
|
|
553
|
+
perf.avgSpeed = perf.samples.reduce((a, b) => a + b, 0) / perf.samples.length;
|
|
554
|
+
perf.lastUsed = Date.now();
|
|
555
|
+
|
|
556
|
+
this.savePerformance();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Get fastest available provider from a list
|
|
561
|
+
* @param {Array} providers - Array of {provider, model, ...} objects
|
|
562
|
+
* @returns {Object|null} - Fastest available provider or null
|
|
563
|
+
*/
|
|
564
|
+
getFastestAvailable(providers) {
|
|
565
|
+
const available = providers.filter(p => !this.isRateLimited(p.provider, p.model));
|
|
566
|
+
|
|
567
|
+
if (available.length === 0) return null;
|
|
568
|
+
if (available.length === 1) return available[0];
|
|
569
|
+
|
|
570
|
+
// Sort by average speed (fastest first)
|
|
571
|
+
available.sort((a, b) => {
|
|
572
|
+
const aKey = `${a.provider}:${a.model}`;
|
|
573
|
+
const bKey = `${b.provider}:${b.model}`;
|
|
574
|
+
const aAvg = this.performance[aKey]?.avgSpeed;
|
|
575
|
+
const bAvg = this.performance[bKey]?.avgSpeed;
|
|
576
|
+
const aSpeed = (aAvg ?? a.estimatedSpeed ?? Infinity);
|
|
577
|
+
const bSpeed = (bAvg ?? b.estimatedSpeed ?? Infinity);
|
|
578
|
+
return aSpeed - bSpeed;
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
return available[0];
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Get provider rankings by speed
|
|
586
|
+
* @returns {Array} - Sorted array of {provider, model, avgSpeed}
|
|
587
|
+
*/
|
|
588
|
+
getProviderRankings() {
|
|
589
|
+
return Object.values(this.performance)
|
|
590
|
+
.filter(p => p.avgSpeed > 0)
|
|
591
|
+
.sort((a, b) => a.avgSpeed - b.avgSpeed);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
module.exports = ProviderManager;
|