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
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Models Provider
|
|
3
|
+
*
|
|
4
|
+
* Implements authentication via GitHub Personal Access Token (PAT).
|
|
5
|
+
* Supports models from GitHub Models marketplace.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import BaseProvider from './base-provider.js';
|
|
9
|
+
|
|
10
|
+
export class GitHubProvider extends BaseProvider {
|
|
11
|
+
constructor(config = {}) {
|
|
12
|
+
super('github', 'GitHub Models', {
|
|
13
|
+
apiEndpoint: config.apiEndpoint || 'https://models.inference.ai.azure.com',
|
|
14
|
+
modelsEndpoint: config.modelsEndpoint || 'https://api.github.com/models',
|
|
15
|
+
...config
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Validate GitHub PAT
|
|
21
|
+
*
|
|
22
|
+
* @param {Object} account - Account with apiKey (PAT)
|
|
23
|
+
* @returns {Promise<{valid: boolean, error?: string, email?: string}>}
|
|
24
|
+
*/
|
|
25
|
+
async validateCredentials(account) {
|
|
26
|
+
if (!account.apiKey) {
|
|
27
|
+
return { valid: false, error: 'Missing Personal Access Token' };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// Verify PAT by fetching user info
|
|
32
|
+
const response = await fetch('https://api.github.com/user', {
|
|
33
|
+
method: 'GET',
|
|
34
|
+
headers: {
|
|
35
|
+
'Authorization': `Bearer ${account.apiKey}`,
|
|
36
|
+
'Accept': 'application/vnd.github+json',
|
|
37
|
+
'X-GitHub-Api-Version': '2022-11-28'
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
const error = await response.text();
|
|
43
|
+
return { valid: false, error: `PAT validation failed: ${error}` };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const userData = await response.json();
|
|
47
|
+
const email = userData.email || `${userData.login}@github`;
|
|
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 PAT (for GitHub, PAT IS the access token)
|
|
58
|
+
*
|
|
59
|
+
* @param {Object} account - Account with apiKey (PAT)
|
|
60
|
+
* @returns {Promise<string>} PAT
|
|
61
|
+
*/
|
|
62
|
+
async getAccessToken(account) {
|
|
63
|
+
if (!account.apiKey) {
|
|
64
|
+
throw new Error('Account missing Personal Access Token');
|
|
65
|
+
}
|
|
66
|
+
return account.apiKey;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Fetch usage/quota information from GitHub Models API
|
|
71
|
+
*
|
|
72
|
+
* @param {Object} account - Account object
|
|
73
|
+
* @param {string} token - PAT
|
|
74
|
+
* @returns {Promise<Object>} Quota data
|
|
75
|
+
*/
|
|
76
|
+
async getQuotas(account, token) {
|
|
77
|
+
try {
|
|
78
|
+
// Fetch available models from GitHub
|
|
79
|
+
const response = await fetch(this.config.modelsEndpoint, {
|
|
80
|
+
method: 'GET',
|
|
81
|
+
headers: {
|
|
82
|
+
'Authorization': `Bearer ${token}`,
|
|
83
|
+
'Accept': 'application/vnd.github+json',
|
|
84
|
+
'X-GitHub-Api-Version': '2022-11-28'
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
throw new Error(`Failed to fetch models: ${response.status}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const models = await response.json();
|
|
93
|
+
const quotaData = {};
|
|
94
|
+
|
|
95
|
+
// GitHub Models has rate limits per model
|
|
96
|
+
// Default quota assumption since GitHub doesn't expose usage API
|
|
97
|
+
if (Array.isArray(models)) {
|
|
98
|
+
models.forEach(model => {
|
|
99
|
+
quotaData[model.name] = {
|
|
100
|
+
remainingFraction: 1.0, // Unknown - assume available
|
|
101
|
+
resetTime: null
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Add common models if list is empty
|
|
107
|
+
if (Object.keys(quotaData).length === 0) {
|
|
108
|
+
const commonModels = [
|
|
109
|
+
'gpt-4o',
|
|
110
|
+
'gpt-4o-mini',
|
|
111
|
+
'claude-3-5-sonnet',
|
|
112
|
+
'gemini-1.5-pro',
|
|
113
|
+
'gemini-1.5-flash',
|
|
114
|
+
'llama-3.1-405b-instruct',
|
|
115
|
+
'mistral-large'
|
|
116
|
+
];
|
|
117
|
+
commonModels.forEach(modelId => {
|
|
118
|
+
quotaData[modelId] = {
|
|
119
|
+
remainingFraction: 1.0,
|
|
120
|
+
resetTime: null
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { models: quotaData };
|
|
126
|
+
} catch (error) {
|
|
127
|
+
this.error('Failed to fetch quotas', error);
|
|
128
|
+
// Return default quota on error
|
|
129
|
+
return {
|
|
130
|
+
models: {
|
|
131
|
+
'gpt-4o': { remainingFraction: 1.0, resetTime: null },
|
|
132
|
+
'claude-3-5-sonnet': { remainingFraction: 1.0, resetTime: null }
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get subscription tier (GitHub Models is free for developers)
|
|
140
|
+
*
|
|
141
|
+
* @param {Object} account - Account object
|
|
142
|
+
* @param {string} token - PAT
|
|
143
|
+
* @returns {Promise<{tier: string, projectId: null}>}
|
|
144
|
+
*/
|
|
145
|
+
async getSubscriptionTier(account, token) {
|
|
146
|
+
try {
|
|
147
|
+
// Check GitHub user/org details
|
|
148
|
+
const response = await fetch('https://api.github.com/user', {
|
|
149
|
+
method: 'GET',
|
|
150
|
+
headers: {
|
|
151
|
+
'Authorization': `Bearer ${token}`,
|
|
152
|
+
'Accept': 'application/vnd.github+json',
|
|
153
|
+
'X-GitHub-Api-Version': '2022-11-28'
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
if (!response.ok) {
|
|
158
|
+
return { tier: 'unknown', projectId: null };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const userData = await response.json();
|
|
162
|
+
|
|
163
|
+
// GitHub Copilot subscribers likely have higher rate limits
|
|
164
|
+
// But we can't directly check subscription status via API
|
|
165
|
+
// Default to "developer" tier
|
|
166
|
+
return {
|
|
167
|
+
tier: userData.plan?.name || 'developer',
|
|
168
|
+
projectId: userData.login
|
|
169
|
+
};
|
|
170
|
+
} catch (error) {
|
|
171
|
+
this.error('Failed to fetch subscription tier', error);
|
|
172
|
+
return { tier: 'developer', projectId: null };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get available models from GitHub Models
|
|
178
|
+
*
|
|
179
|
+
* @param {Object} account - Account object
|
|
180
|
+
* @param {string} token - PAT
|
|
181
|
+
* @returns {Promise<Array>} List of available models
|
|
182
|
+
*/
|
|
183
|
+
async getAvailableModels(account, token) {
|
|
184
|
+
try {
|
|
185
|
+
const response = await fetch(this.config.modelsEndpoint, {
|
|
186
|
+
method: 'GET',
|
|
187
|
+
headers: {
|
|
188
|
+
'Authorization': `Bearer ${token}`,
|
|
189
|
+
'Accept': 'application/vnd.github+json',
|
|
190
|
+
'X-GitHub-Api-Version': '2022-11-28'
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
if (!response.ok) {
|
|
195
|
+
throw new Error(`Failed to fetch models: ${response.status}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const models = await response.json();
|
|
199
|
+
if (Array.isArray(models)) {
|
|
200
|
+
return models.map(model => {
|
|
201
|
+
// Determine family from model name
|
|
202
|
+
let family = 'unknown';
|
|
203
|
+
if (model.name.includes('gpt')) family = 'gpt';
|
|
204
|
+
else if (model.name.includes('claude')) family = 'claude';
|
|
205
|
+
else if (model.name.includes('gemini')) family = 'gemini';
|
|
206
|
+
else if (model.name.includes('llama')) family = 'llama';
|
|
207
|
+
else if (model.name.includes('mistral')) family = 'mistral';
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
id: model.name,
|
|
211
|
+
name: model.summary || model.name,
|
|
212
|
+
family
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return [];
|
|
218
|
+
} catch (error) {
|
|
219
|
+
this.error('Failed to fetch available models', error);
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Parse GitHub API rate limit headers
|
|
226
|
+
*
|
|
227
|
+
* @param {Response} response - Fetch response
|
|
228
|
+
* @param {Object} errorData - Error data from response body
|
|
229
|
+
* @returns {Object|null} Rate limit info
|
|
230
|
+
*/
|
|
231
|
+
parseRateLimitInfo(response, errorData = null) {
|
|
232
|
+
// GitHub uses these headers:
|
|
233
|
+
// - x-ratelimit-limit
|
|
234
|
+
// - x-ratelimit-remaining
|
|
235
|
+
// - x-ratelimit-reset (Unix timestamp)
|
|
236
|
+
// - x-ratelimit-resource
|
|
237
|
+
|
|
238
|
+
const rateLimitReset = response.headers.get('x-ratelimit-reset');
|
|
239
|
+
const retryAfter = response.headers.get('retry-after');
|
|
240
|
+
|
|
241
|
+
if (rateLimitReset) {
|
|
242
|
+
const resetTimestamp = parseInt(rateLimitReset, 10);
|
|
243
|
+
return {
|
|
244
|
+
resetTime: new Date(resetTimestamp * 1000),
|
|
245
|
+
retryAfter: Math.max(0, Math.floor((resetTimestamp * 1000 - Date.now()) / 1000))
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (retryAfter) {
|
|
250
|
+
const retrySeconds = parseInt(retryAfter, 10);
|
|
251
|
+
return {
|
|
252
|
+
resetTime: new Date(Date.now() + retrySeconds * 1000),
|
|
253
|
+
retryAfter: retrySeconds
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Check error response
|
|
258
|
+
if (errorData?.message && errorData.message.includes('rate limit')) {
|
|
259
|
+
return {
|
|
260
|
+
resetTime: new Date(Date.now() + 3600000), // Default: 1 hour
|
|
261
|
+
retryAfter: 3600
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Check if error indicates invalid PAT
|
|
270
|
+
*
|
|
271
|
+
* @param {Error} error - Error object
|
|
272
|
+
* @returns {boolean}
|
|
273
|
+
*/
|
|
274
|
+
shouldInvalidateCredentials(error) {
|
|
275
|
+
if (error.message && (
|
|
276
|
+
error.message.includes('Bad credentials') ||
|
|
277
|
+
error.message.includes('Requires authentication') ||
|
|
278
|
+
error.message.includes('Invalid token')
|
|
279
|
+
)) {
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return super.shouldInvalidateCredentials(error);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export default GitHubProvider;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Cloud Code Provider
|
|
3
|
+
*
|
|
4
|
+
* Implements authentication via Google OAuth with PKCE.
|
|
5
|
+
* Wraps existing OAuth and Cloud Code API logic.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import BaseProvider from './base-provider.js';
|
|
9
|
+
import {
|
|
10
|
+
refreshAccessToken,
|
|
11
|
+
getUserEmail,
|
|
12
|
+
discoverProjectId
|
|
13
|
+
} from '../auth/oauth.js';
|
|
14
|
+
import { getModelQuotas, getSubscriptionTier } from '../cloudcode/model-api.js';
|
|
15
|
+
import { logger } from '../utils/logger.js';
|
|
16
|
+
|
|
17
|
+
export class GoogleProvider extends BaseProvider {
|
|
18
|
+
constructor(config = {}) {
|
|
19
|
+
super('google', 'Google Cloud Code', config);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Validate OAuth refresh token by attempting to refresh
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} account - Account with refreshToken
|
|
26
|
+
* @returns {Promise<{valid: boolean, error?: string, email?: string}>}
|
|
27
|
+
*/
|
|
28
|
+
async validateCredentials(account) {
|
|
29
|
+
if (!account.refreshToken) {
|
|
30
|
+
return { valid: false, error: 'Missing refresh token' };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// Try to refresh the access token
|
|
35
|
+
const { accessToken } = await refreshAccessToken(account.refreshToken);
|
|
36
|
+
|
|
37
|
+
// Get email to confirm account identity
|
|
38
|
+
const email = await getUserEmail(accessToken);
|
|
39
|
+
|
|
40
|
+
return { valid: true, email };
|
|
41
|
+
} catch (error) {
|
|
42
|
+
this.error('Credential validation failed', error);
|
|
43
|
+
return { valid: false, error: error.message };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get access token from OAuth refresh token
|
|
49
|
+
*
|
|
50
|
+
* @param {Object} account - Account with refreshToken
|
|
51
|
+
* @returns {Promise<string>} Access token
|
|
52
|
+
*/
|
|
53
|
+
async getAccessToken(account) {
|
|
54
|
+
if (!account.refreshToken) {
|
|
55
|
+
throw new Error('Account missing refresh token');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const { accessToken } = await refreshAccessToken(account.refreshToken);
|
|
60
|
+
return accessToken;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
this.error('Failed to get access token', error);
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Fetch model quotas from Cloud Code API
|
|
69
|
+
*
|
|
70
|
+
* @param {Object} account - Account object
|
|
71
|
+
* @param {string} token - Access token
|
|
72
|
+
* @returns {Promise<Object>} Quota data
|
|
73
|
+
*/
|
|
74
|
+
async getQuotas(account, token) {
|
|
75
|
+
try {
|
|
76
|
+
const projectId = account.projectId || account.subscription?.projectId;
|
|
77
|
+
if (!projectId) {
|
|
78
|
+
this.debug('No project ID available, attempting discovery...');
|
|
79
|
+
const discoveredProject = await discoverProjectId(token);
|
|
80
|
+
if (!discoveredProject) {
|
|
81
|
+
throw new Error('Could not discover project ID');
|
|
82
|
+
}
|
|
83
|
+
return await getModelQuotas(token, discoveredProject);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return await getModelQuotas(token, projectId);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
this.error('Failed to fetch quotas', error);
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Fetch subscription tier from loadCodeAssist API
|
|
95
|
+
*
|
|
96
|
+
* @param {Object} account - Account object
|
|
97
|
+
* @param {string} token - Access token
|
|
98
|
+
* @returns {Promise<{tier: string, projectId: string}>}
|
|
99
|
+
*/
|
|
100
|
+
async getSubscriptionTier(account, token) {
|
|
101
|
+
try {
|
|
102
|
+
return await getSubscriptionTier(token);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
this.error('Failed to fetch subscription tier', error);
|
|
105
|
+
// Return cached value if available
|
|
106
|
+
if (account.subscription?.tier) {
|
|
107
|
+
this.debug('Using cached subscription tier');
|
|
108
|
+
return {
|
|
109
|
+
tier: account.subscription.tier,
|
|
110
|
+
projectId: account.subscription.projectId
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return { tier: 'unknown', projectId: null };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Refresh OAuth credentials
|
|
119
|
+
*
|
|
120
|
+
* @param {Object} account - Account object
|
|
121
|
+
* @returns {Promise<Object>} Updated account (no changes for OAuth - refresh happens on-demand)
|
|
122
|
+
*/
|
|
123
|
+
async refreshCredentials(account) {
|
|
124
|
+
// OAuth refresh happens on-demand via refreshAccessToken()
|
|
125
|
+
// No need to update account object
|
|
126
|
+
return account;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Parse rate limit headers from Cloud Code API
|
|
131
|
+
*
|
|
132
|
+
* @param {Response} response - Fetch response
|
|
133
|
+
* @param {Object} errorData - Error data from response body
|
|
134
|
+
* @returns {Object|null} Rate limit info
|
|
135
|
+
*/
|
|
136
|
+
parseRateLimitInfo(response, errorData = null) {
|
|
137
|
+
// Check for rate limit headers (X-RateLimit-Reset, Retry-After, etc.)
|
|
138
|
+
const retryAfter = response.headers.get('retry-after');
|
|
139
|
+
const rateLimitReset = response.headers.get('x-ratelimit-reset');
|
|
140
|
+
|
|
141
|
+
if (retryAfter) {
|
|
142
|
+
const retrySeconds = parseInt(retryAfter, 10);
|
|
143
|
+
return {
|
|
144
|
+
resetTime: new Date(Date.now() + retrySeconds * 1000),
|
|
145
|
+
retryAfter: retrySeconds
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (rateLimitReset) {
|
|
150
|
+
const resetTimestamp = parseInt(rateLimitReset, 10);
|
|
151
|
+
return {
|
|
152
|
+
resetTime: new Date(resetTimestamp * 1000),
|
|
153
|
+
retryAfter: Math.max(0, Math.floor((resetTimestamp * 1000 - Date.now()) / 1000))
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Check error data for rate limit info
|
|
158
|
+
if (errorData?.error?.details) {
|
|
159
|
+
const details = errorData.error.details;
|
|
160
|
+
// Look for quota reset time in error details
|
|
161
|
+
if (details.quotaResetTime) {
|
|
162
|
+
return {
|
|
163
|
+
resetTime: new Date(details.quotaResetTime),
|
|
164
|
+
retryAfter: Math.max(0, Math.floor((new Date(details.quotaResetTime) - Date.now()) / 1000))
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Check if error indicates invalid OAuth credentials
|
|
174
|
+
*
|
|
175
|
+
* @param {Error} error - Error object
|
|
176
|
+
* @returns {boolean}
|
|
177
|
+
*/
|
|
178
|
+
shouldInvalidateCredentials(error) {
|
|
179
|
+
// Check for OAuth-specific errors
|
|
180
|
+
if (error.message && (
|
|
181
|
+
error.message.includes('invalid_grant') ||
|
|
182
|
+
error.message.includes('Token has been expired or revoked') ||
|
|
183
|
+
error.message.includes('Invalid Credentials')
|
|
184
|
+
)) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return super.shouldInvalidateCredentials(error);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export default GoogleProvider;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider System for CommonsProxy
|
|
3
|
+
*
|
|
4
|
+
* Multi-provider support inspired by opencode's provider architecture.
|
|
5
|
+
* Allows CommonsProxy to work with multiple AI backends:
|
|
6
|
+
* - Google Cloud Code (OAuth, default)
|
|
7
|
+
* - Anthropic (API key)
|
|
8
|
+
* - OpenAI (API key)
|
|
9
|
+
* - GitHub Models (PAT)
|
|
10
|
+
* - GitHub Copilot
|
|
11
|
+
* - OpenAI-compatible endpoints
|
|
12
|
+
* - Custom providers
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { logger } from '../utils/logger.js';
|
|
16
|
+
import GoogleProvider from './google-provider.js';
|
|
17
|
+
import AnthropicProvider from './anthropic-provider.js';
|
|
18
|
+
import OpenAIProvider from './openai-provider.js';
|
|
19
|
+
import GitHubProvider from './github-provider.js';
|
|
20
|
+
import CopilotProvider from './copilot.js';
|
|
21
|
+
import OpenRouterProvider from './openrouter-provider.js';
|
|
22
|
+
|
|
23
|
+
// Provider registry (legacy system for message routing providers)
|
|
24
|
+
const messagingProviders = new Map();
|
|
25
|
+
|
|
26
|
+
// Authentication provider registry (new system)
|
|
27
|
+
const authProviders = new Map();
|
|
28
|
+
|
|
29
|
+
// Initialize authentication providers
|
|
30
|
+
authProviders.set('google', new GoogleProvider());
|
|
31
|
+
authProviders.set('anthropic', new AnthropicProvider());
|
|
32
|
+
authProviders.set('openai', new OpenAIProvider());
|
|
33
|
+
authProviders.set('github', new GitHubProvider());
|
|
34
|
+
authProviders.set('copilot', new CopilotProvider());
|
|
35
|
+
authProviders.set('openrouter', new OpenRouterProvider());
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Provider interface definition
|
|
39
|
+
* Each provider must implement these methods
|
|
40
|
+
*/
|
|
41
|
+
export const ProviderInterface = {
|
|
42
|
+
id: 'string', // Unique provider ID
|
|
43
|
+
name: 'string', // Display name
|
|
44
|
+
type: 'string', // Provider type: 'cloudcode', 'copilot', 'openai', 'custom'
|
|
45
|
+
|
|
46
|
+
// Required methods
|
|
47
|
+
// authenticate: async (credentials) => { accessToken, refreshToken, expiresAt }
|
|
48
|
+
// sendMessage: async (request, options) => response
|
|
49
|
+
// sendMessageStream: async function* (request, options) => yields events
|
|
50
|
+
// listModels: async () => [{ id, name, capabilities }]
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Register a legacy messaging provider
|
|
55
|
+
* @param {Object} provider - Provider implementation
|
|
56
|
+
*/
|
|
57
|
+
export function registerProvider(provider) {
|
|
58
|
+
if (!provider.id || !provider.name) {
|
|
59
|
+
throw new Error('Provider must have id and name');
|
|
60
|
+
}
|
|
61
|
+
messagingProviders.set(provider.id, provider);
|
|
62
|
+
logger.info(`[Providers] Registered messaging provider: ${provider.name} (${provider.id})`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get a legacy messaging provider by ID
|
|
67
|
+
* @param {string} id - Provider ID
|
|
68
|
+
* @returns {Object|null} Provider or null
|
|
69
|
+
*/
|
|
70
|
+
export function getProvider(id) {
|
|
71
|
+
return messagingProviders.get(id) || null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get authentication provider by ID
|
|
76
|
+
* @param {string} providerId - Provider identifier ('google', 'anthropic', 'openai', 'github')
|
|
77
|
+
* @returns {BaseProvider} Provider instance
|
|
78
|
+
*/
|
|
79
|
+
export function getAuthProvider(providerId) {
|
|
80
|
+
const provider = authProviders.get(providerId);
|
|
81
|
+
if (!provider) {
|
|
82
|
+
throw new Error(`Unknown auth provider: ${providerId}`);
|
|
83
|
+
}
|
|
84
|
+
return provider;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get authentication provider for an account
|
|
89
|
+
* @param {Object} account - Account object with provider field
|
|
90
|
+
* @returns {BaseProvider} Provider instance
|
|
91
|
+
*/
|
|
92
|
+
export function getProviderForAccount(account) {
|
|
93
|
+
// Determine provider from account source or explicit provider field
|
|
94
|
+
const providerId = account.provider || detectProviderFromSource(account.source);
|
|
95
|
+
return getAuthProvider(providerId);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Detect provider from legacy source field
|
|
100
|
+
* @param {string} source - Account source ('oauth', 'manual', 'database')
|
|
101
|
+
* @returns {string} Provider ID
|
|
102
|
+
*/
|
|
103
|
+
function detectProviderFromSource(source) {
|
|
104
|
+
// Legacy accounts use 'oauth' or 'database' for Google OAuth
|
|
105
|
+
if (source === 'oauth' || source === 'database') {
|
|
106
|
+
return 'google';
|
|
107
|
+
}
|
|
108
|
+
// Manual accounts default to Google (legacy behavior)
|
|
109
|
+
if (source === 'manual') {
|
|
110
|
+
return 'google';
|
|
111
|
+
}
|
|
112
|
+
// Default to Google
|
|
113
|
+
return 'google';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Register a custom authentication provider
|
|
118
|
+
* @param {string} id - Provider ID
|
|
119
|
+
* @param {BaseProvider} provider - Provider instance
|
|
120
|
+
*/
|
|
121
|
+
export function registerAuthProvider(id, provider) {
|
|
122
|
+
authProviders.set(id, provider);
|
|
123
|
+
logger.info(`[Providers] Registered auth provider: ${provider.name} (${id})`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get list of all available authentication providers
|
|
128
|
+
* @returns {Array<{id: string, name: string, authType: string}>} Provider list
|
|
129
|
+
*/
|
|
130
|
+
export function getAllAuthProviders() {
|
|
131
|
+
return Array.from(authProviders.entries()).map(([id, provider]) => ({
|
|
132
|
+
id,
|
|
133
|
+
name: provider.name,
|
|
134
|
+
authType: id === 'google' ? 'oauth' : (id === 'copilot' ? 'device-auth' : 'api-key')
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* List all registered legacy messaging providers
|
|
140
|
+
* @returns {Array} Array of provider info
|
|
141
|
+
*/
|
|
142
|
+
export function listProviders() {
|
|
143
|
+
return Array.from(messagingProviders.values()).map(p => ({
|
|
144
|
+
id: p.id,
|
|
145
|
+
name: p.name,
|
|
146
|
+
type: p.type,
|
|
147
|
+
enabled: p.enabled !== false
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get the default provider
|
|
153
|
+
* @returns {Object} Default provider (cloudcode)
|
|
154
|
+
*/
|
|
155
|
+
export function getDefaultProvider() {
|
|
156
|
+
return messagingProviders.get('cloudcode') || messagingProviders.values().next().value;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Check if an authentication provider is registered
|
|
161
|
+
* @param {string} providerId - Provider ID to check
|
|
162
|
+
* @returns {boolean} True if provider exists
|
|
163
|
+
*/
|
|
164
|
+
export function hasAuthProvider(providerId) {
|
|
165
|
+
return authProviders.has(providerId);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Provider types enum
|
|
170
|
+
*/
|
|
171
|
+
export const ProviderType = {
|
|
172
|
+
CLOUDCODE: 'cloudcode',
|
|
173
|
+
COPILOT: 'copilot',
|
|
174
|
+
OPENROUTER: 'openrouter',
|
|
175
|
+
ANTHROPIC: 'anthropic',
|
|
176
|
+
GITHUB: 'github',
|
|
177
|
+
CUSTOM: 'custom'
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Export provider classes for direct instantiation if needed
|
|
181
|
+
export {
|
|
182
|
+
GoogleProvider,
|
|
183
|
+
AnthropicProvider,
|
|
184
|
+
OpenAIProvider,
|
|
185
|
+
GitHubProvider,
|
|
186
|
+
CopilotProvider,
|
|
187
|
+
OpenRouterProvider
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export default {
|
|
191
|
+
// Legacy messaging provider functions
|
|
192
|
+
registerProvider,
|
|
193
|
+
getProvider,
|
|
194
|
+
listProviders,
|
|
195
|
+
getDefaultProvider,
|
|
196
|
+
// New authentication provider functions
|
|
197
|
+
getAuthProvider,
|
|
198
|
+
getProviderForAccount,
|
|
199
|
+
registerAuthProvider,
|
|
200
|
+
getAllAuthProviders,
|
|
201
|
+
hasAuthProvider,
|
|
202
|
+
// Provider classes
|
|
203
|
+
GoogleProvider,
|
|
204
|
+
AnthropicProvider,
|
|
205
|
+
OpenAIProvider,
|
|
206
|
+
GitHubProvider,
|
|
207
|
+
CopilotProvider,
|
|
208
|
+
OpenRouterProvider,
|
|
209
|
+
// Enums
|
|
210
|
+
ProviderType
|
|
211
|
+
};
|