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/errors.js ADDED
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Custom Error Classes
3
+ *
4
+ * Provides structured error types for better error handling and classification.
5
+ * Replaces string-based error detection with proper error class checking.
6
+ */
7
+
8
+ /**
9
+ * Base error class for CommonsProxy errors
10
+ */
11
+ export class CommonsProxyError extends Error {
12
+ /**
13
+ * @param {string} message - Error message
14
+ * @param {string} code - Error code for programmatic handling
15
+ * @param {boolean} retryable - Whether the error is retryable
16
+ * @param {Object} metadata - Additional error metadata
17
+ */
18
+ constructor(message, code, retryable = false, metadata = {}) {
19
+ super(message);
20
+ this.name = 'CommonsProxyError';
21
+ this.code = code;
22
+ this.retryable = retryable;
23
+ this.metadata = metadata;
24
+ }
25
+
26
+ /**
27
+ * Convert to JSON for API responses
28
+ */
29
+ toJSON() {
30
+ return {
31
+ name: this.name,
32
+ code: this.code,
33
+ message: this.message,
34
+ retryable: this.retryable,
35
+ ...this.metadata
36
+ };
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Rate limit error (429 / RESOURCE_EXHAUSTED)
42
+ */
43
+ export class RateLimitError extends CommonsProxyError {
44
+ /**
45
+ * @param {string} message - Error message
46
+ * @param {number|null} resetMs - Time in ms until rate limit resets
47
+ * @param {string} accountEmail - Email of the rate-limited account
48
+ */
49
+ constructor(message, resetMs = null, accountEmail = null) {
50
+ super(message, 'RATE_LIMITED', true, { resetMs, accountEmail });
51
+ this.name = 'RateLimitError';
52
+ this.resetMs = resetMs;
53
+ this.accountEmail = accountEmail;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Authentication error (invalid credentials, token expired, etc.)
59
+ */
60
+ export class AuthError extends CommonsProxyError {
61
+ /**
62
+ * @param {string} message - Error message
63
+ * @param {string} accountEmail - Email of the account with auth issues
64
+ * @param {string} reason - Specific reason for auth failure
65
+ */
66
+ constructor(message, accountEmail = null, reason = null) {
67
+ super(message, 'AUTH_INVALID', false, { accountEmail, reason });
68
+ this.name = 'AuthError';
69
+ this.accountEmail = accountEmail;
70
+ this.reason = reason;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * No accounts available error
76
+ */
77
+ export class NoAccountsError extends CommonsProxyError {
78
+ /**
79
+ * @param {string} message - Error message
80
+ * @param {boolean} allRateLimited - Whether all accounts are rate limited
81
+ */
82
+ constructor(message = 'No accounts available', allRateLimited = false) {
83
+ super(message, 'NO_ACCOUNTS', allRateLimited, { allRateLimited });
84
+ this.name = 'NoAccountsError';
85
+ this.allRateLimited = allRateLimited;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Max retries exceeded error
91
+ */
92
+ export class MaxRetriesError extends CommonsProxyError {
93
+ /**
94
+ * @param {string} message - Error message
95
+ * @param {number} attempts - Number of attempts made
96
+ */
97
+ constructor(message = 'Max retries exceeded', attempts = 0) {
98
+ super(message, 'MAX_RETRIES', false, { attempts });
99
+ this.name = 'MaxRetriesError';
100
+ this.attempts = attempts;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * API error from upstream service
106
+ */
107
+ export class ApiError extends CommonsProxyError {
108
+ /**
109
+ * @param {string} message - Error message
110
+ * @param {number} statusCode - HTTP status code
111
+ * @param {string} errorType - Type of API error
112
+ */
113
+ constructor(message, statusCode = 500, errorType = 'api_error') {
114
+ super(message, errorType.toUpperCase(), statusCode >= 500, { statusCode, errorType });
115
+ this.name = 'ApiError';
116
+ this.statusCode = statusCode;
117
+ this.errorType = errorType;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Native module error (version mismatch, rebuild required)
123
+ */
124
+ export class NativeModuleError extends CommonsProxyError {
125
+ /**
126
+ * @param {string} message - Error message
127
+ * @param {boolean} rebuildSucceeded - Whether auto-rebuild succeeded
128
+ * @param {boolean} restartRequired - Whether server restart is needed
129
+ */
130
+ constructor(message, rebuildSucceeded = false, restartRequired = false) {
131
+ super(message, 'NATIVE_MODULE_ERROR', false, { rebuildSucceeded, restartRequired });
132
+ this.name = 'NativeModuleError';
133
+ this.rebuildSucceeded = rebuildSucceeded;
134
+ this.restartRequired = restartRequired;
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Empty response error - thrown when API returns no content
140
+ * Used to trigger retry logic in streaming handler
141
+ */
142
+ export class EmptyResponseError extends CommonsProxyError {
143
+ /**
144
+ * @param {string} message - Error message
145
+ */
146
+ constructor(message = 'No content received from API') {
147
+ super(message, 'EMPTY_RESPONSE', true, {});
148
+ this.name = 'EmptyResponseError';
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Capacity exhausted error - Google's model is at capacity (not user quota)
154
+ * Should retry on same account with shorter delay, not switch accounts immediately
155
+ * Different from QUOTA_EXHAUSTED which indicates user's daily/hourly limit
156
+ */
157
+ export class CapacityExhaustedError extends CommonsProxyError {
158
+ /**
159
+ * @param {string} message - Error message
160
+ * @param {number|null} retryAfterMs - Suggested retry delay in ms
161
+ */
162
+ constructor(message = 'Model capacity exhausted', retryAfterMs = null) {
163
+ super(message, 'CAPACITY_EXHAUSTED', true, { retryAfterMs });
164
+ this.name = 'CapacityExhaustedError';
165
+ this.retryAfterMs = retryAfterMs;
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Check if an error is a rate limit error
171
+ * Works with both custom error classes and legacy string-based errors
172
+ * @param {Error} error - Error to check
173
+ * @returns {boolean}
174
+ */
175
+ export function isRateLimitError(error) {
176
+ if (error instanceof RateLimitError) return true;
177
+ const msg = (error.message || '').toLowerCase();
178
+ return msg.includes('429') ||
179
+ msg.includes('resource_exhausted') ||
180
+ msg.includes('quota_exhausted') ||
181
+ msg.includes('rate limit');
182
+ }
183
+
184
+ /**
185
+ * Check if an error is an authentication error
186
+ * Works with both custom error classes and legacy string-based errors
187
+ * @param {Error} error - Error to check
188
+ * @returns {boolean}
189
+ */
190
+ export function isAuthError(error) {
191
+ if (error instanceof AuthError) return true;
192
+ const msg = (error.message || '').toUpperCase();
193
+ return msg.includes('AUTH_INVALID') ||
194
+ msg.includes('INVALID_GRANT') ||
195
+ msg.includes('TOKEN REFRESH FAILED');
196
+ }
197
+
198
+ /**
199
+ * Check if an error is an empty response error
200
+ * @param {Error} error - Error to check
201
+ * @returns {boolean}
202
+ */
203
+ export function isEmptyResponseError(error) {
204
+ return error instanceof EmptyResponseError ||
205
+ error?.name === 'EmptyResponseError';
206
+ }
207
+
208
+ /**
209
+ * Check if an error is a capacity exhausted error (model overload, not user quota)
210
+ * This is different from quota exhaustion - capacity issues are temporary infrastructure
211
+ * limits that should be retried on the SAME account with shorter delays
212
+ * @param {Error} error - Error to check
213
+ * @returns {boolean}
214
+ */
215
+ export function isCapacityExhaustedError(error) {
216
+ if (error instanceof CapacityExhaustedError) return true;
217
+ const msg = (error.message || '').toLowerCase();
218
+ return msg.includes('model_capacity_exhausted') ||
219
+ msg.includes('capacity_exhausted') ||
220
+ msg.includes('model is currently overloaded') ||
221
+ msg.includes('service temporarily unavailable');
222
+ }
223
+
224
+ // Legacy alias for backward compatibility
225
+ export const AntigravityError = CommonsProxyError;
226
+
227
+ export default {
228
+ CommonsProxyError,
229
+ AntigravityError, // Legacy alias
230
+ RateLimitError,
231
+ AuthError,
232
+ NoAccountsError,
233
+ MaxRetriesError,
234
+ ApiError,
235
+ NativeModuleError,
236
+ EmptyResponseError,
237
+ CapacityExhaustedError,
238
+ isRateLimitError,
239
+ isAuthError,
240
+ isEmptyResponseError,
241
+ isCapacityExhaustedError
242
+ };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Model Fallback Configuration
3
+ *
4
+ * Defines fallback mappings for when a model's quota is exhausted across all accounts.
5
+ * Enables graceful degradation to alternative models with similar capabilities.
6
+ */
7
+
8
+ import { MODEL_FALLBACK_MAP } from './constants.js';
9
+
10
+ // Re-export for convenience
11
+ export { MODEL_FALLBACK_MAP };
12
+
13
+ /**
14
+ * Get fallback model for a given model ID
15
+ * @param {string} model - Primary model ID
16
+ * @returns {string|null} Fallback model ID or null if no fallback exists
17
+ */
18
+ export function getFallbackModel(model) {
19
+ return MODEL_FALLBACK_MAP[model] || null;
20
+ }
21
+
22
+ /**
23
+ * Check if a model has a fallback configured
24
+ * @param {string} model - Model ID to check
25
+ * @returns {boolean} True if fallback exists
26
+ */
27
+ export function hasFallback(model) {
28
+ return model in MODEL_FALLBACK_MAP;
29
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Content Converter
3
+ * Converts Anthropic message content to Google Generative AI parts format
4
+ */
5
+
6
+ import { MIN_SIGNATURE_LENGTH, GEMINI_SKIP_SIGNATURE } from '../constants.js';
7
+ import { getCachedSignature, getCachedSignatureFamily } from './signature-cache.js';
8
+ import { logger } from '../utils/logger.js';
9
+
10
+ /**
11
+ * Convert Anthropic role to Google role
12
+ * @param {string} role - Anthropic role ('user', 'assistant')
13
+ * @returns {string} Google role ('user', 'model')
14
+ */
15
+ export function convertRole(role) {
16
+ if (role === 'assistant') return 'model';
17
+ if (role === 'user') return 'user';
18
+ return 'user'; // Default to user
19
+ }
20
+
21
+ /**
22
+ * Convert Anthropic message content to Google Generative AI parts
23
+ * @param {string|Array} content - Anthropic message content
24
+ * @param {boolean} isClaudeModel - Whether the model is a Claude model
25
+ * @param {boolean} isGeminiModel - Whether the model is a Gemini model
26
+ * @returns {Array} Google Generative AI parts array
27
+ */
28
+ export function convertContentToParts(content, isClaudeModel = false, isGeminiModel = false) {
29
+ if (typeof content === 'string') {
30
+ return [{ text: content }];
31
+ }
32
+
33
+ if (!Array.isArray(content)) {
34
+ return [{ text: String(content) }];
35
+ }
36
+
37
+ const parts = [];
38
+ const deferredInlineData = []; // Collect inlineData to add at the end (Issue #91)
39
+
40
+ for (const block of content) {
41
+ if (!block) continue;
42
+
43
+ if (block.type === 'text') {
44
+ // Skip empty text blocks - they cause API errors
45
+ if (block.text && block.text.trim()) {
46
+ parts.push({ text: block.text });
47
+ }
48
+ } else if (block.type === 'image') {
49
+ // Handle image content
50
+ if (block.source?.type === 'base64') {
51
+ // Base64-encoded image
52
+ parts.push({
53
+ inlineData: {
54
+ mimeType: block.source.media_type,
55
+ data: block.source.data
56
+ }
57
+ });
58
+ } else if (block.source?.type === 'url') {
59
+ // URL-referenced image
60
+ parts.push({
61
+ fileData: {
62
+ mimeType: block.source.media_type || 'image/jpeg',
63
+ fileUri: block.source.url
64
+ }
65
+ });
66
+ }
67
+ } else if (block.type === 'document') {
68
+ // Handle document content (e.g. PDF)
69
+ if (block.source?.type === 'base64') {
70
+ parts.push({
71
+ inlineData: {
72
+ mimeType: block.source.media_type,
73
+ data: block.source.data
74
+ }
75
+ });
76
+ } else if (block.source?.type === 'url') {
77
+ parts.push({
78
+ fileData: {
79
+ mimeType: block.source.media_type || 'application/pdf',
80
+ fileUri: block.source.url
81
+ }
82
+ });
83
+ }
84
+ } else if (block.type === 'tool_use') {
85
+ // Convert tool_use to functionCall (Google format)
86
+ // For Claude models, include the id field
87
+ const functionCall = {
88
+ name: block.name,
89
+ args: block.input || {}
90
+ };
91
+
92
+ if (isClaudeModel && block.id) {
93
+ functionCall.id = block.id;
94
+ }
95
+
96
+ // Build the part with functionCall
97
+ const part = { functionCall };
98
+
99
+ // For Gemini models, include thoughtSignature at the part level
100
+ // This is required by Gemini 3+ for tool calls to work correctly
101
+ if (isGeminiModel) {
102
+ // Priority: block.thoughtSignature > cache > GEMINI_SKIP_SIGNATURE
103
+ let signature = block.thoughtSignature;
104
+
105
+ if (!signature && block.id) {
106
+ signature = getCachedSignature(block.id);
107
+ if (signature) {
108
+ logger.debug(`[ContentConverter] Restored signature from cache for: ${block.id}`);
109
+ }
110
+ }
111
+
112
+ part.thoughtSignature = signature || GEMINI_SKIP_SIGNATURE;
113
+ }
114
+
115
+ parts.push(part);
116
+ } else if (block.type === 'tool_result') {
117
+ // Convert tool_result to functionResponse (Google format)
118
+ let responseContent = block.content;
119
+ let imageParts = [];
120
+
121
+ if (typeof responseContent === 'string') {
122
+ responseContent = { result: responseContent };
123
+ } else if (Array.isArray(responseContent)) {
124
+ // Extract images from tool results first (e.g., from Read tool reading image files)
125
+ for (const item of responseContent) {
126
+ if (item.type === 'image' && item.source?.type === 'base64') {
127
+ imageParts.push({
128
+ inlineData: {
129
+ mimeType: item.source.media_type,
130
+ data: item.source.data
131
+ }
132
+ });
133
+ }
134
+ }
135
+
136
+ // Extract text content
137
+ const texts = responseContent
138
+ .filter(c => c.type === 'text')
139
+ .map(c => c.text)
140
+ .join('\n');
141
+ responseContent = { result: texts || (imageParts.length > 0 ? 'Image attached' : '') };
142
+ }
143
+
144
+ const functionResponse = {
145
+ name: block.tool_use_id || 'unknown',
146
+ response: responseContent
147
+ };
148
+
149
+ // For Claude models, the id field must match the tool_use_id
150
+ if (isClaudeModel && block.tool_use_id) {
151
+ functionResponse.id = block.tool_use_id;
152
+ }
153
+
154
+ parts.push({ functionResponse });
155
+
156
+ // Defer images from the tool result to end of parts array (Issue #91)
157
+ // This ensures all functionResponse parts are consecutive
158
+ deferredInlineData.push(...imageParts);
159
+ } else if (block.type === 'thinking') {
160
+ // Handle thinking blocks with signature compatibility check
161
+ if (block.signature && block.signature.length >= MIN_SIGNATURE_LENGTH) {
162
+ const signatureFamily = getCachedSignatureFamily(block.signature);
163
+ const targetFamily = isClaudeModel ? 'claude' : isGeminiModel ? 'gemini' : null;
164
+
165
+ // Drop blocks with incompatible signatures for Gemini (cross-model switch)
166
+ if (isGeminiModel && signatureFamily && targetFamily && signatureFamily !== targetFamily) {
167
+ logger.debug(`[ContentConverter] Dropping incompatible ${signatureFamily} thinking for ${targetFamily} model`);
168
+ continue;
169
+ }
170
+
171
+ // Drop blocks with unknown signature origin for Gemini (cold cache - safe default)
172
+ if (isGeminiModel && !signatureFamily && targetFamily) {
173
+ logger.debug(`[ContentConverter] Dropping thinking with unknown signature origin`);
174
+ continue;
175
+ }
176
+
177
+ // Compatible - convert to Gemini format with signature
178
+ parts.push({
179
+ text: block.thinking,
180
+ thought: true,
181
+ thoughtSignature: block.signature
182
+ });
183
+ }
184
+ // Unsigned thinking blocks are dropped (existing behavior)
185
+ }
186
+ }
187
+
188
+ // Add deferred inlineData at the end (Issue #91)
189
+ // This ensures functionResponse parts are consecutive, which Claude's API requires
190
+ parts.push(...deferredInlineData);
191
+
192
+ return parts;
193
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Format Converter Module
3
+ * Converts between Anthropic Messages API format and Google Generative AI format
4
+ */
5
+
6
+ // Re-export all from each module
7
+ export * from './request-converter.js';
8
+ export * from './response-converter.js';
9
+ export * from './content-converter.js';
10
+ export * from './schema-sanitizer.js';
11
+ export * from './thinking-utils.js';
12
+
13
+ // Default export for backward compatibility
14
+ import { convertAnthropicToGoogle } from './request-converter.js';
15
+ import { convertGoogleToAnthropic } from './response-converter.js';
16
+
17
+ export default {
18
+ convertAnthropicToGoogle,
19
+ convertGoogleToAnthropic
20
+ };