commons-proxy 2.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 +757 -0
- package/bin/cli.js +146 -0
- package/package.json +97 -0
- package/public/Complaint Details.pdf +0 -0
- package/public/Cyber Crime Portal.pdf +0 -0
- package/public/app.js +229 -0
- package/public/css/src/input.css +523 -0
- package/public/css/style.css +1 -0
- package/public/favicon.png +0 -0
- package/public/index.html +549 -0
- package/public/js/components/account-manager.js +356 -0
- package/public/js/components/add-account-modal.js +414 -0
- package/public/js/components/claude-config.js +420 -0
- package/public/js/components/dashboard/charts.js +605 -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 +375 -0
- package/public/js/settings-store.js +58 -0
- package/public/js/store.js +99 -0
- package/public/js/translations/en.js +367 -0
- package/public/js/translations/id.js +412 -0
- package/public/js/translations/pt.js +308 -0
- package/public/js/translations/tr.js +358 -0
- package/public/js/translations/zh.js +373 -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/ui-logger.js +143 -0
- package/public/js/utils/validators.js +77 -0
- package/public/js/utils.js +69 -0
- package/public/proxy-server-64.png +0 -0
- package/public/views/accounts.html +361 -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 +1327 -0
- package/src/account-manager/credentials.js +378 -0
- package/src/account-manager/index.js +462 -0
- package/src/account-manager/onboarding.js +112 -0
- package/src/account-manager/rate-limits.js +369 -0
- package/src/account-manager/storage.js +160 -0
- package/src/account-manager/strategies/base-strategy.js +109 -0
- package/src/account-manager/strategies/hybrid-strategy.js +339 -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 +9 -0
- package/src/account-manager/strategies/trackers/quota-tracker.js +120 -0
- package/src/account-manager/strategies/trackers/token-bucket-tracker.js +155 -0
- package/src/auth/database.js +169 -0
- package/src/auth/oauth.js +548 -0
- package/src/auth/token-extractor.js +117 -0
- package/src/cli/accounts.js +648 -0
- package/src/cloudcode/index.js +29 -0
- package/src/cloudcode/message-handler.js +510 -0
- package/src/cloudcode/model-api.js +248 -0
- package/src/cloudcode/rate-limit-parser.js +235 -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 +615 -0
- package/src/config.js +125 -0
- package/src/constants.js +407 -0
- package/src/errors.js +242 -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 +255 -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 +648 -0
- package/src/index.js +148 -0
- package/src/modules/usage-stats.js +205 -0
- package/src/providers/anthropic-provider.js +258 -0
- package/src/providers/base-provider.js +157 -0
- package/src/providers/cloudcode.js +94 -0
- package/src/providers/copilot.js +399 -0
- package/src/providers/github-provider.js +287 -0
- package/src/providers/google-provider.js +192 -0
- package/src/providers/index.js +211 -0
- package/src/providers/openai-compatible.js +265 -0
- package/src/providers/openai-provider.js +271 -0
- package/src/providers/openrouter-provider.js +325 -0
- package/src/providers/setup.js +83 -0
- package/src/server.js +870 -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 +1134 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CommonsProxy
|
|
3
|
+
* Entry point - starts the proxy server
|
|
4
|
+
* Universal proxy for Claude Code CLI with multi-provider support
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import app, { accountManager } from './server.js';
|
|
8
|
+
import { DEFAULT_PORT } from './constants.js';
|
|
9
|
+
import { logger } from './utils/logger.js';
|
|
10
|
+
import { config } from './config.js';
|
|
11
|
+
import { getStrategyLabel, STRATEGY_NAMES, DEFAULT_STRATEGY } from './account-manager/strategies/index.js';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import os from 'os';
|
|
14
|
+
|
|
15
|
+
// Parse command line arguments
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
const isDebug = args.includes('--debug') || process.env.DEBUG === 'true';
|
|
18
|
+
const isFallbackEnabled = args.includes('--fallback') || process.env.FALLBACK === 'true';
|
|
19
|
+
|
|
20
|
+
// Parse --strategy flag (format: --strategy=sticky or --strategy sticky)
|
|
21
|
+
let strategyOverride = null;
|
|
22
|
+
for (let i = 0; i < args.length; i++) {
|
|
23
|
+
if (args[i].startsWith('--strategy=')) {
|
|
24
|
+
strategyOverride = args[i].split('=')[1];
|
|
25
|
+
} else if (args[i] === '--strategy' && args[i + 1]) {
|
|
26
|
+
strategyOverride = args[i + 1];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Validate strategy
|
|
30
|
+
if (strategyOverride && !STRATEGY_NAMES.includes(strategyOverride.toLowerCase())) {
|
|
31
|
+
logger.warn(`[Startup] Invalid strategy "${strategyOverride}". Valid options: ${STRATEGY_NAMES.join(', ')}. Using default.`);
|
|
32
|
+
strategyOverride = null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Initialize logger
|
|
36
|
+
logger.setDebug(isDebug);
|
|
37
|
+
|
|
38
|
+
if (isDebug) {
|
|
39
|
+
logger.debug('Debug mode enabled');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (isFallbackEnabled) {
|
|
43
|
+
logger.info('Model fallback mode enabled');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Export fallback flag for server to use
|
|
47
|
+
export const FALLBACK_ENABLED = isFallbackEnabled;
|
|
48
|
+
|
|
49
|
+
const PORT = process.env.PORT || DEFAULT_PORT;
|
|
50
|
+
|
|
51
|
+
// Home directory for account storage
|
|
52
|
+
const HOME_DIR = os.homedir();
|
|
53
|
+
const CONFIG_DIR = path.join(HOME_DIR, '.config/commons-proxy');
|
|
54
|
+
|
|
55
|
+
const server = app.listen(PORT, () => {
|
|
56
|
+
// Clear console for a clean start
|
|
57
|
+
console.clear();
|
|
58
|
+
|
|
59
|
+
const border = '║';
|
|
60
|
+
// align for 2-space indent (60 chars), align4 for 4-space indent (58 chars)
|
|
61
|
+
const align = (text) => text + ' '.repeat(Math.max(0, 60 - text.length));
|
|
62
|
+
const align4 = (text) => text + ' '.repeat(Math.max(0, 58 - text.length));
|
|
63
|
+
|
|
64
|
+
// Build Control section dynamically
|
|
65
|
+
const strategyOptions = `(${STRATEGY_NAMES.join('/')})`;
|
|
66
|
+
const strategyLine2 = ' ' + strategyOptions;
|
|
67
|
+
let controlSection = '║ Control: ║\n';
|
|
68
|
+
controlSection += '║ --strategy=<s> Set account selection strategy ║\n';
|
|
69
|
+
controlSection += `${border} ${align(strategyLine2)}${border}\n`;
|
|
70
|
+
if (!isDebug) {
|
|
71
|
+
controlSection += '║ --debug Enable debug logging ║\n';
|
|
72
|
+
}
|
|
73
|
+
if (!isFallbackEnabled) {
|
|
74
|
+
controlSection += '║ --fallback Enable model fallback on quota exhaust ║\n';
|
|
75
|
+
}
|
|
76
|
+
controlSection += '║ Ctrl+C Stop server ║';
|
|
77
|
+
|
|
78
|
+
// Get the strategy label (accountManager will be initialized by now)
|
|
79
|
+
const strategyLabel = accountManager.getStrategyLabel();
|
|
80
|
+
|
|
81
|
+
// Build status section - always show strategy, plus any active modes
|
|
82
|
+
let statusSection = '║ ║\n';
|
|
83
|
+
statusSection += '║ Active Modes: ║\n';
|
|
84
|
+
statusSection += `${border} ${align4(`✓ Strategy: ${strategyLabel}`)}${border}\n`;
|
|
85
|
+
if (isDebug) {
|
|
86
|
+
statusSection += '║ ✓ Debug mode enabled ║\n';
|
|
87
|
+
}
|
|
88
|
+
if (isFallbackEnabled) {
|
|
89
|
+
statusSection += '║ ✓ Model fallback enabled ║\n';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
logger.log(`
|
|
93
|
+
╔══════════════════════════════════════════════════════════════╗
|
|
94
|
+
║ CommonsProxy Server ║
|
|
95
|
+
╠══════════════════════════════════════════════════════════════╣
|
|
96
|
+
║ ║
|
|
97
|
+
${border} ${align(`Server and WebUI running at: http://localhost:${PORT}`)}${border}
|
|
98
|
+
${statusSection}║ ║
|
|
99
|
+
${controlSection}
|
|
100
|
+
║ ║
|
|
101
|
+
║ Endpoints: ║
|
|
102
|
+
║ POST /v1/messages - Anthropic Messages API ║
|
|
103
|
+
║ GET /v1/models - List available models ║
|
|
104
|
+
║ GET /health - Health check ║
|
|
105
|
+
║ GET /account-limits - Account status & quotas ║
|
|
106
|
+
║ POST /refresh-token - Force token refresh ║
|
|
107
|
+
║ ║
|
|
108
|
+
${border} ${align(`Configuration:`)}${border}
|
|
109
|
+
${border} ${align4(`Storage: ${CONFIG_DIR}`)}${border}
|
|
110
|
+
║ ║
|
|
111
|
+
║ Usage with Claude Code: ║
|
|
112
|
+
${border} ${align4(`export ANTHROPIC_BASE_URL=http://localhost:${PORT}`)}${border}
|
|
113
|
+
${border} ${align4(`export ANTHROPIC_API_KEY=${config.apiKey || 'dummy'}`)}${border}
|
|
114
|
+
║ claude ║
|
|
115
|
+
║ ║
|
|
116
|
+
║ Add Google accounts: ║
|
|
117
|
+
║ npm run accounts ║
|
|
118
|
+
║ ║
|
|
119
|
+
║ Prerequisites (if no accounts configured): ║
|
|
120
|
+
║ - Windsurf/Cursor IDE must be running ║
|
|
121
|
+
║ - Have a chat panel open in the IDE ║
|
|
122
|
+
║ ║
|
|
123
|
+
╚══════════════════════════════════════════════════════════════╝
|
|
124
|
+
`);
|
|
125
|
+
|
|
126
|
+
logger.success(`Server started successfully on port ${PORT}`);
|
|
127
|
+
if (isDebug) {
|
|
128
|
+
logger.warn('Running in DEBUG mode - verbose logs enabled');
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Graceful shutdown
|
|
133
|
+
const shutdown = () => {
|
|
134
|
+
logger.info('Shutting down server...');
|
|
135
|
+
server.close(() => {
|
|
136
|
+
logger.success('Server stopped');
|
|
137
|
+
process.exit(0);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Force close if it takes too long
|
|
141
|
+
setTimeout(() => {
|
|
142
|
+
logger.error('Could not close connections in time, forcefully shutting down');
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}, 10000);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
process.on('SIGTERM', shutdown);
|
|
148
|
+
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
|
+
};
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic API Provider
|
|
3
|
+
*
|
|
4
|
+
* Implements authentication via Anthropic API keys.
|
|
5
|
+
* Supports Claude models via direct Anthropic API.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import BaseProvider from './base-provider.js';
|
|
9
|
+
|
|
10
|
+
export class AnthropicProvider extends BaseProvider {
|
|
11
|
+
constructor(config = {}) {
|
|
12
|
+
super('anthropic', 'Anthropic', {
|
|
13
|
+
apiEndpoint: config.apiEndpoint || 'https://api.anthropic.com',
|
|
14
|
+
apiVersion: config.apiVersion || '2023-06-01',
|
|
15
|
+
...config
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Validate Anthropic API key
|
|
21
|
+
*
|
|
22
|
+
* @param {Object} account - Account with apiKey
|
|
23
|
+
* @returns {Promise<{valid: boolean, error?: string, email?: string}>}
|
|
24
|
+
*/
|
|
25
|
+
async validateCredentials(account) {
|
|
26
|
+
if (!account.apiKey) {
|
|
27
|
+
return { valid: false, error: 'Missing API key' };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// Test API key by fetching model list
|
|
32
|
+
const endpoint = account.customApiEndpoint || this.config.apiEndpoint;
|
|
33
|
+
const response = await fetch(`${endpoint}/v1/models`, {
|
|
34
|
+
method: 'GET',
|
|
35
|
+
headers: {
|
|
36
|
+
'x-api-key': account.apiKey,
|
|
37
|
+
'anthropic-version': this.config.apiVersion
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
const error = await response.text();
|
|
43
|
+
return { valid: false, error: `API key validation failed: ${error}` };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Anthropic doesn't provide email in API, use a placeholder
|
|
47
|
+
const email = account.email || `anthropic-${account.apiKey.slice(0, 8)}`;
|
|
48
|
+
|
|
49
|
+
return { valid: true, email };
|
|
50
|
+
} catch (error) {
|
|
51
|
+
this.error('Credential validation failed', error);
|
|
52
|
+
return { valid: false, error: error.message };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get API key (for Anthropic, API key IS the access token)
|
|
58
|
+
*
|
|
59
|
+
* @param {Object} account - Account with apiKey
|
|
60
|
+
* @returns {Promise<string>} API key
|
|
61
|
+
*/
|
|
62
|
+
async getAccessToken(account) {
|
|
63
|
+
if (!account.apiKey) {
|
|
64
|
+
throw new Error('Account missing API key');
|
|
65
|
+
}
|
|
66
|
+
return account.apiKey;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Fetch usage/quota information from Anthropic API
|
|
71
|
+
* Note: Anthropic doesn't expose quota via API, so we track usage client-side
|
|
72
|
+
*
|
|
73
|
+
* @param {Object} account - Account object
|
|
74
|
+
* @param {string} token - API key
|
|
75
|
+
* @returns {Promise<Object>} Quota data (estimated based on usage tracking)
|
|
76
|
+
*/
|
|
77
|
+
async getQuotas(account, token) {
|
|
78
|
+
try {
|
|
79
|
+
const endpoint = account.customApiEndpoint || this.config.apiEndpoint;
|
|
80
|
+
|
|
81
|
+
// Anthropic doesn't have a direct quota API yet
|
|
82
|
+
// We'll attempt to fetch models to check if key is active
|
|
83
|
+
const response = await fetch(`${endpoint}/v1/models`, {
|
|
84
|
+
method: 'GET',
|
|
85
|
+
headers: {
|
|
86
|
+
'x-api-key': token,
|
|
87
|
+
'anthropic-version': this.config.apiVersion
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
throw new Error(`Failed to fetch models: ${response.status}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const data = await response.json();
|
|
96
|
+
const models = {};
|
|
97
|
+
|
|
98
|
+
// Create default quota entries for available models
|
|
99
|
+
if (data.data && Array.isArray(data.data)) {
|
|
100
|
+
data.data.forEach(model => {
|
|
101
|
+
models[model.id] = {
|
|
102
|
+
remainingFraction: 1.0, // Default: full quota (no API to check actual)
|
|
103
|
+
resetTime: null // Unknown
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
} else {
|
|
107
|
+
// Fallback: Common Claude models
|
|
108
|
+
const commonModels = [
|
|
109
|
+
'claude-3-opus-20240229',
|
|
110
|
+
'claude-3-sonnet-20240229',
|
|
111
|
+
'claude-3-haiku-20240307',
|
|
112
|
+
'claude-opus-4-5-thinking',
|
|
113
|
+
'claude-sonnet-4-5-thinking'
|
|
114
|
+
];
|
|
115
|
+
commonModels.forEach(modelId => {
|
|
116
|
+
models[modelId] = {
|
|
117
|
+
remainingFraction: 1.0,
|
|
118
|
+
resetTime: null
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { models };
|
|
124
|
+
} catch (error) {
|
|
125
|
+
this.error('Failed to fetch quotas', error);
|
|
126
|
+
// Return default quota on error
|
|
127
|
+
return {
|
|
128
|
+
models: {
|
|
129
|
+
'claude-opus-4-5-thinking': { remainingFraction: 1.0, resetTime: null },
|
|
130
|
+
'claude-sonnet-4-5-thinking': { remainingFraction: 1.0, resetTime: null }
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get subscription tier (Anthropic uses usage-based pricing)
|
|
138
|
+
*
|
|
139
|
+
* @param {Object} account - Account object
|
|
140
|
+
* @param {string} token - API key
|
|
141
|
+
* @returns {Promise<{tier: string, projectId: null}>}
|
|
142
|
+
*/
|
|
143
|
+
async getSubscriptionTier(account, token) {
|
|
144
|
+
// Anthropic uses usage-based pricing, no fixed tiers
|
|
145
|
+
// We can check the organization if the API supports it in the future
|
|
146
|
+
return { tier: 'usage-based', projectId: null };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get available Claude models
|
|
151
|
+
*
|
|
152
|
+
* @param {Object} account - Account object
|
|
153
|
+
* @param {string} token - API key
|
|
154
|
+
* @returns {Promise<Array>} List of available models
|
|
155
|
+
*/
|
|
156
|
+
async getAvailableModels(account, token) {
|
|
157
|
+
try {
|
|
158
|
+
const endpoint = account.customApiEndpoint || this.config.apiEndpoint;
|
|
159
|
+
const response = await fetch(`${endpoint}/v1/models`, {
|
|
160
|
+
method: 'GET',
|
|
161
|
+
headers: {
|
|
162
|
+
'x-api-key': token,
|
|
163
|
+
'anthropic-version': this.config.apiVersion
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
throw new Error(`Failed to fetch models: ${response.status}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const data = await response.json();
|
|
172
|
+
if (data.data && Array.isArray(data.data)) {
|
|
173
|
+
return data.data.map(model => ({
|
|
174
|
+
id: model.id,
|
|
175
|
+
name: model.display_name || model.id,
|
|
176
|
+
family: 'claude'
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return [];
|
|
181
|
+
} catch (error) {
|
|
182
|
+
this.error('Failed to fetch available models', error);
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Parse Anthropic rate limit headers
|
|
189
|
+
*
|
|
190
|
+
* @param {Response} response - Fetch response
|
|
191
|
+
* @param {Object} errorData - Error data from response body
|
|
192
|
+
* @returns {Object|null} Rate limit info
|
|
193
|
+
*/
|
|
194
|
+
parseRateLimitInfo(response, errorData = null) {
|
|
195
|
+
// Anthropic uses these headers:
|
|
196
|
+
// - anthropic-ratelimit-requests-limit
|
|
197
|
+
// - anthropic-ratelimit-requests-remaining
|
|
198
|
+
// - anthropic-ratelimit-requests-reset
|
|
199
|
+
// - anthropic-ratelimit-tokens-limit
|
|
200
|
+
// - anthropic-ratelimit-tokens-remaining
|
|
201
|
+
// - anthropic-ratelimit-tokens-reset
|
|
202
|
+
// - retry-after
|
|
203
|
+
|
|
204
|
+
const retryAfter = response.headers.get('retry-after');
|
|
205
|
+
const requestsReset = response.headers.get('anthropic-ratelimit-requests-reset');
|
|
206
|
+
const tokensReset = response.headers.get('anthropic-ratelimit-tokens-reset');
|
|
207
|
+
|
|
208
|
+
if (retryAfter) {
|
|
209
|
+
const retrySeconds = parseInt(retryAfter, 10);
|
|
210
|
+
return {
|
|
211
|
+
resetTime: new Date(Date.now() + retrySeconds * 1000),
|
|
212
|
+
retryAfter: retrySeconds
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Use the later of requests or tokens reset time
|
|
217
|
+
const resets = [requestsReset, tokensReset].filter(Boolean);
|
|
218
|
+
if (resets.length > 0) {
|
|
219
|
+
const resetDates = resets.map(r => new Date(r));
|
|
220
|
+
const latestReset = new Date(Math.max(...resetDates));
|
|
221
|
+
return {
|
|
222
|
+
resetTime: latestReset,
|
|
223
|
+
retryAfter: Math.max(0, Math.floor((latestReset - Date.now()) / 1000))
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Check error response for rate limit info
|
|
228
|
+
if (errorData?.error?.type === 'rate_limit_error') {
|
|
229
|
+
// Anthropic may include reset time in error message
|
|
230
|
+
return {
|
|
231
|
+
resetTime: new Date(Date.now() + 60000), // Default: 1 minute
|
|
232
|
+
retryAfter: 60
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Check if error indicates invalid API key
|
|
241
|
+
*
|
|
242
|
+
* @param {Error} error - Error object
|
|
243
|
+
* @returns {boolean}
|
|
244
|
+
*/
|
|
245
|
+
shouldInvalidateCredentials(error) {
|
|
246
|
+
if (error.message && (
|
|
247
|
+
error.message.includes('invalid_api_key') ||
|
|
248
|
+
error.message.includes('authentication_error') ||
|
|
249
|
+
error.message.includes('Invalid API Key')
|
|
250
|
+
)) {
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return super.shouldInvalidateCredentials(error);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export default AnthropicProvider;
|