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,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Provider Interface
|
|
3
|
+
*
|
|
4
|
+
* Abstract base class for authentication providers.
|
|
5
|
+
* Each provider implements:
|
|
6
|
+
* - Authentication (OAuth, API keys, PAT, etc.)
|
|
7
|
+
* - Token/credential validation
|
|
8
|
+
* - Quota/rate limit fetching
|
|
9
|
+
* - Account information retrieval
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { logger } from '../utils/logger.js';
|
|
13
|
+
|
|
14
|
+
export class BaseProvider {
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} id - Unique provider identifier ('google', 'anthropic', 'openai', 'github')
|
|
17
|
+
* @param {string} name - Display name for UI
|
|
18
|
+
* @param {Object} config - Provider-specific configuration
|
|
19
|
+
*/
|
|
20
|
+
constructor(id, name, config = {}) {
|
|
21
|
+
if (new.target === BaseProvider) {
|
|
22
|
+
throw new Error('BaseProvider is abstract and cannot be instantiated directly');
|
|
23
|
+
}
|
|
24
|
+
this.id = id;
|
|
25
|
+
this.name = name;
|
|
26
|
+
this.config = config;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Validate account credentials (API key, token, etc.)
|
|
31
|
+
*
|
|
32
|
+
* @param {Object} account - Account object with credentials
|
|
33
|
+
* @returns {Promise<{valid: boolean, error?: string, email?: string}>}
|
|
34
|
+
*/
|
|
35
|
+
async validateCredentials(account) {
|
|
36
|
+
throw new Error('validateCredentials() must be implemented by subclass');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get access token for making API requests
|
|
41
|
+
*
|
|
42
|
+
* @param {Object} account - Account object
|
|
43
|
+
* @returns {Promise<string>} Access token or API key
|
|
44
|
+
*/
|
|
45
|
+
async getAccessToken(account) {
|
|
46
|
+
throw new Error('getAccessToken() must be implemented by subclass');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Fetch account quota/usage information
|
|
51
|
+
*
|
|
52
|
+
* @param {Object} account - Account object
|
|
53
|
+
* @param {string} token - Access token
|
|
54
|
+
* @returns {Promise<Object>} Quota data: { models: { [modelId]: { remainingFraction, resetTime } } }
|
|
55
|
+
*/
|
|
56
|
+
async getQuotas(account, token) {
|
|
57
|
+
throw new Error('getQuotas() must be implemented by subclass');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Fetch subscription/account tier information
|
|
62
|
+
*
|
|
63
|
+
* @param {Object} account - Account object
|
|
64
|
+
* @param {string} token - Access token
|
|
65
|
+
* @returns {Promise<{tier: string, projectId?: string}>} Subscription info
|
|
66
|
+
*/
|
|
67
|
+
async getSubscriptionTier(account, token) {
|
|
68
|
+
// Default implementation - can be overridden
|
|
69
|
+
return { tier: 'unknown', projectId: null };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Refresh expired credentials (if applicable)
|
|
74
|
+
*
|
|
75
|
+
* @param {Object} account - Account object
|
|
76
|
+
* @returns {Promise<Object>} Updated account object with refreshed credentials
|
|
77
|
+
*/
|
|
78
|
+
async refreshCredentials(account) {
|
|
79
|
+
// Default: no-op for API key based providers
|
|
80
|
+
return account;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get available models for this provider
|
|
85
|
+
*
|
|
86
|
+
* @param {Object} account - Account object
|
|
87
|
+
* @param {string} token - Access token
|
|
88
|
+
* @returns {Promise<Array<{id: string, name: string, family: string}>>} Available models
|
|
89
|
+
*/
|
|
90
|
+
async getAvailableModels(account, token) {
|
|
91
|
+
// Default: return empty array - can be overridden
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Parse rate limit information from API response
|
|
97
|
+
*
|
|
98
|
+
* @param {Response} response - Fetch API response object
|
|
99
|
+
* @param {Object} [errorData] - Optional parsed error data
|
|
100
|
+
* @returns {Object|null} Rate limit info: { resetTime: Date, retryAfter: number }
|
|
101
|
+
*/
|
|
102
|
+
parseRateLimitInfo(response, errorData = null) {
|
|
103
|
+
// Default implementation - can be overridden
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Handle authentication error and determine if credentials need refresh
|
|
109
|
+
*
|
|
110
|
+
* @param {Error} error - Authentication error
|
|
111
|
+
* @returns {boolean} True if credentials should be marked invalid
|
|
112
|
+
*/
|
|
113
|
+
shouldInvalidateCredentials(error) {
|
|
114
|
+
// Default: invalidate on 401/403 errors
|
|
115
|
+
if (error.status === 401 || error.status === 403) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
if (error.message && error.message.toLowerCase().includes('invalid') &&
|
|
119
|
+
error.message.toLowerCase().includes('api')) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Log provider-specific debug information
|
|
127
|
+
*
|
|
128
|
+
* @param {string} message - Debug message
|
|
129
|
+
* @param {*} data - Optional data to log
|
|
130
|
+
*/
|
|
131
|
+
debug(message, data = null) {
|
|
132
|
+
if (logger.isDebugEnabled) {
|
|
133
|
+
logger.debug(`[Provider:${this.name}] ${message}`, data || '');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Log provider info
|
|
139
|
+
*
|
|
140
|
+
* @param {string} message - Info message
|
|
141
|
+
*/
|
|
142
|
+
info(message) {
|
|
143
|
+
logger.info(`[Provider:${this.name}] ${message}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Log provider error
|
|
148
|
+
*
|
|
149
|
+
* @param {string} message - Error message
|
|
150
|
+
* @param {Error} [error] - Optional error object
|
|
151
|
+
*/
|
|
152
|
+
error(message, error = null) {
|
|
153
|
+
logger.error(`[Provider:${this.name}] ${message}`, error ? error.message : '');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export default BaseProvider;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Cloud Code Provider
|
|
3
|
+
*
|
|
4
|
+
* Default provider for CommonsProxy - uses Google's Cloud Code API
|
|
5
|
+
* to access Claude and Gemini models.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ProviderType } from './index.js';
|
|
9
|
+
import {
|
|
10
|
+
CLOUDCODE_ENDPOINT_FALLBACKS,
|
|
11
|
+
CLOUDCODE_HEADERS,
|
|
12
|
+
OAUTH_CONFIG
|
|
13
|
+
} from '../constants.js';
|
|
14
|
+
import { sendMessage } from '../cloudcode/message-handler.js';
|
|
15
|
+
import { sendMessageStream } from '../cloudcode/streaming-handler.js';
|
|
16
|
+
import { fetchAvailableModels } from '../cloudcode/model-api.js';
|
|
17
|
+
import { logger } from '../utils/logger.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Google Cloud Code Provider
|
|
21
|
+
*/
|
|
22
|
+
export const CloudCodeProvider = {
|
|
23
|
+
id: 'cloudcode',
|
|
24
|
+
name: 'Google Cloud Code',
|
|
25
|
+
type: ProviderType.CLOUDCODE,
|
|
26
|
+
enabled: true,
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Provider configuration
|
|
30
|
+
*/
|
|
31
|
+
config: {
|
|
32
|
+
endpoints: CLOUDCODE_ENDPOINT_FALLBACKS,
|
|
33
|
+
headers: CLOUDCODE_HEADERS,
|
|
34
|
+
oauth: OAUTH_CONFIG
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Send a non-streaming message
|
|
39
|
+
* @param {Object} request - Anthropic-format request
|
|
40
|
+
* @param {Object} accountManager - Account manager instance
|
|
41
|
+
* @param {Object} options - Additional options
|
|
42
|
+
* @returns {Promise<Object>} Anthropic-format response
|
|
43
|
+
*/
|
|
44
|
+
async sendMessage(request, accountManager, options = {}) {
|
|
45
|
+
const fallbackEnabled = options.fallback !== false;
|
|
46
|
+
return sendMessage(request, accountManager, fallbackEnabled);
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Send a streaming message
|
|
51
|
+
* @param {Object} request - Anthropic-format request
|
|
52
|
+
* @param {Object} accountManager - Account manager instance
|
|
53
|
+
* @param {Object} options - Additional options
|
|
54
|
+
* @yields {Object} Anthropic-format SSE events
|
|
55
|
+
*/
|
|
56
|
+
async *sendMessageStream(request, accountManager, options = {}) {
|
|
57
|
+
const fallbackEnabled = options.fallback !== false;
|
|
58
|
+
yield* sendMessageStream(request, accountManager, fallbackEnabled);
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* List available models
|
|
63
|
+
* @param {string} token - OAuth access token
|
|
64
|
+
* @param {string} projectId - Project ID
|
|
65
|
+
* @returns {Promise<Array>} Array of model info
|
|
66
|
+
*/
|
|
67
|
+
async listModels(token, projectId) {
|
|
68
|
+
try {
|
|
69
|
+
return await fetchAvailableModels(token, projectId);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
logger.warn(`[CloudCode] Failed to fetch models: ${error.message}`);
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get supported model families
|
|
78
|
+
*/
|
|
79
|
+
getModelFamilies() {
|
|
80
|
+
return ['claude', 'gemini'];
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if a model is supported
|
|
85
|
+
* @param {string} modelId - Model ID to check
|
|
86
|
+
* @returns {boolean}
|
|
87
|
+
*/
|
|
88
|
+
supportsModel(modelId) {
|
|
89
|
+
const lower = modelId.toLowerCase();
|
|
90
|
+
return lower.includes('claude') || lower.includes('gemini');
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export default CloudCodeProvider;
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Copilot Provider
|
|
3
|
+
*
|
|
4
|
+
* Enables CommonsProxy to work with GitHub Copilot's API.
|
|
5
|
+
* Uses GitHub Device Authorization flow for authentication,
|
|
6
|
+
* then exchanges the GitHub token for a Copilot API token.
|
|
7
|
+
*
|
|
8
|
+
* Inspired by opencode's copilot plugin implementation.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import BaseProvider from './base-provider.js';
|
|
12
|
+
|
|
13
|
+
// GitHub Copilot OAuth configuration
|
|
14
|
+
const COPILOT_CLIENT_ID = 'Iv1.b507a08c87ecfe98';
|
|
15
|
+
const COPILOT_DEVICE_CODE_URL = 'https://github.com/login/device/code';
|
|
16
|
+
const COPILOT_ACCESS_TOKEN_URL = 'https://github.com/login/oauth/access_token';
|
|
17
|
+
const COPILOT_API_URL = 'https://api.githubcopilot.com';
|
|
18
|
+
const COPILOT_TOKEN_URL = 'https://api.github.com/copilot_internal/v2/token';
|
|
19
|
+
|
|
20
|
+
// In-memory cache for short-lived Copilot API tokens
|
|
21
|
+
// Maps GitHub access token -> { token, expiresAt }
|
|
22
|
+
const copilotTokenCache = new Map();
|
|
23
|
+
|
|
24
|
+
export class CopilotProvider extends BaseProvider {
|
|
25
|
+
constructor(config = {}) {
|
|
26
|
+
super('copilot', 'GitHub Copilot', {
|
|
27
|
+
clientId: COPILOT_CLIENT_ID,
|
|
28
|
+
deviceCodeUrl: COPILOT_DEVICE_CODE_URL,
|
|
29
|
+
accessTokenUrl: COPILOT_ACCESS_TOKEN_URL,
|
|
30
|
+
apiUrl: config.apiUrl || COPILOT_API_URL,
|
|
31
|
+
tokenUrl: config.tokenUrl || COPILOT_TOKEN_URL,
|
|
32
|
+
...config
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Validate Copilot credentials by attempting to get a Copilot API token
|
|
38
|
+
*
|
|
39
|
+
* @param {Object} account - Account with apiKey (GitHub access token)
|
|
40
|
+
* @returns {Promise<{valid: boolean, error?: string, email?: string}>}
|
|
41
|
+
*/
|
|
42
|
+
async validateCredentials(account) {
|
|
43
|
+
if (!account.apiKey) {
|
|
44
|
+
return { valid: false, error: 'Missing GitHub access token' };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// Verify by fetching user info
|
|
49
|
+
const userResponse = await fetch('https://api.github.com/user', {
|
|
50
|
+
method: 'GET',
|
|
51
|
+
headers: {
|
|
52
|
+
'Authorization': `Bearer ${account.apiKey}`,
|
|
53
|
+
'Accept': 'application/vnd.github+json',
|
|
54
|
+
'User-Agent': 'commons-proxy/2.0.0',
|
|
55
|
+
'X-GitHub-Api-Version': '2022-11-28'
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!userResponse.ok) {
|
|
60
|
+
const error = await userResponse.text();
|
|
61
|
+
return { valid: false, error: `GitHub token validation failed: ${error}` };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const userData = await userResponse.json();
|
|
65
|
+
|
|
66
|
+
// Try to get a Copilot token to verify Copilot access
|
|
67
|
+
try {
|
|
68
|
+
await this._getCopilotToken(account.apiKey);
|
|
69
|
+
} catch (copilotError) {
|
|
70
|
+
return {
|
|
71
|
+
valid: false,
|
|
72
|
+
error: `GitHub token valid but Copilot access denied: ${copilotError.message}. Ensure you have an active GitHub Copilot subscription.`
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const email = userData.email || `${userData.login}@github`;
|
|
77
|
+
return { valid: true, email };
|
|
78
|
+
} catch (error) {
|
|
79
|
+
this.error('Credential validation failed', error);
|
|
80
|
+
return { valid: false, error: error.message };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get Copilot API token from the stored GitHub access token.
|
|
86
|
+
* Copilot tokens are short-lived (~30 min), so we cache and refresh.
|
|
87
|
+
*
|
|
88
|
+
* @param {Object} account - Account with apiKey (GitHub access token)
|
|
89
|
+
* @returns {Promise<string>} Copilot API token
|
|
90
|
+
*/
|
|
91
|
+
async getAccessToken(account) {
|
|
92
|
+
if (!account.apiKey) {
|
|
93
|
+
throw new Error('Account missing GitHub access token');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const copilotToken = await this._getCopilotToken(account.apiKey);
|
|
97
|
+
return copilotToken;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Internal: Get or refresh Copilot API token
|
|
102
|
+
*
|
|
103
|
+
* @param {string} githubToken - GitHub OAuth access token
|
|
104
|
+
* @returns {Promise<string>} Copilot API token
|
|
105
|
+
*/
|
|
106
|
+
async _getCopilotToken(githubToken) {
|
|
107
|
+
// Check cache
|
|
108
|
+
const cached = copilotTokenCache.get(githubToken);
|
|
109
|
+
if (cached && cached.expiresAt > Date.now() + 60000) {
|
|
110
|
+
// Return cached token if it has > 1 minute left
|
|
111
|
+
return cached.token;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const response = await fetch(this.config.tokenUrl, {
|
|
115
|
+
method: 'GET',
|
|
116
|
+
headers: {
|
|
117
|
+
'Authorization': `Bearer ${githubToken}`,
|
|
118
|
+
'User-Agent': 'commons-proxy/2.0.0',
|
|
119
|
+
'Accept': 'application/json'
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
const text = await response.text();
|
|
125
|
+
throw new Error(`Failed to get Copilot token: ${response.status} ${text}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const data = await response.json();
|
|
129
|
+
const expiresAt = data.expires_at
|
|
130
|
+
? new Date(data.expires_at * 1000).getTime()
|
|
131
|
+
: Date.now() + 30 * 60 * 1000; // Default 30 min
|
|
132
|
+
|
|
133
|
+
copilotTokenCache.set(githubToken, {
|
|
134
|
+
token: data.token,
|
|
135
|
+
expiresAt
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return data.token;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Fetch quota information (Copilot doesn't expose usage API)
|
|
143
|
+
*
|
|
144
|
+
* @param {Object} account - Account object
|
|
145
|
+
* @param {string} token - Copilot API token
|
|
146
|
+
* @returns {Promise<Object>} Quota data
|
|
147
|
+
*/
|
|
148
|
+
async getQuotas(account, token) {
|
|
149
|
+
// Copilot doesn't expose quota/usage API
|
|
150
|
+
// Return default models with full availability
|
|
151
|
+
const defaultModels = [
|
|
152
|
+
'gpt-4o', 'gpt-4', 'gpt-4-turbo', 'gpt-3.5-turbo',
|
|
153
|
+
'claude-3.5-sonnet', 'o1-preview', 'o1-mini'
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
const models = {};
|
|
157
|
+
defaultModels.forEach(modelId => {
|
|
158
|
+
models[modelId] = {
|
|
159
|
+
remainingFraction: 1.0,
|
|
160
|
+
resetTime: null
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return { models };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get subscription tier
|
|
169
|
+
*
|
|
170
|
+
* @param {Object} account - Account object
|
|
171
|
+
* @param {string} token - Copilot API token
|
|
172
|
+
* @returns {Promise<{tier: string, projectId: string|null}>}
|
|
173
|
+
*/
|
|
174
|
+
async getSubscriptionTier(account, token) {
|
|
175
|
+
try {
|
|
176
|
+
// Use the stored GitHub token (apiKey), not the Copilot token
|
|
177
|
+
const response = await fetch('https://api.github.com/user', {
|
|
178
|
+
method: 'GET',
|
|
179
|
+
headers: {
|
|
180
|
+
'Authorization': `Bearer ${account.apiKey}`,
|
|
181
|
+
'Accept': 'application/vnd.github+json',
|
|
182
|
+
'User-Agent': 'commons-proxy/2.0.0',
|
|
183
|
+
'X-GitHub-Api-Version': '2022-11-28'
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
return { tier: 'copilot', projectId: null };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const userData = await response.json();
|
|
192
|
+
return {
|
|
193
|
+
tier: 'copilot',
|
|
194
|
+
projectId: userData.login
|
|
195
|
+
};
|
|
196
|
+
} catch (error) {
|
|
197
|
+
this.error('Failed to fetch subscription tier', error);
|
|
198
|
+
return { tier: 'copilot', projectId: null };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Get available models from Copilot
|
|
204
|
+
*
|
|
205
|
+
* @param {Object} account - Account object
|
|
206
|
+
* @param {string} token - Copilot API token
|
|
207
|
+
* @returns {Promise<Array>} List of available models
|
|
208
|
+
*/
|
|
209
|
+
async getAvailableModels(account, token) {
|
|
210
|
+
return [
|
|
211
|
+
{ id: 'gpt-4o', name: 'GPT-4o', family: 'gpt' },
|
|
212
|
+
{ id: 'gpt-4', name: 'GPT-4', family: 'gpt' },
|
|
213
|
+
{ id: 'gpt-4-turbo', name: 'GPT-4 Turbo', family: 'gpt' },
|
|
214
|
+
{ id: 'gpt-3.5-turbo', name: 'GPT-3.5 Turbo', family: 'gpt' },
|
|
215
|
+
{ id: 'claude-3.5-sonnet', name: 'Claude 3.5 Sonnet', family: 'claude' },
|
|
216
|
+
{ id: 'o1-preview', name: 'o1 Preview', family: 'o1' },
|
|
217
|
+
{ id: 'o1-mini', name: 'o1 Mini', family: 'o1' }
|
|
218
|
+
];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Parse rate limit headers from Copilot API
|
|
223
|
+
*
|
|
224
|
+
* @param {Response} response - Fetch response
|
|
225
|
+
* @param {Object} errorData - Error data from response body
|
|
226
|
+
* @returns {Object|null} Rate limit info
|
|
227
|
+
*/
|
|
228
|
+
parseRateLimitInfo(response, errorData = null) {
|
|
229
|
+
const retryAfter = response.headers.get('retry-after');
|
|
230
|
+
const rateLimitReset = response.headers.get('x-ratelimit-reset');
|
|
231
|
+
|
|
232
|
+
if (retryAfter) {
|
|
233
|
+
const retrySeconds = parseInt(retryAfter, 10);
|
|
234
|
+
return {
|
|
235
|
+
resetTime: new Date(Date.now() + retrySeconds * 1000),
|
|
236
|
+
retryAfter: retrySeconds
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (rateLimitReset) {
|
|
241
|
+
const resetTimestamp = parseInt(rateLimitReset, 10);
|
|
242
|
+
return {
|
|
243
|
+
resetTime: new Date(resetTimestamp * 1000),
|
|
244
|
+
retryAfter: Math.max(0, Math.floor((resetTimestamp * 1000 - Date.now()) / 1000))
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Check if error indicates invalid credentials
|
|
253
|
+
*
|
|
254
|
+
* @param {Error} error - Error object
|
|
255
|
+
* @returns {boolean}
|
|
256
|
+
*/
|
|
257
|
+
shouldInvalidateCredentials(error) {
|
|
258
|
+
if (error.message && (
|
|
259
|
+
error.message.includes('Bad credentials') ||
|
|
260
|
+
error.message.includes('Requires authentication') ||
|
|
261
|
+
error.message.includes('Invalid token') ||
|
|
262
|
+
error.message.includes('Copilot access denied')
|
|
263
|
+
)) {
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return super.shouldInvalidateCredentials(error);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ============================================================================
|
|
271
|
+
// Static Device Auth Flow Methods (used by WebUI and CLI)
|
|
272
|
+
// ============================================================================
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Initiate device authorization flow
|
|
276
|
+
* @returns {Promise<Object>} Device code response with verification_uri and user_code
|
|
277
|
+
*/
|
|
278
|
+
static async initiateDeviceAuth() {
|
|
279
|
+
const response = await fetch(COPILOT_DEVICE_CODE_URL, {
|
|
280
|
+
method: 'POST',
|
|
281
|
+
headers: {
|
|
282
|
+
'Accept': 'application/json',
|
|
283
|
+
'Content-Type': 'application/json',
|
|
284
|
+
'User-Agent': 'commons-proxy/2.0.0'
|
|
285
|
+
},
|
|
286
|
+
body: JSON.stringify({
|
|
287
|
+
client_id: COPILOT_CLIENT_ID,
|
|
288
|
+
scope: 'read:user'
|
|
289
|
+
})
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
if (!response.ok) {
|
|
293
|
+
const text = await response.text();
|
|
294
|
+
throw new Error(`Failed to initiate device authorization: ${response.status} ${text}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return response.json();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Poll for access token after user completes device auth
|
|
302
|
+
* @param {string} deviceCode - Device code from initiateDeviceAuth
|
|
303
|
+
* @param {number} interval - Polling interval in seconds
|
|
304
|
+
* @param {AbortSignal} [signal] - Optional abort signal
|
|
305
|
+
* @returns {Promise<Object>} { accessToken, tokenType }
|
|
306
|
+
*/
|
|
307
|
+
static async pollForToken(deviceCode, interval = 5, signal = null) {
|
|
308
|
+
const maxAttempts = 60; // 5 minutes max
|
|
309
|
+
|
|
310
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
311
|
+
if (signal?.aborted) {
|
|
312
|
+
throw new Error('Device auth polling aborted');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
await new Promise(resolve => setTimeout(resolve, interval * 1000));
|
|
316
|
+
|
|
317
|
+
const response = await fetch(COPILOT_ACCESS_TOKEN_URL, {
|
|
318
|
+
method: 'POST',
|
|
319
|
+
headers: {
|
|
320
|
+
'Accept': 'application/json',
|
|
321
|
+
'Content-Type': 'application/json',
|
|
322
|
+
'User-Agent': 'commons-proxy/2.0.0'
|
|
323
|
+
},
|
|
324
|
+
body: JSON.stringify({
|
|
325
|
+
client_id: COPILOT_CLIENT_ID,
|
|
326
|
+
device_code: deviceCode,
|
|
327
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
|
|
328
|
+
})
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
if (!response.ok) {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const data = await response.json();
|
|
336
|
+
|
|
337
|
+
if (data.access_token) {
|
|
338
|
+
return {
|
|
339
|
+
accessToken: data.access_token,
|
|
340
|
+
tokenType: data.token_type || 'bearer'
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (data.error === 'authorization_pending') {
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (data.error === 'slow_down') {
|
|
349
|
+
interval += 5;
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (data.error) {
|
|
354
|
+
throw new Error(`OAuth error: ${data.error_description || data.error}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
throw new Error('Authorization timed out after 5 minutes');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Get GitHub user info from access token
|
|
363
|
+
* @param {string} accessToken - GitHub access token
|
|
364
|
+
* @returns {Promise<Object>} User info { login, email, name }
|
|
365
|
+
*/
|
|
366
|
+
static async getUserInfo(accessToken) {
|
|
367
|
+
const response = await fetch('https://api.github.com/user', {
|
|
368
|
+
method: 'GET',
|
|
369
|
+
headers: {
|
|
370
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
371
|
+
'Accept': 'application/vnd.github+json',
|
|
372
|
+
'User-Agent': 'commons-proxy/2.0.0',
|
|
373
|
+
'X-GitHub-Api-Version': '2022-11-28'
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
if (!response.ok) {
|
|
378
|
+
throw new Error(`Failed to get user info: ${response.status}`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const data = await response.json();
|
|
382
|
+
return {
|
|
383
|
+
login: data.login,
|
|
384
|
+
email: data.email || `${data.login}@github`,
|
|
385
|
+
name: data.name || data.login
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Export config constants for use by other modules
|
|
391
|
+
export const COPILOT_CONFIG = {
|
|
392
|
+
clientId: COPILOT_CLIENT_ID,
|
|
393
|
+
deviceCodeUrl: COPILOT_DEVICE_CODE_URL,
|
|
394
|
+
accessTokenUrl: COPILOT_ACCESS_TOKEN_URL,
|
|
395
|
+
apiUrl: COPILOT_API_URL,
|
|
396
|
+
tokenUrl: COPILOT_TOKEN_URL
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
export default CopilotProvider;
|