@kamel-ahmed/proxy-claude 1.0.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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +622 -0
  3. package/bin/cli.js +124 -0
  4. package/package.json +80 -0
  5. package/public/app.js +228 -0
  6. package/public/css/src/input.css +523 -0
  7. package/public/css/style.css +1 -0
  8. package/public/favicon.svg +10 -0
  9. package/public/index.html +381 -0
  10. package/public/js/components/account-manager.js +245 -0
  11. package/public/js/components/claude-config.js +420 -0
  12. package/public/js/components/dashboard/charts.js +589 -0
  13. package/public/js/components/dashboard/filters.js +362 -0
  14. package/public/js/components/dashboard/stats.js +110 -0
  15. package/public/js/components/dashboard.js +236 -0
  16. package/public/js/components/logs-viewer.js +100 -0
  17. package/public/js/components/models.js +36 -0
  18. package/public/js/components/server-config.js +349 -0
  19. package/public/js/config/constants.js +102 -0
  20. package/public/js/data-store.js +386 -0
  21. package/public/js/settings-store.js +58 -0
  22. package/public/js/store.js +78 -0
  23. package/public/js/translations/en.js +351 -0
  24. package/public/js/translations/id.js +396 -0
  25. package/public/js/translations/pt.js +287 -0
  26. package/public/js/translations/tr.js +342 -0
  27. package/public/js/translations/zh.js +357 -0
  28. package/public/js/utils/account-actions.js +189 -0
  29. package/public/js/utils/error-handler.js +96 -0
  30. package/public/js/utils/model-config.js +42 -0
  31. package/public/js/utils/validators.js +77 -0
  32. package/public/js/utils.js +69 -0
  33. package/public/views/accounts.html +329 -0
  34. package/public/views/dashboard.html +484 -0
  35. package/public/views/logs.html +97 -0
  36. package/public/views/models.html +331 -0
  37. package/public/views/settings.html +1329 -0
  38. package/src/account-manager/credentials.js +243 -0
  39. package/src/account-manager/index.js +380 -0
  40. package/src/account-manager/onboarding.js +117 -0
  41. package/src/account-manager/rate-limits.js +237 -0
  42. package/src/account-manager/storage.js +136 -0
  43. package/src/account-manager/strategies/base-strategy.js +104 -0
  44. package/src/account-manager/strategies/hybrid-strategy.js +195 -0
  45. package/src/account-manager/strategies/index.js +79 -0
  46. package/src/account-manager/strategies/round-robin-strategy.js +76 -0
  47. package/src/account-manager/strategies/sticky-strategy.js +138 -0
  48. package/src/account-manager/strategies/trackers/health-tracker.js +162 -0
  49. package/src/account-manager/strategies/trackers/index.js +8 -0
  50. package/src/account-manager/strategies/trackers/token-bucket-tracker.js +121 -0
  51. package/src/auth/database.js +169 -0
  52. package/src/auth/oauth.js +419 -0
  53. package/src/auth/token-extractor.js +117 -0
  54. package/src/cli/accounts.js +512 -0
  55. package/src/cli/refresh.js +201 -0
  56. package/src/cli/setup.js +338 -0
  57. package/src/cloudcode/index.js +29 -0
  58. package/src/cloudcode/message-handler.js +386 -0
  59. package/src/cloudcode/model-api.js +248 -0
  60. package/src/cloudcode/rate-limit-parser.js +181 -0
  61. package/src/cloudcode/request-builder.js +93 -0
  62. package/src/cloudcode/session-manager.js +47 -0
  63. package/src/cloudcode/sse-parser.js +121 -0
  64. package/src/cloudcode/sse-streamer.js +293 -0
  65. package/src/cloudcode/streaming-handler.js +492 -0
  66. package/src/config.js +107 -0
  67. package/src/constants.js +278 -0
  68. package/src/errors.js +238 -0
  69. package/src/fallback-config.js +29 -0
  70. package/src/format/content-converter.js +193 -0
  71. package/src/format/index.js +20 -0
  72. package/src/format/request-converter.js +248 -0
  73. package/src/format/response-converter.js +120 -0
  74. package/src/format/schema-sanitizer.js +673 -0
  75. package/src/format/signature-cache.js +88 -0
  76. package/src/format/thinking-utils.js +558 -0
  77. package/src/index.js +146 -0
  78. package/src/modules/usage-stats.js +205 -0
  79. package/src/server.js +861 -0
  80. package/src/utils/claude-config.js +245 -0
  81. package/src/utils/helpers.js +51 -0
  82. package/src/utils/logger.js +142 -0
  83. package/src/utils/native-module-helper.js +162 -0
  84. package/src/webui/index.js +707 -0
package/src/index.js ADDED
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Antigravity Claude Proxy
3
+ * Entry point - starts the proxy server
4
+ */
5
+
6
+ import app, { accountManager } from './server.js';
7
+ import { DEFAULT_PORT } from './constants.js';
8
+ import { logger } from './utils/logger.js';
9
+ import { getStrategyLabel, STRATEGY_NAMES, DEFAULT_STRATEGY } from './account-manager/strategies/index.js';
10
+ import path from 'path';
11
+ import os from 'os';
12
+
13
+ // Parse command line arguments
14
+ const args = process.argv.slice(2);
15
+ const isDebug = args.includes('--debug') || process.env.DEBUG === 'true';
16
+ const isFallbackEnabled = args.includes('--fallback') || process.env.FALLBACK === 'true';
17
+
18
+ // Parse --strategy flag (format: --strategy=sticky or --strategy sticky)
19
+ let strategyOverride = null;
20
+ for (let i = 0; i < args.length; i++) {
21
+ if (args[i].startsWith('--strategy=')) {
22
+ strategyOverride = args[i].split('=')[1];
23
+ } else if (args[i] === '--strategy' && args[i + 1]) {
24
+ strategyOverride = args[i + 1];
25
+ }
26
+ }
27
+ // Validate strategy
28
+ if (strategyOverride && !STRATEGY_NAMES.includes(strategyOverride.toLowerCase())) {
29
+ logger.warn(`[Startup] Invalid strategy "${strategyOverride}". Valid options: ${STRATEGY_NAMES.join(', ')}. Using default.`);
30
+ strategyOverride = null;
31
+ }
32
+
33
+ // Initialize logger
34
+ logger.setDebug(isDebug);
35
+
36
+ if (isDebug) {
37
+ logger.debug('Debug mode enabled');
38
+ }
39
+
40
+ if (isFallbackEnabled) {
41
+ logger.info('Model fallback mode enabled');
42
+ }
43
+
44
+ // Export fallback flag for server to use
45
+ export const FALLBACK_ENABLED = isFallbackEnabled;
46
+
47
+ const PORT = process.env.PORT || DEFAULT_PORT;
48
+
49
+ // Home directory for account storage
50
+ const HOME_DIR = os.homedir();
51
+ const CONFIG_DIR = path.join(HOME_DIR, '.antigravity-claude-proxy');
52
+
53
+ const server = app.listen(PORT, () => {
54
+ // Clear console for a clean start
55
+ console.clear();
56
+
57
+ const border = '║';
58
+ // align for 2-space indent (60 chars), align4 for 4-space indent (58 chars)
59
+ const align = (text) => text + ' '.repeat(Math.max(0, 60 - text.length));
60
+ const align4 = (text) => text + ' '.repeat(Math.max(0, 58 - text.length));
61
+
62
+ // Build Control section dynamically
63
+ const strategyOptions = `(${STRATEGY_NAMES.join('/')})`;
64
+ const strategyLine2 = ' ' + strategyOptions;
65
+ let controlSection = '║ Control: ║\n';
66
+ controlSection += '║ --strategy=<s> Set account selection strategy ║\n';
67
+ controlSection += `${border} ${align(strategyLine2)}${border}\n`;
68
+ if (!isDebug) {
69
+ controlSection += '║ --debug Enable debug logging ║\n';
70
+ }
71
+ if (!isFallbackEnabled) {
72
+ controlSection += '║ --fallback Enable model fallback on quota exhaust ║\n';
73
+ }
74
+ controlSection += '║ Ctrl+C Stop server ║';
75
+
76
+ // Get the strategy label (accountManager will be initialized by now)
77
+ const strategyLabel = accountManager.getStrategyLabel();
78
+
79
+ // Build status section - always show strategy, plus any active modes
80
+ let statusSection = '║ ║\n';
81
+ statusSection += '║ Active Modes: ║\n';
82
+ statusSection += `${border} ${align4(`✓ Strategy: ${strategyLabel}`)}${border}\n`;
83
+ if (isDebug) {
84
+ statusSection += '║ ✓ Debug mode enabled ║\n';
85
+ }
86
+ if (isFallbackEnabled) {
87
+ statusSection += '║ ✓ Model fallback enabled ║\n';
88
+ }
89
+
90
+ logger.log(`
91
+ ╔══════════════════════════════════════════════════════════════╗
92
+ ║ Antigravity Claude Proxy Server ║
93
+ ╠══════════════════════════════════════════════════════════════╣
94
+ ║ ║
95
+ ${border} ${align(`Server and WebUI running at: http://localhost:${PORT}`)}${border}
96
+ ${statusSection}║ ║
97
+ ${controlSection}
98
+ ║ ║
99
+ ║ Endpoints: ║
100
+ ║ POST /v1/messages - Anthropic Messages API ║
101
+ ║ GET /v1/models - List available models ║
102
+ ║ GET /health - Health check ║
103
+ ║ GET /account-limits - Account status & quotas ║
104
+ ║ POST /refresh-token - Force token refresh ║
105
+ ║ ║
106
+ ${border} ${align(`Configuration:`)}${border}
107
+ ${border} ${align4(`Storage: ${CONFIG_DIR}`)}${border}
108
+ ║ ║
109
+ ║ Usage with Claude Code: ║
110
+ ${border} ${align4(`export ANTHROPIC_BASE_URL=http://localhost:${PORT}`)}${border}
111
+ ║ export ANTHROPIC_API_KEY=dummy ║
112
+ ║ claude ║
113
+ ║ ║
114
+ ║ Add Google accounts: ║
115
+ ║ npm run accounts ║
116
+ ║ ║
117
+ ║ Prerequisites (if no accounts configured): ║
118
+ ║ - Antigravity must be running ║
119
+ ║ - Have a chat panel open in Antigravity ║
120
+ ║ ║
121
+ ╚══════════════════════════════════════════════════════════════╝
122
+ `);
123
+
124
+ logger.success(`Server started successfully on port ${PORT}`);
125
+ if (isDebug) {
126
+ logger.warn('Running in DEBUG mode - verbose logs enabled');
127
+ }
128
+ });
129
+
130
+ // Graceful shutdown
131
+ const shutdown = () => {
132
+ logger.info('Shutting down server...');
133
+ server.close(() => {
134
+ logger.success('Server stopped');
135
+ process.exit(0);
136
+ });
137
+
138
+ // Force close if it takes too long
139
+ setTimeout(() => {
140
+ logger.error('Could not close connections in time, forcefully shutting down');
141
+ process.exit(1);
142
+ }, 10000);
143
+ };
144
+
145
+ process.on('SIGTERM', shutdown);
146
+ process.on('SIGINT', shutdown);
@@ -0,0 +1,205 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ import { USAGE_HISTORY_PATH } from '../constants.js';
5
+
6
+ // Persistence path
7
+ const HISTORY_FILE = USAGE_HISTORY_PATH;
8
+ const DATA_DIR = path.dirname(HISTORY_FILE);
9
+ const OLD_DATA_DIR = path.join(process.cwd(), 'data');
10
+ const OLD_HISTORY_FILE = path.join(OLD_DATA_DIR, 'usage-history.json');
11
+
12
+ // In-memory storage
13
+ // Structure: { "YYYY-MM-DDTHH:00:00.000Z": { "claude": { "model-name": count, "_subtotal": count }, "_total": count } }
14
+ let history = {};
15
+ let isDirty = false;
16
+
17
+ /**
18
+ * Extract model family from model ID
19
+ * @param {string} modelId - The model identifier (e.g., "claude-opus-4-5-thinking")
20
+ * @returns {string} The family name (claude, gemini, or other)
21
+ */
22
+ function getFamily(modelId) {
23
+ const lower = (modelId || '').toLowerCase();
24
+ if (lower.includes('claude')) return 'claude';
25
+ if (lower.includes('gemini')) return 'gemini';
26
+ return 'other';
27
+ }
28
+
29
+ /**
30
+ * Extract short model name (without family prefix)
31
+ * @param {string} modelId - The model identifier
32
+ * @param {string} family - The model family
33
+ * @returns {string} Short model name
34
+ */
35
+ function getShortName(modelId, family) {
36
+ if (family === 'other') return modelId;
37
+ // Remove family prefix (e.g., "claude-opus-4-5" -> "opus-4-5")
38
+ return modelId.replace(new RegExp(`^${family}-`, 'i'), '');
39
+ }
40
+
41
+ /**
42
+ * Ensure data directory exists and load history.
43
+ * Includes migration from legacy local data directory.
44
+ */
45
+ function load() {
46
+ try {
47
+ // Migration logic: if old file exists and new one doesn't
48
+ if (fs.existsSync(OLD_HISTORY_FILE) && !fs.existsSync(HISTORY_FILE)) {
49
+ console.log('[UsageStats] Migrating legacy usage data...');
50
+ if (!fs.existsSync(DATA_DIR)) {
51
+ fs.mkdirSync(DATA_DIR, { recursive: true });
52
+ }
53
+ fs.copyFileSync(OLD_HISTORY_FILE, HISTORY_FILE);
54
+ // We keep the old file for safety initially, but could delete it
55
+ console.log(`[UsageStats] Migration complete: ${OLD_HISTORY_FILE} -> ${HISTORY_FILE}`);
56
+ }
57
+
58
+ if (!fs.existsSync(DATA_DIR)) {
59
+ fs.mkdirSync(DATA_DIR, { recursive: true });
60
+ }
61
+ if (fs.existsSync(HISTORY_FILE)) {
62
+ const data = fs.readFileSync(HISTORY_FILE, 'utf8');
63
+ history = JSON.parse(data);
64
+ }
65
+ } catch (err) {
66
+ console.error('[UsageStats] Failed to load history:', err);
67
+ history = {};
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Save history to disk
73
+ */
74
+ function save() {
75
+ if (!isDirty) return;
76
+ try {
77
+ fs.writeFileSync(HISTORY_FILE, JSON.stringify(history, null, 2));
78
+ isDirty = false;
79
+ } catch (err) {
80
+ console.error('[UsageStats] Failed to save history:', err);
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Prune old data (keep last 30 days)
86
+ */
87
+ function prune() {
88
+ const now = new Date();
89
+ const cutoff = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
90
+
91
+ let pruned = false;
92
+ Object.keys(history).forEach(key => {
93
+ if (new Date(key) < cutoff) {
94
+ delete history[key];
95
+ pruned = true;
96
+ }
97
+ });
98
+
99
+ if (pruned) isDirty = true;
100
+ }
101
+
102
+ /**
103
+ * Track a request by model ID using hierarchical structure
104
+ * @param {string} modelId - The specific model identifier
105
+ */
106
+ function track(modelId) {
107
+ const now = new Date();
108
+ // Round down to nearest hour
109
+ now.setMinutes(0, 0, 0);
110
+ const key = now.toISOString();
111
+
112
+ if (!history[key]) {
113
+ history[key] = { _total: 0 };
114
+ }
115
+
116
+ const hourData = history[key];
117
+ const family = getFamily(modelId);
118
+ const shortName = getShortName(modelId, family);
119
+
120
+ // Initialize family object if needed
121
+ if (!hourData[family]) {
122
+ hourData[family] = { _subtotal: 0 };
123
+ }
124
+
125
+ // Increment model-specific count
126
+ hourData[family][shortName] = (hourData[family][shortName] || 0) + 1;
127
+
128
+ // Increment family subtotal
129
+ hourData[family]._subtotal = (hourData[family]._subtotal || 0) + 1;
130
+
131
+ // Increment global total
132
+ hourData._total = (hourData._total || 0) + 1;
133
+
134
+ isDirty = true;
135
+ }
136
+
137
+ /**
138
+ * Setup Express Middleware
139
+ * @param {import('express').Application} app
140
+ */
141
+ function setupMiddleware(app) {
142
+ load();
143
+
144
+ // Auto-save every minute
145
+ setInterval(() => {
146
+ save();
147
+ prune();
148
+ }, 60 * 1000);
149
+
150
+ // Save on exit
151
+ process.on('SIGINT', () => { save(); process.exit(); });
152
+ process.on('SIGTERM', () => { save(); process.exit(); });
153
+
154
+ // Request interceptor
155
+ // Track both Anthropic (/v1/messages) and OpenAI compatible (/v1/chat/completions) endpoints
156
+ const TRACKED_PATHS = ['/v1/messages', '/v1/chat/completions'];
157
+
158
+ app.use((req, res, next) => {
159
+ if (req.method === 'POST' && TRACKED_PATHS.includes(req.path)) {
160
+ const model = req.body?.model;
161
+ if (model) {
162
+ track(model);
163
+ }
164
+ }
165
+ next();
166
+ });
167
+ }
168
+
169
+ /**
170
+ * Setup API Routes
171
+ * @param {import('express').Application} app
172
+ */
173
+ function setupRoutes(app) {
174
+ app.get('/api/stats/history', (req, res) => {
175
+ // Sort keys to ensure chronological order
176
+ const sortedKeys = Object.keys(history).sort();
177
+ const sortedData = {};
178
+ sortedKeys.forEach(key => {
179
+ sortedData[key] = history[key];
180
+ });
181
+ res.json(sortedData);
182
+ });
183
+ }
184
+
185
+ /**
186
+ * Get usage history data
187
+ * @returns {object} History data sorted by timestamp
188
+ */
189
+ function getHistory() {
190
+ const sortedKeys = Object.keys(history).sort();
191
+ const sortedData = {};
192
+ sortedKeys.forEach(key => {
193
+ sortedData[key] = history[key];
194
+ });
195
+ return sortedData;
196
+ }
197
+
198
+ export default {
199
+ setupMiddleware,
200
+ setupRoutes,
201
+ track,
202
+ getFamily,
203
+ getShortName,
204
+ getHistory
205
+ };