Skip to content

Commit 1d93409

Browse files
committed
v1.2.1 - Multiple Gemini API key support with automatic rotation
1 parent 38b1d0c commit 1d93409

8 files changed

Lines changed: 730 additions & 86 deletions

File tree

README.md

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<img width="120" height="120" alt="AutoSort+ Logo" src="https://github.com/user-attachments/assets/32e8e1fb-7cb0-4b65-9bcc-e1cf693bf5e5" />
88

99
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10-
[![Version](https://img.shields.io/badge/version-1.2.0-blue.svg)](https://github.com/Nigel1992/AutoSort-Plus/releases)
10+
[![Version](https://img.shields.io/badge/version-1.2.1-blue.svg)](https://github.com/Nigel1992/AutoSort-Plus/releases)
1111
[![Thunderbird](https://img.shields.io/badge/Thunderbird-78.0%2B-0a84ff.svg)](https://www.thunderbird.net/)
1212
[![Development Status](https://img.shields.io/badge/status-active-success)](https://github.com/Nigel1992/AutoSort-Plus)
1313

@@ -43,7 +43,7 @@
4343

4444
### 🤖 Multi-Provider AI
4545
Choose from **5 leading AI providers**:
46-
- **Google Gemini** - Best free tier
46+
- **Google Gemini** - Best free tier + **Multi-key support**
4747
- **OpenAI** - Superior accuracy
4848
- **Anthropic Claude** - Privacy-focused
4949
- **Groq** - Fastest processing
@@ -52,11 +52,11 @@ Choose from **5 leading AI providers**:
5252
</td>
5353
<td width="50%">
5454

55-
### 📊 Usage Tracking
56-
- Real-time usage monitoring
57-
- Daily limit tracking
58-
- Automatic warnings
59-
- API key switching alerts
55+
### 🔑 Multiple API Keys (Gemini)
56+
- Add keys from multiple projects
57+
- Automatic rotation on limit
58+
- Per-key usage tracking
59+
- 5 keys = 100 requests/day
6060

6161
</td>
6262
</tr>
@@ -202,12 +202,13 @@ Select emails → Let AI categorize them automatically
202202
<details>
203203
<summary><b>💡 Tips for Managing Free Tier Limits</b></summary>
204204

205-
**For Gemini users:**
206-
- Each API key = 20 requests/day
207-
- Create keys in different projects for more quota
208-
- Switch keys when limit reached (Reset Counter button)
205+
**For Gemini users (NEW in v1.2.1!):**
206+
- 🆕 **Multiple API Keys**: Add keys from different Google Cloud projects
207+
- 🔄 **Automatic Rotation**: Extension switches keys when limits are reached
208+
- 📊 **Per-Key Tracking**: Monitor usage for each key independently
209+
-**Example**: 5 keys = 100 requests/day total (20 per key)
210+
- 🔧 **How to add**: Settings → Add Another Gemini Key
209211
- Check usage: [AI Studio Usage](https://aistudio.google.com/usage)
210-
- Addon tracks usage automatically
211212

212213
**For all providers:**
213214
- Process emails in small batches
@@ -229,10 +230,18 @@ Select emails → Let AI categorize them automatically
229230

230231
### Advanced Features
231232

233+
**� Multiple API Keys (Gemini - NEW!)**
234+
- Add unlimited keys from different projects
235+
- Automatic rotation when limits reached
236+
- Individual testing and status tracking
237+
- Visual indicators (Active, Ready, Near Limit)
238+
- Combined quota = keys × 20 requests/day
239+
232240
**📊 Usage Monitoring (Gemini)**
233241
- Real-time usage display in settings
242+
- Per-key usage statistics
234243
- Automatic warnings at 15/20 limit
235-
- Reset counter when switching API keys
244+
- Smart key rotation
236245

237246
**📁 Folder Management**
238247
- Load folders from IMAP accounts

autosortplus.xpi

3.39 KB
Binary file not shown.

background.js

Lines changed: 172 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,108 @@ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
1010
}
1111
});
1212

13-
// Gemini rate limiting functions (free tier: 5/min, 20/day)
13+
// Gemini rate limiting functions (free tier: 5/min, 20/day per key)
1414
async function checkGeminiRateLimit() {
1515
const now = Date.now();
16-
const data = await browser.storage.local.get(['geminiRateLimit']);
16+
const data = await browser.storage.local.get([
17+
'geminiApiKeys',
18+
'geminiRateLimits',
19+
'currentGeminiKeyIndex',
20+
'geminiPaidPlan',
21+
'geminiRateLimit' // Legacy single-key support
22+
]);
23+
24+
// Handle paid plan - no limits
25+
if (data.geminiPaidPlan) {
26+
return { allowed: true, waitTime: 0 };
27+
}
28+
29+
// Multi-key mode
30+
if (data.geminiApiKeys && data.geminiApiKeys.length > 0) {
31+
const keys = data.geminiApiKeys;
32+
const rateLimits = data.geminiRateLimits || keys.map(() => ({
33+
requests: [],
34+
dailyCount: 0,
35+
dailyResetTime: now + (24 * 60 * 60 * 1000)
36+
}));
37+
let currentIndex = data.currentGeminiKeyIndex || 0;
38+
39+
// Try to find an available key
40+
const startIndex = currentIndex;
41+
let attempts = 0;
42+
43+
while (attempts < keys.length) {
44+
const rateLimit = rateLimits[currentIndex];
45+
46+
// Reset daily count if it's a new day
47+
if (now > rateLimit.dailyResetTime) {
48+
rateLimit.dailyCount = 0;
49+
rateLimit.dailyResetTime = now + (24 * 60 * 60 * 1000);
50+
rateLimit.requests = [];
51+
}
52+
53+
// Remove requests older than 1 minute
54+
const oneMinuteAgo = now - 60000;
55+
rateLimit.requests = rateLimit.requests.filter(time => time > oneMinuteAgo);
56+
57+
// Check if this key is available
58+
if (rateLimit.dailyCount < 20) {
59+
// Check if we need to wait
60+
if (rateLimit.requests.length > 0) {
61+
const lastRequest = Math.max(...rateLimit.requests);
62+
const timeSinceLastRequest = now - lastRequest;
63+
const minInterval = 12000; // 12 seconds
64+
65+
if (timeSinceLastRequest < minInterval) {
66+
const waitTime = Math.ceil((minInterval - timeSinceLastRequest) / 1000);
67+
return {
68+
allowed: true,
69+
waitTime: waitTime,
70+
keyIndex: currentIndex
71+
};
72+
}
73+
}
74+
75+
// This key is ready to use
76+
await browser.storage.local.set({
77+
currentGeminiKeyIndex: currentIndex,
78+
geminiRateLimits: rateLimits
79+
});
80+
81+
return {
82+
allowed: true,
83+
waitTime: 0,
84+
keyIndex: currentIndex
85+
};
86+
}
87+
88+
// This key has reached its limit, try next one
89+
currentIndex = (currentIndex + 1) % keys.length;
90+
attempts++;
91+
}
92+
93+
// All keys have reached their limits
94+
return {
95+
allowed: false,
96+
message: `All ${keys.length} Gemini API keys have reached their daily limit (20/day each). Please wait for reset or add more API keys in settings.`
97+
};
98+
}
99+
100+
// Legacy single-key mode (backward compatibility)
17101
const rateLimit = data.geminiRateLimit || { requests: [], dailyCount: 0, dailyResetTime: now };
18102

19103
// Reset daily count if it's a new day
20104
if (now > rateLimit.dailyResetTime) {
21105
rateLimit.dailyCount = 0;
22-
rateLimit.dailyResetTime = now + (24 * 60 * 60 * 1000); // 24 hours from now
106+
rateLimit.dailyResetTime = now + (24 * 60 * 60 * 1000);
23107
}
24108

25109
// Check daily limit (20 per day)
26110
if (rateLimit.dailyCount >= 20) {
27111
const hoursUntilReset = Math.ceil((rateLimit.dailyResetTime - now) / (1000 * 60 * 60));
28112
return {
29113
allowed: false,
30-
message: `Gemini free tier daily limit reached (20/day). Resets in ${hoursUntilReset} hours. Upgrade to paid plan in settings to remove limits.`
114+
message: `Gemini free tier daily limit reached (20/day). Resets in ${hoursUntilReset} hours. Upgrade to paid plan or add multiple API keys in settings to remove limits.`
31115
};
32116
}
33117

@@ -56,22 +140,52 @@ async function checkGeminiRateLimit() {
56140
};
57141
}
58142

59-
async function trackGeminiRequest() {
143+
async function trackGeminiRequest(keyIndex = null) {
60144
const now = Date.now();
61-
const data = await browser.storage.local.get(['geminiRateLimit']);
62-
const rateLimit = data.geminiRateLimit || { requests: [], dailyCount: 0, dailyResetTime: now + (24 * 60 * 60 * 1000) };
145+
const data = await browser.storage.local.get([
146+
'geminiApiKeys',
147+
'geminiRateLimits',
148+
'currentGeminiKeyIndex',
149+
'geminiRateLimit' // Legacy
150+
]);
63151

64-
// Add current request
65-
rateLimit.requests.push(now);
66-
rateLimit.dailyCount += 1;
67-
68-
// Clean old requests
69-
const oneMinuteAgo = now - 60000;
70-
rateLimit.requests = rateLimit.requests.filter(time => time > oneMinuteAgo);
71-
72-
await browser.storage.local.set({ geminiRateLimit: rateLimit });
73-
74-
console.log(`Gemini requests: ${rateLimit.dailyCount}/20 today, ${rateLimit.requests.length} in last minute`);
152+
// Multi-key mode
153+
if (data.geminiApiKeys && data.geminiApiKeys.length > 0 && keyIndex !== null) {
154+
const rateLimits = data.geminiRateLimits || data.geminiApiKeys.map(() => ({
155+
requests: [],
156+
dailyCount: 0,
157+
dailyResetTime: now + (24 * 60 * 60 * 1000)
158+
}));
159+
160+
const rateLimit = rateLimits[keyIndex];
161+
162+
// Add current request
163+
rateLimit.requests.push(now);
164+
rateLimit.dailyCount += 1;
165+
166+
// Clean old requests
167+
const oneMinuteAgo = now - 60000;
168+
rateLimit.requests = rateLimit.requests.filter(time => time > oneMinuteAgo);
169+
170+
await browser.storage.local.set({ geminiRateLimits: rateLimits });
171+
172+
console.log(`Gemini Key #${keyIndex + 1}: ${rateLimit.dailyCount}/20 today, ${rateLimit.requests.length} in last minute`);
173+
} else {
174+
// Legacy single-key mode
175+
const rateLimit = data.geminiRateLimit || { requests: [], dailyCount: 0, dailyResetTime: now + (24 * 60 * 60 * 1000) };
176+
177+
// Add current request
178+
rateLimit.requests.push(now);
179+
rateLimit.dailyCount += 1;
180+
181+
// Clean old requests
182+
const oneMinuteAgo = now - 60000;
183+
rateLimit.requests = rateLimit.requests.filter(time => time > oneMinuteAgo);
184+
185+
await browser.storage.local.set({ geminiRateLimit: rateLimit });
186+
187+
console.log(`Gemini requests: ${rateLimit.dailyCount}/20 today, ${rateLimit.requests.length} in last minute`);
188+
}
75189
}
76190

77191
// Function to show notification
@@ -124,10 +238,21 @@ async function analyzeEmailContent(emailContent) {
124238
"Starting email analysis..."
125239
);
126240

127-
const settings = await browser.storage.local.get(['apiKey', 'aiProvider', 'labels', 'enableAi', 'geminiPaidPlan', 'geminiRateLimit']);
241+
const settings = await browser.storage.local.get([
242+
'apiKey',
243+
'geminiApiKeys',
244+
'currentGeminiKeyIndex',
245+
'aiProvider',
246+
'labels',
247+
'enableAi',
248+
'geminiPaidPlan',
249+
'geminiRateLimit',
250+
'geminiRateLimits'
251+
]);
128252
const provider = settings.aiProvider || 'gemini';
129253

130254
// Check Gemini rate limits (free tier only)
255+
let keyIndexToUse = null;
131256
if (provider === 'gemini' && !settings.geminiPaidPlan) {
132257
const rateLimitCheck = await checkGeminiRateLimit();
133258
if (!rateLimitCheck.allowed) {
@@ -136,22 +261,23 @@ async function analyzeEmailContent(emailContent) {
136261
"AutoSort+ Rate Limit",
137262
rateLimitCheck.message
138263
);
139-
return null;
264+
throw new Error(rateLimitCheck.message);
140265
}
141266

142-
// Wait if needed for rate limiting (12 seconds between requests)
143267
if (rateLimitCheck.waitTime > 0) {
144268
await updateNotification(
145269
notificationId,
146-
"AutoSort+ Rate Limiting",
147-
`Waiting ${rateLimitCheck.waitTime} seconds for rate limit...`
270+
"AutoSort+ Rate Limit",
271+
`Rate limit reached. Waiting ${rateLimitCheck.waitTime} seconds...`
148272
);
149273
await new Promise(resolve => setTimeout(resolve, rateLimitCheck.waitTime * 1000));
150274
}
275+
276+
keyIndexToUse = rateLimitCheck.keyIndex;
151277
}
152278

153279
console.log("Settings retrieved:", {
154-
hasApiKey: !!settings.apiKey,
280+
hasApiKey: !!(settings.apiKey || (settings.geminiApiKeys && settings.geminiApiKeys.length > 0)),
155281
provider: provider,
156282
labels: settings.labels,
157283
enableAi: settings.enableAi !== false
@@ -167,7 +293,22 @@ async function analyzeEmailContent(emailContent) {
167293
return null;
168294
}
169295

170-
if (!settings.apiKey) {
296+
// Check API key availability based on provider
297+
let apiKeyToUse = null;
298+
if (provider === 'gemini') {
299+
if (settings.geminiApiKeys && settings.geminiApiKeys.length > 0) {
300+
const keyIndex = keyIndexToUse !== null ? keyIndexToUse : (settings.currentGeminiKeyIndex || 0);
301+
apiKeyToUse = settings.geminiApiKeys[keyIndex];
302+
console.log(`Using Gemini API Key #${keyIndex + 1} of ${settings.geminiApiKeys.length}`);
303+
} else if (settings.apiKey) {
304+
// Legacy single key
305+
apiKeyToUse = settings.apiKey;
306+
}
307+
} else {
308+
apiKeyToUse = settings.apiKey;
309+
}
310+
311+
if (!apiKeyToUse) {
171312
console.error("Missing API key");
172313
await updateNotification(
173314
notificationId,
@@ -209,12 +350,12 @@ async function analyzeEmailContent(emailContent) {
209350
let data;
210351

211352
if (provider === 'gemini') {
212-
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${settings.apiKey}`;
353+
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${apiKeyToUse}`;
213354
console.log("Making API request to Gemini...");
214355

215356
// Track request for rate limiting (free tier only)
216357
if (!settings.geminiPaidPlan) {
217-
await trackGeminiRequest();
358+
await trackGeminiRequest(keyIndexToUse);
218359
}
219360

220361
await updateNotification(
@@ -281,7 +422,7 @@ async function analyzeEmailContent(emailContent) {
281422
method: 'POST',
282423
headers: {
283424
'Content-Type': 'application/json',
284-
'Authorization': `Bearer ${settings.apiKey}`
425+
'Authorization': `Bearer ${apiKeyToUse}`
285426
},
286427
body: JSON.stringify({
287428
model: 'gpt-4o-mini',
@@ -304,7 +445,7 @@ async function analyzeEmailContent(emailContent) {
304445
method: 'POST',
305446
headers: {
306447
'Content-Type': 'application/json',
307-
'x-api-key': settings.apiKey,
448+
'x-api-key': apiKeyToUse,
308449
'anthropic-version': '2023-06-01'
309450
},
310451
body: JSON.stringify({
@@ -327,7 +468,7 @@ async function analyzeEmailContent(emailContent) {
327468
method: 'POST',
328469
headers: {
329470
'Content-Type': 'application/json',
330-
'Authorization': `Bearer ${settings.apiKey}`
471+
'Authorization': `Bearer ${apiKeyToUse}`
331472
},
332473
body: JSON.stringify({
333474
model: 'llama-3.3-70b-versatile',
@@ -350,7 +491,7 @@ async function analyzeEmailContent(emailContent) {
350491
method: 'POST',
351492
headers: {
352493
'Content-Type': 'application/json',
353-
'Authorization': `Bearer ${settings.apiKey}`
494+
'Authorization': `Bearer ${apiKeyToUse}`
354495
},
355496
body: JSON.stringify({
356497
model: 'mistral-small-latest',

manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": 2,
33
"name": "AutoSort+",
4-
"version": "1.2.0",
4+
"version": "1.2.1",
55
"description": "Automatically sort and label your emails with custom rules using AI",
66
"author": "Nigel Hagen",
77
"applications": {

0 commit comments

Comments
 (0)