@theia/ai-copilot 1.76.0-next.4 → 1.76.0-next.42
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 +87 -14
- package/lib/browser/copilot-auth-dialog.d.ts.map +1 -1
- package/lib/browser/copilot-auth-dialog.js +19 -11
- package/lib/browser/copilot-auth-dialog.js.map +1 -1
- package/lib/browser/copilot-frontend-application-contribution.d.ts +21 -1
- package/lib/browser/copilot-frontend-application-contribution.d.ts.map +1 -1
- package/lib/browser/copilot-frontend-application-contribution.js +129 -26
- package/lib/browser/copilot-frontend-application-contribution.js.map +1 -1
- package/lib/common/copilot-auth-service.d.ts +27 -24
- package/lib/common/copilot-auth-service.d.ts.map +1 -1
- package/lib/common/copilot-language-models-manager.d.ts +6 -22
- package/lib/common/copilot-language-models-manager.d.ts.map +1 -1
- package/lib/common/copilot-language-models-manager.js +1 -13
- package/lib/common/copilot-language-models-manager.js.map +1 -1
- package/lib/common/copilot-preferences.d.ts +1 -0
- package/lib/common/copilot-preferences.d.ts.map +1 -1
- package/lib/common/copilot-preferences.js +24 -6
- package/lib/common/copilot-preferences.js.map +1 -1
- package/lib/common/index.d.ts +0 -1
- package/lib/common/index.d.ts.map +1 -1
- package/lib/common/index.js +0 -1
- package/lib/common/index.js.map +1 -1
- package/lib/node/copilot-auth-service-impl.d.ts +25 -16
- package/lib/node/copilot-auth-service-impl.d.ts.map +1 -1
- package/lib/node/copilot-auth-service-impl.js +67 -183
- package/lib/node/copilot-auth-service-impl.js.map +1 -1
- package/lib/node/copilot-auth-service-impl.spec.js +104 -168
- package/lib/node/copilot-auth-service-impl.spec.js.map +1 -1
- package/lib/node/copilot-backend-module.d.ts.map +1 -1
- package/lib/node/copilot-backend-module.js +10 -2
- package/lib/node/copilot-backend-module.js.map +1 -1
- package/lib/node/copilot-cli-auth-provider.d.ts +146 -0
- package/lib/node/copilot-cli-auth-provider.d.ts.map +1 -0
- package/lib/node/copilot-cli-auth-provider.js +430 -0
- package/lib/node/copilot-cli-auth-provider.js.map +1 -0
- package/lib/node/copilot-cli-auth-provider.spec.d.ts +2 -0
- package/lib/node/copilot-cli-auth-provider.spec.d.ts.map +1 -0
- package/lib/node/copilot-cli-auth-provider.spec.js +249 -0
- package/lib/node/copilot-cli-auth-provider.spec.js.map +1 -0
- package/lib/node/copilot-cli-locator.d.ts +120 -0
- package/lib/node/copilot-cli-locator.d.ts.map +1 -0
- package/lib/node/copilot-cli-locator.js +348 -0
- package/lib/node/copilot-cli-locator.js.map +1 -0
- package/lib/node/copilot-cli-locator.spec.d.ts +2 -0
- package/lib/node/copilot-cli-locator.spec.d.ts.map +1 -0
- package/lib/node/copilot-cli-locator.spec.js +327 -0
- package/lib/node/copilot-cli-locator.spec.js.map +1 -0
- package/lib/node/copilot-credential-store.d.ts +45 -0
- package/lib/node/copilot-credential-store.d.ts.map +1 -0
- package/lib/node/copilot-credential-store.js +96 -0
- package/lib/node/copilot-credential-store.js.map +1 -0
- package/lib/node/copilot-credential-store.spec.d.ts +2 -0
- package/lib/node/copilot-credential-store.spec.d.ts.map +1 -0
- package/lib/node/copilot-credential-store.spec.js +93 -0
- package/lib/node/copilot-credential-store.spec.js.map +1 -0
- package/lib/node/copilot-language-models-manager-impl.d.ts +44 -20
- package/lib/node/copilot-language-models-manager-impl.d.ts.map +1 -1
- package/lib/node/copilot-language-models-manager-impl.js +112 -65
- package/lib/node/copilot-language-models-manager-impl.js.map +1 -1
- package/lib/node/copilot-language-models-manager-impl.spec.d.ts +2 -0
- package/lib/node/copilot-language-models-manager-impl.spec.d.ts.map +1 -0
- package/lib/node/copilot-language-models-manager-impl.spec.js +162 -0
- package/lib/node/copilot-language-models-manager-impl.spec.js.map +1 -0
- package/lib/node/copilot-runtime-env.d.ts +20 -0
- package/lib/node/copilot-runtime-env.d.ts.map +1 -0
- package/lib/node/copilot-runtime-env.js +49 -0
- package/lib/node/copilot-runtime-env.js.map +1 -0
- package/lib/node/copilot-sdk-client-provider.d.ts +103 -0
- package/lib/node/copilot-sdk-client-provider.d.ts.map +1 -0
- package/lib/node/copilot-sdk-client-provider.js +247 -0
- package/lib/node/copilot-sdk-client-provider.js.map +1 -0
- package/lib/node/copilot-sdk-client-provider.spec.d.ts +2 -0
- package/lib/node/copilot-sdk-client-provider.spec.d.ts.map +1 -0
- package/lib/node/copilot-sdk-client-provider.spec.js +215 -0
- package/lib/node/copilot-sdk-client-provider.spec.js.map +1 -0
- package/lib/node/copilot-sdk-language-model.d.ts +85 -0
- package/lib/node/copilot-sdk-language-model.d.ts.map +1 -0
- package/lib/node/copilot-sdk-language-model.js +301 -0
- package/lib/node/copilot-sdk-language-model.js.map +1 -0
- package/lib/node/copilot-sdk-language-model.spec.d.ts +2 -0
- package/lib/node/copilot-sdk-language-model.spec.d.ts.map +1 -0
- package/lib/node/copilot-sdk-language-model.spec.js +315 -0
- package/lib/node/copilot-sdk-language-model.spec.js.map +1 -0
- package/lib/node/copilot-sdk-loader.d.ts +57 -0
- package/lib/node/copilot-sdk-loader.d.ts.map +1 -0
- package/lib/node/copilot-sdk-loader.js +133 -0
- package/lib/node/copilot-sdk-loader.js.map +1 -0
- package/lib/node/copilot-sdk-loader.spec.d.ts +2 -0
- package/lib/node/copilot-sdk-loader.spec.d.ts.map +1 -0
- package/lib/node/copilot-sdk-loader.spec.js +113 -0
- package/lib/node/copilot-sdk-loader.spec.js.map +1 -0
- package/lib/node/copilot-sdk-mappers.d.ts +54 -0
- package/lib/node/copilot-sdk-mappers.d.ts.map +1 -0
- package/lib/node/copilot-sdk-mappers.js +158 -0
- package/lib/node/copilot-sdk-mappers.js.map +1 -0
- package/lib/node/copilot-sdk-mappers.spec.d.ts +2 -0
- package/lib/node/copilot-sdk-mappers.spec.d.ts.map +1 -0
- package/lib/node/copilot-sdk-mappers.spec.js +168 -0
- package/lib/node/copilot-sdk-mappers.spec.js.map +1 -0
- package/lib/node/copilot-sdk-types.d.ts +208 -0
- package/lib/node/copilot-sdk-types.d.ts.map +1 -0
- package/lib/{common/copilot-oauth-config.js → node/copilot-sdk-types.js} +2 -13
- package/lib/node/copilot-sdk-types.js.map +1 -0
- package/lib/node/index.d.ts +9 -1
- package/lib/node/index.d.ts.map +1 -1
- package/lib/node/index.js +9 -1
- package/lib/node/index.js.map +1 -1
- package/package.json +4 -6
- package/src/browser/copilot-auth-dialog.tsx +20 -16
- package/src/browser/copilot-frontend-application-contribution.ts +144 -31
- package/src/common/copilot-auth-service.ts +28 -24
- package/src/common/copilot-language-models-manager.ts +6 -29
- package/src/common/copilot-preferences.ts +24 -5
- package/src/common/index.ts +0 -1
- package/src/node/copilot-auth-service-impl.spec.ts +122 -207
- package/src/node/copilot-auth-service-impl.ts +63 -218
- package/src/node/copilot-backend-module.ts +11 -2
- package/src/node/copilot-cli-auth-provider.spec.ts +315 -0
- package/src/node/copilot-cli-auth-provider.ts +444 -0
- package/src/node/copilot-cli-locator.spec.ts +386 -0
- package/src/node/copilot-cli-locator.ts +363 -0
- package/src/node/copilot-credential-store.spec.ts +106 -0
- package/src/node/copilot-credential-store.ts +99 -0
- package/src/node/copilot-language-models-manager-impl.spec.ts +188 -0
- package/src/node/copilot-language-models-manager-impl.ts +131 -84
- package/src/node/copilot-runtime-env.ts +46 -0
- package/src/node/copilot-sdk-client-provider.spec.ts +253 -0
- package/src/node/copilot-sdk-client-provider.ts +241 -0
- package/src/node/copilot-sdk-language-model.spec.ts +377 -0
- package/src/node/copilot-sdk-language-model.ts +330 -0
- package/src/node/copilot-sdk-loader.spec.ts +126 -0
- package/src/node/copilot-sdk-loader.ts +142 -0
- package/src/node/copilot-sdk-mappers.spec.ts +190 -0
- package/src/node/copilot-sdk-mappers.ts +172 -0
- package/src/node/copilot-sdk-types.ts +290 -0
- package/src/node/index.ts +9 -1
- package/lib/common/copilot-oauth-config.d.ts +0 -33
- package/lib/common/copilot-oauth-config.d.ts.map +0 -1
- package/lib/common/copilot-oauth-config.js.map +0 -1
- package/lib/node/copilot-language-model.d.ts +0 -34
- package/lib/node/copilot-language-model.d.ts.map +0 -1
- package/lib/node/copilot-language-model.js +0 -236
- package/lib/node/copilot-language-model.js.map +0 -1
- package/lib/node/copilot-language-model.spec.d.ts +0 -2
- package/lib/node/copilot-language-model.spec.d.ts.map +0 -1
- package/lib/node/copilot-language-model.spec.js +0 -89
- package/lib/node/copilot-language-model.spec.js.map +0 -1
- package/src/common/copilot-oauth-config.ts +0 -55
- package/src/node/copilot-language-model.spec.ts +0 -106
- package/src/node/copilot-language-model.ts +0 -273
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH and others.
|
|
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 { expect } from 'chai';
|
|
18
|
+
import { LanguageModel, LanguageModelStatus } from '@theia/ai-core';
|
|
19
|
+
import { MockLogger } from '@theia/core/lib/common/test/mock-logger';
|
|
20
|
+
import { CopilotAuthState } from '../common/copilot-auth-service';
|
|
21
|
+
import { CopilotLanguageModelsManagerImpl } from './copilot-language-models-manager-impl';
|
|
22
|
+
|
|
23
|
+
/** A registry that records the models and the patches it received. */
|
|
24
|
+
class FakeRegistry {
|
|
25
|
+
|
|
26
|
+
readonly models: LanguageModel[] = [];
|
|
27
|
+
readonly patches: Array<{ id: string, patch: Partial<LanguageModel> }> = [];
|
|
28
|
+
|
|
29
|
+
async getLanguageModel(id: string): Promise<LanguageModel | undefined> {
|
|
30
|
+
return this.models.find(model => model.id === id);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async getLanguageModels(): Promise<LanguageModel[]> {
|
|
34
|
+
return this.models;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
addLanguageModels(models: LanguageModel[]): void {
|
|
38
|
+
this.models.push(...models);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
removeLanguageModels(ids: string[]): void {
|
|
42
|
+
for (const id of ids) {
|
|
43
|
+
const index = this.models.findIndex(model => model.id === id);
|
|
44
|
+
if (index >= 0) {
|
|
45
|
+
this.models.splice(index, 1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async patchLanguageModel(id: string, patch: Partial<LanguageModel>): Promise<void> {
|
|
51
|
+
this.patches.push({ id, patch });
|
|
52
|
+
Object.assign(this.models.find(model => model.id === id) ?? {}, patch);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class TestableCopilotLanguageModelsManagerImpl extends CopilotLanguageModelsManagerImpl {
|
|
57
|
+
|
|
58
|
+
authState: CopilotAuthState = { isAuthenticated: true };
|
|
59
|
+
listFailure: Error | undefined;
|
|
60
|
+
modelIds: string[] = ['gpt-5'];
|
|
61
|
+
|
|
62
|
+
callVendorOf(id: string): string | undefined {
|
|
63
|
+
return this.vendorOf(id);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
constructor(readonly registry: FakeRegistry) {
|
|
67
|
+
super();
|
|
68
|
+
Object.assign(this, {
|
|
69
|
+
logger: new MockLogger(),
|
|
70
|
+
languageModelRegistry: registry,
|
|
71
|
+
authService: {
|
|
72
|
+
getAuthState: async () => this.authState,
|
|
73
|
+
onAuthStateChanged: () => ({ dispose: () => { } })
|
|
74
|
+
},
|
|
75
|
+
sdkClientProvider: {
|
|
76
|
+
listModelIds: async () => {
|
|
77
|
+
if (this.listFailure) {
|
|
78
|
+
throw this.listFailure;
|
|
79
|
+
}
|
|
80
|
+
return this.modelIds;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
callCalculateStatus(): Promise<LanguageModelStatus> {
|
|
87
|
+
return this.calculateStatus();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
describe('CopilotLanguageModelsManagerImpl - status', () => {
|
|
92
|
+
|
|
93
|
+
let manager: TestableCopilotLanguageModelsManagerImpl;
|
|
94
|
+
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
manager = new TestableCopilotLanguageModelsManagerImpl(new FakeRegistry());
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should report a model as ready for a user who is signed in', async () => {
|
|
100
|
+
await manager.createOrUpdateLanguageModels({ id: 'copilot/gpt-5', model: 'gpt-5', maxRetries: 3 });
|
|
101
|
+
expect(manager.registry.models[0].status).to.deep.equal({ status: 'ready' });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should report a model as unavailable while nobody is signed in', async () => {
|
|
105
|
+
manager.authState = { isAuthenticated: false };
|
|
106
|
+
expect(await manager.callCalculateStatus()).to.deep.include({ status: 'unavailable' });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should report models as unavailable when they cannot be listed, rather than as ready', async () => {
|
|
110
|
+
manager.listFailure = new Error('not authorized to use this Copilot feature');
|
|
111
|
+
const result = await manager.fetchAvailableModels();
|
|
112
|
+
expect(result.models).to.be.empty;
|
|
113
|
+
expect(result.error).to.contain('not authorized');
|
|
114
|
+
const status = await manager.callCalculateStatus();
|
|
115
|
+
expect(status.status).to.equal('unavailable');
|
|
116
|
+
expect(status.message).to.contain('not authorized');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should reflect a failed listing in the models that are already registered', async () => {
|
|
120
|
+
await manager.createOrUpdateLanguageModels({ id: 'copilot/gpt-5', model: 'gpt-5', maxRetries: 3 });
|
|
121
|
+
manager.listFailure = new Error('socket hang up');
|
|
122
|
+
await manager.fetchAvailableModels();
|
|
123
|
+
expect(manager.registry.patches.map(patch => patch.id)).to.deep.equal(['copilot/gpt-5']);
|
|
124
|
+
expect(manager.registry.models[0].status).to.deep.include({ status: 'unavailable' });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should report ready again once the models can be listed', async () => {
|
|
128
|
+
await manager.createOrUpdateLanguageModels({ id: 'copilot/gpt-5', model: 'gpt-5', maxRetries: 3 });
|
|
129
|
+
manager.listFailure = new Error('socket hang up');
|
|
130
|
+
await manager.fetchAvailableModels();
|
|
131
|
+
manager.listFailure = undefined;
|
|
132
|
+
expect((await manager.fetchAvailableModels()).models).to.deep.equal([{ id: 'gpt-5', featured: true }]);
|
|
133
|
+
expect(manager.registry.models[0].status).to.deep.equal({ status: 'ready' });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('nominates auto and one model of each major vendor before a second of any', async () => {
|
|
137
|
+
manager.modelIds = ['auto', 'gpt-5', 'gpt-4.1', 'claude-sonnet-4.5', 'claude-haiku-4', 'gemini-2.5-pro', 'gpt-5-2026-04-17'];
|
|
138
|
+
const featured = (await manager.fetchAvailableModels()).models.filter(model => model.featured).map(model => model.id);
|
|
139
|
+
// Five in total: auto, the newest of each vendor, then the next of the first vendor with one left.
|
|
140
|
+
expect(featured).to.have.members(['auto', 'gpt-5', 'claude-sonnet-4.5', 'gemini-2.5-pro', 'claude-haiku-4']);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('fills the list to five even when the CLI offers no auto', async () => {
|
|
144
|
+
manager.modelIds = ['gpt-5', 'gpt-4.1', 'claude-sonnet-4.5', 'claude-haiku-4', 'gemini-3-pro', 'gemini-2.5-pro'];
|
|
145
|
+
const featured = (await manager.fetchAvailableModels()).models.filter(model => model.featured).map(model => model.id);
|
|
146
|
+
expect(featured).to.have.members(['gpt-5', 'claude-sonnet-4.5', 'gemini-3-pro', 'gpt-4.1', 'claude-haiku-4']);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('nominates no more than the five, however much one vendor offers', async () => {
|
|
150
|
+
manager.modelIds = ['gpt-5', 'gpt-5-mini', 'gpt-4.1', 'gpt-4o', 'o5-preview', 'chatgpt-5-latest'];
|
|
151
|
+
const featured = (await manager.fetchAvailableModels()).models.filter(model => model.featured).map(model => model.id);
|
|
152
|
+
expect(featured).to.have.lengthOf(5);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('counts the reasoning models as OpenAI, rather than as a vendor of their own', () => {
|
|
156
|
+
expect(manager.callVendorOf('o5-preview')).to.equal('openai');
|
|
157
|
+
expect(manager.callVendorOf('gpt-4.1')).to.equal('openai');
|
|
158
|
+
expect(manager.callVendorOf('claude-sonnet-4.5')).to.equal('anthropic');
|
|
159
|
+
expect(manager.callVendorOf('grok-code-fast-1')).to.equal(undefined);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('nominates nothing for the vendors beyond the three, which stay selectable but not preselected', async () => {
|
|
163
|
+
manager.modelIds = ['claude-sonnet-4.5', 'grok-code-fast-1', 'mistral-large', 'llama-4-maverick'];
|
|
164
|
+
const result = await manager.fetchAvailableModels();
|
|
165
|
+
expect(result.models.map(model => model.id)).to.have.lengthOf(4);
|
|
166
|
+
expect(result.models.filter(model => model.featured).map(model => model.id)).to.deep.equal(['claude-sonnet-4.5']);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('recognises a vendor-qualified id, in case the CLI reports them that way', async () => {
|
|
170
|
+
manager.modelIds = ['google/gemini-2.5-pro', 'anthropic/claude-sonnet-4.5'];
|
|
171
|
+
const featured = (await manager.fetchAvailableModels()).models.filter(model => model.featured).map(model => model.id);
|
|
172
|
+
expect(featured).to.have.members(['google/gemini-2.5-pro', 'anthropic/claude-sonnet-4.5']);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('never nominates a release-pinned id, whose undated form is nominated instead', async () => {
|
|
176
|
+
manager.modelIds = ['gpt-5', 'gpt-5-2026-04-17'];
|
|
177
|
+
const featured = (await manager.fetchAvailableModels()).models.filter(model => model.featured).map(model => model.id);
|
|
178
|
+
expect(featured).to.deep.equal(['gpt-5']);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should update an existing model instead of registering it twice', async () => {
|
|
182
|
+
await manager.createOrUpdateLanguageModels({ id: 'copilot/gpt-5', model: 'gpt-5', maxRetries: 3 });
|
|
183
|
+
await manager.createOrUpdateLanguageModels({ id: 'copilot/gpt-5', model: 'gpt-5', maxRetries: 5 });
|
|
184
|
+
expect(manager.registry.models).to.have.lengthOf(1);
|
|
185
|
+
expect(manager.registry.patches).to.have.lengthOf(1);
|
|
186
|
+
expect(manager.registry.patches[0].patch).to.deep.include({ maxRetries: 5 });
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -14,12 +14,30 @@
|
|
|
14
14
|
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
|
-
import { LanguageModelRegistry, LanguageModelStatus } from '@theia/ai-core';
|
|
18
|
-
import {
|
|
19
|
-
import { inject, injectable, postConstruct,
|
|
20
|
-
import { CopilotLanguageModelsManager, CopilotModelDescription, COPILOT_PROVIDER_ID
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
import { DiscoveredModels, LanguageModelRegistry, LanguageModelStatus, ModelDiscoveryResult } from '@theia/ai-core';
|
|
18
|
+
import { DisposableCollection, ILogger, nls } from '@theia/core';
|
|
19
|
+
import { inject, injectable, named, postConstruct, preDestroy } from '@theia/core/shared/inversify';
|
|
20
|
+
import { CopilotLanguageModelsManager, CopilotModelDescription, COPILOT_PROVIDER_ID } from '../common';
|
|
21
|
+
|
|
22
|
+
/** The id under which Copilot offers its own model selection; always worth showing. */
|
|
23
|
+
const COPILOT_AUTO_MODEL_ID = 'auto';
|
|
24
|
+
|
|
25
|
+
/** How many models Copilot nominates, matching what the other providers feature. */
|
|
26
|
+
const NOMINATED_MODEL_COUNT = 5;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The vendors one Copilot model each is nominated for, recognised by the prefix of the model id.
|
|
30
|
+
* Deliberately the three whose models the other providers of this application offer directly: those
|
|
31
|
+
* are the ones a user coming to the chat input expects to switch between. Copilot carries more, and
|
|
32
|
+
* they remain registered and selectable — just not preselected.
|
|
33
|
+
*/
|
|
34
|
+
const NOMINATED_VENDORS: ReadonlyArray<{ vendor: string; matches: RegExp }> = [
|
|
35
|
+
{ vendor: 'anthropic', matches: /^claude/i },
|
|
36
|
+
{ vendor: 'openai', matches: /^(gpt|chatgpt|o\d)/i },
|
|
37
|
+
{ vendor: 'google', matches: /^gemini/i }
|
|
38
|
+
];
|
|
39
|
+
import { CopilotSdkLanguageModel } from './copilot-sdk-language-model';
|
|
40
|
+
import { CopilotSdkClientProvider } from './copilot-sdk-client-provider';
|
|
23
41
|
import { CopilotAuthServiceImpl } from './copilot-auth-service-impl';
|
|
24
42
|
|
|
25
43
|
/**
|
|
@@ -27,7 +45,7 @@ import { CopilotAuthServiceImpl } from './copilot-auth-service-impl';
|
|
|
27
45
|
* Manages registration and lifecycle of Copilot language models in the AI language model registry.
|
|
28
46
|
*/
|
|
29
47
|
@injectable()
|
|
30
|
-
export class CopilotLanguageModelsManagerImpl implements CopilotLanguageModelsManager
|
|
48
|
+
export class CopilotLanguageModelsManagerImpl implements CopilotLanguageModelsManager {
|
|
31
49
|
|
|
32
50
|
@inject(LanguageModelRegistry)
|
|
33
51
|
protected readonly languageModelRegistry: LanguageModelRegistry;
|
|
@@ -35,36 +53,51 @@ export class CopilotLanguageModelsManagerImpl implements CopilotLanguageModelsMa
|
|
|
35
53
|
@inject(CopilotAuthServiceImpl)
|
|
36
54
|
protected readonly authService: CopilotAuthServiceImpl;
|
|
37
55
|
|
|
38
|
-
@inject(
|
|
39
|
-
protected readonly
|
|
56
|
+
@inject(CopilotSdkClientProvider)
|
|
57
|
+
protected readonly sdkClientProvider: CopilotSdkClientProvider;
|
|
40
58
|
|
|
41
59
|
@inject(ILogger) @named('ai-copilot:CopilotLanguageModelsManagerImpl')
|
|
42
60
|
protected readonly logger: ILogger;
|
|
43
61
|
|
|
44
|
-
protected enterpriseUrl: string | undefined;
|
|
45
62
|
protected readonly toDispose = new DisposableCollection();
|
|
46
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Why the last model discovery failed, if it did.
|
|
66
|
+
*
|
|
67
|
+
* Being signed in does not imply that requests are accepted: the credentials can lack the Copilot
|
|
68
|
+
* entitlement, be revoked, or belong to a subscription served by a different API host. Reporting
|
|
69
|
+
* the models as ready in those cases claims a working setup that is not there, so the failure is
|
|
70
|
+
* remembered and reflected in the status of the models.
|
|
71
|
+
*/
|
|
72
|
+
protected discoveryFailure: string | undefined;
|
|
73
|
+
|
|
47
74
|
@postConstruct()
|
|
48
75
|
protected init(): void {
|
|
49
76
|
this.toDispose.push(this.authService.onAuthStateChanged(() => {
|
|
50
|
-
|
|
77
|
+
// The listener cannot be awaited, so the failure is reported here rather than escaping as
|
|
78
|
+
// an unhandled rejection.
|
|
79
|
+
this.refreshModelsStatus().catch(error => this.logger.warn('Copilot: failed to update the status of the models:', error));
|
|
51
80
|
}));
|
|
52
81
|
}
|
|
53
82
|
|
|
54
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Detaches from the auth service when the container this manager lives in goes away.
|
|
85
|
+
* Unbinding a container runs `@preDestroy` rather than `dispose`, see the client provider.
|
|
86
|
+
*/
|
|
87
|
+
@preDestroy()
|
|
88
|
+
protected stop(): void {
|
|
55
89
|
this.toDispose.dispose();
|
|
56
90
|
}
|
|
57
91
|
|
|
58
|
-
setEnterpriseUrl(url: string | undefined): void {
|
|
59
|
-
this.enterpriseUrl = url;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
92
|
protected async calculateStatus(): Promise<LanguageModelStatus> {
|
|
63
93
|
const authState = await this.authService.getAuthState();
|
|
64
|
-
if (authState.isAuthenticated) {
|
|
65
|
-
return { status: '
|
|
94
|
+
if (!authState.isAuthenticated) {
|
|
95
|
+
return { status: 'unavailable', message: nls.localize('theia/ai/copilot/notSignedIn', 'Not signed in to GitHub Copilot') };
|
|
66
96
|
}
|
|
67
|
-
|
|
97
|
+
if (this.discoveryFailure) {
|
|
98
|
+
return { status: 'unavailable', message: this.discoveryFailure };
|
|
99
|
+
}
|
|
100
|
+
return { status: 'ready' };
|
|
68
101
|
}
|
|
69
102
|
|
|
70
103
|
async createOrUpdateLanguageModels(...modelDescriptions: CopilotModelDescription[]): Promise<void> {
|
|
@@ -74,29 +107,24 @@ export class CopilotLanguageModelsManagerImpl implements CopilotLanguageModelsMa
|
|
|
74
107
|
const model = await this.languageModelRegistry.getLanguageModel(modelDescription.id);
|
|
75
108
|
|
|
76
109
|
if (model) {
|
|
77
|
-
if (!(model instanceof
|
|
110
|
+
if (!(model instanceof CopilotSdkLanguageModel)) {
|
|
78
111
|
this.logger.warn(`Copilot: model ${modelDescription.id} is not a Copilot model`);
|
|
79
112
|
continue;
|
|
80
113
|
}
|
|
81
|
-
await this.languageModelRegistry.patchLanguageModel<
|
|
114
|
+
await this.languageModelRegistry.patchLanguageModel<CopilotSdkLanguageModel>(modelDescription.id, {
|
|
82
115
|
model: modelDescription.model,
|
|
83
|
-
enableStreaming: modelDescription.enableStreaming,
|
|
84
|
-
supportsStructuredOutput: modelDescription.supportsStructuredOutput,
|
|
85
116
|
status,
|
|
86
117
|
maxRetries: modelDescription.maxRetries
|
|
87
118
|
});
|
|
88
119
|
} else {
|
|
89
120
|
this.languageModelRegistry.addLanguageModels([
|
|
90
|
-
new
|
|
121
|
+
new CopilotSdkLanguageModel(
|
|
91
122
|
modelDescription.id,
|
|
92
123
|
modelDescription.model,
|
|
93
124
|
status,
|
|
94
|
-
modelDescription.enableStreaming,
|
|
95
|
-
modelDescription.supportsStructuredOutput,
|
|
96
125
|
modelDescription.maxRetries,
|
|
97
|
-
() => this.
|
|
98
|
-
|
|
99
|
-
() => this.oauthConfig.userAgent
|
|
126
|
+
() => this.sdkClientProvider.getClient(),
|
|
127
|
+
this.logger
|
|
100
128
|
)
|
|
101
129
|
]);
|
|
102
130
|
}
|
|
@@ -112,79 +140,98 @@ export class CopilotLanguageModelsManagerImpl implements CopilotLanguageModelsMa
|
|
|
112
140
|
const allModels = await this.languageModelRegistry.getLanguageModels();
|
|
113
141
|
|
|
114
142
|
for (const model of allModels) {
|
|
115
|
-
if (model instanceof
|
|
116
|
-
await this.languageModelRegistry.patchLanguageModel<
|
|
143
|
+
if (model instanceof CopilotSdkLanguageModel && model.id.startsWith(`${COPILOT_PROVIDER_ID}/`)) {
|
|
144
|
+
await this.languageModelRegistry.patchLanguageModel<CopilotSdkLanguageModel>(model.id, {
|
|
117
145
|
status
|
|
118
146
|
});
|
|
119
147
|
}
|
|
120
148
|
}
|
|
121
149
|
}
|
|
122
150
|
|
|
123
|
-
async
|
|
124
|
-
const accessToken = await this.authService.getAccessToken();
|
|
125
|
-
if (!accessToken) {
|
|
126
|
-
return [];
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const baseURL = getCopilotApiBaseUrl(this.enterpriseUrl);
|
|
130
|
-
|
|
151
|
+
async fetchAvailableModels(): Promise<ModelDiscoveryResult> {
|
|
131
152
|
try {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
if (!response.ok) {
|
|
141
|
-
this.logger.warn(`Copilot: failed to fetch available models: ${response.status} ${response.statusText}`);
|
|
142
|
-
return [];
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const data = await response.json() as {
|
|
146
|
-
data?: Array<{ id: string; capabilities?: { family?: string } }>
|
|
147
|
-
};
|
|
148
|
-
const models = data.data ?? [];
|
|
149
|
-
const modelIds = this.deduplicateModels(models);
|
|
150
|
-
this.logger.info(`Copilot: discovered ${modelIds.length} available models: ${modelIds.join(', ')}`);
|
|
151
|
-
return modelIds;
|
|
153
|
+
const modelIds = await this.sdkClientProvider.listModelIds();
|
|
154
|
+
this.logger.info(`Copilot: discovered ${modelIds.length} models [${modelIds.join(', ')}]`);
|
|
155
|
+
await this.setDiscoveryFailure(undefined);
|
|
156
|
+
// The CLI reports nothing but the id: no display name, and no release date to order by.
|
|
157
|
+
const featured = this.selectFeaturedModelIds(modelIds);
|
|
158
|
+
return { models: modelIds.map(id => ({ id, featured: featured.has(id) })), fromCache: false };
|
|
152
159
|
} catch (error) {
|
|
153
|
-
|
|
154
|
-
|
|
160
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
161
|
+
this.logger.warn('Copilot: failed to fetch available models via the Copilot CLI:', error);
|
|
162
|
+
await this.setDiscoveryFailure(message);
|
|
163
|
+
return { models: [], fromCache: false, error: message };
|
|
155
164
|
}
|
|
156
165
|
}
|
|
157
166
|
|
|
158
167
|
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
* the
|
|
168
|
+
* Nominates the models the chat input should show without the user asking, up to
|
|
169
|
+
* {@link NOMINATED_MODEL_COUNT}. Copilot serves models of several vendors under one provider, so
|
|
170
|
+
* ranking them against each other by the numbers in their ids would show several models of
|
|
171
|
+
* whichever vendor counts highest and none of the others. The list is therefore filled vendor by
|
|
172
|
+
* vendor: `auto` first, which lets Copilot itself choose and is the entry most users want, then the
|
|
173
|
+
* newest model of each major vendor, then the next of each in turn until the list is full.
|
|
174
|
+
*
|
|
175
|
+
* Everything else Copilot carries is left to be chosen deliberately: the list is meant to be the
|
|
176
|
+
* handful of models one switches between while chatting, not a survey of the catalogue.
|
|
177
|
+
*
|
|
178
|
+
* Release-pinned ids are left out: the undated id of the same model is nominated, and pinning a
|
|
179
|
+
* release is the deliberate choice the check on its row makes.
|
|
164
180
|
*/
|
|
165
|
-
protected
|
|
166
|
-
const
|
|
167
|
-
const
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const family = model.capabilities?.family;
|
|
172
|
-
if (family && seenFamilies.has(family)) {
|
|
181
|
+
protected selectFeaturedModelIds(modelIds: string[]): Set<string> {
|
|
182
|
+
const featured = new Set<string>();
|
|
183
|
+
const byVendor = new Map<string, string[]>();
|
|
184
|
+
for (const id of modelIds) {
|
|
185
|
+
if (id === COPILOT_AUTO_MODEL_ID) {
|
|
186
|
+
featured.add(id);
|
|
173
187
|
continue;
|
|
174
188
|
}
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
189
|
+
if (DiscoveredModels.undatedId(id) !== undefined) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const vendor = this.vendorOf(id);
|
|
193
|
+
if (!vendor) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
byVendor.set(vendor, [...byVendor.get(vendor) ?? [], id]);
|
|
197
|
+
}
|
|
198
|
+
// Newest first within each vendor, the vendors themselves in the order they are named.
|
|
199
|
+
const ranked = NOMINATED_VENDORS.map(({ vendor }) =>
|
|
200
|
+
[...byVendor.get(vendor) ?? []].sort((left, right) => DiscoveredModels.compareByVersion(left, right)));
|
|
201
|
+
for (let rank = 0; featured.size < NOMINATED_MODEL_COUNT && ranked.some(ids => ids.length > rank); rank++) {
|
|
202
|
+
for (const ids of ranked) {
|
|
203
|
+
if (featured.size >= NOMINATED_MODEL_COUNT) {
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
if (ids[rank] !== undefined) {
|
|
207
|
+
featured.add(ids[rank]);
|
|
182
208
|
}
|
|
183
|
-
} else {
|
|
184
|
-
result.push(model.id);
|
|
185
209
|
}
|
|
186
210
|
}
|
|
211
|
+
return featured;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The vendor a Copilot model id belongs to, or `undefined` for a vendor that is not nominated.
|
|
216
|
+
* Matching on the id is all there is — the CLI reports nothing else — and it is deliberately narrow:
|
|
217
|
+
* a model of a vendor this does not name is still registered and still selectable, it is simply not
|
|
218
|
+
* one of the few the chat input starts with.
|
|
219
|
+
*/
|
|
220
|
+
protected vendorOf(id: string): string | undefined {
|
|
221
|
+
// Matched against the last segment, so that an id the CLI qualifies with its vendor
|
|
222
|
+
// (`google/gemini-2.5-pro`) is recognised as readily as a bare one.
|
|
223
|
+
const name = id.substring(id.lastIndexOf('/') + 1);
|
|
224
|
+
return NOMINATED_VENDORS.find(candidate => candidate.matches.test(name))?.vendor;
|
|
225
|
+
}
|
|
187
226
|
|
|
188
|
-
|
|
227
|
+
/**
|
|
228
|
+
* Records the outcome of the last model discovery and reflects it in the status of the models.
|
|
229
|
+
*/
|
|
230
|
+
protected async setDiscoveryFailure(failure: string | undefined): Promise<void> {
|
|
231
|
+
if (this.discoveryFailure === failure) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
this.discoveryFailure = failure;
|
|
235
|
+
await this.refreshModelsStatus();
|
|
189
236
|
}
|
|
190
237
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH and others.
|
|
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
|
+
/**
|
|
18
|
+
* The environment of the processes this integration starts, which are the Copilot CLI of a sign-in
|
|
19
|
+
* and the one that serves the requests.
|
|
20
|
+
*/
|
|
21
|
+
export namespace CopilotRuntimeEnv {
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The variables the Copilot CLI accepts a token from. None of them is a sign-in of this
|
|
25
|
+
* application, so a token this application does not own must not reach the CLI through them.
|
|
26
|
+
*/
|
|
27
|
+
export const TOKEN_VARIABLES = ['COPILOT_GITHUB_TOKEN', 'GH_TOKEN', 'GITHUB_TOKEN'];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The given environment without the variables that carry a token.
|
|
31
|
+
*
|
|
32
|
+
* They are dropped by name and regardless of their case, rather than masked with `undefined`.
|
|
33
|
+
* Environment variables are case-insensitive on Windows, so a `Github_Token` of the backend would
|
|
34
|
+
* survive a `GITHUB_TOKEN: undefined` next to it and would still be read by the CLI.
|
|
35
|
+
*/
|
|
36
|
+
export function withoutTokens(env: NodeJS.ProcessEnv): Record<string, string | undefined> {
|
|
37
|
+
const dropped = new Set(TOKEN_VARIABLES.map(name => name.toLowerCase()));
|
|
38
|
+
const result: Record<string, string | undefined> = {};
|
|
39
|
+
for (const [name, value] of Object.entries(env)) {
|
|
40
|
+
if (!dropped.has(name.toLowerCase())) {
|
|
41
|
+
result[name] = value;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
}
|