@theia/ai-core 1.76.0-next.7 → 1.76.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/browser/ai-core-frontend-module.d.ts.map +1 -1
- package/lib/browser/ai-core-frontend-module.js +4 -0
- package/lib/browser/ai-core-frontend-module.js.map +1 -1
- package/lib/browser/discovering-provider-contribution.d.ts +92 -0
- package/lib/browser/discovering-provider-contribution.d.ts.map +1 -0
- package/lib/browser/discovering-provider-contribution.js +189 -0
- package/lib/browser/discovering-provider-contribution.js.map +1 -0
- package/lib/browser/discovering-provider-contribution.spec.d.ts +2 -0
- package/lib/browser/discovering-provider-contribution.spec.d.ts.map +1 -0
- package/lib/browser/discovering-provider-contribution.spec.js +233 -0
- package/lib/browser/discovering-provider-contribution.spec.js.map +1 -0
- package/lib/browser/favorite-models-service.d.ts +135 -0
- package/lib/browser/favorite-models-service.d.ts.map +1 -0
- package/lib/browser/favorite-models-service.js +279 -0
- package/lib/browser/favorite-models-service.js.map +1 -0
- package/lib/browser/favorite-models-service.spec.d.ts +2 -0
- package/lib/browser/favorite-models-service.spec.d.ts.map +1 -0
- package/lib/browser/favorite-models-service.spec.js +277 -0
- package/lib/browser/favorite-models-service.spec.js.map +1 -0
- package/lib/browser/frontend-language-model-alias-registry.js +1 -1
- package/lib/browser/frontend-language-model-service.d.ts.map +1 -1
- package/lib/browser/frontend-language-model-service.js +8 -0
- package/lib/browser/frontend-language-model-service.js.map +1 -1
- package/lib/browser/frontend-language-model-service.spec.d.ts +2 -0
- package/lib/browser/frontend-language-model-service.spec.d.ts.map +1 -0
- package/lib/browser/frontend-language-model-service.spec.js +76 -0
- package/lib/browser/frontend-language-model-service.spec.js.map +1 -0
- package/lib/browser/index.d.ts +3 -0
- package/lib/browser/index.d.ts.map +1 -1
- package/lib/browser/index.js +3 -0
- package/lib/browser/index.js.map +1 -1
- package/lib/browser/model-discovery-status-service.d.ts +52 -0
- package/lib/browser/model-discovery-status-service.d.ts.map +1 -0
- package/lib/browser/model-discovery-status-service.js +110 -0
- package/lib/browser/model-discovery-status-service.js.map +1 -0
- package/lib/browser/model-discovery-status-service.spec.d.ts +2 -0
- package/lib/browser/model-discovery-status-service.spec.d.ts.map +1 -0
- package/lib/browser/model-discovery-status-service.spec.js +86 -0
- package/lib/browser/model-discovery-status-service.spec.js.map +1 -0
- package/lib/common/ai-core-preferences.d.ts +11 -0
- package/lib/common/ai-core-preferences.d.ts.map +1 -1
- package/lib/common/ai-core-preferences.js +37 -1
- package/lib/common/ai-core-preferences.js.map +1 -1
- package/lib/common/index.d.ts +2 -0
- package/lib/common/index.d.ts.map +1 -1
- package/lib/common/index.js +2 -0
- package/lib/common/index.js.map +1 -1
- package/lib/common/language-model-ordering.spec.d.ts +2 -0
- package/lib/common/language-model-ordering.spec.d.ts.map +1 -0
- package/lib/common/language-model-ordering.spec.js +72 -0
- package/lib/common/language-model-ordering.spec.js.map +1 -0
- package/lib/common/language-model-util.d.ts +17 -1
- package/lib/common/language-model-util.d.ts.map +1 -1
- package/lib/common/language-model-util.js +44 -1
- package/lib/common/language-model-util.js.map +1 -1
- package/lib/common/language-model.d.ts +21 -0
- package/lib/common/language-model.d.ts.map +1 -1
- package/lib/common/language-model.js +50 -1
- package/lib/common/language-model.js.map +1 -1
- package/lib/common/language-model.spec.js +36 -0
- package/lib/common/language-model.spec.js.map +1 -1
- package/lib/common/model-discovery-status.d.ts +112 -0
- package/lib/common/model-discovery-status.d.ts.map +1 -0
- package/lib/common/model-discovery-status.js +18 -0
- package/lib/common/model-discovery-status.js.map +1 -0
- package/lib/common/model-discovery-util.d.ts +46 -0
- package/lib/common/model-discovery-util.d.ts.map +1 -0
- package/lib/common/model-discovery-util.js +128 -0
- package/lib/common/model-discovery-util.js.map +1 -0
- package/lib/common/model-discovery-util.spec.d.ts +2 -0
- package/lib/common/model-discovery-util.spec.d.ts.map +1 -0
- package/lib/common/model-discovery-util.spec.js +95 -0
- package/lib/common/model-discovery-util.spec.js.map +1 -0
- package/lib/common/prompt-service.js +3 -3
- package/lib/common/prompt-service.js.map +1 -1
- package/lib/common/prompt-service.spec.js +5 -0
- package/lib/common/prompt-service.spec.js.map +1 -1
- package/lib/node/ai-core-backend-module.d.ts.map +1 -1
- package/lib/node/ai-core-backend-module.js +8 -0
- package/lib/node/ai-core-backend-module.js.map +1 -1
- package/lib/node/backend-language-model-registry.d.ts.map +1 -1
- package/lib/node/backend-language-model-registry.js +1 -0
- package/lib/node/backend-language-model-registry.js.map +1 -1
- package/lib/node/index.d.ts +2 -0
- package/lib/node/index.d.ts.map +1 -1
- package/lib/node/index.js +2 -0
- package/lib/node/index.js.map +1 -1
- package/lib/node/model-discovery-fetcher.d.ts +43 -0
- package/lib/node/model-discovery-fetcher.d.ts.map +1 -0
- package/lib/node/model-discovery-fetcher.js +85 -0
- package/lib/node/model-discovery-fetcher.js.map +1 -0
- package/lib/node/model-snapshot-store.d.ts +26 -0
- package/lib/node/model-snapshot-store.d.ts.map +1 -0
- package/lib/node/model-snapshot-store.js +73 -0
- package/lib/node/model-snapshot-store.js.map +1 -0
- package/lib/node/test/test-model-discovery-fetcher.d.ts +21 -0
- package/lib/node/test/test-model-discovery-fetcher.d.ts.map +1 -0
- package/lib/node/test/test-model-discovery-fetcher.js +54 -0
- package/lib/node/test/test-model-discovery-fetcher.js.map +1 -0
- package/package.json +12 -12
- package/src/browser/ai-core-frontend-module.ts +5 -0
- package/src/browser/discovering-provider-contribution.spec.ts +304 -0
- package/src/browser/discovering-provider-contribution.ts +241 -0
- package/src/browser/favorite-models-service.spec.ts +336 -0
- package/src/browser/favorite-models-service.ts +297 -0
- package/src/browser/frontend-language-model-alias-registry.ts +1 -1
- package/src/browser/frontend-language-model-service.spec.ts +86 -0
- package/src/browser/frontend-language-model-service.ts +9 -1
- package/src/browser/index.ts +3 -0
- package/src/browser/model-discovery-status-service.spec.ts +101 -0
- package/src/browser/model-discovery-status-service.ts +123 -0
- package/src/common/ai-core-preferences.ts +40 -0
- package/src/common/index.ts +2 -0
- package/src/common/language-model-ordering.spec.ts +83 -0
- package/src/common/language-model-util.ts +43 -0
- package/src/common/language-model.spec.ts +45 -0
- package/src/common/language-model.ts +54 -0
- package/src/common/model-discovery-status.ts +132 -0
- package/src/common/model-discovery-util.spec.ts +105 -0
- package/src/common/model-discovery-util.ts +129 -0
- package/src/common/prompt-service.spec.ts +6 -0
- package/src/common/prompt-service.ts +3 -3
- package/src/node/ai-core-backend-module.ts +8 -0
- package/src/node/backend-language-model-registry.ts +1 -0
- package/src/node/index.ts +2 -0
- package/src/node/model-discovery-fetcher.ts +105 -0
- package/src/node/model-snapshot-store.ts +80 -0
- package/src/node/test/test-model-discovery-fetcher.ts +62 -0
|
@@ -0,0 +1,336 @@
|
|
|
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 { expect } from 'chai';
|
|
18
|
+
import { PreferenceService } from '@theia/core';
|
|
19
|
+
import { PREFERENCE_NAME_FAVORITE_MODELS, PREFERENCE_NAME_HIDDEN_MODELS } from '../common/ai-core-preferences';
|
|
20
|
+
import { DiscoveredModel, ModelDiscoveryStatus } from '../common/model-discovery-status';
|
|
21
|
+
import { FavoriteModelsService } from './favorite-models-service';
|
|
22
|
+
import { ModelDiscoveryStatusService } from './model-discovery-status-service';
|
|
23
|
+
|
|
24
|
+
function dated(id: string, released: string): DiscoveredModel {
|
|
25
|
+
return { id, released: Date.parse(released) };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createService(
|
|
29
|
+
statuses: ModelDiscoveryStatus[],
|
|
30
|
+
marked: string[] = [],
|
|
31
|
+
hidden: string[] = []
|
|
32
|
+
): { service: FavoriteModelsService; written: string[][]; stored: Record<string, string[]> } {
|
|
33
|
+
const service = new FavoriteModelsService();
|
|
34
|
+
const written: string[][] = [];
|
|
35
|
+
const stored: Record<string, string[]> = {
|
|
36
|
+
[PREFERENCE_NAME_FAVORITE_MODELS]: marked,
|
|
37
|
+
[PREFERENCE_NAME_HIDDEN_MODELS]: hidden
|
|
38
|
+
};
|
|
39
|
+
const discoveryStatus: Partial<ModelDiscoveryStatusService> = { getStatuses: () => statuses };
|
|
40
|
+
const preferenceService: Partial<PreferenceService> = {
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
+
get: ((name: string, defaultValue: unknown) => stored[name] ?? defaultValue) as any,
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
44
|
+
set: (async (name: string, value: any) => { stored[name] = value; written.push(value); }) as any
|
|
45
|
+
};
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
47
|
+
(service as any).discoveryStatus = discoveryStatus;
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
49
|
+
(service as any).preferenceService = preferenceService;
|
|
50
|
+
return { service, written, stored };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function status(overrides: Partial<ModelDiscoveryStatus> = {}): ModelDiscoveryStatus {
|
|
54
|
+
return { providerId: 'anthropic', label: 'Anthropic', state: 'ready', ...overrides };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe('FavoriteModelsService', () => {
|
|
58
|
+
|
|
59
|
+
it('features the five most recently released undated models, never a release-pinned one', () => {
|
|
60
|
+
const { service } = createService([status({
|
|
61
|
+
discovered: [
|
|
62
|
+
dated('claude-opus-5', '2026-04-01'),
|
|
63
|
+
dated('claude-sonnet-5', '2026-02-01'),
|
|
64
|
+
dated('claude-haiku-4-5', '2025-10-01'),
|
|
65
|
+
dated('claude-fable-5', '2025-09-01'),
|
|
66
|
+
dated('claude-opus-4-8', '2025-08-01'),
|
|
67
|
+
dated('claude-opus-4-7', '2025-06-01'),
|
|
68
|
+
dated('claude-opus-5-20260401', '2026-04-01')
|
|
69
|
+
]
|
|
70
|
+
})]);
|
|
71
|
+
expect(service.isFeatured('anthropic/claude-opus-5')).to.equal(true);
|
|
72
|
+
expect(service.isFeatured('anthropic/claude-opus-4-8')).to.equal(true);
|
|
73
|
+
// The sixth-newest falls outside the featured set.
|
|
74
|
+
expect(service.isFeatured('anthropic/claude-opus-4-7')).to.equal(false);
|
|
75
|
+
// A release-pinned id is never featured, however recent it is.
|
|
76
|
+
expect(service.isFeatured('anthropic/claude-opus-5-20260401')).to.equal(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('caps a provider that reports no release dates by the versions in its ids', () => {
|
|
80
|
+
const { service } = createService([status({
|
|
81
|
+
providerId: 'google',
|
|
82
|
+
discovered: [
|
|
83
|
+
{ id: 'gemini-2.0-pro' }, { id: 'gemini-2.5-flash' }, { id: 'gemini-2.5-pro' },
|
|
84
|
+
{ id: 'gemini-3.1-pro-preview' }, { id: 'gemini-3.5-flash' }, { id: 'gemini-3.7-flash' }
|
|
85
|
+
]
|
|
86
|
+
})]);
|
|
87
|
+
// The highest versions in the ids; the oldest of the six drops out.
|
|
88
|
+
expect(service.isFeatured('google/gemini-3.7-flash')).to.equal(true);
|
|
89
|
+
expect(service.isFeatured('google/gemini-2.5-pro')).to.equal(true);
|
|
90
|
+
expect(service.isFeatured('google/gemini-2.0-pro')).to.equal(false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('features a `-latest` pointer ahead of the versioned ids it stands for', () => {
|
|
94
|
+
const { service } = createService([status({
|
|
95
|
+
providerId: 'google',
|
|
96
|
+
discovered: [
|
|
97
|
+
{ id: 'gemini-3.5-flash' }, { id: 'gemini-3.7-flash' }, { id: 'gemini-3.8-flash' },
|
|
98
|
+
{ id: 'gemini-3.1-pro-preview' }, { id: 'gemini-2.5-pro' }, { id: 'gemini-flash-latest' }
|
|
99
|
+
]
|
|
100
|
+
})]);
|
|
101
|
+
// The pointer carries no version, so without a rule of its own it would sort behind all of them.
|
|
102
|
+
expect(service.isFeatured('google/gemini-flash-latest')).to.equal(true);
|
|
103
|
+
expect(service.isFeatured('google/gemini-2.5-pro')).to.equal(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('offers the pointers and the newest versions for a provider that reports no dates', () => {
|
|
107
|
+
const { service } = createService([status({
|
|
108
|
+
providerId: 'google',
|
|
109
|
+
discovered: [
|
|
110
|
+
{ id: 'gemini-flash-latest' }, { id: 'gemini-flash-lite-latest' }, { id: 'gemini-pro-latest' },
|
|
111
|
+
{ id: 'gemini-3.8-flash' }, { id: 'gemini-3.7-flash' }, { id: 'gemini-3.5-flash' },
|
|
112
|
+
{ id: 'gemini-3.1-pro-preview' }, { id: 'gemini-2.5-pro' }
|
|
113
|
+
]
|
|
114
|
+
})]);
|
|
115
|
+
expect(service.isFeatured('google/gemini-pro-latest')).to.equal(true);
|
|
116
|
+
expect(service.isFeatured('google/gemini-flash-latest')).to.equal(true);
|
|
117
|
+
expect(service.isFeatured('google/gemini-flash-lite-latest')).to.equal(true);
|
|
118
|
+
// The pointers first, then the newest concrete models fill the rest.
|
|
119
|
+
expect(service.isFeatured('google/gemini-3.8-flash')).to.equal(true);
|
|
120
|
+
expect(service.isFeatured('google/gemini-3.7-flash')).to.equal(true);
|
|
121
|
+
expect(service.isFeatured('google/gemini-3.1-pro-preview')).to.equal(false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('orders a `-latest` id by its release date where the provider reports one', () => {
|
|
125
|
+
const { service } = createService([status({
|
|
126
|
+
providerId: 'openAiOfficial',
|
|
127
|
+
modelIdPrefix: 'openai',
|
|
128
|
+
discovered: [
|
|
129
|
+
dated('gpt-5.6-sol', '2026-02-01'),
|
|
130
|
+
dated('gpt-5.5', '2025-11-01'),
|
|
131
|
+
dated('gpt-5.1', '2025-08-01'),
|
|
132
|
+
dated('gpt-5', '2025-06-01'),
|
|
133
|
+
dated('o4-mini', '2025-04-01'),
|
|
134
|
+
dated('chatgpt-4o-latest', '2024-08-01')
|
|
135
|
+
]
|
|
136
|
+
})]);
|
|
137
|
+
// A provider that dates its models dates its pointers too, so the pointer rule stays out of it.
|
|
138
|
+
expect(service.isFeatured('openai/gpt-5.6-sol')).to.equal(true);
|
|
139
|
+
expect(service.isFeatured('openai/o4-mini')).to.equal(true);
|
|
140
|
+
expect(service.isFeatured('openai/chatgpt-4o-latest')).to.equal(false);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('features what the provider reports as newest, whatever the model aliases name', () => {
|
|
144
|
+
const { service } = createService([status({
|
|
145
|
+
discovered: [
|
|
146
|
+
dated('claude-opus-5', '2026-04-01'),
|
|
147
|
+
dated('claude-sonnet-5', '2026-03-01'),
|
|
148
|
+
dated('claude-fable-5', '2026-02-01'),
|
|
149
|
+
dated('claude-opus-4-8', '2026-01-01'),
|
|
150
|
+
dated('claude-opus-4-7', '2025-12-01'),
|
|
151
|
+
// Named by the built-in aliases, and the oldest of the six: an alias is a curated list of
|
|
152
|
+
// its own, saying which model a purpose uses rather than which are worth offering here.
|
|
153
|
+
dated('claude-haiku-4-5', '2025-10-01')
|
|
154
|
+
]
|
|
155
|
+
})]);
|
|
156
|
+
expect(service.isFeatured('anthropic/claude-opus-4-7')).to.equal(true);
|
|
157
|
+
expect(service.isFeatured('anthropic/claude-haiku-4-5')).to.equal(false);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('prunes the marked and hidden models that a successful discovery no longer offers', () => {
|
|
161
|
+
const { service, stored } = createService(
|
|
162
|
+
[status({ discovered: [dated('claude-opus-5', '2026-04-01')] })],
|
|
163
|
+
['anthropic/claude-gone', 'openai/gpt-5.5'],
|
|
164
|
+
['anthropic/claude-also-gone', 'anthropic/claude-opus-5']
|
|
165
|
+
);
|
|
166
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
167
|
+
(service as any).pruneFavorites(status({ discovered: [dated('claude-opus-5', '2026-04-01')] }));
|
|
168
|
+
// Only the vanished models of that provider are dropped; another provider's stars are untouched.
|
|
169
|
+
expect(stored[PREFERENCE_NAME_FAVORITE_MODELS]).to.deep.equal(['openai/gpt-5.5']);
|
|
170
|
+
expect(stored[PREFERENCE_NAME_HIDDEN_MODELS]).to.deep.equal(['anthropic/claude-opus-5']);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('keeps the marked models when the discovery did not actually reach the provider', () => {
|
|
174
|
+
const { service, written } = createService([], ['anthropic/claude-gone']);
|
|
175
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
176
|
+
const prune = (candidate: ModelDiscoveryStatus) => (service as any).pruneFavorites(candidate);
|
|
177
|
+
prune(status({ state: 'ready', fromCache: true, discovered: [] }));
|
|
178
|
+
prune(status({ state: 'error' }));
|
|
179
|
+
prune(status({ state: 'no-credentials' }));
|
|
180
|
+
prune(status({ state: 'ready', discovered: undefined }));
|
|
181
|
+
expect(written).to.be.empty;
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('respects a provider that nominates its own models, as Copilot does across vendors', () => {
|
|
185
|
+
const { service } = createService([status({
|
|
186
|
+
providerId: 'copilot',
|
|
187
|
+
discovered: [
|
|
188
|
+
{ id: 'auto', featured: true },
|
|
189
|
+
{ id: 'gpt-5', featured: true },
|
|
190
|
+
{ id: 'claude-sonnet-4.5', featured: true },
|
|
191
|
+
{ id: 'gpt-4.1' },
|
|
192
|
+
{ id: 'claude-haiku-4' },
|
|
193
|
+
{ id: 'gemini-2.5-pro' }
|
|
194
|
+
]
|
|
195
|
+
})]);
|
|
196
|
+
expect(service.isFeatured('copilot/auto')).to.equal(true);
|
|
197
|
+
expect(service.isFeatured('copilot/claude-sonnet-4.5')).to.equal(true);
|
|
198
|
+
// Not nominated, and the generic "five newest" ranking does not apply where a provider nominated.
|
|
199
|
+
expect(service.isFeatured('copilot/gpt-4.1')).to.equal(false);
|
|
200
|
+
expect(service.isFeatured('copilot/gemini-2.5-pro')).to.equal(false);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('offers exactly what a nominating provider nominated, and nothing beside it', () => {
|
|
204
|
+
const { service } = createService([status({
|
|
205
|
+
providerId: 'copilot',
|
|
206
|
+
discovered: [{ id: 'gpt-5', featured: true }, { id: 'claude-haiku-4' }]
|
|
207
|
+
})]);
|
|
208
|
+
expect(service.isFeatured('copilot/gpt-5')).to.equal(true);
|
|
209
|
+
expect(service.isFeatured('copilot/claude-haiku-4')).to.equal(false);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('features a manually configured list in full', () => {
|
|
213
|
+
const { service } = createService([status({
|
|
214
|
+
state: 'overridden',
|
|
215
|
+
discovered: [{ id: 'a' }, { id: 'b' }, { id: 'c' }, { id: 'd' }, { id: 'e' }, { id: 'f' }]
|
|
216
|
+
})]);
|
|
217
|
+
expect(service.isFeatured('anthropic/f')).to.equal(true);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('honours the models the user marked', () => {
|
|
221
|
+
const { service } = createService(
|
|
222
|
+
[status({ discovered: [dated('claude-opus-5', '2026-04-01'), dated('claude-opus-4-7', '2024-06-01')] })],
|
|
223
|
+
['anthropic/claude-opus-5-20260401']
|
|
224
|
+
);
|
|
225
|
+
expect(service.isFavorite('anthropic/claude-opus-5-20260401')).to.equal(true);
|
|
226
|
+
expect(service.isFavorite('anthropic/claude-opus-4-7')).to.equal(true); // featured, only two undated ids
|
|
227
|
+
expect(service.isFavorite('anthropic/claude-unknown')).to.equal(false);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('treats the models of a provider that does not participate in discovery as favorites', () => {
|
|
231
|
+
const { service } = createService([status({ discovered: [dated('claude-opus-5', '2026-04-01')] })]);
|
|
232
|
+
// Manually configured providers (ollama, hugging face, …) and custom endpoints are the user's own list.
|
|
233
|
+
expect(service.isFavorite('ollama/llama3')).to.equal(true);
|
|
234
|
+
expect(service.isFavorite('my-custom-endpoint-model')).to.equal(true);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('honours the model-id prefix when it differs from the provider id', () => {
|
|
238
|
+
const { service } = createService([status({
|
|
239
|
+
providerId: 'openAiOfficial',
|
|
240
|
+
modelIdPrefix: 'openai',
|
|
241
|
+
discovered: [dated('gpt-5.6-sol', '2026-02-01')]
|
|
242
|
+
})]);
|
|
243
|
+
expect(service.isFeatured('openai/gpt-5.6-sol')).to.equal(true);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe('resetToDefaults', () => {
|
|
247
|
+
|
|
248
|
+
it('drops what the user decided about one provider and leaves the others alone', async () => {
|
|
249
|
+
const { service, stored } = createService(
|
|
250
|
+
[status({ discovered: [dated('claude-opus-5', '2026-04-01')] })],
|
|
251
|
+
['anthropic/claude-opus-4-7', 'openai/gpt-5.5'],
|
|
252
|
+
['anthropic/claude-opus-5', 'openai/gpt-5.6-sol']
|
|
253
|
+
);
|
|
254
|
+
expect(service.hasOverrides('anthropic')).to.equal(true);
|
|
255
|
+
|
|
256
|
+
await service.resetToDefaults('anthropic');
|
|
257
|
+
|
|
258
|
+
expect(stored[PREFERENCE_NAME_FAVORITE_MODELS]).to.deep.equal(['openai/gpt-5.5']);
|
|
259
|
+
expect(stored[PREFERENCE_NAME_HIDDEN_MODELS]).to.deep.equal(['openai/gpt-5.6-sol']);
|
|
260
|
+
expect(service.hasOverrides('anthropic')).to.equal(false);
|
|
261
|
+
expect(service.hasOverrides('openai')).to.equal(true);
|
|
262
|
+
// The provider is back to what it offers by default.
|
|
263
|
+
expect(service.isFavorite('anthropic/claude-opus-5')).to.equal(true);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('writes nothing when there is nothing to reset', async () => {
|
|
267
|
+
const { service, written } = createService([status({ discovered: [dated('claude-opus-5', '2026-04-01')] })]);
|
|
268
|
+
expect(service.hasOverrides('anthropic')).to.equal(false);
|
|
269
|
+
await service.resetToDefaults('anthropic');
|
|
270
|
+
expect(written).to.be.empty;
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe('toggleFavorite', () => {
|
|
275
|
+
|
|
276
|
+
it('adds and removes an id the user can control', async () => {
|
|
277
|
+
const { service, written } = createService([status({ discovered: [dated('claude-opus-5', '2026-04-01')] })], []);
|
|
278
|
+
await service.toggleFavorite('anthropic/claude-opus-5-20260401');
|
|
279
|
+
expect(written[0]).to.deep.equal(['anthropic/claude-opus-5-20260401']);
|
|
280
|
+
|
|
281
|
+
const removing = createService([status({ discovered: [dated('claude-opus-5', '2026-04-01')] })], ['anthropic/claude-opus-5-20260401']);
|
|
282
|
+
await removing.service.toggleFavorite('anthropic/claude-opus-5-20260401');
|
|
283
|
+
expect(removing.written[0]).to.deep.equal([]);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('hides a model that is offered by default, storing only that', async () => {
|
|
287
|
+
const { service, stored } = createService([status({ discovered: [dated('claude-opus-5', '2026-04-01')] })]);
|
|
288
|
+
expect(service.isFavorite('anthropic/claude-opus-5')).to.equal(true);
|
|
289
|
+
|
|
290
|
+
await service.toggleFavorite('anthropic/claude-opus-5');
|
|
291
|
+
|
|
292
|
+
expect(stored[PREFERENCE_NAME_HIDDEN_MODELS]).to.deep.equal(['anthropic/claude-opus-5']);
|
|
293
|
+
// Featured all the same, it is only kept out of the picker.
|
|
294
|
+
expect(service.isFeatured('anthropic/claude-opus-5')).to.equal(true);
|
|
295
|
+
expect(service.isFavorite('anthropic/claude-opus-5')).to.equal(false);
|
|
296
|
+
// Nothing is written to the favorites: a model offered by default was never listed there.
|
|
297
|
+
expect(stored[PREFERENCE_NAME_FAVORITE_MODELS]).to.be.empty;
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('shows a hidden model again by dropping its entry', async () => {
|
|
301
|
+
const { service, stored } = createService(
|
|
302
|
+
[status({ discovered: [dated('claude-opus-5', '2026-04-01')] })],
|
|
303
|
+
[],
|
|
304
|
+
['anthropic/claude-opus-5']
|
|
305
|
+
);
|
|
306
|
+
expect(service.isFavorite('anthropic/claude-opus-5')).to.equal(false);
|
|
307
|
+
|
|
308
|
+
await service.toggleFavorite('anthropic/claude-opus-5');
|
|
309
|
+
|
|
310
|
+
expect(stored[PREFERENCE_NAME_HIDDEN_MODELS]).to.be.empty;
|
|
311
|
+
expect(stored[PREFERENCE_NAME_FAVORITE_MODELS]).to.be.empty;
|
|
312
|
+
expect(service.isFavorite('anthropic/claude-opus-5')).to.equal(true);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('marks a model that fell out of the featured set while it was hidden', async () => {
|
|
316
|
+
const { service, stored } = createService(
|
|
317
|
+
[status({ discovered: [dated('claude-opus-5', '2026-04-01'), dated('claude-opus-4-7', '2024-06-01')] })],
|
|
318
|
+
[],
|
|
319
|
+
['anthropic/claude-opus-5-20260401']
|
|
320
|
+
);
|
|
321
|
+
// Release-pinned, so never featured: dropping the entry alone would not bring it back.
|
|
322
|
+
await service.toggleFavorite('anthropic/claude-opus-5-20260401');
|
|
323
|
+
|
|
324
|
+
expect(stored[PREFERENCE_NAME_HIDDEN_MODELS]).to.be.empty;
|
|
325
|
+
expect(stored[PREFERENCE_NAME_FAVORITE_MODELS]).to.deep.equal(['anthropic/claude-opus-5-20260401']);
|
|
326
|
+
expect(service.isFavorite('anthropic/claude-opus-5-20260401')).to.equal(true);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('hides a model of a provider that does not participate in discovery', async () => {
|
|
330
|
+
const { service, stored } = createService([status({ discovered: [dated('claude-opus-5', '2026-04-01')] })]);
|
|
331
|
+
await service.toggleFavorite('ollama/llama3');
|
|
332
|
+
expect(stored[PREFERENCE_NAME_HIDDEN_MODELS]).to.deep.equal(['ollama/llama3']);
|
|
333
|
+
expect(service.isFavorite('ollama/llama3')).to.equal(false);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
});
|
|
@@ -0,0 +1,297 @@
|
|
|
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 { Emitter, Event, PreferenceScope, PreferenceService } from '@theia/core';
|
|
18
|
+
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
|
19
|
+
import { PREFERENCE_NAME_FAVORITE_MODELS, PREFERENCE_NAME_HIDDEN_MODELS } from '../common/ai-core-preferences';
|
|
20
|
+
import { DiscoveredModel, ModelDiscoveryStatus } from '../common/model-discovery-status';
|
|
21
|
+
import { DiscoveredModels } from '../common/model-discovery-util';
|
|
22
|
+
import { ModelDiscoveryStatusService } from './model-discovery-status-service';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* How many of a provider's models are offered without the user asking. Enough to cover a provider's
|
|
26
|
+
* current line-up (a flagship, a fast one, a cheap one and their immediate predecessors) without
|
|
27
|
+
* turning the model pickers back into a catalogue.
|
|
28
|
+
*/
|
|
29
|
+
export const FEATURED_MODEL_COUNT = 5;
|
|
30
|
+
|
|
31
|
+
/** What {@link FavoriteModelsService} derives from what the providers last reported. */
|
|
32
|
+
export interface DerivedFavorites {
|
|
33
|
+
/** Fully qualified ids of the models that are offered without the user asking. */
|
|
34
|
+
readonly featured: Set<string>;
|
|
35
|
+
/** Model-id prefixes of the providers that participate in discovery. */
|
|
36
|
+
readonly discoveryPrefixes: Set<string>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Decides which models the model picker in the AI chat input offers.
|
|
41
|
+
*
|
|
42
|
+
* Discovery registers everything a provider offers, including one entry per release, which is far
|
|
43
|
+
* more than anyone picks between while chatting. What that picker shows is therefore the *favorites*:
|
|
44
|
+
* the models that are featured automatically plus the ones the user marked. Everywhere a model is
|
|
45
|
+
* chosen deliberately — an agent's model, a model alias — every discovered model stays on offer.
|
|
46
|
+
*
|
|
47
|
+
* A model is featured when it is one of the {@link FEATURED_MODEL_COUNT} most recently released
|
|
48
|
+
* undated ids of its provider — so `claude-opus-5` is featured while `claude-opus-5-20260401` is
|
|
49
|
+
* not. Because the featured set is derived from each discovery rather than stored, a model released
|
|
50
|
+
* tomorrow is offered the day it appears, and the user cannot end up pinned to a stale line-up.
|
|
51
|
+
*
|
|
52
|
+
* Both directions are the user's to override, and only the override is stored: a model offered by
|
|
53
|
+
* default is taken out of the picker by an entry in {@link PREFERENCE_NAME_HIDDEN_MODELS}, any other
|
|
54
|
+
* model is put into it by an entry in {@link PREFERENCE_NAME_FAVORITE_MODELS}. A model nobody touched
|
|
55
|
+
* appears in neither.
|
|
56
|
+
*
|
|
57
|
+
* Models of a provider that does not participate in discovery — one whose models are configured by
|
|
58
|
+
* hand, or a custom endpoint — are all favorites: listing them *was* the user's choice.
|
|
59
|
+
*/
|
|
60
|
+
@injectable()
|
|
61
|
+
export class FavoriteModelsService {
|
|
62
|
+
|
|
63
|
+
@inject(PreferenceService)
|
|
64
|
+
protected readonly preferenceService: PreferenceService;
|
|
65
|
+
|
|
66
|
+
@inject(ModelDiscoveryStatusService)
|
|
67
|
+
protected readonly discoveryStatus: ModelDiscoveryStatusService;
|
|
68
|
+
|
|
69
|
+
protected readonly onDidChangeEmitter = new Emitter<void>();
|
|
70
|
+
/** Fired when the marked models or the featured set change. */
|
|
71
|
+
readonly onDidChange: Event<void> = this.onDidChangeEmitter.event;
|
|
72
|
+
|
|
73
|
+
/** Memoized {@link getDerived}, dropped whenever a discovery reports something new. */
|
|
74
|
+
protected derived: DerivedFavorites | undefined;
|
|
75
|
+
|
|
76
|
+
@postConstruct()
|
|
77
|
+
protected init(): void {
|
|
78
|
+
this.discoveryStatus.onDidChange(status => {
|
|
79
|
+
this.derived = undefined;
|
|
80
|
+
this.pruneFavorites(status);
|
|
81
|
+
this.onDidChangeEmitter.fire();
|
|
82
|
+
});
|
|
83
|
+
this.preferenceService.onPreferenceChanged(event => {
|
|
84
|
+
if (event.preferenceName === PREFERENCE_NAME_FAVORITE_MODELS || event.preferenceName === PREFERENCE_NAME_HIDDEN_MODELS) {
|
|
85
|
+
this.onDidChangeEmitter.fire();
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Whether the chat input's model picker offers this model, be it featured, marked or hand-configured. */
|
|
91
|
+
isFavorite(modelId: string): boolean {
|
|
92
|
+
if (this.getHidden().includes(modelId)) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
return this.isOfferedByDefault(modelId) || this.getFavorites().includes(modelId);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Whether the picker would offer this model without the user marking it: one of the newest of a
|
|
100
|
+
* provider that discovers, or any model of a provider that does not. Nothing was discovered for
|
|
101
|
+
* the latter, so its models are there because someone put them there - a manually configured
|
|
102
|
+
* provider, or a custom endpoint. That is choice enough.
|
|
103
|
+
*/
|
|
104
|
+
protected isOfferedByDefault(modelId: string): boolean {
|
|
105
|
+
return !this.participatesInDiscovery(modelId) || this.isFeatured(modelId);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Whether the model's provider reports a discovery status, i.e. whether featuring applies to it at all. */
|
|
109
|
+
protected participatesInDiscovery(modelId: string): boolean {
|
|
110
|
+
const separator = modelId.indexOf('/');
|
|
111
|
+
if (separator <= 0) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
return this.getDerived().discoveryPrefixes.has(modelId.substring(0, separator));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Whether the model is offered automatically because it is among its provider's newest, in which
|
|
119
|
+
* case it cannot be unmarked.
|
|
120
|
+
*/
|
|
121
|
+
isFeatured(modelId: string): boolean {
|
|
122
|
+
return this.getFeatured().has(modelId);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The ids the user marked explicitly, the ones offered by default excluded. */
|
|
126
|
+
getFavorites(): string[] {
|
|
127
|
+
return this.preferenceService.get<string[]>(PREFERENCE_NAME_FAVORITE_MODELS, []);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The ids the user took out of the picker, which would otherwise be offered by default. */
|
|
131
|
+
getHidden(): string[] {
|
|
132
|
+
return this.preferenceService.get<string[]>(PREFERENCE_NAME_HIDDEN_MODELS, []);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Whether the user decided anything about this provider's models, i.e. whether resetting it to the
|
|
137
|
+
* models it offers by default would change what the picker shows.
|
|
138
|
+
*
|
|
139
|
+
* @param modelIdPrefix prefix of the provider's registered model ids, e.g. `openai`
|
|
140
|
+
*/
|
|
141
|
+
hasOverrides(modelIdPrefix: string): boolean {
|
|
142
|
+
return [...this.getFavorites(), ...this.getHidden()].some(id => id.startsWith(`${modelIdPrefix}/`));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Drops what the user decided about one provider's models, so the picker shows the models that
|
|
147
|
+
* provider offers by default again. The other providers are left alone.
|
|
148
|
+
*
|
|
149
|
+
* @param modelIdPrefix prefix of the provider's registered model ids, e.g. `openai`
|
|
150
|
+
*/
|
|
151
|
+
async resetToDefaults(modelIdPrefix: string): Promise<void> {
|
|
152
|
+
for (const preferenceName of [PREFERENCE_NAME_FAVORITE_MODELS, PREFERENCE_NAME_HIDDEN_MODELS]) {
|
|
153
|
+
const ids = this.preferenceService.get<string[]>(preferenceName, []);
|
|
154
|
+
const kept = ids.filter(id => !id.startsWith(`${modelIdPrefix}/`));
|
|
155
|
+
if (kept.length !== ids.length) {
|
|
156
|
+
await this.write(preferenceName, kept);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Puts a model into the picker or takes it out of it, whichever the current state calls for. Only
|
|
163
|
+
* the deviation from the default is stored: a model offered by default is taken out by an entry in
|
|
164
|
+
* the hidden list, and any other by dropping its entry from the favorites (or adding one).
|
|
165
|
+
*/
|
|
166
|
+
async toggleFavorite(modelId: string): Promise<void> {
|
|
167
|
+
const favorites = this.getFavorites();
|
|
168
|
+
const hidden = this.getHidden();
|
|
169
|
+
if (this.isFavorite(modelId)) {
|
|
170
|
+
if (favorites.includes(modelId)) {
|
|
171
|
+
await this.write(PREFERENCE_NAME_FAVORITE_MODELS, favorites.filter(id => id !== modelId));
|
|
172
|
+
}
|
|
173
|
+
if (this.isOfferedByDefault(modelId)) {
|
|
174
|
+
await this.write(PREFERENCE_NAME_HIDDEN_MODELS, [...hidden, modelId]);
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (hidden.includes(modelId)) {
|
|
179
|
+
await this.write(PREFERENCE_NAME_HIDDEN_MODELS, hidden.filter(id => id !== modelId));
|
|
180
|
+
}
|
|
181
|
+
// A model that was hidden while it was featured and has since fallen out of the featured set
|
|
182
|
+
// needs a mark of its own, or removing the entry would not bring it back.
|
|
183
|
+
if (!this.isOfferedByDefault(modelId)) {
|
|
184
|
+
await this.write(PREFERENCE_NAME_FAVORITE_MODELS, [...favorites, modelId]);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
protected async write(preferenceName: string, ids: string[]): Promise<void> {
|
|
189
|
+
await this.preferenceService.set(preferenceName, ids, PreferenceScope.User);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The featured ids across all providers that participate in discovery, fully qualified. */
|
|
193
|
+
protected getFeatured(): Set<string> {
|
|
194
|
+
return this.getDerived().featured;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* What the last discovery implies, computed once and kept until it changes. The chat input asks
|
|
199
|
+
* per model on every render, and ranking each provider's models per question would mean sorting
|
|
200
|
+
* the whole catalogue as many times as it has entries.
|
|
201
|
+
*/
|
|
202
|
+
protected getDerived(): DerivedFavorites {
|
|
203
|
+
if (!this.derived) {
|
|
204
|
+
const featured = new Set<string>();
|
|
205
|
+
const discoveryPrefixes = new Set<string>();
|
|
206
|
+
for (const status of this.discoveryStatus.getStatuses()) {
|
|
207
|
+
const prefix = status.modelIdPrefix ?? status.providerId;
|
|
208
|
+
discoveryPrefixes.add(prefix);
|
|
209
|
+
for (const model of this.selectFeatured(status)) {
|
|
210
|
+
featured.add(`${prefix}/${model.id}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
this.derived = { featured, discoveryPrefixes };
|
|
214
|
+
}
|
|
215
|
+
return this.derived;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The models of one provider that are offered automatically: at most {@link FEATURED_MODEL_COUNT}
|
|
220
|
+
* of its undated ids. A release-pinned id is never featured — the undated form of the same model
|
|
221
|
+
* already is, and pinning a release is the deliberate choice the switch on its row makes.
|
|
222
|
+
*
|
|
223
|
+
* They are ranked by what the provider reports about them, in that order:
|
|
224
|
+
*
|
|
225
|
+
* 1. most recently released, where the provider reports release dates (Anthropic, OpenAI);
|
|
226
|
+
* 2. a `-latest` pointer the provider maintains at the current model of a family, which only
|
|
227
|
+
* decides where there are no dates: a provider that reports them dates its pointers as well;
|
|
228
|
+
* 3. highest version in the id, which is what is left for Gemini — the naming of those models
|
|
229
|
+
* carries their generation (`gemini-3.7-flash` over `gemini-2.5-pro`).
|
|
230
|
+
*
|
|
231
|
+
* What the model aliases name has no say in this: an alias is a curated list of its own, pointing
|
|
232
|
+
* at the model a purpose should use, which is a different question from which models are worth
|
|
233
|
+
* offering in the picker.
|
|
234
|
+
*
|
|
235
|
+
* An explicitly configured list (`modelOverrides`) is featured in full: the user named it. So is a
|
|
236
|
+
* provider's own nomination ({@link DiscoveredModel.featured}), which a provider makes when the
|
|
237
|
+
* generic ranking cannot serve it — Copilot carries models of many vendors and nominates one each
|
|
238
|
+
* of the three the other providers offer directly, where "the newest five" would pick five of
|
|
239
|
+
* whichever vendor numbers its models highest.
|
|
240
|
+
*/
|
|
241
|
+
protected selectFeatured(status: ModelDiscoveryStatus): DiscoveredModel[] {
|
|
242
|
+
const discovered = status.discovered ?? [];
|
|
243
|
+
if (status.state === 'overridden') {
|
|
244
|
+
return [...discovered];
|
|
245
|
+
}
|
|
246
|
+
const nominated = discovered.filter(model => model.featured);
|
|
247
|
+
if (nominated.length > 0) {
|
|
248
|
+
return nominated;
|
|
249
|
+
}
|
|
250
|
+
return discovered
|
|
251
|
+
.filter(model => DiscoveredModels.undatedId(model.id) === undefined)
|
|
252
|
+
.map((model, index) => ({ model, index }))
|
|
253
|
+
.sort((left, right) => this.compareFeatured(left, right))
|
|
254
|
+
.slice(0, FEATURED_MODEL_COUNT)
|
|
255
|
+
.map(entry => entry.model);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Orders two candidates of the same provider; `index` preserves the reported order as the last resort. */
|
|
259
|
+
protected compareFeatured(
|
|
260
|
+
left: { model: DiscoveredModel; index: number },
|
|
261
|
+
right: { model: DiscoveredModel; index: number }
|
|
262
|
+
): number {
|
|
263
|
+
const byRelease = (right.model.released ?? 0) - (left.model.released ?? 0);
|
|
264
|
+
if (byRelease !== 0) {
|
|
265
|
+
return byRelease;
|
|
266
|
+
}
|
|
267
|
+
// Only where a provider reports no dates: a pointer at its current model of a family is as
|
|
268
|
+
// current as a model gets, and it carries no version, so it would otherwise sort behind every
|
|
269
|
+
// id that does. Where there are dates, the pointer carries one too and has been ordered by it.
|
|
270
|
+
const byPointer = Number(DiscoveredModels.isLatestPointer(right.model.id)) - Number(DiscoveredModels.isLatestPointer(left.model.id));
|
|
271
|
+
if (byPointer !== 0) {
|
|
272
|
+
return byPointer;
|
|
273
|
+
}
|
|
274
|
+
const byVersion = DiscoveredModels.compareByVersion(left.model.id, right.model.id);
|
|
275
|
+
return byVersion !== 0 ? byVersion : left.index - right.index;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Drops the marked and hidden ids of a provider that a successful discovery no longer offers, so
|
|
280
|
+
* that neither list accumulates models that no longer exist. Only a live discovery prunes: a
|
|
281
|
+
* cached result, a failure or a missing credential says nothing about what the provider offers.
|
|
282
|
+
*/
|
|
283
|
+
protected pruneFavorites(status: ModelDiscoveryStatus): void {
|
|
284
|
+
if (status.state !== 'ready' || status.fromCache || !status.discovered) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const prefix = status.modelIdPrefix ?? status.providerId;
|
|
288
|
+
const offered = new Set(status.discovered.map(model => `${prefix}/${model.id}`));
|
|
289
|
+
for (const preferenceName of [PREFERENCE_NAME_FAVORITE_MODELS, PREFERENCE_NAME_HIDDEN_MODELS]) {
|
|
290
|
+
const ids = this.preferenceService.get<string[]>(preferenceName, []);
|
|
291
|
+
const kept = ids.filter(id => !id.startsWith(`${prefix}/`) || offered.has(id));
|
|
292
|
+
if (kept.length !== ids.length) {
|
|
293
|
+
this.write(preferenceName, kept);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
@@ -67,7 +67,7 @@ export class DefaultLanguageModelAliasRegistry implements LanguageModelAliasRegi
|
|
|
67
67
|
defaultModelIds: [
|
|
68
68
|
'anthropic/claude-haiku-4-5',
|
|
69
69
|
'openai/gpt-5.6-luna',
|
|
70
|
-
'google/gemini-3.
|
|
70
|
+
'google/gemini-3.8-flash'
|
|
71
71
|
],
|
|
72
72
|
description: nls.localize('theia/ai/core/defaultModelAliases/fast/description',
|
|
73
73
|
'Faster and cheaper models for simpler tasks like exploration or basic tool calling, where deep reasoning is not required.')
|