@theia/ai-copilot 1.68.0-next.79
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/README.md +73 -0
- package/lib/browser/copilot-auth-dialog.d.ts +46 -0
- package/lib/browser/copilot-auth-dialog.d.ts.map +1 -0
- package/lib/browser/copilot-auth-dialog.js +246 -0
- package/lib/browser/copilot-auth-dialog.js.map +1 -0
- package/lib/browser/copilot-command-contribution.d.ts +22 -0
- package/lib/browser/copilot-command-contribution.d.ts.map +1 -0
- package/lib/browser/copilot-command-contribution.js +95 -0
- package/lib/browser/copilot-command-contribution.js.map +1 -0
- package/lib/browser/copilot-frontend-application-contribution.d.ts +15 -0
- package/lib/browser/copilot-frontend-application-contribution.d.ts.map +1 -0
- package/lib/browser/copilot-frontend-application-contribution.js +90 -0
- package/lib/browser/copilot-frontend-application-contribution.js.map +1 -0
- package/lib/browser/copilot-frontend-module.d.ts +5 -0
- package/lib/browser/copilot-frontend-module.d.ts.map +1 -0
- package/lib/browser/copilot-frontend-module.js +69 -0
- package/lib/browser/copilot-frontend-module.js.map +1 -0
- package/lib/browser/copilot-status-bar-contribution.d.ts +18 -0
- package/lib/browser/copilot-status-bar-contribution.d.ts.map +1 -0
- package/lib/browser/copilot-status-bar-contribution.js +92 -0
- package/lib/browser/copilot-status-bar-contribution.js.map +1 -0
- package/lib/browser/index.d.ts +5 -0
- package/lib/browser/index.d.ts.map +1 -0
- package/lib/browser/index.js +23 -0
- package/lib/browser/index.js.map +1 -0
- package/lib/common/copilot-auth-service.d.ts +77 -0
- package/lib/common/copilot-auth-service.d.ts.map +1 -0
- package/lib/common/copilot-auth-service.js +22 -0
- package/lib/common/copilot-auth-service.js.map +1 -0
- package/lib/common/copilot-language-models-manager.d.ts +41 -0
- package/lib/common/copilot-language-models-manager.d.ts.map +1 -0
- package/lib/common/copilot-language-models-manager.js +22 -0
- package/lib/common/copilot-language-models-manager.js.map +1 -0
- package/lib/common/copilot-preferences.d.ts +5 -0
- package/lib/common/copilot-preferences.d.ts.map +1 -0
- package/lib/common/copilot-preferences.js +52 -0
- package/lib/common/copilot-preferences.js.map +1 -0
- package/lib/common/index.d.ts +4 -0
- package/lib/common/index.d.ts.map +1 -0
- package/lib/common/index.js +22 -0
- package/lib/common/index.js.map +1 -0
- package/lib/node/copilot-auth-service-impl.d.ts +28 -0
- package/lib/node/copilot-auth-service-impl.d.ts.map +1 -0
- package/lib/node/copilot-auth-service-impl.js +229 -0
- package/lib/node/copilot-auth-service-impl.js.map +1 -0
- package/lib/node/copilot-backend-module.d.ts +4 -0
- package/lib/node/copilot-backend-module.d.ts.map +1 -0
- package/lib/node/copilot-backend-module.js +39 -0
- package/lib/node/copilot-backend-module.js.map +1 -0
- package/lib/node/copilot-language-model.d.ts +33 -0
- package/lib/node/copilot-language-model.d.ts.map +1 -0
- package/lib/node/copilot-language-model.js +217 -0
- package/lib/node/copilot-language-model.js.map +1 -0
- package/lib/node/copilot-language-models-manager-impl.d.ts +23 -0
- package/lib/node/copilot-language-models-manager-impl.d.ts.map +1 -0
- package/lib/node/copilot-language-models-manager-impl.js +113 -0
- package/lib/node/copilot-language-models-manager-impl.js.map +1 -0
- package/lib/node/index.d.ts +4 -0
- package/lib/node/index.d.ts.map +1 -0
- package/lib/node/index.js +22 -0
- package/lib/node/index.js.map +1 -0
- package/lib/package.spec.d.ts +1 -0
- package/lib/package.spec.d.ts.map +1 -0
- package/lib/package.spec.js +26 -0
- package/lib/package.spec.js.map +1 -0
- package/package.json +52 -0
- package/src/browser/copilot-auth-dialog.tsx +326 -0
- package/src/browser/copilot-command-contribution.ts +93 -0
- package/src/browser/copilot-frontend-application-contribution.ts +89 -0
- package/src/browser/copilot-frontend-module.ts +86 -0
- package/src/browser/copilot-status-bar-contribution.ts +88 -0
- package/src/browser/index.ts +20 -0
- package/src/browser/style/index.css +167 -0
- package/src/common/copilot-auth-service.ts +103 -0
- package/src/common/copilot-language-models-manager.ts +59 -0
- package/src/common/copilot-preferences.ts +53 -0
- package/src/common/index.ts +19 -0
- package/src/node/copilot-auth-service-impl.ts +274 -0
- package/src/node/copilot-backend-module.ts +58 -0
- package/src/node/copilot-language-model.ts +262 -0
- package/src/node/copilot-language-models-manager-impl.ts +118 -0
- package/src/node/index.ts +19 -0
- package/src/package.spec.ts +27 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
export * from './copilot-language-models-manager';
|
|
18
|
+
export * from './copilot-auth-service';
|
|
19
|
+
export * from './copilot-preferences';
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
18
|
+
import { Emitter, Event } from '@theia/core';
|
|
19
|
+
import { KeyStoreService } from '@theia/core/lib/common/key-store';
|
|
20
|
+
import {
|
|
21
|
+
CopilotAuthService,
|
|
22
|
+
CopilotAuthServiceClient,
|
|
23
|
+
CopilotAuthState,
|
|
24
|
+
DeviceCodeResponse
|
|
25
|
+
} from '../common/copilot-auth-service';
|
|
26
|
+
|
|
27
|
+
const COPILOT_CLIENT_ID = 'Iv23ctNZvWb5IGBKdyPY';
|
|
28
|
+
const COPILOT_SCOPE = 'read:user';
|
|
29
|
+
const COPILOT_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
|
|
30
|
+
const KEYSTORE_SERVICE = 'theia-copilot-auth';
|
|
31
|
+
const KEYSTORE_ACCOUNT = 'github-copilot';
|
|
32
|
+
const USER_AGENT = 'Theia-Copilot/1.0.0';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Maximum number of polling attempts for token retrieval.
|
|
36
|
+
* With a default 5-second interval, this allows approximately 5 minutes of polling.
|
|
37
|
+
*/
|
|
38
|
+
const MAX_POLLING_ATTEMPTS = 60;
|
|
39
|
+
|
|
40
|
+
interface StoredCredentials {
|
|
41
|
+
accessToken: string;
|
|
42
|
+
accountLabel?: string;
|
|
43
|
+
enterpriseUrl?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Backend implementation of the GitHub Copilot OAuth Device Flow authentication service.
|
|
48
|
+
* Handles device code generation, token polling, and credential storage.
|
|
49
|
+
*/
|
|
50
|
+
@injectable()
|
|
51
|
+
export class CopilotAuthServiceImpl implements CopilotAuthService {
|
|
52
|
+
|
|
53
|
+
@inject(KeyStoreService)
|
|
54
|
+
protected readonly keyStoreService: KeyStoreService;
|
|
55
|
+
|
|
56
|
+
protected client: CopilotAuthServiceClient | undefined;
|
|
57
|
+
protected cachedState: CopilotAuthState | undefined;
|
|
58
|
+
|
|
59
|
+
protected readonly onAuthStateChangedEmitter = new Emitter<CopilotAuthState>();
|
|
60
|
+
readonly onAuthStateChanged: Event<CopilotAuthState> = this.onAuthStateChangedEmitter.event;
|
|
61
|
+
|
|
62
|
+
setClient(client: CopilotAuthServiceClient | undefined): void {
|
|
63
|
+
this.client = client;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
protected getOAuthEndpoints(enterpriseUrl?: string): { deviceCodeUrl: string; accessTokenUrl: string } {
|
|
67
|
+
if (enterpriseUrl) {
|
|
68
|
+
const domain = enterpriseUrl
|
|
69
|
+
.replace(/^https?:\/\//, '')
|
|
70
|
+
.replace(/\/$/, '');
|
|
71
|
+
return {
|
|
72
|
+
deviceCodeUrl: `https://${domain}/login/device/code`,
|
|
73
|
+
accessTokenUrl: `https://${domain}/login/oauth/access_token`
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
deviceCodeUrl: 'https://github.com/login/device/code',
|
|
78
|
+
accessTokenUrl: 'https://github.com/login/oauth/access_token'
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async initiateDeviceFlow(enterpriseUrl?: string): Promise<DeviceCodeResponse> {
|
|
83
|
+
const endpoints = this.getOAuthEndpoints(enterpriseUrl);
|
|
84
|
+
|
|
85
|
+
const response = await fetch(endpoints.deviceCodeUrl, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: {
|
|
88
|
+
'Accept': 'application/json',
|
|
89
|
+
'Content-Type': 'application/json',
|
|
90
|
+
'User-Agent': USER_AGENT
|
|
91
|
+
},
|
|
92
|
+
body: JSON.stringify({
|
|
93
|
+
client_id: COPILOT_CLIENT_ID,
|
|
94
|
+
scope: COPILOT_SCOPE
|
|
95
|
+
})
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (!response.ok) {
|
|
99
|
+
const errorText = await response.text();
|
|
100
|
+
throw new Error(`Failed to initiate device authorization: ${response.status} - ${errorText}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const data = await response.json() as DeviceCodeResponse;
|
|
104
|
+
return data;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async pollForToken(deviceCode: string, interval: number, enterpriseUrl?: string): Promise<boolean> {
|
|
108
|
+
const endpoints = this.getOAuthEndpoints(enterpriseUrl);
|
|
109
|
+
let attempts = 0;
|
|
110
|
+
|
|
111
|
+
while (attempts < MAX_POLLING_ATTEMPTS) {
|
|
112
|
+
await this.delay(interval * 1000);
|
|
113
|
+
attempts++;
|
|
114
|
+
|
|
115
|
+
const response = await fetch(endpoints.accessTokenUrl, {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
headers: {
|
|
118
|
+
'Accept': 'application/json',
|
|
119
|
+
'Content-Type': 'application/json',
|
|
120
|
+
'User-Agent': USER_AGENT
|
|
121
|
+
},
|
|
122
|
+
body: JSON.stringify({
|
|
123
|
+
client_id: COPILOT_CLIENT_ID,
|
|
124
|
+
device_code: deviceCode,
|
|
125
|
+
grant_type: COPILOT_GRANT_TYPE
|
|
126
|
+
})
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
console.error(`Token request failed: ${response.status}`);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const data = await response.json() as {
|
|
135
|
+
access_token?: string;
|
|
136
|
+
error?: string;
|
|
137
|
+
error_description?: string;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
if (data.access_token) {
|
|
141
|
+
// Get user info for account label
|
|
142
|
+
const accountLabel = await this.fetchAccountLabel(data.access_token, enterpriseUrl);
|
|
143
|
+
|
|
144
|
+
// Store credentials
|
|
145
|
+
const credentials: StoredCredentials = {
|
|
146
|
+
accessToken: data.access_token,
|
|
147
|
+
accountLabel,
|
|
148
|
+
enterpriseUrl
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
await this.keyStoreService.setPassword(
|
|
152
|
+
KEYSTORE_SERVICE,
|
|
153
|
+
KEYSTORE_ACCOUNT,
|
|
154
|
+
JSON.stringify(credentials)
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// Update cached state and notify
|
|
158
|
+
const newState: CopilotAuthState = {
|
|
159
|
+
isAuthenticated: true,
|
|
160
|
+
accountLabel,
|
|
161
|
+
enterpriseUrl
|
|
162
|
+
};
|
|
163
|
+
this.updateAuthState(newState);
|
|
164
|
+
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (data.error === 'authorization_pending') {
|
|
169
|
+
// User hasn't authorized yet, continue polling
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (data.error === 'slow_down') {
|
|
174
|
+
// Increase polling interval
|
|
175
|
+
interval += 5;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (data.error === 'expired_token' || data.error === 'access_denied') {
|
|
180
|
+
console.error(`Authorization failed: ${data.error} - ${data.error_description}`);
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (data.error) {
|
|
185
|
+
console.error(`Unexpected error: ${data.error} - ${data.error_description}`);
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
protected async fetchAccountLabel(accessToken: string, enterpriseUrl?: string): Promise<string | undefined> {
|
|
194
|
+
try {
|
|
195
|
+
const apiBaseUrl = enterpriseUrl
|
|
196
|
+
? `https://${enterpriseUrl.replace(/^https?:\/\//, '').replace(/\/$/, '')}/api/v3`
|
|
197
|
+
: 'https://api.github.com';
|
|
198
|
+
|
|
199
|
+
const response = await fetch(`${apiBaseUrl}/user`, {
|
|
200
|
+
headers: {
|
|
201
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
202
|
+
'User-Agent': USER_AGENT,
|
|
203
|
+
'Accept': 'application/vnd.github.v3+json'
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
if (response.ok) {
|
|
208
|
+
const userData = await response.json() as { login?: string };
|
|
209
|
+
return userData.login;
|
|
210
|
+
}
|
|
211
|
+
} catch (error) {
|
|
212
|
+
console.warn('Failed to fetch GitHub user info:', error);
|
|
213
|
+
}
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async getAuthState(): Promise<CopilotAuthState> {
|
|
218
|
+
if (this.cachedState) {
|
|
219
|
+
return this.cachedState;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const stored = await this.keyStoreService.getPassword(KEYSTORE_SERVICE, KEYSTORE_ACCOUNT);
|
|
224
|
+
if (stored) {
|
|
225
|
+
const credentials: StoredCredentials = JSON.parse(stored);
|
|
226
|
+
this.cachedState = {
|
|
227
|
+
isAuthenticated: true,
|
|
228
|
+
accountLabel: credentials.accountLabel,
|
|
229
|
+
enterpriseUrl: credentials.enterpriseUrl
|
|
230
|
+
};
|
|
231
|
+
return this.cachedState;
|
|
232
|
+
}
|
|
233
|
+
} catch (error) {
|
|
234
|
+
console.warn('Failed to retrieve Copilot credentials:', error);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
this.cachedState = { isAuthenticated: false };
|
|
238
|
+
return this.cachedState;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async getAccessToken(): Promise<string | undefined> {
|
|
242
|
+
try {
|
|
243
|
+
const stored = await this.keyStoreService.getPassword(KEYSTORE_SERVICE, KEYSTORE_ACCOUNT);
|
|
244
|
+
if (stored) {
|
|
245
|
+
const credentials: StoredCredentials = JSON.parse(stored);
|
|
246
|
+
return credentials.accessToken;
|
|
247
|
+
}
|
|
248
|
+
} catch (error) {
|
|
249
|
+
console.warn('Failed to retrieve Copilot access token:', error);
|
|
250
|
+
}
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async signOut(): Promise<void> {
|
|
255
|
+
try {
|
|
256
|
+
await this.keyStoreService.deletePassword(KEYSTORE_SERVICE, KEYSTORE_ACCOUNT);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
console.warn('Failed to delete Copilot credentials:', error);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const newState: CopilotAuthState = { isAuthenticated: false };
|
|
262
|
+
this.updateAuthState(newState);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
protected updateAuthState(state: CopilotAuthState): void {
|
|
266
|
+
this.cachedState = state;
|
|
267
|
+
this.onAuthStateChangedEmitter.fire(state);
|
|
268
|
+
this.client?.onAuthStateChanged(state);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
protected delay(ms: number): Promise<void> {
|
|
272
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { ContainerModule } from '@theia/core/shared/inversify';
|
|
18
|
+
import { ConnectionHandler, RpcConnectionHandler } from '@theia/core';
|
|
19
|
+
import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connection-container-module';
|
|
20
|
+
import {
|
|
21
|
+
CopilotLanguageModelsManager,
|
|
22
|
+
COPILOT_LANGUAGE_MODELS_MANAGER_PATH,
|
|
23
|
+
CopilotAuthService,
|
|
24
|
+
COPILOT_AUTH_SERVICE_PATH,
|
|
25
|
+
CopilotAuthServiceClient
|
|
26
|
+
} from '../common';
|
|
27
|
+
import { CopilotLanguageModelsManagerImpl } from './copilot-language-models-manager-impl';
|
|
28
|
+
import { CopilotAuthServiceImpl } from './copilot-auth-service-impl';
|
|
29
|
+
|
|
30
|
+
const copilotConnectionModule = ConnectionContainerModule.create(({ bind }) => {
|
|
31
|
+
bind(CopilotAuthServiceImpl).toSelf().inSingletonScope();
|
|
32
|
+
bind(CopilotAuthService).toService(CopilotAuthServiceImpl);
|
|
33
|
+
|
|
34
|
+
bind(CopilotLanguageModelsManagerImpl).toSelf().inSingletonScope();
|
|
35
|
+
bind(CopilotLanguageModelsManager).toService(CopilotLanguageModelsManagerImpl);
|
|
36
|
+
|
|
37
|
+
bind(ConnectionHandler).toDynamicValue(ctx =>
|
|
38
|
+
new RpcConnectionHandler<CopilotAuthServiceClient>(
|
|
39
|
+
COPILOT_AUTH_SERVICE_PATH,
|
|
40
|
+
client => {
|
|
41
|
+
const authService = ctx.container.get<CopilotAuthServiceImpl>(CopilotAuthService);
|
|
42
|
+
authService.setClient(client);
|
|
43
|
+
return authService;
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
).inSingletonScope();
|
|
47
|
+
|
|
48
|
+
bind(ConnectionHandler).toDynamicValue(ctx =>
|
|
49
|
+
new RpcConnectionHandler(
|
|
50
|
+
COPILOT_LANGUAGE_MODELS_MANAGER_PATH,
|
|
51
|
+
() => ctx.container.get(CopilotLanguageModelsManager)
|
|
52
|
+
)
|
|
53
|
+
).inSingletonScope();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export default new ContainerModule(bind => {
|
|
57
|
+
bind(ConnectionContainerModule).toConstantValue(copilotConnectionModule);
|
|
58
|
+
});
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
ImageContent,
|
|
19
|
+
LanguageModel,
|
|
20
|
+
LanguageModelMessage,
|
|
21
|
+
LanguageModelParsedResponse,
|
|
22
|
+
LanguageModelRequest,
|
|
23
|
+
LanguageModelResponse,
|
|
24
|
+
LanguageModelStatus,
|
|
25
|
+
LanguageModelTextResponse,
|
|
26
|
+
TokenUsageService,
|
|
27
|
+
UserRequest
|
|
28
|
+
} from '@theia/ai-core';
|
|
29
|
+
import { CancellationToken } from '@theia/core';
|
|
30
|
+
import OpenAI from 'openai';
|
|
31
|
+
import { RunnableToolFunctionWithoutParse } from 'openai/lib/RunnableFunction';
|
|
32
|
+
import { ChatCompletionMessageParam } from 'openai/resources';
|
|
33
|
+
import { StreamingAsyncIterator } from '@theia/ai-openai/lib/node/openai-streaming-iterator';
|
|
34
|
+
import { COPILOT_PROVIDER_ID } from '../common';
|
|
35
|
+
import type { RunnerOptions } from 'openai/lib/AbstractChatCompletionRunner';
|
|
36
|
+
import type { ChatCompletionStream } from 'openai/lib/ChatCompletionStream';
|
|
37
|
+
|
|
38
|
+
const COPILOT_API_BASE_URL = 'https://api.githubcopilot.com';
|
|
39
|
+
const USER_AGENT = 'Theia-Copilot/1.0.0';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Language model implementation for GitHub Copilot.
|
|
43
|
+
* Uses the OpenAI SDK to communicate with the Copilot API.
|
|
44
|
+
*/
|
|
45
|
+
export class CopilotLanguageModel implements LanguageModel {
|
|
46
|
+
|
|
47
|
+
protected runnerOptions: RunnerOptions = {
|
|
48
|
+
maxChatCompletions: 100
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
constructor(
|
|
52
|
+
public readonly id: string,
|
|
53
|
+
public model: string,
|
|
54
|
+
public status: LanguageModelStatus,
|
|
55
|
+
public enableStreaming: boolean,
|
|
56
|
+
public supportsStructuredOutput: boolean,
|
|
57
|
+
public maxRetries: number,
|
|
58
|
+
protected readonly accessTokenProvider: () => Promise<string | undefined>,
|
|
59
|
+
protected readonly enterpriseUrlProvider: () => string | undefined,
|
|
60
|
+
protected readonly tokenUsageService?: TokenUsageService
|
|
61
|
+
) { }
|
|
62
|
+
|
|
63
|
+
protected getSettings(request: LanguageModelRequest): Record<string, unknown> {
|
|
64
|
+
return request.settings ?? {};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async request(request: UserRequest, cancellationToken?: CancellationToken): Promise<LanguageModelResponse> {
|
|
68
|
+
const openai = await this.initializeCopilotClient();
|
|
69
|
+
|
|
70
|
+
if (request.response_format?.type === 'json_schema' && this.supportsStructuredOutput) {
|
|
71
|
+
return this.handleStructuredOutputRequest(openai, request);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const settings = this.getSettings(request);
|
|
75
|
+
|
|
76
|
+
if (!this.enableStreaming || (typeof settings.stream === 'boolean' && !settings.stream)) {
|
|
77
|
+
return this.handleNonStreamingRequest(openai, request);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (cancellationToken?.isCancellationRequested) {
|
|
81
|
+
return { text: '' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (this.id.startsWith(`${COPILOT_PROVIDER_ID}/`)) {
|
|
85
|
+
settings['stream_options'] = { include_usage: true };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let runner: ChatCompletionStream;
|
|
89
|
+
const tools = this.createTools(request);
|
|
90
|
+
|
|
91
|
+
if (tools) {
|
|
92
|
+
runner = openai.chat.completions.runTools({
|
|
93
|
+
model: this.model,
|
|
94
|
+
messages: this.processMessages(request.messages),
|
|
95
|
+
stream: true,
|
|
96
|
+
tools: tools,
|
|
97
|
+
tool_choice: 'auto',
|
|
98
|
+
...settings
|
|
99
|
+
}, {
|
|
100
|
+
...this.runnerOptions,
|
|
101
|
+
maxRetries: this.maxRetries
|
|
102
|
+
});
|
|
103
|
+
} else {
|
|
104
|
+
runner = openai.chat.completions.stream({
|
|
105
|
+
model: this.model,
|
|
106
|
+
messages: this.processMessages(request.messages),
|
|
107
|
+
stream: true,
|
|
108
|
+
...settings
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
113
|
+
return { stream: new StreamingAsyncIterator(runner as any, request.requestId, cancellationToken, this.tokenUsageService, this.id) };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
protected async handleNonStreamingRequest(openai: OpenAI, request: UserRequest): Promise<LanguageModelTextResponse> {
|
|
117
|
+
const settings = this.getSettings(request);
|
|
118
|
+
const response = await openai.chat.completions.create({
|
|
119
|
+
model: this.model,
|
|
120
|
+
messages: this.processMessages(request.messages),
|
|
121
|
+
...settings
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const message = response.choices[0].message;
|
|
125
|
+
|
|
126
|
+
if (this.tokenUsageService && response.usage) {
|
|
127
|
+
await this.tokenUsageService.recordTokenUsage(
|
|
128
|
+
this.id,
|
|
129
|
+
{
|
|
130
|
+
inputTokens: response.usage.prompt_tokens,
|
|
131
|
+
outputTokens: response.usage.completion_tokens,
|
|
132
|
+
requestId: request.requestId
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
text: message.content ?? ''
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
protected async handleStructuredOutputRequest(openai: OpenAI, request: UserRequest): Promise<LanguageModelParsedResponse> {
|
|
143
|
+
const settings = this.getSettings(request);
|
|
144
|
+
const result = await openai.chat.completions.parse({
|
|
145
|
+
model: this.model,
|
|
146
|
+
messages: this.processMessages(request.messages),
|
|
147
|
+
response_format: request.response_format,
|
|
148
|
+
...settings
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const message = result.choices[0].message;
|
|
152
|
+
if (message.refusal || message.parsed === undefined) {
|
|
153
|
+
console.error('Error in Copilot chat completion:', JSON.stringify(message));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (this.tokenUsageService && result.usage) {
|
|
157
|
+
await this.tokenUsageService.recordTokenUsage(
|
|
158
|
+
this.id,
|
|
159
|
+
{
|
|
160
|
+
inputTokens: result.usage.prompt_tokens,
|
|
161
|
+
outputTokens: result.usage.completion_tokens,
|
|
162
|
+
requestId: request.requestId
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
content: message.content ?? '',
|
|
169
|
+
parsed: message.parsed
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
protected createTools(request: LanguageModelRequest): RunnableToolFunctionWithoutParse[] | undefined {
|
|
174
|
+
return request.tools?.map(tool => ({
|
|
175
|
+
type: 'function',
|
|
176
|
+
function: {
|
|
177
|
+
name: tool.name,
|
|
178
|
+
description: tool.description,
|
|
179
|
+
parameters: tool.parameters,
|
|
180
|
+
function: (args_string: string) => tool.handler(args_string)
|
|
181
|
+
}
|
|
182
|
+
} as RunnableToolFunctionWithoutParse));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
protected async initializeCopilotClient(): Promise<OpenAI> {
|
|
186
|
+
const accessToken = await this.accessTokenProvider();
|
|
187
|
+
if (!accessToken) {
|
|
188
|
+
throw new Error('Not authenticated with GitHub Copilot. Please sign in first.');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const enterpriseUrl = this.enterpriseUrlProvider();
|
|
192
|
+
const baseURL = enterpriseUrl
|
|
193
|
+
? `https://copilot-api.${enterpriseUrl.replace(/^https?:\/\//, '').replace(/\/$/, '')}`
|
|
194
|
+
: COPILOT_API_BASE_URL;
|
|
195
|
+
|
|
196
|
+
return new OpenAI({
|
|
197
|
+
apiKey: accessToken,
|
|
198
|
+
baseURL,
|
|
199
|
+
defaultHeaders: {
|
|
200
|
+
'User-Agent': USER_AGENT,
|
|
201
|
+
'Openai-Intent': 'conversation-edits',
|
|
202
|
+
'X-Initiator': 'user'
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
protected processMessages(messages: LanguageModelMessage[]): ChatCompletionMessageParam[] {
|
|
208
|
+
return messages.filter(m => m.type !== 'thinking').map(m => this.toOpenAIMessage(m));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
protected toOpenAIMessage(message: LanguageModelMessage): ChatCompletionMessageParam {
|
|
212
|
+
if (LanguageModelMessage.isTextMessage(message)) {
|
|
213
|
+
return {
|
|
214
|
+
role: this.toOpenAiRole(message),
|
|
215
|
+
content: message.text
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if (LanguageModelMessage.isToolUseMessage(message)) {
|
|
219
|
+
return {
|
|
220
|
+
role: 'assistant',
|
|
221
|
+
tool_calls: [{
|
|
222
|
+
id: message.id,
|
|
223
|
+
function: {
|
|
224
|
+
name: message.name,
|
|
225
|
+
arguments: JSON.stringify(message.input)
|
|
226
|
+
},
|
|
227
|
+
type: 'function'
|
|
228
|
+
}]
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
if (LanguageModelMessage.isToolResultMessage(message)) {
|
|
232
|
+
return {
|
|
233
|
+
role: 'tool',
|
|
234
|
+
tool_call_id: message.tool_use_id,
|
|
235
|
+
content: typeof message.content === 'string' ? message.content : JSON.stringify(message.content)
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
if (LanguageModelMessage.isImageMessage(message) && message.actor === 'user') {
|
|
239
|
+
return {
|
|
240
|
+
role: 'user',
|
|
241
|
+
content: [{
|
|
242
|
+
type: 'image_url',
|
|
243
|
+
image_url: {
|
|
244
|
+
url: ImageContent.isBase64(message.image)
|
|
245
|
+
? `data:${message.image.mimeType};base64,${message.image.base64data}`
|
|
246
|
+
: message.image.url
|
|
247
|
+
}
|
|
248
|
+
}]
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
throw new Error(`Unknown message type: '${JSON.stringify(message)}'`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
protected toOpenAiRole(message: LanguageModelMessage): 'developer' | 'user' | 'assistant' | 'system' {
|
|
255
|
+
if (message.actor === 'system') {
|
|
256
|
+
return 'developer';
|
|
257
|
+
} else if (message.actor === 'ai') {
|
|
258
|
+
return 'assistant';
|
|
259
|
+
}
|
|
260
|
+
return 'user';
|
|
261
|
+
}
|
|
262
|
+
}
|