@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.
Files changed (150) hide show
  1. package/README.md +87 -14
  2. package/lib/browser/copilot-auth-dialog.d.ts.map +1 -1
  3. package/lib/browser/copilot-auth-dialog.js +19 -11
  4. package/lib/browser/copilot-auth-dialog.js.map +1 -1
  5. package/lib/browser/copilot-frontend-application-contribution.d.ts +21 -1
  6. package/lib/browser/copilot-frontend-application-contribution.d.ts.map +1 -1
  7. package/lib/browser/copilot-frontend-application-contribution.js +129 -26
  8. package/lib/browser/copilot-frontend-application-contribution.js.map +1 -1
  9. package/lib/common/copilot-auth-service.d.ts +27 -24
  10. package/lib/common/copilot-auth-service.d.ts.map +1 -1
  11. package/lib/common/copilot-language-models-manager.d.ts +6 -22
  12. package/lib/common/copilot-language-models-manager.d.ts.map +1 -1
  13. package/lib/common/copilot-language-models-manager.js +1 -13
  14. package/lib/common/copilot-language-models-manager.js.map +1 -1
  15. package/lib/common/copilot-preferences.d.ts +1 -0
  16. package/lib/common/copilot-preferences.d.ts.map +1 -1
  17. package/lib/common/copilot-preferences.js +24 -6
  18. package/lib/common/copilot-preferences.js.map +1 -1
  19. package/lib/common/index.d.ts +0 -1
  20. package/lib/common/index.d.ts.map +1 -1
  21. package/lib/common/index.js +0 -1
  22. package/lib/common/index.js.map +1 -1
  23. package/lib/node/copilot-auth-service-impl.d.ts +25 -16
  24. package/lib/node/copilot-auth-service-impl.d.ts.map +1 -1
  25. package/lib/node/copilot-auth-service-impl.js +67 -183
  26. package/lib/node/copilot-auth-service-impl.js.map +1 -1
  27. package/lib/node/copilot-auth-service-impl.spec.js +104 -168
  28. package/lib/node/copilot-auth-service-impl.spec.js.map +1 -1
  29. package/lib/node/copilot-backend-module.d.ts.map +1 -1
  30. package/lib/node/copilot-backend-module.js +10 -2
  31. package/lib/node/copilot-backend-module.js.map +1 -1
  32. package/lib/node/copilot-cli-auth-provider.d.ts +146 -0
  33. package/lib/node/copilot-cli-auth-provider.d.ts.map +1 -0
  34. package/lib/node/copilot-cli-auth-provider.js +430 -0
  35. package/lib/node/copilot-cli-auth-provider.js.map +1 -0
  36. package/lib/node/copilot-cli-auth-provider.spec.d.ts +2 -0
  37. package/lib/node/copilot-cli-auth-provider.spec.d.ts.map +1 -0
  38. package/lib/node/copilot-cli-auth-provider.spec.js +249 -0
  39. package/lib/node/copilot-cli-auth-provider.spec.js.map +1 -0
  40. package/lib/node/copilot-cli-locator.d.ts +120 -0
  41. package/lib/node/copilot-cli-locator.d.ts.map +1 -0
  42. package/lib/node/copilot-cli-locator.js +348 -0
  43. package/lib/node/copilot-cli-locator.js.map +1 -0
  44. package/lib/node/copilot-cli-locator.spec.d.ts +2 -0
  45. package/lib/node/copilot-cli-locator.spec.d.ts.map +1 -0
  46. package/lib/node/copilot-cli-locator.spec.js +327 -0
  47. package/lib/node/copilot-cli-locator.spec.js.map +1 -0
  48. package/lib/node/copilot-credential-store.d.ts +45 -0
  49. package/lib/node/copilot-credential-store.d.ts.map +1 -0
  50. package/lib/node/copilot-credential-store.js +96 -0
  51. package/lib/node/copilot-credential-store.js.map +1 -0
  52. package/lib/node/copilot-credential-store.spec.d.ts +2 -0
  53. package/lib/node/copilot-credential-store.spec.d.ts.map +1 -0
  54. package/lib/node/copilot-credential-store.spec.js +93 -0
  55. package/lib/node/copilot-credential-store.spec.js.map +1 -0
  56. package/lib/node/copilot-language-models-manager-impl.d.ts +44 -20
  57. package/lib/node/copilot-language-models-manager-impl.d.ts.map +1 -1
  58. package/lib/node/copilot-language-models-manager-impl.js +112 -65
  59. package/lib/node/copilot-language-models-manager-impl.js.map +1 -1
  60. package/lib/node/copilot-language-models-manager-impl.spec.d.ts +2 -0
  61. package/lib/node/copilot-language-models-manager-impl.spec.d.ts.map +1 -0
  62. package/lib/node/copilot-language-models-manager-impl.spec.js +162 -0
  63. package/lib/node/copilot-language-models-manager-impl.spec.js.map +1 -0
  64. package/lib/node/copilot-runtime-env.d.ts +20 -0
  65. package/lib/node/copilot-runtime-env.d.ts.map +1 -0
  66. package/lib/node/copilot-runtime-env.js +49 -0
  67. package/lib/node/copilot-runtime-env.js.map +1 -0
  68. package/lib/node/copilot-sdk-client-provider.d.ts +103 -0
  69. package/lib/node/copilot-sdk-client-provider.d.ts.map +1 -0
  70. package/lib/node/copilot-sdk-client-provider.js +247 -0
  71. package/lib/node/copilot-sdk-client-provider.js.map +1 -0
  72. package/lib/node/copilot-sdk-client-provider.spec.d.ts +2 -0
  73. package/lib/node/copilot-sdk-client-provider.spec.d.ts.map +1 -0
  74. package/lib/node/copilot-sdk-client-provider.spec.js +215 -0
  75. package/lib/node/copilot-sdk-client-provider.spec.js.map +1 -0
  76. package/lib/node/copilot-sdk-language-model.d.ts +85 -0
  77. package/lib/node/copilot-sdk-language-model.d.ts.map +1 -0
  78. package/lib/node/copilot-sdk-language-model.js +301 -0
  79. package/lib/node/copilot-sdk-language-model.js.map +1 -0
  80. package/lib/node/copilot-sdk-language-model.spec.d.ts +2 -0
  81. package/lib/node/copilot-sdk-language-model.spec.d.ts.map +1 -0
  82. package/lib/node/copilot-sdk-language-model.spec.js +315 -0
  83. package/lib/node/copilot-sdk-language-model.spec.js.map +1 -0
  84. package/lib/node/copilot-sdk-loader.d.ts +57 -0
  85. package/lib/node/copilot-sdk-loader.d.ts.map +1 -0
  86. package/lib/node/copilot-sdk-loader.js +133 -0
  87. package/lib/node/copilot-sdk-loader.js.map +1 -0
  88. package/lib/node/copilot-sdk-loader.spec.d.ts +2 -0
  89. package/lib/node/copilot-sdk-loader.spec.d.ts.map +1 -0
  90. package/lib/node/copilot-sdk-loader.spec.js +113 -0
  91. package/lib/node/copilot-sdk-loader.spec.js.map +1 -0
  92. package/lib/node/copilot-sdk-mappers.d.ts +54 -0
  93. package/lib/node/copilot-sdk-mappers.d.ts.map +1 -0
  94. package/lib/node/copilot-sdk-mappers.js +158 -0
  95. package/lib/node/copilot-sdk-mappers.js.map +1 -0
  96. package/lib/node/copilot-sdk-mappers.spec.d.ts +2 -0
  97. package/lib/node/copilot-sdk-mappers.spec.d.ts.map +1 -0
  98. package/lib/node/copilot-sdk-mappers.spec.js +168 -0
  99. package/lib/node/copilot-sdk-mappers.spec.js.map +1 -0
  100. package/lib/node/copilot-sdk-types.d.ts +208 -0
  101. package/lib/node/copilot-sdk-types.d.ts.map +1 -0
  102. package/lib/{common/copilot-oauth-config.js → node/copilot-sdk-types.js} +2 -13
  103. package/lib/node/copilot-sdk-types.js.map +1 -0
  104. package/lib/node/index.d.ts +9 -1
  105. package/lib/node/index.d.ts.map +1 -1
  106. package/lib/node/index.js +9 -1
  107. package/lib/node/index.js.map +1 -1
  108. package/package.json +4 -6
  109. package/src/browser/copilot-auth-dialog.tsx +20 -16
  110. package/src/browser/copilot-frontend-application-contribution.ts +144 -31
  111. package/src/common/copilot-auth-service.ts +28 -24
  112. package/src/common/copilot-language-models-manager.ts +6 -29
  113. package/src/common/copilot-preferences.ts +24 -5
  114. package/src/common/index.ts +0 -1
  115. package/src/node/copilot-auth-service-impl.spec.ts +122 -207
  116. package/src/node/copilot-auth-service-impl.ts +63 -218
  117. package/src/node/copilot-backend-module.ts +11 -2
  118. package/src/node/copilot-cli-auth-provider.spec.ts +315 -0
  119. package/src/node/copilot-cli-auth-provider.ts +444 -0
  120. package/src/node/copilot-cli-locator.spec.ts +386 -0
  121. package/src/node/copilot-cli-locator.ts +363 -0
  122. package/src/node/copilot-credential-store.spec.ts +106 -0
  123. package/src/node/copilot-credential-store.ts +99 -0
  124. package/src/node/copilot-language-models-manager-impl.spec.ts +188 -0
  125. package/src/node/copilot-language-models-manager-impl.ts +131 -84
  126. package/src/node/copilot-runtime-env.ts +46 -0
  127. package/src/node/copilot-sdk-client-provider.spec.ts +253 -0
  128. package/src/node/copilot-sdk-client-provider.ts +241 -0
  129. package/src/node/copilot-sdk-language-model.spec.ts +377 -0
  130. package/src/node/copilot-sdk-language-model.ts +330 -0
  131. package/src/node/copilot-sdk-loader.spec.ts +126 -0
  132. package/src/node/copilot-sdk-loader.ts +142 -0
  133. package/src/node/copilot-sdk-mappers.spec.ts +190 -0
  134. package/src/node/copilot-sdk-mappers.ts +172 -0
  135. package/src/node/copilot-sdk-types.ts +290 -0
  136. package/src/node/index.ts +9 -1
  137. package/lib/common/copilot-oauth-config.d.ts +0 -33
  138. package/lib/common/copilot-oauth-config.d.ts.map +0 -1
  139. package/lib/common/copilot-oauth-config.js.map +0 -1
  140. package/lib/node/copilot-language-model.d.ts +0 -34
  141. package/lib/node/copilot-language-model.d.ts.map +0 -1
  142. package/lib/node/copilot-language-model.js +0 -236
  143. package/lib/node/copilot-language-model.js.map +0 -1
  144. package/lib/node/copilot-language-model.spec.d.ts +0 -2
  145. package/lib/node/copilot-language-model.spec.d.ts.map +0 -1
  146. package/lib/node/copilot-language-model.spec.js +0 -89
  147. package/lib/node/copilot-language-model.spec.js.map +0 -1
  148. package/src/common/copilot-oauth-config.ts +0 -55
  149. package/src/node/copilot-language-model.spec.ts +0 -106
  150. 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 { Disposable, DisposableCollection, ILogger } from '@theia/core';
19
- import { inject, injectable, postConstruct, named } from '@theia/core/shared/inversify';
20
- import { CopilotLanguageModelsManager, CopilotModelDescription, COPILOT_PROVIDER_ID, getCopilotApiBaseUrl } from '../common';
21
- import { CopilotOAuthConfig } from '../common/copilot-oauth-config';
22
- import { CopilotLanguageModel } from './copilot-language-model';
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, Disposable {
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(CopilotOAuthConfig)
39
- protected readonly oauthConfig: CopilotOAuthConfig;
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
- this.refreshModelsStatus();
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
- dispose(): void {
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: 'ready' };
94
+ if (!authState.isAuthenticated) {
95
+ return { status: 'unavailable', message: nls.localize('theia/ai/copilot/notSignedIn', 'Not signed in to GitHub Copilot') };
66
96
  }
67
- return { status: 'unavailable', message: 'Not signed in to GitHub Copilot' };
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 CopilotLanguageModel)) {
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<CopilotLanguageModel>(modelDescription.id, {
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 CopilotLanguageModel(
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.authService.getAccessToken(),
98
- () => this.enterpriseUrl,
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 CopilotLanguageModel && model.id.startsWith(`${COPILOT_PROVIDER_ID}/`)) {
116
- await this.languageModelRegistry.patchLanguageModel<CopilotLanguageModel>(model.id, {
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 fetchAvailableModelIds(): Promise<string[]> {
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 response = await fetch(`${baseURL}/models`, {
133
- headers: {
134
- 'Authorization': `Bearer ${accessToken}`,
135
- 'User-Agent': this.oauthConfig.userAgent,
136
- 'Accept': 'application/json'
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
- this.logger.warn('Copilot: failed to fetch available models:', error);
154
- return [];
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
- * Deduplicates models returned by the Copilot API.
160
- * The API returns both alias IDs (e.g., `gpt-4o`) and versioned IDs
161
- * (e.g., `gpt-4o-2024-11-20`) for the same model family.
162
- * We keep only the family alias when it exists, falling back to
163
- * the versioned ID otherwise.
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 deduplicateModels(models: Array<{ id: string; capabilities?: { family?: string } }>): string[] {
166
- const allIds = new Set(models.map(m => m.id));
167
- const result: string[] = [];
168
- const seenFamilies = new Set<string>();
169
-
170
- for (const model of models) {
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 (family) {
176
- seenFamilies.add(family);
177
- // Prefer the family alias if it exists as a model ID
178
- if (allIds.has(family)) {
179
- result.push(family);
180
- } else {
181
- result.push(model.id);
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
- return result;
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
+ }