src/vs/workbench/contrib/chat/common/languageModels.ts

2510 LOC · 1820 covered · 690 uncovered · 312 ranges · 1047 concepts · 72 introducers · 526 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- languageModels.ts ×93
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { SequencerByKey, timeout } from '../../../../base/common/async.js';
7 > import { VSBuffer } from '../../../../base/common/buffer.js';
8 > import { CancellationToken } from '../../../../base/common/cancellation.js';
9 > import { IStringDictionary } from '../../../../base/common/collections.js';
10 > import { CancellationError, getErrorMessage, isCancellationError } from '../../../../base/common/errors.js';
11 > import { Emitter, Event } from '../../../../base/common/event.js';
12 > import { hash } from '../../../../base/common/hash.js';
13 > import { Iterable } from '../../../../base/common/iterator.js';
14 > import { IJSONSchema, TypeFromJsonSchema } from '../../../../base/common/jsonSchema.js';
15 > import { DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
16 > import { IObservable, observableValue } from '../../../../base/common/observable.js';
17 > import { equals } from '../../../../base/common/objects.js';
18 > import Severity from '../../../../base/common/severity.js';
19 > import { format, isFalsyOrWhitespace } from '../../../../base/common/strings.js';
20 > import { ThemeIcon } from '../../../../base/common/themables.js';
21 > import { IAction, SubmenuAction } from '../../../../base/common/actions.js';
22 > import { isObject, isString } from '../../../../base/common/types.js';
23 > import { Schemas } from '../../../../base/common/network.js';
24 > import { URI } from '../../../../base/common/uri.js';
25 > import { generateUuid } from '../../../../base/common/uuid.js';
26 > import { localize } from '../../../../nls.js';
27 > import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
28 > import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js';
29 > import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
30 > import { ILogService } from '../../../../platform/log/common/log.js';
31 > import { INotificationService, NeverShowAgainScope } from '../../../../platform/notification/common/notification.js';
32 > import { IOpenerService } from '../../../../platform/opener/common/opener.js';
33 > import { IProductService } from '../../../../platform/product/common/productService.js';
34 > import { asJson, IRequestService } from '../../../../platform/request/common/request.js';
35 > import { IQuickInputService, IQuickPickItem, QuickInputHideReason } from '../../../../platform/quickinput/common/quickInput.js';
36 > import { ISecretStorageService } from '../../../../platform/secrets/common/secrets.js';
37 > import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
38 > import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
39 > import { IExtensionService } from '../../../services/extensions/common/extensions.js';
40 > import { ExtensionsRegistry } from '../../../services/extensions/common/extensionsRegistry.js';
41 > import { ChatContextKeys } from './actions/chatContextKeys.js';
42 > import { ChatAgentLocation } from './constants.js';
43 > import { ILanguageModelsProviderGroup, ILanguageModelsConfigurationService } from './languageModelsConfiguration.js';
44 >
45 > /**
46 > * Vendor id used for the built-in GitHub Copilot language model provider. Treated as the default
47 > * vendor across the chat stack (see `ILanguageModelProviderDescriptor.isDefault`).
48 > */
49 > export const COPILOT_VENDOR_ID = 'copilot';
50 >
51 > /** Whether a missing model is conclusively absent from a vendor's live model list. Empty Copilot results remain transient while token-backed discovery completes. */
52 > export function isLanguageModelVendorAbsenceConclusive(vendor: string, hasLiveModels: boolean, hasResolved: boolean): boolean {
53 > return hasLiveModels || (hasResolved && vendor !== COPILOT_VENDOR_ID); modelSelection.ts ×2
54 > }
56 > /**
57 > * Vendor ids of the BYOK language-model providers that ship in-built with the GitHub Copilot Chat
58 > * extension. Each provider's vendor id is `providerName.toLowerCase()` (see
59 > * `extensions/copilot/src/extension/byok/vscode-node/*Provider.ts`). This list is intentionally
60 > * hardcoded: the in-built provider set is stable and known ahead of time, which lets us report these
61 > * providers by name while bucketing every other (third-party) provider as `3p-extension`.
62 > */
63 > const BUILT_IN_BYOK_VENDOR_IDS = new Set<string>([
64 > 'openai',
65 > 'anthropic',
66 > 'gemini',
67 > 'ollama',
68 > 'openrouter',
69 > 'azure',
70 > 'xai',
71 > 'customoai',
72 > 'customendpoint',
73 > ]);
74 >
75 > /**
76 > * Bucket reported for any non-Copilot provider that is not an in-built BYOK provider, i.e. a model
77 > * contributed by a third-party extension. We never report the third-party vendor id directly to avoid
78 > * logging potentially identifying values.
79 > */
80 > export const THIRD_PARTY_PROVIDER_TELEMETRY_NAME = '3p-extension';
81 >
82 > const BUILT_IN_BYOK_EXTENSION_IDS = [
83 > 'github.copilot-chat',
84 > 'github.copilot',
85 > ];
86 >
87 > /**
88 > * Normalizes a non-Copilot model vendor into a non-identifying provider name suitable for telemetry:
89 > * the in-built BYOK vendor id (e.g. `openai`, `ollama`) when contributed by the built-in Copilot
90 > * extensions, or {@link THIRD_PARTY_PROVIDER_TELEMETRY_NAME} otherwise. Returns `undefined` for the
91 > * first-party Copilot vendor (or no vendor) so callers skip logging first-party usage.
92 > */
93 > export function getByokProviderTelemetryName(vendor: string | undefined, extension: ExtensionIdentifier | undefined): string | undefined {
94 > if (!vendor || vendor === COPILOT_VENDOR_ID) { languageModels.ts ×2
95 > return undefined; languageModels.ts ×1
96 > }
97 > if (BUILT_IN_BYOK_VENDOR_IDS.has(vendor) && extension && BUILT_IN_BYOK_EXTENSION_IDS.some(id => ExtensionIdentifier.equals(extension, id))) { languageModels.ts ×2
98 > return vendor; languageModels.ts ×1
99 > }
100 > return THIRD_PARTY_PROVIDER_TELEMETRY_NAME; languageModels.ts ×1
101 > }
103 > export const enum ChatMessageRole {
104 > System,
105 > User,
106 > Assistant,
107 > }
108 >
109 > export enum LanguageModelPartAudience {
110 > Assistant = 0,
111 > User = 1,
112 > Extension = 2,
113 > }
114 >
115 > export interface IChatMessageTextPart {
116 > type: 'text';
117 > value: string;
118 > audience?: LanguageModelPartAudience[];
119 > }
120 >
121 > export interface IChatMessageImagePart {
122 > type: 'image_url';
123 > value: IChatImageURLPart;
124 > }
125 >
126 > export interface IChatMessageThinkingPart {
127 > type: 'thinking';
128 > value: string | string[];
129 > id?: string;
130 > // eslint-disable-next-line @typescript-eslint/no-explicit-any
131 > metadata?: { readonly [key: string]: any };
132 > }
133 >
134 > export interface IChatMessageDataPart {
135 > type: 'data';
136 > mimeType: string;
137 > data: VSBuffer;
138 > audience?: LanguageModelPartAudience[];
139 > }
140 >
141 > export interface IChatImageURLPart {
142 > /**
143 > * The image's MIME type (e.g., "image/png", "image/jpeg").
144 > */
145 > mimeType: ChatImageMimeType;
146 >
147 > /**
148 > * The raw binary data of the image, encoded as a Uint8Array. Note: do not use base64 encoding. Maximum image size is 5MB.
149 > */
150 > data: VSBuffer;
151 > }
152 >
153 > /**
154 > * Enum for supported image MIME types.
155 > */
156 > export enum ChatImageMimeType {
157 > PNG = 'image/png',
158 > JPEG = 'image/jpeg',
159 > GIF = 'image/gif',
160 > WEBP = 'image/webp',
161 > BMP = 'image/bmp',
162 > }
163 >
164 > /**
165 > * Specifies the detail level of the image.
166 > */
167 > export enum ImageDetailLevel {
168 > Low = 'low',
169 > High = 'high'
170 > }
171 >
172 >
173 > export interface IChatMessageToolResultPart {
174 > type: 'tool_result';
175 > toolCallId: string;
176 > value: (IChatResponseTextPart | IChatResponsePromptTsxPart | IChatResponseDataPart)[];
177 > isError?: boolean;
178 > }
179 >
180 > export type IChatMessagePart = IChatMessageTextPart | IChatMessageToolResultPart | IChatResponseToolUsePart | IChatMessageImagePart | IChatMessageDataPart | IChatMessageThinkingPart;
181 >
182 > export interface IChatMessage {
183 > readonly name?: string | undefined;
184 > readonly role: ChatMessageRole;
185 > readonly content: IChatMessagePart[];
186 > }
187 >
188 > export interface IChatResponseTextPart {
189 > type: 'text';
190 > value: string;
191 > audience?: LanguageModelPartAudience[];
192 > }
193 >
194 > export interface IChatResponsePromptTsxPart {
195 > type: 'prompt_tsx';
196 > value: unknown;
197 > }
198 >
199 > export interface IChatResponseDataPart {
200 > type: 'data';
201 > mimeType: string;
202 > data: VSBuffer;
203 > audience?: LanguageModelPartAudience[];
204 > }
205 >
206 > export interface IChatResponseToolUsePart {
207 > type: 'tool_use';
208 > name: string;
209 > toolCallId: string;
210 > // eslint-disable-next-line @typescript-eslint/no-explicit-any
211 > parameters: any;
212 > }
213 >
214 > export interface IChatResponseThinkingPart {
215 > type: 'thinking';
216 > value: string | string[];
217 > id?: string;
218 > // eslint-disable-next-line @typescript-eslint/no-explicit-any
219 > metadata?: { readonly [key: string]: any };
220 > }
221 >
222 > export interface IChatResponsePullRequestPart {
223 > type: 'pullRequest';
224 > uri: URI;
225 > title: string;
226 > description: string;
227 > author: string;
228 > linkTag: string;
229 > }
230 >
231 > export type IChatResponsePart = IChatResponseTextPart | IChatResponseToolUsePart | IChatResponseDataPart | IChatResponseThinkingPart;
232 >
233 > export type IExtendedChatResponsePart = IChatResponsePullRequestPart;
234 >
235 > export interface ILanguageModelConfigurationSchema extends IJSONSchema {
236 > properties?: {
237 > [key: string]: IJSONSchema & {
238 > /** When set to `'navigation'`, the property is shown as a primary action in the model picker. */
239 > group?: string;
240 > /** Labels for enum values. If provided, these are shown instead of the raw enum values. */
241 > enumItemLabels?: string[];
242 > };
243 > };
244 > }
245 >
246 > export interface ILanguageModelChatMetadata {
247 > readonly extension: ExtensionIdentifier;
248 >
249 > readonly name: string;
250 > readonly id: string;
251 > readonly vendor: string;
252 > readonly version: string;
253 > readonly tooltip?: string;
254 > readonly detail?: string;
255 > readonly multiplierNumeric?: number;
256 > readonly isBYOK?: boolean;
257 > readonly pricing?: string;
258 > readonly inputCost?: number;
259 > readonly cacheCost?: number;
260 > readonly cacheWriteCost?: number;
261 > readonly outputCost?: number;
262 > readonly longContextInputCost?: number;
263 > readonly longContextCacheCost?: number;
264 > readonly longContextCacheWriteCost?: number;
265 > readonly longContextOutputCost?: number;
266 > readonly priceCategory?: string;
267 > readonly category?: string;
268 > readonly family: string;
269 > readonly maxInputTokens: number;
270 > readonly maxOutputTokens: number;
271 >
272 > readonly isDefaultForLocation: { [K in ChatAgentLocation]?: boolean };
273 > readonly isUserSelectable?: boolean;
274 > readonly statusIcon?: ThemeIcon;
275 > readonly auth?: {
276 > readonly providerLabel: string;
277 > readonly accountLabel?: string;
278 > };
279 > readonly capabilities?: {
280 > readonly vision?: boolean;
281 > readonly toolCalling?: boolean;
282 > readonly agentMode?: boolean;
283 > readonly editTools?: ReadonlyArray<string>;
284 > };
285 > /**
286 > * When set, this model is only shown in the model picker for the specified chat session type.
287 > * Models with this property are excluded from the general model picker and only appear
288 > * when the user is in a session matching this type.
289 > */
290 > readonly targetChatSessionType?: string;
291 > /**
292 > * Optional grouping hint for the model picker. When set, the picker buckets this model
293 > * under a sub-group within its vendor, identified by this vendor id — e.g. agent-host models,
294 > * which all share one vendor, grouped by their upstream provider — instead of a single
295 > * vendor-wide bucket. The display name is resolved from the vendor registry
296 > * ({@link ILanguageModelsService.getVendors}), the same source used for every other vendor.
297 > * Presentation-only; it does not affect model selection or routing.
298 > */
299 > readonly modelGroup?: { readonly id: string };
300 > /**
301 > * For an agent-host copy of an extension-provided BYOK model, the identifier the
302 > * original model is registered under in the renderer's LM service
303 > * (`toModelIdentifier(vendor, group, id)` — `<vendor>/<group>/<id>` or `<vendor>/<id>`).
304 > * This is exactly the id the "Manage Models" view keys visibility by; it is carried
305 > * across the agent-host bridge and surfaced here so the model picker can honour the
306 > * model's visibility toggle. Absent for native agent-host models and non-agent-host
307 > * models.
308 > */
309 > readonly byokModelIdentifier?: string;
310 > /**
311 > * An optional JSON schema describing the per-model configuration options.
312 > * Used to validate user-provided per-model configuration in `chatLanguageModels.json`.
313 > */
314 > readonly configurationSchema?: ILanguageModelConfigurationSchema;
315 > /**
316 > * Optional warning text to display in the model picker hover as a warning banner.
317 > * The keys are warning categories (e.g. "data_retention") and the values are markdown strings.
318 > */
319 > readonly warningText?: IStringDictionary<string>;
320 > /**
321 > * Optional promotional information for this model. Positive discounts surface
322 > * promotional UI; non-positive discounts only feature the model in the picker.
323 > */
324 > readonly promo?: {
325 > readonly id: string;
326 > readonly discountPercent: number;
327 > readonly endsAt: string;
328 > readonly message: string;
329 > };
330 > }
331 >
332 > export namespace ILanguageModelChatMetadata {
333 > export function suitableForAgentMode(metadata: ILanguageModelChatMetadata): boolean {
334 > const supportsToolsAgent = typeof metadata.capabilities?.agentMode === 'undefined' || metadata.capabilities.agentMode; runSubagentTool.ts ×4
335 > return supportsToolsAgent && !!metadata.capabilities?.toolCalling;
336 > }
338 > export function asQualifiedName(metadata: ILanguageModelChatMetadata): string {
339 > return `${metadata.name} (${metadata.vendor})`; runSubagentTool.ts ×4
340 > }
342 > export function matchesQualifiedName(name: string, metadata: ILanguageModelChatMetadata): boolean {
343 if (metadata.vendor === COPILOT_VENDOR_ID && name === metadata.name) {
344 return true;
345 }
346 return name === asQualifiedName(metadata);
347 }
349 > export function hasPromoDiscount(metadata: ILanguageModelChatMetadata): metadata is ILanguageModelChatMetadata & { readonly promo: NonNullable<ILanguageModelChatMetadata['promo']> } {
350 return !!metadata.promo && metadata.promo.discountPercent > 0;
351 }
353 > /**
354 > * Documentation link explaining how Auto model selection works.
355 > * NOTE: Also defined in extensions/copilot/src/extension/conversation/common/languageModelAccess.ts — keep in sync.
356 > */
357 > export const autoModelSelectionDocsUrl = 'https://docs.github.com/en/copilot/concepts/models/auto-model-selection';
358 >
359 > /**
360 > * Builds the shared description shown for the Auto model, rendered as Markdown
361 > * (it contains a "Learn More" link). The discount sentence is only included
362 > * when a positive discount is provided.
363 > *
364 > * @param discountPercent Whole-number percentage (e.g. `10` for 10%). When
365 > * omitted or not positive, the discount sentence is left out entirely.
366 > */
367 > export function getAutoModelDescription(discountPercent?: number): string {
368 const base = localize('autoModel.description', "Auto routes based on your task and real-time system health and model performance.");
369 const learnMore = localize('autoModel.learnMore', "[Learn More]({0})", autoModelSelectionDocsUrl);
370 if (typeof discountPercent === 'number' && discountPercent > 0) {
371 const discount = localize('autoModel.discount', "Models routed via auto receive a {0}% discount.", discountPercent);
372 return `${base} ${discount} ${learnMore}`;
373 }
374 return `${base} ${learnMore}`;
375 }
377 > /**
378 > * The "Manage Models" identifier that an agent-host copy of an extension-provided
379 > * BYOK model is toggled under, or `undefined` when the model is not such a copy.
380 > *
381 > * Agent-host BYOK models make a round trip that rewrites their id (the node agent host
382 > * re-advertises the extension model under the agent-host vendor). Their original LM
383 > * service identifier — `toModelIdentifier(vendor, group, id)`, i.e. `<vendor>/<group>/<id>`
384 > * or `<vendor>/<id>`, which is what the Manage Models view stores when hiding the model —
385 > * is carried across the bridge and surfaced on {@link ILanguageModelChatMetadata.byokModelIdentifier}.
386 > * This returns it, so callers can match the copy against the user's visibility toggles.
387 > *
388 > * Returns `undefined` for models that are not agent-host BYOK copies (native harness
389 > * models and non-agent-host models), which are matched by their own identifier instead.
390 > */
391 > export function getAgentHostByokManageModelsIdentifier(metadata: ILanguageModelChatMetadata): string | undefined {
392 > return metadata.byokModelIdentifier; languageModels.ts ×5
393 > }
395 >
396 > export interface ILanguageModelChatResponse {
397 > stream: AsyncIterable<IChatResponsePart | IChatResponsePart[]>;
398 > // eslint-disable-next-line @typescript-eslint/no-explicit-any
399 > result: Promise<any>;
400 > }
401 >
402 export async function getTextResponseFromStream(response: ILanguageModelChatResponse): Promise<string> {
403 let responseText = '';
404 const streaming = (async () => {
405 if (!response?.stream) {
406 return;
407 }
408 for await (const part of response.stream) {
409 if (Array.isArray(part)) {
410 for (const item of part) {
411 if (item.type === 'text') {
412 responseText += item.value;
413 }
414 }
415 } else if (part.type === 'text') {
416 responseText += part.value;
417 }
418 }
419 })();
420
421 try {
422 await Promise.all([response.result, streaming]);
423 return responseText;
424 } catch (err) {
425 if (responseText) {
426 return responseText;
427 }
428 throw err;
429 }
430 }
432 > export interface ILanguageModelChatProvider {
433 > readonly onDidChange: Event<void>;
434 > provideLanguageModelChatInfo(options: ILanguageModelChatInfoOptions, token: CancellationToken): Promise<ILanguageModelChatMetadataAndIdentifier[]>;
435 > sendChatRequest(modelId: string, messages: IChatMessage[], from: ExtensionIdentifier | undefined, options: ILanguageModelChatRequestOptions, token: CancellationToken): Promise<ILanguageModelChatResponse>;
436 > provideTokenCount(modelId: string, message: string | IChatMessage, token: CancellationToken): Promise<number>;
437 > }
438 >
439 > export interface ILanguageModelChat {
440 > metadata: ILanguageModelChatMetadata;
441 > sendChatRequest(messages: IChatMessage[], from: ExtensionIdentifier | undefined, options: ILanguageModelChatRequestOptions, token: CancellationToken): Promise<ILanguageModelChatResponse>;
442 > provideTokenCount(message: string | IChatMessage, token: CancellationToken): Promise<number>;
443 > }
444 >
445 > export interface ILanguageModelChatSelector {
446 > readonly name?: string;
447 > readonly id?: string;
448 > readonly vendor?: string;
449 > readonly version?: string;
450 > readonly family?: string;
451 > readonly tokens?: number;
452 > readonly extension?: ExtensionIdentifier;
453 > }
454 >
455 >
456 > export function isILanguageModelChatSelector(value: unknown): value is ILanguageModelChatSelector {
457 if (typeof value !== 'object' || value === null) {
458 return false;
459 }
460 const obj = value as Record<string, unknown>;
461 return (
462 (obj.name === undefined || typeof obj.name === 'string') &&
463 (obj.id === undefined || typeof obj.id === 'string') &&
464 (obj.vendor === undefined || typeof obj.vendor === 'string') &&
465 (obj.version === undefined || typeof obj.version === 'string') &&
466 (obj.family === undefined || typeof obj.family === 'string') &&
467 (obj.tokens === undefined || typeof obj.tokens === 'number') &&
468 (obj.extension === undefined || typeof obj.extension === 'object')
469 );
470 }
472 > export const ILanguageModelsService = createDecorator<ILanguageModelsService>('ILanguageModelsService');
473 >
474 > export interface ILanguageModelChatMetadataAndIdentifier {
475 > metadata: ILanguageModelChatMetadata;
476 > identifier: string;
477 > }
478 >
479 > export interface ILanguageModelChatInfoOptions {
480 > readonly group?: string;
481 > readonly silent: boolean;
482 > readonly configuration?: IStringDictionary<unknown>;
483 > }
484 >
485 > export interface ILanguageModelChatRequestOptions {
486 > readonly modelOptions?: IStringDictionary<unknown>;
487 > readonly configuration?: IStringDictionary<unknown>;
488 > // eslint-disable-next-line @typescript-eslint/no-explicit-any
489 > readonly [name: string]: any;
490 > }
491 >
492 > export interface ILanguageModelsGroup {
493 > readonly group?: ILanguageModelsProviderGroup;
494 > readonly modelIdentifiers: string[];
495 > readonly status?: {
496 > readonly message: string;
497 > readonly severity: Severity;
498 > };
499 > }
500 >
501 > export interface ILanguageModelsService {
502 >
503 > readonly _serviceBrand: undefined;
504 >
505 > readonly onDidChangeLanguageModelVendors: Event<readonly string[]>;
506 > readonly onDidChangeLanguageModels: Event<string>;
507 >
508 > getLanguageModelIds(): string[];
509 >
510 > getVendors(): ILanguageModelProviderDescriptor[];
511 >
512 > lookupLanguageModel(modelId: string): ILanguageModelChatMetadata | undefined;
513 >
514 > /**
515 > * Find a model by its qualified name. The qualified name is what is used in prompt and agent files and is in the format "Model Name (Vendor)".
516 > */
517 > lookupLanguageModelByQualifiedName(qualifiedName: string): ILanguageModelChatMetadataAndIdentifier | undefined;
518 >
519 > getLanguageModelGroups(vendor: string): ILanguageModelsGroup[];
520 >
521 > /**
522 > * Returns true if the given vendor's provider has completed at least one
523 > * model resolution since registration. A `false` result indicates the
524 > * vendor is still in a startup/reload race where its model list isn't yet
525 > * authoritative — callers can fall back to a cached list in that case.
526 > */
527 > hasResolvedVendor(vendor: string): boolean;
528 >
529 > /**
530 > * Given a selector, returns a list of model identifiers
531 > * @param selector The selector to lookup for language models. If the selector is empty, all language models are returned.
532 > */
533 > selectLanguageModels(selector: ILanguageModelChatSelector): Promise<string[]>;
534 >
535 > registerLanguageModelProvider(vendor: string, provider: ILanguageModelChatProvider): IDisposable;
536 >
537 > deltaLanguageModelChatProviderDescriptors(added: IUserFriendlyLanguageModel[], removed: IUserFriendlyLanguageModel[]): void;
538 >
539 > sendChatRequest(modelId: string, from: ExtensionIdentifier | undefined, messages: IChatMessage[], options: ILanguageModelChatRequestOptions, token: CancellationToken): Promise<ILanguageModelChatResponse>;
540 >
541 > computeTokenLength(modelId: string, message: string | IChatMessage, token: CancellationToken): Promise<number>;
542 >
543 > /**
544 > * Returns the resolved per-model configuration for the given model identifier.
545 > * Includes schema defaults with user overrides applied on top.
546 > * Returns undefined if the model has no configuration schema and no user config.
547 > */
548 > getModelConfiguration(modelId: string): IStringDictionary<unknown> | undefined;
549 >
550 > /**
551 > * Updates the per-model configuration for the given model.
552 > * Merges the provided values into the existing configuration.
553 > */
554 > setModelConfiguration(modelId: string, values: IStringDictionary<unknown>): Promise<void>;
555 >
556 > /**
557 > * Returns actions for configuring the given model based on its configuration schema.
558 > * For enum properties, returns submenu actions with checkable values.
559 > * Returns an empty array if the model has no configuration schema.
560 > */
561 > getModelConfigurationActions(modelId: string): IAction[];
562 >
563 > addLanguageModelsProviderGroup(name: string, vendorId: string, configuration: IStringDictionary<unknown> | undefined): Promise<void>;
564 >
565 > removeLanguageModelsProviderGroup(vendorId: string, providerGroupName: string): Promise<void>;
566 >
567 > configureLanguageModelsProviderGroup(vendorId: string, name?: string): Promise<void>;
568 >
569 > renameLanguageModelsProviderGroup(vendorId: string, providerGroupName: string): Promise<void>;
570 >
571 > updateLanguageModelsProviderGroupApiKey(vendorId: string, providerGroupName: string): Promise<void>;
572 >
573 > addLanguageModelsProviderGroupModel(vendorId: string, providerGroupName: string): Promise<void>;
574 >
575 > openLanguageModelsProviderGroupSettings(vendorId: string, providerGroupName: string): Promise<void>;
576 >
577 > /**
578 > * Opens the language models configuration file and navigates to
579 > * or creates the per-model configuration for the given model.
580 > */
581 > configureModel(modelId: string): Promise<void>;
582 >
583 > migrateLanguageModelsProviderGroup(languageModelsProviderGroup: ILanguageModelsProviderGroup): Promise<void>;
584 >
585 > /**
586 > * Returns the most recently used model identifiers, ordered by most-recent-first.
587 > * @param maxCount Maximum number of entries to return (default 7).
588 > */
589 > getRecentlyUsedModelIds(): string[];
590 >
591 > /**
592 > * Records that a model was used, updating the recently used list.
593 > */
594 > addToRecentlyUsedList(modelIdentifier: string): void;
595 >
596 > /**
597 > * Clears the recently used model list.
598 > */
599 > clearRecentlyUsedList(): void;
600 >
601 > /**
602 > * Returns the pinned model identifiers, in the order they were pinned.
603 > */
604 > getPinnedModelIds(): string[];
605 >
606 > /**
607 > * Pins a model so it appears in the pinned section of the model picker.
608 > */
609 > pinModel(modelIdentifier: string): void;
610 >
611 > /**
612 > * Unpins a model, removing it from the pinned section.
613 > */
614 > unpinModel(modelIdentifier: string): void;
615 >
616 > /**
617 > * Returns whether the given model is pinned.
618 > */
619 > isModelPinned(modelIdentifier: string): boolean;
620 >
621 > /**
622 > * Fires when the pinned models list changes.
623 > */
624 > readonly onDidChangePinnedModels: Event<void>;
625 >
626 > /**
627 > * Returns whether the given model is hidden from the chat model picker.
628 > */
629 > isModelHidden(modelIdentifier: string): boolean;
630 >
631 > /**
632 > * Returns whether every resolved model in the given (vendor, groupName)
633 > * bucket is hidden from the chat model picker.
634 > */
635 > isGroupHidden(vendor: string, groupName: string): boolean;
636 >
637 > /**
638 > * Hide or show a single model in the chat model picker.
639 > */
640 > setModelHidden(modelIdentifier: string, hidden: boolean): void;
641 >
642 > /**
643 > * Hide or show every model in a (vendor, groupName) bucket.
644 > */
645 > setGroupHidden(vendor: string, groupName: string, hidden: boolean): void;
646 >
647 > /**
648 > * Returns the persisted per-model hidden identifiers.
649 > */
650 > getHiddenModelIds(): string[];
651 >
652 > /**
653 > * Fires when any model or group visibility state changes.
654 > */
655 > readonly onDidChangeModelVisibility: Event<void>;
656 >
657 > /**
658 > * Returns the models from the control manifest,
659 > * separated into free and paid tiers.
660 > */
661 > getModelsControlManifest(): IModelsControlManifest;
662 >
663 > /**
664 > * Fires when models control manifest changes.
665 > */
666 > readonly onDidChangeModelsControlManifest: Event<IModelsControlManifest>;
667 >
668 > /**
669 > * Observable map of restricted chat participant names to allowed extension publisher/IDs.
670 > * Fetched from the chat control manifest.
671 > */
672 > readonly restrictedChatParticipants: IObservable<{ [name: string]: string[] }>;
673 > }
674 >
675 > export interface IModelControlEntry {
676 > readonly label: string;
677 > readonly featured?: boolean;
678 > readonly minVSCodeVersion?: string;
679 > readonly exists: boolean;
680 > }
681 >
682 > export interface IModelsControlManifest {
683 > readonly free: IStringDictionary<IModelControlEntry>;
684 > readonly paid: IStringDictionary<IModelControlEntry>;
685 > }
686 >
687 > const languageModelChatProviderType = {
688 > type: 'object',
689 > required: ['vendor', 'displayName'],
690 > properties: {
691 > vendor: {
692 > type: 'string',
693 > description: localize('vscode.extension.contributes.languageModels.vendor', "A globally unique vendor of language model chat provider.")
694 > },
695 > displayName: {
696 > type: 'string',
697 > description: localize('vscode.extension.contributes.languageModels.displayName', "The display name of the language model chat provider.")
698 > },
699 > configuration: {
700 > type: 'object',
701 > description: localize('vscode.extension.contributes.languageModels.configuration', "Configuration options for the language model chat provider."),
702 > anyOf: [
703 > {
704 > $ref: 'http://json-schema.org/draft-07/schema#'
705 > },
706 > {
707 > properties: {
708 > properties: {
709 > type: 'object',
710 > additionalProperties: {
711 > $ref: 'http://json-schema.org/draft-07/schema#',
712 > properties: {
713 > secret: {
714 > type: 'boolean',
715 > description: localize('vscode.extension.contributes.languageModels.configuration.secret', "Whether the property is a secret.")
716 > }
717 > }
718 > }
719 > },
720 > additionalProperties: {
721 > $ref: 'http://json-schema.org/draft-07/schema#',
722 > properties: {
723 > secret: {
724 > type: 'boolean',
725 > description: localize('vscode.extension.contributes.languageModels.configuration.secret', "Whether the property is a secret.")
726 > }
727 > }
728 > }
729 > }
730 > }
731 > ]
732 >
733 > },
734 > managementCommand: {
735 > type: 'string',
736 > description: localize('vscode.extension.contributes.languageModels.managementCommand', "A command to manage the language model chat provider, e.g. 'Manage Copilot models'. This is used in the chat model picker. If not provided, a gear icon is not rendered during vendor selection."),
737 > deprecated: true,
738 > deprecationMessage: localize('vscode.extension.contributes.languageModels.managementCommand.deprecated', "The managementCommand property is deprecated and will be removed in a future release. Use the new configuration property instead.")
739 > },
740 > deprecation: {
741 > type: 'object',
742 > description: localize('vscode.extension.contributes.languageModels.deprecation', "Marks this language model chat provider as deprecated. When set, the Manage Models view renders the provider with a link pointing to a replacement."),
743 > properties: {
744 > link: {
745 > type: 'string',
746 > description: localize('vscode.extension.contributes.languageModels.deprecation.link', "A URL opened when the user clicks the deprecation link shown next to the provider name. Use a 'vscode:extension/<publisher>.<name>' URI to open a replacement extension in the Extensions view.")
747 > }
748 > }
749 > },
750 > when: {
751 > type: 'string',
752 > description: localize('vscode.extension.contributes.languageModels.when', "Condition which must be true to show this language model chat provider in the Manage Models list.")
753 > }
754 > }
755 > } as const satisfies IJSONSchema;
756 >
757 > export type IUserFriendlyLanguageModel = Omit<TypeFromJsonSchema<typeof languageModelChatProviderType>, 'deprecation'> & {
758 > /**
759 > * Marks a provider as deprecated. The Manage Models view renders a link
760 > * (pointing to a replacement, e.g. a `vscode:extension/<publisher>.<name>` URI)
761 > * next to the provider name. Optional so existing provider descriptors are unaffected.
762 > */
763 > readonly deprecation?: { readonly link?: string };
764 > };
765 >
766 > export interface ILanguageModelProviderDescriptor extends IUserFriendlyLanguageModel {
767 > readonly isDefault: boolean;
768 > }
769 >
770 > /**
771 > * Resolves a provider `deprecation.link` for opening inside the current build. Contributions point
772 > * at the replacement extension with a stable `vscode:extension/<id>` URI, but the URL service only
773 > * routes URIs whose scheme matches this build's `urlProtocol` (e.g. `code-oss`, `vscode-insiders`).
774 > * The `vscode:` scheme is therefore rewritten to the current protocol so the extensions URL handler
775 > * opens the extension; without this the opener falls back to treating the URI as a (non-existent)
776 > * file resource and fails. Other schemes (http(s), command) are returned unchanged.
777 > */
778 > export function resolveProviderDeprecationLink(link: string, urlProtocol: string | undefined): URI {
779 > const uri = URI.parse(link); languageModels.ts ×1
780 > return uri.scheme === Schemas.vscode && urlProtocol ? uri.with({ scheme: urlProtocol }) : uri;
781 > }
783 > export const languageModelChatProviderExtensionPoint = ExtensionsRegistry.registerExtensionPoint<IUserFriendlyLanguageModel | IUserFriendlyLanguageModel[]>({
784 > extensionPoint: 'languageModelChatProviders',
785 > jsonSchema: {
786 > description: localize('vscode.extension.contributes.languageModelChatProviders', "Contribute language model chat providers of a specific vendor."),
787 > oneOf: [
788 > languageModelChatProviderType,
789 > {
790 > type: 'array',
791 > items: languageModelChatProviderType
792 > }
793 > ]
794 > },
795 > activationEventsGenerator: function* (contribs: readonly IUserFriendlyLanguageModel[]) {
796 for (const contrib of contribs) {
797 yield `onLanguageModelChatProvider:${contrib.vendor}`;
798 }
799 }
801 >
802 > const CHAT_MODEL_RECENTLY_USED_STORAGE_KEY = 'chatModelRecentlyUsed';
803 > const CHAT_MODEL_PINNED_STORAGE_KEY = 'chatModelPinned';
804 > const CHAT_MODEL_VISIBILITY_STORAGE_KEY = 'chatModelVisibility';
805 >
806 > /**
807 > * The identifier for the Auto model which dynamically routes to the best backend.
808 > * Auto should never appear in user-curated lists (MRU, pinned).
809 > */
810 > const AUTO_MODEL_IDENTIFIER = 'copilot/auto';
811 >
812 > export function isAutoLanguageModel(model: ILanguageModelChatMetadataAndIdentifier | undefined): boolean {
813 return model?.metadata.id === 'auto' || model?.identifier === AUTO_MODEL_IDENTIFIER;
814 }
816 > const CHAT_PARTICIPANT_NAME_REGISTRY_STORAGE_KEY = 'chat.participantNameRegistry';
817 > const CHAT_MODELS_CONTROL_STORAGE_KEY = 'chat.modelsControl';
818 >
819 > interface IChatControlResponse {
820 > readonly version: number;
821 > readonly restrictedChatParticipants: { [name: string]: string[] };
822 > readonly models?: {
823 > readonly free?: Record<string, { readonly label: string; readonly featured?: boolean }>;
824 > readonly paid?: Record<string, { readonly label: string; readonly featured?: boolean; readonly minVSCodeVersion?: string }>;
825 > };
826 > }
827 >
828 > /**
829 > * Builds the per-model configuration submenu actions from a model's
830 > * {@link ILanguageModelConfigurationSchema}. The current value is read from
831 > * `currentConfig` and selections are routed through `setValue`, allowing the
832 > * caller to decide whether changes apply globally or to a per-editor override.
833 > */
834 > export function createModelConfigurationActions(
835 > schema: ILanguageModelConfigurationSchema | undefined, languageModels.ts ×1
836 > currentConfig: IStringDictionary<unknown>,
837 > setValue: (key: string, value: unknown) => void,
838 > ): IAction[] {
839 > if (!schema?.properties) {
840 > return []; languageModels.ts ×1
841 > }
843 > const actions: IAction[] = [];
844 >
845 > for (const [key, propSchema] of Object.entries(schema.properties)) {
846 > if (!propSchema.enum || !Array.isArray(propSchema.enum) || propSchema.enum.length < 1) {
847 > continue;
848 > }
849 > const currentValue = currentConfig[key] ?? propSchema.default;
850 > const label = (typeof propSchema.title === 'string' ? propSchema.title : undefined)
851 > ?? key.replace(/([a-z])([A-Z])/g, '$1 $2')
852 > .replace(/^./, s => s.toUpperCase());
853 > const defaultValue = propSchema.default;
854 > const enumItemLabels = propSchema.enumItemLabels;
855 > const enumDescriptions = propSchema.enumDescriptions;
856 > const enumActions: IAction[] = propSchema.enum.map((value: unknown, index: number) => {
857 > const itemLabel = enumItemLabels?.[index] ?? String(value);
858 > const displayLabel = value === defaultValue ? localize('models.enumDefault', "{0} (default)", itemLabel) : itemLabel;
859 > const tooltip = enumDescriptions?.[index] ?? '';
860 > return {
861 > id: `configureModel.${key}.${value}`,
862 > label: displayLabel,
863 > class: undefined,
864 > enabled: true,
865 > tooltip,
866 > checked: currentValue === value,
867 > run: () => setValue(key, value)
868 > };
869 > });
870 > actions.push(new SubmenuAction(`configureModel.${key}`, label, enumActions));
871 > }
872 >
873 > return actions;
874 > }
876 > export class LanguageModelsService implements ILanguageModelsService {
877 >
878 > private static SECRET_KEY_PREFIX = 'chat.lm.secret.';
879 > private static SECRET_INPUT = '${input:{0}}';
880 >
881 > readonly _serviceBrand: undefined;
882 >
883 > private readonly _store = new DisposableStore();
884 >
885 > private readonly _providers = new Map<string, ILanguageModelChatProvider>();
886 > private readonly _vendors = new Map<string, ILanguageModelProviderDescriptor>();
887 >
888 > /** Vendors for which a deprecation notice has already been shown this session. */
889 > private readonly _deprecationNoticeShownVendors = new Set<string>();
890 >
891 > private readonly _onDidChangeLanguageModelVendors = this._store.add(new Emitter<string[]>());
892 > readonly onDidChangeLanguageModelVendors = this._onDidChangeLanguageModelVendors.event;
893 >
894 > private readonly _modelsGroups = new Map<string, ILanguageModelsGroup[]>();
895 > private readonly _modelCache = new Map<string, ILanguageModelChatMetadata>();
896 > private readonly _resolveLMSequencer = new SequencerByKey<string>();
897 > private readonly _modelConfigurations = new Map<string, IStringDictionary<unknown>>();
898 > private readonly _hasUserSelectableModels: IContextKey<boolean>;
899 > private readonly _hasNonCopilotUserSelectableModels: IContextKey<boolean>;
900 >
901 > private readonly _onLanguageModelChange = this._store.add(new Emitter<string>());
902 > readonly onDidChangeLanguageModels: Event<string> = this._onLanguageModelChange.event;
903 >
904 > private _recentlyUsedModelIds: string[] = [];
905 > private _pinnedModelIds: string[] = [];
906 >
907 > private _hiddenModelIds = new Set<string>();
908 >
909 > private readonly _onDidChangeModelsControlManifest = this._store.add(new Emitter<IModelsControlManifest>());
910 > readonly onDidChangeModelsControlManifest = this._onDidChangeModelsControlManifest.event;
911 >
912 > private readonly _onDidChangePinnedModels = this._store.add(new Emitter<void>());
913 > readonly onDidChangePinnedModels = this._onDidChangePinnedModels.event;
914 >
915 > private readonly _onDidChangeModelVisibility = this._store.add(new Emitter<void>());
916 > readonly onDidChangeModelVisibility = this._onDidChangeModelVisibility.event;
917 >
918 > private _modelsControlManifest: IModelsControlManifest = { free: {}, paid: {} };
919 > private _modelsControlRawResponse: IChatControlResponse['models'] | undefined;
920 >
921 > private _chatControlUrl: string | undefined;
922 > private _chatControlDisposed = false;
923 >
924 > private readonly _restrictedChatParticipants = observableValue<{ [name: string]: string[] }>(this, Object.create(null));
925 > readonly restrictedChatParticipants: IObservable<{ [name: string]: string[] }> = this._restrictedChatParticipants;
926 >
927 > constructor(
928 > @IExtensionService private readonly _extensionService: IExtensionService, languageModels.ts ×19
929 > @ILogService private readonly _logService: ILogService,
930 > @IStorageService private readonly _storageService: IStorageService,
931 > @IContextKeyService private readonly _contextKeyService: IContextKeyService,
932 > @ILanguageModelsConfigurationService private readonly _languageModelsConfigurationService: ILanguageModelsConfigurationService,
933 > @IQuickInputService private readonly _quickInputService: IQuickInputService,
934 > @ISecretStorageService private readonly _secretStorageService: ISecretStorageService,
935 > @IProductService private readonly _productService: IProductService,
936 > @IRequestService private readonly _requestService: IRequestService,
937 > @INotificationService private readonly _notificationService: INotificationService,
938 > @IOpenerService private readonly _openerService: IOpenerService,
939 > @ITelemetryService private readonly _telemetryService: ITelemetryService,
940 > ) {
941 > this._hasUserSelectableModels = ChatContextKeys.languageModelsAreUserSelectable.bindTo(_contextKeyService);
942 > this._hasNonCopilotUserSelectableModels = ChatContextKeys.nonCopilotLanguageModelsAreUserSelectable.bindTo(_contextKeyService);
943 > this._recentlyUsedModelIds = this._readRecentlyUsedModels();
944 > this._pinnedModelIds = this._readPinnedModels();
945 > this._readVisibility();
946 > this._initChatControlData();
947 >
948 > this._store.add(this.onDidChangeLanguageModels(() => {
949 > let hasUserSelectable = false; languageModels.ts ×5
950 > let hasNonCopilotUserSelectable = false;
951 > for (const model of this._modelCache.values()) {
952 > if (model.isUserSelectable === false) { languageModels.ts ×6
953 > continue; languageModels.ts ×2
954 > }
955 > hasUserSelectable = true; languageModels.ts ×6
956 > if (model.vendor !== COPILOT_VENDOR_ID) {
957 > hasNonCopilotUserSelectable = true; languageModels.ts ×1
958 > break;
959 > }
961 > this._hasUserSelectableModels.set(hasUserSelectable); languageModels.ts ×5
962 > this._hasNonCopilotUserSelectableModels.set(hasNonCopilotUserSelectable);
963 > this._refreshModelsControlManifest();
965 > this._store.add(this._languageModelsConfigurationService.onDidChangeLanguageModelGroups(changedGroups => this._onDidChangeLanguageModelGroups(changedGroups)));
966 >
967 > this._store.add(languageModelChatProviderExtensionPoint.setHandler((extensions, { added, removed }) => {
968 const addedVendors: IUserFriendlyLanguageModel[] = [];
969 const removedVendors: IUserFriendlyLanguageModel[] = [];
970
971 for (const extension of added) {
972 for (const item of Iterable.wrap(extension.value)) {
973 if (this._vendors.has(item.vendor)) {
974 extension.collector.error(localize('vscode.extension.contributes.languageModels.vendorAlreadyRegistered', "The vendor '{0}' is already registered and cannot be registered twice", item.vendor));
975 continue;
976 }
977 if (isFalsyOrWhitespace(item.vendor)) {
978 extension.collector.error(localize('vscode.extension.contributes.languageModels.emptyVendor', "The vendor field cannot be empty."));
979 continue;
980 }
981 if (item.vendor.trim() !== item.vendor) {
982 extension.collector.error(localize('vscode.extension.contributes.languageModels.whitespaceVendor', "The vendor field cannot start or end with whitespace."));
983 continue;
984 }
985 addedVendors.push(item);
986 }
987 }
988
989 for (const extension of removed) {
990 for (const item of Iterable.wrap(extension.value)) {
991 removedVendors.push(item);
992 }
993 }
994
995 this.deltaLanguageModelChatProviderDescriptors(addedVendors, removedVendors);
997 > }
999 > deltaLanguageModelChatProviderDescriptors(added: IUserFriendlyLanguageModel[], removed: IUserFriendlyLanguageModel[]): void {
1000 > const addedVendorIds: string[] = []; languageModels.ts ×19
1001 > const removedVendorIds: string[] = [];
1002 >
1003 > for (const item of added) {
1004 > if (this._vendors.has(item.vendor)) {
1005 > this._logService.error(`The vendor '${item.vendor}' is already registered and cannot be registered twice`); languageModels.ts ×1
1006 > continue;
1007 > }
1008 > if (isFalsyOrWhitespace(item.vendor)) { languageModels.ts ×19
1009 this._logService.error('The vendor field cannot be empty.');
1010 continue;
1011 }
1012 > if (item.vendor.trim() !== item.vendor) { languageModels.ts ×19
1013 this._logService.error('The vendor field cannot start or end with whitespace.');
1014 continue;
1015 }
1016 > const vendor: ILanguageModelProviderDescriptor = { languageModels.ts ×19
1017 > vendor: item.vendor,
1018 > displayName: item.displayName,
1019 > configuration: item.configuration,
1020 > managementCommand: item.managementCommand,
1021 > deprecation: item.deprecation,
1022 > when: item.when,
1023 > isDefault: item.vendor === COPILOT_VENDOR_ID
1024 > };
1025 > this._vendors.set(item.vendor, vendor);
1026 > addedVendorIds.push(item.vendor);
1027 > // Have some models we want from this vendor, so activate the extension
1028 > }
1029 >
1030 > for (const item of removed) {
1031 > this._vendors.delete(item.vendor); languageModels.ts ×2
1032 > this._providers.delete(item.vendor);
1033 > this._clearModelCache(item.vendor);
1034 > this._modelsGroups.delete(item.vendor);
1035 > removedVendorIds.push(item.vendor);
1036 > }
1038 > for (const [vendor, _] of this._providers) {
1039 > if (!this._vendors.has(vendor)) { languageModels.ts ×2
1040 this._providers.delete(vendor);
1041 }
1044 > if (addedVendorIds.length > 0 || removedVendorIds.length > 0) {
1045 > this._onDidChangeLanguageModelVendors.fire([...addedVendorIds, ...removedVendorIds]);
1046 > if (removedVendorIds.length > 0) {
1047 > for (const vendor of removedVendorIds) { languageModels.ts ×2
1048 > this._onLanguageModelChange.fire(vendor);
1049 > }
1050 > }
1052 > }
1054 > private async _onDidChangeLanguageModelGroups(changedGroups: readonly ILanguageModelsProviderGroup[]): Promise<void> {
1055 const changedVendors = new Set(changedGroups.map(g => g.vendor));
1056 await Promise.all(Array.from(changedVendors).map(vendor => this._resolveAllLanguageModels(vendor, true)));
1057 }
1059 > getVendors(): ILanguageModelProviderDescriptor[] {
1060 > return Array.from(this._vendors.values()) languageModels.ts ×2
1061 > .filter(vendor => {
1062 > if (!vendor.when) {
1063 > return true; // No when clause means always visible
1064 > }
1065 > const whenClause = ContextKeyExpr.deserialize(vendor.when); languageModels.ts ×1
1066 > return whenClause ? this._contextKeyService.contextMatchesRules(whenClause) : false; languageModels.ts ×2
1067 > });
1068 > }
1070 > getLanguageModelIds(): string[] {
1071 return Array.from(this._modelCache.keys());
1072 }
1074 > lookupLanguageModel(modelIdentifier: string): ILanguageModelChatMetadata | undefined {
1075 > return this._modelCache.get(modelIdentifier); languageModels.ts ×1
1076 > }
1078 > lookupLanguageModelByQualifiedName(referenceName: string): ILanguageModelChatMetadataAndIdentifier | undefined {
1079 for (const [identifier, model] of this._modelCache.entries()) {
1080 if (ILanguageModelChatMetadata.matchesQualifiedName(referenceName, model)) {
1081 return { metadata: model, identifier };
1082 }
1083 }
1084 return undefined;
1085 }
1087 > private async _resolveAllLanguageModels(vendorId: string, silent: boolean): Promise<void> {
1089 > const vendor = this._vendors.get(vendorId);
1090 >
1091 > if (!vendor) {
1092 return;
1093 }
1095 > // If a provider is already registered (e.g. a renderer-side provider
1096 > // such as the agent host), skip the activation wait — there's nothing
1097 > // more for an extension to contribute, and waiting would block on
1098 > // extension host startup unnecessarily.
1099 > let provider = this._providers.get(vendorId);
1100 > if (!provider) {
1101 > // Activate extensions before requesting to resolve the models languageModels.ts ×2
1102 > await this._extensionService.activateByEvent(`onLanguageModelChatProvider:${vendorId}`);
1103 > provider = this._providers.get(vendorId);
1104 > }
1105 > if (!provider) { languageModels.ts ×13
1106 > this._logService.warn(`[LM] No provider registered for vendor ${vendorId}`); languageModels.ts ×2
1107 > return;
1108 > }
1110 > return this._resolveLMSequencer.queue(vendorId, async () => {
1111 >
1112 > const allModels: ILanguageModelChatMetadataAndIdentifier[] = [];
1113 > const languageModelsGroups: ILanguageModelsGroup[] = [];
1114 >
1115 > try {
1116 > const models = await provider.provideLanguageModelChatInfo({ silent }, CancellationToken.None);
1117 > if (models.length) {
1118 > allModels.push(...models); languageModels.ts ×3
1119 > const modelIdentifiers = [];
1120 > for (const m of models) {
1121 > if (vendor.isDefault) {
1122 > // Special case for copilot models - they are all user selectable unless marked otherwise languageModels.ts ×1
1123 > if (m.metadata.isUserSelectable !== false) {
1124 > modelIdentifiers.push(m.identifier);
1125 > } else {
1126 > this._logService.trace(`[LM] Skipping model ${m.identifier} from model picker as it is not user selectable.`); languageModels.ts ×2
1127 > }
1128 > } else { languageModels.ts ×3
1129 > modelIdentifiers.push(m.identifier); languageModels.ts ×1
1130 > }
1132 > languageModelsGroups.push({ modelIdentifiers });
1133 > }
1134 > } catch (error) { languageModels.ts ×13
1135 languageModelsGroups.push({
1136 modelIdentifiers: [],
1137 status: {
1138 message: getErrorMessage(error),
1139 severity: Severity.Error
1140 }
1141 });
1142 }
1144 > const groups = this._languageModelsConfigurationService.getLanguageModelsProviderGroups();
1145 > const perModelConfigurations = new Map<string, IStringDictionary<unknown>>();
1146 > for (const group of groups) {
1147 > if (group.vendor !== vendorId) { languageModels.ts ×6
1148 continue;
1149 }
1151 > // For vendors without a configuration schema whose models were already
1152 > // resolved in the initial (groupless) load, groups only carry per-model
1153 > // settings and should not trigger a separate model resolution call.
1154 > // Instead, apply the per-model config to the already-resolved models.
1155 > if (!vendor.configuration && allModels.length > 0) {
1156 if (group.settings) {
1157 for (const model of allModels) {
1158 const modelConfig = group.settings[model.metadata.id];
1159 if (modelConfig) {
1160 // Store raw config (without resolving secrets) to avoid leaking secrets on persist
1161 perModelConfigurations.set(model.identifier, { ...modelConfig });
1162 }
1163 }
1164 }
1165 languageModelsGroups.push({ group, modelIdentifiers: [] });
1166 continue;
1167 }
1169 > const configuration = await this._resolveConfiguration(group, vendor.configuration);
1170 >
1171 > try {
1172 > const models = await provider.provideLanguageModelChatInfo({ group: group.name, silent, configuration }, CancellationToken.None);
1173 > if (models.length) {
1174 > // Provide a sensible default for `metadata.detail` so that
1175 > // multiple instances of the same vendor (e.g. multiple
1176 > // Ollama servers) are distinguishable in the model picker.
1177 > // Providers that supply their own `detail` keep it; when
1178 > // the provider does not set one, fall back to the user-
1179 > // configured group name.
1180 > for (let i = 0; i < models.length; i++) {
1181 > if (!models[i].metadata.detail) {
1182 > models[i] = { ...models[i], metadata: { ...models[i].metadata, detail: group.name } }; languageModels.ts ×1
1183 > }
1185 > allModels.push(...models);
1186 > languageModelsGroups.push({ group, modelIdentifiers: models.map(m => m.identifier) });
1187 > }
1188 >
1189 > // Collect per-model configurations from the group
1190 > if (group.settings) {
1191 > for (const model of models) { languageModels.ts ×2
1192 > const modelConfig = group.settings[model.metadata.id];
1193 > if (modelConfig) {
1194 > // Store raw config (without resolving secrets) to avoid leaking secrets on persist
1195 > perModelConfigurations.set(model.identifier, { ...modelConfig });
1196 > }
1197 > }
1198 > }
1199 > } catch (error) { languageModels.ts ×6
1200 languageModelsGroups.push({
1201 group,
1202 modelIdentifiers: [],
1203 status: {
1204 message: getErrorMessage(error),
1205 severity: Severity.Error
1206 }
1207 });
1208 }
1211 > const wasResolved = this._modelsGroups.has(vendorId);
1212 > const oldGroups = this._modelsGroups.get(vendorId) ?? [];
1213 > this._modelsGroups.set(vendorId, languageModelsGroups);
1214 > const oldModels = this._clearModelCache(vendorId);
1215 > let hasChanges = !wasResolved;
1216 > for (const model of allModels) {
1217 > if (this._modelCache.has(model.identifier)) { languageModels.ts ×6
1218 this._logService.warn(`[LM] Model ${model.identifier} is already registered. Skipping.`);
1219 continue;
1220 }
1221 > this._modelCache.set(model.identifier, model.metadata); languageModels.ts ×6
1222 > hasChanges = hasChanges || !equals(oldModels.get(model.identifier), model.metadata);
1223 > oldModels.delete(model.identifier);
1224 > }
1225 > this._logService.trace(`[LM] Resolved language models for vendor ${vendorId}`, allModels); languageModels.ts ×13
1226 > hasChanges = hasChanges || oldModels.size > 0;
1227 >
1228 > // Also detect group structure changes (added/removed groups, status changes)
1229 > // so the UI updates even when individual models haven't changed
1230 > if (!hasChanges) {
1231 > hasChanges = this._hasGroupStructureChanged(oldGroups, languageModelsGroups); languageModels.ts ×5
1232 > }
1234 > // Update per-model configurations for this vendor
1235 > this._clearModelConfigurations(vendorId);
1236 > for (const [identifier, config] of perModelConfigurations) {
1237 > if (this._modelCache.has(identifier)) { languageModels.ts ×2
1238 > this._modelConfigurations.set(identifier, config);
1239 > }
1240 > }
1242 > if (hasChanges) {
1243 > this._onLanguageModelChange.fire(vendorId);
1244 > } else {
1245 > this._logService.trace(`[LM] No changes in language models for vendor ${vendorId}`); languageModels.ts ×5
1246 > }
1248 > }
1250 > private _hasGroupStructureChanged(oldGroups: readonly ILanguageModelsGroup[], newGroups: readonly ILanguageModelsGroup[]): boolean {
1251 > if (oldGroups.length !== newGroups.length) { languageModels.ts ×5
1252 return true;
1253 }
1254 > for (let i = 0; i < oldGroups.length; i++) { languageModels.ts ×5
1255 > const oldGroup = oldGroups[i];
1256 > const newGroup = newGroups[i];
1257 > if (oldGroup.group?.name !== newGroup.group?.name
1258 > || oldGroup.group?.vendor !== newGroup.group?.vendor
1259 > || oldGroup.status?.message !== newGroup.status?.message
1260 > || oldGroup.status?.severity !== newGroup.status?.severity
1261 > || oldGroup.modelIdentifiers.length !== newGroup.modelIdentifiers.length) {
1262 return true;
1263 }
1265 > return false;
1266 > }
1268 > getLanguageModelGroups(vendor: string): ILanguageModelsGroup[] {
1269 return this._modelsGroups.get(vendor) ?? [];
1270 }
1272 > hasResolvedVendor(vendor: string): boolean {
1273 return this._modelsGroups.has(vendor);
1274 }
1276 > async selectLanguageModels(selector: ILanguageModelChatSelector): Promise<string[]> {
1278 > if (selector.vendor) {
1279 > await this._resolveAllLanguageModels(selector.vendor, true); languageModels.ts ×1
1280 > } else { languageModels.ts ×4
1281 > const allVendors = Array.from(this._vendors.keys()); languageModels.ts ×1
1282 > await Promise.all(allVendors.map(vendor => this._resolveAllLanguageModels(vendor, true)));
1283 > }
1285 > const result: string[] = [];
1286 >
1287 > for (const [internalModelIdentifier, model] of this._modelCache) {
1288 > if ((selector.vendor === undefined || model.vendor === selector.vendor) languageModels.ts ×1
1289 > && (selector.family === undefined || model.family === selector.family)
1290 > && (selector.version === undefined || model.version === selector.version)
1291 > && (selector.id === undefined || model.id === selector.id)) {
1292 > result.push(internalModelIdentifier);
1293 > }
1294 > }
1296 > this._logService.trace('[LM] selected language models', selector, result);
1297 >
1298 > return result;
1299 > }
1301 > registerLanguageModelProvider(vendor: string, provider: ILanguageModelChatProvider): IDisposable {
1302 > this._logService.trace('[LM] registering language model provider', vendor, provider); languageModels.ts ×4
1303 >
1304 > if (!this._vendors.has(vendor)) {
1305 throw new Error(`Chat model provider uses UNKNOWN vendor ${vendor}.`);
1306 }
1307 > if (this._providers.has(vendor)) { languageModels.ts ×4
1308 throw new Error(`Chat model provider for vendor ${vendor} is already registered.`);
1309 }
1311 > this._providers.set(vendor, provider);
1312 >
1313 > const modelChangeListener = provider.onDidChange(() => {
1314 > this._resolveAllLanguageModels(vendor, true); languageModels.ts ×1
1316 >
1317 > return toDisposable(() => {
1318 > this._logService.trace('[LM] UNregistered language model provider', vendor);
1319 > this._clearModelCache(vendor);
1320 > this._modelsGroups.delete(vendor);
1321 > this._providers.delete(vendor);
1322 > modelChangeListener.dispose();
1323 > });
1324 > }
1326 > async sendChatRequest(modelId: string, from: ExtensionIdentifier | undefined, messages: IChatMessage[], options: ILanguageModelChatRequestOptions, token: CancellationToken): Promise<ILanguageModelChatResponse> {
1327 > const metadata = this._modelCache.get(modelId); languageModels.ts ×6
1328 > const provider = this._providers.get(metadata?.vendor || '');
1329 > if (!provider) {
1330 throw new Error(`Chat provider for model ${modelId} is not registered.`);
1331 }
1332 > if (metadata) { languageModels.ts ×6
1333 > this._logProviderUsageTelemetry(metadata);
1334 > this._maybeShowProviderDeprecationNotice(metadata);
1335 > }
1336 > const configuration = this.getModelConfiguration(modelId);
1337 > const mergedOptions = configuration ? { ...options, configuration: { ...configuration, ...options.configuration } } : options;
1338 > return provider.sendChatRequest(modelId, messages, from, mergedOptions, token);
1339 > }
1341 > /**
1342 > * When a chat request is made against a deprecated provider (one that contributes a
1343 > * `deprecation.link`), prompt the user once per session to install the replacement
1344 > * extension. The notification can be dismissed, and offers a "Don't Show Again" choice that
1345 > * is persisted across sessions via the notification service's `neverShowAgain` support.
1346 > */
1347 > private _maybeShowProviderDeprecationNotice(metadata: ILanguageModelChatMetadata): void {
1348 > const vendor = this._vendors.get(metadata.vendor); languageModels.ts ×6
1349 > const link = vendor?.deprecation?.link;
1350 > if (!link) {
1351 > return; languageModels.ts ×1
1352 > }
1353 > if (this._deprecationNoticeShownVendors.has(metadata.vendor)) { languageModels.ts ×2
1354 > return; languageModels.ts ×1
1355 > }
1356 > this._deprecationNoticeShownVendors.add(metadata.vendor); languageModels.ts ×2
1357 >
1358 > const providerName = (vendor.displayName || metadata.vendor).replace(/\s*\(deprecated\)\s*$/i, ''); languageModels.ts ×6
1359 > this._notificationService.prompt(
1360 > Severity.Info,
1361 > localize('chat.providerDeprecation.message', "The internal {0} language model provider is being deprecated. Please migrate to the official extension.", providerName),
1362 > [{
1363 > label: localize('chat.providerDeprecation.install', "Install Extension"),
1364 > run: () => { this._openerService.open(resolveProviderDeprecationLink(link, this._productService.urlProtocol)); }
1365 > }],
1366 > {
1367 > neverShowAgain: { id: `chat.providerDeprecation.${metadata.vendor}`, scope: NeverShowAgainScope.APPLICATION }
1368 > }
1369 > );
1370 > }
1372 > /**
1373 > * Reports which in-built BYOK provider (or third-party extension) backs a model request. First-party
1374 > * Copilot models are intentionally not reported here (see {@link getByokProviderTelemetryName}).
1375 > */
1376 > private _logProviderUsageTelemetry(metadata: ILanguageModelChatMetadata | undefined): void {
1377 > const provider = getByokProviderTelemetryName(metadata?.vendor, metadata?.extension); languageModels.ts ×6
1378 > if (!provider) {
1379 > return; languageModels.ts ×1
1380 > }
1381 > type LanguageModelRequestEvent = { languageModels.ts ×1
1382 > provider: string;
1383 > isBYOK: boolean;
1384 > };
1385 > type LanguageModelRequestClassification = {
1386 > provider: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Normalized non-Copilot model provider: an in-built BYOK vendor id (for models contributed by the built-in Copilot extensions) or "3p-extension" for any third-party extension provider.' };
1387 > isBYOK: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the model is a BYOK model.' };
1388 > owner: 'vritant24';
1389 > comment: 'Tracks which non-Copilot language-model provider is used per request to understand adoption of in-built Copilot BYOK providers vs third-party extension providers.';
1390 > };
1391 > this._telemetryService.publicLog2<LanguageModelRequestEvent, LanguageModelRequestClassification>('chat.languageModelRequest', {
1392 > provider,
1393 > isBYOK: !!metadata?.isBYOK,
1395 > }
1397 > private _resolveModelConfigurationWithDefaults(modelId: string, metadata: ILanguageModelChatMetadata | undefined): IStringDictionary<unknown> | undefined {
1398 > const userConfig = this._modelConfigurations.get(modelId); languageModels.ts ×5
1399 > const schema = metadata?.configurationSchema;
1400 >
1401 > if (!schema?.properties && !userConfig) {
1402 > return undefined; languageModels.ts ×1
1403 > }
1405 > // Start with schema defaults
1406 > const defaults: IStringDictionary<unknown> = {};
1407 > if (schema?.properties) { languageModels.ts ×5
1408 > for (const [key, propSchema] of Object.entries(schema.properties)) { languageModels.ts ×1
1409 > if (propSchema.default !== undefined) {
1410 > defaults[key] = propSchema.default;
1411 > }
1412 > }
1413 > }
1415 > if (!userConfig && Object.keys(defaults).length === 0) { languageModels.ts ×5
1416 return undefined;
1417 }
1419 > // User config overrides defaults
1420 > return { ...defaults, ...userConfig };
1423 > computeTokenLength(modelId: string, message: string | IChatMessage, token: CancellationToken): Promise<number> {
1424 const model = this._modelCache.get(modelId);
1425 if (!model) {
1426 throw new Error(`Chat model ${modelId} could not be found.`);
1427 }
1428 const provider = this._providers.get(model.vendor);
1429 if (!provider) {
1430 throw new Error(`Chat provider for model ${modelId} is not registered.`);
1431 }
1432 return provider.provideTokenCount(modelId, message, token);
1433 }
1435 > getModelConfiguration(modelId: string): IStringDictionary<unknown> | undefined {
1436 > const metadata = this._modelCache.get(modelId); languageModels.ts ×5
1437 > return this._resolveModelConfigurationWithDefaults(modelId, metadata);
1438 > }
1440 > async setModelConfiguration(modelId: string, values: IStringDictionary<unknown>): Promise<void> {
1441 > const metadata = this._modelCache.get(modelId); languageModels.ts ×8
1442 > if (!metadata) {
1443 return;
1444 }
1446 > // Find the group from the configuration service (source of truth)
1447 > const allGroups = this._languageModelsConfigurationService.getLanguageModelsProviderGroups();
1448 > let group: ILanguageModelsProviderGroup | undefined;
1449 >
1450 > // First try to find a group that already has config for this model.
1451 > group = allGroups.find(g => g.vendor === metadata.vendor && g.settings?.[metadata.id] !== undefined);
1452 >
1453 > // Otherwise find the group that actually *defines* this model. Several
1454 > // groups can share the same `vendor` (e.g. multiple `customendpoint`
1455 > // providers like DeepSeek and MyCustom), so matching by vendor alone would
1456 > // write the config to the first group of that vendor — not the one the
1457 > // model belongs to. Resolve via the model→group map instead. See #322872.
1458 > if (!group) {
1459 > const vendorGroups = this._modelsGroups.get(metadata.vendor);
1460 > const containingGroup = vendorGroups?.find(vg => vg.modelIdentifiers.includes(modelId) && vg.group)?.group;
1461 > if (containingGroup) {
1462 > group = allGroups.find(g => g.vendor === containingGroup.vendor && g.name === containingGroup.name) ?? containingGroup;
1463 > }
1464 > }
1465 >
1466 > // As a last resort (model not yet resolved into any group), fall back to
1467 > // any group for this vendor.
1468 > if (!group) {
1469 group = allGroups.find(g => g.vendor === metadata.vendor);
1470 }
1472 > // Merge new values into existing config, removing properties set to their schema default
1473 > const existingConfig = this._modelConfigurations.get(modelId) ?? {};
1474 > const updatedConfig = { ...existingConfig, ...values };
1475 > const schema = metadata.configurationSchema;
1476 > if (schema?.properties) {
1477 > for (const [key, value] of Object.entries(updatedConfig)) {
1478 > const propSchema = schema.properties[key];
1479 > if (propSchema?.default !== undefined && propSchema.default === value) {
1480 delete updatedConfig[key];
1481 }
1483 > }
1484 >
1485 > if (group) {
1486 > const existingSettings = (group.settings as IStringDictionary<IStringDictionary<unknown>> | undefined) ?? {};
1487 > let updatedSettings: IStringDictionary<IStringDictionary<unknown>>;
1488 > if (Object.keys(updatedConfig).length === 0) {
1489 updatedSettings = { ...existingSettings };
1490 delete updatedSettings[metadata.id];
1491 > } else { languageModels.ts ×8
1492 > updatedSettings = { ...existingSettings, [metadata.id]: updatedConfig };
1493 > }
1494 > const updatedGroup: ILanguageModelsProviderGroup = {
1495 > ...group,
1496 > settings: Object.keys(updatedSettings).length > 0 ? updatedSettings : undefined
1497 > };
1498 > if (!updatedGroup.settings && Object.keys(updatedGroup).filter(k => k !== 'name' && k !== 'vendor' && k !== 'range' && k !== 'modelsRange' && k !== 'settings').length === 0) {
1499 // Remove the group entirely if it only had model config
1500 await this._languageModelsConfigurationService.removeLanguageModelsProviderGroup(group);
1501 > } else { languageModels.ts ×8
1502 > await this._languageModelsConfigurationService.updateLanguageModelsProviderGroup(group, updatedGroup);
1503 > }
1504 > } else if (Object.keys(updatedConfig).length > 0) {
1505 // Only create a new group if there's non-default config
1506 // Use _vendors directly instead of getVendors() which filters by `when` clause,
1507 // because we need to store config for all vendors regardless of UI visibility.
1508 const vendor = this._vendors.get(metadata.vendor);
1509 if (!vendor) {
1510 return;
1511 }
1512 const newGroup: ILanguageModelsProviderGroup = {
1513 name: vendor.displayName,
1514 vendor: metadata.vendor,
1515 settings: { [metadata.id]: updatedConfig }
1516 };
1517 await this._languageModelsConfigurationService.addLanguageModelsProviderGroup(newGroup);
1518 }
1520 > // Update the in-memory cache
1521 > if (Object.keys(updatedConfig).length > 0) {
1522 > this._modelConfigurations.set(modelId, updatedConfig);
1523 > } else {
1524 this._modelConfigurations.delete(modelId);
1525 }
1527 > // Notify listeners so UI (e.g., model picker label) updates
1528 > this._onLanguageModelChange.fire(metadata.vendor);
1529 > }
1531 > getModelConfigurationActions(modelId: string): IAction[] {
1532 const metadata = this._modelCache.get(modelId);
1533 const currentConfig = this._modelConfigurations.get(modelId) ?? {};
1534 return createModelConfigurationActions(
1535 metadata?.configurationSchema,
1536 currentConfig,
1537 (key, value) => this.setModelConfiguration(modelId, { [key]: value })
1538 );
1539 }
1541 > async configureLanguageModelsProviderGroup(vendorId: string, providerGroupName?: string): Promise<void> {
1542
1543 const vendor = this.getVendors().find(({ vendor }) => vendor === vendorId);
1544 if (!vendor) {
1545 throw new Error(`Vendor ${vendorId} not found.`);
1546 }
1547
1548 if (vendor.managementCommand) {
1549 await this._resolveAllLanguageModels(vendor.vendor, false);
1550 return;
1551 }
1552
1553 const languageModelProviderGroups = this._languageModelsConfigurationService.getLanguageModelsProviderGroups();
1554 const existing = languageModelProviderGroups.find(g => g.vendor === vendorId && g.name === providerGroupName);
1555
1556 const name = await this.promptForName(languageModelProviderGroups, vendor, existing);
1557 if (!name) {
1558 return;
1559 }
1560
1561 const existingConfiguration = existing ? await this._resolveConfiguration(existing, vendor.configuration) : undefined;
1562
1563 try {
1564 const configuration = vendor.configuration ? await this.promptForConfiguration(name, vendor.configuration, existingConfiguration) : undefined;
1565 if (vendor.configuration && !configuration) {
1566 return;
1567 }
1568
1569 const languageModelProviderGroup = await this._resolveLanguageModelProviderGroup(name, vendorId, configuration, vendor.configuration);
1570 const saved = existing
1571 ? await this._languageModelsConfigurationService.updateLanguageModelsProviderGroup(existing, languageModelProviderGroup)
1572 : await this._languageModelsConfigurationService.addLanguageModelsProviderGroup(languageModelProviderGroup);
1573
1574 if (vendor.configuration && this.requireConfiguring(vendor.configuration)) {
1575 const snippet = this.getSnippetForFirstUnconfiguredProperty(configuration ?? {}, vendor.configuration);
1576 await this._languageModelsConfigurationService.configureLanguageModels({ group: saved, snippet });
1577 }
1578 } catch (error) {
1579 if (isCancellationError(error)) {
1580 return;
1581 }
1582 throw error;
1583 }
1584 }
1586 > async renameLanguageModelsProviderGroup(vendorId: string, providerGroupName: string): Promise<void> {
1587 > const vendor = this.getVendors().find(({ vendor }) => vendor === vendorId); languageModels.ts ×8
1588 > if (!vendor) {
1589 throw new Error(`Vendor ${vendorId} not found.`);
1590 }
1592 > const languageModelProviderGroups = this._languageModelsConfigurationService.getLanguageModelsProviderGroups();
1593 > const existing = languageModelProviderGroups.find(group => group.vendor === vendorId && group.name === providerGroupName);
1594 > if (!existing) {
1595 throw new Error(`Language model provider group ${providerGroupName} for vendor ${vendorId} not found.`);
1596 }
1598 > const name = await this.promptForName(languageModelProviderGroups, vendor, existing);
1599 > if (!name || name === existing.name) {
1600 return;
1601 }
1603 > await this._languageModelsConfigurationService.updateLanguageModelsProviderGroup(existing, { ...existing, name });
1604 > }
1606 > async updateLanguageModelsProviderGroupApiKey(vendorId: string, providerGroupName: string): Promise<void> {
1607 > const vendor = this.getVendors().find(({ vendor }) => vendor === vendorId); languageModels.ts ×31
1608 > const schema = vendor?.configuration as IJSONSchema | undefined;
1609 > const apiKeySchema = schema?.properties?.apiKey;
1610 > if (!vendor || !schema || !apiKeySchema) {
1611 return;
1612 }
1614 > const existing = this._languageModelsConfigurationService.getLanguageModelsProviderGroups().find(group => group.vendor === vendorId && group.name === providerGroupName);
1615 > if (!existing) {
1616 throw new Error(`Language model provider group ${providerGroupName} for vendor ${vendorId} not found.`);
1617 }
1619 > try {
1620 > const existingConfiguration = await this._resolveConfiguration(existing, schema);
1621 > const apiKey = await this.promptForValue(existing.name, 'apiKey', apiKeySchema, !!schema.required?.includes('apiKey'), existingConfiguration);
1622 > if (apiKey === undefined || apiKey === existingConfiguration.apiKey) {
1624 > }
1626 > const configuration = { ...existingConfiguration, apiKey };
1627 > const updated = {
1628 > ...await this._resolveLanguageModelProviderGroup(existing.name, vendorId, configuration, schema),
1629 > settings: existing.settings
1630 > };
1631 > await this._languageModelsConfigurationService.updateLanguageModelsProviderGroup(existing, updated);
1632 > await this._deleteSecretsInConfiguration(existing, schema);
1633 > } catch (error) {
1634 if (isCancellationError(error)) {
1635 return;
1636 }
1637 throw error;
1638 }
1641 > async addLanguageModelsProviderGroupModel(vendorId: string, providerGroupName: string): Promise<void> {
1642 > const vendor = this.getVendors().find(({ vendor }) => vendor === vendorId); languageModels.ts ×8
1643 > const schema = vendor?.configuration as IJSONSchema | undefined;
1644 > const modelsSchema = schema?.properties?.models;
1645 > if (!vendor || !modelsSchema) {
1646 return;
1647 }
1649 > const group = this._languageModelsConfigurationService.getLanguageModelsProviderGroups().find(group => group.vendor === vendorId && group.name === providerGroupName);
1650 > if (!group) {
1651 throw new Error(`Language model provider group ${providerGroupName} for vendor ${vendorId} not found.`);
1652 }
1654 > const hasModels = Array.isArray(group.models);
1655 > const snippet = hasModels ? this.getSnippetForArrayItem(modelsSchema) : this.getSnippetForProperty('models', modelsSchema);
1656 > if (!snippet) {
1657 return;
1658 }
1660 > await this._languageModelsConfigurationService.configureLanguageModels({
1661 > group,
1662 > snippet,
1663 > snippetTarget: hasModels ? 'models' : 'group'
1664 > });
1665 > }
1667 > async openLanguageModelsProviderGroupSettings(vendorId: string, providerGroupName: string): Promise<void> {
1668 > const group = this._languageModelsConfigurationService.getLanguageModelsProviderGroups().find(group => group.vendor === vendorId && group.name === providerGroupName); languageModels.ts ×2
1669 > if (!group) {
1670 throw new Error(`Language model provider group ${providerGroupName} for vendor ${vendorId} not found.`);
1671 }
1673 > await this._languageModelsConfigurationService.configureLanguageModels({ group });
1674 > }
1676 > async configureModel(modelId: string): Promise<void> {
1677 const metadata = this._modelCache.get(modelId);
1678 if (!metadata || !metadata.configurationSchema) {
1679 return;
1680 }
1681
1682 // Find the group that contains this model
1683 const vendorGroups = this._modelsGroups.get(metadata.vendor);
1684 let group: ILanguageModelsProviderGroup | undefined;
1685 if (vendorGroups) {
1686 for (const vg of vendorGroups) {
1687 if (vg.modelIdentifiers.includes(modelId) && vg.group) {
1688 group = vg.group;
1689 break;
1690 }
1691 }
1692 }
1693
1694 // If the model doesn't belong to any configured group, create one
1695 if (!group) {
1696 const vendor = this.getVendors().find(v => v.vendor === metadata.vendor);
1697 if (!vendor) {
1698 return;
1699 }
1700 const groupName = vendor.displayName;
1701 const newGroup: ILanguageModelsProviderGroup = { name: groupName, vendor: metadata.vendor, settings: { [metadata.id]: {} } };
1702 group = await this._languageModelsConfigurationService.addLanguageModelsProviderGroup(newGroup);
1703 await this._resolveAllLanguageModels(metadata.vendor, true);
1704 }
1705
1706 // Generate a snippet for the model's configuration schema
1707 const snippet = this._getModelConfigurationSnippet(metadata.id, metadata.configurationSchema);
1708 await this._languageModelsConfigurationService.configureLanguageModels({ group, snippet });
1709 }
1711 > private _getModelConfigurationSnippet(modelId: string, schema: ILanguageModelConfigurationSchema): string {
1712 const properties: string[] = [];
1713 if (schema.properties) {
1714 for (const [key, propSchema] of Object.entries(schema.properties)) {
1715 if (propSchema.defaultSnippets?.[0]) {
1716 const snippet = propSchema.defaultSnippets[0];
1717 let bodyText = snippet.bodyText ?? JSON.stringify(snippet.body, null, '\t\t\t');
1718 bodyText = bodyText.replace(/"(\^[^"]*)"/g, (_, value) => value.substring(1));
1719 properties.push(`\t\t\t"${key}": ${bodyText}`);
1720 } else if (propSchema.default !== undefined) {
1721 properties.push(`\t\t\t"${key}": ${JSON.stringify(propSchema.default)}`);
1722 } else {
1723 properties.push(`\t\t\t"${key}": $\{${key}\}`);
1724 }
1725 }
1726 }
1727 const modelContent = properties.length > 0
1728 ? `{\n${properties.join(',\n')}\n\t\t}`
1729 : '{\n\t\t\t$0\n\t\t}';
1730 return `"settings": {\n\t\t"${modelId}": ${modelContent}\n\t}`;
1731 }
1733 > async addLanguageModelsProviderGroup(name: string, vendorId: string, configuration: IStringDictionary<unknown> | undefined): Promise<void> {
1734 const vendor = this.getVendors().find(({ vendor }) => vendor === vendorId);
1735 if (!vendor) {
1736 throw new Error(`Vendor ${vendorId} not found.`);
1737 }
1738
1739 const languageModelProviderGroup = await this._resolveLanguageModelProviderGroup(name, vendorId, configuration, vendor.configuration);
1740 await this._languageModelsConfigurationService.addLanguageModelsProviderGroup(languageModelProviderGroup);
1741 }
1743 > async removeLanguageModelsProviderGroup(vendorId: string, providerGroupName: string): Promise<void> {
1744 const vendor = this.getVendors().find(({ vendor }) => vendor === vendorId);
1745 if (!vendor) {
1746 throw new Error(`Vendor ${vendorId} not found.`);
1747 }
1748
1749 const languageModelProviderGroups = this._languageModelsConfigurationService.getLanguageModelsProviderGroups();
1750 const existing = languageModelProviderGroups.find(g => g.vendor === vendorId && g.name === providerGroupName);
1751
1752 if (!existing) {
1753 throw new Error(`Language model provider group ${providerGroupName} for vendor ${vendorId} not found.`);
1754 }
1755
1756 await this._deleteSecretsInConfiguration(existing, vendor.configuration);
1757 await this._languageModelsConfigurationService.removeLanguageModelsProviderGroup(existing);
1758 }
1760 > private requireConfiguring(schema: IJSONSchema): boolean {
1761 if (schema.additionalProperties) {
1762 return true;
1763 }
1764 if (!schema.properties) {
1765 return false;
1766 }
1767 for (const property of Object.keys(schema.properties)) {
1768 if (!this.canPromptForProperty(schema.properties[property])) {
1769 return true;
1770 }
1771 }
1772 return false;
1773 }
1775 > private getSnippetForFirstUnconfiguredProperty(configuration: IStringDictionary<unknown>, schema: IJSONSchema): string | undefined {
1776 if (!schema.properties) {
1777 return undefined;
1778 }
1779 for (const property of Object.keys(schema.properties)) {
1780 if (configuration[property] === undefined) {
1781 const propertySchema = schema.properties[property];
1782 const snippet = this.getSnippetForProperty(property, propertySchema);
1783 if (snippet) {
1784 return snippet;
1785 }
1786 }
1787 }
1788 return undefined;
1789 }
1791 > private getSnippetForProperty(property: string, propertySchema: IJSONSchema): string | undefined {
1792 > const bodyText = this.getDefaultSnippetBodyText(propertySchema); languageModels.ts ×2
1793 > return bodyText ? `"${property}": ${bodyText}` : undefined;
1794 > }
1796 > private getSnippetForArrayItem(propertySchema: IJSONSchema): string | undefined {
1797 > return this.getDefaultSnippetBodyText(propertySchema, true); languageModels.ts ×2
1798 > }
1800 > private getDefaultSnippetBodyText(propertySchema: IJSONSchema, arrayItem = false): string | undefined {
1801 > const snippet = propertySchema.defaultSnippets?.[0]; languageModels.ts ×8
1802 > if (!snippet) {
1803 return undefined;
1804 }
1806 > const bodyText = arrayItem
1807 > ? Array.isArray(snippet.body) && snippet.body.length > 0 ? JSON.stringify(snippet.body[0], null, '\t') : undefined languageModels.ts ×2
1808 > : snippet.bodyText ?? JSON.stringify(snippet.body, null, '\t'); languageModels.ts ×2
1809 > if (!bodyText) { languageModels.ts ×8
1810 return undefined;
1811 }
1813 > return bodyText.replace(/"(\^[^"]*)"/g, (_, value) => value.substring(1));
1814 > }
1816 > private async promptForName(languageModelProviderGroups: readonly ILanguageModelsProviderGroup[], vendor: IUserFriendlyLanguageModel, existing: ILanguageModelsProviderGroup | undefined): Promise<string | undefined> {
1817 > let providerGroupName = existing?.name; languageModels.ts ×8
1818 > if (!providerGroupName) {
1819 providerGroupName = vendor.displayName;
1820 let count = 1;
1821 while (languageModelProviderGroups.some(g => g.vendor === vendor.vendor && g.name === providerGroupName)) {
1822 count++;
1823 providerGroupName = `${vendor.displayName} ${count}`;
1824 }
1825 }
1827 > let result: string | undefined;
1828 > const disposables = new DisposableStore();
1829 > try {
1830 > await new Promise<void>(resolve => {
1831 > const inputBox = disposables.add(this._quickInputService.createInputBox());
1832 > inputBox.title = localize('configureLanguageModelGroup', "Group Name");
1833 > inputBox.placeholder = localize('languageModelGroupName', "Enter a name for the group");
1834 > inputBox.value = providerGroupName;
1835 > inputBox.ignoreFocusOut = true;
1836 >
1837 > disposables.add(inputBox.onDidChangeValue(value => {
1838 > if (!value) {
1839 inputBox.validationMessage = localize('enterName', "Please enter a name");
1840 inputBox.severity = Severity.Error;
1841 return;
1842 }
1843 > if (languageModelProviderGroups.some(group => group !== existing && group.vendor === vendor.vendor && group.name === value)) { languageModels.ts ×8
1844 inputBox.validationMessage = localize('nameExists', "A language models group with this name already exists");
1845 inputBox.severity = Severity.Error;
1846 return;
1847 }
1848 > inputBox.validationMessage = undefined; languageModels.ts ×8
1849 > inputBox.severity = Severity.Ignore;
1850 > }));
1851 > disposables.add(inputBox.onDidAccept(async () => {
1852 > result = inputBox.value;
1853 > inputBox.hide();
1854 > }));
1855 > disposables.add(inputBox.onDidHide(() => resolve()));
1856 > inputBox.show();
1857 > });
1858 > } finally {
1859 > disposables.dispose();
1860 > }
1861 > return result;
1862 > }
1864 > private async promptForConfiguration(groupName: string, configuration: IJSONSchema, existing: IStringDictionary<unknown> | undefined): Promise<IStringDictionary<unknown> | undefined> {
1865 if (!configuration.properties) {
1866 return;
1867 }
1868
1869 const result: IStringDictionary<unknown> = existing ? { ...existing } : {};
1870
1871 for (const property of Object.keys(configuration.properties)) {
1872 const propertySchema = configuration.properties[property];
1873 const required = !!configuration.required?.includes(property);
1874 const value = await this.promptForValue(groupName, property, propertySchema, required, existing);
1875 if (value !== undefined) {
1876 result[property] = value;
1877 }
1878 }
1879
1880 return result;
1881 }
1883 > private async promptForValue(groupName: string, property: string, propertySchema: IJSONSchema | undefined, required: boolean, existing: IStringDictionary<unknown> | undefined): Promise<unknown | undefined> {
1884 > if (!propertySchema) { languageModels.ts ×31
1885 return undefined;
1886 }
1888 > if (!this.canPromptForProperty(propertySchema)) {
1889 return undefined;
1890 }
1892 > if (propertySchema.type === 'array' && propertySchema.items && !Array.isArray(propertySchema.items) && propertySchema.items.enum) {
1893 const selectedItems = await this.promptForArray(groupName, property, propertySchema);
1894 if (selectedItems === undefined) {
1895 return undefined;
1896 }
1897 return selectedItems;
1898 }
1900 > if (propertySchema.type === 'string' && Array.isArray(propertySchema.enum) && propertySchema.enum.length > 0) {
1901 return this.promptForEnum(groupName, property, propertySchema, existing);
1902 }
1904 > const value = await this.promptForInput(groupName, property, propertySchema, required, existing);
1905 > if (value === undefined) {
1906 return undefined;
1907 }
1909 > return value;
1910 > }
1912 > private canPromptForProperty(propertySchema: IJSONSchema | undefined): boolean {
1913 > if (!propertySchema || typeof propertySchema === 'boolean') { languageModels.ts ×31
1914 return false;
1915 }
1917 > if (propertySchema.type === 'array' && propertySchema.items && !Array.isArray(propertySchema.items) && propertySchema.items.enum) {
1918 return true;
1919 }
1921 > if (propertySchema.type === 'string' || propertySchema.type === 'number' || propertySchema.type === 'integer' || propertySchema.type === 'boolean') {
1922 > return true;
1923 > }
1924
1925 return false;
1928 > private getDescriptionPlaintext(propertySchema: IJSONSchema): string | undefined {
1929 > if (propertySchema.description) { languageModels.ts ×31
1930 return propertySchema.description;
1931 }
1932 > const md = propertySchema.markdownDescription; languageModels.ts ×31
1933 > if (!md) {
1934 > return undefined;
1935 > }
1936 // Quick input renders plain text only. Strip the inline markdown features used by
1937 // our schemas (inline code, bold/italic, links) so users see readable help.
1938 return md
1939 .replace(/`([^`]+)`/g, '$1')
1940 .replace(/\*\*([^*]+)\*\*/g, '$1')
1941 .replace(/\*([^*]+)\*/g, '$1')
1942 .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
1945 > private async promptForArray(groupName: string, property: string, propertySchema: IJSONSchema): Promise<string[] | undefined> {
1946 if (!propertySchema.items || Array.isArray(propertySchema.items) || !propertySchema.items.enum) {
1947 return undefined;
1948 }
1949 const items = propertySchema.items.enum;
1950 const disposables = new DisposableStore();
1951 try {
1952 return await new Promise<string[] | undefined>(resolve => {
1953 const quickPick = disposables.add(this._quickInputService.createQuickPick());
1954 quickPick.title = `${groupName}: ${propertySchema.title ?? property}`;
1955 quickPick.items = items.map(item => ({ label: item }));
1956 quickPick.placeholder = this.getDescriptionPlaintext(propertySchema) ?? localize('selectValue', "Select value for {0}", property);
1957 quickPick.canSelectMany = true;
1958 quickPick.ignoreFocusOut = true;
1959
1960 disposables.add(quickPick.onDidAccept(() => {
1961 resolve(quickPick.selectedItems.map(item => item.label));
1962 quickPick.hide();
1963 }));
1964 disposables.add(quickPick.onDidHide(() => {
1965 resolve(undefined);
1966 }));
1967 quickPick.show();
1968 });
1969 } finally {
1970 disposables.dispose();
1971 }
1972 }
1974 > private async promptForEnum(groupName: string, property: string, propertySchema: IJSONSchema & { enumItemLabels?: string[] }, existing: IStringDictionary<unknown> | undefined): Promise<string | undefined> {
1975 const values = propertySchema.enum;
1976 if (!Array.isArray(values) || values.length === 0) {
1977 return undefined;
1978 }
1979 const enumDescriptions = propertySchema.enumDescriptions;
1980 const enumItemLabels = Array.isArray(propertySchema.enumItemLabels) ? propertySchema.enumItemLabels : undefined;
1981 const initial = existing?.[property] !== undefined ? String(existing[property]) : (propertySchema.default !== undefined ? String(propertySchema.default) : undefined);
1982 const items: IQuickPickItem[] = values.map((value, index) => ({
1983 label: enumItemLabels?.[index] ?? String(value),
1984 description: enumDescriptions?.[index],
1985 id: String(value)
1986 }));
1987 const disposables = new DisposableStore();
1988 try {
1989 return await new Promise<string | undefined>(resolve => {
1990 const quickPick = disposables.add(this._quickInputService.createQuickPick<IQuickPickItem>());
1991 quickPick.title = `${groupName}: ${propertySchema.title ?? property}`;
1992 quickPick.items = items;
1993 quickPick.placeholder = this.getDescriptionPlaintext(propertySchema) ?? localize('selectValue', "Select value for {0}", property);
1994 quickPick.ignoreFocusOut = true;
1995 if (initial !== undefined) {
1996 const match = items.find(item => item.id === initial);
1997 if (match) {
1998 quickPick.activeItems = [match];
1999 }
2000 }
2001
2002 disposables.add(quickPick.onDidAccept(() => {
2003 const selected = quickPick.selectedItems[0];
2004 resolve(selected?.id);
2005 quickPick.hide();
2006 }));
2007 disposables.add(quickPick.onDidHide(() => {
2008 resolve(undefined);
2009 }));
2010 quickPick.show();
2011 });
2012 } finally {
2013 disposables.dispose();
2014 }
2015 }
2017 > private async promptForInput(groupName: string, property: string, propertySchema: IJSONSchema, required: boolean, existing: IStringDictionary<unknown> | undefined): Promise<string | number | boolean | undefined> {
2018 > const disposables = new DisposableStore(); languageModels.ts ×31
2019 > try {
2020 > const validate = (value: string): string | undefined => {
2021 > if (!value && required) {
2022 return localize('valueRequired', "Value is required");
2023 }
2024 > return undefined; languageModels.ts ×31
2025 > };
2026 >
2027 > const value = await new Promise<string | undefined>((resolve, reject) => {
2028 > const inputBox = disposables.add(this._quickInputService.createInputBox());
2029 > inputBox.title = `${groupName}: ${propertySchema.title ?? property}`;
2030 > inputBox.placeholder = localize('enterValue', "Enter value for {0}", property);
2031 > inputBox.password = !!propertySchema.secret;
2032 > inputBox.ignoreFocusOut = true;
2033 > if (existing?.[property]) {
2034 > inputBox.value = String(existing?.[property]);
2035 > } else if (propertySchema.default) {
2036 inputBox.value = String(propertySchema.default);
2037 }
2038 > const promptText = this.getDescriptionPlaintext(propertySchema); languageModels.ts ×31
2039 > if (promptText) {
2040 inputBox.prompt = promptText;
2041 }
2043 > disposables.add(inputBox.onDidChangeValue(value => {
2044 > const message = validate(value);
2045 > if (message) {
2046 inputBox.validationMessage = message;
2047 inputBox.severity = Severity.Error;
2048 > } else { languageModels.ts ×31
2049 > inputBox.validationMessage = undefined;
2050 > inputBox.severity = Severity.Ignore;
2051 > }
2052 > }));
2053 >
2054 > disposables.add(inputBox.onDidAccept(() => {
2055 > const message = validate(inputBox.value);
2056 > if (message) {
2057 inputBox.validationMessage = message;
2058 inputBox.severity = Severity.Error;
2059 return;
2060 }
2061 > resolve(inputBox.value); languageModels.ts ×31
2062 > inputBox.hide();
2063 > }));
2064 >
2065 > disposables.add(inputBox.onDidHide((e) => {
2066 > if (e.reason === QuickInputHideReason.Gesture) {
2067 reject(new CancellationError());
2068 > } else { languageModels.ts ×31
2069 > resolve(undefined);
2070 > }
2071 > }));
2072 >
2073 > inputBox.show();
2074 > });
2075 >
2076 > if (!value) {
2077 return undefined; // User cancelled
2078 }
2080 > if (propertySchema.type === 'number' || propertySchema.type === 'integer') {
2081 return Number(value);
2082 > } else if (propertySchema.type === 'boolean') { languageModels.ts ×31
2083 return value === 'true';
2084 > } else { languageModels.ts ×31
2085 > return value;
2086 > }
2087 >
2088 > } finally {
2089 > disposables.dispose();
2090 > }
2091 > }
2093 > private encodeSecretKey(property: string): string {
2094 > return format(LanguageModelsService.SECRET_INPUT, property); languageModels.ts ×6
2095 > }
2097 > private decodeSecretKey(secretInput: unknown): string | undefined {
2098 > if (!isString(secretInput)) { languageModels.ts ×31
2099 return undefined;
2100 }
2101 > return secretInput.substring(secretInput.indexOf(':') + 1, secretInput.length - 1); languageModels.ts ×31
2102 > }
2104 > private _clearModelCache(vendor: string): Map<string, ILanguageModelChatMetadata> {
2105 > const removed = new Map<string, ILanguageModelChatMetadata>(); languageModels.ts ×2
2106 > for (const [id, model] of this._modelCache.entries()) {
2107 > if (model.vendor === vendor) { languageModels.ts ×6
2108 > removed.set(id, model);
2109 > this._modelCache.delete(id);
2110 > }
2111 > }
2112 > return removed; languageModels.ts ×2
2113 > }
2115 > private _clearModelConfigurations(vendor: string): void {
2116 > for (const [id] of this._modelConfigurations) { languageModels.ts ×13
2117 if (this._modelCache.get(id)?.vendor === vendor || id.startsWith(`${vendor}/`)) {
2118 this._modelConfigurations.delete(id);
2119 }
2120 }
2123 > private async _resolveConfiguration(group: ILanguageModelsProviderGroup, schema: IJSONSchema | undefined): Promise<IStringDictionary<unknown>> {
2124 > if (!schema) { languageModels.ts ×2
2125 > return {}; languageModels.ts ×1
2126 > }
2128 > const result: IStringDictionary<unknown> = {};
2129 > for (const key in group) {
2130 > if (key === 'vendor' || key === 'name' || key === 'range' || key === 'modelsRange' || key === 'settings') {
2131 > continue;
2132 > }
2133 > let value = group[key]; languageModels.ts ×31
2134 > if (schema.properties?.[key]?.secret) { languageModels.ts ×3
2135 > const secretKey = this.decodeSecretKey(value); languageModels.ts ×31
2136 > value = secretKey ? await this._secretStorageService.get(secretKey) : undefined;
2137 > }
2138 > result[key] = value;
2139 > }
2141 > return result;
2144 > private async _resolveLanguageModelProviderGroup(name: string, vendor: string, configuration: IStringDictionary<unknown> | undefined, schema: IJSONSchema | undefined): Promise<ILanguageModelsProviderGroup> {
2145 > if (!schema) { languageModels.ts ×6
2146 return { name, vendor };
2147 }
2149 > const result: IStringDictionary<unknown> = {};
2150 > for (const key in configuration) {
2151 > let value = configuration[key];
2152 > if (schema.properties?.[key]?.secret && isString(value)) {
2153 > const secretKey = `${LanguageModelsService.SECRET_KEY_PREFIX}${hash(generateUuid()).toString(16)}`;
2154 > await this._secretStorageService.set(secretKey, value);
2155 > value = this.encodeSecretKey(secretKey);
2156 > }
2157 > result[key] = value;
2158 > }
2159 >
2160 > return { name, vendor, ...result };
2161 > }
2163 > private async _deleteSecretsInConfiguration(group: ILanguageModelsProviderGroup, schema: IJSONSchema | undefined): Promise<void> {
2164 > if (!schema) { languageModels.ts ×6
2165 return;
2166 }
2168 > const { vendor, name, range, modelsRange, ...configuration } = group;
2169 > for (const key in configuration) {
2170 > const value = group[key];
2171 > if (schema.properties?.[key]?.secret) {
2172 > const secretKey = this.decodeSecretKey(value);
2173 > if (secretKey) {
2174 > await this._secretStorageService.delete(secretKey);
2175 > }
2176 > }
2177 > }
2178 > }
2180 > async migrateLanguageModelsProviderGroup(languageModelsProviderGroup: ILanguageModelsProviderGroup): Promise<void> {
2181 const { vendor, name, ...configuration } = languageModelsProviderGroup;
2182 if (!this._vendors.get(vendor)) {
2183 throw new Error(`Vendor ${vendor} not found.`);
2184 }
2185
2186 await this._extensionService.activateByEvent(`onLanguageModelChatProvider:${vendor}`);
2187 const provider = this._providers.get(vendor);
2188 if (!provider) {
2189 throw new Error(`Chat model provider for vendor ${vendor} is not registered.`);
2190 }
2191
2192 await provider.provideLanguageModelChatInfo({ group: name, silent: false, configuration }, CancellationToken.None);
2193
2194 await this.addLanguageModelsProviderGroup(name, vendor, configuration);
2195 }
2197 > //#region Recently used models
2198 >
2199 > private _readRecentlyUsedModels(): string[] {
2200 > return this._storageService.getObject<string[]>(CHAT_MODEL_RECENTLY_USED_STORAGE_KEY, StorageScope.PROFILE, []); languageModels.ts ×19
2201 > }
2203 > private _saveRecentlyUsedModels(): void {
2204 this._storageService.store(CHAT_MODEL_RECENTLY_USED_STORAGE_KEY, this._recentlyUsedModelIds, StorageScope.PROFILE, StorageTarget.USER);
2205 }
2207 > getRecentlyUsedModelIds(): string[] {
2208 // Filter to only include models that still exist in the cache
2209 return this._recentlyUsedModelIds
2210 .filter(id => this._modelCache.has(id) && id !== AUTO_MODEL_IDENTIFIER)
2211 .slice(0, 4);
2212 }
2214 > addToRecentlyUsedList(modelIdentifier: string): void {
2215 if (modelIdentifier === AUTO_MODEL_IDENTIFIER) {
2216 return;
2217 }
2218
2219 // Remove if already present (to move to front)
2220 const index = this._recentlyUsedModelIds.indexOf(modelIdentifier);
2221 if (index !== -1) {
2222 this._recentlyUsedModelIds.splice(index, 1);
2223 }
2224 // Add to front
2225 this._recentlyUsedModelIds.unshift(modelIdentifier);
2226 // Cap at a reasonable max to avoid unbounded growth
2227 if (this._recentlyUsedModelIds.length > 20) {
2228 this._recentlyUsedModelIds.length = 20;
2229 }
2230 this._saveRecentlyUsedModels();
2231 }
2233 > clearRecentlyUsedList(): void {
2234 this._recentlyUsedModelIds = [];
2235 this._saveRecentlyUsedModels();
2236 }
2238 > //#endregion
2239 >
2240 > //#region Pinned models
2241 >
2242 > private _readPinnedModels(): string[] {
2243 > return this._storageService.getObject<string[]>(CHAT_MODEL_PINNED_STORAGE_KEY, StorageScope.PROFILE, []); languageModels.ts ×19
2244 > }
2246 > private _savePinnedModels(): void {
2247 this._storageService.store(CHAT_MODEL_PINNED_STORAGE_KEY, this._pinnedModelIds, StorageScope.PROFILE, StorageTarget.USER);
2248 }
2250 > getPinnedModelIds(): string[] {
2251 return this._pinnedModelIds.filter(id => id !== AUTO_MODEL_IDENTIFIER && this._modelCache.has(id));
2252 }
2254 > pinModel(modelIdentifier: string): void {
2255 if (modelIdentifier === AUTO_MODEL_IDENTIFIER || this._pinnedModelIds.includes(modelIdentifier)) {
2256 return;
2257 }
2258 this._pinnedModelIds.push(modelIdentifier);
2259 this._savePinnedModels();
2260 this._onDidChangePinnedModels.fire();
2261 }
2263 > unpinModel(modelIdentifier: string): void {
2264 const index = this._pinnedModelIds.indexOf(modelIdentifier);
2265 if (index === -1) {
2266 return;
2267 }
2268 this._pinnedModelIds.splice(index, 1);
2269 this._savePinnedModels();
2270 this._onDidChangePinnedModels.fire();
2271 }
2273 > isModelPinned(modelIdentifier: string): boolean {
2274 return modelIdentifier !== AUTO_MODEL_IDENTIFIER && this._pinnedModelIds.includes(modelIdentifier);
2275 }
2277 > //#endregion
2278 >
2279 > //#region Model visibility
2280 >
2281 > private _getGroupNameForVendor(vendor: string): string {
2282 > return this._vendors.get(vendor)?.displayName ?? vendor; languageModels.ts ×5
2283 > }
2285 > private _getModelIdsInGroup(vendor: string, groupName: string): string[] {
2286 > const vendorGroups = this._modelsGroups.get(vendor); languageModels.ts ×5
2287 > if (!vendorGroups) {
2288 return [];
2289 }
2290 > const result: string[] = []; languageModels.ts ×5
2291 > const fallbackName = this._getGroupNameForVendor(vendor);
2292 > for (const g of vendorGroups) {
2293 > const name = g.group?.name ?? fallbackName;
2294 > if (name === groupName) {
2295 > for (const id of g.modelIdentifiers) {
2296 > // Exclude agent-host BYOK copies. They are not shown as rows in this
2297 > // group (they surface under their real provider), so group-level
2298 > // visibility toggles (`isGroupHidden` / `setGroupHidden`) must not
2299 > // touch them — otherwise hiding the agent-host group would flip the
2300 > // hidden state of these copies in the underlying model set even though
2301 > // the UI never lists them here. Their visibility is owned by the real
2302 > // provider row and honoured in the picker via the reconstructed id.
2303 > const metadata = this._modelCache.get(id);
2304 > if (metadata && ILanguageModelChatMetadata.getAgentHostByokManageModelsIdentifier(metadata) !== undefined) {
2305 > continue; languageModels.ts ×1
2306 > }
2307 > result.push(id); languageModels.ts ×5
2308 > }
2309 > }
2310 > }
2311 > return result;
2312 > }
2314 > private _readVisibility(): void {
2315 > const raw = this._storageService.getObject<{ hiddenModels?: string[] }>(CHAT_MODEL_VISIBILITY_STORAGE_KEY, StorageScope.PROFILE, {}); languageModels.ts ×19
2316 > this._hiddenModelIds = new Set(Array.isArray(raw?.hiddenModels) ? raw.hiddenModels : []);
2317 > }
2319 > private _saveVisibility(): void {
2320 > this._storageService.store( languageModels.ts ×1
2321 > CHAT_MODEL_VISIBILITY_STORAGE_KEY,
2322 > { hiddenModels: Array.from(this._hiddenModelIds) },
2323 > StorageScope.PROFILE,
2324 > StorageTarget.USER,
2325 > );
2326 > }
2328 > isGroupHidden(vendor: string, groupName: string): boolean {
2329 > const modelIds = this._getModelIdsInGroup(vendor, groupName); languageModels.ts ×1
2330 > return modelIds.length > 0 && modelIds.every(id => this._hiddenModelIds.has(id));
2331 > }
2333 > isModelHidden(modelIdentifier: string): boolean {
2334 > return this._hiddenModelIds.has(modelIdentifier); languageModels.ts ×2
2335 > }
2337 > setGroupHidden(vendor: string, groupName: string, hidden: boolean): void {
2338 > let changed = false; languageModels.ts ×2
2339 > const modelIds = this._getModelIdsInGroup(vendor, groupName);
2340 > for (const id of modelIds) {
2341 > if (hidden) {
2342 > if (!this._hiddenModelIds.has(id)) {
2343 > this._hiddenModelIds.add(id);
2344 > changed = true;
2345 > }
2346 > } else if (this._hiddenModelIds.delete(id)) {
2347 > changed = true; languageModels.ts ×1
2348 > }
2350 > if (changed) {
2351 > this._saveVisibility();
2352 > this._onDidChangeModelVisibility.fire();
2353 > }
2354 > }
2356 > setModelHidden(modelIdentifier: string, hidden: boolean): void {
2357 > let changed = false; languageModels.ts ×4
2358 > if (hidden) {
2359 > if (!this._hiddenModelIds.has(modelIdentifier)) { languageModels.ts ×1
2360 > this._hiddenModelIds.add(modelIdentifier); languageModels.ts ×1
2361 > changed = true;
2362 > }
2363 > } else if (this._hiddenModelIds.delete(modelIdentifier)) { languageModels.ts ×4
2364 > changed = true; languageModels.ts ×1
2365 > }
2366 > if (changed) { languageModels.ts ×4
2367 > this._saveVisibility(); languageModels.ts ×1
2368 > this._onDidChangeModelVisibility.fire();
2369 > }
2372 > getHiddenModelIds(): string[] {
2373 > return Array.from(this._hiddenModelIds); languageModels.ts ×2
2374 > }
2376 > //#endregion
2377 >
2378 > //#region Models control manifest
2379 >
2380 > getModelsControlManifest(): IModelsControlManifest {
2381 return this._modelsControlManifest;
2382 }
2384 > private _setModelsControlManifest(response: IChatControlResponse['models']): void {
2385 this._modelsControlRawResponse = response;
2386 this._refreshModelsControlManifest();
2387 }
2389 > private _refreshModelsControlManifest(): void {
2390 > const response = this._modelsControlRawResponse; languageModels.ts ×5
2391 > const free: IStringDictionary<IModelControlEntry> = {};
2392 > const paid: IStringDictionary<IModelControlEntry> = {};
2393 >
2394 > if (response?.free) {
2395 const freeEntries = Array.isArray(response.free) ? response.free : Object.values(response.free);
2396 for (const entry of freeEntries) {
2397 if (!entry || !isObject(entry)) {
2398 continue;
2399 }
2400 free[entry.id] = { label: entry.label, featured: entry.featured, exists: this._modelCache.has(`copilot/${entry.id}`) };
2401 }
2402 }
2404 > if (response?.paid) {
2405 const paidEntries = Array.isArray(response.paid) ? response.paid : Object.values(response.paid);
2406 for (const entry of paidEntries) {
2407 if (!entry || !isObject(entry)) {
2408 continue;
2409 }
2410 paid[entry.id] = { label: entry.label, featured: entry.featured, minVSCodeVersion: entry.minVSCodeVersion, exists: this._modelCache.has(`copilot/${entry.id}`) };
2411 }
2412 }
2414 > this._modelsControlManifest = { free, paid };
2415 > this._onDidChangeModelsControlManifest.fire(this._modelsControlManifest);
2416 > }
2418 > //#region Chat control data
2419 > private _initChatControlData(): void {
2420 > this._chatControlUrl = this._productService.chatParticipantRegistry; languageModels.ts ×19
2421 > if (!this._chatControlUrl) {
2422 > return;
2423 > }
2424
2425 // Restore participant registry from storage
2426 const raw = this._storageService.get(CHAT_PARTICIPANT_NAME_REGISTRY_STORAGE_KEY, StorageScope.APPLICATION);
2427 try {
2428 > this._restrictedChatParticipants.set(JSON.parse(raw ?? '{}'), undefined); languageModels.ts ×19
2429 > } catch (err) {
2430 this._storageService.remove(CHAT_PARTICIPANT_NAME_REGISTRY_STORAGE_KEY, StorageScope.APPLICATION);
2431 }
2432
2433 // Restore models control manifest from storage
2434 const rawModels = this._storageService.get(CHAT_MODELS_CONTROL_STORAGE_KEY, StorageScope.APPLICATION);
2435 try {
2436 > const models = JSON.parse(rawModels ?? '{}'); languageModels.ts ×19
2437 > if (isObject(models)) {
2438 this._setModelsControlManifest(models);
2439 }
2440 > } catch (err) { languageModels.ts ×19
2441 this._storageService.remove(CHAT_MODELS_CONTROL_STORAGE_KEY, StorageScope.APPLICATION);
2442 }
2443
2444 this._refreshChatControlData();
2447 > private _refreshChatControlData(): void {
2448 if (this._chatControlDisposed) {
2449 return;
2450 }
2451
2452 this._fetchChatControlData()
2453 .catch(err => this._logService.warn('Failed to fetch chat control data', err))
2454 .then(() => timeout(5 * 60 * 1000)) // every 5 minutes
2455 .then(() => this._refreshChatControlData());
2456 }
2458 > private async _fetchChatControlData(): Promise<void> {
2459 this._logService.trace('[LM] Fetching chat control data from', this._chatControlUrl);
2460
2461 let context;
2462 try {
2463 context = await this._requestService.request({ type: 'GET', url: this._chatControlUrl!, callSite: 'languageModels.fetchChatControlData' }, CancellationToken.None);
2464 } catch (err) {
2465 this._logService.warn('[LM] Failed to request chat control data', getErrorMessage(err));
2466 return;
2467 }
2468
2469 if (context.res.statusCode !== 200) {
2470 this._logService.warn(`[LM] Chat control data request failed with status ${context.res.statusCode}`);
2471 return;
2472 }
2473
2474 let result: IChatControlResponse | null;
2475 try {
2476 result = await asJson<IChatControlResponse>(context);
2477 } catch (err) {
2478 this._logService.warn('[LM] Failed to parse chat control response', getErrorMessage(err));
2479 return;
2480 }
2481
2482 this._logService.trace('[LM] Received chat control response', result ? Object.keys(result) : 'null');
2483
2484 if (!result || result.version !== 1) {
2485 this._logService.warn('[LM] Unexpected chat control response version', result?.version);
2486 return;
2487 }
2488
2489 // Update restricted chat participants
2490 const registry = result.restrictedChatParticipants;
2491 this._restrictedChatParticipants.set(registry, undefined);
2492 this._storageService.store(CHAT_PARTICIPANT_NAME_REGISTRY_STORAGE_KEY, JSON.stringify(registry), StorageScope.APPLICATION, StorageTarget.MACHINE);
2493
2494 // Update models control manifest
2495 if (result.models) {
2496 this._logService.trace('[LM] Updating models control manifest', { freeCount: Object.keys(result.models.free ?? {}).length, paidCount: Object.keys(result.models.paid ?? {}).length });
2497 this._setModelsControlManifest(result.models);
2498 this._storageService.store(CHAT_MODELS_CONTROL_STORAGE_KEY, JSON.stringify(result.models), StorageScope.APPLICATION, StorageTarget.MACHINE);
2499 }
2500 }
2502 > //#endregion
2503 >
2504 > dispose() {
2505 > this._chatControlDisposed = true; languageModels.ts ×19
2506 > this._store.dispose();
2507 > this._providers.clear();
2508 > }
2510 > }