antigravity-claude-proxy 2.2.2 → 2.3.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.
package/README.md CHANGED
@@ -362,6 +362,7 @@ While most users can use the default settings, you can tune the proxy behavior v
362
362
  - **Retry Logic**: Configure `maxRetries`, `retryBaseMs`, and `retryMaxMs`.
363
363
  - **Load Balancing**: Adjust `defaultCooldownMs` and `maxWaitBeforeErrorMs`.
364
364
  - **Persistence**: Enable `persistTokenCache` to save OAuth sessions across restarts.
365
+ - **Max Accounts**: Set `maxAccounts` (1-100) to limit the number of Google accounts. Default: 10.
365
366
 
366
367
  Refer to `config.example.json` for a complete list of fields and documentation.
367
368
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antigravity-claude-proxy",
3
- "version": "2.2.2",
3
+ "version": "2.3.0",
4
4
  "description": "Proxy server to use Antigravity's Claude models with Claude Code CLI",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -250,6 +250,12 @@ window.Components.serverConfig = () => ({
250
250
  (v) => window.Validators.validateTimeout(v, MAX_WAIT_MIN, MAX_WAIT_MAX));
251
251
  },
252
252
 
253
+ toggleMaxAccounts(value) {
254
+ const { MAX_ACCOUNTS_MIN, MAX_ACCOUNTS_MAX } = window.AppConstants.VALIDATION;
255
+ this.saveConfigField('maxAccounts', value, 'Max Accounts',
256
+ (v) => window.Validators.validateRange(v, MAX_ACCOUNTS_MIN, MAX_ACCOUNTS_MAX, 'Max Accounts'));
257
+ },
258
+
253
259
  toggleRateLimitDedupWindowMs(value) {
254
260
  const { RATE_LIMIT_DEDUP_MIN, RATE_LIMIT_DEDUP_MAX } = window.AppConstants.VALIDATION;
255
261
  this.saveConfigField('rateLimitDedupWindowMs', value, 'Rate Limit Dedup Window',
@@ -69,6 +69,10 @@ window.AppConstants.VALIDATION = {
69
69
  MAX_WAIT_MIN: 60000,
70
70
  MAX_WAIT_MAX: 1800000,
71
71
 
72
+ // Max accounts range (1 - 100)
73
+ MAX_ACCOUNTS_MIN: 1,
74
+ MAX_ACCOUNTS_MAX: 100,
75
+
72
76
  // Rate limit dedup window (1 - 30 seconds)
73
77
  RATE_LIMIT_DEDUP_MIN: 1000,
74
78
  RATE_LIMIT_DEDUP_MAX: 30000,
@@ -13,6 +13,7 @@ document.addEventListener('alpine:init', () => {
13
13
  modelConfig: {}, // Model metadata (hidden, pinned, alias)
14
14
  quotaRows: [], // Filtered view
15
15
  usageHistory: {}, // Usage statistics history (from /account-limits?includeHistory=true)
16
+ maxAccounts: 10, // Maximum number of accounts allowed (from config)
16
17
  loading: false,
17
18
  initialLoad: true, // Track first load for skeleton screen
18
19
  connectionStatus: 'connecting',
@@ -125,11 +126,6 @@ document.addEventListener('alpine:init', () => {
125
126
  this.computeQuotaRows();
126
127
 
127
128
  this.lastUpdated = new Date().toLocaleTimeString();
128
-
129
- // Fetch version from config endpoint if not already loaded
130
- if (this.initialLoad) {
131
- this.fetchVersion(password);
132
- }
133
129
  } catch (error) {
134
130
  console.error('Fetch error:', error);
135
131
  const store = Alpine.store('global');
@@ -140,20 +136,6 @@ document.addEventListener('alpine:init', () => {
140
136
  }
141
137
  },
142
138
 
143
- async fetchVersion(password) {
144
- try {
145
- const { response } = await window.utils.request('/api/config', {}, password);
146
- if (response.ok) {
147
- const data = await response.json();
148
- if (data.version) {
149
- Alpine.store('global').version = data.version;
150
- }
151
- }
152
- } catch (error) {
153
- console.warn('Failed to fetch version:', error);
154
- }
155
- },
156
-
157
139
  async performHealthCheck() {
158
140
  try {
159
141
  // Get password from global store
@@ -30,6 +30,27 @@ document.addEventListener('alpine:init', () => {
30
30
  this.activeTab = hash;
31
31
  }
32
32
  });
33
+
34
+ // 4. Fetch version from API
35
+ this.fetchVersion();
36
+ },
37
+
38
+ async fetchVersion() {
39
+ try {
40
+ const response = await fetch('/api/config');
41
+ if (response.ok) {
42
+ const data = await response.json();
43
+ if (data.version) {
44
+ this.version = data.version;
45
+ }
46
+ // Update maxAccounts in data store
47
+ if (data.config && typeof data.config.maxAccounts === 'number') {
48
+ Alpine.store('data').maxAccounts = data.config.maxAccounts;
49
+ }
50
+ }
51
+ } catch (error) {
52
+ console.debug('Could not fetch version:', error);
53
+ }
33
54
  },
34
55
 
35
56
  // App State
@@ -39,8 +39,17 @@
39
39
  </svg>
40
40
  <span x-text="$store.global.t('reload')">Reload</span>
41
41
  </button>
42
+ <!-- Account Count Indicator -->
43
+ <div class="flex items-center h-6 px-2 rounded bg-space-800/80 border border-space-border/50"
44
+ :class="{ 'border-yellow-500/50': $store.data.accounts.length >= $store.data.maxAccounts }">
45
+ <span class="text-[11px] font-mono"
46
+ :class="$store.data.accounts.length >= $store.data.maxAccounts ? 'text-yellow-400' : 'text-gray-400'"
47
+ x-text="$store.data.accounts.length + '/' + $store.data.maxAccounts"></span>
48
+ </div>
42
49
  <button class="btn bg-neon-purple hover:bg-purple-600 border-none text-white btn-xs gap-2 shadow-lg shadow-neon-purple/20 h-8"
43
- onclick="document.getElementById('add_account_modal').showModal()">
50
+ :class="{ 'opacity-50 cursor-not-allowed': $store.data.accounts.length >= $store.data.maxAccounts }"
51
+ :disabled="$store.data.accounts.length >= $store.data.maxAccounts"
52
+ @click="$store.data.accounts.length < $store.data.maxAccounts && document.getElementById('add_account_modal').showModal()">
44
53
  <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
45
54
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
46
55
  </svg>
@@ -934,6 +934,28 @@
934
934
  </label>
935
935
  </div>
936
936
  </div>
937
+
938
+ <!-- Max Accounts -->
939
+ <div class="form-control view-card border-space-border/50 hover:border-neon-cyan/50">
940
+ <label class="label pt-0">
941
+ <span class="label-text text-gray-400 text-xs">Max Accounts</span>
942
+ <span class="label-text-alt font-mono text-neon-cyan text-xs font-semibold"
943
+ x-text="serverConfig.maxAccounts || 10"></span>
944
+ </label>
945
+ <div class="flex gap-3 items-center">
946
+ <input type="range" min="1" max="100" class="custom-range custom-range-cyan flex-1"
947
+ :value="serverConfig.maxAccounts || 10"
948
+ :style="`background-size: ${((serverConfig.maxAccounts || 10) - 1) / 99 * 100}% 100%`"
949
+ @input="toggleMaxAccounts($event.target.value)"
950
+ aria-label="Max accounts slider">
951
+ <input type="number" min="1" max="100"
952
+ class="input input-xs input-bordered w-16 bg-space-800 border-space-border text-white font-mono text-center"
953
+ :value="serverConfig.maxAccounts || 10"
954
+ @change="toggleMaxAccounts($event.target.value)"
955
+ aria-label="Max accounts value">
956
+ </div>
957
+ <span class="text-[11px] text-gray-500 mt-1">Maximum number of Google accounts allowed</span>
958
+ </div>
937
959
  </div>
938
960
 
939
961
  <!-- 🔀 Account Selection Strategy -->
@@ -279,8 +279,10 @@ export async function sendMessage(anthropicRequest, accountManager, fallbackEnab
279
279
 
280
280
  if (response.status >= 400) {
281
281
  lastError = new Error(`API error ${response.status}: ${errorText}`);
282
- // If it's a 5xx error, wait a bit before trying the next endpoint
283
- if (response.status >= 500) {
282
+ // Try next endpoint for 403/404/5xx errors (matches opencode-antigravity-auth behavior)
283
+ if (response.status === 403 || response.status === 404) {
284
+ logger.warn(`[CloudCode] ${response.status} at ${endpoint}...`);
285
+ } else if (response.status >= 500) {
284
286
  logger.warn(`[CloudCode] ${response.status} error, waiting 1s before retry...`);
285
287
  await sleep(1000);
286
288
  }
@@ -274,8 +274,10 @@ export async function* sendMessageStream(anthropicRequest, accountManager, fallb
274
274
 
275
275
  lastError = new Error(`API error ${response.status}: ${errorText}`);
276
276
 
277
- // If it's a 5xx error, wait a bit before trying the next endpoint
278
- if (response.status >= 500) {
277
+ // Try next endpoint for 403/404/5xx errors (matches opencode-antigravity-auth behavior)
278
+ if (response.status === 403 || response.status === 404) {
279
+ logger.warn(`[CloudCode] ${response.status} at ${endpoint}..`);
280
+ } else if (response.status >= 500) {
279
281
  logger.warn(`[CloudCode] ${response.status} stream error, waiting 1s before retry...`);
280
282
  await sleep(1000);
281
283
  }
package/src/config.js CHANGED
@@ -15,6 +15,7 @@ const DEFAULT_CONFIG = {
15
15
  persistTokenCache: false,
16
16
  defaultCooldownMs: 10000, // 10 seconds
17
17
  maxWaitBeforeErrorMs: 120000, // 2 minutes
18
+ maxAccounts: 10, // Maximum number of accounts allowed
18
19
  modelMapping: {},
19
20
  // Account selection strategy configuration
20
21
  accountSelection: {
package/src/server.js CHANGED
@@ -638,7 +638,7 @@ app.post('/refresh-token', async (req, res) => {
638
638
  app.get('/v1/models', async (req, res) => {
639
639
  try {
640
640
  await ensureInitialized();
641
- const account = accountManager.pickNext();
641
+ const { account } = accountManager.selectAccount();
642
642
  if (!account) {
643
643
  return res.status(503).json({
644
644
  type: 'error',
@@ -17,7 +17,7 @@ import { readFileSync } from 'fs';
17
17
  import { fileURLToPath } from 'url';
18
18
  import express from 'express';
19
19
  import { getPublicConfig, saveConfig, config } from '../config.js';
20
- import { DEFAULT_PORT, ACCOUNT_CONFIG_PATH } from '../constants.js';
20
+ import { DEFAULT_PORT, ACCOUNT_CONFIG_PATH, MAX_ACCOUNTS } from '../constants.js';
21
21
  import { readClaudeConfig, updateClaudeConfig, replaceClaudeConfig, getClaudeConfigPath, readPresets, savePreset, deletePreset } from '../utils/claude-config.js';
22
22
  import { logger } from '../utils/logger.js';
23
23
  import { getAuthorizationUrl, completeOAuthFlow, startCallbackServer } from '../auth/oauth.js';
@@ -77,6 +77,7 @@ async function removeAccount(email) {
77
77
 
78
78
  /**
79
79
  * Add new account to config
80
+ * @throws {Error} If MAX_ACCOUNTS limit is reached (for new accounts only)
80
81
  */
81
82
  async function addAccount(accountData) {
82
83
  const { accounts, settings, activeIndex } = await loadAccounts(ACCOUNT_CONFIG_PATH);
@@ -95,6 +96,10 @@ async function addAccount(accountData) {
95
96
  };
96
97
  logger.info(`[WebUI] Account ${accountData.email} updated`);
97
98
  } else {
99
+ // Check MAX_ACCOUNTS limit before adding new account
100
+ if (accounts.length >= MAX_ACCOUNTS) {
101
+ throw new Error(`Maximum of ${MAX_ACCOUNTS} accounts reached. Update maxAccounts in config to increase the limit.`);
102
+ }
98
103
  // Add new account
99
104
  accounts.push({
100
105
  ...accountData,
@@ -282,7 +287,7 @@ export function mountWebUI(app, dirname, accountManager) {
282
287
  */
283
288
  app.post('/api/config', (req, res) => {
284
289
  try {
285
- const { debug, logLevel, maxRetries, retryBaseMs, retryMaxMs, persistTokenCache, defaultCooldownMs, maxWaitBeforeErrorMs, accountSelection } = req.body;
290
+ const { debug, logLevel, maxRetries, retryBaseMs, retryMaxMs, persistTokenCache, defaultCooldownMs, maxWaitBeforeErrorMs, maxAccounts, accountSelection } = req.body;
286
291
 
287
292
  // Only allow updating specific fields (security)
288
293
  const updates = {};
@@ -308,6 +313,9 @@ export function mountWebUI(app, dirname, accountManager) {
308
313
  if (typeof maxWaitBeforeErrorMs === 'number' && maxWaitBeforeErrorMs >= 0 && maxWaitBeforeErrorMs <= 600000) {
309
314
  updates.maxWaitBeforeErrorMs = maxWaitBeforeErrorMs;
310
315
  }
316
+ if (typeof maxAccounts === 'number' && maxAccounts >= 1 && maxAccounts <= 100) {
317
+ updates.maxAccounts = maxAccounts;
318
+ }
311
319
  // Account selection strategy validation
312
320
  if (accountSelection && typeof accountSelection === 'object') {
313
321
  const validStrategies = ['sticky', 'round-robin', 'hybrid'];