loren-code 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,346 @@
1
+ import logger from './logger.js';
2
+ import { loadConfig } from './config.js';
3
+
4
+ const usageData = new Map();
5
+ const rateLimitData = new Map();
6
+
7
+ const SESSION_RESET_HOURS = 3;
8
+ const WEEKLY_RESET_DAY = 1; // Monday
9
+ const WEEKLY_RESET_HOUR = 14; // 2pm
10
+ const WEEKLY_RESET_MINUTE = 0;
11
+
12
+ function createObservedUsage() {
13
+ return {
14
+ observed: {
15
+ requests: 0,
16
+ tokens: 0,
17
+ firstSeenAt: Date.now(),
18
+ },
19
+ probe: {
20
+ tested: false,
21
+ status: 'untested',
22
+ lastProbeAt: null,
23
+ lastError: null,
24
+ },
25
+ lastUpdated: Date.now(),
26
+ };
27
+ }
28
+
29
+ function getNextSessionReset(now = new Date()) {
30
+ const next = new Date(now.getTime());
31
+ next.setMinutes(0, 0, 0);
32
+
33
+ const currentHour = next.getHours();
34
+ const nextBoundaryHour = Math.floor(currentHour / SESSION_RESET_HOURS) * SESSION_RESET_HOURS + SESSION_RESET_HOURS;
35
+
36
+ if (nextBoundaryHour >= 24) {
37
+ next.setDate(next.getDate() + 1);
38
+ next.setHours(0, 0, 0, 0);
39
+ return next.getTime();
40
+ }
41
+
42
+ next.setHours(nextBoundaryHour, 0, 0, 0);
43
+ return next.getTime();
44
+ }
45
+
46
+ function getNextWeeklyReset(now = new Date()) {
47
+ const next = new Date(now.getTime());
48
+ next.setSeconds(0, 0);
49
+ next.setHours(WEEKLY_RESET_HOUR, WEEKLY_RESET_MINUTE, 0, 0);
50
+
51
+ const currentDay = next.getDay();
52
+ let daysUntilReset = (WEEKLY_RESET_DAY - currentDay + 7) % 7;
53
+
54
+ if (daysUntilReset === 0 && next.getTime() <= now.getTime()) {
55
+ daysUntilReset = 7;
56
+ }
57
+
58
+ next.setDate(next.getDate() + daysUntilReset);
59
+ return next.getTime();
60
+ }
61
+
62
+ export class UsageTracker {
63
+ constructor() {
64
+ this.syncKeysFromConfig();
65
+ }
66
+
67
+ syncKeysFromConfig() {
68
+ try {
69
+ const config = loadConfig();
70
+ const activeKeys = new Set(config.apiKeys);
71
+
72
+ for (const key of usageData.keys()) {
73
+ if (!activeKeys.has(key)) {
74
+ usageData.delete(key);
75
+ rateLimitData.delete(key);
76
+ }
77
+ }
78
+
79
+ config.apiKeys.forEach((key) => {
80
+ if (!usageData.has(key)) {
81
+ usageData.set(key, createObservedUsage());
82
+ logger.info(`Initialized usage tracking for key ${key.substring(0, 20)}...`);
83
+ }
84
+ });
85
+ } catch (error) {
86
+ logger.error(`Failed to sync keys from config: ${error.message}`);
87
+ }
88
+ }
89
+
90
+ recordUsage(apiKey, tokens = 0) {
91
+ if (!apiKey) {
92
+ return;
93
+ }
94
+
95
+ let data = usageData.get(apiKey);
96
+ if (!data) {
97
+ data = createObservedUsage();
98
+ usageData.set(apiKey, data);
99
+ }
100
+
101
+ data.observed.requests++;
102
+ data.observed.tokens += tokens;
103
+ data.lastUpdated = Date.now();
104
+
105
+ logger.debug(`Usage recorded for key ${apiKey.substring(0, 20)}...: +1 request, +${tokens} tokens`);
106
+ }
107
+
108
+ isRateLimited(apiKey) {
109
+ const rateLimit = rateLimitData.get(apiKey);
110
+ if (!rateLimit) {
111
+ return false;
112
+ }
113
+
114
+ if (typeof rateLimit.resetTime === 'number' && Date.now() > rateLimit.resetTime) {
115
+ rateLimitData.delete(apiKey);
116
+ logger.info(`Rate limit expired for key ${apiKey.substring(0, 20)}...`);
117
+ return false;
118
+ }
119
+
120
+ return rateLimit.rateLimited;
121
+ }
122
+
123
+ markRateLimited(apiKey, resetTime = null, reason = 'Rate limit reached') {
124
+ const data = usageData.get(apiKey) || createObservedUsage();
125
+ data.probe.tested = true;
126
+ data.probe.status = 'rate_limited';
127
+ data.probe.lastProbeAt = Date.now();
128
+ data.probe.lastError = reason;
129
+ data.lastUpdated = Date.now();
130
+ usageData.set(apiKey, data);
131
+
132
+ rateLimitData.set(apiKey, {
133
+ rateLimited: true,
134
+ resetTime: typeof resetTime === 'number' ? resetTime : null,
135
+ reason,
136
+ markedAt: Date.now(),
137
+ });
138
+
139
+ if (typeof resetTime === 'number') {
140
+ logger.warn(`Key ${apiKey.substring(0, 20)}... marked as rate limited. Reset in ${Math.ceil((resetTime - Date.now()) / 60000)} minutes`);
141
+ } else {
142
+ logger.warn(`Key ${apiKey.substring(0, 20)}... marked as rate limited. Reset time unknown.`);
143
+ }
144
+ }
145
+
146
+ markHealthy(apiKey) {
147
+ const data = usageData.get(apiKey) || createObservedUsage();
148
+ data.probe.tested = true;
149
+ data.probe.status = 'available';
150
+ data.probe.lastProbeAt = Date.now();
151
+ data.probe.lastError = null;
152
+ data.lastUpdated = Date.now();
153
+ usageData.set(apiKey, data);
154
+
155
+ if (!rateLimitData.has(apiKey)) {
156
+ return;
157
+ }
158
+
159
+ rateLimitData.delete(apiKey);
160
+ logger.info(`Cleared rate limit state for key ${apiKey.substring(0, 20)}...`);
161
+ }
162
+
163
+ markUnhealthy(apiKey, reason = 'Request failed') {
164
+ const data = usageData.get(apiKey) || createObservedUsage();
165
+ data.probe.tested = true;
166
+ data.probe.status = 'unhealthy';
167
+ data.probe.lastProbeAt = Date.now();
168
+ data.probe.lastError = reason;
169
+ data.lastUpdated = Date.now();
170
+ usageData.set(apiKey, data);
171
+ }
172
+
173
+ getKeyUsage(apiKey) {
174
+ const data = usageData.get(apiKey) || createObservedUsage();
175
+ const rateLimit = rateLimitData.get(apiKey);
176
+ const sessionResetTime = getNextSessionReset();
177
+ const weeklyResetTime = getNextWeeklyReset();
178
+
179
+ return {
180
+ observed: {
181
+ requests: data.observed.requests,
182
+ tokens: data.observed.tokens,
183
+ firstSeenAt: data.observed.firstSeenAt,
184
+ },
185
+ probe: {
186
+ tested: data.probe?.tested || false,
187
+ status: data.probe?.status || 'untested',
188
+ lastProbeAt: data.probe?.lastProbeAt || null,
189
+ lastError: data.probe?.lastError || null,
190
+ },
191
+ limits: {
192
+ session: {
193
+ used: null,
194
+ limit: null,
195
+ percentage: null,
196
+ resetTime: sessionResetTime,
197
+ },
198
+ weekly: {
199
+ used: null,
200
+ limit: null,
201
+ percentage: null,
202
+ resetTime: weeklyResetTime,
203
+ },
204
+ },
205
+ rateLimit: {
206
+ active: rateLimit?.rateLimited || false,
207
+ reason: rateLimit?.reason || null,
208
+ resetTime: rateLimit?.resetTime || null,
209
+ resetInMs: typeof rateLimit?.resetTime === 'number' ? Math.max(0, rateLimit.resetTime - Date.now()) : null,
210
+ },
211
+ };
212
+ }
213
+
214
+ getAllKeysUsage() {
215
+ const result = [];
216
+
217
+ for (const [key, data] of usageData) {
218
+ const usage = this.getKeyUsage(key);
219
+ result.push({
220
+ key,
221
+ usage,
222
+ isRateLimited: usage.rateLimit.active,
223
+ isTested: usage.probe.tested,
224
+ availabilityStatus: usage.probe.status,
225
+ rateLimitResetTime: usage.rateLimit.resetTime,
226
+ rateLimitReason: usage.rateLimit.reason,
227
+ lastUpdated: data.lastUpdated,
228
+ });
229
+ }
230
+
231
+ return result;
232
+ }
233
+
234
+ getRateLimitResetTime(apiKey) {
235
+ const rateLimit = rateLimitData.get(apiKey);
236
+ if (!rateLimit || !rateLimit.rateLimited || typeof rateLimit.resetTime !== 'number') {
237
+ return null;
238
+ }
239
+
240
+ const now = Date.now();
241
+ if (now > rateLimit.resetTime) {
242
+ return 0;
243
+ }
244
+
245
+ return rateLimit.resetTime - now;
246
+ }
247
+
248
+ suggestNextKey(availableKeys) {
249
+ const healthyKeys = availableKeys.filter((key) => !this.isRateLimited(key));
250
+
251
+ if (healthyKeys.length === 0) {
252
+ logger.warn('All API keys are rate limited!');
253
+ return null;
254
+ }
255
+
256
+ let suggestedKey = healthyKeys[0];
257
+ let minRequests = Infinity;
258
+
259
+ for (const key of healthyKeys) {
260
+ const usage = this.getKeyUsage(key);
261
+ if (usage.observed.requests < minRequests) {
262
+ minRequests = usage.observed.requests;
263
+ suggestedKey = key;
264
+ }
265
+ }
266
+
267
+ logger.debug(`Suggested key: ${suggestedKey.substring(0, 20)}... (observed requests: ${minRequests})`);
268
+ return suggestedKey;
269
+ }
270
+
271
+ resetAll() {
272
+ try {
273
+ const config = loadConfig();
274
+
275
+ usageData.clear();
276
+ rateLimitData.clear();
277
+
278
+ config.apiKeys.forEach((key) => {
279
+ usageData.set(key, createObservedUsage());
280
+ });
281
+
282
+ logger.info('Usage data reset to zero for all keys');
283
+ return true;
284
+ } catch (error) {
285
+ logger.error(`Failed to reset usage data: ${error.message}`);
286
+ return false;
287
+ }
288
+ }
289
+
290
+ getDashboardData() {
291
+ const keysUsage = this.getAllKeysUsage();
292
+ const totalUsage = keysUsage.reduce((acc, key) => {
293
+ acc.requests += key.usage.observed.requests;
294
+ acc.tokens += key.usage.observed.tokens;
295
+ return acc;
296
+ }, { requests: 0, tokens: 0 });
297
+
298
+ const rateLimitedKeys = keysUsage.filter((key) => key.isRateLimited).length;
299
+ const availableKeys = keysUsage.filter((key) => key.availabilityStatus === 'available').length;
300
+ const untestedKeys = keysUsage.filter((key) => !key.isTested).length;
301
+ const unhealthyKeys = keysUsage.filter((key) => key.availabilityStatus === 'unhealthy').length;
302
+ const knownResets = keysUsage
303
+ .map((key) => key.rateLimitResetTime)
304
+ .filter((value) => typeof value === 'number' && value > Date.now());
305
+ const sessionResetTime = getNextSessionReset();
306
+ const weeklyResetTime = getNextWeeklyReset();
307
+
308
+ return {
309
+ summary: {
310
+ totalKeys: keysUsage.length,
311
+ healthyKeys: availableKeys,
312
+ availableKeys,
313
+ untestedKeys,
314
+ unhealthyKeys,
315
+ rateLimitedKeys,
316
+ observed: {
317
+ requests: totalUsage.requests,
318
+ tokens: totalUsage.tokens,
319
+ },
320
+ limits: {
321
+ session: {
322
+ used: null,
323
+ limit: null,
324
+ percentage: null,
325
+ resetsIn: sessionResetTime - Date.now(),
326
+ resetTime: sessionResetTime,
327
+ },
328
+ weekly: {
329
+ used: null,
330
+ limit: null,
331
+ percentage: null,
332
+ resetsIn: weeklyResetTime - Date.now(),
333
+ resetTime: weeklyResetTime,
334
+ },
335
+ },
336
+ rateLimit: {
337
+ active: rateLimitedKeys,
338
+ nextResetIn: knownResets.length ? Math.min(...knownResets) - Date.now() : null,
339
+ },
340
+ },
341
+ keys: keysUsage,
342
+ };
343
+ }
344
+ }
345
+
346
+ export default new UsageTracker();