@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.
- package/LICENSE +21 -0
- package/README.md +622 -0
- package/bin/cli.js +124 -0
- package/package.json +80 -0
- package/public/app.js +228 -0
- package/public/css/src/input.css +523 -0
- package/public/css/style.css +1 -0
- package/public/favicon.svg +10 -0
- package/public/index.html +381 -0
- package/public/js/components/account-manager.js +245 -0
- package/public/js/components/claude-config.js +420 -0
- package/public/js/components/dashboard/charts.js +589 -0
- package/public/js/components/dashboard/filters.js +362 -0
- package/public/js/components/dashboard/stats.js +110 -0
- package/public/js/components/dashboard.js +236 -0
- package/public/js/components/logs-viewer.js +100 -0
- package/public/js/components/models.js +36 -0
- package/public/js/components/server-config.js +349 -0
- package/public/js/config/constants.js +102 -0
- package/public/js/data-store.js +386 -0
- package/public/js/settings-store.js +58 -0
- package/public/js/store.js +78 -0
- package/public/js/translations/en.js +351 -0
- package/public/js/translations/id.js +396 -0
- package/public/js/translations/pt.js +287 -0
- package/public/js/translations/tr.js +342 -0
- package/public/js/translations/zh.js +357 -0
- package/public/js/utils/account-actions.js +189 -0
- package/public/js/utils/error-handler.js +96 -0
- package/public/js/utils/model-config.js +42 -0
- package/public/js/utils/validators.js +77 -0
- package/public/js/utils.js +69 -0
- package/public/views/accounts.html +329 -0
- package/public/views/dashboard.html +484 -0
- package/public/views/logs.html +97 -0
- package/public/views/models.html +331 -0
- package/public/views/settings.html +1329 -0
- package/src/account-manager/credentials.js +243 -0
- package/src/account-manager/index.js +380 -0
- package/src/account-manager/onboarding.js +117 -0
- package/src/account-manager/rate-limits.js +237 -0
- package/src/account-manager/storage.js +136 -0
- package/src/account-manager/strategies/base-strategy.js +104 -0
- package/src/account-manager/strategies/hybrid-strategy.js +195 -0
- package/src/account-manager/strategies/index.js +79 -0
- package/src/account-manager/strategies/round-robin-strategy.js +76 -0
- package/src/account-manager/strategies/sticky-strategy.js +138 -0
- package/src/account-manager/strategies/trackers/health-tracker.js +162 -0
- package/src/account-manager/strategies/trackers/index.js +8 -0
- package/src/account-manager/strategies/trackers/token-bucket-tracker.js +121 -0
- package/src/auth/database.js +169 -0
- package/src/auth/oauth.js +419 -0
- package/src/auth/token-extractor.js +117 -0
- package/src/cli/accounts.js +512 -0
- package/src/cli/refresh.js +201 -0
- package/src/cli/setup.js +338 -0
- package/src/cloudcode/index.js +29 -0
- package/src/cloudcode/message-handler.js +386 -0
- package/src/cloudcode/model-api.js +248 -0
- package/src/cloudcode/rate-limit-parser.js +181 -0
- package/src/cloudcode/request-builder.js +93 -0
- package/src/cloudcode/session-manager.js +47 -0
- package/src/cloudcode/sse-parser.js +121 -0
- package/src/cloudcode/sse-streamer.js +293 -0
- package/src/cloudcode/streaming-handler.js +492 -0
- package/src/config.js +107 -0
- package/src/constants.js +278 -0
- package/src/errors.js +238 -0
- package/src/fallback-config.js +29 -0
- package/src/format/content-converter.js +193 -0
- package/src/format/index.js +20 -0
- package/src/format/request-converter.js +248 -0
- package/src/format/response-converter.js +120 -0
- package/src/format/schema-sanitizer.js +673 -0
- package/src/format/signature-cache.js +88 -0
- package/src/format/thinking-utils.js +558 -0
- package/src/index.js +146 -0
- package/src/modules/usage-stats.js +205 -0
- package/src/server.js +861 -0
- package/src/utils/claude-config.js +245 -0
- package/src/utils/helpers.js +51 -0
- package/src/utils/logger.js +142 -0
- package/src/utils/native-module-helper.js +162 -0
- 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
|
+
};
|