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.
Files changed (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +757 -0
  3. package/bin/cli.js +146 -0
  4. package/package.json +97 -0
  5. package/public/Complaint Details.pdf +0 -0
  6. package/public/Cyber Crime Portal.pdf +0 -0
  7. package/public/app.js +229 -0
  8. package/public/css/src/input.css +523 -0
  9. package/public/css/style.css +1 -0
  10. package/public/favicon.png +0 -0
  11. package/public/index.html +549 -0
  12. package/public/js/components/account-manager.js +356 -0
  13. package/public/js/components/add-account-modal.js +414 -0
  14. package/public/js/components/claude-config.js +420 -0
  15. package/public/js/components/dashboard/charts.js +605 -0
  16. package/public/js/components/dashboard/filters.js +362 -0
  17. package/public/js/components/dashboard/stats.js +110 -0
  18. package/public/js/components/dashboard.js +236 -0
  19. package/public/js/components/logs-viewer.js +100 -0
  20. package/public/js/components/models.js +36 -0
  21. package/public/js/components/server-config.js +349 -0
  22. package/public/js/config/constants.js +102 -0
  23. package/public/js/data-store.js +375 -0
  24. package/public/js/settings-store.js +58 -0
  25. package/public/js/store.js +99 -0
  26. package/public/js/translations/en.js +367 -0
  27. package/public/js/translations/id.js +412 -0
  28. package/public/js/translations/pt.js +308 -0
  29. package/public/js/translations/tr.js +358 -0
  30. package/public/js/translations/zh.js +373 -0
  31. package/public/js/utils/account-actions.js +189 -0
  32. package/public/js/utils/error-handler.js +96 -0
  33. package/public/js/utils/model-config.js +42 -0
  34. package/public/js/utils/ui-logger.js +143 -0
  35. package/public/js/utils/validators.js +77 -0
  36. package/public/js/utils.js +69 -0
  37. package/public/proxy-server-64.png +0 -0
  38. package/public/views/accounts.html +361 -0
  39. package/public/views/dashboard.html +484 -0
  40. package/public/views/logs.html +97 -0
  41. package/public/views/models.html +331 -0
  42. package/public/views/settings.html +1327 -0
  43. package/src/account-manager/credentials.js +378 -0
  44. package/src/account-manager/index.js +462 -0
  45. package/src/account-manager/onboarding.js +112 -0
  46. package/src/account-manager/rate-limits.js +369 -0
  47. package/src/account-manager/storage.js +160 -0
  48. package/src/account-manager/strategies/base-strategy.js +109 -0
  49. package/src/account-manager/strategies/hybrid-strategy.js +339 -0
  50. package/src/account-manager/strategies/index.js +79 -0
  51. package/src/account-manager/strategies/round-robin-strategy.js +76 -0
  52. package/src/account-manager/strategies/sticky-strategy.js +138 -0
  53. package/src/account-manager/strategies/trackers/health-tracker.js +162 -0
  54. package/src/account-manager/strategies/trackers/index.js +9 -0
  55. package/src/account-manager/strategies/trackers/quota-tracker.js +120 -0
  56. package/src/account-manager/strategies/trackers/token-bucket-tracker.js +155 -0
  57. package/src/auth/database.js +169 -0
  58. package/src/auth/oauth.js +548 -0
  59. package/src/auth/token-extractor.js +117 -0
  60. package/src/cli/accounts.js +648 -0
  61. package/src/cloudcode/index.js +29 -0
  62. package/src/cloudcode/message-handler.js +510 -0
  63. package/src/cloudcode/model-api.js +248 -0
  64. package/src/cloudcode/rate-limit-parser.js +235 -0
  65. package/src/cloudcode/request-builder.js +93 -0
  66. package/src/cloudcode/session-manager.js +47 -0
  67. package/src/cloudcode/sse-parser.js +121 -0
  68. package/src/cloudcode/sse-streamer.js +293 -0
  69. package/src/cloudcode/streaming-handler.js +615 -0
  70. package/src/config.js +125 -0
  71. package/src/constants.js +407 -0
  72. package/src/errors.js +242 -0
  73. package/src/fallback-config.js +29 -0
  74. package/src/format/content-converter.js +193 -0
  75. package/src/format/index.js +20 -0
  76. package/src/format/request-converter.js +255 -0
  77. package/src/format/response-converter.js +120 -0
  78. package/src/format/schema-sanitizer.js +673 -0
  79. package/src/format/signature-cache.js +88 -0
  80. package/src/format/thinking-utils.js +648 -0
  81. package/src/index.js +148 -0
  82. package/src/modules/usage-stats.js +205 -0
  83. package/src/providers/anthropic-provider.js +258 -0
  84. package/src/providers/base-provider.js +157 -0
  85. package/src/providers/cloudcode.js +94 -0
  86. package/src/providers/copilot.js +399 -0
  87. package/src/providers/github-provider.js +287 -0
  88. package/src/providers/google-provider.js +192 -0
  89. package/src/providers/index.js +211 -0
  90. package/src/providers/openai-compatible.js +265 -0
  91. package/src/providers/openai-provider.js +271 -0
  92. package/src/providers/openrouter-provider.js +325 -0
  93. package/src/providers/setup.js +83 -0
  94. package/src/server.js +870 -0
  95. package/src/utils/claude-config.js +245 -0
  96. package/src/utils/helpers.js +51 -0
  97. package/src/utils/logger.js +142 -0
  98. package/src/utils/native-module-helper.js +162 -0
  99. 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;