antigravity-claude-proxy 3.0.0 → 3.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.
Files changed (53) hide show
  1. package/bin/cli.js +4 -11
  2. package/package.json +1 -1
  3. package/public/app.js +7 -4
  4. package/public/css/src/input.css +379 -216
  5. package/public/css/style.css +1 -1
  6. package/public/favicon.svg +4 -8
  7. package/public/index.html +78 -72
  8. package/public/js/components/claude-config.js +0 -8
  9. package/public/js/components/dashboard/charts.js +25 -51
  10. package/public/js/components/dashboard/stats.js +1 -1
  11. package/public/js/components/dashboard.js +16 -21
  12. package/public/js/components/logs-viewer.js +1 -1
  13. package/public/js/components/models.js +12 -12
  14. package/public/js/components/server-config.js +1 -9
  15. package/public/js/data-store.js +2 -2
  16. package/public/js/store.js +1 -1
  17. package/public/js/translations/en.js +37 -39
  18. package/public/js/translations/id.js +36 -37
  19. package/public/js/translations/pt.js +37 -38
  20. package/public/js/translations/tr.js +37 -39
  21. package/public/js/translations/zh.js +0 -2
  22. package/public/js/utils/error-handler.js +0 -2
  23. package/public/js/utils/ui-logger.js +4 -3
  24. package/public/views/accounts.html +34 -38
  25. package/public/views/dashboard.html +86 -99
  26. package/public/views/logs.html +4 -4
  27. package/public/views/models.html +32 -36
  28. package/public/views/settings.html +50 -50
  29. package/src/account-manager/index.js +0 -4
  30. package/src/account-manager/rate-limits.js +0 -5
  31. package/src/account-manager/strategies/base-strategy.js +4 -4
  32. package/src/account-manager/strategies/hybrid-strategy.js +3 -3
  33. package/src/auth/database.js +1 -0
  34. package/src/auth/oauth.js +43 -41
  35. package/src/auth/token-extractor.js +1 -1
  36. package/src/cli/accounts.js +2 -1
  37. package/src/cloudcode/model-registry.js +0 -3
  38. package/src/cloudcode/rate-limit-state.js +1 -7
  39. package/src/config.js +1 -1
  40. package/src/constants.js +0 -2
  41. package/src/format/content-converter.js +0 -4
  42. package/src/format/request-converter.js +1 -6
  43. package/src/format/response-converter.js +0 -3
  44. package/src/format/schema-sanitizer.js +3 -22
  45. package/src/format/thinking-utils.js +1 -13
  46. package/src/index.js +1 -6
  47. package/src/modules/usage-stats.js +1 -1
  48. package/src/server.js +1 -28
  49. package/src/utils/claude-config.js +2 -6
  50. package/src/utils/native-module-helper.js +0 -4
  51. package/src/utils/proxy.js +1 -10
  52. package/src/utils/version-detector.js +14 -5
  53. package/src/webui/index.js +1 -37
package/src/auth/oauth.js CHANGED
@@ -178,7 +178,7 @@ export function startCallbackServer(expectedState, timeoutMs = 120000) {
178
178
  let actualPort = OAUTH_CONFIG.callbackPort;
179
179
  const host = process.env.HOST || '0.0.0.0';
180
180
 
181
- const promise = new Promise(async (resolve, reject) => {
181
+ const promise = new Promise((resolve, reject) => {
182
182
  // Build list of ports to try: primary + fallbacks
183
183
  const portsToTry = [OAUTH_CONFIG.callbackPort, ...(OAUTH_CONFIG.callbackFallbackPorts || [])];
184
184
  const errors = [];
@@ -265,37 +265,38 @@ export function startCallbackServer(expectedState, timeoutMs = 120000) {
265
265
  });
266
266
 
267
267
  // Try ports with fallback logic (issue #176 - Windows EACCES fix)
268
- let boundSuccessfully = false;
269
- for (const port of portsToTry) {
270
- try {
271
- await tryBindPort(server, port, host);
272
- actualPort = port;
273
- boundSuccessfully = true;
274
-
275
- if (port !== OAUTH_CONFIG.callbackPort) {
276
- logger.warn(`[OAuth] Primary port ${OAUTH_CONFIG.callbackPort} unavailable, using fallback port ${port}`);
277
- } else {
278
- logger.info(`[OAuth] Callback server listening on ${host}:${port}`);
268
+ (async () => {
269
+ let boundSuccessfully = false;
270
+ for (const port of portsToTry) {
271
+ try {
272
+ await tryBindPort(server, port, host);
273
+ actualPort = port;
274
+ boundSuccessfully = true;
275
+
276
+ if (port !== OAUTH_CONFIG.callbackPort) {
277
+ logger.warn(`[OAuth] Primary port ${OAUTH_CONFIG.callbackPort} unavailable, using fallback port ${port}`);
278
+ } else {
279
+ logger.info(`[OAuth] Callback server listening on ${host}:${port}`);
280
+ }
281
+ break;
282
+ } catch (err) {
283
+ const errMsg = err.code === 'EACCES'
284
+ ? `Permission denied on port ${port}`
285
+ : err.code === 'EADDRINUSE'
286
+ ? `Port ${port} already in use`
287
+ : `Failed to bind port ${port}: ${err.message}`;
288
+ errors.push(errMsg);
289
+ logger.warn(`[OAuth] ${errMsg}`);
279
290
  }
280
- break;
281
- } catch (err) {
282
- const errMsg = err.code === 'EACCES'
283
- ? `Permission denied on port ${port}`
284
- : err.code === 'EADDRINUSE'
285
- ? `Port ${port} already in use`
286
- : `Failed to bind port ${port}: ${err.message}`;
287
- errors.push(errMsg);
288
- logger.warn(`[OAuth] ${errMsg}`);
289
291
  }
290
- }
291
292
 
292
- if (!boundSuccessfully) {
293
- // All ports failed - provide helpful error message
294
- const isWindows = process.platform === 'win32';
295
- let errorMsg = `Failed to start OAuth callback server.\nTried ports: ${portsToTry.join(', ')}\n\nErrors:\n${errors.join('\n')}`;
293
+ if (!boundSuccessfully) {
294
+ // All ports failed - provide helpful error message
295
+ const isWindows = process.platform === 'win32';
296
+ let errorMsg = `Failed to start OAuth callback server.\nTried ports: ${portsToTry.join(', ')}\n\nErrors:\n${errors.join('\n')}`;
296
297
 
297
- if (isWindows) {
298
- errorMsg += `\n
298
+ if (isWindows) {
299
+ errorMsg += `\n
299
300
  ================== WINDOWS TROUBLESHOOTING ==================
300
301
  The default port range may be reserved by Hyper-V/WSL2/Docker.
301
302
 
@@ -311,21 +312,22 @@ Option 3: Check reserved port ranges
311
312
  Option 4: Exclude port from reservation (run as Administrator)
312
313
  netsh int ipv4 add excludedportrange protocol=tcp startport=51121 numberofports=1
313
314
  ==============================================================`;
314
- } else {
315
- errorMsg += `\n\nTry setting a custom port: OAUTH_CALLBACK_PORT=3456`;
316
- }
317
-
318
- reject(new Error(errorMsg));
319
- return;
320
- }
315
+ } else {
316
+ errorMsg += `\n\nTry setting a custom port: OAUTH_CALLBACK_PORT=3456`;
317
+ }
321
318
 
322
- // Timeout after specified duration
323
- timeoutId = setTimeout(() => {
324
- if (!isAborted) {
325
- server.close();
326
- reject(new Error('OAuth callback timeout - no response received'));
319
+ reject(new Error(errorMsg));
320
+ return;
327
321
  }
328
- }, timeoutMs);
322
+
323
+ // Timeout after specified duration
324
+ timeoutId = setTimeout(() => {
325
+ if (!isAborted) {
326
+ server.close();
327
+ reject(new Error('OAuth callback timeout - no response received'));
328
+ }
329
+ }, timeoutMs);
330
+ })().catch(reject);
329
331
  });
330
332
 
331
333
  // Abort function to clean up server when manual completion happens
@@ -60,7 +60,7 @@ async function getTokenData() {
60
60
  return dbData;
61
61
  }
62
62
  } catch (err) {
63
- logger.warn('[Token] DB extraction failed, trying HTML page...');
63
+ logger.warn(`[Token] DB extraction failed (${err.message}), trying HTML page...`);
64
64
  }
65
65
 
66
66
  // Fallback to HTML page
@@ -50,7 +50,7 @@ function isServerRunning() {
50
50
  resolve(false);
51
51
  });
52
52
 
53
- socket.on('error', (err) => {
53
+ socket.on('error', () => {
54
54
  socket.destroy();
55
55
  resolve(false); // Port free
56
56
  });
@@ -447,6 +447,7 @@ async function verifyAccounts() {
447
447
  console.log(` ✓ ${email} - OK`);
448
448
  } catch (error) {
449
449
  console.log(` ✗ ${account.email} - ${error.message}`);
450
+ process.exitCode = 1;
450
451
  }
451
452
  }
452
453
  }
@@ -88,9 +88,6 @@ export function getRegisteredModel(modelId) {
88
88
  return registry.get(modelId);
89
89
  }
90
90
 
91
- /**
92
- * Clear all recorded metadata (used by tests).
93
- */
94
91
  export function clearModelRegistry() {
95
92
  registry.clear();
96
93
  }
@@ -1,10 +1,4 @@
1
- /**
2
- * Rate Limit State Management
3
- *
4
- * Shared utilities for rate limit tracking, backoff calculation,
5
- * and error classification. Used by both streaming and non-streaming handlers.
6
- */
7
-
1
+ // Rate limit tracking, backoff and error classification shared by streaming and non-streaming handlers.
8
2
  import {
9
3
  RATE_LIMIT_DEDUP_WINDOW_MS,
10
4
  RATE_LIMIT_STATE_RESET_MS,
package/src/config.js CHANGED
@@ -100,7 +100,7 @@ if (!fs.existsSync(CONFIG_DIR)) {
100
100
  try {
101
101
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
102
102
  } catch (err) {
103
- // Ignore
103
+ logger.warn(`[Config] Could not create config directory ${CONFIG_DIR}: ${err.message}`);
104
104
  }
105
105
  }
106
106
 
package/src/constants.js CHANGED
@@ -301,13 +301,11 @@ export const BUILTIN_MODEL_MAPPINGS = {
301
301
  'claude-sonnet-5': 'claude-sonnet-4-6'
302
302
  };
303
303
 
304
- // Default test models for each family (used by test suite)
305
304
  export const TEST_MODELS = {
306
305
  claude: 'claude-sonnet-4-6',
307
306
  gemini: 'gemini-3.8-flash-tiered'
308
307
  };
309
308
 
310
- // Default Claude CLI presets (used by WebUI settings)
311
309
  export const DEFAULT_PRESETS = [
312
310
  {
313
311
  name: 'Claude Thinking',
@@ -46,7 +46,6 @@ export function convertContentToParts(content, isClaudeModel = false, isGeminiMo
46
46
  parts.push({ text: block.text });
47
47
  }
48
48
  } else if (block.type === 'image') {
49
- // Handle image content
50
49
  if (block.source?.type === 'base64') {
51
50
  // Base64-encoded image
52
51
  parts.push({
@@ -93,7 +92,6 @@ export function convertContentToParts(content, isClaudeModel = false, isGeminiMo
93
92
  functionCall.id = block.id;
94
93
  }
95
94
 
96
- // Build the part with functionCall
97
95
  const part = { functionCall };
98
96
 
99
97
  // For Gemini models, include thoughtSignature at the part level
@@ -133,7 +131,6 @@ export function convertContentToParts(content, isClaudeModel = false, isGeminiMo
133
131
  }
134
132
  }
135
133
 
136
- // Extract text content
137
134
  const texts = responseContent
138
135
  .filter(c => c.type === 'text')
139
136
  .map(c => c.text)
@@ -157,7 +154,6 @@ export function convertContentToParts(content, isClaudeModel = false, isGeminiMo
157
154
  // This ensures all functionResponse parts are consecutive
158
155
  deferredInlineData.push(...imageParts);
159
156
  } else if (block.type === 'thinking') {
160
- // Handle thinking blocks with signature compatibility check
161
157
  if (block.signature && block.signature.length >= MIN_SIGNATURE_LENGTH) {
162
158
  const signatureFamily = getCachedSignatureFamily(block.signature);
163
159
  const targetFamily = isClaudeModel ? 'claude' : isGeminiModel ? 'gemini' : null;
@@ -51,7 +51,7 @@ export function convertAnthropicToGoogle(anthropicRequest) {
51
51
  // before any other processing, following the pattern from Antigravity-Manager.
52
52
  const messages = cleanCacheControl(anthropicRequest.messages || []);
53
53
 
54
- const { system, max_tokens, temperature, top_p, top_k, stop_sequences, tools, tool_choice, thinking } = anthropicRequest;
54
+ const { system, max_tokens, temperature, top_p, top_k, stop_sequences, tools, thinking } = anthropicRequest;
55
55
  const modelName = anthropicRequest.model || '';
56
56
  const modelFamily = getModelFamily(modelName);
57
57
  const isClaudeModel = modelFamily === 'claude';
@@ -63,7 +63,6 @@ export function convertAnthropicToGoogle(anthropicRequest) {
63
63
  generationConfig: {}
64
64
  };
65
65
 
66
- // Handle system instruction
67
66
  if (system) {
68
67
  let systemParts = [];
69
68
  if (typeof system === 'string') {
@@ -126,7 +125,6 @@ export function convertAnthropicToGoogle(anthropicRequest) {
126
125
  if ((msg.role === 'assistant' || msg.role === 'model') && Array.isArray(msgContent)) {
127
126
  // First, try to restore signatures for unsigned thinking blocks from cache
128
127
  msgContent = restoreThinkingSignatures(msgContent);
129
- // Remove trailing unsigned thinking blocks
130
128
  msgContent = removeTrailingThinkingBlocks(msgContent);
131
129
  // Reorder: thinking first, then text, then tool_use
132
130
  msgContent = reorderAssistantContent(msgContent);
@@ -228,13 +226,10 @@ export function convertAnthropicToGoogle(anthropicRequest) {
228
226
  // Convert tools to Google format
229
227
  if (tools && tools.length > 0) {
230
228
  const functionDeclarations = tools.map((tool, idx) => {
231
- // Extract name from various possible locations
232
229
  const name = tool.name || tool.function?.name || tool.custom?.name || `tool-${idx}`;
233
230
 
234
- // Extract description from various possible locations
235
231
  const description = tool.description || tool.function?.description || tool.custom?.description || '';
236
232
 
237
- // Extract schema from various possible locations
238
233
  const schema = tool.input_schema
239
234
  || tool.function?.input_schema
240
235
  || tool.function?.parameters
@@ -15,7 +15,6 @@ import { cacheSignature, cacheThinkingSignature } from './signature-cache.js';
15
15
  * @returns {Object} Anthropic format response
16
16
  */
17
17
  export function convertGoogleToAnthropic(googleResponse, model) {
18
- // Handle the response wrapper
19
18
  const response = googleResponse.response || googleResponse;
20
19
 
21
20
  const candidates = response.candidates || [];
@@ -29,7 +28,6 @@ export function convertGoogleToAnthropic(googleResponse, model) {
29
28
 
30
29
  for (const part of parts) {
31
30
  if (part.text !== undefined) {
32
- // Handle thinking blocks
33
31
  if (part.thought === true) {
34
32
  const signature = part.thoughtSignature || '';
35
33
 
@@ -72,7 +70,6 @@ export function convertGoogleToAnthropic(googleResponse, model) {
72
70
  anthropicContent.push(toolUseBlock);
73
71
  hasToolCalls = true;
74
72
  } else if (part.inlineData) {
75
- // Handle image content from Google format
76
73
  anthropicContent.push({
77
74
  type: 'image',
78
75
  source: {
@@ -163,7 +163,7 @@ function mergeAllOf(schema) {
163
163
 
164
164
  // Merge properties (allOf properties override parent for same keys)
165
165
  if (Object.keys(mergedProperties).length > 0) {
166
- result.properties = { ...mergedProperties, ...(result.properties || {}) };
166
+ result.properties = { ...mergedProperties, ...result.properties };
167
167
  }
168
168
 
169
169
  // Merge required
@@ -289,10 +289,6 @@ function flattenAnyOfOneOf(schema) {
289
289
  return result;
290
290
  }
291
291
 
292
- // ============================================================================
293
- // Enhanced Schema Hints (for preserving semantic information)
294
- // ============================================================================
295
-
296
292
  /**
297
293
  * Add hints for enum values (if ≤10 values).
298
294
  * This preserves enum information in the description since Gemini
@@ -611,7 +607,6 @@ export function cleanSchema(schema) {
611
607
  if (!schema || typeof schema !== 'object') return schema;
612
608
  if (Array.isArray(schema)) return schema.map(cleanSchema);
613
609
 
614
- // Phase 1: Convert $refs to hints
615
610
  let result = convertRefsToHints(schema);
616
611
 
617
612
  // Phase 1b: Add enum hints (preserves enum info in description)
@@ -632,7 +627,6 @@ export function cleanSchema(schema) {
632
627
  // Phase 2c: Flatten type arrays and update required for nullable
633
628
  result = flattenTypeArrays(result);
634
629
 
635
- // Phase 3: Remove unsupported keywords
636
630
  const unsupported = [
637
631
  'additionalProperties', 'default', '$schema', '$defs',
638
632
  'definitions', '$ref', '$id', '$comment', 'title',
@@ -652,7 +646,6 @@ export function cleanSchema(schema) {
652
646
  }
653
647
  }
654
648
 
655
- // Phase 4: Final cleanup - recursively clean nested schemas and validate required
656
649
  if (result.properties && typeof result.properties === 'object') {
657
650
  const newProps = {};
658
651
  for (const [key, value] of Object.entries(result.properties)) {
@@ -678,21 +671,11 @@ export function cleanSchema(schema) {
678
671
  }
679
672
  }
680
673
 
681
- // Phase 5: Convert type to Google's uppercase format (STRING, OBJECT, ARRAY, etc.)
682
- // Only convert at current level - nested types already converted by recursive cleanSchema calls
683
674
  if (result.type && typeof result.type === 'string') {
684
675
  result.type = toGoogleType(result.type);
685
676
  }
686
677
 
687
- // Phase 6: Google's protobuf-based schema requires every ARRAY type to
688
- // declare a single object `items` schema. Plain JSON Schema allows this
689
- // to be omitted (`{ type: 'array' }` = "array of anything"), boolean
690
- // (`items: true` = "any"), or tuple-form (`items: [...]`) - none of
691
- // which the Cloud Code API accepts; it rejects the first two with
692
- // "items: missing field". This runs after the items/properties
693
- // recursion above (and after Phase 5's type conversion), so it applies
694
- // at every nesting depth - fixing issue #368 (nested array schemas like
695
- // `where.items.items` losing their innermost `items`).
678
+ // Cloud Code rejects ARRAY schemas without a single object `items` ("items: missing field"), issue #368.
696
679
  if (result.type === 'ARRAY') {
697
680
  if (!result.items || typeof result.items !== 'object') {
698
681
  // Missing entirely, or a boolean schema like `items: true`.
@@ -713,9 +696,7 @@ export function cleanSchema(schema) {
713
696
  }
714
697
  }
715
698
 
716
- // Phase 7: Google rejects `properties`/`required` on a non-OBJECT schema.
717
- // These can survive on the current node after anyOf/oneOf flattening
718
- // picked a non-object option while the parent still carried them.
699
+ // Google rejects properties/required on non-OBJECT nodes, which anyOf/oneOf flattening can leave behind.
719
700
  if (result.type !== 'OBJECT') {
720
701
  delete result.properties;
721
702
  delete result.required;
@@ -7,10 +7,6 @@ import { MIN_SIGNATURE_LENGTH } from '../constants.js';
7
7
  import { getCachedSignature, getCachedSignatureFamily } from './signature-cache.js';
8
8
  import { logger } from '../utils/logger.js';
9
9
 
10
- // ============================================================================
11
- // Gemini Thinking Budget Limits (Issue #289)
12
- // ============================================================================
13
-
14
10
  // Max thinking budget per Gemini model version
15
11
  // Gemini 2.5 Flash: max 24,576 (API error: "supported values are integers from 1 to 24576")
16
12
  const GEMINI_THINKING_BUDGET_LIMITS = {
@@ -57,10 +53,6 @@ export function clampGeminiThinkingBudget(modelName, budget) {
57
53
  return requestedBudget;
58
54
  }
59
55
 
60
- // ============================================================================
61
- // Cache Control Cleaning (Issue #189)
62
- // ============================================================================
63
-
64
56
  /**
65
57
  * Remove cache_control fields from all content blocks in messages.
66
58
  * This is a critical fix for Issue #189 where Claude Code CLI sends cache_control
@@ -94,7 +86,7 @@ export function cleanCacheControl(messages) {
94
86
  if (block.cache_control === undefined) return block;
95
87
 
96
88
  // Create a shallow copy without cache_control
97
- const { cache_control, ...cleanBlock } = block;
89
+ const { cache_control: _cacheControl, ...cleanBlock } = block;
98
90
  removedCount++;
99
91
 
100
92
  return cleanBlock;
@@ -431,10 +423,6 @@ export function reorderAssistantContent(content) {
431
423
  return reordered;
432
424
  }
433
425
 
434
- // ============================================================================
435
- // Thinking Recovery Functions
436
- // ============================================================================
437
-
438
426
  /**
439
427
  * Check if a message has any VALID (signed) thinking blocks.
440
428
  * Only counts thinking blocks that have valid signatures, not unsigned ones
package/src/index.js CHANGED
@@ -10,14 +10,13 @@ import app, { accountManager } from './server.js';
10
10
  import { DEFAULT_PORT } from './constants.js';
11
11
  import { logger } from './utils/logger.js';
12
12
  import { config } from './config.js';
13
- import { getStrategyLabel, STRATEGY_NAMES, DEFAULT_STRATEGY } from './account-manager/strategies/index.js';
13
+ import { STRATEGY_NAMES } from './account-manager/strategies/index.js';
14
14
  import { getPackageVersion } from './utils/helpers.js';
15
15
  import path from 'path';
16
16
  import os from 'os';
17
17
 
18
18
  const packageVersion = getPackageVersion();
19
19
 
20
- // Parse command line arguments
21
20
  const args = process.argv.slice(2);
22
21
  const isDebug = args.includes('--debug') || args.includes('--dev-mode') || process.env.DEBUG === 'true' || process.env.DEV_MODE === 'true';
23
22
  const isFallbackEnabled = args.includes('--fallback') || process.env.FALLBACK === 'true';
@@ -31,13 +30,11 @@ for (let i = 0; i < args.length; i++) {
31
30
  strategyOverride = args[i + 1];
32
31
  }
33
32
  }
34
- // Validate strategy
35
33
  if (strategyOverride && !STRATEGY_NAMES.includes(strategyOverride.toLowerCase())) {
36
34
  logger.warn(`[Startup] Invalid strategy "${strategyOverride}". Valid options: ${STRATEGY_NAMES.join(', ')}. Using default.`);
37
35
  strategyOverride = null;
38
36
  }
39
37
 
40
- // Initialize logger and devMode
41
38
  logger.setDebug(isDebug);
42
39
 
43
40
  if (isDebug) {
@@ -65,7 +62,6 @@ const HOME_DIR = os.homedir();
65
62
  const CONFIG_DIR = path.join(HOME_DIR, '.antigravity-claude-proxy');
66
63
 
67
64
  const server = app.listen(PORT, HOST, () => {
68
- // Get actual bound address
69
65
  const address = server.address();
70
66
  const boundHost = typeof address === 'string' ? address : address.address;
71
67
  const boundPort = typeof address === 'string' ? null : address.port;
@@ -78,7 +74,6 @@ const server = app.listen(PORT, HOST, () => {
78
74
  const align = (text) => text + ' '.repeat(Math.max(0, 60 - text.length));
79
75
  const align4 = (text) => text + ' '.repeat(Math.max(0, 58 - text.length));
80
76
 
81
- // Build Control section dynamically
82
77
  const strategyOptions = `(${STRATEGY_NAMES.join('/')})`;
83
78
  const strategyLine2 = ' ' + strategyOptions;
84
79
  let controlSection = '║ Control: ║\n';
@@ -179,7 +179,7 @@ function track(modelId) {
179
179
  * Setup Express Middleware
180
180
  * @param {import('express').Application} app
181
181
  */
182
- function setupMiddleware(app) {
182
+ function setupMiddleware(_app) {
183
183
  load();
184
184
 
185
185
  // Auto-save every minute
package/src/server.js CHANGED
@@ -46,7 +46,6 @@ export const accountManager = new AccountManager();
46
46
 
47
47
  // Track initialization status
48
48
  let isInitialized = false;
49
- let initError = null;
50
49
  let initPromise = null;
51
50
 
52
51
  /**
@@ -65,7 +64,6 @@ async function ensureInitialized() {
65
64
  const status = accountManager.getStatus();
66
65
  logger.success(`[Server] Account pool initialized: ${status.summary}`);
67
66
  } catch (error) {
68
- initError = error;
69
67
  initPromise = null; // Allow retry on failure
70
68
  logger.error('[Server] Failed to initialize account manager:', error.message);
71
69
  throw error;
@@ -116,7 +114,6 @@ app.use('/v1', (req, res, next) => {
116
114
  next();
117
115
  });
118
116
 
119
- // Setup usage statistics middleware
120
117
  usageStats.setupMiddleware(app);
121
118
 
122
119
  /**
@@ -125,11 +122,9 @@ usageStats.setupMiddleware(app);
125
122
  * Using app.use instead of app.post for earlier middleware interception
126
123
  */
127
124
  app.use((req, res, next) => {
128
- // Handle Claude Code event logging requests silently
129
125
  if (req.method === 'POST' && req.path === '/api/event_logging/batch') {
130
126
  return res.status(200).json({ status: 'ok' });
131
127
  }
132
- // Handle Claude Code root POST requests silently
133
128
  if (req.method === 'POST' && req.path === '/') {
134
129
  return res.status(200).json({ status: 'ok' });
135
130
  }
@@ -178,7 +173,6 @@ function parseError(error) {
178
173
  } else if (error.message.includes('PERMISSION_DENIED')) {
179
174
  errorType = 'permission_error';
180
175
  statusCode = 403;
181
- errorMessage = errorMessage;
182
176
  }
183
177
 
184
178
  return { errorType, statusCode, errorMessage };
@@ -188,7 +182,6 @@ function parseError(error) {
188
182
  app.use((req, res, next) => {
189
183
  const start = Date.now();
190
184
 
191
- // Log response on finish
192
185
  res.on('finish', () => {
193
186
  const duration = Date.now() - start;
194
187
  const status = res.statusCode;
@@ -241,14 +234,12 @@ app.get('/health', async (req, res) => {
241
234
  await ensureInitialized();
242
235
  const start = Date.now();
243
236
 
244
- // Get high-level status first
245
237
  const status = accountManager.getStatus();
246
238
  const allAccounts = accountManager.getAllAccounts();
247
239
 
248
240
  // Fetch quotas for each account in parallel to get detailed model info
249
241
  const accountDetails = await Promise.allSettled(
250
242
  allAccounts.map(async (account) => {
251
- // Check model-specific rate limits
252
243
  const activeModelLimits = Object.entries(account.modelRateLimits || {})
253
244
  .filter(([_, limit]) => limit.isRateLimited && limit.resetTime > Date.now());
254
245
  const isRateLimited = activeModelLimits.length > 0;
@@ -306,7 +297,6 @@ app.get('/health', async (req, res) => {
306
297
  })
307
298
  );
308
299
 
309
- // Process results
310
300
  const detailedAccounts = accountDetails.map((result, index) => {
311
301
  if (result.status === 'fulfilled') {
312
302
  return result.value;
@@ -357,7 +347,6 @@ app.get('/account-limits', async (req, res) => {
357
347
  const format = req.query.format || 'json';
358
348
  const includeHistory = req.query.includeHistory === 'true';
359
349
 
360
- // Fetch quotas for each account in parallel
361
350
  const results = await Promise.allSettled(
362
351
  allAccounts.map(async (account) => {
363
352
  // Skip invalid accounts
@@ -373,13 +362,11 @@ app.get('/account-limits', async (req, res) => {
373
362
  try {
374
363
  const token = await accountManager.getTokenForAccount(account);
375
364
 
376
- // Fetch subscription tier first to get project ID
377
365
  const subscription = await getSubscriptionTier(token);
378
366
 
379
367
  // Then fetch quotas with project ID for accurate quota info
380
368
  const quotas = await getModelQuotas(token, subscription.projectId);
381
369
 
382
- // Update account object with fresh data
383
370
  account.subscription = {
384
371
  tier: subscription.tier,
385
372
  projectId: subscription.projectId,
@@ -424,7 +411,6 @@ app.get('/account-limits', async (req, res) => {
424
411
  })
425
412
  );
426
413
 
427
- // Process results
428
414
  const accountLimits = results.map((result, index) => {
429
415
  if (result.status === 'fulfilled') {
430
416
  return result.value;
@@ -448,16 +434,13 @@ app.get('/account-limits', async (req, res) => {
448
434
 
449
435
  const sortedModels = Array.from(allModelIds).sort();
450
436
 
451
- // Return ASCII table format
452
437
  if (format === 'table') {
453
438
  res.setHeader('Content-Type', 'text/plain; charset=utf-8');
454
439
 
455
- // Build table
456
440
  const lines = [];
457
441
  const timestamp = new Date().toLocaleString();
458
442
  lines.push(`Account Limits (${timestamp})`);
459
443
 
460
- // Get account status info
461
444
  const status = accountManager.getStatus();
462
445
  lines.push(`Accounts: ${status.total} total, ${status.available} available, ${status.rateLimited} rate-limited, ${status.invalid} invalid`);
463
446
  lines.push('');
@@ -476,7 +459,6 @@ app.get('/account-limits', async (req, res) => {
476
459
  const shortEmail = acc.email.split('@')[0].slice(0, 22);
477
460
  const lastUsed = acc.lastUsed ? new Date(acc.lastUsed).toLocaleString() : 'never';
478
461
 
479
- // Get status and error from accountLimits
480
462
  const accLimit = accountLimits.find(a => a.email === acc.email);
481
463
  let accStatus;
482
464
  if (acc.isInvalid) {
@@ -498,7 +480,6 @@ app.get('/account-limits', async (req, res) => {
498
480
  }
499
481
  }
500
482
 
501
- // Get reset time from quota API
502
483
  const claudeModel = sortedModels.find(m => m.includes('claude'));
503
484
  const quota = claudeModel && accLimit?.models?.[claudeModel];
504
485
  const resetTime = quota?.resetTime
@@ -564,13 +545,11 @@ app.get('/account-limits', async (req, res) => {
564
545
  return res.send(lines.join('\n'));
565
546
  }
566
547
 
567
- // Get account metadata from AccountManager
568
548
  const accountStatus = accountManager.getStatus();
569
549
  const accountMetadataMap = new Map(
570
550
  accountStatus.accounts.map(a => [a.email, a])
571
551
  );
572
552
 
573
- // Build response data
574
553
  const responseData = {
575
554
  timestamp: new Date().toLocaleString(),
576
555
  totalAccounts: allAccounts.length,
@@ -705,7 +684,6 @@ app.post('/v1/messages/count_tokens', (req, res) => {
705
684
  * Main messages endpoint - Anthropic Messages API compatible
706
685
  */
707
686
 
708
-
709
687
  /**
710
688
  * Anthropic-compatible Messages API
711
689
  * POST /v1/messages
@@ -744,7 +722,6 @@ app.post('/v1/messages', async (req, res) => {
744
722
 
745
723
  const modelId = requestedModel;
746
724
 
747
- // Validate model ID before processing
748
725
  const { account: validationAccount } = accountManager.selectAccount();
749
726
  if (validationAccount) {
750
727
  const token = await accountManager.getTokenForAccount(validationAccount);
@@ -763,7 +740,6 @@ app.post('/v1/messages', async (req, res) => {
763
740
  accountManager.resetAllRateLimits();
764
741
  }
765
742
 
766
- // Validate required fields
767
743
  if (!messages || !Array.isArray(messages)) {
768
744
  return res.status(400).json({
769
745
  type: 'error',
@@ -779,7 +755,6 @@ app.post('/v1/messages', async (req, res) => {
779
755
  return res.json({});
780
756
  }
781
757
 
782
- // Build the request object
783
758
  const request = {
784
759
  model: modelId,
785
760
  messages,
@@ -813,7 +788,6 @@ app.post('/v1/messages', async (req, res) => {
813
788
  // to ensure we don't send a 200 OK if the upstream fails immediately (e.g. 429/503).
814
789
 
815
790
  try {
816
- // Initialize the generator
817
791
  const generator = sendMessageStream(request, accountManager, FALLBACK_ENABLED);
818
792
 
819
793
  // BUFFERING STRATEGY:
@@ -872,7 +846,6 @@ app.post('/v1/messages', async (req, res) => {
872
846
  }
873
847
 
874
848
  } else {
875
- // Handle non-streaming response
876
849
  const response = await sendMessage(request, accountManager, FALLBACK_ENABLED);
877
850
  usageStats.track(response?.model || modelId);
878
851
  res.json(response);
@@ -892,6 +865,7 @@ app.post('/v1/messages', async (req, res) => {
892
865
  await forceRefresh();
893
866
  errorMessage = 'Token was expired and has been refreshed. Please retry your request.';
894
867
  } catch (refreshError) {
868
+ logger.warn(`[API] Token refresh failed: ${refreshError.message}`);
895
869
  errorMessage = 'Could not refresh token. Make sure Antigravity is running.';
896
870
  }
897
871
  }
@@ -924,7 +898,6 @@ app.post('/v1/messages', async (req, res) => {
924
898
  usageStats.setupRoutes(app);
925
899
 
926
900
  app.use((req, res) => {
927
- // Log 404s
928
901
  if (logger.isDebugEnabled) {
929
902
  logger.debug(`[API] 404 Not Found: ${req.method} ${req.originalUrl}`);
930
903
  }